ticket master

This commit is contained in:
hamid zarghami
2026-07-25 16:36:35 +03:30
parent f53b5d8fe1
commit b60a38e21d
12 changed files with 391 additions and 1 deletions
+3
View File
@@ -36,6 +36,9 @@ export const Pages = {
create: "/tickets/create", create: "/tickets/create",
detail: "/tickets/messages/", detail: "/tickets/messages/",
category: "/tickets/category", category: "/tickets/category",
masters: "/tickets/masters",
mastersCreate: "/tickets/masters/create",
mastersUpdate: "/tickets/masters/update/",
}, },
announcement: { announcement: {
list: "/announcement", list: "/announcement",
+5 -1
View File
@@ -143,6 +143,7 @@
"representative_list": "لیست نمایندگان", "representative_list": "لیست نمایندگان",
"ticket_list": "لیست تیکت ها", "ticket_list": "لیست تیکت ها",
"ticket_category": "دسته بندی تیکت ها", "ticket_category": "دسته بندی تیکت ها",
"ticket_masters": "مستر",
"send_ticket": "ارسال تیکت", "send_ticket": "ارسال تیکت",
"category": "دسته بندی", "category": "دسته بندی",
"user_list": "لیست کاربران", "user_list": "لیست کاربران",
@@ -377,7 +378,10 @@
"column": "ستون", "column": "ستون",
"select_workspace_required": "فضای کاری را انتخاب کنید", "select_workspace_required": "فضای کاری را انتخاب کنید",
"select_project_required": "پروژه را انتخاب کنید", "select_project_required": "پروژه را انتخاب کنید",
"select_column_required": "ستون را انتخاب کنید" "select_column_required": "ستون را انتخاب کنید",
"masters": "مستر",
"add_master": "افزودن مستر",
"edit_master": "ویرایش مستر"
}, },
"active": "فعال", "active": "فعال",
"inactive": "غیرفعال", "inactive": "غیرفعال",
+51
View File
@@ -4,6 +4,7 @@ import {
AddMessageTicketType, AddMessageTicketType,
CreateCategoryType, CreateCategoryType,
CreateTicketAdminType, CreateTicketAdminType,
CreateTicketMasterType,
} from "../types/TicketTypes"; } from "../types/TicketTypes";
export const useGetTickets = ( export const useGetTickets = (
@@ -78,3 +79,53 @@ export const useCreateTicket = () => {
api.createTicket(variables), api.createTicket(variables),
}); });
}; };
export const useGetMasters = () => {
return useQuery({
queryKey: ["ticket-masters"],
queryFn: () => api.getMasters(),
});
};
export const useGetMasterById = (id: string) => {
return useQuery({
queryKey: ["ticket-master", id],
queryFn: () => api.getMasterById(id),
enabled: !!id,
});
};
export const useCreateMaster = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (variables: CreateTicketMasterType) =>
api.createMaster(variables),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["ticket-masters"] });
},
});
};
export const useUpdateMaster = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (variables: { id: string; params: CreateTicketMasterType }) =>
api.updateMaster(variables.id, variables.params),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["ticket-masters"] });
},
});
};
export const useDeleteMaster = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => api.deleteMaster(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["ticket-masters"] });
},
});
};
+72
View File
@@ -0,0 +1,72 @@
import { useFormik } from "formik";
import { FC } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import * as Yup from "yup";
import Button from "../../../components/Button";
import Select from "../../../components/Select";
import { toast } from "../../../components/Toast";
import { Pages } from "../../../config/Pages";
import { ErrorType } from "../../../helpers/types";
import { useGetAdminsListAll } from "../../users/hooks/useUserData";
import { UserItemType } from "../../users/types/UserTypes";
import { useCreateMaster } from "../hooks/useTicketData";
import { CreateTicketMasterType } from "../types/TicketTypes";
const CreateTicketMaster: FC = () => {
const { t } = useTranslation("global");
const navigate = useNavigate();
const getAdmins = useGetAdminsListAll();
const createMaster = useCreateMaster();
const formik = useFormik<CreateTicketMasterType>({
initialValues: {
userId: "",
},
validationSchema: Yup.object({
userId: Yup.string().required(t("errors.required")),
}),
onSubmit: (values) => {
createMaster.mutate(values, {
onSuccess: () => {
toast(t("success"), "success");
navigate(Pages.ticket.masters);
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0], "error");
},
});
},
});
return (
<div className="mt-4">
<div>{t("ticket.add_master")}</div>
<div className="bg-white mt-8 py-8 xl:px-10 px-4 rounded-3xl max-w-xl">
<Select
label={t("ticket.user")}
placeholder={t("select")}
className="border"
items={getAdmins.data?.data?.map((item: UserItemType) => ({
label: `${item.firstName} ${item.lastName}`,
value: item.id,
}))}
{...formik.getFieldProps("userId")}
error_text={formik.touched.userId && formik.errors.userId ? formik.errors.userId : ""}
/>
<div className="mt-8 flex justify-end">
<Button
label={t("submit")}
className="w-fit px-7"
onClick={() => formik.handleSubmit()}
isLoading={createMaster.isPending}
/>
</div>
</div>
</div>
);
};
export default CreateTicketMaster;
+94
View File
@@ -0,0 +1,94 @@
import { Add } from "iconsax-react";
import { FC } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import Button from "../../../components/Button";
import PageLoading from "../../../components/PageLoading";
import Td from "../../../components/Td";
import { toast } from "../../../components/Toast";
import TrashWithConfrim from "../../../components/TrashWithConfrim";
import { Pages } from "../../../config/Pages";
import { ErrorType } from "../../../helpers/types";
import { useDeleteMaster, useGetMasters } from "../hooks/useTicketData";
import { TicketMasterItemType } from "../types/TicketTypes";
const TicketMastersList: FC = () => {
const { t } = useTranslation("global");
const getMasters = useGetMasters();
const deleteMaster = useDeleteMaster();
const handleDelete = (id: string) => {
deleteMaster.mutate(id, {
onSuccess: () => {
toast(t("success"), "success");
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0], "error");
},
});
};
const masters: TicketMasterItemType[] = getMasters.data?.data?.masters ?? getMasters.data?.data?.ticketMasters ?? [];
return (
<div className="mt-4">
<div className="flex w-full justify-between items-center">
<div>{t("ticket.masters")}</div>
<div>
<Link to={Pages.ticket.mastersCreate}>
<Button className="px-5">
<div className="flex gap-2">
<Add
className="size-5"
color="#fff"
/>
<div>{t("ticket.add_master")}</div>
</div>
</Button>
</Link>
</div>
</div>
{getMasters.isPending ? (
<PageLoading />
) : (
<div className="relative overflow-x-auto rounded-3xl mt-9 w-full">
<table className="w-full text-sm">
<thead className="thead">
<tr>
<Td text={t("ticket.user_name")} />
<Td text={t("user.email")} />
<Td text={t("ticket.detail")} />
</tr>
</thead>
<tbody>
{masters.map((item) => (
<tr
key={item.id}
className="tr"
>
<Td text={item.user ? `${item.user.firstName} ${item.user.lastName}` : item.userId} />
<Td text={item.user?.email ?? item.user?.phone ?? "-"} />
<Td text="">
<div className="flex items-center gap-3">
{/* <Link to={Pages.ticket.mastersUpdate + item.id}>
<Edit size={20} color="#8C90A3" />
</Link> */}
<TrashWithConfrim
onDelete={() => handleDelete(item.id)}
isLoading={deleteMaster.isPending}
color="#8C90A3"
/>
</div>
</Td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
};
export default TicketMastersList;
+89
View File
@@ -0,0 +1,89 @@
import { useFormik } from "formik";
import { FC, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
import * as Yup from "yup";
import Button from "../../../components/Button";
import PageLoading from "../../../components/PageLoading";
import Select from "../../../components/Select";
import { toast } from "../../../components/Toast";
import { Pages } from "../../../config/Pages";
import { ErrorType } from "../../../helpers/types";
import { useGetAdminsListAll } from "../../users/hooks/useUserData";
import { UserItemType } from "../../users/types/UserTypes";
import { useGetMasterById, useUpdateMaster } from "../hooks/useTicketData";
import { CreateTicketMasterType } from "../types/TicketTypes";
const UpdateTicketMaster: FC = () => {
const { t } = useTranslation("global");
const { id } = useParams();
const navigate = useNavigate();
const getAdmins = useGetAdminsListAll();
const getMaster = useGetMasterById(id ?? "");
const updateMaster = useUpdateMaster();
const formik = useFormik<CreateTicketMasterType>({
initialValues: {
userId: "",
},
validationSchema: Yup.object({
userId: Yup.string().required(t("errors.required")),
}),
onSubmit: (values) => {
updateMaster.mutate(
{ id: id as string, params: values },
{
onSuccess: () => {
toast(t("success"), "success");
navigate(Pages.ticket.masters);
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0], "error");
},
},
);
},
});
useEffect(() => {
const master = getMaster.data?.data?.master ?? getMaster.data?.data?.ticketMaster;
if (master?.userId) {
formik.setFieldValue("userId", master.userId);
}
}, [getMaster.data]);
if (getMaster.isPending) {
return <PageLoading />;
}
return (
<div className="mt-4">
<div>{t("ticket.edit_master")}</div>
<div className="bg-white mt-8 py-8 xl:px-10 px-4 rounded-3xl max-w-xl">
<Select
label={t("ticket.user")}
placeholder={t("select")}
className="border"
items={getAdmins.data?.data?.map((item: UserItemType) => ({
label: `${item.firstName} ${item.lastName}`,
value: item.id,
}))}
{...formik.getFieldProps("userId")}
error_text={formik.touched.userId && formik.errors.userId ? formik.errors.userId : ""}
/>
<div className="mt-8 flex justify-end">
<Button
label={t("submit")}
className="w-fit px-7"
onClick={() => formik.handleSubmit()}
isLoading={updateMaster.isPending}
/>
</div>
</div>
</div>
);
};
export default UpdateTicketMaster;
+26
View File
@@ -3,6 +3,7 @@ import {
AddMessageTicketType, AddMessageTicketType,
CreateCategoryType, CreateCategoryType,
CreateTicketAdminType, CreateTicketAdminType,
CreateTicketMasterType,
} from "../types/TicketTypes"; } from "../types/TicketTypes";
export const getTickets = async ( export const getTickets = async (
@@ -63,3 +64,28 @@ export const createTicket = async (params: CreateTicketAdminType) => {
const { data } = await axios.post(`/tickets/admin`, params); const { data } = await axios.post(`/tickets/admin`, params);
return data; return data;
}; };
export const getMasters = async () => {
const { data } = await axios.get(`/tickets/masters`);
return data;
};
export const getMasterById = async (id: string) => {
const { data } = await axios.get(`/tickets/masters/${id}`);
return data;
};
export const createMaster = async (params: CreateTicketMasterType) => {
const { data } = await axios.post(`/tickets/masters`, params);
return data;
};
export const updateMaster = async (id: string, params: CreateTicketMasterType) => {
const { data } = await axios.patch(`/tickets/masters/${id}`, params);
return data;
};
export const deleteMaster = async (id: string) => {
const { data } = await axios.delete(`/tickets/masters/${id}`);
return data;
};
+18
View File
@@ -89,3 +89,21 @@ export type CreateTicketAdminType = {
attachmentUrls?: string[]; attachmentUrls?: string[];
userId: string; userId: string;
}; };
export type CreateTicketMasterType = {
userId: string;
};
export type TicketMasterItemType = {
id: string;
userId: string;
user?: {
id: string;
firstName: string;
lastName: string;
email?: string;
phone?: string;
};
createdAt?: string;
updatedAt?: string;
};
+7
View File
@@ -54,6 +54,13 @@ export const useGetUsers = (search: string, permission?: string, page = 1) => {
}); });
}; };
export const useGetAdminsListAll = () => {
return useQuery({
queryKey: ["admins-list-all"],
queryFn: () => api.getAdminsListAll(),
});
};
export const useGetUsersInfinite = (search: string, permission?: string, enabled = true) => { export const useGetUsersInfinite = (search: string, permission?: string, enabled = true) => {
return useInfiniteQuery({ return useInfiniteQuery({
queryKey: ["users-infinite", search, permission], queryKey: ["users-infinite", search, permission],
+5
View File
@@ -42,6 +42,11 @@ export const getUsers = async (search: string, permission?: string, page = 1) =>
return data; return data;
}; };
export const getAdminsListAll = async () => {
const { data } = await axios.get(`/users/admins-list/all`);
return data;
};
export const getUserDetail = async (id: string) => { export const getUserDetail = async (id: string) => {
const { data } = await axios.get(`/users/admins/${id}`); const { data } = await axios.get(`/users/admins/${id}`);
return data; return data;
+15
View File
@@ -89,6 +89,9 @@ import TicketCategory from "../pages/ticket/Category";
import CreateTicket from "../pages/ticket/CreateTicket"; import CreateTicket from "../pages/ticket/CreateTicket";
import TicketDetail from "../pages/ticket/Detail"; import TicketDetail from "../pages/ticket/Detail";
import TicketList from "../pages/ticket/TicketList"; import TicketList from "../pages/ticket/TicketList";
import CreateTicketMaster from "../pages/ticket/masters/Create";
import TicketMastersList from "../pages/ticket/masters/List";
import UpdateTicketMaster from "../pages/ticket/masters/Update";
import TransactionList from "../pages/transaction/List"; import TransactionList from "../pages/transaction/List";
import CreateUser from "../pages/users/Create"; import CreateUser from "../pages/users/Create";
import GroupCreate from "../pages/users/GroupCreate"; import GroupCreate from "../pages/users/GroupCreate";
@@ -169,6 +172,18 @@ const MainRouter: FC = () => {
path={Pages.ticket.category} path={Pages.ticket.category}
element={<TicketCategory />} element={<TicketCategory />}
/> />
<Route
path={Pages.ticket.masters}
element={<TicketMastersList />}
/>
<Route
path={Pages.ticket.mastersCreate}
element={<CreateTicketMaster />}
/>
<Route
path={Pages.ticket.mastersUpdate + ":id"}
element={<UpdateTicketMaster />}
/>
<Route <Route
path={Pages.ticket.detail + ":id"} path={Pages.ticket.detail + ":id"}
element={<TicketDetail />} element={<TicketDetail />}
+6
View File
@@ -41,6 +41,12 @@ const SupportSubMenu: FC = () => {
link={Pages.ticket.category} link={Pages.ticket.category}
activeName="tickets" activeName="tickets"
/> />
<SubMenuItem
title={t("submenu.ticket_masters")}
isActive={isActive("/tickets/masters")}
link={Pages.ticket.masters}
activeName="tickets"
/>
</div> </div>
</div> </div>
); );