618 lines
19 KiB
TypeScript
618 lines
19 KiB
TypeScript
import { useMemo, useState } from "react";
|
||
import {
|
||
useCreateTicketDepartment,
|
||
useCreateTicketStatus,
|
||
useCreateTicketSubject,
|
||
useDeleteTicketDepartmentMutation,
|
||
useDeleteTicketStatusMutation,
|
||
useDeleteTicketSubjectMutation,
|
||
useTicketDepartments,
|
||
useTicketStatuses,
|
||
useTicketSubjects,
|
||
useUpdateTicketDepartmentMutation,
|
||
useUpdateTicketStatusConfigMutation,
|
||
useUpdateTicketSubjectMutation,
|
||
} from "../core/_hooks";
|
||
import {
|
||
TicketDepartment,
|
||
TicketStatus,
|
||
TicketSubject,
|
||
} from "../core/_models";
|
||
import { PageContainer, PageTitle, SectionTitle } from "@/components/ui/Typography";
|
||
import { Button } from "@/components/ui/Button";
|
||
import { Input } from "@/components/ui/Input";
|
||
import { Table } from "@/components/ui/Table";
|
||
import { TableColumn } from "@/types";
|
||
import { Settings, Edit3, Trash2 } from "lucide-react";
|
||
|
||
type TabKey = "departments" | "statuses" | "subjects";
|
||
|
||
const TicketConfigPage = () => {
|
||
const [activeTab, setActiveTab] = useState<TabKey>("departments");
|
||
|
||
const { data: departments } = useTicketDepartments({ activeOnly: true });
|
||
const { data: statuses } = useTicketStatuses({ activeOnly: false });
|
||
const { data: subjects } = useTicketSubjects({ activeOnly: false });
|
||
|
||
const { mutate: createDepartment, isPending: isCreatingDepartment } =
|
||
useCreateTicketDepartment();
|
||
const { mutate: updateDepartment, isPending: isUpdatingDepartment } =
|
||
useUpdateTicketDepartmentMutation();
|
||
const { mutate: deleteDepartment } = useDeleteTicketDepartmentMutation();
|
||
|
||
const { mutate: createStatus, isPending: isCreatingStatus } =
|
||
useCreateTicketStatus();
|
||
const { mutate: updateStatus, isPending: isUpdatingStatus } =
|
||
useUpdateTicketStatusConfigMutation();
|
||
const { mutate: deleteStatus } = useDeleteTicketStatusMutation();
|
||
|
||
const { mutate: createSubject, isPending: isCreatingSubject } =
|
||
useCreateTicketSubject();
|
||
const { mutate: updateSubject, isPending: isUpdatingSubject } =
|
||
useUpdateTicketSubjectMutation();
|
||
const { mutate: deleteSubject } = useDeleteTicketSubjectMutation();
|
||
|
||
const [departmentForm, setDepartmentForm] = useState({
|
||
id: null as number | null,
|
||
name: "",
|
||
slug: "",
|
||
position: "",
|
||
is_active: "true",
|
||
});
|
||
|
||
const [statusForm, setStatusForm] = useState({
|
||
id: null as number | null,
|
||
name: "",
|
||
slug: "",
|
||
position: "",
|
||
is_active: "true",
|
||
});
|
||
|
||
const [subjectForm, setSubjectForm] = useState({
|
||
id: null as number | null,
|
||
department_id: "",
|
||
name: "",
|
||
slug: "",
|
||
position: "",
|
||
is_active: "true",
|
||
});
|
||
|
||
const resetDepartmentForm = () =>
|
||
setDepartmentForm({
|
||
id: null,
|
||
name: "",
|
||
slug: "",
|
||
position: "",
|
||
is_active: "true",
|
||
});
|
||
|
||
const resetStatusForm = () =>
|
||
setStatusForm({
|
||
id: null,
|
||
name: "",
|
||
slug: "",
|
||
position: "",
|
||
is_active: "true",
|
||
});
|
||
|
||
const resetSubjectForm = () =>
|
||
setSubjectForm({
|
||
id: null,
|
||
department_id: "",
|
||
name: "",
|
||
slug: "",
|
||
position: "",
|
||
is_active: "true",
|
||
});
|
||
|
||
const handleDepartmentSubmit = (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!departmentForm.name || !departmentForm.slug || !departmentForm.position)
|
||
return;
|
||
const payload = {
|
||
name: departmentForm.name,
|
||
slug: departmentForm.slug,
|
||
position: Number(departmentForm.position),
|
||
is_active: departmentForm.is_active === "true",
|
||
};
|
||
if (departmentForm.id) {
|
||
updateDepartment(
|
||
{ id: departmentForm.id, payload },
|
||
{ onSuccess: resetDepartmentForm }
|
||
);
|
||
} else {
|
||
createDepartment(payload, { onSuccess: resetDepartmentForm });
|
||
}
|
||
};
|
||
|
||
const handleStatusSubmit = (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!statusForm.name || !statusForm.slug || !statusForm.position) return;
|
||
const payload = {
|
||
name: statusForm.name,
|
||
slug: statusForm.slug,
|
||
position: Number(statusForm.position),
|
||
is_active: statusForm.is_active === "true",
|
||
};
|
||
if (statusForm.id) {
|
||
updateStatus(
|
||
{ id: statusForm.id, payload },
|
||
{ onSuccess: resetStatusForm }
|
||
);
|
||
} else {
|
||
createStatus(payload, { onSuccess: resetStatusForm });
|
||
}
|
||
};
|
||
|
||
const handleSubjectSubmit = (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (
|
||
!subjectForm.department_id ||
|
||
!subjectForm.name ||
|
||
!subjectForm.slug ||
|
||
!subjectForm.position
|
||
)
|
||
return;
|
||
const payload = {
|
||
department_id: Number(subjectForm.department_id),
|
||
name: subjectForm.name,
|
||
slug: subjectForm.slug,
|
||
position: Number(subjectForm.position),
|
||
is_active: subjectForm.is_active === "true",
|
||
};
|
||
if (subjectForm.id) {
|
||
updateSubject(
|
||
{ id: subjectForm.id, payload },
|
||
{ onSuccess: resetSubjectForm }
|
||
);
|
||
} else {
|
||
createSubject(payload, { onSuccess: resetSubjectForm });
|
||
}
|
||
};
|
||
|
||
const departmentColumns: TableColumn[] = useMemo(
|
||
() => [
|
||
{ key: "name", label: "نام", align: "right" },
|
||
{ key: "slug", label: "شناسه", align: "right" },
|
||
{ key: "position", label: "ترتیب", align: "center" },
|
||
{
|
||
key: "is_active",
|
||
label: "وضعیت",
|
||
render: (value: boolean) => (value ? "فعال" : "غیرفعال"),
|
||
},
|
||
{
|
||
key: "actions",
|
||
label: "عملیات",
|
||
render: (_val: unknown, row: TicketDepartment) => (
|
||
<div className="flex items-center justify-end gap-2">
|
||
<button
|
||
className="text-primary-600"
|
||
onClick={() =>
|
||
setDepartmentForm({
|
||
id: row.id,
|
||
name: row.name,
|
||
slug: row.slug,
|
||
position: row.position.toString(),
|
||
is_active: row.is_active ? "true" : "false",
|
||
})
|
||
}
|
||
>
|
||
<Edit3 className="h-4 w-4" />
|
||
</button>
|
||
<button
|
||
className="text-red-600"
|
||
onClick={() => deleteDepartment(row.id)}
|
||
>
|
||
<Trash2 className="h-4 w-4" />
|
||
</button>
|
||
</div>
|
||
),
|
||
},
|
||
],
|
||
[deleteDepartment]
|
||
);
|
||
|
||
const statusColumns: TableColumn[] = useMemo(
|
||
() => [
|
||
{ key: "name", label: "نام", align: "right" },
|
||
{ key: "slug", label: "شناسه", align: "right" },
|
||
{ key: "position", label: "ترتیب", align: "center" },
|
||
{
|
||
key: "is_active",
|
||
label: "وضعیت",
|
||
render: (value: boolean) => (value ? "فعال" : "غیرفعال"),
|
||
},
|
||
{
|
||
key: "actions",
|
||
label: "عملیات",
|
||
render: (_val: unknown, row: TicketStatus) => (
|
||
<div className="flex items-center justify-end gap-2">
|
||
<button
|
||
className="text-primary-600"
|
||
onClick={() =>
|
||
setStatusForm({
|
||
id: row.id,
|
||
name: row.name,
|
||
slug: row.slug,
|
||
position: row.position.toString(),
|
||
is_active: row.is_active ? "true" : "false",
|
||
})
|
||
}
|
||
>
|
||
<Edit3 className="h-4 w-4" />
|
||
</button>
|
||
<button
|
||
className="text-red-600"
|
||
onClick={() => deleteStatus(row.id)}
|
||
>
|
||
<Trash2 className="h-4 w-4" />
|
||
</button>
|
||
</div>
|
||
),
|
||
},
|
||
],
|
||
[deleteStatus]
|
||
);
|
||
|
||
const subjectColumns: TableColumn[] = useMemo(
|
||
() => [
|
||
{ key: "name", label: "نام", align: "right" },
|
||
{
|
||
key: "department",
|
||
label: "دپارتمان",
|
||
align: "right",
|
||
render: (_val: unknown, row: TicketSubject) => row.department?.name || "-",
|
||
},
|
||
{ key: "slug", label: "شناسه", align: "right" },
|
||
{ key: "position", label: "ترتیب", align: "center" },
|
||
{
|
||
key: "is_active",
|
||
label: "وضعیت",
|
||
render: (value: boolean) => (value ? "فعال" : "غیرفعال"),
|
||
},
|
||
{
|
||
key: "actions",
|
||
label: "عملیات",
|
||
render: (_val: unknown, row: TicketSubject) => (
|
||
<div className="flex items-center justify-end gap-2">
|
||
<button
|
||
className="text-primary-600"
|
||
onClick={() =>
|
||
setSubjectForm({
|
||
id: row.id,
|
||
department_id: row.department_id.toString(),
|
||
name: row.name,
|
||
slug: row.slug,
|
||
position: row.position.toString(),
|
||
is_active: row.is_active ? "true" : "false",
|
||
})
|
||
}
|
||
>
|
||
<Edit3 className="h-4 w-4" />
|
||
</button>
|
||
<button
|
||
className="text-red-600"
|
||
onClick={() => deleteSubject(row.id)}
|
||
>
|
||
<Trash2 className="h-4 w-4" />
|
||
</button>
|
||
</div>
|
||
),
|
||
},
|
||
],
|
||
[deleteSubject]
|
||
);
|
||
|
||
const renderDepartments = () => (
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
<div className="card p-6 space-y-4">
|
||
<SectionTitle>لیست دپارتمانها</SectionTitle>
|
||
<Table columns={departmentColumns} data={(departments || []) as any[]} />
|
||
</div>
|
||
<div className="card p-6 space-y-4">
|
||
<SectionTitle>
|
||
{departmentForm.id ? "ویرایش دپارتمان" : "دپارتمان جدید"}
|
||
</SectionTitle>
|
||
<form className="space-y-4" onSubmit={handleDepartmentSubmit}>
|
||
<Input
|
||
label="نام"
|
||
value={departmentForm.name}
|
||
onChange={(e) =>
|
||
setDepartmentForm((prev) => ({ ...prev, name: e.target.value }))
|
||
}
|
||
/>
|
||
<Input
|
||
label="Slug"
|
||
value={departmentForm.slug}
|
||
onChange={(e) =>
|
||
setDepartmentForm((prev) => ({ ...prev, slug: e.target.value }))
|
||
}
|
||
/>
|
||
<Input
|
||
label="ترتیب"
|
||
type="number"
|
||
value={departmentForm.position}
|
||
onChange={(e) =>
|
||
setDepartmentForm((prev) => ({
|
||
...prev,
|
||
position: e.target.value,
|
||
}))
|
||
}
|
||
/>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
وضعیت
|
||
</label>
|
||
<select
|
||
value={departmentForm.is_active}
|
||
onChange={(e) =>
|
||
setDepartmentForm((prev) => ({
|
||
...prev,
|
||
is_active: e.target.value,
|
||
}))
|
||
}
|
||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
|
||
>
|
||
<option value="true">فعال</option>
|
||
<option value="false">غیرفعال</option>
|
||
</select>
|
||
</div>
|
||
<div className="flex gap-3">
|
||
<Button
|
||
type="submit"
|
||
variant="primary"
|
||
loading={isCreatingDepartment || isUpdatingDepartment}
|
||
disabled={
|
||
!departmentForm.name ||
|
||
!departmentForm.slug ||
|
||
!departmentForm.position
|
||
}
|
||
className="flex-1"
|
||
>
|
||
{departmentForm.id ? "ویرایش دپارتمان" : "ایجاد دپارتمان"}
|
||
</Button>
|
||
{departmentForm.id && (
|
||
<Button
|
||
type="button"
|
||
variant="secondary"
|
||
onClick={resetDepartmentForm}
|
||
className="flex-1"
|
||
>
|
||
انصراف
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
const renderStatuses = () => (
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
<div className="card p-6 space-y-4">
|
||
<SectionTitle>لیست وضعیتها</SectionTitle>
|
||
<Table columns={statusColumns} data={(statuses || []) as any[]} />
|
||
</div>
|
||
<div className="card p-6 space-y-4">
|
||
<SectionTitle>
|
||
{statusForm.id ? "ویرایش وضعیت" : "وضعیت جدید"}
|
||
</SectionTitle>
|
||
<form className="space-y-4" onSubmit={handleStatusSubmit}>
|
||
<Input
|
||
label="نام"
|
||
value={statusForm.name}
|
||
onChange={(e) =>
|
||
setStatusForm((prev) => ({ ...prev, name: e.target.value }))
|
||
}
|
||
/>
|
||
<Input
|
||
label="Slug"
|
||
value={statusForm.slug}
|
||
onChange={(e) =>
|
||
setStatusForm((prev) => ({ ...prev, slug: e.target.value }))
|
||
}
|
||
/>
|
||
<Input
|
||
label="ترتیب"
|
||
type="number"
|
||
value={statusForm.position}
|
||
onChange={(e) =>
|
||
setStatusForm((prev) => ({
|
||
...prev,
|
||
position: e.target.value,
|
||
}))
|
||
}
|
||
/>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
وضعیت
|
||
</label>
|
||
<select
|
||
value={statusForm.is_active}
|
||
onChange={(e) =>
|
||
setStatusForm((prev) => ({
|
||
...prev,
|
||
is_active: e.target.value,
|
||
}))
|
||
}
|
||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
|
||
>
|
||
<option value="true">فعال</option>
|
||
<option value="false">غیرفعال</option>
|
||
</select>
|
||
</div>
|
||
<div className="flex gap-3">
|
||
<Button
|
||
type="submit"
|
||
variant="primary"
|
||
loading={isCreatingStatus || isUpdatingStatus}
|
||
disabled={
|
||
!statusForm.name || !statusForm.slug || !statusForm.position
|
||
}
|
||
className="flex-1"
|
||
>
|
||
{statusForm.id ? "ویرایش وضعیت" : "ایجاد وضعیت"}
|
||
</Button>
|
||
{statusForm.id && (
|
||
<Button
|
||
type="button"
|
||
variant="secondary"
|
||
onClick={resetStatusForm}
|
||
className="flex-1"
|
||
>
|
||
انصراف
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
const renderSubjects = () => (
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
<div className="card p-6 space-y-4">
|
||
<SectionTitle>لیست موضوعات</SectionTitle>
|
||
<Table columns={subjectColumns} data={(subjects || []) as any[]} />
|
||
</div>
|
||
<div className="card p-6 space-y-4">
|
||
<SectionTitle>
|
||
{subjectForm.id ? "ویرایش موضوع" : "موضوع جدید"}
|
||
</SectionTitle>
|
||
<form className="space-y-4" onSubmit={handleSubjectSubmit}>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
دپارتمان
|
||
</label>
|
||
<select
|
||
value={subjectForm.department_id}
|
||
onChange={(e) =>
|
||
setSubjectForm((prev) => ({
|
||
...prev,
|
||
department_id: e.target.value,
|
||
}))
|
||
}
|
||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
|
||
>
|
||
<option value="">انتخاب دپارتمان</option>
|
||
{departments?.map((department) => (
|
||
<option key={department.id} value={department.id}>
|
||
{department.name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
<Input
|
||
label="نام"
|
||
value={subjectForm.name}
|
||
onChange={(e) =>
|
||
setSubjectForm((prev) => ({ ...prev, name: e.target.value }))
|
||
}
|
||
/>
|
||
<Input
|
||
label="Slug"
|
||
value={subjectForm.slug}
|
||
onChange={(e) =>
|
||
setSubjectForm((prev) => ({ ...prev, slug: e.target.value }))
|
||
}
|
||
/>
|
||
<Input
|
||
label="ترتیب"
|
||
type="number"
|
||
value={subjectForm.position}
|
||
onChange={(e) =>
|
||
setSubjectForm((prev) => ({
|
||
...prev,
|
||
position: e.target.value,
|
||
}))
|
||
}
|
||
/>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
وضعیت
|
||
</label>
|
||
<select
|
||
value={subjectForm.is_active}
|
||
onChange={(e) =>
|
||
setSubjectForm((prev) => ({
|
||
...prev,
|
||
is_active: e.target.value,
|
||
}))
|
||
}
|
||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
|
||
>
|
||
<option value="true">فعال</option>
|
||
<option value="false">غیرفعال</option>
|
||
</select>
|
||
</div>
|
||
<div className="flex gap-3">
|
||
<Button
|
||
type="submit"
|
||
variant="primary"
|
||
loading={isCreatingSubject || isUpdatingSubject}
|
||
disabled={
|
||
!subjectForm.department_id ||
|
||
!subjectForm.name ||
|
||
!subjectForm.slug ||
|
||
!subjectForm.position
|
||
}
|
||
className="flex-1"
|
||
>
|
||
{subjectForm.id ? "ویرایش موضوع" : "ایجاد موضوع"}
|
||
</Button>
|
||
{subjectForm.id && (
|
||
<Button
|
||
type="button"
|
||
variant="secondary"
|
||
onClick={resetSubjectForm}
|
||
className="flex-1"
|
||
>
|
||
انصراف
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
return (
|
||
<PageContainer className="space-y-6">
|
||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||
<PageTitle className="flex items-center gap-2">
|
||
<Settings className="h-6 w-6" />
|
||
تنظیمات تیکت
|
||
</PageTitle>
|
||
</div>
|
||
|
||
<div className="card p-2 flex flex-wrap gap-2">
|
||
<Button
|
||
variant={activeTab === "departments" ? "primary" : "secondary"}
|
||
onClick={() => setActiveTab("departments")}
|
||
>
|
||
دپارتمانها
|
||
</Button>
|
||
<Button
|
||
variant={activeTab === "statuses" ? "primary" : "secondary"}
|
||
onClick={() => setActiveTab("statuses")}
|
||
>
|
||
وضعیتها
|
||
</Button>
|
||
<Button
|
||
variant={activeTab === "subjects" ? "primary" : "secondary"}
|
||
onClick={() => setActiveTab("subjects")}
|
||
>
|
||
موضوعات
|
||
</Button>
|
||
</div>
|
||
|
||
{activeTab === "departments" && renderDepartments()}
|
||
{activeTab === "statuses" && renderStatuses()}
|
||
{activeTab === "subjects" && renderSubjects()}
|
||
</PageContainer>
|
||
);
|
||
};
|
||
|
||
export default TicketConfigPage;
|
||
|