Files
shomal-hospital-ticketing-f…/app/(panel)/tickets/page.tsx
2026-05-23 13:22:10 +03:30

481 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
</>
);
}