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;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
/* @theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
} */
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
/* @media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
background:linear-gradient(135deg,#1e3c72 0%, #2a5298 40%, #4facfe 100%);
|
||||
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";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
"use client";
|
||||
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
import { CacheProvider } from "@emotion/react";
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import rtlCache from "./theme/rtlCache";
|
||||
import theme from "./theme/theme";
|
||||
import { CssBaseline } from "@mui/material";
|
||||
import { FontVazir } from "@/config/font.config";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import ReactQueryProvider from "@/ui/ReactQueryProvider";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
@@ -24,10 +18,29 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
lang="fa"
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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() {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<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">
|
||||
<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>
|
||||
<LoginForm />
|
||||
</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;
|
||||
Reference in New Issue
Block a user