Zammad docker and Link structure updates

This commit is contained in:
Darren Clarke 2023-07-10 10:13:06 +00:00 committed by GitHub
parent 2a37297ae1
commit 60b82f6fb4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 94 additions and 36 deletions

View file

@ -0,0 +1,102 @@
"use client";
import { FC, useState } from "react";
import {
Grid,
Button,
Dialog,
DialogActions,
DialogContent,
TextField,
} from "@mui/material";
import { useSWRConfig } from "swr";
import { updateTicketMutation } from "@/app/_graphql/updateTicketMutation";
interface ArticleCreateDialogProps {
ticketID: string;
open: boolean;
closeDialog: () => void;
kind: "reply" | "note";
}
export const ArticleCreateDialog: FC<ArticleCreateDialogProps> = ({
ticketID,
open,
closeDialog,
kind,
}) => {
const [body, setBody] = useState("");
const backgroundColor = kind === "reply" ? "#1982FC" : "#FFB620";
const color = kind === "reply" ? "white" : "black";
const { fetcher } = useSWRConfig();
const createArticle = async () => {
await fetcher({
document: updateTicketMutation,
variables: {
ticketId: `gid://zammad/Ticket/${ticketID}`,
input: {
article: {
body,
type: kind === "note" ? "note" : "phone",
internal: kind === "note",
},
},
},
});
closeDialog();
setBody("");
};
return (
<Dialog open={open} maxWidth="sm" fullWidth>
<DialogContent>
<TextField
label={kind === "reply" ? "Write reply" : "Write internal note"}
multiline
rows={10}
fullWidth
value={body}
onChange={(e: any) => setBody(e.target.value)}
/>
</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={createArticle}
>
{kind === "reply" ? "Send Reply" : "Save Note"}
</Button>
</Grid>
</Grid>
</DialogActions>
</Dialog>
);
};

View file

@ -0,0 +1,177 @@
"use client";
import { FC, useState } from "react";
import useSWR from "swr";
import { getTicketQuery } from "@/app/_graphql/getTicketQuery";
import {
Grid,
Box,
Typography,
Button,
Dialog,
DialogActions,
DialogContent,
} from "@mui/material";
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
import {
MainContainer,
ChatContainer,
MessageList,
Message,
ConversationHeader,
} from "@chatscope/chat-ui-kit-react";
import { ArticleCreateDialog } from "./ArticleCreateDialog";
interface TicketDetailProps {
id: string;
}
export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
const { data: ticketData, error: ticketError }: any = useSWR(
{
document: getTicketQuery,
variables: { ticketId: `gid://zammad/Ticket/${id}` },
},
{ refreshInterval: 100000 }
);
console.log({ ticketData, ticketError });
const ticket = ticketData?.ticket;
const [dialogOpen, setDialogOpen] = useState(false);
const [articleKind, setArticleKind] = useState<"reply" | "note">("reply");
const closeDialog = () => setDialogOpen(false);
const shouldRender = ticketData && !ticketError;
return (
shouldRender && (
<>
<MainContainer>
<ChatContainer>
<ConversationHeader>
<ConversationHeader.Content>
<Box
sx={{
width: "100%",
textAlign: "center",
fontWeight: "bold",
}}
>
<Typography
variant="h5"
sx={{ fontFamily: "Poppins", fontWeight: 700 }}
>
{ticket.title}
</Typography>
<Typography
variant="h6"
sx={{ fontFamily: "Roboto", fontWeight: 400 }}
>{`Ticket #${ticket.number} (created ${new Date(
ticket.createdAt
).toLocaleDateString()})`}</Typography>
</Box>
</ConversationHeader.Content>
</ConversationHeader>
<MessageList style={{ marginBottom: 80 }}>
{ticket.articles.edges.map(({ node: article }: any) => (
<Message
key={article.id}
className={
article.internal
? "internal-note"
: article.sender.name === "Agent"
? "outgoing-message"
: "incoming-message"
}
model={{
message: article.body.replace(/<div>*<br>*<div>/g, ""),
sentTime: article.updated_at,
sender: article.from,
direction:
article.sender === "Agent" ? "outgoing" : "incoming",
position: "single",
}}
/>
))}
</MessageList>
</ChatContainer>
<Box
sx={{
height: 80,
background: "#eeeeee",
borderTop: "1px solid #ddd",
position: "absolute",
bottom: 0,
width: "100%",
zIndex: 1000,
}}
>
<Grid
container
spacing={4}
justifyContent="center"
alignItems="center"
alignContent="center"
>
<Grid item>
<Button
variant="contained"
disableElevation
sx={{
fontFamily: "Poppins, sans-serif",
fontWeight: 700,
borderRadius: 2,
textTransform: "none",
backgroundColor: "#1982FC",
padding: "6px 30px",
margin: "20px 0px",
whiteSpace: "nowrap",
py: "10px",
mt: 2,
}}
onClick={() => {
setArticleKind("reply");
setDialogOpen(true);
}}
>
Reply to ticket
</Button>
</Grid>
<Grid item>
<Button
variant="contained"
disableElevation
sx={{
fontFamily: "Poppins, sans-serif",
fontWeight: 700,
borderRadius: 2,
textTransform: "none",
color: "black",
backgroundColor: "#FFB620",
padding: "6px 30px",
margin: "20px 0px",
whiteSpace: "nowrap",
py: "10px",
mt: 2,
}}
onClick={() => {
setArticleKind("note");
setDialogOpen(true);
}}
>
Write note to agent
</Button>
</Grid>
</Grid>
</Box>
</MainContainer>
<ArticleCreateDialog
ticketID={ticket.internalId}
open={dialogOpen}
closeDialog={closeDialog}
kind={articleKind}
/>
</>
)
);
};

View file

@ -0,0 +1,11 @@
import { TicketDetail } from "./_components/TicketDetail";
type PageProps = {
params: {
id: string;
};
};
export default function Page({ params: { id } }: PageProps) {
return <TicketDetail id={id} />;
}

View file

@ -0,0 +1,167 @@
"use client";
import { FC, useEffect, useState } from "react";
import {
Grid,
Box,
Typography,
TextField,
Stack,
Chip,
Select,
MenuItem,
} from "@mui/material";
import useSWR, { useSWRConfig } from "swr";
import { updateTicketMutation } from "@/app/_graphql/updateTicketMutation";
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
interface TicketEditProps {
ticket: any;
}
export const TicketEdit: FC<TicketEditProps> = ({ ticket }) => {
const [selectedGroup, setSelectedGroup] = useState(1);
const [selectedOwner, setSelectedOwner] = useState(1);
const [selectedPriority, setSelectedPriority] = useState(1);
const [selectedState, setSelectedState] = useState(1);
const [selectedTags, setSelectedTags] = useState(["tag1", "tag2"]);
const handleDelete = () => {
console.info("You clicked the delete icon.");
};
const restFetcher = (url: string) => fetch(url).then((r) => r.json());
/*
const { data: groups } = useSWR("/api/v1/groups", restFetcher);
console.log({ groups });
const { data: users } = useSWR("/api/v1/users", restFetcher);
console.log({ users });
const { data: states } = useSWR("/api/v1/ticket_states", restFetcher);
console.log({ states });
const { data: priorities } = useSWR("/api/v1/ticket_priorities", restFetcher);
console.log({ priorities });
*/
const groups = [];
const users = [];
const states = [];
const priorities = [];
const { fetcher } = useSWRConfig();
const updateTicket = async () => {
await fetcher({
document: updateTicketMutation,
variables: {
ticketId: ticket.id,
input: {
ownerId: `gid://zammad/User/${selectedOwner}`,
tags: ["tag1", "tag2"],
},
},
});
};
return (
<Box sx={{ height: "100vh", background: "#ddd", p: 2 }}>
<Grid container direction="column" spacing={3}>
<Grid item>
<Box sx={{ m: 1 }}>Group</Box>
<Select
defaultValue={selectedGroup}
value={selectedGroup}
onChange={(e: any) => {
setSelectedGroup(e.target.value);
updateTicket();
}}
size="small"
sx={{
width: "100%",
backgroundColor: "white",
}}
>
{groups?.map((group: any) => (
<MenuItem key={group.id} value={group.id}>
{group.name}
</MenuItem>
))}
</Select>
</Grid>
<Grid item>
<Box sx={{ m: 1, mt: 0 }}>Owner</Box>
<Select
value={selectedOwner}
onChange={(e: any) => {
setSelectedOwner(e.target.value);
updateTicket();
}}
size="small"
sx={{
width: "100%",
backgroundColor: "white",
}}
>
{users?.map((user: any) => (
<MenuItem key={user.id} value={user.id}>
{user.firstname} {user.lastname}
</MenuItem>
))}
</Select>
</Grid>
<Grid item>
<Box sx={{ m: 1, mt: 0 }}>State</Box>
<Select
value={selectedState}
onChange={(e: any) => setSelectedState(e.target.value)}
size="small"
sx={{
width: "100%",
backgroundColor: "white",
}}
>
{states?.map((state: any) => (
<MenuItem key={state.id} value={state.id}>
{state.name}
</MenuItem>
))}
</Select>
</Grid>
<Grid item>
<Box sx={{ m: 1, mt: 0 }}>Priority</Box>
<Select
value={selectedPriority}
onChange={(e: any) => setSelectedPriority(e.target.value)}
size="small"
sx={{
width: "100%",
backgroundColor: "white",
}}
>
{priorities?.map((priority: any) => (
<MenuItem key={priority.id} value={priority.id}>
{priority.name}
</MenuItem>
))}
</Select>
</Grid>
<Grid item>
<Box sx={{ mb: 1 }}>Tags</Box>
<Stack
direction="row"
spacing={1}
sx={{
backgroundColor: "white",
p: 1,
borderRadius: "6px",
border: "1px solid #bbb",
minHeight: 120,
}}
flexWrap="wrap"
>
{selectedTags.map((tag: string) => (
<Chip key={tag} label={tag} onDelete={handleDelete} />
))}
</Stack>
</Grid>
</Grid>
</Box>
);
};

View file

@ -0,0 +1,5 @@
import { TicketEdit } from "./_components/TicketEdit";
export default function Page() {
return <TicketEdit ticket={undefined} />;
}

View file

@ -0,0 +1,11 @@
"use client";
import { DisplayError } from "app/_components/DisplayError";
type PageProps = {
error: Error;
};
export default function Page({ error }: PageProps) {
return <DisplayError error={error} />;
}

View file

@ -0,0 +1,25 @@
"use client";
import { ReactNode } from "react";
import { Grid } from "@mui/material";
type LayoutProps = {
detail: any;
edit: any;
params: {
id: string;
};
};
export default function Layout({ detail, edit, params: { id } }: LayoutProps) {
return (
<Grid container spacing={0} sx={{ height: "100vh" }} direction="row">
<Grid item sx={{ height: "100vh" }} xs={9}>
{detail}
</Grid>
<Grid item xs={3} sx={{ height: "100vh" }}>
{edit}
</Grid>
</Grid>
);
}

View file

@ -0,0 +1,15 @@
"use client";
import Link from "next/link";
export default function Page() {
return (
<div>
<h2>Not Found</h2>
<p>Could not find requested resource</p>
<p>
View <Link href="/blog">all posts</Link>
</p>
</div>
);
}

View file

@ -0,0 +1,62 @@
type PageProps = {
params: {
id: string;
};
};
export default function Page({ params: { id } }: PageProps) {
return <div>Page</div>;
}
/*
import { GetServerSideProps, GetServerSidePropsContext } from "next";
import Head from "next/head";
import useSWR from "swr";
import { NextPage } from "next";
import { Grid } from "@mui/material";
import { TicketDetail } from "@/app/_components/TicketDetail";
import { TicketEdit } from "@/app/_components/TicketEdit";
import { getTicketQuery } from "@/app/_graphql/getTicketQuery";
type TicketProps = {
id: string;
};
const Ticket: NextPage<TicketProps> = ({ id }) => {
const { data: ticketData, error: ticketError }: any = useSWR(
{
document: getTicketQuery,
variables: { ticketId: `gid://zammad/Ticket/${id}` },
},
{ refreshInterval: 100000 }
);
const shouldRender = !ticketError && ticketData;
return (
<>
{shouldRender && (
<Grid container spacing={0} sx={{ height: "100vh" }} direction="row">
<Grid item sx={{ height: "100vh" }} xs={9}>
<TicketDetail ticket={ticketData.ticket} />
</Grid>
<Grid item xs={3} sx={{ height: "100vh" }}>
<TicketEdit ticket={ticketData.ticket} />
</Grid>
</Grid>
)}
{ticketError && <div>{ticketError.toString()}</div>}
</>
);
};
export const getServerSideProps: GetServerSideProps = async (
context: GetServerSidePropsContext
) => {
const { id } = context.query;
return { props: { id } };
};
export default Ticket;
*/