admin/src/pages/tickets/ticket-config/TicketConfigPage.tsx

618 lines
19 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

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;