first commit
This commit is contained in:
10
app/(panel)/dashboard/page.tsx
Normal file
10
app/(panel)/dashboard/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { API_URL } from "@/core/constant";
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
app/(panel)/departments/page.tsx
Normal file
9
app/(panel)/departments/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
21
app/(panel)/layout.tsx
Normal file
21
app/(panel)/layout.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"use client"
|
||||||
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
|
import Sidebar from "@/ui/layout/Sidebar";
|
||||||
|
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||||
|
import { AdapterDateFnsJalali } from '@mui/x-date-pickers/AdapterDateFnsJalali';
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-12">
|
||||||
|
<div className="col-span-2 flex flex-col justify-center items-center h-full">
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
<LocalizationProvider dateAdapter={AdapterDateFnsJalali}>
|
||||||
|
<div className="col-span-10 h-full ">{children}</div>
|
||||||
|
</LocalizationProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
621
app/(panel)/reports/page.tsx
Normal file
621
app/(panel)/reports/page.tsx
Normal file
@@ -0,0 +1,621 @@
|
|||||||
|
"use client";
|
||||||
|
import { requestType, ticketStatuses } from "@/core/constant";
|
||||||
|
import {
|
||||||
|
exportToExcel,
|
||||||
|
formatDurationPersian,
|
||||||
|
handleAxiosError,
|
||||||
|
} from "@/core/utils";
|
||||||
|
import {
|
||||||
|
useMutateAgentEfficiency,
|
||||||
|
useMutateAgentPerformance,
|
||||||
|
useMutateAgingReport,
|
||||||
|
useMutateAvgResolutionTime,
|
||||||
|
useMutateClosureRate,
|
||||||
|
useMutateCriticalTickets,
|
||||||
|
useMutateDepartmentLoad,
|
||||||
|
useMutateDepartmentReport,
|
||||||
|
useMutateKpiReport,
|
||||||
|
useMutatePredictionReport,
|
||||||
|
useMutateSlaBreach,
|
||||||
|
useMutateStatsReport,
|
||||||
|
} from "@/services/hooks/report.hook";
|
||||||
|
import { DownloadOutlined } from "@mui/icons-material";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Stack,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { UseMutateAsyncFunction } from "@tanstack/react-query";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
// ۱. آمار کلی تیکتها
|
||||||
|
const { mutateAsync: totalStatsAsync } = useMutateStatsReport();
|
||||||
|
|
||||||
|
// ۲. گزارش عملکرد دپارتمانها
|
||||||
|
const { mutateAsync: deptReportAsync } = useMutateDepartmentReport();
|
||||||
|
|
||||||
|
// ۳. گزارش عملکرد کارشناسان
|
||||||
|
const { mutateAsync: agentPerfAsync } = useMutateAgentPerformance();
|
||||||
|
|
||||||
|
// ۴. میانگین زمان پاسخگویی
|
||||||
|
const { mutateAsync: avgResTimeAsync } = useMutateAvgResolutionTime();
|
||||||
|
|
||||||
|
// ۵. تیکتهای بحرانی
|
||||||
|
const { mutateAsync: criticalTicketsAsync } = useMutateCriticalTickets();
|
||||||
|
|
||||||
|
// ۶. روند ثبت تیکتها (نیاز به params دارد)
|
||||||
|
// const { mutateAsync: ticketsTrend, isLoading: trendLoading } = useMutateTicketsTrend(dateParams);
|
||||||
|
|
||||||
|
// ۷. نرخ بستن تیکتها
|
||||||
|
const { mutateAsync: closureRateAsync } = useMutateClosureRate();
|
||||||
|
|
||||||
|
// ۸. تیکتهای خارج از SLA
|
||||||
|
const { mutateAsync: slaBreachAsync } = useMutateSlaBreach();
|
||||||
|
|
||||||
|
// ۹. گزارش قدمت تیکتها
|
||||||
|
const { mutateAsync: agingReportAsync } = useMutateAgingReport();
|
||||||
|
|
||||||
|
// ۱۰. بهرهوری کارشناسان
|
||||||
|
const { mutateAsync: agentEfficiencyAsync } = useMutateAgentEfficiency();
|
||||||
|
|
||||||
|
// ۱۱. بار کاری دپارتمانها
|
||||||
|
const { mutateAsync: deptLoadAsync } = useMutateDepartmentLoad();
|
||||||
|
|
||||||
|
// ۱۲. شاخصهای کلیدی عملکرد (KPI)
|
||||||
|
const { mutateAsync: kpiReportAsync } = useMutateKpiReport();
|
||||||
|
|
||||||
|
// ۱۳. پیشبینی وضعیت تیکتها
|
||||||
|
const { mutateAsync: predictionReportAsync } = useMutatePredictionReport();
|
||||||
|
|
||||||
|
const handleGetStatsReport = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await totalStatsAsync();
|
||||||
|
|
||||||
|
const formattedData = [data]?.map((t: any) => ({
|
||||||
|
"كل تيكت ها": t.total,
|
||||||
|
"تيكت هاي باز": t.open,
|
||||||
|
"تيكت هاي در حال بررسي": t.inProgress,
|
||||||
|
"تيكت هاي حل شده": t.resolved,
|
||||||
|
"تيكت هاي بسته شده": t.closed,
|
||||||
|
}));
|
||||||
|
exportToExcel("excel", formattedData, "گزارش كلي تيكت ها");
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
toast.error(handleAxiosError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDepReport = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await deptReportAsync();
|
||||||
|
|
||||||
|
const formattedData = data?.map((t: any) => ({
|
||||||
|
"بخش / واحد": t.department?.displayName,
|
||||||
|
"كل تيكت ها": t.totalTickets,
|
||||||
|
"تيكت هاي باز": t.openTickets,
|
||||||
|
"تيكت هاي حل شده": t.resolvedTickets,
|
||||||
|
}));
|
||||||
|
exportToExcel("excel", formattedData, "گزارش تيكت ها به تفكيك واحد");
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
toast.error(handleAxiosError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAgentPerformanceReport = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await agentPerfAsync();
|
||||||
|
|
||||||
|
const formattedData = data?.map((t: any) => ({
|
||||||
|
"كارشناس مربوطه": t.assignee?.fullname,
|
||||||
|
"كل تيكت ها": t.totalAssigned,
|
||||||
|
"تيكت هاي باز": t.open,
|
||||||
|
"تيكت هاي حل شده": t.resolved,
|
||||||
|
}));
|
||||||
|
exportToExcel("excel", formattedData, "گزارش تيكت ها به تفكيك كارشناسان");
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
toast.error(handleAxiosError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAvgPerformanceReport = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await avgResTimeAsync();
|
||||||
|
|
||||||
|
const formattedData = [data]?.map((t: any) => ({
|
||||||
|
"نرخ زمان پاسخگويي": formatDurationPersian(t.avgResolutionSeconds),
|
||||||
|
"نرخ دقيق بر حسب ثانيه": t.avgResolutionSeconds,
|
||||||
|
}));
|
||||||
|
exportToExcel("excel", formattedData, "گزارش نرخ پاسخگويي ");
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
toast.error(handleAxiosError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCriticalTicketsReport = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await criticalTicketsAsync();
|
||||||
|
|
||||||
|
const formattedData = data?.map((t: any) => ({
|
||||||
|
"شماره تيكت": t.ticketNumber,
|
||||||
|
"واحد / بخش": t.department?.displayName,
|
||||||
|
"تلفن داخلي": t.internalPhone,
|
||||||
|
كاربر: t.createdBy,
|
||||||
|
"محل وقوع مشكل": t.location,
|
||||||
|
"نوع درخواست": requestType.find((p) => p.id === t.requestType)
|
||||||
|
?.displayName,
|
||||||
|
|
||||||
|
"كارشناس مربوطه": t.assignee?.fullname,
|
||||||
|
"دستگاه مربوطه": t.relatedSystem,
|
||||||
|
وضعيت: ticketStatuses.find((p) => p.id === t.status)?.displayName,
|
||||||
|
توضيحات: t.description,
|
||||||
|
|
||||||
|
"اقدامات كارشناس": t.helpdeskAction,
|
||||||
|
"يادداشت ها": t.finalNotes,
|
||||||
|
"تاريخ ثبت": new Date(t.createdAt).toLocaleDateString("fa-IR"),
|
||||||
|
}));
|
||||||
|
exportToExcel("excel", formattedData, "گزارش تيكت هاي بحراني");
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
toast.error(handleAxiosError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
// ریسپانسیو بودن: در موبایل ۱ ستون، در تبلت ۲، در دسکتاپ ۴ ستون
|
||||||
|
gridTemplateColumns: {
|
||||||
|
xs: "1fr",
|
||||||
|
sm: "1fr 1fr",
|
||||||
|
md: "repeat(6, 1fr)",
|
||||||
|
},
|
||||||
|
gap: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
aspectRatio: "1/1", // مربع کردن کارت
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
transition: "0.3s",
|
||||||
|
"&:hover": { boxShadow: 6, transform: "translateY(-5px)" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" align="center" gutterBottom>
|
||||||
|
آمار کلی تیکتها{" "}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<Stack spacing={1} sx={{ p: 2 }}>
|
||||||
|
{/* <Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<VisibilityIcon />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
مشاهده
|
||||||
|
</Button> */}
|
||||||
|
<Button
|
||||||
|
onClick={handleGetStatsReport}
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<DownloadOutlined />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
اکسل
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
aspectRatio: "1/1", // مربع کردن کارت
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
transition: "0.3s",
|
||||||
|
"&:hover": { boxShadow: 6, transform: "translateY(-5px)" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" align="center" gutterBottom>
|
||||||
|
گزارش عملکرد دپارتمانها
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<Stack spacing={1} sx={{ p: 2 }}>
|
||||||
|
{/* <Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<VisibilityIcon />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
مشاهده
|
||||||
|
</Button> */}
|
||||||
|
<Button
|
||||||
|
onClick={handleDepReport}
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<DownloadOutlined />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
اکسل
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
aspectRatio: "1/1", // مربع کردن کارت
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
transition: "0.3s",
|
||||||
|
"&:hover": { boxShadow: 6, transform: "translateY(-5px)" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" align="center" gutterBottom>
|
||||||
|
گزارش عملکرد کارشناسان
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<Stack spacing={1} sx={{ p: 2 }}>
|
||||||
|
{/* <Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<VisibilityIcon />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
مشاهده
|
||||||
|
</Button> */}
|
||||||
|
<Button
|
||||||
|
onClick={handleAgentPerformanceReport}
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<DownloadOutlined />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
اکسل
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
aspectRatio: "1/1", // مربع کردن کارت
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
transition: "0.3s",
|
||||||
|
"&:hover": { boxShadow: 6, transform: "translateY(-5px)" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" align="center" gutterBottom>
|
||||||
|
میانگین زمان پاسخگویی
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<Stack spacing={1} sx={{ p: 2 }}>
|
||||||
|
{/* <Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<VisibilityIcon />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
مشاهده
|
||||||
|
</Button> */}
|
||||||
|
<Button
|
||||||
|
onClick={handleAvgPerformanceReport}
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<DownloadOutlined />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
اکسل
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
aspectRatio: "1/1", // مربع کردن کارت
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
transition: "0.3s",
|
||||||
|
"&:hover": { boxShadow: 6, transform: "translateY(-5px)" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" align="center" gutterBottom>
|
||||||
|
تیکتهای بحرانی
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<Stack spacing={1} sx={{ p: 2 }}>
|
||||||
|
{/* <Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<VisibilityIcon />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
مشاهده
|
||||||
|
</Button> */}
|
||||||
|
<Button
|
||||||
|
onClick={handleCriticalTicketsReport}
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<DownloadOutlined />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
اکسل
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
aspectRatio: "1/1", // مربع کردن کارت
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
transition: "0.3s",
|
||||||
|
"&:hover": { boxShadow: 6, transform: "translateY(-5px)" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" align="center" gutterBottom>
|
||||||
|
روند ثبت تیکتها{" "}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<Stack spacing={1} sx={{ p: 2 }}>
|
||||||
|
{/* <Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<VisibilityIcon />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
مشاهده
|
||||||
|
</Button> */}
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<DownloadOutlined />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
اکسل
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
aspectRatio: "1/1", // مربع کردن کارت
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
transition: "0.3s",
|
||||||
|
"&:hover": { boxShadow: 6, transform: "translateY(-5px)" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" align="center" gutterBottom>
|
||||||
|
نرخ بستن تیکتها
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<Stack spacing={1} sx={{ p: 2 }}>
|
||||||
|
{/* <Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<VisibilityIcon />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
مشاهده
|
||||||
|
</Button> */}
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<DownloadOutlined />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
اکسل
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
aspectRatio: "1/1", // مربع کردن کارت
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
transition: "0.3s",
|
||||||
|
"&:hover": { boxShadow: 6, transform: "translateY(-5px)" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" align="center" gutterBottom>
|
||||||
|
تیکتهای خارج از SLA
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<Stack spacing={1} sx={{ p: 2 }}>
|
||||||
|
{/* <Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<VisibilityIcon />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
مشاهده
|
||||||
|
</Button> */}
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<DownloadOutlined />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
اکسل
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
aspectRatio: "1/1", // مربع کردن کارت
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
transition: "0.3s",
|
||||||
|
"&:hover": { boxShadow: 6, transform: "translateY(-5px)" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" align="center" gutterBottom>
|
||||||
|
گزارش قدمت تیکتها
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<Stack spacing={1} sx={{ p: 2 }}>
|
||||||
|
{/* <Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<VisibilityIcon />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
مشاهده
|
||||||
|
</Button> */}
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<DownloadOutlined />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
اکسل
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
aspectRatio: "1/1", // مربع کردن کارت
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
transition: "0.3s",
|
||||||
|
"&:hover": { boxShadow: 6, transform: "translateY(-5px)" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" align="center" gutterBottom>
|
||||||
|
بهرهوری کارشناسان
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<Stack spacing={1} sx={{ p: 2 }}>
|
||||||
|
{/* <Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<VisibilityIcon />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
مشاهده
|
||||||
|
</Button> */}
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<DownloadOutlined />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
اکسل
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
aspectRatio: "1/1", // مربع کردن کارت
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
transition: "0.3s",
|
||||||
|
"&:hover": { boxShadow: 6, transform: "translateY(-5px)" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" align="center" gutterBottom>
|
||||||
|
بار کاری دپارتمانها{" "}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<Stack spacing={1} sx={{ p: 2 }}>
|
||||||
|
{/* <Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<VisibilityIcon />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
مشاهده
|
||||||
|
</Button> */}
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<DownloadOutlined />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
اکسل
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
aspectRatio: "1/1", // مربع کردن کارت
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
transition: "0.3s",
|
||||||
|
"&:hover": { boxShadow: 6, transform: "translateY(-5px)" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" align="center" gutterBottom>
|
||||||
|
شاخصهای کلیدی عملکرد (KPI){" "}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<Stack spacing={1} sx={{ p: 2 }}>
|
||||||
|
{/* <Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<VisibilityIcon />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
مشاهده
|
||||||
|
</Button> */}
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<DownloadOutlined />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
اکسل
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
aspectRatio: "1/1", // مربع کردن کارت
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
transition: "0.3s",
|
||||||
|
"&:hover": { boxShadow: 6, transform: "translateY(-5px)" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" align="center" gutterBottom>
|
||||||
|
پیشبینی وضعیت تیکتها{" "}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<Stack spacing={1} sx={{ p: 2 }}>
|
||||||
|
{/* <Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<VisibilityIcon />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
مشاهده
|
||||||
|
</Button> */}
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<DownloadOutlined />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
اکسل
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
app/(panel)/tickets/create/page.tsx
Normal file
60
app/(panel)/tickets/create/page.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { API_URL } from "@/core/constant";
|
||||||
|
import TicketForm from "@/ui/form/TicketForm";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
|
async function getDepartmentsData() {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const tokenCookie = cookieStore.get("userToken"); // گرفتن کوکی از درخواست کاربر
|
||||||
|
|
||||||
|
const res = await fetch(`${API_URL}/department/all`, {
|
||||||
|
cache: "no-cache",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Cookie: `${tokenCookie?.name}=${tokenCookie?.value}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error("خطا در واكشي ديتاي واحد ها");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUsersData() {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const tokenCookie = cookieStore.get("userToken"); // گرفتن کوکی از درخواست کاربر
|
||||||
|
const res = await fetch(`${API_URL}/user/all`, {
|
||||||
|
cache: "no-cache",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Cookie: `${tokenCookie?.name}=${tokenCookie?.value}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error("خطا در واكشي ديتاي واحد ها");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
const getdepartmentsdata = await getDepartmentsData();
|
||||||
|
const getusersdata = await getUsersData();
|
||||||
|
|
||||||
|
const departments = getdepartmentsdata?.data;
|
||||||
|
const users = getusersdata?.data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TicketForm departments={departments} users={users} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
480
app/(panel)/tickets/page.tsx
Normal file
480
app/(panel)/tickets/page.tsx
Normal file
@@ -0,0 +1,480 @@
|
|||||||
|
"use client";
|
||||||
|
import {
|
||||||
|
API_URL,
|
||||||
|
requestType,
|
||||||
|
ticketPriorities,
|
||||||
|
ticketStatuses,
|
||||||
|
} from "@/core/constant";
|
||||||
|
import { TicketInterface } from "@/core/types";
|
||||||
|
import { exportToExcel, handleAxiosError, handleExport } from "@/core/utils";
|
||||||
|
import { useGetAllDepartments } from "@/services/hooks/department.hook";
|
||||||
|
import {
|
||||||
|
useGetAllTickets,
|
||||||
|
useGetAllTicketsExport,
|
||||||
|
useRemoveTicket,
|
||||||
|
} from "@/services/hooks/ticket.hook";
|
||||||
|
import { useGetAllUsers } from "@/services/hooks/users.hook";
|
||||||
|
import DateFilters from "@/ui/DateFilter";
|
||||||
|
import DeleteConfirmModal from "@/ui/DeleteConfirmModal";
|
||||||
|
import TicketDetailModal from "@/ui/TicketDetailModel";
|
||||||
|
import { Box, Button, MenuItem, Pagination, TextField } from "@mui/material";
|
||||||
|
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||||
|
import { PickerValue } from "@mui/x-date-pickers/internals";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
|
interface ticketFilters {
|
||||||
|
departmentId: string;
|
||||||
|
priority: string;
|
||||||
|
status: string;
|
||||||
|
user: string;
|
||||||
|
requestType: string;
|
||||||
|
startDate: PickerValue | string | null;
|
||||||
|
endDate: PickerValue | string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [selectedTicket, setSelectedTicket] = useState(null);
|
||||||
|
const [seletectedTicketForDelete, setSelectedTicketForDelete] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||||
|
const handleOpen = (ticket: any) => {
|
||||||
|
setSelectedTicket(ticket);
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
const router = useRouter();
|
||||||
|
const [filters, setFilters] = useState<ticketFilters>({
|
||||||
|
departmentId: "",
|
||||||
|
priority: "",
|
||||||
|
status: "",
|
||||||
|
user: "",
|
||||||
|
requestType: "",
|
||||||
|
startDate: null,
|
||||||
|
endDate: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
|
const { mutateAsync } = useRemoveTicket();
|
||||||
|
const { data, isLoading } = useGetAllTickets({ page, ...filters });
|
||||||
|
const { mutateAsync: getAllTicketExportAsync } = useGetAllTicketsExport();
|
||||||
|
|
||||||
|
const { data: users, isLoading: getusersloading } = useGetAllUsers();
|
||||||
|
const { data: departments, isLoading: departmentsLoading } =
|
||||||
|
useGetAllDepartments();
|
||||||
|
|
||||||
|
const handleRemoveTicket = async () => {
|
||||||
|
if (seletectedTicketForDelete) {
|
||||||
|
try {
|
||||||
|
const { message } = await mutateAsync(seletectedTicketForDelete);
|
||||||
|
toast.success(message);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["get-all-tickets"] });
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(handleAxiosError(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setOpenDeleteModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownload = async (type: "excel" | "spss", filename: string) => {
|
||||||
|
const res = await getAllTicketExportAsync({ ...filters });
|
||||||
|
const formattedData = res?.data?.data.map((t: any) => ({
|
||||||
|
"شماره تیکت": t.ticketNumber || t.id,
|
||||||
|
موضوع: t.title,
|
||||||
|
وضعیت: t.status === "open" ? "باز" : "بسته", // ترجمه وضعیتها
|
||||||
|
دپارتمان: t.department?.displayName || "نامشخص",
|
||||||
|
کارشناس: t.assignee?.fullname || "بدون متصدی",
|
||||||
|
"تاریخ ثبت": t.createdAt
|
||||||
|
? new Date(t.createdAt).toLocaleDateString("fa-IR")
|
||||||
|
: "-",
|
||||||
|
اولویت: t.priority || "عادی",
|
||||||
|
// میتوانید فیلد جدید اضافه کنید:
|
||||||
|
توضیحات: t.description?.substring(0, 50) + "...", // محدود کردن طول متن
|
||||||
|
}));
|
||||||
|
exportToExcel(type, formattedData, filename);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-12 gap-y-4">
|
||||||
|
<div className="col-span-12 bg-white p-3 rounded-xl flex justify-between items-center gap-x-6">
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
size="medium"
|
||||||
|
label="واحد / بخش"
|
||||||
|
name="departmentId"
|
||||||
|
value={filters.departmentId}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFilters((prev) => ({ ...prev, departmentId: e.target.value }))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem value="">
|
||||||
|
<em>همه</em>
|
||||||
|
</MenuItem>
|
||||||
|
{departments?.data.map((dep: any) => (
|
||||||
|
<MenuItem key={dep.id} value={dep.id}>
|
||||||
|
{dep.displayName}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
size="medium"
|
||||||
|
label="اولويت"
|
||||||
|
name="priority"
|
||||||
|
value={filters.priority}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFilters((prev) => ({ ...prev, priority: e.target.value }))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem value="">
|
||||||
|
<em>همه</em>
|
||||||
|
</MenuItem>
|
||||||
|
{ticketPriorities?.map((p: any) => (
|
||||||
|
<MenuItem key={p.id} value={p.id}>
|
||||||
|
{p.displayName}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
size="medium"
|
||||||
|
label="وضعيت تيكت"
|
||||||
|
name="status"
|
||||||
|
value={filters.status}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFilters((prev) => ({ ...prev, status: e.target.value }))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem value="">
|
||||||
|
<em>همه</em>
|
||||||
|
</MenuItem>
|
||||||
|
{ticketStatuses?.map((p: any) => (
|
||||||
|
<MenuItem key={p.id} value={p.id}>
|
||||||
|
{p.displayName}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
size="medium"
|
||||||
|
label="نوع درخواست"
|
||||||
|
name="requestType"
|
||||||
|
value={filters.requestType}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFilters((prev) => ({ ...prev, requestType: e.target.value }))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem value="">
|
||||||
|
<em>همه</em>
|
||||||
|
</MenuItem>
|
||||||
|
{requestType?.map((p: any) => (
|
||||||
|
<MenuItem key={p.id} value={p.id}>
|
||||||
|
{p.displayName}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
size="medium"
|
||||||
|
label="كارشناس"
|
||||||
|
name="user"
|
||||||
|
value={filters.user}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFilters((prev) => ({ ...prev, user: e.target.value }))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem value="">
|
||||||
|
<em>همه</em>
|
||||||
|
</MenuItem>
|
||||||
|
{users?.data?.map((p: any) => (
|
||||||
|
<MenuItem key={p.id} value={p.id}>
|
||||||
|
{p.fullname}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
{/* <DateFilters /> */}
|
||||||
|
<Box>
|
||||||
|
<DatePicker
|
||||||
|
value={filters.startDate ? new Date(filters.startDate) : null}
|
||||||
|
onChange={(value) =>
|
||||||
|
setFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
startDate: value?.toISOString() ?? null,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
label="از تاريخ"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<DatePicker
|
||||||
|
value={filters.endDate ? new Date(filters.endDate) : null}
|
||||||
|
onChange={(value) =>
|
||||||
|
setFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
endDate: value?.toISOString() ?? null,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
label="تا تاريخ"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setFilters({
|
||||||
|
departmentId: "",
|
||||||
|
priority: "",
|
||||||
|
status: "",
|
||||||
|
user: "",
|
||||||
|
requestType: "",
|
||||||
|
startDate: null,
|
||||||
|
endDate: null,
|
||||||
|
});
|
||||||
|
// router.push('/tickets')
|
||||||
|
}}
|
||||||
|
className="rounded-lg px-3 py-1.5 text-xs font-semibold text-red-700 ring-1 ring-inset ring-red-200 hover:bg-red-200 cursor-pointer"
|
||||||
|
>
|
||||||
|
حذف فيلتر ها
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-12 flex items-center gap-2 rounded-2xl border border-slate-200 bg-white shadow-sm w-full p-1">
|
||||||
|
<button
|
||||||
|
className="rounded-lg px-3 py-1.5 text-xs font-medium ring-1 ring-inset ring-green-600 text-green-700 bg-green-100 cursor-pointer"
|
||||||
|
onClick={() => handleExport(data?.data?.data, "excel")}
|
||||||
|
>
|
||||||
|
خروجي اكسل جدول فعلي
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="rounded-lg px-3 py-1.5 text-xs font-medium ring-1 ring-inset ring-green-600 text-green-700 bg-green-100 cursor-pointer"
|
||||||
|
onClick={() => handleExport(data?.data?.data, "spss")}
|
||||||
|
>
|
||||||
|
خروجي برای SPSS (CSV) جدول فعلي
|
||||||
|
</button>
|
||||||
|
|
|
||||||
|
<button
|
||||||
|
className="rounded-lg px-3 py-1.5 text-xs font-medium ring-1 ring-inset ring-blue-600 text-blue-700 bg-blue-100 cursor-pointer"
|
||||||
|
onClick={() => handleDownload("excel", "گزارش كلي")}
|
||||||
|
>
|
||||||
|
خروجي اكسل كلي
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="rounded-lg px-3 py-1.5 text-xs font-medium ring-1 ring-inset ring-blue-600 text-blue-700 bg-blue-100 cursor-pointer"
|
||||||
|
onClick={() => handleDownload("spss", "گزارش كلي")}
|
||||||
|
>
|
||||||
|
خروجي spss كلي
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-12">
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="overflow-x-auto rounded-2xl border border-slate-200 bg-white shadow-sm">
|
||||||
|
<table className="min-w-225 w-full text-sm">
|
||||||
|
<thead className="bg-slate-50 text-slate-600">
|
||||||
|
<tr className="[&>th]:px-4 [&>th]:py-3 [&>th]:text-right [&>th]:font-semibold">
|
||||||
|
<th>رديف</th>
|
||||||
|
|
||||||
|
<th>کد</th>
|
||||||
|
<th>كاربر</th>
|
||||||
|
<th>واحد</th>
|
||||||
|
<th>نوع درخواست</th>
|
||||||
|
<th>وضعیت</th>
|
||||||
|
<th>اولويت</th>
|
||||||
|
<th>ارجاع به</th>
|
||||||
|
<th>تاریخ</th>
|
||||||
|
<th className="w-28">عملیات</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody className="divide-y divide-slate-200 text-slate-800">
|
||||||
|
{data?.data?.data.map(
|
||||||
|
(item: TicketInterface, index: number) => {
|
||||||
|
const status = ticketStatuses.find(
|
||||||
|
(p) => p.id === item.status,
|
||||||
|
);
|
||||||
|
const priority = ticketPriorities.find(
|
||||||
|
(p) => p.id === item.priority,
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldBlinkPriority = item.priority === "critical";
|
||||||
|
const shouldBlinkStatus = item.status === "open";
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={item.id}
|
||||||
|
className="hover:bg-blue-50/40 transition-colors"
|
||||||
|
>
|
||||||
|
<td className="px-4 py-3 font-medium text-slate-900">
|
||||||
|
{Number(index + 1).toLocaleString("fa-IR")}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 font-medium text-slate-900">
|
||||||
|
{item.ticketNumber}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">{item.createdBy}</td>
|
||||||
|
<td className="px-4 py-3 text-slate-600">
|
||||||
|
{item.department.displayName}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
{
|
||||||
|
requestType.find((p) => p.id === item.requestType)
|
||||||
|
?.displayName
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-semibold ring-1 ring-inset ${status?.bgClass} `}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`h-1.5 w-1.5 rounded-full ${status?.dotClass} ${shouldBlinkStatus ? "animate-ping" : ""}`}
|
||||||
|
></span>
|
||||||
|
{status?.displayName || "نامشخص"}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-slate-600">
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-semibold ring-1 ring-inset ${priority?.bgClass}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`h-1.5 w-1.5 rounded-full ${priority?.dotClass} ${shouldBlinkPriority ? "animate-ping" : ""}`}
|
||||||
|
></span>
|
||||||
|
{priority?.displayName || "نامشخص"}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
{item.assignee.fullname}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
{new Date(item.createdAt).toLocaleDateString(
|
||||||
|
"fa-IR",
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleOpen(item)}
|
||||||
|
className="rounded-lg px-3 py-1.5 text-xs font-semibold text-blue-700 bg-blue-50 ring-1 ring-inset ring-blue-200 hover:bg-blue-100"
|
||||||
|
>
|
||||||
|
مشاهده
|
||||||
|
</button>
|
||||||
|
<Link
|
||||||
|
href={`/tickets/update/${item.ticketNumber}`}
|
||||||
|
className="rounded-lg px-3 py-1.5 text-xs font-semibold text-slate-700 bg-slate-100 ring-1 ring-inset ring-slate-200 hover:bg-slate-200"
|
||||||
|
>
|
||||||
|
ویرایش
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedTicketForDelete(item.id);
|
||||||
|
setOpenDeleteModal(true);
|
||||||
|
}}
|
||||||
|
className="rounded-lg px-3 py-1.5 text-xs font-semibold text-red-700 bg-red-100 ring-1 ring-inset ring-red-200 hover:bg-red-200"
|
||||||
|
>
|
||||||
|
حذف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center mt-4 bg-white rounded-xl py-2">
|
||||||
|
<Pagination
|
||||||
|
count={data?.data?.totalPages}
|
||||||
|
page={page}
|
||||||
|
onChange={(e, p) => setPage(p)}
|
||||||
|
variant="outlined"
|
||||||
|
shape="rounded"
|
||||||
|
sx={{
|
||||||
|
"& .MuiPaginationItem-root": {
|
||||||
|
borderRadius: "28px",
|
||||||
|
borderColor: "#e2e8f0", // slate-200
|
||||||
|
color: "#475569", // slate-600
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "#eff6ff", // blue-50
|
||||||
|
borderColor: "#bfdbfe", // blue-200
|
||||||
|
},
|
||||||
|
"&.Mui-selected": {
|
||||||
|
backgroundColor: "#2563eb", // blue-600
|
||||||
|
color: "#ffffff",
|
||||||
|
borderColor: "#2563eb",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "#1d4ed8", // blue-700
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<TicketDetailModal
|
||||||
|
open={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
ticket={selectedTicket}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeleteConfirmModal
|
||||||
|
open={openDeleteModal}
|
||||||
|
onClose={() => setOpenDeleteModal(false)}
|
||||||
|
onConfirm={handleRemoveTicket}
|
||||||
|
/>
|
||||||
|
{/* <div className="mt-4 grid gap-3 md:hidden">
|
||||||
|
<div className="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-slate-500">کد</div>
|
||||||
|
<div className="font-semibold text-slate-900">#10241</div>
|
||||||
|
</div>
|
||||||
|
<span className="inline-flex items-center gap-1.5 rounded-full bg-blue-50 px-2.5 py-1 text-xs font-semibold text-blue-700 ring-1 ring-inset ring-blue-200">
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-blue-600"></span>
|
||||||
|
در حال پردازش
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-slate-500">مشتری</div>
|
||||||
|
<div className="text-slate-800">علی رضایی</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-slate-500">مبلغ</div>
|
||||||
|
<div className="text-slate-800">1,250,000</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<div className="text-xs text-slate-500">ایمیل</div>
|
||||||
|
<div className="text-slate-600">ali@example.com</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<div className="text-xs text-slate-500">تاریخ</div>
|
||||||
|
<div className="text-slate-600">1403/08/12</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex gap-2">
|
||||||
|
<button className="flex-1 rounded-xl px-3 py-2 text-xs font-semibold text-blue-700 bg-blue-50 ring-1 ring-inset ring-blue-200 hover:bg-blue-100">
|
||||||
|
مشاهده
|
||||||
|
</button>
|
||||||
|
<button className="flex-1 rounded-xl px-3 py-2 text-xs font-semibold text-slate-700 bg-slate-100 ring-1 ring-inset ring-slate-200 hover:bg-slate-200">
|
||||||
|
ویرایش
|
||||||
|
</button>
|
||||||
|
<button className="flex-1 rounded-xl px-3 py-1.5 text-xs font-semibold text-red-700 bg-red-100 ring-1 ring-inset ring-red-200 hover:bg-red-200">
|
||||||
|
حذف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
app/(panel)/tickets/update/[id]/page.tsx
Normal file
23
app/(panel)/tickets/update/[id]/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { API_URL } from "@/core/constant";
|
||||||
|
interface PageProps {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTicket(id: string) {
|
||||||
|
const res = await fetch(`${API_URL}/ticket/get/${id}`, {
|
||||||
|
cache: "no-store", // برای اینکه همیشه دیتای تازه از سرور بگیرد
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error("Error");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Page({ params }: PageProps) {
|
||||||
|
const { id } = await params;
|
||||||
|
const getdata = await getTicket(id);
|
||||||
|
console.log(getdata)
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
5
app/(panel)/users/page.tsx
Normal file
5
app/(panel)/users/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
31
app/error.tsx
Normal file
31
app/error.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"use client"; // الزامی برای صفحات خطا
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export default function Error({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error;
|
||||||
|
reset: () => void;
|
||||||
|
}) {
|
||||||
|
useEffect(() => {
|
||||||
|
console.error(error); // لاگ کردن خطا برای بررسی
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-screen p-4">
|
||||||
|
<h1 className="text-6xl font-bold text-white">اوپس!</h1>
|
||||||
|
<h2 className="text-xl font-semibold mt-4 text-neutral-50">مشکلی در سرور رخ داده است</h2>
|
||||||
|
<p className="text-neutral-300 mt-2 text-center max-w-md">
|
||||||
|
در حال حاضر امکان پردازش درخواست شما وجود ندارد. لطفاً دوباره تلاش کنید.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => reset()}
|
||||||
|
className="mt-6 px-6 py-2 bg-white text-blue-900 rounded-lg transition cursor-pointer"
|
||||||
|
>
|
||||||
|
تلاش مجدد
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,22 +5,23 @@
|
|||||||
--foreground: #171717;
|
--foreground: #171717;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
/* @theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-geist-sans);
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
}
|
} */
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
/* @media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--background: #0a0a0a;
|
--background: #0a0a0a;
|
||||||
--foreground: #ededed;
|
--foreground: #ededed;
|
||||||
}
|
}
|
||||||
}
|
} */
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
background:linear-gradient(135deg,#1e3c72 0%, #2a5298 40%, #4facfe 100%);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: var(--font-vazir);
|
||||||
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
import type { Metadata } from "next";
|
"use client";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import { CacheProvider } from "@emotion/react";
|
||||||
const geistSans = Geist({
|
import { ThemeProvider } from "@mui/material/styles";
|
||||||
variable: "--font-geist-sans",
|
import rtlCache from "./theme/rtlCache";
|
||||||
subsets: ["latin"],
|
import theme from "./theme/theme";
|
||||||
});
|
import { CssBaseline } from "@mui/material";
|
||||||
|
import { FontVazir } from "@/config/font.config";
|
||||||
const geistMono = Geist_Mono({
|
import { ToastContainer } from "react-toastify";
|
||||||
variable: "--font-geist-mono",
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
subsets: ["latin"],
|
import ReactQueryProvider from "@/ui/ReactQueryProvider";
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Create Next App",
|
|
||||||
description: "Generated by create next app",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
@@ -24,10 +18,29 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang="en"
|
lang="fa"
|
||||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
dir="rtl"
|
||||||
|
className={`${FontVazir.variable} h-full antialiased`}
|
||||||
|
style={{ fontFamily: FontVazir.style.fontFamily }}
|
||||||
>
|
>
|
||||||
<body className="min-h-full flex flex-col">{children}</body>
|
<body className="min-h-screen flex items-center justify-center">
|
||||||
|
<ReactQueryProvider>
|
||||||
|
<CacheProvider value={rtlCache}>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
<main className="w-full">
|
||||||
|
<div className=" mx-auto px-35">{children}</div>
|
||||||
|
</main>
|
||||||
|
<ToastContainer
|
||||||
|
position="top-center"
|
||||||
|
autoClose={3000}
|
||||||
|
rtl
|
||||||
|
theme="colored"
|
||||||
|
/>
|
||||||
|
</ThemeProvider>
|
||||||
|
</CacheProvider>
|
||||||
|
</ReactQueryProvider>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
17
app/not-found.tsx
Normal file
17
app/not-found.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-screen ">
|
||||||
|
<h1 className="text-9xl font-bold text-white">404</h1>
|
||||||
|
<h2 className="text-2xl font-semibold mt-4 text-neutral-50">صفحه مورد نظر پیدا نشد</h2>
|
||||||
|
<p className="text-neutral-300 mt-2">متأسفیم، صفحهای که به دنبال آن هستید وجود ندارد.</p>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="mt-6 px-6 py-2 bg-white text-blue-900 rounded-lg transition"
|
||||||
|
>
|
||||||
|
بازگشت به داشبورد
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
62
app/page.tsx
62
app/page.tsx
@@ -1,65 +1,9 @@
|
|||||||
import Image from "next/image";
|
import LoginForm from "@/ui/form/LoginForm";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
<div>
|
||||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
<LoginForm />
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js logo"
|
|
||||||
width={100}
|
|
||||||
height={20}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
|
||||||
To get started, edit the page.tsx file.
|
|
||||||
</h1>
|
|
||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Templates
|
|
||||||
</a>{" "}
|
|
||||||
or the{" "}
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Learning
|
|
||||||
</a>{" "}
|
|
||||||
center.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Deploy Now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
10
app/theme/rtlCache.ts
Normal file
10
app/theme/rtlCache.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import createCache from "@emotion/cache";
|
||||||
|
import { prefixer } from "stylis";
|
||||||
|
import rtlPlugin from "stylis-plugin-rtl";
|
||||||
|
|
||||||
|
const rtlCache = createCache({
|
||||||
|
key: "muirtl",
|
||||||
|
stylisPlugins: [prefixer, rtlPlugin],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default rtlCache;
|
||||||
10
app/theme/theme.ts
Normal file
10
app/theme/theme.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { createTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
const theme = createTheme({
|
||||||
|
direction: "rtl",
|
||||||
|
typography: {
|
||||||
|
fontFamily: "var(--font-vazir)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default theme;
|
||||||
48
config/font.config.ts
Normal file
48
config/font.config.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import localFont from "next/font/local";
|
||||||
|
|
||||||
|
export const FontVazir = localFont({
|
||||||
|
src: [
|
||||||
|
{
|
||||||
|
path: "../fonts/vazir/Vazirmatn-Thin.woff2",
|
||||||
|
weight: "100",
|
||||||
|
style: "normal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "../fonts/vazir/Vazirmatn-ExtraLight.woff2",
|
||||||
|
weight: "200",
|
||||||
|
style: "normal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "../fonts/vazir/Vazirmatn-Light.woff2",
|
||||||
|
weight: "300",
|
||||||
|
style: "normal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "../fonts/vazir/Vazirmatn-Regular.woff2",
|
||||||
|
weight: "400",
|
||||||
|
style: "normal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "../fonts/vazir/Vazirmatn-Medium.woff2",
|
||||||
|
weight: "500",
|
||||||
|
style: "normal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "../fonts/vazir/Vazirmatn-SemiBold.woff2",
|
||||||
|
weight: "600",
|
||||||
|
style: "normal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "../fonts/vazir/Vazirmatn-Bold.woff2",
|
||||||
|
weight: "700",
|
||||||
|
style: "normal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "../fonts/vazir/Vazirmatn-ExtraBold.woff2",
|
||||||
|
weight: "800",
|
||||||
|
style: "normal",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variable: "--font-vazir", // اگر خواستی متغیر CSS بسازی برای Tailwind یا CSS مدول
|
||||||
|
display: "swap", // بهترین گزینه برای نمایش فونت
|
||||||
|
});
|
||||||
109
core/constant/index.ts
Normal file
109
core/constant/index.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
export const API_URL = "http://localhost:8000/api/v1";
|
||||||
|
export const requestType = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
name: "software",
|
||||||
|
displayName: "نرم افزار",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
name: "hardware",
|
||||||
|
displayName: "سخت افزار",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
name: "his",
|
||||||
|
displayName: "HIS",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
name: "general",
|
||||||
|
displayName: "عمومي",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
export const ticketStatuses = [
|
||||||
|
{
|
||||||
|
id: "open",
|
||||||
|
displayName: "باز",
|
||||||
|
bgClass: "bg-blue-50 text-blue-700 ring-blue-200",
|
||||||
|
dotClass: "bg-blue-600",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "in-progress",
|
||||||
|
displayName: "در حال بررسی",
|
||||||
|
bgClass: "bg-amber-50 text-amber-700 ring-amber-200",
|
||||||
|
dotClass: "bg-amber-600",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "resolved",
|
||||||
|
displayName: "حل شده",
|
||||||
|
bgClass: "bg-emerald-50 text-emerald-700 ring-emerald-200",
|
||||||
|
dotClass: "bg-emerald-600",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "closed",
|
||||||
|
displayName: "بسته شده",
|
||||||
|
bgClass: "bg-slate-100 text-slate-700 ring-slate-300",
|
||||||
|
dotClass: "bg-slate-500",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ticketPriorities = [
|
||||||
|
{
|
||||||
|
id: "low",
|
||||||
|
displayName: "پایین",
|
||||||
|
bgClass: "bg-slate-50 text-slate-700 ring-slate-200",
|
||||||
|
dotClass: "bg-slate-500",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "medium",
|
||||||
|
displayName: "متوسط",
|
||||||
|
bgClass: "bg-sky-50 text-sky-700 ring-sky-200",
|
||||||
|
dotClass: "bg-sky-600",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "high",
|
||||||
|
displayName: "بالا",
|
||||||
|
bgClass: "bg-orange-50 text-orange-700 ring-orange-200",
|
||||||
|
dotClass: "bg-orange-600",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "critical",
|
||||||
|
displayName: "بحرانی",
|
||||||
|
bgClass: "bg-rose-50 text-rose-700 ring-rose-200",
|
||||||
|
dotClass: "bg-rose-600",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const hospitalSoftwares = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
name: "rahkaran",
|
||||||
|
displayName: "راهكاران",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
name: "timex",
|
||||||
|
displayName: "تايمكس",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
name: "automation",
|
||||||
|
displayName: "اتوماسيون",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
name: "pacs",
|
||||||
|
displayName: "پكس",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5",
|
||||||
|
name: "kasra",
|
||||||
|
displayName: "كسري",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "6",
|
||||||
|
name: "ican",
|
||||||
|
displayName: "ican",
|
||||||
|
},
|
||||||
|
];
|
||||||
37
core/types/index.ts
Normal file
37
core/types/index.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// اینترفیس برای اطلاعات اساسی اشیاء وابسته
|
||||||
|
interface Assignee {
|
||||||
|
fullname: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Department {
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// اینترفیس اصلی تیکت
|
||||||
|
export interface TicketInterface {
|
||||||
|
id: string;
|
||||||
|
ticketNumber: string;
|
||||||
|
description: string;
|
||||||
|
priority: "low" | "medium" | "high" | "critical"; // اگر مقادیر مشخصی داری
|
||||||
|
status: "open" | "pending" | "resolved" | "closed";
|
||||||
|
requestType: string;
|
||||||
|
relatedSystem: string;
|
||||||
|
location: string;
|
||||||
|
internalPhone: string;
|
||||||
|
helpdeskAction: string;
|
||||||
|
finalNotes: string;
|
||||||
|
|
||||||
|
// فیلدهای کلیدی (FK)
|
||||||
|
departmentId: string;
|
||||||
|
assignedTo: string;
|
||||||
|
createdBy: string;
|
||||||
|
|
||||||
|
// فیلدهای شامل شده (Include)
|
||||||
|
assignee: Assignee;
|
||||||
|
department: Department;
|
||||||
|
|
||||||
|
// تاریخها
|
||||||
|
resolvedAt: Date | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
91
core/utils/index.ts
Normal file
91
core/utils/index.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import * as XLSX from "xlsx";
|
||||||
|
import { saveAs } from "file-saver";
|
||||||
|
export function handleAxiosError(error: unknown) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
// اینجا میدونیم که خطا از axios است
|
||||||
|
return error.response?.data?.error?.message;
|
||||||
|
} else {
|
||||||
|
return "Unexpected error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleExport = (data: any, type: any) => {
|
||||||
|
// ۱. تبدیل دیتا به یک Worksheet
|
||||||
|
const worksheet = XLSX.utils.json_to_sheet(data);
|
||||||
|
|
||||||
|
// ۲. ایجاد یک Workbook جدید
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, "Data");
|
||||||
|
|
||||||
|
if (type === "excel") {
|
||||||
|
// خروجی اکسل
|
||||||
|
const excelBuffer = XLSX.write(workbook, {
|
||||||
|
bookType: "xlsx",
|
||||||
|
type: "array",
|
||||||
|
});
|
||||||
|
const blob = new Blob([excelBuffer], {
|
||||||
|
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
});
|
||||||
|
saveAs(blob, "data.xlsx");
|
||||||
|
} else if (type === "spss") {
|
||||||
|
// برای SPSS، بهترین فرمت CSV است که در SPSS به خوبی باز میشود
|
||||||
|
const csvData = XLSX.utils.sheet_to_csv(worksheet);
|
||||||
|
const blob = new Blob([csvData], { type: "text/csv;charset=utf-8;" });
|
||||||
|
saveAs(blob, "data.csv"); // فایل CSV در SPSS به راحتی Import میشود
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exportToExcel = (
|
||||||
|
type: "spss" | "excel",
|
||||||
|
formattedData: any,
|
||||||
|
filename: string = "Report",
|
||||||
|
) => {
|
||||||
|
// تبدیل دادهها به فرمت قابل فهم برای XLSX
|
||||||
|
|
||||||
|
let dataToProcess = Array.isArray(formattedData)
|
||||||
|
? formattedData
|
||||||
|
: [formattedData];
|
||||||
|
const worksheet = XLSX.utils.json_to_sheet(formattedData);
|
||||||
|
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, filename);
|
||||||
|
|
||||||
|
if (type === "excel") {
|
||||||
|
// خروجی اکسل
|
||||||
|
const excelBuffer = XLSX.write(workbook, {
|
||||||
|
bookType: "xlsx",
|
||||||
|
type: "array",
|
||||||
|
});
|
||||||
|
const blob = new Blob([excelBuffer], {
|
||||||
|
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
});
|
||||||
|
saveAs(blob, `${filename}.xlsx`);
|
||||||
|
} else if (type === "spss") {
|
||||||
|
// برای SPSS، بهترین فرمت CSV است که در SPSS به خوبی باز میشود
|
||||||
|
const csvData = XLSX.utils.sheet_to_csv(worksheet);
|
||||||
|
const blob = new Blob([csvData], { type: "text/csv;charset=utf-8;" });
|
||||||
|
saveAs(blob, `${filename}.csv`); // فایل CSV در SPSS به راحتی Import میشود
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export function formatDurationPersian(seconds:string) {
|
||||||
|
const sec = Math.abs(parseFloat(seconds));
|
||||||
|
|
||||||
|
if (sec < 1) return "بلافاصله"; // برای مقادیر بسیار ناچیز
|
||||||
|
if (sec < 60) return `${Math.floor(sec)} ثانیه`;
|
||||||
|
|
||||||
|
const minutes = Math.floor(sec / 60);
|
||||||
|
const remainingSeconds = Math.floor(sec % 60);
|
||||||
|
|
||||||
|
if (minutes < 60) {
|
||||||
|
return `${minutes} دقیقه و ${remainingSeconds} ثانیه`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const remainingMinutes = minutes % 60;
|
||||||
|
return `${hours} ساعت و ${remainingMinutes} دقیقه`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// مثال برای مقدار شما:
|
||||||
|
console.log(formatDurationPersian("-0.00100000000000000000"));
|
||||||
|
// خروجی: "بلافاصله" (یا ۰ ثانیه)
|
||||||
BIN
fonts/sogand/SOGAND.ttf
Normal file
BIN
fonts/sogand/SOGAND.ttf
Normal file
Binary file not shown.
BIN
fonts/vazir/Vazirmatn-Black.woff2
Normal file
BIN
fonts/vazir/Vazirmatn-Black.woff2
Normal file
Binary file not shown.
BIN
fonts/vazir/Vazirmatn-Bold.woff2
Normal file
BIN
fonts/vazir/Vazirmatn-Bold.woff2
Normal file
Binary file not shown.
BIN
fonts/vazir/Vazirmatn-ExtraBold.woff2
Normal file
BIN
fonts/vazir/Vazirmatn-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
fonts/vazir/Vazirmatn-ExtraLight.woff2
Normal file
BIN
fonts/vazir/Vazirmatn-ExtraLight.woff2
Normal file
Binary file not shown.
BIN
fonts/vazir/Vazirmatn-Light.woff2
Normal file
BIN
fonts/vazir/Vazirmatn-Light.woff2
Normal file
Binary file not shown.
BIN
fonts/vazir/Vazirmatn-Medium.woff2
Normal file
BIN
fonts/vazir/Vazirmatn-Medium.woff2
Normal file
Binary file not shown.
BIN
fonts/vazir/Vazirmatn-Regular.woff2
Normal file
BIN
fonts/vazir/Vazirmatn-Regular.woff2
Normal file
Binary file not shown.
BIN
fonts/vazir/Vazirmatn-SemiBold.woff2
Normal file
BIN
fonts/vazir/Vazirmatn-SemiBold.woff2
Normal file
Binary file not shown.
BIN
fonts/vazir/Vazirmatn-Thin.woff2
Normal file
BIN
fonts/vazir/Vazirmatn-Thin.woff2
Normal file
Binary file not shown.
54
middleware.ts
Normal file
54
middleware.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// middleware.ts
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import type { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
// کلید کوکی که توکن در آن ذخیره میشود
|
||||||
|
const AUTH_COOKIE_KEY = "userToken"; // <<< این را با نام واقعی کوکی خودتان جایگزین کنید
|
||||||
|
|
||||||
|
export function middleware(request: NextRequest) {
|
||||||
|
const token = request.cookies.get(AUTH_COOKIE_KEY)?.value;
|
||||||
|
const { pathname } = request.nextUrl;
|
||||||
|
|
||||||
|
// مسیرهای عمومی که نیاز به احراز هویت ندارند (مثلا برای لاگین و ثبت نام)
|
||||||
|
const publicPaths = ["/"];
|
||||||
|
|
||||||
|
// اگر کاربر توکن دارد
|
||||||
|
if (token) {
|
||||||
|
// اگر در مسیر لاگین است، به داشبورد هدایت کن
|
||||||
|
if (pathname === "/") {
|
||||||
|
const url = request.nextUrl.clone();
|
||||||
|
url.pathname = "/tickets/create";
|
||||||
|
return NextResponse.redirect(url);
|
||||||
|
}
|
||||||
|
// در غیر این صورت، اجازه دسترسی به مسیر فعلی را بده
|
||||||
|
return NextResponse.next();
|
||||||
|
} else {
|
||||||
|
// اگر کاربر توکن ندارد
|
||||||
|
// اگر در مسیرهای عمومی است، اجازه دسترسی بده
|
||||||
|
if (publicPaths.includes(pathname)) {
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
// اگر در مسیرهای خصوصی است (مثل داشبورد)، به صفحه لاگین هدایت کن
|
||||||
|
if (pathname.startsWith("/tickets/create")) {
|
||||||
|
const url = request.nextUrl.clone();
|
||||||
|
url.pathname = "/";
|
||||||
|
return NextResponse.redirect(url);
|
||||||
|
}
|
||||||
|
// برای سایر مسیرهای خصوصی ناشناس
|
||||||
|
return NextResponse.next(); // یا هدایت به صفحه لاگین
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// تنظیمات middleware: کدام مسیرها را پوشش دهد
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
/*
|
||||||
|
* Match all request paths except for the ones starting with:
|
||||||
|
* - api (API routes)
|
||||||
|
* - _next/static (static files)
|
||||||
|
* - _next/image (image optimization files)
|
||||||
|
* - favicon.ico (favicon file)
|
||||||
|
*/
|
||||||
|
"/((?!api|_next/static|_next/image|favicon.ico).*)",
|
||||||
|
],
|
||||||
|
};
|
||||||
1210
package-lock.json
generated
1210
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@@ -9,15 +9,33 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@mui/icons-material": "^9.0.1",
|
||||||
|
"@mui/material": "^9.0.1",
|
||||||
|
"@mui/material-nextjs": "^9.0.1",
|
||||||
|
"@mui/x-date-pickers": "^9.3.0",
|
||||||
|
"@tanstack/react-query": "^5.100.11",
|
||||||
|
"axios": "^1.16.1",
|
||||||
|
"date-fns": "^4.3.0",
|
||||||
|
"date-fns-jalali": "^4.0.0-0",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"next": "16.2.6",
|
"next": "16.2.6",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4"
|
"react-dom": "19.2.4",
|
||||||
|
"react-paginate": "^8.3.0",
|
||||||
|
"react-toastify": "^11.1.0",
|
||||||
|
"stylis": "^4.4.0",
|
||||||
|
"stylis-plugin-rtl": "^2.1.1",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"@types/stylis": "^4.2.7",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.2.6",
|
"eslint-config-next": "16.2.6",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
|||||||
6
services/api/auth.api.ts
Normal file
6
services/api/auth.api.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import callAPI from "../caller/config";
|
||||||
|
|
||||||
|
export async function loginUser(data: any) {
|
||||||
|
return await callAPI.post("/auth/login", data).then((res) => res.data);
|
||||||
|
}
|
||||||
|
|
||||||
5
services/api/department.api.ts
Normal file
5
services/api/department.api.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import callAPI from "../caller/config";
|
||||||
|
|
||||||
|
export async function getDepartments() {
|
||||||
|
return await callAPI.get("/department/all").then((res) => res.data);
|
||||||
|
}
|
||||||
53
services/api/report.api.ts
Normal file
53
services/api/report.api.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import callAPI from "../caller/config";
|
||||||
|
|
||||||
|
export async function getStatsReport() {
|
||||||
|
return await callAPI.get("/report/stats").then((res) => res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDepartmentReport() {
|
||||||
|
return await callAPI.get("/report/departments").then((res) => res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAgentPerformance() {
|
||||||
|
return await callAPI.get("/report/agents").then((res) => res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAvgResolutionTime() {
|
||||||
|
return await callAPI.get("/report/avg-resolution").then((res) => res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCriticalTickets() {
|
||||||
|
return await callAPI.get("/report/critical").then((res) => res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTicketsTrend(params: any) {
|
||||||
|
return await callAPI.get("/report/trend", { params }).then((res) => res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getClosureRate() {
|
||||||
|
return await callAPI.get("/report/closure-rate").then((res) => res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSlaBreach() {
|
||||||
|
return await callAPI.get("/report/sla").then((res) => res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAgingReport() {
|
||||||
|
return await callAPI.get("/report/aging").then((res) => res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAgentEfficiency() {
|
||||||
|
return await callAPI.get("/report/agent-efficiency").then((res) => res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDepartmentLoad() {
|
||||||
|
return await callAPI.get("/report/department-load").then((res) => res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getKpiReport() {
|
||||||
|
return await callAPI.get("/report/kpi").then((res) => res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPredictionReport() {
|
||||||
|
return await callAPI.get("/report/prediction").then((res) => res.data);
|
||||||
|
}
|
||||||
17
services/api/ticket.api.ts
Normal file
17
services/api/ticket.api.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import callAPI from "../caller/config";
|
||||||
|
|
||||||
|
export async function createTicket(data: any) {
|
||||||
|
return await callAPI.post("/ticket/create", data).then((res) => res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllTickets(params: any) {
|
||||||
|
return await callAPI.get("/ticket/all", { params }).then((res) => res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeTicket(id: string) {
|
||||||
|
return await callAPI.delete(`/ticket/remove/${id}`).then((res) => res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllTicketsExport(params: any) {
|
||||||
|
return await callAPI.get("/ticket/all/export", { params }).then((res) => res.data);
|
||||||
|
}
|
||||||
5
services/api/users.api.ts
Normal file
5
services/api/users.api.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import callAPI from "../caller/config";
|
||||||
|
|
||||||
|
export async function getUsers() {
|
||||||
|
return await callAPI.get("/user/all").then((res) => res.data);
|
||||||
|
}
|
||||||
25
services/caller/config.ts
Normal file
25
services/caller/config.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const callAPISetting = axios.create({
|
||||||
|
baseURL: "http://localhost:8000/api/v1",
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
callAPISetting.interceptors.request.use(
|
||||||
|
(res) => res,
|
||||||
|
(err) => Promise.reject(err),
|
||||||
|
);
|
||||||
|
|
||||||
|
callAPISetting.interceptors.response.use(
|
||||||
|
(res) => res,
|
||||||
|
async (err) => Promise.reject(err),
|
||||||
|
);
|
||||||
|
|
||||||
|
const callAPI = {
|
||||||
|
post: callAPISetting.post,
|
||||||
|
get: callAPISetting.get,
|
||||||
|
put: callAPISetting.put,
|
||||||
|
delete: callAPISetting.delete,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default callAPI;
|
||||||
12
services/hooks/department.hook.ts
Normal file
12
services/hooks/department.hook.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getDepartments } from "../api/department.api";
|
||||||
|
|
||||||
|
export const useGetAllDepartments = () => {
|
||||||
|
return useQuery({
|
||||||
|
// قرار دادن پارامترها در queryKey باعث میشود با تغییر هر کدام، کش باطل و درخواست جدید ارسال شود
|
||||||
|
queryKey: ['get-all-tickets'],
|
||||||
|
queryFn: () => getDepartments(),
|
||||||
|
retry: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
30
services/hooks/report.hook.ts
Normal file
30
services/hooks/report.hook.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import * as api from "../api/report.api";
|
||||||
|
|
||||||
|
// هوکهای Mutation برای دریافت گزارشها به صورت دستی (On-Demand)
|
||||||
|
|
||||||
|
export const useMutateStatsReport = () => useMutation({ mutationFn: api.getStatsReport });
|
||||||
|
|
||||||
|
export const useMutateDepartmentReport = () => useMutation({ mutationFn: api.getDepartmentReport });
|
||||||
|
|
||||||
|
export const useMutateAgentPerformance = () => useMutation({ mutationFn: api.getAgentPerformance });
|
||||||
|
|
||||||
|
export const useMutateAvgResolutionTime = () => useMutation({ mutationFn: api.getAvgResolutionTime });
|
||||||
|
|
||||||
|
export const useMutateCriticalTickets = () => useMutation({ mutationFn: api.getCriticalTickets });
|
||||||
|
|
||||||
|
export const useMutateTicketsTrend = () => useMutation({ mutationFn: (params: any) => api.getTicketsTrend(params) });
|
||||||
|
|
||||||
|
export const useMutateClosureRate = () => useMutation({ mutationFn: api.getClosureRate });
|
||||||
|
|
||||||
|
export const useMutateSlaBreach = () => useMutation({ mutationFn: api.getSlaBreach });
|
||||||
|
|
||||||
|
export const useMutateAgingReport = () => useMutation({ mutationFn: api.getAgingReport });
|
||||||
|
|
||||||
|
export const useMutateAgentEfficiency = () => useMutation({ mutationFn: api.getAgentEfficiency });
|
||||||
|
|
||||||
|
export const useMutateDepartmentLoad = () => useMutation({ mutationFn: api.getDepartmentLoad });
|
||||||
|
|
||||||
|
export const useMutateKpiReport = () => useMutation({ mutationFn: api.getKpiReport });
|
||||||
|
|
||||||
|
export const useMutatePredictionReport = () => useMutation({ mutationFn: api.getPredictionReport });
|
||||||
20
services/hooks/ticket.hook.ts
Normal file
20
services/hooks/ticket.hook.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
import { getAllTickets, getAllTicketsExport, removeTicket } from "../api/ticket.api";
|
||||||
|
|
||||||
|
export const useGetAllTickets = (params: {
|
||||||
|
page: number;
|
||||||
|
departmentId?: string;
|
||||||
|
priority?: string;
|
||||||
|
status?: string;
|
||||||
|
}) => {
|
||||||
|
return useQuery({
|
||||||
|
// قرار دادن پارامترها در queryKey باعث میشود با تغییر هر کدام، کش باطل و درخواست جدید ارسال شود
|
||||||
|
queryKey: ["get-all-tickets", params],
|
||||||
|
queryFn: () => getAllTickets(params),
|
||||||
|
retry: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRemoveTicket = () => useMutation({mutationFn:removeTicket})
|
||||||
|
export const useGetAllTicketsExport = () => useMutation({mutationFn:getAllTicketsExport})
|
||||||
8
services/hooks/users.hook.ts
Normal file
8
services/hooks/users.hook.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getUsers } from "../api/users.api";
|
||||||
|
|
||||||
|
export const useGetAllUsers = () =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: ["get-all-users"],
|
||||||
|
queryFn: getUsers,
|
||||||
|
});
|
||||||
16
tailwind.config.ts
Normal file
16
tailwind.config.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// tailwind.config.ts
|
||||||
|
export default {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
animation: {
|
||||||
|
blink: 'blink 1.5s infinite',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
blink: {
|
||||||
|
'0%, 100%': { opacity: '1' },
|
||||||
|
'50%': { opacity: '0.4' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
32
ui/DateFilter.tsx
Normal file
32
ui/DateFilter.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||||
|
import { TextField, Box } from "@mui/material";
|
||||||
|
|
||||||
|
const DateFilters = ({
|
||||||
|
startDate,
|
||||||
|
setStartDate,
|
||||||
|
endDate,
|
||||||
|
setEndDate,
|
||||||
|
}: {
|
||||||
|
startDate: any;
|
||||||
|
setStartDate: any;
|
||||||
|
endDate: any;
|
||||||
|
setEndDate: any;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: "flex", gap: 2, my: 2 }}>
|
||||||
|
<DatePicker
|
||||||
|
label="از تاریخ"
|
||||||
|
value={startDate}
|
||||||
|
onChange={(newValue) => setStartDate(newValue)}
|
||||||
|
slotProps={{ textField: { fullWidth: true } }}
|
||||||
|
/>
|
||||||
|
<DatePicker
|
||||||
|
label="تا تاریخ"
|
||||||
|
value={endDate}
|
||||||
|
onChange={(newValue) => setEndDate(newValue)}
|
||||||
|
slotProps={{ textField: { fullWidth: true } }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default DateFilters;
|
||||||
80
ui/DeleteConfirmModal.tsx
Normal file
80
ui/DeleteConfirmModal.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Button,
|
||||||
|
Typography,
|
||||||
|
IconButton,
|
||||||
|
} from "@mui/material";
|
||||||
|
import WarningAmberIcon from "@mui/icons-material/WarningAmber";
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
|
||||||
|
export default function DeleteConfirmModal({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
title = "آیا از حذف این مورد مطمئن هستید؟",
|
||||||
|
description = "بعد از حذف، امکان بازگردانی وجود ندارد.",
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onClose} maxWidth="xs">
|
||||||
|
<DialogTitle className="flex items-center justify-between text-slate-800">
|
||||||
|
<span className="font-semibold">تأیید حذف</span>
|
||||||
|
<IconButton onClick={onClose} size="small">
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent className="flex flex-col items-center text-center space-y-3 pt-4">
|
||||||
|
<WarningAmberIcon className="text-yellow-500" sx={{ fontSize: 50 }} />
|
||||||
|
|
||||||
|
<Typography className="text-lg font-semibold text-slate-700">
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography className="text-slate-500 text-sm leading-relaxed">
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions className="flex justify-between px-4 pb-3 mt-2">
|
||||||
|
<Button
|
||||||
|
onClick={onClose}
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
borderRadius: "8px",
|
||||||
|
color: "#475569",
|
||||||
|
borderColor: "#cbd5e1",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "#f1f5f9",
|
||||||
|
borderColor: "#94a3b8",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
لغو
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={onConfirm}
|
||||||
|
variant="contained"
|
||||||
|
sx={{
|
||||||
|
borderRadius: "8px",
|
||||||
|
backgroundColor: "#dc2626",
|
||||||
|
"&:hover": { backgroundColor: "#b91c1c" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
حذف آیتم
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
ui/ReactQueryProvider.tsx
Normal file
30
ui/ReactQueryProvider.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function ReactQueryProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const [queryClient] = useState(
|
||||||
|
() =>
|
||||||
|
new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 1000 * 60,
|
||||||
|
retry: 1,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
retry: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
ui/SearchBox.tsx
Normal file
31
ui/SearchBox.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { ChangeEventHandler } from 'react';
|
||||||
|
import { TextField } from '@mui/material';
|
||||||
|
|
||||||
|
const SearchBox = ({ placeholder, onChange }:{placeholder:string,onChange:ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement, Element>}) => {
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
placeholder={placeholder || "جستجو کنید..."}
|
||||||
|
onChange={onChange}
|
||||||
|
fullWidth
|
||||||
|
|
||||||
|
sx={{
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
borderRadius: '16px', // گرد کردن لبهها برای ظاهر مدرن
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
'& fieldset': {
|
||||||
|
borderColor: 'transparent', // حذف خط پیشفرض
|
||||||
|
},
|
||||||
|
'&:hover fieldset': {
|
||||||
|
borderColor: '#1976d2',
|
||||||
|
},
|
||||||
|
'&.Mui-focused fieldset': {
|
||||||
|
borderColor: '#1976d2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchBox;
|
||||||
83
ui/Test.tsx
Normal file
83
ui/Test.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Typography,
|
||||||
|
Button,
|
||||||
|
Stack,
|
||||||
|
} from "@mui/material";
|
||||||
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
|
import FileDownloadIcon from "@mui/icons-material/FileDownload";
|
||||||
|
|
||||||
|
const DataCategories = () => {
|
||||||
|
// لیست کارتها (میتوانید این را از دیتابیس بگیرید)
|
||||||
|
const categories = [
|
||||||
|
{ id: 1, title: "آماركلي" },
|
||||||
|
{ id: 2, title: "تیکتهای در انتظار" },
|
||||||
|
{ id: 3, title: "تيكت هاي بحراني" },
|
||||||
|
{ id: 4, title: "گزارشهای ماهانه" },
|
||||||
|
{ id: 5, title: "گزارشهای ماهانه" },
|
||||||
|
{ id: 6, title: "گزارشهای ماهانه" },
|
||||||
|
{ id: 7, title: "گزارشهای ماهانه" },
|
||||||
|
{ id: 8, title: "گزارشهای ماهانه" },
|
||||||
|
{ id: 9, title: "گزارشهای ماهانه" },
|
||||||
|
{ id: 10, title: "گزارشهای ماهانه" },
|
||||||
|
{ id: 11, title: "گزارشهای ماهانه" },
|
||||||
|
{ id: 12, title: "گزارشهای ماهانه" },
|
||||||
|
{ id: 13, title: "گزارشهای ماهانه" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
// ریسپانسیو بودن: در موبایل ۱ ستون، در تبلت ۲، در دسکتاپ ۴ ستون
|
||||||
|
gridTemplateColumns: {
|
||||||
|
xs: "1fr",
|
||||||
|
sm: "1fr 1fr",
|
||||||
|
md: "repeat(6, 1fr)",
|
||||||
|
},
|
||||||
|
gap: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{categories.map((item) => (
|
||||||
|
<Card
|
||||||
|
key={item.id}
|
||||||
|
sx={{
|
||||||
|
aspectRatio: "1/1", // مربع کردن کارت
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
transition: "0.3s",
|
||||||
|
"&:hover": { boxShadow: 6, transform: "translateY(-5px)" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" align="center" gutterBottom>
|
||||||
|
{item.title}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<Stack spacing={1} sx={{ p: 2 }}>
|
||||||
|
{/* <Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<VisibilityIcon />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
مشاهده
|
||||||
|
</Button> */}
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<FileDownloadIcon />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
اکسل
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DataCategories;
|
||||||
115
ui/TicketDetailModel.tsx
Normal file
115
ui/TicketDetailModel.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
IconButton,
|
||||||
|
Typography,
|
||||||
|
Chip,
|
||||||
|
Divider,
|
||||||
|
} from "@mui/material";
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
import { ticketPriorities, ticketStatuses } from "@/core/constant";
|
||||||
|
import { TicketInterface } from "@/core/types";
|
||||||
|
|
||||||
|
export default function TicketDetailModal({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
ticket,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
ticket: TicketInterface | null;
|
||||||
|
}) {
|
||||||
|
if (!ticket) return null;
|
||||||
|
const status = ticketStatuses.find((p) => p.id === ticket?.status);
|
||||||
|
const priority = ticketPriorities.find((p) => p.id === ticket?.priority);
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onClose} fullWidth maxWidth="sm">
|
||||||
|
<DialogTitle className="flex justify-between items-center text-slate-800">
|
||||||
|
<span>جزئیات تیکت #{ticket.id.slice(-6)}</span>
|
||||||
|
<IconButton onClick={onClose} size="small">
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent dividers>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Typography variant="subtitle2" className="text-slate-500">
|
||||||
|
واحد / بخش
|
||||||
|
</Typography>
|
||||||
|
<Typography className="font-semibold text-slate-900">
|
||||||
|
{ticket.department?.displayName}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="subtitle2" className="text-slate-500">
|
||||||
|
كاربر
|
||||||
|
</Typography>
|
||||||
|
<Typography className="font-semibold text-slate-900">
|
||||||
|
{ticket.createdBy}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="subtitle2" className="text-slate-500">
|
||||||
|
ارجاع به
|
||||||
|
</Typography>
|
||||||
|
<Typography className="font-semibold text-slate-900">
|
||||||
|
{ticket.assignee.fullname}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-start gap-4">
|
||||||
|
<div>
|
||||||
|
<Typography variant="subtitle2" className="text-slate-500">
|
||||||
|
وضعیت
|
||||||
|
</Typography>
|
||||||
|
<Chip
|
||||||
|
label={status?.displayName}
|
||||||
|
className={`mt-1 bg-blue-100 text-blue-700 font-medium`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="subtitle2" className="text-slate-500">
|
||||||
|
اولویت
|
||||||
|
</Typography>
|
||||||
|
<Chip
|
||||||
|
label={priority?.displayName}
|
||||||
|
className={`mt-1 bg-red-100 text-red-700 font-medium`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="subtitle2" className="text-slate-500">
|
||||||
|
محل وقوع مشكل
|
||||||
|
</Typography>
|
||||||
|
<Chip
|
||||||
|
label={ticket?.location}
|
||||||
|
className={`mt-1 bg-blue-100 text-blue-700 font-medium`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Typography variant="subtitle2" className="text-slate-500">
|
||||||
|
توضیحات
|
||||||
|
</Typography>
|
||||||
|
<Typography className="text-slate-700 mt-2 leading-relaxed bg-slate-50 p-3 rounded-lg border border-slate-100">
|
||||||
|
{ticket.description || "توضیحی ثبت نشده است."}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="subtitle2" className="text-slate-500">
|
||||||
|
اقدام كارشناس
|
||||||
|
</Typography>
|
||||||
|
<Typography className="text-slate-700 mt-2 leading-relaxed bg-slate-50 p-3 rounded-lg border border-slate-100">
|
||||||
|
{ticket.helpdeskAction || "توضیحی ثبت نشده است."}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
158
ui/form/LoginForm.tsx
Normal file
158
ui/form/LoginForm.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Box, Button, TextField, Paper } from "@mui/material";
|
||||||
|
import { handleAxiosError } from "@/core/utils";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { loginUser } from "@/services/api/auth.api";
|
||||||
|
|
||||||
|
export default function LoginForm() {
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setForm({
|
||||||
|
...form,
|
||||||
|
[e.target.name]: e.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
const { message } = await loginUser(form);
|
||||||
|
toast.success(message);
|
||||||
|
router.push("/");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(handleAxiosError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
minHeight: "100vh",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
width: 420,
|
||||||
|
p: 5,
|
||||||
|
borderRadius: 4,
|
||||||
|
backdropFilter: "blur(10px)",
|
||||||
|
background: "rgba(255,255,255,0.9)",
|
||||||
|
boxShadow: "0 20px 60px rgba(0,0,0,0.15)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h5 className="font-medium text-center mb-5 text-xl">
|
||||||
|
ورود به سيستم ثبت تيكت ها
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="نام کاربری"
|
||||||
|
name="username"
|
||||||
|
value={form.username}
|
||||||
|
onChange={handleChange}
|
||||||
|
margin="normal"
|
||||||
|
// InputProps={{
|
||||||
|
// startAdornment: (
|
||||||
|
// <InputAdornment position="start">
|
||||||
|
// <Person sx={{ color: "#1976d2" }} />
|
||||||
|
// </InputAdornment>
|
||||||
|
// ),
|
||||||
|
// }}
|
||||||
|
sx={{
|
||||||
|
mb: 2,
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
borderRadius: 3,
|
||||||
|
background: "#f9fbff",
|
||||||
|
"& fieldset": {
|
||||||
|
borderColor: "#dbeafe",
|
||||||
|
},
|
||||||
|
"&:hover fieldset": {
|
||||||
|
borderColor: "#90caf9",
|
||||||
|
},
|
||||||
|
"&.Mui-focused fieldset": {
|
||||||
|
borderColor: "#1976d2",
|
||||||
|
borderWidth: "2px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="رمز عبور"
|
||||||
|
name="password"
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
value={form.password}
|
||||||
|
onChange={handleChange}
|
||||||
|
margin="normal"
|
||||||
|
// InputProps={{
|
||||||
|
// startAdornment: (
|
||||||
|
// <InputAdornment position="start">
|
||||||
|
// <Lock sx={{ color: "#1976d2" }} />
|
||||||
|
// </InputAdornment>
|
||||||
|
// ),
|
||||||
|
// endAdornment: (
|
||||||
|
// <InputAdornment position="end">
|
||||||
|
// <IconButton onClick={() => setShowPassword(!showPassword)}>
|
||||||
|
// {showPassword ? <VisibilityOff /> : <Visibility />}
|
||||||
|
// </IconButton>
|
||||||
|
// </InputAdornment>
|
||||||
|
// ),
|
||||||
|
// }}
|
||||||
|
sx={{
|
||||||
|
mb: 3,
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
borderRadius: 3,
|
||||||
|
background: "#f9fbff",
|
||||||
|
"& fieldset": {
|
||||||
|
borderColor: "#dbeafe",
|
||||||
|
},
|
||||||
|
"&:hover fieldset": {
|
||||||
|
borderColor: "#90caf9",
|
||||||
|
},
|
||||||
|
"&.Mui-focused fieldset": {
|
||||||
|
borderColor: "#1976d2",
|
||||||
|
borderWidth: "2px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
sx={{
|
||||||
|
py: 1.5,
|
||||||
|
borderRadius: 3,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 600,
|
||||||
|
background: "linear-gradient(90deg,#1976d2,#42a5f5,#64b5f6)",
|
||||||
|
boxShadow: "0 10px 25px rgba(25,118,210,0.4)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "linear-gradient(90deg,#1565c0,#1e88e5,#42a5f5)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
ورود
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
268
ui/form/TicketForm.tsx
Normal file
268
ui/form/TicketForm.tsx
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Paper,
|
||||||
|
Typography,
|
||||||
|
TextField,
|
||||||
|
MenuItem,
|
||||||
|
Button,
|
||||||
|
Grid,
|
||||||
|
} from "@mui/material";
|
||||||
|
import {
|
||||||
|
hospitalSoftwares,
|
||||||
|
requestType,
|
||||||
|
ticketPriorities,
|
||||||
|
ticketStatuses,
|
||||||
|
} from "@/core/constant";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { handleAxiosError } from "@/core/utils";
|
||||||
|
import { createTicket } from "@/services/api/ticket.api";
|
||||||
|
interface DepartmentType {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
export interface IUserListItem {
|
||||||
|
id: string;
|
||||||
|
fullname: string;
|
||||||
|
nationalCode: string;
|
||||||
|
mobile: string;
|
||||||
|
roleId: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TicketForm({
|
||||||
|
departments,
|
||||||
|
users,
|
||||||
|
}: {
|
||||||
|
departments: DepartmentType[];
|
||||||
|
users: IUserListItem[];
|
||||||
|
}) {
|
||||||
|
const [showFieldRelatedSystem, setShowFieldRelatedSystem] = useState(false);
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
createdBy: "",
|
||||||
|
departmentId: "",
|
||||||
|
internalPhone: "",
|
||||||
|
requestType: "",
|
||||||
|
priority: "medium",
|
||||||
|
description: "",
|
||||||
|
relatedSystem: "",
|
||||||
|
location: "",
|
||||||
|
helpdeskAction: "",
|
||||||
|
assignedTo: "",
|
||||||
|
status: "open",
|
||||||
|
finalNotes: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChange = (e: any) => {
|
||||||
|
setForm({
|
||||||
|
...form,
|
||||||
|
[e.target.name]: e.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { message } = await createTicket(form);
|
||||||
|
toast.success(message);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(handleAxiosError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (form.requestType === "1") {
|
||||||
|
form.relatedSystem = "";
|
||||||
|
setShowFieldRelatedSystem(true);
|
||||||
|
} else {
|
||||||
|
form.relatedSystem = "";
|
||||||
|
setShowFieldRelatedSystem(false);
|
||||||
|
}
|
||||||
|
}, [form.requestType]);
|
||||||
|
return (
|
||||||
|
<div className=" h-full w-full">
|
||||||
|
<div className="bg-white p-6 rounded-2xl">
|
||||||
|
<h5 className="mb-5 text-center text-xl font-bold">فرم تيكت</h5>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div className="flex items-center justify-start gap-x-5">
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="نام و نام خانوادگي كاربر"
|
||||||
|
name="createdBy"
|
||||||
|
value={form.createdBy}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
size="medium"
|
||||||
|
label="واحد / بخش"
|
||||||
|
name="departmentId"
|
||||||
|
value={form.departmentId}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
{departments.map((dep) => (
|
||||||
|
<MenuItem key={dep.id} value={dep.id}>
|
||||||
|
{dep.displayName}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="تلفن داخلي"
|
||||||
|
name="internalPhone"
|
||||||
|
value={form.internalPhone}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-start gap-x-5">
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
label="نوع درخواست"
|
||||||
|
name="requestType"
|
||||||
|
value={form.requestType}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
{requestType.map((dep) => (
|
||||||
|
<MenuItem key={dep.id} value={dep.id}>
|
||||||
|
{dep.displayName}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
{showFieldRelatedSystem && (
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
size="medium"
|
||||||
|
label="نرم افزار مرتبط"
|
||||||
|
name="relatedSystem"
|
||||||
|
value={form.relatedSystem}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
{hospitalSoftwares.map((item) => (
|
||||||
|
<MenuItem value={item.id}>{item.displayName}</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
)}
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="محل وقوع مشكل"
|
||||||
|
name="location"
|
||||||
|
value={form.location}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-y-5">
|
||||||
|
<TextField
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
fullWidth
|
||||||
|
label="توضيحات"
|
||||||
|
name="description"
|
||||||
|
value={form.description}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
fullWidth
|
||||||
|
label="اقدام helpdesk"
|
||||||
|
name="helpdeskAction"
|
||||||
|
value={form.helpdeskAction}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-start gap-x-5">
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
label="ارجاع به"
|
||||||
|
name="assignedTo"
|
||||||
|
value={form.assignedTo}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
{users.map((dep) => (
|
||||||
|
<MenuItem key={dep.id} value={dep.id}>
|
||||||
|
{dep.fullname}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
size="medium"
|
||||||
|
label="اولويت درخواست"
|
||||||
|
name="priority"
|
||||||
|
value={form.priority}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
{ticketPriorities?.map((p: any) => (
|
||||||
|
<MenuItem key={p.id} value={p.id}>
|
||||||
|
{p.displayName}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
label="وضعيت درخواست"
|
||||||
|
name="status"
|
||||||
|
value={form.status}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
{ticketStatuses?.map((p: any) => (
|
||||||
|
<MenuItem key={p.id} value={p.id}>
|
||||||
|
{p.displayName}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
fullWidth
|
||||||
|
label="توضيحات نهايي"
|
||||||
|
name="finalNotes"
|
||||||
|
value={form.finalNotes}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
type="submit"
|
||||||
|
sx={{
|
||||||
|
py: 1.5,
|
||||||
|
borderRadius: 3,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 600,
|
||||||
|
background: "linear-gradient(90deg,#1976d2,#42a5f5,#64b5f6)",
|
||||||
|
boxShadow: "0 10px 25px rgba(25,118,210,0.4)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "linear-gradient(90deg,#1565c0,#1e88e5,#42a5f5)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
ثبت تيكت
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
82
ui/layout/Sidebar.tsx
Normal file
82
ui/layout/Sidebar.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { Apartment, Dashboard, Logout, Notes, Person, PictureInPicture, ShowChart } from "@mui/icons-material";
|
||||||
|
import { Button } from "@mui/material";
|
||||||
|
import Link from "next/link";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function Sidebar() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="bg-white rounded-2xl h-full py-4 space-y-10 flex flex-col items-start justify-center">
|
||||||
|
{/* <Link
|
||||||
|
href={"/dashboard"}
|
||||||
|
className="flex items-center text-[#2a5298] w-full justify-start gap-x-4 hover:bg-linear-to-r hover:from-[#1e3c72] hover:via-[#2a5298] hover:to-[#4facfe] hover:text-white px-4 py-4 hover:cursor-pointer transition-all duration-300
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<Dashboard />
|
||||||
|
</span>
|
||||||
|
<span>داشبورد</span>
|
||||||
|
</Link> */}
|
||||||
|
<Link
|
||||||
|
href={"/tickets/create"}
|
||||||
|
className="flex items-center text-[#2a5298] w-full justify-start gap-x-4 hover:bg-linear-to-r hover:from-[#1e3c72] hover:via-[#2a5298] hover:to-[#4facfe] hover:text-white px-4 py-4 hover:cursor-pointer transition-all duration-300
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<PictureInPicture />
|
||||||
|
</span>
|
||||||
|
<span> ثبت تيكت</span>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={"/tickets"}
|
||||||
|
className="flex items-center text-[#2a5298] w-full justify-start gap-x-4 hover:bg-linear-to-r hover:from-[#1e3c72] hover:via-[#2a5298] hover:to-[#4facfe] hover:text-white px-4 py-4 hover:cursor-pointer transition-all duration-300
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<Notes />
|
||||||
|
</span>
|
||||||
|
<span>مشاهده تيكت ها</span>
|
||||||
|
</Link>
|
||||||
|
{/* <Link
|
||||||
|
href={"/departments"}
|
||||||
|
className="flex items-center text-[#2a5298] w-full justify-start gap-x-4 hover:bg-linear-to-r hover:from-[#1e3c72] hover:via-[#2a5298] hover:to-[#4facfe] hover:text-white px-4 py-4 hover:cursor-pointer transition-all duration-300
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<Apartment />
|
||||||
|
</span>
|
||||||
|
<span>مديريت واحد ها</span>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={"/users"}
|
||||||
|
className="flex items-center text-[#2a5298] w-full justify-start gap-x-4 hover:bg-linear-to-r hover:from-[#1e3c72] hover:via-[#2a5298] hover:to-[#4facfe] hover:text-white px-4 py-4 hover:cursor-pointer transition-all duration-300
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<Person />
|
||||||
|
</span>
|
||||||
|
<span>مديريت كاربران</span>
|
||||||
|
</Link> */}
|
||||||
|
<Link
|
||||||
|
href={"/reports"}
|
||||||
|
className="flex items-center text-[#2a5298] w-full justify-start gap-x-4 hover:bg-linear-to-r hover:from-[#1e3c72] hover:via-[#2a5298] hover:to-[#4facfe] hover:text-white px-4 py-4 hover:cursor-pointer transition-all duration-300
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<ShowChart />
|
||||||
|
</span>
|
||||||
|
<span> گزارش گيري</span>
|
||||||
|
</Link>
|
||||||
|
<Button
|
||||||
|
className="flex items-center text-[#2a5298] w-full justify-start gap-x-4 hover:cursor-pointer transition-all duration-300
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<Logout />
|
||||||
|
</span>
|
||||||
|
<span> خروج از حساب</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user