first commit

This commit is contained in:
2026-05-23 13:22:10 +03:30
parent 5f9ee72174
commit 6591a52f27
52 changed files with 3937 additions and 133 deletions

View File

@@ -0,0 +1,10 @@
import { API_URL } from "@/core/constant";
export default async function Page() {
return (
<>
</>
);
}

View File

@@ -0,0 +1,9 @@
import React from 'react'
export default function Page() {
return (
<div>
</div>
)
}

21
app/(panel)/layout.tsx Normal file
View 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>
);
}

View 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>
);
}

View 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} />
</>
);
}

View 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>
</>
);
}

View 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>;
}

View File

@@ -0,0 +1,5 @@
import React from "react";
export default function Page() {
return <div></div>;
}

31
app/error.tsx Normal file
View 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>
);
}

View File

@@ -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;
} }

View File

@@ -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
View 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>
);
}

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

54
middleware.ts Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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);
}

View File

@@ -0,0 +1,5 @@
import callAPI from "../caller/config";
export async function getDepartments() {
return await callAPI.get("/department/all").then((res) => res.data);
}

View 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);
}

View 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);
}

View 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
View 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;

View 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,
});
};

View 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 });

View 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})

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>
</>
);
}