Develop
This commit is contained in:
parent
7ca5f2d45a
commit
f901f203b0
302 changed files with 9897 additions and 10332 deletions
6
apps/link/app/(main)/_components/Home.tsx
Normal file
6
apps/link/app/(main)/_components/Home.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { ZammadWrapper } from "./ZammadWrapper";
|
||||
|
||||
export const Home: FC = () => <ZammadWrapper path="/#dashboard" hideSidebar />;
|
||||
21
apps/link/app/(main)/_components/InternalLayout.tsx
Normal file
21
apps/link/app/(main)/_components/InternalLayout.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
"use client";
|
||||
|
||||
import { FC, PropsWithChildren, useState } from "react";
|
||||
import { Grid } from "@mui/material";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
|
||||
export const InternalLayout: FC<PropsWithChildren> = ({ children }) => {
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
return (
|
||||
<Grid container direction="row">
|
||||
<Sidebar open={open} setOpen={setOpen} />
|
||||
<Grid
|
||||
item
|
||||
sx={{ ml: open ? "270px" : "100px", width: "100%", height: "100vh" }}
|
||||
>
|
||||
{children as any}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
554
apps/link/app/(main)/_components/Sidebar.tsx
Normal file
554
apps/link/app/(main)/_components/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,554 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
Typography,
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
ListItemSecondaryAction,
|
||||
Drawer,
|
||||
Collapse,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
FeaturedPlayList as FeaturedPlayListIcon,
|
||||
Person as PersonIcon,
|
||||
Analytics as AnalyticsIcon,
|
||||
Logout as LogoutIcon,
|
||||
Cottage as CottageIcon,
|
||||
Settings as SettingsIcon,
|
||||
ExpandCircleDown as ExpandCircleDownIcon,
|
||||
Dvr as DvrIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { usePathname } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
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";
|
||||
|
||||
const openWidth = 270;
|
||||
const closedWidth = 100;
|
||||
|
||||
const MenuItem = ({
|
||||
name,
|
||||
href,
|
||||
Icon,
|
||||
iconSize,
|
||||
inset = false,
|
||||
selected = false,
|
||||
open = true,
|
||||
badge,
|
||||
target = "_self",
|
||||
}: any) => (
|
||||
<Link href={href} target={target}>
|
||||
<ListItemButton
|
||||
sx={{
|
||||
p: 0,
|
||||
mb: 1,
|
||||
bl: iconSize === 0 ? "1px solid white" : "inherit",
|
||||
}}
|
||||
selected={selected}
|
||||
>
|
||||
{iconSize > 0 ? (
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
color: `white`,
|
||||
minWidth: 0,
|
||||
mr: 2,
|
||||
textAlign: "center",
|
||||
margin: open ? "0 8 0 0" : "0 auto",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
mr: 0.5,
|
||||
mt: "-4px",
|
||||
}}
|
||||
>
|
||||
<Icon />
|
||||
</Box>
|
||||
</ListItemIcon>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
width: 30,
|
||||
height: "28px",
|
||||
position: "relative",
|
||||
ml: "9px",
|
||||
mr: "1px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "1px",
|
||||
height: "56px",
|
||||
backgroundColor: "white",
|
||||
position: "absolute",
|
||||
left: "3px",
|
||||
top: "-10px",
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
width: "42px",
|
||||
height: "42px",
|
||||
position: "absolute",
|
||||
top: "-27px",
|
||||
left: "3px",
|
||||
border: "solid 1px #fff",
|
||||
borderColor: "transparent transparent transparent #fff",
|
||||
borderRadius: "60px",
|
||||
rotate: "-35deg",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{open && (
|
||||
<ListItemText
|
||||
inset={inset}
|
||||
primary={
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
fontSize: 16,
|
||||
fontFamily: "Roboto",
|
||||
fontWeight: "bold",
|
||||
border: 0,
|
||||
textAlign: "left",
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{badge && badge > 0 ? (
|
||||
<ListItemSecondaryAction>
|
||||
<Typography
|
||||
color="textSecondary"
|
||||
variant="body1"
|
||||
className="badge"
|
||||
sx={{
|
||||
backgroundColor: "#FFB620",
|
||||
color: "black !important",
|
||||
borderRadius: 10,
|
||||
px: 1,
|
||||
fontSize: 12,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{badge}
|
||||
</Typography>
|
||||
</ListItemSecondaryAction>
|
||||
) : null}
|
||||
</ListItemButton>
|
||||
</Link>
|
||||
);
|
||||
|
||||
interface SidebarProps {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||
const pathname = usePathname();
|
||||
const { data: session } = useSession();
|
||||
const username = session?.user?.name || "User";
|
||||
const { data: overviewData, error: overviewError }: any = useSWR(
|
||||
{
|
||||
document: getTicketOverviewCountsQuery,
|
||||
},
|
||||
{ refreshInterval: 10000 }
|
||||
);
|
||||
const findOverviewCountByID = (id: number) =>
|
||||
overviewData?.ticketOverviews?.edges?.find((overview: any) =>
|
||||
overview.node.id.endsWith(`/${id}`)
|
||||
)?.node?.ticketCount ?? 0;
|
||||
const assignedCount = findOverviewCountByID(1);
|
||||
const urgentCount = findOverviewCountByID(7);
|
||||
const pendingCount = findOverviewCountByID(3);
|
||||
const unassignedCount = findOverviewCountByID(2);
|
||||
|
||||
const logout = () => {
|
||||
signOut({ callbackUrl: "/login" });
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
sx={{ width: open ? openWidth : closedWidth, flexShrink: 0 }}
|
||||
variant="permanent"
|
||||
anchor="left"
|
||||
open={open}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
width: open ? openWidth : closedWidth,
|
||||
border: 0,
|
||||
overflow: "visible",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 20,
|
||||
right: open ? -8 : -16,
|
||||
color: "#1C75FD",
|
||||
rotate: open ? "90deg" : "-90deg",
|
||||
}}
|
||||
onClick={() => {
|
||||
setOpen!(!open);
|
||||
}}
|
||||
>
|
||||
<ExpandCircleDownIcon
|
||||
sx={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
background: "white",
|
||||
borderRadius: 500,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="space-between"
|
||||
wrap="nowrap"
|
||||
sx={{ backgroundColor: "#25272A", height: "100%", p: 2 }}
|
||||
>
|
||||
<Grid item container>
|
||||
<Grid item sx={{ width: open ? "40px" : "100%" }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: "40px",
|
||||
height: "40px",
|
||||
margin: open ? "0" : "0 auto",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={LinkLogo}
|
||||
alt="Link logo"
|
||||
width={40}
|
||||
height={40}
|
||||
style={{
|
||||
objectFit: "cover",
|
||||
filter: "grayscale(100) brightness(100)",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
.
|
||||
</Grid>
|
||||
{open && (
|
||||
<Grid item>
|
||||
<Typography
|
||||
variant="h2"
|
||||
sx={{
|
||||
fontSize: 26,
|
||||
color: "white",
|
||||
fontWeight: 700,
|
||||
mt: 1,
|
||||
ml: 0.5,
|
||||
fontFamily: "Poppins",
|
||||
}}
|
||||
>
|
||||
CDR Link
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Box
|
||||
sx={{
|
||||
height: "0.5px",
|
||||
width: "100%",
|
||||
backgroundColor: "#666",
|
||||
mb: 1,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontSize: 12,
|
||||
color: "#999",
|
||||
fontWeight: "bold",
|
||||
textAlign: open ? "left" : "center",
|
||||
}}
|
||||
>
|
||||
Hello
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography
|
||||
variant="h2"
|
||||
sx={{
|
||||
fontSize: 22,
|
||||
color: "white",
|
||||
mb: 1.5,
|
||||
fontWeight: "bold",
|
||||
textAlign: open ? "left" : "center",
|
||||
}}
|
||||
>
|
||||
{open
|
||||
? username
|
||||
: username
|
||||
.split(" ")
|
||||
.map((name) => name.substring(0, 1))
|
||||
.join("")}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Box
|
||||
sx={{ height: "0.5px", width: "100%", backgroundColor: "#666" }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item container direction="column" sx={{ mt: "6px" }} flexGrow={1}>
|
||||
<List
|
||||
component="nav"
|
||||
sx={{
|
||||
a: {
|
||||
textDecoration: "none",
|
||||
|
||||
".MuiListItemButton-root": {
|
||||
p: 1,
|
||||
borderRadius: 2,
|
||||
"&:hover": {
|
||||
background: "#555",
|
||||
},
|
||||
".MuiTypography-root": {
|
||||
p: {
|
||||
color: "#999 !important",
|
||||
fontSize: 16,
|
||||
},
|
||||
},
|
||||
".badge": {
|
||||
p: { fontSize: 12, color: "black !important" },
|
||||
},
|
||||
},
|
||||
".Mui-selected": {
|
||||
background: "#444",
|
||||
color: "#fff !important",
|
||||
".MuiTypography-root": {
|
||||
p: {
|
||||
color: "#fff !important",
|
||||
fontSize: 16,
|
||||
},
|
||||
},
|
||||
".badge": {
|
||||
p: { fontSize: 12, color: "black !important" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
name="Home"
|
||||
href="/"
|
||||
Icon={CottageIcon}
|
||||
iconSize={20}
|
||||
selected={pathname.endsWith("/")}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
name="Tickets"
|
||||
href="/overview/assigned"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
selected={
|
||||
pathname.startsWith("/overview") ||
|
||||
pathname.startsWith("/tickets")
|
||||
}
|
||||
iconSize={20}
|
||||
open={open}
|
||||
/>
|
||||
<Collapse
|
||||
in={
|
||||
pathname.startsWith("/overview") ||
|
||||
pathname.startsWith("/tickets")
|
||||
}
|
||||
timeout="auto"
|
||||
unmountOnExit
|
||||
onClick={undefined}
|
||||
>
|
||||
<List component="div" disablePadding>
|
||||
<MenuItem
|
||||
name="Assigned"
|
||||
href="/overview/assigned"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/overview/assigned")}
|
||||
badge={assignedCount}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
name="Urgent"
|
||||
href="/overview/urgent"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/overview/urgent")}
|
||||
badge={urgentCount}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
name="Pending"
|
||||
href="/overview/pending"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/overview/pending")}
|
||||
badge={pendingCount}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
name="Unassigned"
|
||||
href="/overview/unassigned"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/overview/unassigned")}
|
||||
badge={unassignedCount}
|
||||
open={open}
|
||||
/>
|
||||
</List>
|
||||
</Collapse>
|
||||
<MenuItem
|
||||
name="Knowledge Base"
|
||||
href="/knowledge"
|
||||
Icon={CottageIcon}
|
||||
iconSize={20}
|
||||
selected={pathname.endsWith("/knowledge")}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
name="Leafcutter"
|
||||
href="/leafcutter/about"
|
||||
Icon={AnalyticsIcon}
|
||||
iconSize={20}
|
||||
selected={pathname.endsWith("/leafcutter")}
|
||||
open={open}
|
||||
/>
|
||||
<Collapse
|
||||
in={pathname.startsWith("/leafcutter")}
|
||||
timeout="auto"
|
||||
unmountOnExit
|
||||
onClick={undefined}
|
||||
>
|
||||
<List component="div" disablePadding>
|
||||
{/*
|
||||
<MenuItem
|
||||
name="Dashboard"
|
||||
href="/leafcutter"
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/leafcutter")}
|
||||
open={open}
|
||||
/>
|
||||
|
||||
<MenuItem
|
||||
name="Search and Create"
|
||||
href="/leafcutter/create"
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/leafcutter/create")}
|
||||
open={open}
|
||||
/>
|
||||
|
||||
<MenuItem
|
||||
name="Trends"
|
||||
href="/leafcutter/trends"
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/leafcutter/trends")}
|
||||
open={open}
|
||||
/>
|
||||
*/}
|
||||
<MenuItem
|
||||
name="FAQ"
|
||||
href="/leafcutter/faq"
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/leafcutter/faq")}
|
||||
open={open}
|
||||
/>
|
||||
|
||||
<MenuItem
|
||||
name="About"
|
||||
href="/leafcutter/about"
|
||||
Icon={AnalyticsIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/leafcutter/about")}
|
||||
open={open}
|
||||
/>
|
||||
</List>
|
||||
</Collapse>
|
||||
<MenuItem
|
||||
name="Profile"
|
||||
href="/profile"
|
||||
Icon={PersonIcon}
|
||||
iconSize={20}
|
||||
selected={pathname.endsWith("/profile")}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
name="Admin"
|
||||
href="/admin/zammad"
|
||||
Icon={SettingsIcon}
|
||||
iconSize={20}
|
||||
open={open}
|
||||
/>
|
||||
<Collapse
|
||||
in={pathname.startsWith("/admin/")}
|
||||
timeout="auto"
|
||||
unmountOnExit
|
||||
onClick={undefined}
|
||||
>
|
||||
<List component="div" disablePadding>
|
||||
<MenuItem
|
||||
name="Zammad Settings"
|
||||
href="/admin/zammad"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/admin/zammad")}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
name="Metamigo"
|
||||
href="/admin/metamigo"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/admin/metamigo")}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
name="Label Studio"
|
||||
href="/admin/label-studio"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/admin/label-studio")}
|
||||
open={open}
|
||||
/>
|
||||
</List>
|
||||
</Collapse>
|
||||
<MenuItem
|
||||
name="Zammad Interface"
|
||||
href="/proxy/zammad"
|
||||
Icon={DvrIcon}
|
||||
iconSize={20}
|
||||
open={open}
|
||||
target="_blank"
|
||||
/>
|
||||
<MenuItem
|
||||
name="Logout"
|
||||
href="/logout"
|
||||
Icon={LogoutIcon}
|
||||
iconSize={20}
|
||||
open={open}
|
||||
onClick={logout}
|
||||
/>
|
||||
</List>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
86
apps/link/app/(main)/_components/StyledDataGrid.tsx
Normal file
86
apps/link/app/(main)/_components/StyledDataGrid.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { Box } from "@mui/material";
|
||||
import {
|
||||
DataGridPro,
|
||||
GridColDef,
|
||||
GridColumnVisibilityModel,
|
||||
GridRowSelectionModel,
|
||||
} from "@mui/x-data-grid-pro";
|
||||
import { useCookies } from "react-cookie";
|
||||
|
||||
interface StyledDataGridProps {
|
||||
name: string;
|
||||
columns: GridColDef[];
|
||||
rows: any[];
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
onRowClick?: (row: any) => void;
|
||||
height?: string;
|
||||
selectedRows?: GridRowSelectionModel;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
setSelectedRows?: (rows: GridRowSelectionModel) => void;
|
||||
}
|
||||
|
||||
export const StyledDataGrid: FC<StyledDataGridProps> = ({
|
||||
name,
|
||||
columns,
|
||||
rows,
|
||||
onRowClick,
|
||||
height = "calc(100vh - 20px)",
|
||||
selectedRows,
|
||||
setSelectedRows,
|
||||
}) => {
|
||||
const cookieName = `${name}DataGridColumnState`;
|
||||
const [cookies, setCookie] = useCookies([cookieName]);
|
||||
const handleColumnVisibilityChange = (model: GridColumnVisibilityModel) =>
|
||||
setCookie(cookieName, model, { path: "/" });
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: "#ddd",
|
||||
border: 0,
|
||||
width: "100%",
|
||||
height,
|
||||
".MuiDataGrid-row:nth-of-type(1n)": {
|
||||
backgroundColor: "#f3f3f3",
|
||||
},
|
||||
".MuiDataGrid-row:nth-of-type(2n)": {
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
".MuiDataGrid-columnHeaderTitle": {
|
||||
color: "#333",
|
||||
fontWeight: "bold",
|
||||
fontSize: 11,
|
||||
margin: "0 auto",
|
||||
},
|
||||
".MuiDataGrid-columnHeader": {
|
||||
backgroundColor: "#ccc",
|
||||
border: 0,
|
||||
borderBottom: "3px solid #ddd",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DataGridPro
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
density="compact"
|
||||
hideFooter
|
||||
sx={{ height }}
|
||||
rowBuffer={30}
|
||||
checkboxSelection={!!setSelectedRows}
|
||||
onRowSelectionModelChange={setSelectedRows}
|
||||
rowSelectionModel={selectedRows}
|
||||
rowHeight={46}
|
||||
scrollbarSize={0}
|
||||
disableVirtualization
|
||||
columnVisibilityModel={
|
||||
(cookies[cookieName] as GridColumnVisibilityModel) ?? undefined
|
||||
}
|
||||
onColumnVisibilityModelChange={handleColumnVisibilityChange}
|
||||
onRowClick={onRowClick}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
82
apps/link/app/(main)/_components/ZammadWrapper.tsx
Normal file
82
apps/link/app/(main)/_components/ZammadWrapper.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import getConfig from "next/config";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Iframe from "react-iframe";
|
||||
|
||||
type ZammadWrapperProps = {
|
||||
path: string;
|
||||
hideSidebar?: boolean;
|
||||
};
|
||||
|
||||
export const ZammadWrapper: FC<ZammadWrapperProps> = ({
|
||||
path,
|
||||
hideSidebar = true,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const [display, setDisplay] = useState("none");
|
||||
const url = `/proxy/zammad${path}`;
|
||||
console.log({ url });
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Iframe
|
||||
id="zammad"
|
||||
url={url}
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameBorder={0}
|
||||
styles={{ display }}
|
||||
onLoad={() => {
|
||||
const linkElement = document.querySelector("iframe");
|
||||
if (
|
||||
linkElement.contentDocument &&
|
||||
linkElement.contentDocument?.querySelector &&
|
||||
linkElement.contentDocument.querySelector("#navigation") &&
|
||||
linkElement.contentDocument.querySelector("body") &&
|
||||
linkElement.contentDocument.querySelector(".sidebar")
|
||||
) {
|
||||
// @ts-ignore
|
||||
linkElement.contentDocument.querySelector("#navigation").style =
|
||||
"display: none";
|
||||
// @ts-ignore
|
||||
linkElement.contentDocument.querySelector("body").style =
|
||||
"font-family: Arial";
|
||||
|
||||
if (hideSidebar) {
|
||||
// @ts-ignore
|
||||
linkElement.contentDocument.querySelector(".sidebar").style =
|
||||
"display: none";
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (linkElement.contentDocument.querySelector(".overview-header")) {
|
||||
// @ts-ignore
|
||||
(
|
||||
linkElement.contentDocument.querySelector(
|
||||
".overview-header"
|
||||
) as any
|
||||
).style = "display: none";
|
||||
}
|
||||
|
||||
setDisplay("inherit");
|
||||
|
||||
if (linkElement.contentWindow) {
|
||||
linkElement.contentWindow.addEventListener("hashchange", () => {
|
||||
const hash = linkElement.contentWindow?.location?.hash ?? "";
|
||||
if (hash.startsWith("#ticket/zoom/")) {
|
||||
setDisplay("none");
|
||||
const ticketID = hash.split("/").pop();
|
||||
router.push(`/tickets/${ticketID}`);
|
||||
setTimeout(() => {
|
||||
setDisplay("inherit");
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { Grid } from "@mui/material";
|
||||
import Iframe from "react-iframe";
|
||||
|
||||
export const LabelStudioWrapper: FC = () => (
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
sx={{ height: "100%", width: "100%" }}
|
||||
direction="column"
|
||||
>
|
||||
<Grid item sx={{ height: "100vh", width: "100%" }}>
|
||||
<Iframe
|
||||
id="link"
|
||||
url={"https://label-studio:3000"}
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameBorder={0}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
10
apps/link/app/(main)/admin/label-studio/page.tsx
Normal file
10
apps/link/app/(main)/admin/label-studio/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Metadata } from "next";
|
||||
import { LabelStudioWrapper } from "./_components/LabelStudioWrapper";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Label Studio",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return <LabelStudioWrapper />;
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import getConfig from "next/config";
|
||||
import { Grid } from "@mui/material";
|
||||
import Iframe from "react-iframe";
|
||||
|
||||
type MetamigoWrapperProps = {
|
||||
path: string;
|
||||
};
|
||||
|
||||
export const MetamigoWrapper: FC<MetamigoWrapperProps> = ({ path }) => {
|
||||
const {
|
||||
publicRuntimeConfig: { linkURL },
|
||||
} = getConfig();
|
||||
const fullMetamigoURL = `${linkURL}/metamigo/${path}`;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
sx={{ height: "100%", width: "100%" }}
|
||||
direction="column"
|
||||
>
|
||||
<Grid item sx={{ height: "100vh", width: "100%" }}>
|
||||
<Iframe
|
||||
id="link"
|
||||
url={fullMetamigoURL}
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameBorder={0}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
16
apps/link/app/(main)/admin/metamigo/[...path]/page.tsx
Normal file
16
apps/link/app/(main)/admin/metamigo/[...path]/page.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { Metadata } from "next";
|
||||
import { MetamigoWrapper } from "./_components/MetamigoWrapper";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Metamigo",
|
||||
};
|
||||
|
||||
type PageProps = {
|
||||
params: {
|
||||
path: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function Page({ params: { path } }: PageProps) {
|
||||
return <MetamigoWrapper path={path} />;
|
||||
}
|
||||
10
apps/link/app/(main)/admin/zammad/page.tsx
Normal file
10
apps/link/app/(main)/admin/zammad/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Metadata } from "next";
|
||||
import { ZammadWrapper } from "../../../(main)/_components/ZammadWrapper";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Zammad",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return <ZammadWrapper path="/#manage" hideSidebar={false} />;
|
||||
}
|
||||
22
apps/link/app/(main)/api/auth/[...nextauth]/route.ts
Normal file
22
apps/link/app/(main)/api/auth/[...nextauth]/route.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import NextAuth from "next-auth";
|
||||
import Google from "next-auth/providers/google";
|
||||
// import Apple from "next-auth/providers/apple";
|
||||
|
||||
const handler = NextAuth({
|
||||
providers: [
|
||||
Google({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
}),
|
||||
/*
|
||||
Apple({
|
||||
clientId: process.env.APPLE_CLIENT_ID,
|
||||
clientSecret: process.env.APPLE_CLIENT_SECRET
|
||||
}),
|
||||
*/
|
||||
],
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
});
|
||||
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
7
apps/link/app/(main)/api/v1/users/route.ts
Normal file
7
apps/link/app/(main)/api/v1/users/route.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const handler = (req: NextRequest) => {
|
||||
NextResponse.redirect('/proxy/zammad/api/v1' + req.url.substring('/api/v1'.length));
|
||||
};
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
10
apps/link/app/(main)/knowledge/page.tsx
Normal file
10
apps/link/app/(main)/knowledge/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Metadata } from "next";
|
||||
import { ZammadWrapper } from "../../(main)/_components/ZammadWrapper";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Knowledge Base",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return <ZammadWrapper path="/#knowledge_base/1/locale/en-us" />;
|
||||
}
|
||||
15
apps/link/app/(main)/layout.tsx
Normal file
15
apps/link/app/(main)/layout.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { ReactNode } from "react";
|
||||
import { Metadata } from "next";
|
||||
import { InternalLayout } from "./_components/InternalLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Link",
|
||||
};
|
||||
|
||||
type LayoutProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function Layout({ children }: LayoutProps) {
|
||||
return <InternalLayout>{children}</InternalLayout>;
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import getConfig from "next/config";
|
||||
import { Grid } from "@mui/material";
|
||||
import Iframe from "react-iframe";
|
||||
|
||||
type LeafcutterWrapperProps = {
|
||||
path: string;
|
||||
};
|
||||
|
||||
export const LeafcutterWrapper: FC<LeafcutterWrapperProps> = ({ path }) => {
|
||||
const {
|
||||
publicRuntimeConfig: { linkURL },
|
||||
} = getConfig();
|
||||
const fullLeafcutterURL = `${linkURL}/proxy/leafcutter/${path}`;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
sx={{ height: "100%", width: "100%" }}
|
||||
direction="column"
|
||||
>
|
||||
<Grid item sx={{ height: "100vh", width: "100%" }}>
|
||||
<Iframe
|
||||
id="leafcutter"
|
||||
url={fullLeafcutterURL}
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameBorder={0}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
11
apps/link/app/(main)/leafcutter/[view]/page.tsx
Normal file
11
apps/link/app/(main)/leafcutter/[view]/page.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { LeafcutterWrapper } from "./_components/LeafcutterWrapper";
|
||||
|
||||
type PageProps = {
|
||||
params: {
|
||||
view: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function Page({ params: { view } }: PageProps) {
|
||||
<LeafcutterWrapper path={view} />;
|
||||
}
|
||||
5
apps/link/app/(main)/leafcutter/page.tsx
Normal file
5
apps/link/app/(main)/leafcutter/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
redirect("/leafcutter/home");
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
"use client";
|
||||
|
||||
import { FC } 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";
|
||||
|
||||
interface TicketListProps {
|
||||
title: string;
|
||||
tickets: any;
|
||||
}
|
||||
|
||||
export const TicketList: FC<TicketListProps> = ({ title, tickets }) => {
|
||||
const router = useRouter();
|
||||
let gridColumns: GridColDef[] = [
|
||||
{
|
||||
field: "number",
|
||||
headerName: "Number",
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: "title",
|
||||
headerName: "Title",
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: "customer",
|
||||
headerName: "Sender",
|
||||
valueGetter: (params) => params.row?.customer?.fullname,
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: "group",
|
||||
headerName: "Group",
|
||||
valueGetter: (params) => params.row?.group?.name,
|
||||
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>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button href="/tickets/create" text="Create" color="#1982FC" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<StyledDataGrid
|
||||
name={title}
|
||||
columns={gridColumns}
|
||||
rows={tickets}
|
||||
onRowClick={rowClick}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import useSWR from "swr";
|
||||
import { TicketList } from "./TicketList";
|
||||
import { getTicketsByOverviewQuery } from "../../../../_graphql/getTicketsByOverviewQuery";
|
||||
|
||||
type ZammadOverviewProps = {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const ZammadOverview: FC<ZammadOverviewProps> = ({ name, id }) => {
|
||||
const { data: ticketData, error: ticketError }: any = useSWR(
|
||||
{
|
||||
document: getTicketsByOverviewQuery,
|
||||
variables: { overviewId: `gid://zammad/Overview/${id}` },
|
||||
},
|
||||
{ refreshInterval: 10000 },
|
||||
);
|
||||
|
||||
const shouldRender = !ticketError && ticketData;
|
||||
const tickets =
|
||||
ticketData?.ticketsByOverview?.edges.map((edge: any) => edge.node) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
{shouldRender && <TicketList title={name} tickets={tickets} />}
|
||||
{ticketError && <div>{ticketError.toString()}</div>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
11
apps/link/app/(main)/overview/[overview]/error.tsx
Normal file
11
apps/link/app/(main)/overview/[overview]/error.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import { DisplayError } from "../../../_components/DisplayError";
|
||||
|
||||
type PageProps = {
|
||||
error: Error;
|
||||
};
|
||||
|
||||
export default function Page({ error }: PageProps) {
|
||||
return <DisplayError error={error} />;
|
||||
}
|
||||
41
apps/link/app/(main)/overview/[overview]/page.tsx
Normal file
41
apps/link/app/(main)/overview/[overview]/page.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { Metadata } from "next";
|
||||
import { ZammadOverview } from "./_components/ZammadOverview";
|
||||
|
||||
const getSection = (overview: string) => {
|
||||
return overview.charAt(0).toUpperCase() + overview.slice(1);
|
||||
};
|
||||
|
||||
type MetadataProps = {
|
||||
params: {
|
||||
overview: string;
|
||||
};
|
||||
};
|
||||
|
||||
export async function generateMetadata({
|
||||
params: { overview },
|
||||
}: MetadataProps): Promise<Metadata> {
|
||||
const section = getSection(overview);
|
||||
|
||||
return {
|
||||
title: `Link - ${section} Tickets`,
|
||||
};
|
||||
}
|
||||
|
||||
const overviews = {
|
||||
assigned: 1,
|
||||
unassigned: 2,
|
||||
pending: 3,
|
||||
urgent: 7,
|
||||
};
|
||||
|
||||
type PageProps = {
|
||||
params: {
|
||||
overview: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function Page({ params: { overview } }: PageProps) {
|
||||
const section = getSection(overview);
|
||||
|
||||
return <ZammadOverview name={section} id={overviews[overview]} />;
|
||||
}
|
||||
10
apps/link/app/(main)/page.tsx
Normal file
10
apps/link/app/(main)/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Metadata } from "next";
|
||||
import { Home } from "./_components/Home";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Link",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return <Home />;
|
||||
}
|
||||
10
apps/link/app/(main)/profile/page.tsx
Normal file
10
apps/link/app/(main)/profile/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Metadata } from "next";
|
||||
import { ZammadWrapper } from "../../(main)/_components/ZammadWrapper";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Profile",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return <ZammadWrapper path="/#profile" hideSidebar={false} />;
|
||||
}
|
||||
14
apps/link/app/(main)/setup/_components/Setup.tsx
Normal file
14
apps/link/app/(main)/setup/_components/Setup.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useLayoutEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ZammadWrapper } from "../../../(main)/_components/ZammadWrapper";
|
||||
|
||||
export const Setup: FC = () => {
|
||||
const router = useRouter();
|
||||
useLayoutEffect(() => {
|
||||
setTimeout(() => router.push("/"), 4000);
|
||||
}, [router]);
|
||||
|
||||
return <ZammadWrapper path="/auth/sso" hideSidebar={false} />;
|
||||
};
|
||||
10
apps/link/app/(main)/setup/page.tsx
Normal file
10
apps/link/app/(main)/setup/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Metadata } from "next";
|
||||
import { Setup } from "./_components/Setup";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Link",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return <Setup />;
|
||||
}
|
||||
|
|
@ -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 "../../../../../_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>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import { getTicketQuery } from "../../../../../_graphql/getTicketQuery";
|
||||
import { getTicketArticlesQuery } from "../../../../../_graphql/getTicketArticlesQuery";
|
||||
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 },
|
||||
);
|
||||
const { data: ticketArticlesData, error: ticketArticlesError }: any = useSWR(
|
||||
{
|
||||
document: getTicketArticlesQuery,
|
||||
variables: { ticketId: `gid://zammad/Ticket/${id}` },
|
||||
},
|
||||
{ refreshInterval: 2000 },
|
||||
);
|
||||
|
||||
const ticket = ticketData?.ticket;
|
||||
const ticketArticles = ticketArticlesData?.ticketArticles;
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [articleKind, setArticleKind] = useState<"reply" | "note">("reply");
|
||||
const closeDialog = () => setDialogOpen(false);
|
||||
|
||||
const shouldRender =
|
||||
ticketData && !ticketError && ticketArticlesData && !ticketArticlesError;
|
||||
|
||||
return (
|
||||
shouldRender && (
|
||||
<Box sx={{ height: "100%", width: "100%" }}>
|
||||
<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 }}>
|
||||
{ticketArticles.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}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
};
|
||||
11
apps/link/app/(main)/tickets/[id]/@detail/page.tsx
Normal file
11
apps/link/app/(main)/tickets/[id]/@detail/page.tsx
Normal 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} />;
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import {
|
||||
Grid,
|
||||
Box,
|
||||
// Typography,
|
||||
// TextField,
|
||||
// Stack,
|
||||
Select,
|
||||
MenuItem,
|
||||
} 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 "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
|
||||
|
||||
interface TicketEditProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
||||
const [selectedGroup, setSelectedGroup] = useState("");
|
||||
const [selectedOwner, setSelectedOwner] = useState("");
|
||||
const [selectedPriority, setSelectedPriority] = useState("");
|
||||
const [selectedState, setSelectedState] = useState("");
|
||||
const [selectedTags, setSelectedTags] = useState([]);
|
||||
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);
|
||||
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 { fetcher } = useSWRConfig();
|
||||
const { data: ticketData, error: ticketError }: any = useSWR(
|
||||
{
|
||||
document: getTicketQuery,
|
||||
variables: { ticketId: `gid://zammad/Ticket/${id}` },
|
||||
},
|
||||
{ refreshInterval: 100000 },
|
||||
);
|
||||
useEffect(() => {
|
||||
const ticket = ticketData?.ticket;
|
||||
if (ticket) {
|
||||
const groupID = ticket.group.id?.split("/").pop();
|
||||
setSelectedGroup(groupID);
|
||||
const ownerID = ticket.owner.id?.split("/").pop();
|
||||
setSelectedOwner(ownerID);
|
||||
const priorityID = ticket.priority.id?.split("/").pop();
|
||||
setSelectedPriority(priorityID);
|
||||
const stateID = ticket.state.id?.split("/").pop();
|
||||
setSelectedState(stateID);
|
||||
setSelectedTags(ticket.tags);
|
||||
}
|
||||
}, [ticketData, ticketError]);
|
||||
const updateTicket = async () => {
|
||||
const input = {
|
||||
ownerId: `gid://zammad/User/${selectedOwner}`,
|
||||
priorityId: `gid://zammad/Ticket::Priority/${selectedPriority}`,
|
||||
stateId: `gid://zammad/Ticket::State/${selectedState}`,
|
||||
groupId: `gid://zammad/Group/${selectedGroup}`,
|
||||
// tags: selectedTags,
|
||||
};
|
||||
const res = await fetcher({
|
||||
document: updateTicketMutation,
|
||||
variables: {
|
||||
ticketId: `gid://zammad/Ticket/${id}`,
|
||||
input,
|
||||
},
|
||||
});
|
||||
console.log({ res });
|
||||
};
|
||||
const shouldRender = ticketData && !ticketError;
|
||||
|
||||
return (
|
||||
shouldRender && (
|
||||
<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);
|
||||
updateTicket();
|
||||
}}
|
||||
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);
|
||||
updateTicket();
|
||||
}}
|
||||
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>
|
||||
<MuiChipsInput
|
||||
sx={{ backgroundColor: "white", width: "100%" }}
|
||||
value={selectedTags}
|
||||
onChange={(tags: any) => {
|
||||
setSelectedTags(tags);
|
||||
updateTicket();
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
};
|
||||
11
apps/link/app/(main)/tickets/[id]/@edit/page.tsx
Normal file
11
apps/link/app/(main)/tickets/[id]/@edit/page.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { TicketEdit } from "./_components/TicketEdit";
|
||||
|
||||
type PageProps = {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function Page({ params: { id } }: PageProps) {
|
||||
return <TicketEdit id={id} />;
|
||||
}
|
||||
11
apps/link/app/(main)/tickets/[id]/error.tsx
Normal file
11
apps/link/app/(main)/tickets/[id]/error.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import { DisplayError } from "../../../_components/DisplayError";
|
||||
|
||||
type PageProps = {
|
||||
error: Error;
|
||||
};
|
||||
|
||||
export default function Page({ error }: PageProps) {
|
||||
return <DisplayError error={error} />;
|
||||
}
|
||||
24
apps/link/app/(main)/tickets/[id]/layout.tsx
Normal file
24
apps/link/app/(main)/tickets/[id]/layout.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
"use client";
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
15
apps/link/app/(main)/tickets/[id]/not-found.tsx
Normal file
15
apps/link/app/(main)/tickets/[id]/not-found.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
62
apps/link/app/(main)/tickets/[id]/notpage.tsx
Normal file
62
apps/link/app/(main)/tickets/[id]/notpage.tsx
Normal 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 "../_components/TicketDetail";
|
||||
import { TicketEdit } from "../_components/TicketEdit";
|
||||
import { getTicketQuery } from "../_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;
|
||||
*/
|
||||
Loading…
Add table
Add a link
Reference in a new issue