Use server actions instead of client-side API calls
This commit is contained in:
parent
5a3127dcb0
commit
aa453954ed
30 changed files with 703 additions and 462 deletions
|
|
@ -20,11 +20,12 @@ import {
|
||||||
WhatsApp as WhatsAppIcon,
|
WhatsApp as WhatsAppIcon,
|
||||||
Facebook as FacebookIcon,
|
Facebook as FacebookIcon,
|
||||||
AirlineStops as AirlineStopsIcon,
|
AirlineStops as AirlineStopsIcon,
|
||||||
|
Logout as LogoutIcon,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { typography, fonts } from "@link-stack/ui";
|
import { typography, fonts, Button } from "@link-stack/ui";
|
||||||
import LinkLogo from "@/app/_images/link-logo-small.png";
|
import LinkLogo from "@/app/_images/link-logo-small.png";
|
||||||
import { useSession, signOut } from "next-auth/react";
|
import { useSession, signOut } from "next-auth/react";
|
||||||
|
|
||||||
|
|
@ -161,9 +162,9 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const user = session?.user;
|
const user = session?.user;
|
||||||
|
|
||||||
// const logout = () => {
|
const logout = () => {
|
||||||
// signOut({ callbackUrl: "/login" });
|
signOut({ callbackUrl: "/login" });
|
||||||
// };
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
|
|
@ -377,7 +378,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
{open && (
|
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|
@ -388,7 +389,9 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
{user?.email}
|
{user?.email}
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
<Grid item>
|
||||||
|
<Button text="Logout" kind="secondary" onClick={logout} />
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Kysely } from "kysely";
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema.alterTable("User").addColumn("role", "text").execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema.alterTable("User").dropColumn("role").execute();
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,6 @@ const fetchSignalMessagesTask = async (): Promise<void> => {
|
||||||
const messages = await messagesClient.v1ReceiveNumberGet({ number });
|
const messages = await messagesClient.v1ReceiveNumberGet({ number });
|
||||||
|
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
console.log(msg);
|
|
||||||
const { envelope } = msg as any;
|
const { envelope } = msg as any;
|
||||||
const { source, sourceUuid, dataMessage } = envelope;
|
const { source, sourceUuid, dataMessage } = envelope;
|
||||||
const message = dataMessage?.message;
|
const message = dataMessage?.message;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import { FC } from "react";
|
||||||
import { OpenSearchWrapper } from "@link-stack/leafcutter-ui";
|
import { OpenSearchWrapper } from "@link-stack/leafcutter-ui";
|
||||||
|
|
||||||
export const Home: FC = () => (
|
export const Home: FC = () => (
|
||||||
<OpenSearchWrapper url="/app/visualize#/edit/237b8f00-e6a0-11ee-94b3-d7b7409294e7?embed=true" marginTop="0"
|
<OpenSearchWrapper
|
||||||
|
url="/app/visualize#/edit/237b8f00-e6a0-11ee-94b3-d7b7409294e7?embed=true"
|
||||||
|
marginTop="0"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,23 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC, PropsWithChildren, useState } from "react";
|
import { FC, PropsWithChildren, useState } from "react";
|
||||||
import { Grid } from "@mui/material";
|
import { Grid, Box } from "@mui/material";
|
||||||
import { Sidebar } from "./Sidebar";
|
import { Sidebar } from "./Sidebar";
|
||||||
|
import { SetupModeWarning } from "./SetupModeWarning";
|
||||||
|
|
||||||
export const InternalLayout: FC<PropsWithChildren> = ({ children }) => {
|
interface InternalLayoutProps extends PropsWithChildren {
|
||||||
|
setupModeActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InternalLayout: FC<InternalLayoutProps> = ({
|
||||||
|
children,
|
||||||
|
setupModeActive,
|
||||||
|
}) => {
|
||||||
const [open, setOpen] = useState(true);
|
const [open, setOpen] = useState(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Box sx={{ position: "relative" }}>
|
||||||
|
<SetupModeWarning setupModeActive={setupModeActive} />
|
||||||
<Grid container direction="row">
|
<Grid container direction="row">
|
||||||
<Sidebar open={open} setOpen={setOpen} />
|
<Sidebar open={open} setOpen={setOpen} />
|
||||||
<Grid
|
<Grid
|
||||||
|
|
@ -17,5 +27,6 @@ export const InternalLayout: FC<PropsWithChildren> = ({ children }) => {
|
||||||
{children as any}
|
{children as any}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { FC, useState, useEffect } from "react";
|
import { FC, useState, useEffect } from "react";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import useSWR from "swr";
|
|
||||||
import { Grid, Box, TextField, Autocomplete } from "@mui/material";
|
import { Grid, Box, TextField, Autocomplete } from "@mui/material";
|
||||||
import { searchQuery } from "@/app/_graphql/searchQuery";
|
import { searchAllAction } from "@/app/_actions/search";
|
||||||
import { colors } from "@link-stack/ui";
|
import { colors } from "@link-stack/ui";
|
||||||
|
|
||||||
type SearchResultProps = {
|
type SearchResultProps = {
|
||||||
|
|
@ -42,8 +41,6 @@ const SearchInput = (params: any) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
const SearchResult: FC<SearchResultProps> = ({ props, option }) => {
|
const SearchResult: FC<SearchResultProps> = ({ props, option }) => {
|
||||||
console.log({ option });
|
|
||||||
|
|
||||||
const { lightGrey, mediumGray, black, white } = colors;
|
const { lightGrey, mediumGray, black, white } = colors;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -95,22 +92,20 @@ const SearchResult: FC<SearchResultProps> = ({ props, option }) => {
|
||||||
|
|
||||||
export const SearchBox: FC = () => {
|
export const SearchBox: FC = () => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [searchResults, setSearchResults] = useState([]);
|
||||||
const [selectedValue, setSelectedValue] = useState(null);
|
const [selectedValue, setSelectedValue] = useState(null);
|
||||||
const [searchTerms, setSearchTerms] = useState("");
|
const [searchTerms, setSearchTerms] = useState("");
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data, error }: any = useSWR({
|
|
||||||
document: searchQuery,
|
|
||||||
variables: {
|
|
||||||
search: searchTerms ?? "",
|
|
||||||
limit: 50,
|
|
||||||
},
|
|
||||||
refreshInterval: 10000,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOpen(false);
|
const fetchSearchResults = async () => {
|
||||||
}, [pathname]);
|
const results = await searchAllAction(searchTerms ?? "", 50);
|
||||||
|
setSearchResults(results);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchSearchResults();
|
||||||
|
}, [searchTerms]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
|
|
@ -132,7 +127,7 @@ export const SearchBox: FC = () => {
|
||||||
open={open}
|
open={open}
|
||||||
onOpen={() => setOpen(true)}
|
onOpen={() => setOpen(true)}
|
||||||
noOptionsText="No results"
|
noOptionsText="No results"
|
||||||
options={data?.search ?? []}
|
options={searchResults ?? []}
|
||||||
getOptionLabel={(option: any) => {
|
getOptionLabel={(option: any) => {
|
||||||
if (option) {
|
if (option) {
|
||||||
return option.title;
|
return option.title;
|
||||||
|
|
|
||||||
29
apps/link/app/(main)/_components/SetupModeWarning.tsx
Normal file
29
apps/link/app/(main)/_components/SetupModeWarning.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
|
||||||
|
interface SetupModeWarningProps {
|
||||||
|
setupModeActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SetupModeWarning: FC<SetupModeWarningProps> = ({
|
||||||
|
setupModeActive,
|
||||||
|
}) =>
|
||||||
|
setupModeActive ? (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "red",
|
||||||
|
textAlign: "center",
|
||||||
|
zIndex: 10000,
|
||||||
|
position: "absolute",
|
||||||
|
width: "100vw",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box component="h2" sx={{ color: "white", m: 0, p: 0 }}>
|
||||||
|
Setup Mode Active
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
) : null;
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import useSWR from "swr";
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Grid,
|
Grid,
|
||||||
|
|
@ -33,7 +32,7 @@ import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import LinkLogo from "public/link-logo-small.png";
|
import LinkLogo from "public/link-logo-small.png";
|
||||||
import { useSession, signOut } from "next-auth/react";
|
import { useSession, signOut } from "next-auth/react";
|
||||||
import { getTicketOverviewCountsQuery } from "app/_graphql/getTicketOverviewCountsQuery";
|
import { getOverviewTicketCountsAction } from "app/_actions/overviews";
|
||||||
import { SearchBox } from "./SearchBox";
|
import { SearchBox } from "./SearchBox";
|
||||||
import { fonts } from "@link-stack/ui";
|
import { fonts } from "@link-stack/ui";
|
||||||
|
|
||||||
|
|
@ -185,33 +184,32 @@ interface SidebarProps {
|
||||||
export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
const [overviewCounts, setOverviewCounts] = useState<any>(null);
|
||||||
const { poppins } = fonts;
|
const { poppins } = fonts;
|
||||||
const username = session?.user?.name || "User";
|
const username = session?.user?.name || "User";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const roles = session?.user?.roles || [];
|
const roles = session?.user?.roles || [];
|
||||||
const { data: overviewData, error: overviewError }: any = useSWR(
|
const leafcutterEnabled = false;
|
||||||
{
|
|
||||||
document: getTicketOverviewCountsQuery,
|
useEffect(() => {
|
||||||
},
|
const fetchCounts = async () => {
|
||||||
{ refreshInterval: 10000 },
|
const counts = await getOverviewTicketCountsAction();
|
||||||
);
|
console.log({ counts });
|
||||||
const findOverviewByName = (name: string) =>
|
setOverviewCounts(counts);
|
||||||
overviewData?.ticketOverviews?.edges?.find(
|
};
|
||||||
(overview: any) => overview.node.name === name,
|
|
||||||
)?.node?.id;
|
fetchCounts();
|
||||||
const findOverviewCountByID = (id: string) =>
|
|
||||||
overviewData?.ticketOverviews?.edges?.find(
|
const interval = setInterval(fetchCounts, 10000);
|
||||||
(overview: any) => overview.node.id === id,
|
|
||||||
)?.node?.ticketCount ?? 0;
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const recentCount = 0;
|
const recentCount = 0;
|
||||||
const assignedID = findOverviewByName("My Assigned Tickets");
|
const assignedCount = overviewCounts?.["My Assigned Tickets"] ?? 0;
|
||||||
const assignedCount = findOverviewCountByID(assignedID);
|
const openCount = overviewCounts?.["Open Tickets"] ?? 0;
|
||||||
const openID = findOverviewByName("Open Tickets");
|
const urgentCount = overviewCounts?.["Escalated Tickets"] ?? 0;
|
||||||
const openCount = findOverviewCountByID(openID);
|
const unassignedCount = overviewCounts?.["Unassigned & Open Tickets"] ?? 0;
|
||||||
const urgentID = findOverviewByName("Escalated Tickets");
|
|
||||||
const urgentCount = findOverviewCountByID(urgentID);
|
|
||||||
const unassignedID = findOverviewByName("Unassigned & Open Tickets");
|
|
||||||
const unassignedCount = findOverviewCountByID(unassignedID);
|
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
signOut({ callbackUrl: "/login" });
|
signOut({ callbackUrl: "/login" });
|
||||||
|
|
@ -404,6 +402,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{leafcutterEnabled && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
name="Home"
|
name="Home"
|
||||||
href="/"
|
href="/"
|
||||||
|
|
@ -412,6 +411,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
selected={pathname.endsWith("/")}
|
selected={pathname.endsWith("/")}
|
||||||
open={open}
|
open={open}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
name="Tickets"
|
name="Tickets"
|
||||||
href="/overview/recent"
|
href="/overview/recent"
|
||||||
|
|
@ -504,6 +504,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
selected={pathname.endsWith("/reporting")}
|
selected={pathname.endsWith("/reporting")}
|
||||||
open={open}
|
open={open}
|
||||||
/>
|
/>
|
||||||
|
{leafcutterEnabled && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
name="Leafcutter"
|
name="Leafcutter"
|
||||||
href="/leafcutter"
|
href="/leafcutter"
|
||||||
|
|
@ -512,6 +513,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
selected={false}
|
selected={false}
|
||||||
open={open}
|
open={open}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<Collapse
|
<Collapse
|
||||||
in={pathname.startsWith("/leafcutter")}
|
in={pathname.startsWith("/leafcutter")}
|
||||||
timeout="auto"
|
timeout="auto"
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,11 @@ type LayoutProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Layout({ children }: LayoutProps) {
|
export default function Layout({ children }: LayoutProps) {
|
||||||
return <InternalLayout>{children}</InternalLayout>;
|
const setupModeActive = process.env.SETUP_MODE === "true";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InternalLayout setupModeActive={setupModeActive}>
|
||||||
|
{children}
|
||||||
|
</InternalLayout>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,19 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC } from "react";
|
import { FC, useState, useEffect } from "react";
|
||||||
import { useFormState } from "react-dom";
|
import { useFormState } from "react-dom";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import { Grid } from "@mui/material";
|
import { Grid } from "@mui/material";
|
||||||
import { Dialog, Button, TextField, Autocomplete } from "@link-stack/ui";
|
import {
|
||||||
|
Dialog,
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
Autocomplete,
|
||||||
|
Select,
|
||||||
|
} from "@link-stack/ui";
|
||||||
import { createTicketAction } from "app/_actions/tickets";
|
import { createTicketAction } from "app/_actions/tickets";
|
||||||
import useSWR from "swr";
|
import { getCustomersAction } from "app/_actions/users";
|
||||||
|
import { getGroupsAction } from "app/_actions/groups";
|
||||||
|
|
||||||
interface TicketCreateDialogProps {
|
interface TicketCreateDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -16,6 +24,8 @@ export const TicketCreateDialog: FC<TicketCreateDialogProps> = ({
|
||||||
open,
|
open,
|
||||||
closeDialog,
|
closeDialog,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [customers, setCustomers] = useState([]);
|
||||||
|
const [groups, setGroups] = useState([]);
|
||||||
const initialState = {
|
const initialState = {
|
||||||
messages: [],
|
messages: [],
|
||||||
errors: [],
|
errors: [],
|
||||||
|
|
@ -38,16 +48,41 @@ export const TicketCreateDialog: FC<TicketCreateDialogProps> = ({
|
||||||
createTicketAction,
|
createTicketAction,
|
||||||
initialState,
|
initialState,
|
||||||
);
|
);
|
||||||
const { data: users, error: usersError }: any = useSWR({
|
const [liveFormState, setLiveFormState] = useState(formState);
|
||||||
url: "/api/v1/users",
|
const updateFormState = (field: string, value: any) => {
|
||||||
method: "GET",
|
console.log({ value });
|
||||||
});
|
const newState = { ...liveFormState };
|
||||||
const customers =
|
newState.values[field] = value;
|
||||||
users?.filter((user: any) => user.role_ids.includes(3)) ?? [];
|
setLiveFormState(newState);
|
||||||
const formattedCustomers = customers.map((customer: any) => ({
|
};
|
||||||
label: customer.login,
|
|
||||||
id: `${customer.id}`,
|
useEffect(() => {
|
||||||
}));
|
const fetchUsers = async () => {
|
||||||
|
const result = await getCustomersAction();
|
||||||
|
console.log({ result });
|
||||||
|
setCustomers(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUsers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchGroups = async () => {
|
||||||
|
const result = await getGroupsAction();
|
||||||
|
console.log({ result });
|
||||||
|
setGroups(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchGroups();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (formState.success) {
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
|
}, [formState.success, router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|
@ -74,11 +109,21 @@ export const TicketCreateDialog: FC<TicketCreateDialogProps> = ({
|
||||||
>
|
>
|
||||||
<Grid container direction="column" spacing={3}>
|
<Grid container direction="column" spacing={3}>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Autocomplete
|
<Select
|
||||||
|
name="groupId"
|
||||||
|
label="Group"
|
||||||
|
getOptions={() => groups as any}
|
||||||
|
formState={liveFormState}
|
||||||
|
updateFormState={updateFormState}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Select
|
||||||
name="customerId"
|
name="customerId"
|
||||||
label="Customer"
|
label="Customer"
|
||||||
options={formattedCustomers}
|
getOptions={() => customers as any}
|
||||||
formState={formState}
|
formState={liveFormState}
|
||||||
|
updateFormState={updateFormState}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
|
|
|
||||||
|
|
@ -1,113 +1,29 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import useSWR from "swr";
|
import { getOverviewTicketsAction } from "app/_actions/overviews";
|
||||||
|
|
||||||
import { TicketList } from "./TicketList";
|
import { TicketList } from "./TicketList";
|
||||||
import { getTicketOverviewCountsQuery } from "app/_graphql/getTicketOverviewCountsQuery";
|
|
||||||
import { getTicketsByOverviewQuery } from "app/_graphql/getTicketsByOverviewQuery";
|
|
||||||
|
|
||||||
type ZammadOverviewProps = {
|
type ZammadOverviewProps = {
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZammadOverview: FC<ZammadOverviewProps> = ({ name }) => {
|
export const ZammadOverview: FC<ZammadOverviewProps> = ({ name }) => {
|
||||||
const [overviewID, setOverviewID] = useState(null);
|
|
||||||
const [tickets, setTickets] = useState([]);
|
const [tickets, setTickets] = useState([]);
|
||||||
const [error, setError] = useState(null);
|
|
||||||
|
|
||||||
const { data: overviewData, error: overviewError }: any = useSWR(
|
|
||||||
{
|
|
||||||
document: getTicketOverviewCountsQuery,
|
|
||||||
},
|
|
||||||
{ refreshInterval: 10000 },
|
|
||||||
);
|
|
||||||
const { data: ticketData, error: ticketError }: any = useSWR(
|
|
||||||
{
|
|
||||||
document: getTicketsByOverviewQuery,
|
|
||||||
variables: { overviewId: overviewID, pageSize: 250 },
|
|
||||||
},
|
|
||||||
{ refreshInterval: 10000 },
|
|
||||||
);
|
|
||||||
const overviewLookup = {
|
|
||||||
Assigned: "My Assigned Tickets",
|
|
||||||
Open: "Open Tickets",
|
|
||||||
Urgent: "Escalated Tickets",
|
|
||||||
Unassigned: "Unassigned & Open Tickets",
|
|
||||||
};
|
|
||||||
|
|
||||||
const findOverviewByName = (name: string) => {
|
|
||||||
const fullName = overviewLookup[name];
|
|
||||||
return overviewData?.ticketOverviews?.edges?.find(
|
|
||||||
(overview: any) => overview.node.name === fullName,
|
|
||||||
)?.node?.id;
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
if (overviewData) {
|
|
||||||
setOverviewID(findOverviewByName(name));
|
|
||||||
}
|
|
||||||
}, [overviewData, name]);
|
|
||||||
console.log({
|
|
||||||
name,
|
|
||||||
overviewID,
|
|
||||||
overviewData,
|
|
||||||
overviewError,
|
|
||||||
ticketData,
|
|
||||||
ticketError,
|
|
||||||
});
|
|
||||||
|
|
||||||
const restFetcher = (url: string) => fetch(url).then((r) => r.json());
|
|
||||||
const { data: recent } = useSWR("/api/v1/recent_view", restFetcher);
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (name != "Recent") {
|
const fetchTickets = async () => {
|
||||||
const edges = ticketData?.ticketsByOverview?.edges;
|
const { tickets } = await getOverviewTicketsAction(name);
|
||||||
if (edges) {
|
setTickets(tickets);
|
||||||
const nodes = edges.map((edge: any) => edge.node);
|
|
||||||
console.log({ nodes });
|
|
||||||
setError(null);
|
|
||||||
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();
|
|
||||||
|
fetchTickets();
|
||||||
|
|
||||||
|
const interval = setInterval(fetchTickets, 20000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
}, [name]);
|
}, [name]);
|
||||||
|
|
||||||
const shouldRender = tickets && !error;
|
return <TicketList title={name} tickets={tickets} />;
|
||||||
|
|
||||||
return shouldRender && <TicketList title={name} tickets={tickets} />;
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export async function generateMetadata({
|
||||||
const section = getSection(overview);
|
const section = getSection(overview);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `Link - ${section} Tickets`,
|
title: `CDR Link - ${section} Tickets`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
import { getServerSession } from "app/_lib/authentication";
|
import { getServerSession } from "app/_lib/authentication";
|
||||||
import { Home } from "@link-stack/leafcutter-ui";
|
import { Home } from "@link-stack/leafcutter-ui";
|
||||||
import { getUserVisualizations } from "@link-stack/opensearch-common";
|
import { getUserVisualizations } from "@link-stack/opensearch-common";
|
||||||
|
|
@ -13,11 +14,14 @@ export default async function Page() {
|
||||||
const {
|
const {
|
||||||
user: { email },
|
user: { email },
|
||||||
}: any = session;
|
}: any = session;
|
||||||
const visualizations = await getUserVisualizations(email ?? "none", 20);
|
// const visualizations = await getUserVisualizations(email ?? "none", 20);
|
||||||
|
|
||||||
|
redirect("/overview/recent");
|
||||||
|
/*
|
||||||
return (
|
return (
|
||||||
<LeafcutterWrapper>
|
<LeafcutterWrapper>
|
||||||
<Home visualizations={visualizations} showWelcome={false} />
|
<Home visualizations={visualizations} showWelcome={false} />
|
||||||
</LeafcutterWrapper>
|
</LeafcutterWrapper>
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@ import {
|
||||||
DialogContent,
|
DialogContent,
|
||||||
TextField,
|
TextField,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useSWRConfig } from "swr";
|
import { createTicketArticleAction } from "app/_actions/tickets";
|
||||||
import { updateTicketMutation } from "app/_graphql/updateTicketMutation";
|
|
||||||
|
|
||||||
interface ArticleCreateDialogProps {
|
interface ArticleCreateDialogProps {
|
||||||
ticketID: string;
|
ticketID: string;
|
||||||
|
|
@ -30,7 +29,6 @@ export const ArticleCreateDialog: FC<ArticleCreateDialogProps> = ({
|
||||||
const [body, setBody] = useState("");
|
const [body, setBody] = useState("");
|
||||||
const backgroundColor = kind === "note" ? "#FFB620" : "#1982FC";
|
const backgroundColor = kind === "note" ? "#FFB620" : "#1982FC";
|
||||||
const color = kind === "note" ? "black" : "white";
|
const color = kind === "note" ? "black" : "white";
|
||||||
const { fetcher } = useSWRConfig();
|
|
||||||
const article = {
|
const article = {
|
||||||
body,
|
body,
|
||||||
type: kind,
|
type: kind,
|
||||||
|
|
@ -42,15 +40,7 @@ export const ArticleCreateDialog: FC<ArticleCreateDialogProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const createArticle = async () => {
|
const createArticle = async () => {
|
||||||
await fetcher({
|
await createTicketArticleAction(ticketID, article);
|
||||||
document: updateTicketMutation,
|
|
||||||
variables: {
|
|
||||||
ticketId: `gid://zammad/Ticket/${ticketID}`,
|
|
||||||
input: {
|
|
||||||
article,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
closeDialog();
|
closeDialog();
|
||||||
setBody("");
|
setBody("");
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC, useState } from "react";
|
import { FC, useState, useEffect } from "react";
|
||||||
import useSWR from "swr";
|
import { getTicketAction, getTicketArticlesAction } from "app/_actions/tickets";
|
||||||
import { getTicketQuery } from "app/_graphql/getTicketQuery";
|
|
||||||
import { getTicketArticlesQuery } from "app/_graphql/getTicketArticlesQuery";
|
|
||||||
import { Grid, Box, Typography } from "@mui/material";
|
import { Grid, Box, Typography } from "@mui/material";
|
||||||
import { Button, fonts, colors } from "@link-stack/ui";
|
import { Button, fonts, colors } from "@link-stack/ui";
|
||||||
|
|
||||||
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
|
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
|
||||||
import {
|
import {
|
||||||
MainContainer,
|
MainContainer,
|
||||||
|
|
@ -22,41 +19,46 @@ interface TicketDetailProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
|
export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
|
||||||
|
const [ticket, setTicket] = useState<any>(null);
|
||||||
|
const [ticketArticles, setTicketArticles] = useState<any>(null);
|
||||||
const { poppins, roboto } = fonts;
|
const { poppins, roboto } = fonts;
|
||||||
const { veryLightGray, lightGray } = colors;
|
const { veryLightGray, lightGray } = colors;
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [articleKind, setArticleKind] = useState("note");
|
const [articleKind, setArticleKind] = useState("note");
|
||||||
const { data: ticketData, error: ticketError }: any = useSWR(
|
|
||||||
{
|
|
||||||
document: getTicketQuery,
|
|
||||||
variables: { ticketId: `gid://zammad/Ticket/${id}` },
|
|
||||||
},
|
|
||||||
{ refreshInterval: 10000 },
|
|
||||||
);
|
|
||||||
const { data: ticketArticlesData, error: ticketArticlesError }: any = useSWR(
|
|
||||||
{
|
|
||||||
document: getTicketArticlesQuery,
|
|
||||||
variables: { ticketId: `gid://zammad/Ticket/${id}` },
|
|
||||||
},
|
|
||||||
{ refreshInterval: 2000 },
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: recentViewData, error: recentViewError }: any = useSWR({
|
useEffect(() => {
|
||||||
url: "/api/v1/recent_view",
|
const fetchTicket = async () => {
|
||||||
method: "POST",
|
const result = await getTicketAction(id);
|
||||||
body: JSON.stringify({ object: "Ticket", o_id: id }),
|
setTicket(result);
|
||||||
});
|
};
|
||||||
|
|
||||||
|
fetchTicket();
|
||||||
|
|
||||||
|
const interval = setInterval(fetchTicket, 20000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchTicketArticles = async () => {
|
||||||
|
const result = await getTicketArticlesAction(id);
|
||||||
|
setTicketArticles(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchTicketArticles();
|
||||||
|
|
||||||
|
const interval = setInterval(fetchTicketArticles, 2000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
const closeDialog = () => setDialogOpen(false);
|
const closeDialog = () => setDialogOpen(false);
|
||||||
|
|
||||||
const ticket = ticketData?.ticket;
|
|
||||||
const ticketArticles = ticketArticlesData?.ticketArticles;
|
|
||||||
const firstArticle = ticketArticles?.edges[0]?.node;
|
const firstArticle = ticketArticles?.edges[0]?.node;
|
||||||
const firstArticleKind = firstArticle?.type?.name ?? "phone";
|
const firstArticleKind = firstArticle?.type?.name ?? "phone";
|
||||||
const firstEmailSender = firstArticle?.from?.parsed?.[0]?.emailAddress ?? "";
|
const firstEmailSender = firstArticle?.from?.parsed?.[0]?.emailAddress ?? "";
|
||||||
const recipient = firstEmailSender;
|
const recipient = firstEmailSender;
|
||||||
const shouldRender =
|
const shouldRender = !!ticket && !!ticketArticles;
|
||||||
ticketData && !ticketError && ticketArticlesData && !ticketArticlesError;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
shouldRender && (
|
shouldRender && (
|
||||||
|
|
|
||||||
|
|
@ -5,57 +5,87 @@ import { Grid, Box, MenuItem } from "@mui/material";
|
||||||
import { useFormState } from "react-dom";
|
import { useFormState } from "react-dom";
|
||||||
import { Select, Button } from "@link-stack/ui";
|
import { Select, Button } from "@link-stack/ui";
|
||||||
import { MuiChipsInput } from "mui-chips-input";
|
import { MuiChipsInput } from "mui-chips-input";
|
||||||
import useSWR, { useSWRConfig } from "swr";
|
|
||||||
import { getTicketQuery } from "app/_graphql/getTicketQuery";
|
import { getTicketQuery } from "app/_graphql/getTicketQuery";
|
||||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||||
import { updateTicketAction } from "app/_actions/tickets";
|
import {
|
||||||
|
updateTicketAction,
|
||||||
|
getTicketAction,
|
||||||
|
getTicketStatesAction,
|
||||||
|
getTicketPrioritiesAction,
|
||||||
|
getTicketTagsAction,
|
||||||
|
} from "app/_actions/tickets";
|
||||||
|
import { getAgentsAction } from "app/_actions/users";
|
||||||
|
import { getGroupsAction } from "app/_actions/groups";
|
||||||
|
|
||||||
interface TicketEditProps {
|
interface TicketEditProps {
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
||||||
|
const [ticket, setTicket] = useState<any>();
|
||||||
|
const [ticketStates, setTicketStates] = useState<any>();
|
||||||
|
const [ticketPriorities, setTicketPriorities] = useState<any>();
|
||||||
|
const [groups, setGroups] = useState<any>();
|
||||||
|
const [tags, setTags] = useState<any>();
|
||||||
|
const [agents, setAgents] = useState<any>();
|
||||||
const selectedTags = [];
|
const selectedTags = [];
|
||||||
const pendingVisible = false;
|
const pendingVisible = false;
|
||||||
const pendingDate = new Date();
|
const pendingDate = new Date();
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
console.info("You clicked the delete icon.");
|
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 filteredStates = ticketStates?.filter(
|
||||||
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: recent } = useSWR("/api/v1/recent_view", restFetcher);
|
|
||||||
|
|
||||||
// const { data: tags } = useSWR("/api/v1/tags", restFetcher);
|
|
||||||
const filteredStates = states?.filter(
|
|
||||||
(state: any) => !["new", "merged", "removed"].includes(state.name),
|
(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(
|
|
||||||
{
|
|
||||||
document: getTicketQuery,
|
|
||||||
variables: { ticketId: `gid://zammad/Ticket/${id}` },
|
|
||||||
},
|
|
||||||
{ refreshInterval: 10000 },
|
|
||||||
);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ticket = ticketData?.ticket;
|
const fetchAgents = async () => {
|
||||||
if (ticket) {
|
const result = await getAgentsAction();
|
||||||
const groupID = ticket.group.id?.split("/").pop();
|
console.log({ result });
|
||||||
// setSelectedGroup(groupID);
|
setAgents(result);
|
||||||
const ownerID = ticket.owner.id?.split("/").pop();
|
};
|
||||||
// setSelectedOwner(ownerID);
|
|
||||||
const priorityID = ticket.priority.id?.split("/").pop();
|
const fetchGroups = async () => {
|
||||||
// setSelectedPriority(priorityID);
|
const result = await getGroupsAction();
|
||||||
const stateID = ticket.state.id?.split("/").pop();
|
console.log({ result });
|
||||||
// setSelectedState(stateID);
|
setGroups(result);
|
||||||
// setSelectedTags(ticket.tags);
|
};
|
||||||
}
|
|
||||||
}, [ticketData, ticketError]);
|
const fetchTicketStates = async () => {
|
||||||
|
const result = await getTicketStatesAction();
|
||||||
|
console.log({ result });
|
||||||
|
setTicketStates(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchTicketPriorities = async () => {
|
||||||
|
const result = await getTicketPrioritiesAction();
|
||||||
|
console.log({ result });
|
||||||
|
setTicketPriorities(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchTicketTags = async () => {
|
||||||
|
const result = await getTicketTagsAction();
|
||||||
|
console.log({ result });
|
||||||
|
setTags(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchTicketStates();
|
||||||
|
fetchTicketPriorities();
|
||||||
|
fetchTicketTags();
|
||||||
|
fetchAgents();
|
||||||
|
fetchGroups();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchTicket = async () => {
|
||||||
|
const result = await getTicketAction(id);
|
||||||
|
console.log({ result });
|
||||||
|
setTicket(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchTicket();
|
||||||
|
}, []);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -97,10 +127,10 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
||||||
errors: [],
|
errors: [],
|
||||||
values: {
|
values: {
|
||||||
customer: "",
|
customer: "",
|
||||||
group: "",
|
group: ticket.group.id?.split("/").pop(),
|
||||||
owner: "",
|
owner: ticket.owner.id?.split("/").pop(),
|
||||||
priority: "",
|
priority: ticket.priority.id?.split("/").pop(),
|
||||||
state: "",
|
state: ticket.state.id?.split("/").pop(),
|
||||||
tags: [],
|
tags: [],
|
||||||
title: "",
|
title: "",
|
||||||
article: {
|
article: {
|
||||||
|
|
@ -114,7 +144,7 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
||||||
updateTicketAction,
|
updateTicketAction,
|
||||||
initialState,
|
initialState,
|
||||||
);
|
);
|
||||||
const shouldRender = ticketData && !ticketError;
|
const shouldRender = !!ticket;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
shouldRender && (
|
shouldRender && (
|
||||||
|
|
@ -220,7 +250,7 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
||||||
label="Priority"
|
label="Priority"
|
||||||
formState={formState}
|
formState={formState}
|
||||||
getOptions={() =>
|
getOptions={() =>
|
||||||
priorities?.map((priority: any) => ({
|
ticketPriorities?.map((priority: any) => ({
|
||||||
value: priority.id,
|
value: priority.id,
|
||||||
label: priority.name,
|
label: priority.name,
|
||||||
})) ?? []
|
})) ?? []
|
||||||
|
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
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;
|
|
||||||
*/
|
|
||||||
16
apps/link/app/_actions/groups.ts
Normal file
16
apps/link/app/_actions/groups.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import { executeREST } from "app/_lib/zammad";
|
||||||
|
|
||||||
|
export const getGroupsAction = async () => {
|
||||||
|
const groups = await executeREST({
|
||||||
|
path: "/api/v1/groups",
|
||||||
|
});
|
||||||
|
const allGroups = groups ?? [];
|
||||||
|
const formattedGroups = allGroups.map((group: any) => ({
|
||||||
|
label: group.name,
|
||||||
|
value: `gid://zammad/Group/${group.id}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return formattedGroups;
|
||||||
|
};
|
||||||
82
apps/link/app/_actions/overviews.ts
Normal file
82
apps/link/app/_actions/overviews.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import { executeGraphQL } from "app/_lib/zammad";
|
||||||
|
import { getTicketOverviewCountsQuery } from "app/_graphql/getTicketOverviewCountsQuery";
|
||||||
|
import { getTicketsByOverviewQuery } from "app/_graphql/getTicketsByOverviewQuery";
|
||||||
|
|
||||||
|
export const getOverviewTicketCountsAction = async () => {
|
||||||
|
const countResult = await executeGraphQL({
|
||||||
|
query: getTicketOverviewCountsQuery,
|
||||||
|
});
|
||||||
|
const overviews = countResult?.ticketOverviews?.edges;
|
||||||
|
const counts = overviews?.reduce((acc: any, overview: any) => {
|
||||||
|
acc[overview.node.name] = overview.node.ticketCount;
|
||||||
|
return acc;
|
||||||
|
});
|
||||||
|
|
||||||
|
return counts;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOverviewTicketsAction = async (name: string) => {
|
||||||
|
let tickets = [];
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
if (name === "Recent") {
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.ZAMMAD_URL}/api/v1/recent_view`,
|
||||||
|
);
|
||||||
|
const recent = await response.json();
|
||||||
|
console.log({ recent });
|
||||||
|
|
||||||
|
for (const rec of recent) {
|
||||||
|
const res = await fetch(
|
||||||
|
`${process.env.ZAMMAD_URL}/api/v1/tickets/${rec.o_id}`,
|
||||||
|
);
|
||||||
|
const tkt = await res.json();
|
||||||
|
tickets.push({
|
||||||
|
...tkt,
|
||||||
|
internalId: tkt.id,
|
||||||
|
createdAt: tkt.created_at,
|
||||||
|
updatedAt: tkt.updated_at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const overviewLookup = {
|
||||||
|
Assigned: "My Assigned Tickets",
|
||||||
|
Open: "Open Tickets",
|
||||||
|
Urgent: "Escalated Tickets",
|
||||||
|
Unassigned: "Unassigned & Open Tickets",
|
||||||
|
};
|
||||||
|
const fullName = overviewLookup[name];
|
||||||
|
const countResult = await executeGraphQL({
|
||||||
|
query: getTicketOverviewCountsQuery,
|
||||||
|
});
|
||||||
|
const overviewID = countResult?.ticketOverviews?.edges?.find(
|
||||||
|
(overview: any) => overview.node.name === fullName,
|
||||||
|
)?.node?.id;
|
||||||
|
|
||||||
|
console.log({ overviewID });
|
||||||
|
|
||||||
|
const ticketsResult = await executeGraphQL({
|
||||||
|
query: getTicketsByOverviewQuery,
|
||||||
|
variables: { overviewId: overviewID, pageSize: 250 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const edges = ticketsResult?.ticketsByOverview?.edges;
|
||||||
|
if (edges) {
|
||||||
|
tickets = edges.map((edge: any) => edge.node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedTickets = tickets.sort((a: any, b: any) => {
|
||||||
|
if (a.internalId < b.internalId) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (a.internalId > b.internalId) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { tickets: sortedTickets, error };
|
||||||
|
};
|
||||||
12
apps/link/app/_actions/search.ts
Normal file
12
apps/link/app/_actions/search.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
"use server";
|
||||||
|
import { executeGraphQL } from "app/_lib/zammad";
|
||||||
|
import { searchQuery } from "@/app/_graphql/searchQuery";
|
||||||
|
|
||||||
|
export const searchAllAction = async (query: string, limit: number) => {
|
||||||
|
const result = await executeGraphQL({
|
||||||
|
query: searchQuery,
|
||||||
|
variables: { search: query, limit },
|
||||||
|
});
|
||||||
|
|
||||||
|
return result?.search;
|
||||||
|
};
|
||||||
|
|
@ -1,56 +1,78 @@
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { getTicketQuery } from "app/_graphql/getTicketQuery";
|
||||||
|
import { getTicketArticlesQuery } from "app/_graphql/getTicketArticlesQuery";
|
||||||
import { createTicketMutation } from "app/_graphql/createTicketMutation";
|
import { createTicketMutation } from "app/_graphql/createTicketMutation";
|
||||||
import { updateTicketMutation } from "app/_graphql/updateTicketMutation";
|
import { updateTicketMutation } from "app/_graphql/updateTicketMutation";
|
||||||
import { updateTagsMutation } from "app/_graphql/updateTagsMutation";
|
import { updateTagsMutation } from "app/_graphql/updateTagsMutation";
|
||||||
// import { executeMutation } from "app/_lib/graphql";
|
import { executeGraphQL, executeREST } from "app/_lib/zammad";
|
||||||
// import { AddAssetMutation } from "../_graphql/AddAssetMutation";
|
|
||||||
|
|
||||||
export const createTicketAction = async (
|
export const createTicketAction = async (
|
||||||
currentState: any,
|
currentState: any,
|
||||||
formData: FormData,
|
formData: FormData,
|
||||||
) => {
|
) => {
|
||||||
/*
|
|
||||||
|
|
||||||
const createTicket = async () => {
|
|
||||||
await fetcher({
|
|
||||||
document: createTicketMutation,
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
ticket,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
closeDialog();
|
|
||||||
setBody("");
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ticket = {
|
const ticket = {
|
||||||
|
groupId: formData.get("groupId"),
|
||||||
|
customerId: `gid://zammad/User/3`, // { email: formData.get("customerId") },
|
||||||
title: formData.get("title"),
|
title: formData.get("title"),
|
||||||
|
article: {
|
||||||
|
internal: true,
|
||||||
|
body: formData.get("details"),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await executeMutation({
|
console.log({ ticket });
|
||||||
project,
|
|
||||||
mutation: AddAssetMutation,
|
const result = await executeGraphQL({
|
||||||
|
query: createTicketMutation,
|
||||||
variables: {
|
variables: {
|
||||||
input: asset,
|
input: ticket,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidatePath(`/${project}/assets`);
|
console.log({ result });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
values: { ...asset, ...result.addAsset, project },
|
values: ticket,
|
||||||
success: true,
|
success: true,
|
||||||
};
|
};
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return { success: false, message: e?.message ?? "Unknown error" };
|
console.log({ e });
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
values: {},
|
||||||
|
message: e?.message ?? "Unknown error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createTicketArticleAction = async (
|
||||||
|
ticketID: string,
|
||||||
|
article: Record<string, any>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const result = await executeGraphQL({
|
||||||
|
query: updateTicketMutation,
|
||||||
|
variables: {
|
||||||
|
ticketId: `gid://zammad/Ticket/${ticketID}`,
|
||||||
|
input: { article },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log({ result });
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log({ e });
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: e?.message ?? "Unknown error",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateTicketAction = async (
|
export const updateTicketAction = async (
|
||||||
|
|
@ -85,6 +107,24 @@ export const updateTicketAction = async (
|
||||||
*/
|
*/
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getTicketAction = async (id: string) => {
|
||||||
|
const ticketData = await executeGraphQL({
|
||||||
|
query: getTicketQuery,
|
||||||
|
variables: { ticketId: `gid://zammad/Ticket/${id}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
return ticketData?.ticket;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTicketArticlesAction = async (id: string) => {
|
||||||
|
const ticketData = await executeGraphQL({
|
||||||
|
query: getTicketArticlesQuery,
|
||||||
|
variables: { ticketId: `gid://zammad/Ticket/${id}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
return ticketData?.ticketArticles;
|
||||||
|
};
|
||||||
|
|
||||||
export const updateTicketTagsAction = async (
|
export const updateTicketTagsAction = async (
|
||||||
currentState: any,
|
currentState: any,
|
||||||
formData: FormData,
|
formData: FormData,
|
||||||
|
|
@ -116,3 +156,27 @@ export const updateTicketTagsAction = async (
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getTicketStatesAction = async () => {
|
||||||
|
const states = await executeREST({
|
||||||
|
path: "/api/v1/ticket_states",
|
||||||
|
});
|
||||||
|
|
||||||
|
return states;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTicketTagsAction = async () => {
|
||||||
|
const states = await executeREST({
|
||||||
|
path: "/api/v1/tags",
|
||||||
|
});
|
||||||
|
|
||||||
|
return states;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTicketPrioritiesAction = async () => {
|
||||||
|
const priorities = await executeREST({
|
||||||
|
path: "/api/v1/ticket_priorities",
|
||||||
|
});
|
||||||
|
|
||||||
|
return priorities;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,50 @@
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
const fetchUsersAction = async () => {};
|
import { executeREST } from "app/_lib/zammad";
|
||||||
|
|
||||||
|
export const getAgentsAction = async () => {
|
||||||
|
const users = await executeREST({
|
||||||
|
path: "/api/v1/users",
|
||||||
|
});
|
||||||
|
const agents = users?.filter((user: any) => user.role_ids.includes(2)) ?? [];
|
||||||
|
const formattedAgents = agents
|
||||||
|
.map((agent: any) => ({
|
||||||
|
label: agent.login,
|
||||||
|
value: `gid://zammad/User/${agent.id}`,
|
||||||
|
}))
|
||||||
|
.sort((a: any, b: any) => a.label.localeCompare(b.label));
|
||||||
|
|
||||||
|
return formattedAgents;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCustomersAction = async () => {
|
||||||
|
const users = await executeREST({
|
||||||
|
path: "/api/v1/users",
|
||||||
|
});
|
||||||
|
console.log({ users });
|
||||||
|
const customers =
|
||||||
|
users?.filter((user: any) => user.role_ids.includes(3)) ?? [];
|
||||||
|
const formattedCustomers = customers
|
||||||
|
.map((customer: any) => ({
|
||||||
|
label: customer.login,
|
||||||
|
value: `gid://zammad/User/${customer.id}`,
|
||||||
|
}))
|
||||||
|
.sort((a: any, b: any) => a.label.localeCompare(b.label));
|
||||||
|
|
||||||
|
return formattedCustomers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUsersAction = async () => {
|
||||||
|
const users = await executeREST({
|
||||||
|
path: "/api/v1/users",
|
||||||
|
});
|
||||||
|
console.log({ users });
|
||||||
|
const formattedUsers = users
|
||||||
|
.map((customer: any) => ({
|
||||||
|
label: customer.login,
|
||||||
|
value: `gid://zammad/User/${customer.id}`,
|
||||||
|
}))
|
||||||
|
.sort((a: any, b: any) => a.label.localeCompare(b.label));
|
||||||
|
|
||||||
|
return formattedUsers;
|
||||||
|
};
|
||||||
|
|
|
||||||
23
apps/link/app/_components/CSRFProvider.tsx
Normal file
23
apps/link/app/_components/CSRFProvider.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC, PropsWithChildren, useEffect } from "react";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
|
||||||
|
export const CSRFProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||||
|
const { data: session, status, update } = useSession();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
if (status === "authenticated") {
|
||||||
|
const response = await fetch("/api/v1/users/me");
|
||||||
|
const token = response.headers.get("CSRF-Token");
|
||||||
|
update({ csrfToken: token });
|
||||||
|
console.log({ token });
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [session, status, update]);
|
||||||
|
|
||||||
|
return children;
|
||||||
|
};
|
||||||
|
|
@ -5,109 +5,30 @@ import { CssBaseline } from "@mui/material";
|
||||||
import { CookiesProvider } from "react-cookie";
|
import { CookiesProvider } from "react-cookie";
|
||||||
import { SessionProvider } from "next-auth/react";
|
import { SessionProvider } from "next-auth/react";
|
||||||
import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir";
|
import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir";
|
||||||
import { SWRConfig } from "swr";
|
|
||||||
import { GraphQLClient } from "graphql-request";
|
|
||||||
import { I18n } from "react-polyglot";
|
import { I18n } from "react-polyglot";
|
||||||
import { AdapterDateFns } from "@mui/x-date-pickers-pro/AdapterDateFnsV3";
|
import { AdapterDateFns } from "@mui/x-date-pickers-pro/AdapterDateFnsV3";
|
||||||
import { LocalizationProvider } from "@mui/x-date-pickers-pro";
|
import { LocalizationProvider } from "@mui/x-date-pickers-pro";
|
||||||
import { LicenseInfo } from "@mui/x-license";
|
import { LicenseInfo } from "@mui/x-license";
|
||||||
import { locales, LeafcutterProvider } from "@link-stack/leafcutter-ui";
|
import { locales, LeafcutterProvider } from "@link-stack/leafcutter-ui";
|
||||||
|
import { CSRFProvider } from "./CSRFProvider";
|
||||||
|
|
||||||
LicenseInfo.setLicenseKey(
|
LicenseInfo.setLicenseKey(
|
||||||
"c787ac6613c5f2aa0494c4285fe3e9f2Tz04OTY1NyxFPTE3NDYzNDE0ODkwMDAsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI=",
|
"c787ac6613c5f2aa0494c4285fe3e9f2Tz04OTY1NyxFPTE3NDYzNDE0ODkwMDAsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI=",
|
||||||
);
|
);
|
||||||
|
|
||||||
export const MultiProvider: FC<PropsWithChildren> = ({ children }) => {
|
export const MultiProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||||
const [csrfToken, setCsrfToken] = useState("");
|
|
||||||
const origin =
|
const origin =
|
||||||
typeof window !== "undefined" && window.location.origin
|
typeof window !== "undefined" && window.location.origin
|
||||||
? window.location.origin
|
? window.location.origin
|
||||||
: null;
|
: null;
|
||||||
const client = new GraphQLClient(`${origin}/zammad/graphql`);
|
|
||||||
const messages: any = { en: locales.en, fr: locales.fr };
|
const messages: any = { en: locales.en, fr: locales.fr };
|
||||||
const locale = "en";
|
const locale = "en";
|
||||||
const fetchAndCheckAuth = async ({
|
|
||||||
document,
|
|
||||||
variables,
|
|
||||||
url,
|
|
||||||
method,
|
|
||||||
body,
|
|
||||||
}: any) => {
|
|
||||||
const requestHeaders = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Accept: "application/json",
|
|
||||||
"X-CSRF-Token": csrfToken,
|
|
||||||
};
|
|
||||||
let responseData = null;
|
|
||||||
let responseHeaders = new Headers();
|
|
||||||
let responseStatus = null;
|
|
||||||
|
|
||||||
if (document) {
|
|
||||||
const { data, headers, status } = await client.rawRequest(
|
|
||||||
document,
|
|
||||||
variables,
|
|
||||||
requestHeaders,
|
|
||||||
);
|
|
||||||
responseData = data;
|
|
||||||
responseHeaders = headers;
|
|
||||||
responseStatus = status;
|
|
||||||
} else {
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method,
|
|
||||||
headers: requestHeaders,
|
|
||||||
body,
|
|
||||||
});
|
|
||||||
responseData = await res.json();
|
|
||||||
responseHeaders = res.headers;
|
|
||||||
responseStatus = res.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (responseStatus !== 200) {
|
|
||||||
const res = await fetch("/zammad/auth/sso", {
|
|
||||||
method: "GET",
|
|
||||||
redirect: "manual",
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log({ checkAuth: res });
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = responseHeaders.get("CSRF-Token");
|
|
||||||
setCsrfToken(token);
|
|
||||||
|
|
||||||
return responseData;
|
|
||||||
};
|
|
||||||
|
|
||||||
const multiFetcher = async ({
|
|
||||||
document,
|
|
||||||
variables,
|
|
||||||
url,
|
|
||||||
method,
|
|
||||||
body,
|
|
||||||
}: any) => {
|
|
||||||
let checks = 0;
|
|
||||||
let data = null;
|
|
||||||
|
|
||||||
while (!data && checks < 2) {
|
|
||||||
data = await fetchAndCheckAuth({
|
|
||||||
document,
|
|
||||||
variables,
|
|
||||||
url,
|
|
||||||
method,
|
|
||||||
body,
|
|
||||||
});
|
|
||||||
checks++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NextAppDirEmotionCacheProvider options={{ key: "css" }}>
|
<NextAppDirEmotionCacheProvider options={{ key: "css" }}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<SWRConfig value={{ fetcher: multiFetcher }}>
|
|
||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
|
<CSRFProvider>
|
||||||
<CookiesProvider>
|
<CookiesProvider>
|
||||||
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||||
<I18n locale={locale} messages={messages[locale]}>
|
<I18n locale={locale} messages={messages[locale]}>
|
||||||
|
|
@ -115,8 +36,8 @@ export const MultiProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||||
</I18n>
|
</I18n>
|
||||||
</LocalizationProvider>
|
</LocalizationProvider>
|
||||||
</CookiesProvider>
|
</CookiesProvider>
|
||||||
|
</CSRFProvider>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
</SWRConfig>
|
|
||||||
</NextAppDirEmotionCacheProvider>
|
</NextAppDirEmotionCacheProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
import { gql } from "graphql-request";
|
export const createTicketMutation = `mutation CreateTicket($input: TicketCreateInput!) {
|
||||||
|
|
||||||
export const createTicketMutation = gql`
|
|
||||||
mutation CreateTicket($ticketId: ID!, $input: TicketCreateInput!) {
|
|
||||||
ticketCreate(input: $input) {
|
ticketCreate(input: $input) {
|
||||||
ticket {
|
ticket {
|
||||||
id
|
id
|
||||||
|
|
|
||||||
|
|
@ -121,16 +121,23 @@ export const authOptions: NextAuthOptions = {
|
||||||
session.user.roles = token.roles ?? [];
|
session.user.roles = token.roles ?? [];
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
session.user.leafcutter = token.leafcutter; // remove
|
session.user.leafcutter = token.leafcutter; // remove
|
||||||
|
// @ts-ignore
|
||||||
|
session.user.zammadCsrfToken = token.zammadCsrfToken;
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
jwt: async ({ token, user, account, profile, trigger }) => {
|
jwt: async ({ token, user, account, profile, trigger, session }) => {
|
||||||
if (user) {
|
if (user) {
|
||||||
token.roles = (await getUserRoles(user.email)) ?? [];
|
token.roles = (await getUserRoles(user.email)) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (session && trigger === "update") {
|
||||||
|
token.zammadCsrfToken = session.csrfToken;
|
||||||
|
}
|
||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getServerSession = (
|
export const getServerSession = (
|
||||||
...args:
|
...args:
|
||||||
|
|
|
||||||
68
apps/link/app/_lib/zammad.ts
Normal file
68
apps/link/app/_lib/zammad.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { getServerSession } from "app/_lib/authentication";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
|
const getHeaders = async () => {
|
||||||
|
const allCookies = cookies().getAll();
|
||||||
|
const session = await getServerSession();
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
// @ts-ignore
|
||||||
|
"X-CSRF-Token": session.user.zammadCsrfToken,
|
||||||
|
Cookie: allCookies
|
||||||
|
.map((cookie: any) => `${cookie.name}=${cookie.value}`)
|
||||||
|
.join("; "),
|
||||||
|
};
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ExecuteGraphQLOptions {
|
||||||
|
query: string;
|
||||||
|
variables?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const executeGraphQL = async ({
|
||||||
|
query,
|
||||||
|
variables,
|
||||||
|
}: ExecuteGraphQLOptions): Promise<any> => {
|
||||||
|
const headers = await getHeaders();
|
||||||
|
const endpoint = `${process.env.ZAMMAD_URL}/graphql`;
|
||||||
|
const body = JSON.stringify({ query, ...(variables && { variables }) });
|
||||||
|
const result = await fetch(endpoint, {
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
method: "POST",
|
||||||
|
next: { revalidate: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data, errors } = await result.json();
|
||||||
|
|
||||||
|
if (result.status !== 200) {
|
||||||
|
throw new Error(`Error: ${result.status}`);
|
||||||
|
} else if (errors) {
|
||||||
|
throw new Error(`Error: ${JSON.stringify(errors)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ExecuteRESTOptions {
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const executeREST = async ({
|
||||||
|
path,
|
||||||
|
}: ExecuteRESTOptions): Promise<any> => {
|
||||||
|
const headers = await getHeaders();
|
||||||
|
const result = await fetch(`${process.env.ZAMMAD_URL}${path}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status !== 200) {
|
||||||
|
throw new Error(`Error: ${result.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await result.json();
|
||||||
|
};
|
||||||
|
|
@ -10,11 +10,12 @@ const rewriteURL = (
|
||||||
const destinationURL = request.url.replace(originBaseURL, destinationBaseURL);
|
const destinationURL = request.url.replace(originBaseURL, destinationBaseURL);
|
||||||
console.log(`Rewriting ${request.url} to ${destinationURL}`);
|
console.log(`Rewriting ${request.url} to ${destinationURL}`);
|
||||||
const requestHeaders = new Headers(request.headers);
|
const requestHeaders = new Headers(request.headers);
|
||||||
|
// console.log({ beforeHeaders: requestHeaders });
|
||||||
for (const [key, value] of Object.entries(headers)) {
|
for (const [key, value] of Object.entries(headers)) {
|
||||||
requestHeaders.set(key, value as string);
|
requestHeaders.set(key, value as string);
|
||||||
}
|
}
|
||||||
requestHeaders.delete("connection");
|
requestHeaders.delete("connection");
|
||||||
|
// console.log({ afterHeaders: requestHeaders });
|
||||||
return NextResponse.rewrite(new URL(destinationURL), {
|
return NextResponse.rewrite(new URL(destinationURL), {
|
||||||
request: { headers: requestHeaders },
|
request: { headers: requestHeaders },
|
||||||
});
|
});
|
||||||
|
|
@ -35,7 +36,7 @@ const checkRewrites = async (request: NextRequestWithAuth) => {
|
||||||
const leafcutterRole = roles.includes("admin")
|
const leafcutterRole = roles.includes("admin")
|
||||||
? "leafcutter_admin"
|
? "leafcutter_admin"
|
||||||
: "leafcutter_user";
|
: "leafcutter_user";
|
||||||
headers["x-forwarded-roles"] = "admin"; // leafcutterRole;
|
headers["x-forwarded-roles"] = leafcutterRole;
|
||||||
// headers["secruitytenant"] = "global";
|
// headers["secruitytenant"] = "global";
|
||||||
// headers["x-forwarded-for"] = 'link';
|
// headers["x-forwarded-for"] = 'link';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import { ServiceConfig } from "../lib/service";
|
import { ServiceConfig } from "../lib/service";
|
||||||
|
|
||||||
|
const getRoles = async () => [
|
||||||
|
{ value: "admin", label: "Admin" },
|
||||||
|
{ value: "user", label: "User" },
|
||||||
|
{ value: "none", label: "None" },
|
||||||
|
];
|
||||||
|
|
||||||
export const usersConfig: ServiceConfig = {
|
export const usersConfig: ServiceConfig = {
|
||||||
entity: "users",
|
entity: "users",
|
||||||
table: "User",
|
table: "User",
|
||||||
|
|
@ -17,6 +23,13 @@ export const usersConfig: ServiceConfig = {
|
||||||
required: true,
|
required: true,
|
||||||
size: 12,
|
size: 12,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "role",
|
||||||
|
label: "Role",
|
||||||
|
required: true,
|
||||||
|
getOptions: getRoles,
|
||||||
|
size: 12,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
updateFields: [
|
updateFields: [
|
||||||
{ name: "name", label: "Name", required: true, size: 12 },
|
{ name: "name", label: "Name", required: true, size: 12 },
|
||||||
|
|
@ -47,8 +60,13 @@ export const usersConfig: ServiceConfig = {
|
||||||
flex: 2,
|
flex: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "updatedAt",
|
field: "role",
|
||||||
headerName: "Updated At",
|
headerName: "Role",
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "createdAt",
|
||||||
|
headerName: "Created At",
|
||||||
valueGetter: (value: any) => new Date(value).toLocaleString(),
|
valueGetter: (value: any) => new Date(value).toLocaleString(),
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ type AutocompleteProps = {
|
||||||
label: string;
|
label: string;
|
||||||
options: any[];
|
options: any[];
|
||||||
formState: Record<string, any>;
|
formState: Record<string, any>;
|
||||||
|
updateFormState: (name: string, value: any) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
};
|
};
|
||||||
|
|
@ -16,18 +17,21 @@ export const Autocomplete: FC<AutocompleteProps> = ({
|
||||||
label,
|
label,
|
||||||
options,
|
options,
|
||||||
formState,
|
formState,
|
||||||
|
updateFormState,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
required = false,
|
required = false,
|
||||||
}) => (
|
}) => (
|
||||||
<AutocompleteInternal
|
<AutocompleteInternal
|
||||||
disablePortal
|
disablePortal
|
||||||
options={options}
|
options={options}
|
||||||
defaultValue={formState.values[name]}
|
value={formState.values[name] || ""}
|
||||||
|
onChange={(e: any) => updateFormState?.(name, e.target.id)}
|
||||||
fullWidth
|
fullWidth
|
||||||
size="small"
|
size="small"
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
|
name={name}
|
||||||
label={label}
|
label={label}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
required={required}
|
required={required}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue