Sidebar and edit updates

This commit is contained in:
Darren Clarke 2023-10-16 09:20:40 +02:00
parent d73b194d1f
commit f13530f043
32 changed files with 3057 additions and 1114 deletions

View file

@ -0,0 +1,150 @@
import { FC, useState, useEffect } from "react";
import { usePathname, useRouter } from "next/navigation";
import useSWR from "swr";
import { Grid, Box, TextField, Autocomplete } from "@mui/material";
import { searchQuery } from "@/app/_graphql/searchQuery";
import { colors } from "@/app/_styles/theme";
type SearchResultProps = {
props: any;
option: any;
};
const SearchInput = (params: any) => (
<TextField
{...params}
placeholder="Search"
sx={{
backgroundColor: "white",
borderRadius: 10,
"& .MuiOutlinedInput-root": {
borderRadius: 10,
py: 0,
legend: {
marginLeft: "30px",
},
},
"& .MuiAutocomplete-inputRoot": {
paddingLeft: "20px !important",
borderRadius: 10,
},
"& .MuiInputLabel-outlined": {
paddingLeft: "20px",
},
"& .MuiInputLabel-shrink": {
marginLeft: "20px",
paddingLeft: "10px",
paddingRight: 0,
background: "white",
},
}}
/>
);
const SearchResult: FC<SearchResultProps> = ({ props, option }) => {
console.log({ option });
const { lightGrey, mediumGray, black, white } = colors;
return (
<Box
{...props}
sx={{
px: 2,
py: 1.25,
":hover": {
background: `${lightGrey}`,
},
a: {
color: `${black} !important`,
},
borderBottom: `1px solid ${mediumGray}`,
}}
>
<Grid container direction="column" spacing={0.1}>
<Grid item container direction="row" justifyContent="space-between">
<Grid item>
<Box
sx={{
py: 0,
fontSize: 13,
fontWeight: 500,
}}
>
{option.title}
</Box>
</Grid>
</Grid>
<Grid item>
<Box sx={{ width: "100%" }}>
<Box
sx={{
color: "#999",
fontSize: 13,
wrap: "break-word",
}}
>
{option.note}
</Box>
</Box>
</Grid>
</Grid>
</Box>
);
};
export const SearchBox: FC = () => {
const [open, setOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState(null);
const [searchTerms, setSearchTerms] = useState(null);
const pathname = usePathname();
const router = useRouter();
const { data, error }: any = useSWR({
document: searchQuery,
variables: {
search: searchTerms ?? "",
limit: 50,
},
refreshInterval: 10000,
});
useEffect(() => {
setOpen(false);
}, [pathname]);
return (
<Autocomplete
forcePopupIcon={false}
openOnFocus
blurOnSelect
value={selectedValue}
onBlur={() => setOpen(false)}
inputValue={searchTerms}
onChange={(_event, option, reason) => {
if (!option) return;
const url = `/tickets/${option.internalId}`;
setSelectedValue("");
router.push(url);
}}
onInputChange={(_event, value) => {
setSearchTerms(value);
}}
open={open}
onOpen={() => setOpen(true)}
noOptionsText="No results"
options={data?.search ?? []}
getOptionLabel={(option: any) => {
if (option) {
return option.title;
} else {
return "";
}
}}
renderOption={(props, option: any) => (
<SearchResult props={props} key={option.id} option={option} />
)}
sx={{ width: "100%" }}
renderInput={(params: any) => <SearchInput {...params} />}
/>
);
};

View file

@ -1,6 +1,6 @@
"use client";
import { FC, useState } from "react";
import { FC, useEffect, useState } from "react";
import useSWR from "swr";
import {
Box,
@ -26,6 +26,7 @@ import {
Assessment as AssessmentIcon,
LibraryBooks as LibraryBooksIcon,
School as SchoolIcon,
Search as SearchIcon,
} from "@mui/icons-material";
import { usePathname } from "next/navigation";
import Link from "next/link";
@ -33,8 +34,8 @@ import Image from "next/image";
import LinkLogo from "public/link-logo-small.png";
import { useSession, signOut } from "next-auth/react";
import { getTicketOverviewCountsQuery } from "app/_graphql/getTicketOverviewCountsQuery";
import { SearchBox } from "./SearchBox";
console.log;
const openWidth = 270;
const closedWidth = 100;
@ -174,13 +175,15 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
},
{ refreshInterval: 10000 },
);
console.log({ overviewData });
const findOverviewCountByID = (id: number) =>
overviewData?.ticketOverviews?.edges?.find((overview: any) =>
overview.node.id.endsWith(`/${id}`),
)?.node?.ticketCount ?? 0;
const recentCount = 0;
const assignedCount = findOverviewCountByID(1);
const openCount = findOverviewCountByID(5);
const urgentCount = findOverviewCountByID(7);
const pendingCount = findOverviewCountByID(3);
const unassignedCount = findOverviewCountByID(2);
const logout = () => {
@ -227,6 +230,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
direction="column"
justifyContent="space-between"
wrap="nowrap"
spacing={0}
sx={{ backgroundColor: "#25272A", height: "100%", p: 2 }}
>
<Grid item container>
@ -313,9 +317,17 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
</Grid>
<Grid item>
<Box
sx={{ height: "0.5px", width: "100%", backgroundColor: "#666" }}
sx={{
height: "0.5px",
width: "100%",
backgroundColor: "#666",
mb: 2,
}}
/>
</Grid>
<Grid item>
<SearchBox />
</Grid>
<Grid
item
container
@ -377,7 +389,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
/>
<MenuItem
name="Tickets"
href="/overview/assigned"
href="/overview/recent"
Icon={FeaturedPlayListIcon}
selected={
pathname.startsWith("/overview") ||
@ -397,12 +409,21 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
>
<List component="div" disablePadding>
<MenuItem
name="Assigned"
href="/overview/assigned"
name="Recent"
href="/overview/recent"
Icon={FeaturedPlayListIcon}
iconSize={0}
selected={pathname.endsWith("/overview/assigned")}
badge={assignedCount}
selected={pathname.endsWith("/overview/recent")}
badge={recentCount}
open={open}
/>
<MenuItem
name="Open"
href="/overview/open"
Icon={FeaturedPlayListIcon}
iconSize={0}
selected={pathname.endsWith("/overview/open")}
badge={openCount}
open={open}
/>
<MenuItem
@ -415,12 +436,12 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
open={open}
/>
<MenuItem
name="Pending"
href="/overview/pending"
name="Assigned"
href="/overview/assigned"
Icon={FeaturedPlayListIcon}
iconSize={0}
selected={pathname.endsWith("/overview/pending")}
badge={pendingCount}
selected={pathname.endsWith("/overview/assigned")}
badge={assignedCount}
open={open}
/>
<MenuItem

View file

@ -1,6 +1,6 @@
"use client";
import { FC, useState, useEffect } from "react";
import { FC, useState, useEffect, useRef } from "react";
import { useRouter } from "next/navigation";
import Iframe from "react-iframe";
import { useSession } from "next-auth/react";
@ -16,7 +16,8 @@ export const ZammadWrapper: FC<ZammadWrapperProps> = ({
hideSidebar = true,
}) => {
const router = useRouter();
const { data: session } = useSession();
const { data: session } = useSession({ required: true });
const timeoutRef = useRef(null);
const [authenticated, setAuthenticated] = useState(false);
const [display, setDisplay] = useState("none");
const url = `/zammad${path}`;
@ -28,7 +29,7 @@ export const ZammadWrapper: FC<ZammadWrapperProps> = ({
method: "GET",
redirect: "manual",
});
console.log({ res });
if (res.type === "opaqueredirect") {
setAuthenticated(true);
} else {
@ -39,8 +40,24 @@ export const ZammadWrapper: FC<ZammadWrapperProps> = ({
checkAuthenticated();
}, [path]);
if (!session) {
console.log("No session");
useEffect(() => {
if (session === null) {
timeoutRef.current = setTimeout(() => {
if (session === null) {
router.push("/login");
}
}, 3000);
}
if (session !== null) {
clearTimeout(timeoutRef.current);
}
return () => clearTimeout(timeoutRef.current);
}, [session]);
if (!session || !authenticated) {
console.log("Not authenticated");
return (
<Box sx={{ width: "100%" }}>
<Grid

View file

@ -0,0 +1,139 @@
"use client";
import { FC, useState } from "react";
import {
Grid,
Button,
Dialog,
DialogActions,
DialogContent,
TextField,
Autocomplete
} from "@mui/material";
import { useSWRConfig } from "swr";
import { createTicketMutation } from "app/_graphql/createTicketMutation";
interface TicketCreateDialogProps {
open: boolean;
closeDialog: () => void;
}
export const TicketCreateDialog: FC<TicketCreateDialogProps> = ({
open,
closeDialog,
}) => {
const [kind, setKind] = useState("note");
const [customerID, setCustomerID] = useState("");
const [groupID, setGroupID] = useState("");
const [ownerID, setOwnerID] = useState("");
const [priorityID, setPriorityID] = useState("");
const [stateID, setStateID] = useState("");
const [tags, setTags] = useState([]);
const [title, setTitle] = useState("");
const [body, setBody] = useState("");
const backgroundColor = kind === "note" ? "#FFB620" : "#1982FC";
const color = kind === "note" ? "black" : "white";
const { fetcher } = useSWRConfig();
const ticket = {
customerId: customerID,
groupId: groupID,
ownerId: ownerID,
priorityId: priorityID,
stateId: stateID,
tags,
title,
article: {
body,
type: kind,
internal: kind === "note",
}
};
const createTicket = async () => {
await fetcher({
document: createTicketMutation,
variables: {
input: {
ticket
},
},
});
closeDialog();
setBody("");
};
return (
<Dialog open={open} maxWidth="md" fullWidth>
<DialogContent>
<Grid container direction="column" spacing={2}>
<Grid item>
<TextField
label={"Title"}
fullWidth
value={title}
onChange={(e: any) => setTitle(e.target.value)}
/>
</Grid>
<Grid item>
<Autocomplete
disablePortal
options={[{ label: "Test One", id: 1 }, { label: "Test Two", id: 2 }]}
sx={{ width: 300 }}
onChange={(e: any) => setCustomerID(e.target.value)}
renderInput={(params) => <TextField {...params} label="Customer" />}
/>
</Grid>
<Grid item>
<TextField
label={"Details"}
multiline
rows={10}
fullWidth
value={body}
onChange={(e: any) => setBody(e.target.value)}
/>
</Grid>
</Grid>
</DialogContent>
<DialogActions sx={{ px: 3, pt: 0, pb: 3 }}>
<Grid container justifyContent="space-between">
<Grid item>
<Button
sx={{
backgroundColor: "white",
color: "#666",
fontFamily: "Poppins, sans-serif",
fontWeight: 700,
borderRadius: 2,
textTransform: "none",
}}
onClick={() => {
setBody("");
closeDialog();
}}
>
Cancel
</Button>
</Grid>
<Grid item>
<Button
sx={{
backgroundColor,
color,
fontFamily: "Poppins, sans-serif",
fontWeight: 700,
borderRadius: 2,
textTransform: "none",
px: 3,
}}
onClick={createTicket}
>
{kind === "note" ? "Save Note" : "Send Reply"}
</Button>
</Grid>
</Grid>
</DialogActions>
</Dialog>
);
};

View file

@ -1,12 +1,13 @@
"use client";
import { FC } from "react";
import { FC, useState } from "react";
import { Grid, Box } from "@mui/material";
import { GridColDef } from "@mui/x-data-grid-pro";
import { StyledDataGrid } from "../../../_components/StyledDataGrid";
import { Button } from "../../../../_components/Button";
import { typography } from "../../../../_styles/theme";
import { useRouter } from "next/navigation";
import { TicketCreateDialog } from "./TicketCreateDialog";
interface TicketListProps {
title: string;
@ -14,72 +15,96 @@ interface TicketListProps {
}
export const TicketList: FC<TicketListProps> = ({ title, tickets }) => {
const [dialogOpen, setDialogOpen] = useState(false);
const router = useRouter();
let gridColumns: GridColDef[] = [
{
field: "number",
headerName: "Number",
flex: 0.3,
flex: 1,
},
{
field: "title",
headerName: "Title",
flex: 1.5,
flex: 5,
},
{
field: "customer",
headerName: "Sender",
valueGetter: (params) => params.row?.customer?.fullname,
flex: 0.6,
flex: 2,
},
{
field: "createdAt",
headerName: "Created At",
valueGetter: (params) => new Date(params.row?.createdAt).toLocaleString(),
flex: 1,
},
{
field: "updatedAt",
headerName: "Updated At",
valueGetter: (params) => new Date(params.row?.updatedAt).toLocaleString(),
flex: 1,
},
{
field: "group",
headerName: "Group",
valueGetter: (params) => params.row?.group?.name,
flex: 0.3,
flex: 1,
},
];
console.log({ tickets });
const rowClick = ({ row }) => {
router.push(`/tickets/${row.internalId}`);
};
return (
<Box sx={{ height: "100vh", backgroundColor: "#ddd", p: 3 }}>
<Grid container direction="column">
<Grid
item
container
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Grid item>
<Box
sx={{
backgroundColor: "#ddd",
px: "8px",
pb: "16px",
...typography.h4,
fontSize: 24,
}}
>
{title}
</Box>
<>
<Box sx={{ height: "100vh", backgroundColor: "#ddd", p: 3 }}>
<Grid container direction="column">
<Grid
item
container
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Grid item>
<Box
sx={{
backgroundColor: "#ddd",
px: "8px",
pb: "16px",
...typography.h4,
fontSize: 24,
}}
>
{title}
</Box>
</Grid>
<Grid item>
<Button
href={""}
onClick={() => setDialogOpen(true)}
text="Create"
color="#1982FC"
/>
</Grid>
</Grid>
<Grid item>
<Button href="/tickets/create" text="Create" color="#1982FC" />
<StyledDataGrid
name={title}
columns={gridColumns}
rows={tickets}
onRowClick={rowClick}
/>
</Grid>
</Grid>
<Grid item>
<StyledDataGrid
name={title}
columns={gridColumns}
rows={tickets}
onRowClick={rowClick}
/>
</Grid>
</Grid>
</Box>
</Box>
<TicketCreateDialog
open={dialogOpen}
closeDialog={() => setDialogOpen(false)}
/>
</>
);
};

View file

@ -1,6 +1,6 @@
"use client";
import { FC } from "react";
import { FC, useEffect, useState } from "react";
import useSWR from "swr";
import { TicketList } from "./TicketList";
import { getTicketsByOverviewQuery } from "../../../../_graphql/getTicketsByOverviewQuery";
@ -11,6 +11,8 @@ type ZammadOverviewProps = {
};
export const ZammadOverview: FC<ZammadOverviewProps> = ({ name, id }) => {
const [tickets, setTickets] = useState([]);
const [error, setError] = useState(null);
const { data: ticketData, error: ticketError }: any = useSWR(
{
document: getTicketsByOverviewQuery,
@ -19,24 +21,57 @@ export const ZammadOverview: FC<ZammadOverviewProps> = ({ name, id }) => {
{ refreshInterval: 10000 },
);
const shouldRender = !ticketError && ticketData;
const tickets =
ticketData?.ticketsByOverview?.edges.map((edge: any) => edge.node) || [];
const restFetcher = (url: string) => fetch(url).then((r) => r.json());
const { data: recent } = useSWR("/api/v1/recent_view", restFetcher);
const sortedTickets = tickets.sort((a: any, b: any) => {
if (a.internalId < b.internalId) {
return 1;
}
if (a.internalId > b.internalId) {
return -1;
}
return 0;
});
const sortTickets = (tickets: any) => {
return tickets.sort((a: any, b: any) => {
if (a.internalId < b.internalId) {
return 1;
}
if (a.internalId > b.internalId) {
return -1;
}
return 0;
});
};
return (
<>
{shouldRender && <TicketList title={name} tickets={sortedTickets} />}
{/*ticketError && <div>{ticketError.toString()}</div>*/}
</>
);
useEffect(() => {
if (name != "Recent") {
const edges = ticketData?.ticketsByOverview?.edges;
if (edges) {
const nodes = edges.map((edge: any) => edge.node);
setTickets(sortTickets(nodes));
}
if (ticketError) {
setError(ticketError);
}
}
}, [ticketData, ticketError]);
useEffect(() => {
const fetchRecentTickets = async () => {
if (name === "Recent" && recent) {
let allTickets = [];
for (const rec of recent) {
const res = await fetch(`/api/v1/tickets/${rec.o_id}`);
const tkt = await res.json();
allTickets.push({
...tkt,
internalId: tkt.id,
createdAt: tkt.created_at,
updatedAt: tkt.updated_at,
});
}
setTickets(sortTickets(allTickets));
console.log({ allTickets });
}
};
fetchRecentTickets();
}, [name]);
const shouldRender = tickets && !error;
return shouldRender && <TicketList title={name} tickets={tickets} />;
};

View file

@ -24,7 +24,8 @@ export async function generateMetadata({
const overviews = {
assigned: 1,
unassigned: 2,
pending: 3,
recent: 3,
open: 5,
urgent: 7,
};
@ -36,6 +37,7 @@ type PageProps = {
export default function Page({ params: { overview } }: PageProps) {
const section = getSection(overview);
console.log({ section });
return <ZammadOverview name={section} id={overviews[overview]} />;
}

View file

@ -17,6 +17,7 @@ interface ArticleCreateDialogProps {
open: boolean;
closeDialog: () => void;
kind: string;
recipient?: string;
}
export const ArticleCreateDialog: FC<ArticleCreateDialogProps> = ({
@ -24,22 +25,29 @@ export const ArticleCreateDialog: FC<ArticleCreateDialogProps> = ({
open,
closeDialog,
kind,
recipient,
}) => {
const [body, setBody] = useState("");
const backgroundColor = kind === "note" ? "#FFB620" : "#1982FC";
const color = kind === "note" ? "black" : "white";
const { fetcher } = useSWRConfig();
const article = {
body,
type: kind,
internal: kind === "note",
};
if (kind === "email") {
article["to"] = recipient;
}
const createArticle = async () => {
await fetcher({
document: updateTicketMutation,
variables: {
ticketId: `gid://zammad/Ticket/${ticketID}`,
input: {
article: {
body,
type: kind,
internal: kind === "note",
},
article,
},
},
});

View file

@ -53,8 +53,10 @@ export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
: null;
const mostRecentExternalArticleKind =
mostRecentExternalArticle?.type?.name ?? "phone";
const mostRecentEmailRecipient = mostRecentExternalArticle?.to?.name ?? "";
const [dialogOpen, setDialogOpen] = useState(false);
const [articleKind, setArticleKind] = useState("phone");
const [recipient, setRecipient] = useState("");
const closeDialog = () => setDialogOpen(false);
const shouldRender =

View file

@ -12,10 +12,11 @@ import {
} from "@mui/material";
import { MuiChipsInput } from "mui-chips-input";
import useSWR, { useSWRConfig } from "swr";
import { getTicketQuery } from "../../../../../_graphql/getTicketQuery";
import { updateTicketMutation } from "../../../../../_graphql/updateTicketMutation";
import { updateTagsMutation } from "../../../../../_graphql/updateTagsMutation";
import { getTicketQuery } from "app/_graphql/getTicketQuery";
import { updateTicketMutation } from "app/_graphql/updateTicketMutation";
import { updateTagsMutation } from "app/_graphql/updateTagsMutation";
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
interface TicketEditProps {
id: string;
@ -26,6 +27,8 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
const [selectedOwner, setSelectedOwner] = useState("");
const [selectedPriority, setSelectedPriority] = useState("");
const [selectedState, setSelectedState] = useState("");
const [pendingDate, setPendingDate] = useState(new Date());
const [pendingVisible, setPendingVisible] = useState(false);
const [selectedTags, setSelectedTags] = useState([]);
const handleDelete = () => {
console.info("You clicked the delete icon.");
@ -36,8 +39,13 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
const { data: users } = useSWR("/api/v1/users", restFetcher);
const { data: states } = useSWR("/api/v1/ticket_states", restFetcher);
const { data: priorities } = useSWR("/api/v1/ticket_priorities", restFetcher);
const { data: tags } = useSWR("/api/v1/tags", restFetcher);
const { data: recent } = useSWR("/api/v1/recent_view", restFetcher);
console.log({ recent });
// const { data: tags } = useSWR("/api/v1/tags", restFetcher);
const filteredStates = states?.filter(
(state: any) => !["new", "merged", "removed"].includes(state.name),
);
const agents = users?.filter((user: any) => user.role_ids.includes(2)) ?? [];
const { fetcher } = useSWRConfig();
const { data: ticketData, error: ticketError }: any = useSWR(
{
@ -60,6 +68,13 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
setSelectedTags(ticket.tags);
}
}, [ticketData, ticketError]);
useEffect(() => {
const stateName = filteredStates?.find(
(state: any) => state.id === selectedState,
)?.name;
setPendingVisible(stateName?.includes("pending") ?? false);
}, [selectedState]);
const updateTicket = async (input: any) => {
console.log({ input });
const res = await fetcher({
@ -128,14 +143,14 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
backgroundColor: "white",
}}
>
{users?.map((user: any) => (
<MenuItem key={user.id} value={user.id}>
{agents?.map((user: any) => (
<MenuItem key={user.id} value={`${user.id}`}>
{user.firstname} {user.lastname}
</MenuItem>
))}
</Select>
</Grid>
<Grid item>
<Grid item xs={12}>
<Box sx={{ m: 1, mt: 0 }}>State</Box>
<Select
value={selectedState}
@ -144,6 +159,7 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
setSelectedState(newState);
updateTicket({
stateId: `gid://zammad/Ticket::State/${newState}`,
pendingTime: pendingDate.toISOString(),
});
}}
size="small"
@ -152,13 +168,35 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
backgroundColor: "white",
}}
>
{states?.map((state: any) => (
{filteredStates?.map((state: any) => (
<MenuItem key={state.id} value={state.id}>
{state.name}
</MenuItem>
))}
</Select>
</Grid>
<Grid
item
xs={12}
sx={{ display: pendingVisible ? "inherit" : "none" }}
>
<DatePicker
label="Pending Date"
value={pendingDate}
onChange={(newValue: any) => {
console.log(newValue);
setPendingDate(newValue);
updateTicket({
pendingTime: newValue.toISOString(),
});
}}
slotProps={{ textField: { size: "small" } }}
sx={{
width: "100%",
backgroundColor: "white",
}}
/>
</Grid>
<Grid item>
<Box sx={{ m: 1, mt: 0 }}>Priority</Box>
<Select

View file

@ -8,13 +8,15 @@ interface ButtonProps {
text: string;
color: string;
href: string;
onClick: any;
}
export const Button: FC<ButtonProps> = ({ text, color, href }) => (
export const Button: FC<ButtonProps> = ({ text, color, href, onClick }) => (
<Link href={href} passHref>
<MUIButton
variant="contained"
disableElevation
onClick={onClick}
sx={{
fontFamily: "Poppins, sans-serif",
fontWeight: 700,

View file

@ -1,6 +1,7 @@
"use client";
import { FC, PropsWithChildren, useState } from "react";
import { FC, PropsWithChildren, useState, useEffect } from "react";
import { usePathname } from "next/navigation";
import { CssBaseline } from "@mui/material";
import { CookiesProvider } from "react-cookie";
import { SessionProvider } from "next-auth/react";
@ -31,22 +32,45 @@ export const MultiProvider: FC<PropsWithChildren> = ({ children }) => {
});
const messages: any = { en: locales.en, fr: locales.fr };
const locale = "en";
const graphQLFetcher = async ({ document, variables }: any) => {
const fetchAndCheckAuth = async ({ document, variables }: any) => {
const requestHeaders = {
"X-CSRF-Token": csrfToken,
};
const { data, headers } = await client.rawRequest(
const { data, headers, status } = await client.rawRequest(
document,
variables,
requestHeaders,
);
if (status !== 200) {
const res = await fetch("/zammad/auth/sso", {
method: "GET",
redirect: "manual",
});
console.log({ checkAuth: res });
return null;
}
const token = headers.get("CSRF-Token");
setCsrfToken(token);
return data;
};
const graphQLFetcher = async ({ document, variables }: any) => {
let checks = 0;
let data = null;
while (!data && checks < 2) {
data = await fetchAndCheckAuth({ document, variables });
checks++;
}
return data;
};
return (
<>
<CssBaseline />

View file

@ -0,0 +1,13 @@
import { gql } from "graphql-request";
export const createTicketMutation = gql`
mutation CreateTicket($ticketId: ID!, $input: TicketCreateInput!) {
ticketCreate(input: $input) {
ticket {
id
priority {
id
}
}
}
}`;

View file

@ -0,0 +1,20 @@
import { gql } from 'graphql-request';
export const searchQuery = gql`
query search($search: String!, $limit: Int = 10, $onlyIn: EnumSearchableModels = Ticket) {
search(search: $search, limit: $limit, onlyIn: $onlyIn) {
... on Ticket {
id
number
internalId
title
state {
id
name
}
stateColorCode
note
}
}
}
`;

View file

@ -18,7 +18,6 @@ const fetchRoles = async () => {
const fetchUser = async (email: string) => {
const url = `${process.env.ZAMMAD_URL}/api/v1/users/search?query=email:${email}&limit=1`;
console.log({ url });
const res = await fetch(url, { headers });
const users = await res.json();
const user = users?.[0];