481 lines
19 KiB
TypeScript
481 lines
19 KiB
TypeScript
"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>
|
||
</>
|
||
);
|
||
}
|