Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1990e73368 | |||
| 185961c162 | |||
| b60a38e21d | |||
| f53b5d8fe1 | |||
| f5da44d9cf |
@@ -1,5 +1,5 @@
|
|||||||
VITE_TOKEN_NAME = 'admin_token'
|
VITE_TOKEN_NAME = 'admin_token'
|
||||||
VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token'
|
VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token'
|
||||||
# VITE_BASE_URL = 'https://api.danakcorp.com'
|
VITE_BASE_URL = 'https://api.danakcorp.com'
|
||||||
|
|
||||||
VITE_BASE_URL = 'http://192.168.99.131:3500'
|
# VITE_BASE_URL = 'http://192.168.99.131:3500'
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -95,6 +95,21 @@ export enum PermissionEnum {
|
|||||||
BLOGS_UPDATE = "blogs_update",
|
BLOGS_UPDATE = "blogs_update",
|
||||||
BLOGS_DELETE = "blogs_delete",
|
BLOGS_DELETE = "blogs_delete",
|
||||||
|
|
||||||
|
BLOG_CATEGORIES_CREATE = "blog_categories_create",
|
||||||
|
BLOG_CATEGORIES_READ = "blog_categories_read",
|
||||||
|
BLOG_CATEGORIES_UPDATE = "blog_categories_update",
|
||||||
|
BLOG_CATEGORIES_DELETE = "blog_categories_delete",
|
||||||
|
|
||||||
|
BLOG_COMMENTS_CREATE = "blog_comments_create",
|
||||||
|
BLOG_COMMENTS_READ = "blog_comments_read",
|
||||||
|
BLOG_COMMENTS_UPDATE = "blog_comments_update",
|
||||||
|
BLOG_COMMENTS_DELETE = "blog_comments_delete",
|
||||||
|
|
||||||
|
SLIDERS_CREATE = "sliders_create",
|
||||||
|
SLIDERS_READ = "sliders_read",
|
||||||
|
SLIDERS_UPDATE = "sliders_update",
|
||||||
|
SLIDERS_DELETE = "sliders_delete",
|
||||||
|
|
||||||
LEARNINGS_CREATE = "learnings_create",
|
LEARNINGS_CREATE = "learnings_create",
|
||||||
LEARNINGS_READ = "learnings_read",
|
LEARNINGS_READ = "learnings_read",
|
||||||
LEARNINGS_UPDATE = "learnings_update",
|
LEARNINGS_UPDATE = "learnings_update",
|
||||||
@@ -145,6 +160,19 @@ export enum PermissionEnum {
|
|||||||
TASK_PHASE_UPDATE = "task_phase_update",
|
TASK_PHASE_UPDATE = "task_phase_update",
|
||||||
TASK_PHASE_DELETE = "task_phase_delete",
|
TASK_PHASE_DELETE = "task_phase_delete",
|
||||||
|
|
||||||
|
TICKET_CATEGORIES_CREATE = "ticket_categories_create",
|
||||||
|
TICKET_CATEGORIES_READ = "ticket_categories_read",
|
||||||
|
TICKET_CATEGORIES_UPDATE = "ticket_categories_update",
|
||||||
|
TICKET_CATEGORIES_DELETE = "ticket_categories_delete",
|
||||||
|
|
||||||
|
TICKET_MASTERS_CREATE = "ticket_masters_create",
|
||||||
|
TICKET_MASTERS_READ = "ticket_masters_read",
|
||||||
|
TICKET_MASTERS_UPDATE = "ticket_masters_update",
|
||||||
|
TICKET_MASTERS_DELETE = "ticket_masters_delete",
|
||||||
|
|
||||||
|
VIEW_ALL_WORKSPACES = "view_all_workspaces",
|
||||||
|
VIEW_ALL_PROJECTS = "view_all_projects",
|
||||||
|
|
||||||
LOGS = "logs",
|
LOGS = "logs",
|
||||||
MANAGE_SSO_CLIENTS = "manage_sso_clients",
|
MANAGE_SSO_CLIENTS = "manage_sso_clients",
|
||||||
DMENU = "dmenu",
|
DMENU = "dmenu",
|
||||||
@@ -153,12 +181,34 @@ export enum PermissionEnum {
|
|||||||
DMAIL = "dmail",
|
DMAIL = "dmail",
|
||||||
}
|
}
|
||||||
|
|
||||||
const CRUD_ACTIONS = ["create", "read", "update", "delete"] as const;
|
export type CrudAction = "create" | "read" | "update" | "delete";
|
||||||
|
|
||||||
export const CONTENT_PERMISSION_RESOURCES = ["blogs", "learnings", "advertisements", "announcements"] as const;
|
const CRUD_ACTIONS: readonly CrudAction[] = ["create", "read", "update", "delete"];
|
||||||
|
|
||||||
|
export const CONTENT_PERMISSION_RESOURCES = [
|
||||||
|
"blogs",
|
||||||
|
"blog_categories",
|
||||||
|
"blog_comments",
|
||||||
|
"sliders",
|
||||||
|
"learnings",
|
||||||
|
"advertisements",
|
||||||
|
"announcements",
|
||||||
|
] as const;
|
||||||
export const FINANCIAL_PERMISSION_RESOURCES = ["payments", "invoices", "transactions", "discounts", "bank_accounts"] as const;
|
export const FINANCIAL_PERMISSION_RESOURCES = ["payments", "invoices", "transactions", "discounts", "bank_accounts"] as const;
|
||||||
export const SUPPORT_PERMISSION_RESOURCES = ["support_plan", "tickets"] as const;
|
export const SUPPORT_PERMISSION_RESOURCES = [
|
||||||
export const TASK_MANAGER_PERMISSION_RESOURCES = ["workspace", "project", "task", "task_phase"] as const;
|
"support_plan",
|
||||||
|
"tickets",
|
||||||
|
"ticket_categories",
|
||||||
|
"ticket_masters",
|
||||||
|
] as const;
|
||||||
|
export const TASK_MANAGER_PERMISSION_RESOURCES = [
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
|
"task",
|
||||||
|
"task_phase",
|
||||||
|
"view_all_workspaces",
|
||||||
|
"view_all_projects",
|
||||||
|
] as const;
|
||||||
export const USERS_SIDEBAR_PERMISSION_RESOURCES = ["admins"] as const;
|
export const USERS_SIDEBAR_PERMISSION_RESOURCES = ["admins"] as const;
|
||||||
export const PRODUCT_PERMISSION_RESOURCES = ["dmenu", "dkala", "dpage", "dmail"] as const;
|
export const PRODUCT_PERMISSION_RESOURCES = ["dmenu", "dkala", "dpage", "dmail"] as const;
|
||||||
|
|
||||||
@@ -180,6 +230,32 @@ export const hasPermission = (
|
|||||||
return CRUD_ACTIONS.some((action) => names.includes(`${resourceOrPermission}_${action}`));
|
return CRUD_ACTIONS.some((action) => names.includes(`${resourceOrPermission}_${action}`));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const hasActionPermission = (
|
||||||
|
permissions: PermissionEntry[] | undefined,
|
||||||
|
resource: string,
|
||||||
|
action: CrudAction,
|
||||||
|
): boolean => {
|
||||||
|
const names = getPermissionNames(permissions);
|
||||||
|
|
||||||
|
if (names.includes(resource)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return names.includes(`${resource}_${action}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const canCreate = (permissions: PermissionEntry[] | undefined, resource: string) =>
|
||||||
|
hasActionPermission(permissions, resource, "create");
|
||||||
|
|
||||||
|
export const canRead = (permissions: PermissionEntry[] | undefined, resource: string) =>
|
||||||
|
hasActionPermission(permissions, resource, "read");
|
||||||
|
|
||||||
|
export const canUpdate = (permissions: PermissionEntry[] | undefined, resource: string) =>
|
||||||
|
hasActionPermission(permissions, resource, "update");
|
||||||
|
|
||||||
|
export const canDelete = (permissions: PermissionEntry[] | undefined, resource: string) =>
|
||||||
|
hasActionPermission(permissions, resource, "delete");
|
||||||
|
|
||||||
export const hasAnyPermission = (
|
export const hasAnyPermission = (
|
||||||
permissions: PermissionEntry[] | undefined,
|
permissions: PermissionEntry[] | undefined,
|
||||||
resources: readonly string[],
|
resources: readonly string[],
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import {
|
||||||
|
canCreate,
|
||||||
|
canDelete,
|
||||||
|
canRead,
|
||||||
|
canUpdate,
|
||||||
|
CrudAction,
|
||||||
|
hasActionPermission,
|
||||||
|
hasAnyPermission,
|
||||||
|
hasPermission,
|
||||||
|
} from "../helpers/permissions";
|
||||||
|
import { useGetAdminPermissions } from "../pages/users/hooks/useUserData";
|
||||||
|
|
||||||
|
export const usePermissions = () => {
|
||||||
|
const { data, isPending, isLoading } = useGetAdminPermissions();
|
||||||
|
const permissions = data?.data?.permissions;
|
||||||
|
|
||||||
|
return {
|
||||||
|
permissions,
|
||||||
|
isPending: isPending || isLoading,
|
||||||
|
can: (resource: string, action: CrudAction) => hasActionPermission(permissions, resource, action),
|
||||||
|
canCreate: (resource: string) => canCreate(permissions, resource),
|
||||||
|
canRead: (resource: string) => canRead(permissions, resource),
|
||||||
|
canUpdate: (resource: string) => canUpdate(permissions, resource),
|
||||||
|
canDelete: (resource: string) => canDelete(permissions, resource),
|
||||||
|
has: (resourceOrPermission: string) => hasPermission(permissions, resourceOrPermission),
|
||||||
|
hasAny: (resources: readonly string[]) => hasAnyPermission(permissions, resources),
|
||||||
|
};
|
||||||
|
};
|
||||||
+5
-1
@@ -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": "غیرفعال",
|
||||||
|
|||||||
+23
-15
@@ -14,6 +14,7 @@ import moment from 'moment-jalaali'
|
|||||||
import ToggleStatusAds from './components/ToggleStatus'
|
import ToggleStatusAds from './components/ToggleStatus'
|
||||||
import PageLoading from '../../components/PageLoading'
|
import PageLoading from '../../components/PageLoading'
|
||||||
import Pagination from '../../components/Pagination'
|
import Pagination from '../../components/Pagination'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
const AddList: FC = () => {
|
const AddList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
@@ -23,6 +24,7 @@ const AddList: FC = () => {
|
|||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const getAds = useGetAdsList(page, search, isActive, since)
|
const getAds = useGetAdsList(page, search, isActive, since)
|
||||||
const deleteAds = useDeleteAds()
|
const deleteAds = useDeleteAds()
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = (id: string) => {
|
||||||
deleteAds.mutate(id, {
|
deleteAds.mutate(id, {
|
||||||
@@ -39,18 +41,20 @@ const AddList: FC = () => {
|
|||||||
{t('ads.ads')}
|
{t('ads.ads')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link to={Pages.ads.create}>
|
{canCreate('advertisements') && (
|
||||||
<Button
|
<Link to={Pages.ads.create}>
|
||||||
className='w-[172px]'
|
<Button
|
||||||
>
|
className='w-[172px]'
|
||||||
<div className='flex gap-2 items-center'>
|
>
|
||||||
<Add size={20} color='white' />
|
<div className='flex gap-2 items-center'>
|
||||||
<div>
|
<Add size={20} color='white' />
|
||||||
{t('ads.new_ads')}
|
<div>
|
||||||
|
{t('ads.new_ads')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Button>
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-4'>
|
<div className='mt-4'>
|
||||||
<div className='flex flex-col xl:flex-row justify-between items-center xl:items-end'>
|
<div className='flex flex-col xl:flex-row justify-between items-center xl:items-end'>
|
||||||
@@ -130,10 +134,14 @@ const AddList: FC = () => {
|
|||||||
</Td>
|
</Td>
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<div className='flex gap-3'>
|
<div className='flex gap-3'>
|
||||||
<Link to={Pages.ads.update + item.id}>
|
{canUpdate('advertisements') && (
|
||||||
<Eye size={20} color='#8C90A3' />
|
<Link to={Pages.ads.update + item.id}>
|
||||||
</Link>
|
<Eye size={20} color='#8C90A3' />
|
||||||
<Trash onClick={() => handleDelete(item.id)} size={20} color='#8C90A3' />
|
</Link>
|
||||||
|
)}
|
||||||
|
{canDelete('advertisements') && (
|
||||||
|
<Trash onClick={() => handleDelete(item.id)} size={20} color='#8C90A3' />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import moment from 'moment-jalaali'
|
|||||||
import Pagination from '../../components/Pagination'
|
import Pagination from '../../components/Pagination'
|
||||||
import { toast } from '../../components/Toast';
|
import { toast } from '../../components/Toast';
|
||||||
import { ErrorType } from '../../helpers/types'
|
import { ErrorType } from '../../helpers/types'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const AnnouncementtList: FC = () => {
|
const AnnouncementtList: FC = () => {
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ const AnnouncementtList: FC = () => {
|
|||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const getAnnoncements = useGetAnnoncements(page, '', '')
|
const getAnnoncements = useGetAnnoncements(page, '', '')
|
||||||
const deleteAnnoncement = useDeleteAnnoncement()
|
const deleteAnnoncement = useDeleteAnnoncement()
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = (id: string) => {
|
||||||
deleteAnnoncement.mutate(id, {
|
deleteAnnoncement.mutate(id, {
|
||||||
@@ -38,21 +40,23 @@ const AnnouncementtList: FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
{t('announcement.announcements')}
|
{t('announcement.announcements')}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{canCreate('announcements') && (
|
||||||
<Link to={Pages.announcement.create}>
|
<div>
|
||||||
<Button
|
<Link to={Pages.announcement.create}>
|
||||||
className='px-5'
|
<Button
|
||||||
>
|
className='px-5'
|
||||||
<div className='flex gap-2'>
|
>
|
||||||
<Add
|
<div className='flex gap-2'>
|
||||||
className='size-5'
|
<Add
|
||||||
color='#fff'
|
className='size-5'
|
||||||
/>
|
color='#fff'
|
||||||
<div>{t('announcement.add_announcement')}</div>
|
/>
|
||||||
</div>
|
<div>{t('announcement.add_announcement')}</div>
|
||||||
</Button>
|
</div>
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -88,17 +92,21 @@ const AnnouncementtList: FC = () => {
|
|||||||
<Td text={moment(item.publishAt).format('jYYYY-jMM-jDD')} />
|
<Td text={moment(item.publishAt).format('jYYYY-jMM-jDD')} />
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<Link to={Pages.announcement.detail + item.id}>
|
{canUpdate('announcements') && (
|
||||||
<Edit
|
<Link to={Pages.announcement.detail + item.id}>
|
||||||
|
<Edit
|
||||||
|
className='size-5'
|
||||||
|
color='#888'
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{canDelete('announcements') && (
|
||||||
|
<Trash
|
||||||
className='size-5'
|
className='size-5'
|
||||||
color='#888'
|
color='#888'
|
||||||
|
onClick={() => handleDelete(item.id)}
|
||||||
/>
|
/>
|
||||||
</Link>
|
)}
|
||||||
<Trash
|
|
||||||
className='size-5'
|
|
||||||
color='#888'
|
|
||||||
onClick={() => handleDelete(item.id)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import UploadBoxDraggble from '../../components/UploadBoxDraggble'
|
|||||||
import { useSingleUpload } from '../service/hooks/useServiceData'
|
import { useSingleUpload } from '../service/hooks/useServiceData'
|
||||||
import { toast } from '../../components/Toast';
|
import { toast } from '../../components/Toast';
|
||||||
import UpdateCategory from './components/UpdateCategory'
|
import UpdateCategory from './components/UpdateCategory'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
const BlogCategory: FC = () => {
|
const BlogCategory: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
@@ -25,6 +26,7 @@ const BlogCategory: FC = () => {
|
|||||||
const singleUpload = useSingleUpload()
|
const singleUpload = useSingleUpload()
|
||||||
const createBlogCategory = useCreateBlogCategory()
|
const createBlogCategory = useCreateBlogCategory()
|
||||||
const deleteBlogCategory = useDeleteBlogCategory()
|
const deleteBlogCategory = useDeleteBlogCategory()
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||||
const formik = useFormik<CreateBlogCategoryType>({
|
const formik = useFormik<CreateBlogCategoryType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
title: '',
|
title: '',
|
||||||
@@ -102,15 +104,19 @@ const BlogCategory: FC = () => {
|
|||||||
<Td text={moment(item.createdAt).format('jYYYY/jMM/jDD')} />
|
<Td text={moment(item.createdAt).format('jYYYY/jMM/jDD')} />
|
||||||
<Td text={t('')}>
|
<Td text={t('')}>
|
||||||
<div className='flex gap-2 items-center'>
|
<div className='flex gap-2 items-center'>
|
||||||
<UpdateCategory id={item.id} />
|
{canUpdate('blog_categories') && (
|
||||||
<Trash size={20} color='red' onClick={() => {
|
<UpdateCategory id={item.id} />
|
||||||
deleteBlogCategory.mutateAsync(item.id, {
|
)}
|
||||||
onSuccess: () => {
|
{canDelete('blog_categories') && (
|
||||||
toast('دسته بندی با موفقیت حذف شد', 'success')
|
<Trash size={20} color='red' onClick={() => {
|
||||||
getBlogCategories.refetch()
|
deleteBlogCategory.mutateAsync(item.id, {
|
||||||
}
|
onSuccess: () => {
|
||||||
})
|
toast('دسته بندی با موفقیت حذف شد', 'success')
|
||||||
}} />
|
getBlogCategories.refetch()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -123,59 +129,61 @@ const BlogCategory: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='relative bg-white min-h-[calc(100vh-185px)] w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
{canCreate('blog_categories') && (
|
||||||
<p className='text-md'>{t('blog.add_category')}</p>
|
<div className='relative bg-white min-h-[calc(100vh-185px)] w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
||||||
<div className='text-sm flex items-center justify-between mt-4'>
|
<p className='text-md'>{t('blog.add_category')}</p>
|
||||||
<p>
|
<div className='text-sm flex items-center justify-between mt-4'>
|
||||||
{t('blog.category_status')}
|
<p>
|
||||||
</p>
|
{t('blog.category_status')}
|
||||||
<div className='flex gap-1 text-xs items-center text-description'>
|
</p>
|
||||||
<div>
|
<div className='flex gap-1 text-xs items-center text-description'>
|
||||||
{t('ads.deactive')}
|
<div>
|
||||||
|
{t('ads.deactive')}
|
||||||
|
</div>
|
||||||
|
<SwitchComponent
|
||||||
|
active={formik.values.isActive}
|
||||||
|
onChange={(value) => formik.setFieldValue('isActive', value)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SwitchComponent
|
</div>
|
||||||
active={formik.values.isActive}
|
|
||||||
onChange={(value) => formik.setFieldValue('isActive', value)}
|
<div className='mt-6'>
|
||||||
|
<Input
|
||||||
|
label='عنوان'
|
||||||
|
{...formik.getFieldProps('title')}
|
||||||
|
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-6'>
|
<div className='mt-6'>
|
||||||
<Input
|
<Textarea
|
||||||
label='عنوان'
|
label='توضیحات'
|
||||||
{...formik.getFieldProps('title')}
|
{...formik.getFieldProps('description')}
|
||||||
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : undefined}
|
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-6'>
|
|
||||||
<Textarea
|
|
||||||
label='توضیحات'
|
|
||||||
{...formik.getFieldProps('description')}
|
|
||||||
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : undefined}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-6'>
|
|
||||||
<UploadBoxDraggble
|
|
||||||
label='آپلود آیکون'
|
|
||||||
onChange={(file) => setFile(file[0])}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className='w-[172px] absolute bottom-4 left-4'
|
|
||||||
onClick={() => formik.handleSubmit()}
|
|
||||||
isLoading={createBlogCategory.isPending || singleUpload.isPending}
|
|
||||||
>
|
|
||||||
<div className='flex gap-2 items-center'>
|
|
||||||
<TickCircle size={20} color='white' />
|
|
||||||
<div>
|
|
||||||
ذخیره
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
|
||||||
</div>
|
<div className='mt-6'>
|
||||||
|
<UploadBoxDraggble
|
||||||
|
label='آپلود آیکون'
|
||||||
|
onChange={(file) => setFile(file[0])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className='w-[172px] absolute bottom-4 left-4'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={createBlogCategory.isPending || singleUpload.isPending}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2 items-center'>
|
||||||
|
<TickCircle size={20} color='white' />
|
||||||
|
<div>
|
||||||
|
ذخیره
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
+32
-24
@@ -11,6 +11,7 @@ import { BlogItemType } from './types/BlogTypes'
|
|||||||
import moment from 'moment-jalaali'
|
import moment from 'moment-jalaali'
|
||||||
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
||||||
import Pagination from '../../components/Pagination'
|
import Pagination from '../../components/Pagination'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
const BlogList: FC = () => {
|
const BlogList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
@@ -18,6 +19,7 @@ const BlogList: FC = () => {
|
|||||||
const [search, setSearch] = useState<string>('')
|
const [search, setSearch] = useState<string>('')
|
||||||
const getBlogs = useGetBlogs(search, page);
|
const getBlogs = useGetBlogs(search, page);
|
||||||
const deleteBlog = useDeleteBlog()
|
const deleteBlog = useDeleteBlog()
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-4 min-h-[500px]'>
|
<div className='mt-4 min-h-[500px]'>
|
||||||
@@ -26,18 +28,20 @@ const BlogList: FC = () => {
|
|||||||
{t('blog.blog')}
|
{t('blog.blog')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link to={Pages.blog.create}>
|
{canCreate('blogs') && (
|
||||||
<Button
|
<Link to={Pages.blog.create}>
|
||||||
className='w-[172px]'
|
<Button
|
||||||
>
|
className='w-[172px]'
|
||||||
<div className='flex gap-2 items-center'>
|
>
|
||||||
<Add size={20} color='white' />
|
<div className='flex gap-2 items-center'>
|
||||||
<div>
|
<Add size={20} color='white' />
|
||||||
{t('blog.new_blog')}
|
<div>
|
||||||
|
{t('blog.new_blog')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Button>
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -139,19 +143,23 @@ const BlogList: FC = () => {
|
|||||||
</Td> */}
|
</Td> */}
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<Link to={Pages.blog.detail + item.id}>
|
{canUpdate('blogs') && (
|
||||||
<Eye size={20} color='#8C90A3' />
|
<Link to={Pages.blog.detail + item.id}>
|
||||||
</Link>
|
<Eye size={20} color='#8C90A3' />
|
||||||
<TrashWithConfrim
|
</Link>
|
||||||
onDelete={() => {
|
)}
|
||||||
deleteBlog.mutate(item.id, {
|
{canDelete('blogs') && (
|
||||||
onSuccess: () => {
|
<TrashWithConfrim
|
||||||
getBlogs.refetch()
|
onDelete={() => {
|
||||||
}
|
deleteBlog.mutate(item.id, {
|
||||||
})
|
onSuccess: () => {
|
||||||
}}
|
getBlogs.refetch()
|
||||||
isLoading={deleteBlog.isPending}
|
}
|
||||||
/>
|
})
|
||||||
|
}}
|
||||||
|
isLoading={deleteBlog.isPending}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import moment from 'moment-jalaali'
|
|||||||
import { CloseCircle, TickCircle } from 'iconsax-react'
|
import { CloseCircle, TickCircle } from 'iconsax-react'
|
||||||
import { ErrorType } from '../../helpers/types'
|
import { ErrorType } from '../../helpers/types'
|
||||||
import { toast } from '../../components/Toast';
|
import { toast } from '../../components/Toast';
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
const Comments: FC = () => {
|
const Comments: FC = () => {
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canUpdate } = usePermissions()
|
||||||
const getBlogComments = useGetBlogComments()
|
const getBlogComments = useGetBlogComments()
|
||||||
const changeStatusComment = useChangeStatusComment()
|
const changeStatusComment = useChangeStatusComment()
|
||||||
const handleChangeStatus = (id: string, status: string) => {
|
const handleChangeStatus = (id: string, status: string) => {
|
||||||
@@ -49,7 +51,7 @@ const Comments: FC = () => {
|
|||||||
<Td text={moment(item.createdAt).format('jYYYY/jMM/jDD')} />
|
<Td text={moment(item.createdAt).format('jYYYY/jMM/jDD')} />
|
||||||
<Td text=''>
|
<Td text=''>
|
||||||
{
|
{
|
||||||
item.status === 'PENDING' &&
|
item.status === 'PENDING' && canUpdate('blog_comments') &&
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<TickCircle onClick={() => handleChangeStatus(item.id, 'APPROVED')} color='green' size={20} />
|
<TickCircle onClick={() => handleChangeStatus(item.id, 'APPROVED')} color='green' size={20} />
|
||||||
<CloseCircle onClick={() => handleChangeStatus(item.id, 'REJECTED')} color='red' size={20} />
|
<CloseCircle onClick={() => handleChangeStatus(item.id, 'REJECTED')} color='red' size={20} />
|
||||||
|
|||||||
+22
-16
@@ -8,10 +8,12 @@ import { Link } from 'react-router-dom'
|
|||||||
import { Pages } from '../../config/Pages'
|
import { Pages } from '../../config/Pages'
|
||||||
import Button from '../../components/Button'
|
import Button from '../../components/Button'
|
||||||
import { Add, Eye } from 'iconsax-react'
|
import { Add, Eye } from 'iconsax-react'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const CardBankList: FC = () => {
|
const CardBankList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate, canUpdate } = usePermissions()
|
||||||
const getCardBanks = useGetCardBanks()
|
const getCardBanks = useGetCardBanks()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -23,19 +25,21 @@ const CardBankList: FC = () => {
|
|||||||
{t('cardBank.manage_banks')}
|
{t('cardBank.manage_banks')}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Link to={Pages.cardBank.create}>
|
{canCreate('bank_accounts') &&
|
||||||
<Button
|
<Link to={Pages.cardBank.create}>
|
||||||
className='px-5'
|
<Button
|
||||||
>
|
className='px-5'
|
||||||
<div className='flex gap-2'>
|
>
|
||||||
<Add
|
<div className='flex gap-2'>
|
||||||
className='size-5'
|
<Add
|
||||||
color='#fff'
|
className='size-5'
|
||||||
/>
|
color='#fff'
|
||||||
<div>{t('cardBank.add_account')}</div>
|
/>
|
||||||
</div>
|
<div>{t('cardBank.add_account')}</div>
|
||||||
</Button>
|
</div>
|
||||||
</Link>
|
</Button>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
@@ -63,9 +67,11 @@ const CardBankList: FC = () => {
|
|||||||
<Td text={item.cardNumber} />
|
<Td text={item.cardNumber} />
|
||||||
<Td text={item.IBan} />
|
<Td text={item.IBan} />
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<Link to={Pages.cardBank.detail + item.id}>
|
{canUpdate('bank_accounts') &&
|
||||||
<Eye size={20} color='#8C90A3' />
|
<Link to={Pages.cardBank.detail + item.id}>
|
||||||
</Link>
|
<Eye size={20} color='#8C90A3' />
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import { CriticismsItemTypes } from './types/CriticismsTypes'
|
|||||||
import moment from 'moment-jalaali'
|
import moment from 'moment-jalaali'
|
||||||
import ModalConfrim from '../../components/ModalConfrim'
|
import ModalConfrim from '../../components/ModalConfrim'
|
||||||
import Pagination from '../../components/Pagination'
|
import Pagination from '../../components/Pagination'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const CriticismsList: FC = () => {
|
const CriticismsList: FC = () => {
|
||||||
|
|
||||||
|
const { canDelete } = usePermissions()
|
||||||
const [itemId, setItemId] = useState<string>('')
|
const [itemId, setItemId] = useState<string>('')
|
||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const [openConfrim, setOpenConfrim] = useState(false)
|
const [openConfrim, setOpenConfrim] = useState(false)
|
||||||
@@ -69,7 +71,9 @@ const CriticismsList: FC = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</Td>
|
</Td>
|
||||||
<Td text=''>
|
<Td text=''>
|
||||||
<Trash onClick={() => handleDelete(item.id)} size={20} color='#8C90A3' className='cursor-pointer' />
|
{canDelete('criticisms') &&
|
||||||
|
<Trash onClick={() => handleDelete(item.id)} size={20} color='#8C90A3' className='cursor-pointer' />
|
||||||
|
}
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
|||||||
+23
-17
@@ -8,11 +8,13 @@ import PageLoading from "../../components/PageLoading";
|
|||||||
import Pagination from "../../components/Pagination";
|
import Pagination from "../../components/Pagination";
|
||||||
import Td from "../../components/Td";
|
import Td from "../../components/Td";
|
||||||
import { Pages } from "../../config/Pages";
|
import { Pages } from "../../config/Pages";
|
||||||
|
import { usePermissions } from "../../hooks/usePermissions";
|
||||||
import { useGetCustomers } from "./hooks/useCustomerData";
|
import { useGetCustomers } from "./hooks/useCustomerData";
|
||||||
import { CustomerItemType } from "./types/CustomerTypes";
|
import { CustomerItemType } from "./types/CustomerTypes";
|
||||||
|
|
||||||
const CustomerList: FC = () => {
|
const CustomerList: FC = () => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
const { canCreate, canUpdate } = usePermissions();
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [limit, setLimit] = useState(10);
|
const [limit, setLimit] = useState(10);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -29,17 +31,19 @@ const CustomerList: FC = () => {
|
|||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<div className="flex w-full justify-between items-center">
|
<div className="flex w-full justify-between items-center">
|
||||||
<div>{t("customer.customer_list")}</div>
|
<div>{t("customer.customer_list")}</div>
|
||||||
<Link to={Pages.customer.create}>
|
{canCreate("customers") && (
|
||||||
<Button className="px-5">
|
<Link to={Pages.customer.create}>
|
||||||
<div className="flex gap-2">
|
<Button className="px-5">
|
||||||
<Add
|
<div className="flex gap-2">
|
||||||
className="size-5"
|
<Add
|
||||||
color="#fff"
|
className="size-5"
|
||||||
/>
|
color="#fff"
|
||||||
<div>{t("customer.add_cutomer")}</div>
|
/>
|
||||||
</div>
|
<div>{t("customer.add_cutomer")}</div>
|
||||||
</Button>
|
</div>
|
||||||
</Link>
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end items-center mt-12">
|
<div className="flex justify-end items-center mt-12">
|
||||||
@@ -92,12 +96,14 @@ const CustomerList: FC = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</Td>
|
</Td>
|
||||||
<Td text={""}>
|
<Td text={""}>
|
||||||
<Link to={Pages.customer.update + item.id}>
|
{canUpdate("customers") && (
|
||||||
<Eye
|
<Link to={Pages.customer.update + item.id}>
|
||||||
size={20}
|
<Eye
|
||||||
color="#8C90A3"
|
size={20}
|
||||||
/>
|
color="#8C90A3"
|
||||||
</Link>
|
/>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
<Td text={""} />
|
<Td text={""} />
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ import PageLoading from '../../components/PageLoading'
|
|||||||
import { NumberFormat } from '../../config/func'
|
import { NumberFormat } from '../../config/func'
|
||||||
import { toast } from '../../components/Toast';
|
import { toast } from '../../components/Toast';
|
||||||
import { ErrorType } from '../../helpers/types'
|
import { ErrorType } from '../../helpers/types'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const DiscountList: FC = () => {
|
const DiscountList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||||
const [search, setSearch] = useState<string>('')
|
const [search, setSearch] = useState<string>('')
|
||||||
const getDiscounts = useGetDiscounts(search)
|
const getDiscounts = useGetDiscounts(search)
|
||||||
const deleteDiscount = useDeleteDiscount()
|
const deleteDiscount = useDeleteDiscount()
|
||||||
@@ -40,18 +42,20 @@ const DiscountList: FC = () => {
|
|||||||
{t('discount.list')}
|
{t('discount.list')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link to={Pages.discount.create}>
|
{canCreate('discounts') &&
|
||||||
<Button
|
<Link to={Pages.discount.create}>
|
||||||
className='w-[172px]'
|
<Button
|
||||||
>
|
className='w-[172px]'
|
||||||
<div className='flex gap-2 items-center'>
|
>
|
||||||
<Add size={20} color='white' />
|
<div className='flex gap-2 items-center'>
|
||||||
<div>
|
<Add size={20} color='white' />
|
||||||
{t('discount.add_new_discount')}
|
<div>
|
||||||
|
{t('discount.add_new_discount')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Button>
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -114,17 +118,21 @@ const DiscountList: FC = () => {
|
|||||||
</Td>
|
</Td>
|
||||||
<Td text={t('')}>
|
<Td text={t('')}>
|
||||||
<div className='flex gap-2 items-center'>
|
<div className='flex gap-2 items-center'>
|
||||||
<Link
|
{canUpdate('discounts') &&
|
||||||
to={Pages.discount.detail + item.id}
|
<Link
|
||||||
>
|
to={Pages.discount.detail + item.id}
|
||||||
<Edit size={20} color='#888888' />
|
>
|
||||||
</Link>
|
<Edit size={20} color='#888888' />
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
|
||||||
<Trash
|
{canDelete('discounts') &&
|
||||||
size={20}
|
<Trash
|
||||||
color='#888888'
|
size={20}
|
||||||
onClick={() => handleDelete(item.id)}
|
color='#888888'
|
||||||
/>
|
onClick={() => handleDelete(item.id)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
+30
-22
@@ -12,6 +12,7 @@ import Td from '../../components/Td'
|
|||||||
import StatusWithText from '../../components/StatusWithText'
|
import StatusWithText from '../../components/StatusWithText'
|
||||||
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
||||||
import { useDeleteGuide } from './hooks/useGuideData'
|
import { useDeleteGuide } from './hooks/useGuideData'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
type GuideItem = {
|
type GuideItem = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -24,6 +25,7 @@ type GuideItem = {
|
|||||||
|
|
||||||
const List: FC = () => {
|
const List: FC = () => {
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const getGuides = useGetGuides(id || '')
|
const getGuides = useGetGuides(id || '')
|
||||||
const deleteGuide = useDeleteGuide(id || '')
|
const deleteGuide = useDeleteGuide(id || '')
|
||||||
@@ -36,21 +38,23 @@ const List: FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
{t('guide.list_guide')}
|
{t('guide.list_guide')}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{canCreate('services') && (
|
||||||
<Link to={Pages.services.guide + id + '/create'}>
|
<div>
|
||||||
<Button
|
<Link to={Pages.services.guide + id + '/create'}>
|
||||||
className='px-5'
|
<Button
|
||||||
>
|
className='px-5'
|
||||||
<div className='flex gap-2'>
|
>
|
||||||
<Add
|
<div className='flex gap-2'>
|
||||||
className='size-5'
|
<Add
|
||||||
color='#fff'
|
className='size-5'
|
||||||
/>
|
color='#fff'
|
||||||
<div>{t('guide.submit_guide')}</div>
|
/>
|
||||||
</div>
|
<div>{t('guide.submit_guide')}</div>
|
||||||
</Button>
|
</div>
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -85,13 +89,17 @@ const List: FC = () => {
|
|||||||
</Td>
|
</Td>
|
||||||
<Td text=''>
|
<Td text=''>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Link to={Pages.services.guide + id + '/' + item.id}>
|
{canUpdate('services') && (
|
||||||
<Eye size={20} color='#8C90A3' />
|
<Link to={Pages.services.guide + id + '/' + item.id}>
|
||||||
</Link>
|
<Eye size={20} color='#8C90A3' />
|
||||||
<TrashWithConfrim
|
</Link>
|
||||||
onDelete={() => deleteGuide.mutate(item.id)}
|
)}
|
||||||
isLoading={deleteGuide.isPending}
|
{canDelete('services') && (
|
||||||
/>
|
<TrashWithConfrim
|
||||||
|
onDelete={() => deleteGuide.mutate(item.id)}
|
||||||
|
isLoading={deleteGuide.isPending}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Pagination from '../../components/Pagination'
|
|||||||
import { useGetCategory } from './hooks/useLearningData'
|
import { useGetCategory } from './hooks/useLearningData'
|
||||||
import { CategoryLearningType } from './types/LearningTypes'
|
import { CategoryLearningType } from './types/LearningTypes'
|
||||||
import CreateCategory from './components/CreateCategory'
|
import CreateCategory from './components/CreateCategory'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
|
|
||||||
const LearningCategory: FC = () => {
|
const LearningCategory: FC = () => {
|
||||||
@@ -13,6 +14,7 @@ const LearningCategory: FC = () => {
|
|||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const getCategory = useGetCategory()
|
const getCategory = useGetCategory()
|
||||||
|
const { canCreate } = usePermissions()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-4'>
|
<div className='mt-4'>
|
||||||
@@ -65,7 +67,9 @@ const LearningCategory: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CreateCategory />
|
{canCreate('learnings') && (
|
||||||
|
<CreateCategory />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
+19
-15
@@ -9,11 +9,13 @@ import PageLoading from '../../components/PageLoading'
|
|||||||
import Td from '../../components/Td'
|
import Td from '../../components/Td'
|
||||||
import { LearningItemType } from './types/LearningTypes'
|
import { LearningItemType } from './types/LearningTypes'
|
||||||
import Pagination from '../../components/Pagination'
|
import Pagination from '../../components/Pagination'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
const LearningList: FC = () => {
|
const LearningList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const getLearning = useGetLearning(page)
|
const getLearning = useGetLearning(page)
|
||||||
|
const { canCreate } = usePermissions()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-4'>
|
<div className='mt-4'>
|
||||||
@@ -21,21 +23,23 @@ const LearningList: FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
{t('learning.list_learning')}
|
{t('learning.list_learning')}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{canCreate('learnings') && (
|
||||||
<Link to={Pages.learning.create}>
|
<div>
|
||||||
<Button
|
<Link to={Pages.learning.create}>
|
||||||
className='px-5'
|
<Button
|
||||||
>
|
className='px-5'
|
||||||
<div className='flex gap-2'>
|
>
|
||||||
<Add
|
<div className='flex gap-2'>
|
||||||
className='size-5'
|
<Add
|
||||||
color='#fff'
|
className='size-5'
|
||||||
/>
|
color='#fff'
|
||||||
<div>{t('learning.submit_learning')}</div>
|
/>
|
||||||
</div>
|
<div>{t('learning.submit_learning')}</div>
|
||||||
</Button>
|
</div>
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ import Pagination from '../../components/Pagination'
|
|||||||
import moment from 'moment-jalaali'
|
import moment from 'moment-jalaali'
|
||||||
import Accept from './components/Accept'
|
import Accept from './components/Accept'
|
||||||
import Reject from './components/Reject'
|
import Reject from './components/Reject'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const PaymentList: FC = () => {
|
const PaymentList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canUpdate } = usePermissions()
|
||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const [pageGateWay, setPageGateWay] = useState<number>(1)
|
const [pageGateWay, setPageGateWay] = useState<number>(1)
|
||||||
const [activeTab, setActiveTab] = useState<PaymentStatusType>('GATEWAY')
|
const [activeTab, setActiveTab] = useState<PaymentStatusType>('GATEWAY')
|
||||||
@@ -140,7 +142,7 @@ const PaymentList: FC = () => {
|
|||||||
<Td text={t(`payment.${item.status}`)} />
|
<Td text={t(`payment.${item.status}`)} />
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
{
|
{
|
||||||
item.status === 'PENDING' &&
|
item.status === 'PENDING' && canUpdate('payments') &&
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<Accept
|
<Accept
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ import { Pages } from '../../config/Pages'
|
|||||||
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
||||||
import { ErrorType } from '../../helpers/types'
|
import { ErrorType } from '../../helpers/types'
|
||||||
import { toast } from '../../components/Toast';
|
import { toast } from '../../components/Toast';
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const ReceiptsList: FC = () => {
|
const ReceiptsList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canUpdate, canDelete } = usePermissions()
|
||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
const [activeTab, setActiveTab] = useState<InvoiceStatus>('PENDING')
|
const [activeTab, setActiveTab] = useState<InvoiceStatus>('PENDING')
|
||||||
@@ -177,13 +179,17 @@ const ReceiptsList: FC = () => {
|
|||||||
</Td>
|
</Td>
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<Link to={Pages.receipts.detail + item.id}>
|
{canUpdate('invoices') &&
|
||||||
<Edit size={20} color='#888' />
|
<Link to={Pages.receipts.detail + item.id}>
|
||||||
</Link>
|
<Edit size={20} color='#888' />
|
||||||
<TrashWithConfrim
|
</Link>
|
||||||
onDelete={() => handleDeleteInvoice(item.id)}
|
}
|
||||||
isLoading={deleteInvoice.isPending}
|
{canDelete('invoices') &&
|
||||||
/>
|
<TrashWithConfrim
|
||||||
|
onDelete={() => handleDeleteInvoice(item.id)}
|
||||||
|
isLoading={deleteInvoice.isPending}
|
||||||
|
/>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
+15
-11
@@ -8,10 +8,12 @@ import { useGetResellers } from './hooks/useResellerData'
|
|||||||
import PageLoading from '../../components/PageLoading'
|
import PageLoading from '../../components/PageLoading'
|
||||||
import Td from '../../components/Td'
|
import Td from '../../components/Td'
|
||||||
import { ResellerItemType } from './types/Types'
|
import { ResellerItemType } from './types/Types'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const ResellerList: FC = () => {
|
const ResellerList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate } = usePermissions()
|
||||||
const getReseller = useGetResellers()
|
const getReseller = useGetResellers()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -21,18 +23,20 @@ const ResellerList: FC = () => {
|
|||||||
{t('reseller.list')}
|
{t('reseller.list')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link to={Pages.reseller.create}>
|
{canCreate('reseller') && (
|
||||||
<Button
|
<Link to={Pages.reseller.create}>
|
||||||
className='w-[172px]'
|
<Button
|
||||||
>
|
className='w-[172px]'
|
||||||
<div className='flex gap-2 items-center'>
|
>
|
||||||
<Add size={20} color='white' />
|
<div className='flex gap-2 items-center'>
|
||||||
<div>
|
<Add size={20} color='white' />
|
||||||
{t('reseller.add_new')}
|
<div>
|
||||||
|
{t('reseller.add_new')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Button>
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ import StatusCategory from './components/StatusCategory'
|
|||||||
import Pagination from '../../components/Pagination'
|
import Pagination from '../../components/Pagination'
|
||||||
import UpdateCategory from './components/UpdateCategory'
|
import UpdateCategory from './components/UpdateCategory'
|
||||||
import { Trash } from 'iconsax-react'
|
import { Trash } from 'iconsax-react'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
|
|
||||||
const Category: FC = () => {
|
const Category: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||||
const [search, setSearch] = useState<string>('')
|
const [search, setSearch] = useState<string>('')
|
||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const [isActive, setIsActive] = useState<'active' | 'deactive' | ''>('')
|
const [isActive, setIsActive] = useState<'active' | 'deactive' | ''>('')
|
||||||
@@ -102,12 +104,16 @@ const Category: FC = () => {
|
|||||||
</Td>
|
</Td>
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<UpdateCategory refetch={() => getCategory.refetch()} id={item.id} />
|
{canUpdate('services') && (
|
||||||
<Trash
|
<UpdateCategory refetch={() => getCategory.refetch()} id={item.id} />
|
||||||
size={20}
|
)}
|
||||||
color='#888888'
|
{canDelete('services') && (
|
||||||
onClick={() => handleDelete(item.id)}
|
<Trash
|
||||||
/>
|
size={20}
|
||||||
|
color='#888888'
|
||||||
|
onClick={() => handleDelete(item.id)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -130,7 +136,7 @@ const Category: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CreateCategory />
|
{canCreate('services') && <CreateCategory />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
+36
-28
@@ -15,10 +15,12 @@ import Pagination from '../../components/Pagination'
|
|||||||
import ToggleStatusService from './components/ToggleStatusService'
|
import ToggleStatusService from './components/ToggleStatusService'
|
||||||
import CheckBoxComponent from '../../components/CheckBoxComponent'
|
import CheckBoxComponent from '../../components/CheckBoxComponent'
|
||||||
import ToggleSliderService from './components/ToggleSliderService'
|
import ToggleSliderService from './components/ToggleSliderService'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const ListService: FC = () => {
|
const ListService: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate, canUpdate } = usePermissions()
|
||||||
const [category, setCategory] = useState<string>('')
|
const [category, setCategory] = useState<string>('')
|
||||||
const [status, setstatus] = useState<'ACTIVE' | 'IN_ACTIVE' | ''>('')
|
const [status, setstatus] = useState<'ACTIVE' | 'IN_ACTIVE' | ''>('')
|
||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
@@ -33,21 +35,23 @@ const ListService: FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
{t('service.list_service')}
|
{t('service.list_service')}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{canCreate('services') && (
|
||||||
<Link to={Pages.services.add}>
|
<div>
|
||||||
<Button
|
<Link to={Pages.services.add}>
|
||||||
className='px-5'
|
<Button
|
||||||
>
|
className='px-5'
|
||||||
<div className='flex gap-2'>
|
>
|
||||||
<Add
|
<div className='flex gap-2'>
|
||||||
className='size-5'
|
<Add
|
||||||
color='#fff'
|
className='size-5'
|
||||||
/>
|
color='#fff'
|
||||||
<div>{t('service.submit_service')}</div>
|
/>
|
||||||
</div>
|
<div>{t('service.submit_service')}</div>
|
||||||
</Button>
|
</div>
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex justify-between items-center mt-12'>
|
<div className='flex justify-between items-center mt-12'>
|
||||||
@@ -134,16 +138,18 @@ const ListService: FC = () => {
|
|||||||
<Td text=''>
|
<Td text=''>
|
||||||
{
|
{
|
||||||
item.subscriptionCount === 0 ?
|
item.subscriptionCount === 0 ?
|
||||||
<Link to={Pages.services.plan + item.id}>
|
(canCreate('services') &&
|
||||||
<Button
|
<Link to={Pages.services.plan + item.id}>
|
||||||
className='h-8 bg-transparent border border-black text-black text-xs'
|
<Button
|
||||||
>
|
className='h-8 bg-transparent border border-black text-black text-xs'
|
||||||
<div className='flex gap-2 items-center'>
|
>
|
||||||
<Add size={20} color='black' />
|
<div className='flex gap-2 items-center'>
|
||||||
<div>{t('service.add_plan')}</div>
|
<Add size={20} color='black' />
|
||||||
</div>
|
<div>{t('service.add_plan')}</div>
|
||||||
</Button>
|
</div>
|
||||||
</Link>
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
:
|
:
|
||||||
<Link to={Pages.services.plan + item.id} className='flex gap-1 text-sm text-[#0047FF]'>
|
<Link to={Pages.services.plan + item.id} className='flex gap-1 text-sm text-[#0047FF]'>
|
||||||
<div>{item.subscriptionCount}</div>
|
<div>{item.subscriptionCount}</div>
|
||||||
@@ -164,9 +170,11 @@ const ListService: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</Td>
|
</Td>
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<Link to={Pages.services.update + item.id}>
|
{canUpdate('services') && (
|
||||||
<Eye size={20} color='#8C90A3' />
|
<Link to={Pages.services.update + item.id}>
|
||||||
</Link>
|
<Eye size={20} color='#8C90A3' />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<Link to={Pages.services.guide + item.id}>
|
<Link to={Pages.services.guide + item.id}>
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import { PlanItemType } from './types/ServiceTypes'
|
|||||||
import ToggleStatusPlan from './components/ToggleStatusPlan'
|
import ToggleStatusPlan from './components/ToggleStatusPlan'
|
||||||
import EditPlan from './components/EditPlan'
|
import EditPlan from './components/EditPlan'
|
||||||
import { NumberFormat } from '../../config/func'
|
import { NumberFormat } from '../../config/func'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const Plans: FC = () => {
|
const Plans: FC = () => {
|
||||||
|
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate, canUpdate } = usePermissions()
|
||||||
const getPlans = useGetPlans(id)
|
const getPlans = useGetPlans(id)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -27,7 +29,7 @@ const Plans: FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
{t('plan.plans_list') + ' ' + getPlans.data?.data?.service?.name}
|
{t('plan.plans_list') + ' ' + getPlans.data?.data?.service?.name}
|
||||||
</div>
|
</div>
|
||||||
<CreatePlan />
|
{canCreate('services') && <CreatePlan />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||||
@@ -54,9 +56,11 @@ const Plans: FC = () => {
|
|||||||
<ToggleStatusPlan defaultActive={item.isActive} id={item.id} />
|
<ToggleStatusPlan defaultActive={item.isActive} id={item.id} />
|
||||||
</Td>
|
</Td>
|
||||||
<Td text=''>
|
<Td text=''>
|
||||||
<EditPlan
|
{canUpdate('services') && (
|
||||||
id={item.id}
|
<EditPlan
|
||||||
/>
|
id={item.id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
|
|||||||
+32
-24
@@ -11,11 +11,13 @@ import PageLoading from '../../components/PageLoading'
|
|||||||
import { toast } from '../../components/Toast';
|
import { toast } from '../../components/Toast';
|
||||||
import { ErrorType } from '../../helpers/types'
|
import { ErrorType } from '../../helpers/types'
|
||||||
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
const SliderList: FC = () => {
|
const SliderList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const { data: sliders, isPending, refetch } = useGetSliders()
|
const { data: sliders, isPending, refetch } = useGetSliders()
|
||||||
const deleteSlider = useDeleteSlider()
|
const deleteSlider = useDeleteSlider()
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = (id: string) => {
|
||||||
deleteSlider.mutate(id, {
|
deleteSlider.mutate(id, {
|
||||||
@@ -35,21 +37,23 @@ const SliderList: FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
{t('slider.list_slider')}
|
{t('slider.list_slider')}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{canCreate('sliders') && (
|
||||||
<Link to={Pages.sliders.create}>
|
<div>
|
||||||
<Button
|
<Link to={Pages.sliders.create}>
|
||||||
className='px-5'
|
<Button
|
||||||
>
|
className='px-5'
|
||||||
<div className='flex gap-2'>
|
>
|
||||||
<Add
|
<div className='flex gap-2'>
|
||||||
className='size-5'
|
<Add
|
||||||
color='#fff'
|
className='size-5'
|
||||||
/>
|
color='#fff'
|
||||||
<div>{t('slider.submit_slider')}</div>
|
/>
|
||||||
</div>
|
<div>{t('slider.submit_slider')}</div>
|
||||||
</Button>
|
</div>
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -80,16 +84,20 @@ const SliderList: FC = () => {
|
|||||||
<Td text={item.isActive ? t('slider.active') : t('slider.inactive')} />
|
<Td text={item.isActive ? t('slider.active') : t('slider.inactive')} />
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<Link to={Pages.sliders.detail + item.id}>
|
{canUpdate('sliders') && (
|
||||||
<Edit
|
<Link to={Pages.sliders.detail + item.id}>
|
||||||
className='size-5'
|
<Edit
|
||||||
color='#888'
|
className='size-5'
|
||||||
|
color='#888'
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{canDelete('sliders') && (
|
||||||
|
<TrashWithConfrim
|
||||||
|
onDelete={() => handleDelete(item.id)}
|
||||||
|
isLoading={deleteSlider.isPending}
|
||||||
/>
|
/>
|
||||||
</Link>
|
)}
|
||||||
<TrashWithConfrim
|
|
||||||
onDelete={() => handleDelete(item.id)}
|
|
||||||
isLoading={deleteSlider.isPending}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ import { ServiceItemType } from '../service/types/ServiceTypes'
|
|||||||
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
||||||
import { ErrorType } from '../../helpers/types'
|
import { ErrorType } from '../../helpers/types'
|
||||||
import { toast } from '../../components/Toast';
|
import { toast } from '../../components/Toast';
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const SubscriptionsList: FC = () => {
|
const SubscriptionsList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canDelete } = usePermissions()
|
||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const [status, setStatus] = useState<string>('')
|
const [status, setStatus] = useState<string>('')
|
||||||
const [serviceId, setServiceId] = useState<string>('')
|
const [serviceId, setServiceId] = useState<string>('')
|
||||||
@@ -199,10 +201,12 @@ const SubscriptionsList: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td text=''>
|
<Td text=''>
|
||||||
<TrashWithConfrim
|
{canDelete('customers') && (
|
||||||
onDelete={() => handleDelete(item.id)}
|
<TrashWithConfrim
|
||||||
isLoading={isPending}
|
onDelete={() => handleDelete(item.id)}
|
||||||
/>
|
isLoading={isPending}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
+23
-15
@@ -8,9 +8,11 @@ import { Link } from 'react-router-dom'
|
|||||||
import { Pages } from '../../config/Pages'
|
import { Pages } from '../../config/Pages'
|
||||||
import Delete from './components/Delete'
|
import Delete from './components/Delete'
|
||||||
import Button from '../../components/Button'
|
import Button from '../../components/Button'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
const SupportList: FC = () => {
|
const SupportList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||||
const { data, refetch } = useGetPlans()
|
const { data, refetch } = useGetPlans()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -20,18 +22,20 @@ const SupportList: FC = () => {
|
|||||||
{t('support.plans')}
|
{t('support.plans')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link to={Pages.support.create}>
|
{canCreate('support_plan') &&
|
||||||
<Button
|
<Link to={Pages.support.create}>
|
||||||
className='w-fit px-6'
|
<Button
|
||||||
>
|
className='w-fit px-6'
|
||||||
<div className='flex gap-2 items-center'>
|
>
|
||||||
<Add size={20} color='white' />
|
<div className='flex gap-2 items-center'>
|
||||||
<div>
|
<Add size={20} color='white' />
|
||||||
{t('support.new_plan')}
|
<div>
|
||||||
|
{t('support.new_plan')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Button>
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||||
@@ -65,10 +69,14 @@ const SupportList: FC = () => {
|
|||||||
</Td>
|
</Td>
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Link to={Pages.support.detail + item.id}>
|
{canUpdate('support_plan') &&
|
||||||
<Edit size={20} color='#8C90A3' />
|
<Link to={Pages.support.detail + item.id}>
|
||||||
</Link>
|
<Edit size={20} color='#8C90A3' />
|
||||||
<Delete id={item.id} refetch={refetch} />
|
</Link>
|
||||||
|
}
|
||||||
|
{canDelete('support_plan') &&
|
||||||
|
<Delete id={item.id} refetch={refetch} />
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { clx } from "../../../helpers/utils";
|
|||||||
import { useCreateTaskPhase } from "../task-phase/hooks/useTaskPhaseData";
|
import { useCreateTaskPhase } from "../task-phase/hooks/useTaskPhaseData";
|
||||||
import type { TaskPhaseItemType } from "../task-phase/types/TaskPhaseTypes";
|
import type { TaskPhaseItemType } from "../task-phase/types/TaskPhaseTypes";
|
||||||
import { LABEL_COLOR_OPTIONS } from "./task-detail/labels/constants";
|
import { LABEL_COLOR_OPTIONS } from "./task-detail/labels/constants";
|
||||||
|
import { usePermissions } from "../../../hooks/usePermissions";
|
||||||
|
|
||||||
const DEFAULT_COLUMN_COLOR = "#4F46E5";
|
const DEFAULT_COLUMN_COLOR = "#4F46E5";
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ type Props = {
|
|||||||
|
|
||||||
const AddNewColumn: FC<Props> = ({ projectId, nextOrder, onColumnCreated }) => {
|
const AddNewColumn: FC<Props> = ({ projectId, nextOrder, onColumnCreated }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
const { canCreate } = usePermissions();
|
||||||
const colorInputRef = useRef<HTMLInputElement>(null);
|
const colorInputRef = useRef<HTMLInputElement>(null);
|
||||||
const createTaskPhase = useCreateTaskPhase();
|
const createTaskPhase = useCreateTaskPhase();
|
||||||
|
|
||||||
@@ -57,6 +59,10 @@ const AddNewColumn: FC<Props> = ({ projectId, nextOrder, onColumnCreated }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!canCreate("task_phase")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isAdding) {
|
if (!isAdding) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { toast } from '../../../components/Toast';
|
import { toast } from '../../../components/Toast';
|
||||||
import ModalConfrim from "../../../components/ModalConfrim";
|
import ModalConfrim from "../../../components/ModalConfrim";
|
||||||
import { ErrorType } from "../../../helpers/types";
|
import { ErrorType } from "../../../helpers/types";
|
||||||
|
import { usePermissions } from "../../../hooks/usePermissions";
|
||||||
import { useCreateTask, useGetTasksByTaskPhase } from "../task/hooks/useTaskData";
|
import { useCreateTask, useGetTasksByTaskPhase } from "../task/hooks/useTaskData";
|
||||||
import type { TaskItemType } from "../task/types/TaskTypes";
|
import type { TaskItemType } from "../task/types/TaskTypes";
|
||||||
import { mapTaskItemToTask } from "../task/utils/mapTaskItemToTask";
|
import { mapTaskItemToTask } from "../task/utils/mapTaskItemToTask";
|
||||||
@@ -37,6 +38,7 @@ const Column: FC<Props> = ({
|
|||||||
onColumnTitleUpdated,
|
onColumnTitleUpdated,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
const { canCreate } = usePermissions();
|
||||||
const deleteTaskPhase = useDeleteTaskPhase();
|
const deleteTaskPhase = useDeleteTaskPhase();
|
||||||
const updateTaskPhase = useUpdateTaskPhase();
|
const updateTaskPhase = useUpdateTaskPhase();
|
||||||
const createTask = useCreateTask();
|
const createTask = useCreateTask();
|
||||||
@@ -220,8 +222,8 @@ const Column: FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={columnRef}
|
ref={columnRef}
|
||||||
className={`bg-[#F0F3F7] rounded-xl p-3 sm:p-4 xl:p-6 w-[272px] sm:w-[288px] xl:w-[310px] shrink-0 flex flex-col h-full min-h-0 transition-colors ${
|
className={`bg-[#F0F3F7] rounded-xl p-3 sm:p-4 xl:p-6 w-[272px] sm:w-[288px] xl:w-[310px] shrink-0 flex flex-col h-full min-h-0 min-w-0 overflow-hidden transition-colors ${
|
||||||
isColumnDragOver ? "ring-2 ring-black/30" : ""
|
isColumnDragOver ? "ring-2 ring-black/30 ring-inset" : ""
|
||||||
} ${isDraggingColumn ? "opacity-50" : ""}`}
|
} ${isDraggingColumn ? "opacity-50" : ""}`}
|
||||||
onDragOver={handleColumnDragOver}
|
onDragOver={handleColumnDragOver}
|
||||||
onDragLeave={handleColumnDragLeave}
|
onDragLeave={handleColumnDragLeave}
|
||||||
@@ -276,8 +278,8 @@ const Column: FC<Props> = ({
|
|||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
onScroll={handleTasksScroll}
|
onScroll={handleTasksScroll}
|
||||||
className={`mt-3 sm:mt-4 xl:mt-5 flex-1 min-h-[80px] overflow-y-auto overscroll-y-contain rounded-lg transition-colors ${
|
className={`mt-3 sm:mt-4 xl:mt-5 flex-1 min-h-[80px] min-w-0 overflow-y-auto overflow-x-hidden overscroll-y-contain rounded-lg transition-colors ${
|
||||||
snapshot.isDraggingOver ? "bg-primary/5 ring-2 ring-primary/30" : ""
|
snapshot.isDraggingOver ? "bg-primary/5 ring-2 ring-primary/30 ring-inset" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isTasksPending
|
{isTasksPending
|
||||||
@@ -294,7 +296,7 @@ const Column: FC<Props> = ({
|
|||||||
ref={dragProvided.innerRef}
|
ref={dragProvided.innerRef}
|
||||||
{...dragProvided.draggableProps}
|
{...dragProvided.draggableProps}
|
||||||
{...dragProvided.dragHandleProps}
|
{...dragProvided.dragHandleProps}
|
||||||
className="mb-3 sm:mb-4"
|
className="mb-3 sm:mb-4 w-full min-w-0 max-w-full"
|
||||||
style={dragProvided.draggableProps.style}
|
style={dragProvided.draggableProps.style}
|
||||||
>
|
>
|
||||||
<Task
|
<Task
|
||||||
@@ -314,48 +316,50 @@ const Column: FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</Droppable>
|
</Droppable>
|
||||||
|
|
||||||
{isAdding ? (
|
{canCreate("task") && (
|
||||||
<div className="mt-3 sm:mt-4 xl:mt-5 shrink-0 flex flex-col gap-3">
|
isAdding ? (
|
||||||
<input
|
<div className="mt-3 sm:mt-4 xl:mt-5 shrink-0 flex flex-col gap-3">
|
||||||
type="text"
|
<input
|
||||||
value={title}
|
type="text"
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
value={title}
|
||||||
onKeyDown={(e) => {
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
if (e.key === "Enter") handleCreateTask();
|
onKeyDown={(e) => {
|
||||||
}}
|
if (e.key === "Enter") handleCreateTask();
|
||||||
placeholder={t("taskmanager.task_title")}
|
}}
|
||||||
className="w-full bg-white rounded-lg px-3 py-2.5 text-sm outline-none"
|
placeholder={t("taskmanager.task_title")}
|
||||||
autoFocus
|
className="w-full bg-white rounded-lg px-3 py-2.5 text-sm outline-none"
|
||||||
/>
|
autoFocus
|
||||||
<div className="flex items-center gap-2">
|
/>
|
||||||
<button
|
<div className="flex items-center gap-2">
|
||||||
type="button"
|
<button
|
||||||
onClick={handleCreateTask}
|
type="button"
|
||||||
disabled={!title.trim() || createTask.isPending}
|
onClick={handleCreateTask}
|
||||||
className="flex items-center gap-2 bg-black text-white text-[13px] rounded-full px-4 py-2 cursor-pointer disabled:opacity-50"
|
disabled={!title.trim() || createTask.isPending}
|
||||||
>
|
className="flex items-center gap-2 bg-black text-white text-[13px] rounded-full px-4 py-2 cursor-pointer disabled:opacity-50"
|
||||||
<span>{t("taskmanager.add_task")}</span>
|
>
|
||||||
<Add size={18} color="white" />
|
<span>{t("taskmanager.add_task")}</span>
|
||||||
</button>
|
<Add size={18} color="white" />
|
||||||
<button
|
</button>
|
||||||
type="button"
|
<button
|
||||||
onClick={resetTaskForm}
|
type="button"
|
||||||
className="cursor-pointer"
|
onClick={resetTaskForm}
|
||||||
aria-label={t("cancel")}
|
className="cursor-pointer"
|
||||||
>
|
aria-label={t("cancel")}
|
||||||
<CloseCircle size={22} color="#888888" variant="Bold" />
|
>
|
||||||
</button>
|
<CloseCircle size={22} color="#888888" variant="Bold" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
) : (
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
onClick={() => setIsAdding(true)}
|
||||||
onClick={() => setIsAdding(true)}
|
className="mt-3 sm:mt-4 xl:mt-5 flex gap-2 items-center shrink-0 cursor-pointer"
|
||||||
className="mt-3 sm:mt-4 xl:mt-5 flex gap-2 items-center shrink-0 cursor-pointer"
|
>
|
||||||
>
|
<AddCircle size={18} color="#888888" />
|
||||||
<AddCircle size={18} color="#888888" />
|
<span className="text-description text-[13px] mt-0.5">{t("taskmanager.add_new_task")}</span>
|
||||||
<span className="text-description text-[13px] mt-0.5">{t("taskmanager.add_new_task")}</span>
|
</button>
|
||||||
</button>
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
|||||||
import { More } from "iconsax-react";
|
import { More } from "iconsax-react";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { usePermissions } from "../../../hooks/usePermissions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
@@ -10,6 +11,7 @@ type Props = {
|
|||||||
|
|
||||||
const ColumnMenu: FC<Props> = ({ onEdit, onDelete }) => {
|
const ColumnMenu: FC<Props> = ({ onEdit, onDelete }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
const { canUpdate, canDelete } = usePermissions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
@@ -26,26 +28,30 @@ const ColumnMenu: FC<Props> = ({ onEdit, onDelete }) => {
|
|||||||
anchor="bottom start"
|
anchor="bottom start"
|
||||||
className="z-20 mt-1 min-w-[120px] rounded-xl bg-white shadow-md border border-border py-1 text-sm"
|
className="z-20 mt-1 min-w-[120px] rounded-xl bg-white shadow-md border border-border py-1 text-sm"
|
||||||
>
|
>
|
||||||
<button
|
{canUpdate("task_phase") && (
|
||||||
type="button"
|
<button
|
||||||
onClick={() => {
|
type="button"
|
||||||
close();
|
onClick={() => {
|
||||||
onEdit();
|
close();
|
||||||
}}
|
onEdit();
|
||||||
className="w-full px-4 py-2.5 text-right hover:bg-black/5 transition-colors"
|
}}
|
||||||
>
|
className="w-full px-4 py-2.5 text-right hover:bg-black/5 transition-colors"
|
||||||
{t("taskmanager.edit_column")}
|
>
|
||||||
</button>
|
{t("taskmanager.edit_column")}
|
||||||
<button
|
</button>
|
||||||
type="button"
|
)}
|
||||||
onClick={() => {
|
{canDelete("task_phase") && (
|
||||||
close();
|
<button
|
||||||
onDelete();
|
type="button"
|
||||||
}}
|
onClick={() => {
|
||||||
className="w-full px-4 py-2.5 text-right text-red-500 hover:bg-red-50 transition-colors"
|
close();
|
||||||
>
|
onDelete();
|
||||||
{t("taskmanager.delete_column")}
|
}}
|
||||||
</button>
|
className="w-full px-4 py-2.5 text-right text-red-500 hover:bg-red-50 transition-colors"
|
||||||
|
>
|
||||||
|
{t("taskmanager.delete_column")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</PopoverPanel>
|
</PopoverPanel>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -33,17 +33,17 @@ const Task: FC<Props> = ({ task, isDragging, onClick }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={`bg-white rounded-lg p-2 cursor-grab active:cursor-grabbing transition-opacity touch-manipulation ${
|
className={`bg-white rounded-lg p-2 w-full min-w-0 max-w-full overflow-hidden cursor-grab active:cursor-grabbing transition-opacity touch-manipulation ${
|
||||||
isDragging ? "opacity-40 shadow-lg" : ""
|
isDragging ? "opacity-40 shadow-lg" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{task.color ? <div className="h-10 rounded-lg" style={{ backgroundColor: task.color }} /> : null}
|
{task.color ? <div className="h-10 rounded-lg" style={{ backgroundColor: task.color }} /> : null}
|
||||||
{task.tag ? (
|
{task.tag ? (
|
||||||
<div className="bg-[#FF76C2] h-5 px-3 flex items-center mt-2 text-[10px] text-white rounded-md w-fit">
|
<div className="bg-[#FF76C2] h-5 px-3 flex items-center mt-2 text-[10px] text-white rounded-md w-fit max-w-full truncate">
|
||||||
{task.tag}
|
{task.tag}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="font-bold text-xs mt-2">{task.title}</div>
|
<div className="font-bold text-xs mt-2 break-words">{task.title}</div>
|
||||||
{task.ticketId ? (
|
{task.ticketId ? (
|
||||||
<Link
|
<Link
|
||||||
to={Pages.ticket.detail + task.ticketId}
|
to={Pages.ticket.detail + task.ticketId}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ArrowDown2, CloseCircle, Paintbucket } from "iconsax-react";
|
|||||||
import { type FC } from "react";
|
import { type FC } from "react";
|
||||||
import TrashWithConfrim from "../../../../components/TrashWithConfrim";
|
import TrashWithConfrim from "../../../../components/TrashWithConfrim";
|
||||||
import { clx } from "../../../../helpers/utils";
|
import { clx } from "../../../../helpers/utils";
|
||||||
|
import { usePermissions } from "../../../../hooks/usePermissions";
|
||||||
import type { Column } from "../../types";
|
import type { Column } from "../../types";
|
||||||
import TaskDetailCoverColorPopover from "./cover/TaskDetailCoverColorPopover";
|
import TaskDetailCoverColorPopover from "./cover/TaskDetailCoverColorPopover";
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ const TaskDetailHeader: FC<Props> = ({
|
|||||||
onDelete,
|
onDelete,
|
||||||
isDeleting,
|
isDeleting,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { canDelete } = usePermissions();
|
||||||
const selectedColumn = columns.find((column) => column.id === selectedPhaseId);
|
const selectedColumn = columns.find((column) => column.id === selectedPhaseId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -99,7 +101,7 @@ const TaskDetailHeader: FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
{onDelete ? (
|
{onDelete && canDelete("task") ? (
|
||||||
<div className="size-8 rounded-full bg-white/60 flex items-center justify-center">
|
<div className="size-8 rounded-full bg-white/60 flex items-center justify-center">
|
||||||
<TrashWithConfrim onDelete={onDelete} isLoading={isDeleting} color="#FF0000" />
|
<TrashWithConfrim onDelete={onDelete} isLoading={isDeleting} color="#FF0000" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { CloseCircle, Document, Paperclip2 } from "iconsax-react";
|
||||||
|
import { useEffect, useState, type FC } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { isImageAttachment } from "../attachment/isImageAttachment";
|
||||||
|
|
||||||
|
const getFileName = (url: string) => {
|
||||||
|
const path = url.split("?")[0].split("#")[0];
|
||||||
|
return decodeURIComponent(path.split("/").pop() || url);
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
attachments: string[];
|
||||||
|
onRemove?: (index: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CommentAttachments: FC<Props> = ({ attachments, onRemove }) => {
|
||||||
|
const { t } = useTranslation("global");
|
||||||
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!previewUrl) return;
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") setPreviewUrl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [previewUrl]);
|
||||||
|
|
||||||
|
if (attachments.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
{attachments.map((url, index) => {
|
||||||
|
const isImage = isImageAttachment(url);
|
||||||
|
const title = getFileName(url);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${url}-${index}`}
|
||||||
|
className="relative flex items-center gap-2 max-w-full rounded-lg bg-white/60 px-2 py-1.5"
|
||||||
|
>
|
||||||
|
{isImage ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPreviewUrl(url)}
|
||||||
|
className="shrink-0 size-8 rounded-md overflow-hidden bg-white outline-none"
|
||||||
|
>
|
||||||
|
<img src={url} alt={title} className="size-full object-cover" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="shrink-0 size-8 rounded-md bg-white flex items-center justify-center">
|
||||||
|
<Document size={16} color="#292D32" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isImage ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPreviewUrl(url)}
|
||||||
|
className="text-[11px] text-[#0047FF] underline truncate max-w-[140px] outline-none"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-[11px] text-[#0047FF] underline truncate max-w-[140px] flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Paperclip2 size={12} color="#0047FF" />
|
||||||
|
{t("attach")} {index + 1}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{onRemove ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onRemove(index)}
|
||||||
|
className="shrink-0 outline-none"
|
||||||
|
aria-label={t("cancel")}
|
||||||
|
>
|
||||||
|
<CloseCircle size={14} color="#FF0000" />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{previewUrl &&
|
||||||
|
createPortal(
|
||||||
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4" role="dialog" aria-modal="true">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={t("cancel")}
|
||||||
|
className="absolute inset-0 bg-black/70 outline-none"
|
||||||
|
onClick={() => setPreviewUrl(null)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative z-10 max-w-[90vw] max-h-[90vh]">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={t("cancel")}
|
||||||
|
onClick={() => setPreviewUrl(null)}
|
||||||
|
className="absolute -top-3 -left-3 size-9 rounded-full bg-white flex items-center justify-center shadow outline-none"
|
||||||
|
>
|
||||||
|
<CloseCircle size={22} color="#292D32" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<img
|
||||||
|
src={previewUrl}
|
||||||
|
alt={getFileName(previewUrl)}
|
||||||
|
className="max-w-[90vw] max-h-[90vh] rounded-2xl object-contain bg-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommentAttachments;
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
import { Edit2 } from "iconsax-react";
|
import { CloseCircle, Edit2, Paperclip2 } from "iconsax-react";
|
||||||
import moment from "moment-jalaali";
|
import moment from "moment-jalaali";
|
||||||
import { type FC, useState } from "react";
|
import { type FC, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from "../../../../../components/Toast";
|
import { toast } from "../../../../../components/Toast";
|
||||||
import TrashWithConfrim from "../../../../../components/TrashWithConfrim";
|
import TrashWithConfrim from "../../../../../components/TrashWithConfrim";
|
||||||
import { ErrorType } from "../../../../../helpers/types";
|
import { ErrorType } from "../../../../../helpers/types";
|
||||||
|
import { useMultiUpload } from "../../../../service/hooks/useServiceData";
|
||||||
import { useDeleteTaskComment, useUpdateTaskComment } from "../../../task/hooks/useTaskCommentData";
|
import { useDeleteTaskComment, useUpdateTaskComment } from "../../../task/hooks/useTaskCommentData";
|
||||||
import type { TaskCommentType } from "../../../task/types/TaskCommentTypes";
|
import type { TaskCommentType } from "../../../task/types/TaskCommentTypes";
|
||||||
import UserAvatar from "../../UserAvatar";
|
import UserAvatar from "../../UserAvatar";
|
||||||
|
import CommentAttachments from "./CommentAttachments";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
comment: TaskCommentType;
|
comment: TaskCommentType;
|
||||||
@@ -16,29 +18,86 @@ type Props = {
|
|||||||
|
|
||||||
const CommentItem: FC<Props> = ({ comment, taskId }) => {
|
const CommentItem: FC<Props> = ({ comment, taskId }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [content, setContent] = useState(comment.content);
|
const [content, setContent] = useState(comment.content);
|
||||||
|
const [attachments, setAttachments] = useState<string[]>(comment.attachments ?? []);
|
||||||
|
const [pendingFiles, setPendingFiles] = useState<File[]>([]);
|
||||||
const updateComment = useUpdateTaskComment();
|
const updateComment = useUpdateTaskComment();
|
||||||
const deleteComment = useDeleteTaskComment();
|
const deleteComment = useDeleteTaskComment();
|
||||||
|
const multiUpload = useMultiUpload();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setContent(comment.content);
|
||||||
|
setAttachments(comment.attachments ?? []);
|
||||||
|
}, [comment.content, comment.attachments]);
|
||||||
|
|
||||||
const authorName = comment.user
|
const authorName = comment.user
|
||||||
? `${comment.user.firstName} ${comment.user.lastName}`.trim()
|
? `${comment.user.firstName} ${comment.user.lastName}`.trim()
|
||||||
: t("taskmanager.task_detail.unknown_user");
|
: t("taskmanager.task_detail.unknown_user");
|
||||||
|
|
||||||
const handleSave = () => {
|
const isSubmitting = updateComment.isPending || multiUpload.isPending;
|
||||||
|
|
||||||
|
const handleFilesSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const selected = Array.from(e.target.files ?? []);
|
||||||
|
if (selected.length === 0) return;
|
||||||
|
|
||||||
|
setPendingFiles((prev) => [...prev, ...selected]);
|
||||||
|
e.target.value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
const trimmedContent = content.trim();
|
const trimmedContent = content.trim();
|
||||||
if (!trimmedContent || trimmedContent === comment.content) {
|
const originalAttachments = comment.attachments ?? [];
|
||||||
|
const contentUnchanged = trimmedContent === comment.content;
|
||||||
|
const attachmentsUnchanged =
|
||||||
|
pendingFiles.length === 0 &&
|
||||||
|
attachments.length === originalAttachments.length &&
|
||||||
|
attachments.every((url, index) => url === originalAttachments[index]);
|
||||||
|
|
||||||
|
if (contentUnchanged && attachmentsUnchanged) {
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
setContent(comment.content);
|
setContent(comment.content);
|
||||||
|
setAttachments(originalAttachments);
|
||||||
|
setPendingFiles([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!trimmedContent && attachments.length === 0 && pendingFiles.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextAttachments = [...attachments];
|
||||||
|
|
||||||
|
if (pendingFiles.length > 0) {
|
||||||
|
const formData = new FormData();
|
||||||
|
pendingFiles.forEach((file) => formData.append("files", file));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uploadResult = await multiUpload.mutateAsync(formData);
|
||||||
|
const uploadedUrls: string[] =
|
||||||
|
uploadResult?.data?.map((item: { url: string }) => item?.url).filter(Boolean) ?? [];
|
||||||
|
nextAttachments = [...nextAttachments, ...uploadedUrls];
|
||||||
|
} catch (error) {
|
||||||
|
toast((error as ErrorType).response?.data?.error.message[0], "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateComment.mutate(
|
updateComment.mutate(
|
||||||
{ id: comment.id, params: { content: trimmedContent }, taskId },
|
{
|
||||||
|
id: comment.id,
|
||||||
|
params: {
|
||||||
|
content: trimmedContent,
|
||||||
|
attachments: nextAttachments,
|
||||||
|
},
|
||||||
|
taskId,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast(t("success"), "success");
|
toast(t("success"), "success");
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
|
setPendingFiles([]);
|
||||||
},
|
},
|
||||||
onError: (error: ErrorType) => {
|
onError: (error: ErrorType) => {
|
||||||
toast(error.response?.data?.error.message[0], "error");
|
toast(error.response?.data?.error.message[0], "error");
|
||||||
@@ -49,6 +108,8 @@ const CommentItem: FC<Props> = ({ comment, taskId }) => {
|
|||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setContent(comment.content);
|
setContent(comment.content);
|
||||||
|
setAttachments(comment.attachments ?? []);
|
||||||
|
setPendingFiles([]);
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,11 +149,42 @@ const CommentItem: FC<Props> = ({ comment, taskId }) => {
|
|||||||
className="w-full bg-white/50 rounded-lg px-3 py-2 text-xs outline-none resize-none"
|
className="w-full bg-white/50 rounded-lg px-3 py-2 text-xs outline-none resize-none"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-2 mt-2">
|
|
||||||
|
<CommentAttachments
|
||||||
|
attachments={attachments}
|
||||||
|
onRemove={(index) => setAttachments((prev) => prev.filter((_, i) => i !== index))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{pendingFiles.length > 0 ? (
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
{pendingFiles.map((file, index) => (
|
||||||
|
<div
|
||||||
|
key={`${file.name}-${file.size}-${index}`}
|
||||||
|
className="flex items-center gap-1.5 max-w-full rounded-lg bg-white/60 px-2 py-1"
|
||||||
|
>
|
||||||
|
<span className="text-[11px] truncate max-w-[160px]">{file.name}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPendingFiles((prev) => prev.filter((_, i) => i !== index))}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="shrink-0 outline-none disabled:opacity-50"
|
||||||
|
aria-label={t("cancel")}
|
||||||
|
>
|
||||||
|
<CloseCircle size={14} color="#FF0000" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="flex gap-2 mt-2 items-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={updateComment.isPending || !content.trim()}
|
disabled={
|
||||||
|
isSubmitting ||
|
||||||
|
(!content.trim() && attachments.length === 0 && pendingFiles.length === 0)
|
||||||
|
}
|
||||||
className="h-7 px-3 rounded-lg bg-[#292D32] text-white text-[11px] disabled:opacity-50"
|
className="h-7 px-3 rounded-lg bg-[#292D32] text-white text-[11px] disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{t("save")}
|
{t("save")}
|
||||||
@@ -100,15 +192,34 @@ const CommentItem: FC<Props> = ({ comment, taskId }) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
disabled={updateComment.isPending}
|
disabled={isSubmitting}
|
||||||
className="h-7 px-3 rounded-lg bg-white/50 text-[#292D32] text-[11px] border border-white"
|
className="h-7 px-3 rounded-lg bg-white/50 text-[#292D32] text-[11px] border border-white"
|
||||||
>
|
>
|
||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="size-7 rounded-lg bg-white/70 flex items-center justify-center disabled:opacity-50"
|
||||||
|
aria-label={t("select_file")}
|
||||||
|
>
|
||||||
|
<Paperclip2 size={16} color="#292D32" />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFilesSelected}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-1 text-xs leading-6 whitespace-pre-line break-words">{comment.content}</div>
|
<>
|
||||||
|
<div className="mt-1 text-xs leading-6 whitespace-pre-line break-words">{comment.content}</div>
|
||||||
|
<CommentAttachments attachments={comment.attachments ?? []} />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { type FC, useState } from "react";
|
import { CloseCircle, Paperclip2 } from "iconsax-react";
|
||||||
|
import { type FC, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from "../../../../../components/Toast";
|
import { toast } from "../../../../../components/Toast";
|
||||||
import { ErrorType } from "../../../../../helpers/types";
|
import { ErrorType } from "../../../../../helpers/types";
|
||||||
|
import { useMultiUpload } from "../../../../service/hooks/useServiceData";
|
||||||
import { useCreateTaskComment, useGetTaskComments } from "../../../task/hooks/useTaskCommentData";
|
import { useCreateTaskComment, useGetTaskComments } from "../../../task/hooks/useTaskCommentData";
|
||||||
import CommentItem from "./CommentItem";
|
import CommentItem from "./CommentItem";
|
||||||
|
|
||||||
@@ -12,23 +14,59 @@ type Props = {
|
|||||||
|
|
||||||
const CommentList: FC<Props> = ({ taskId, enabled = true }) => {
|
const CommentList: FC<Props> = ({ taskId, enabled = true }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
const getComments = useGetTaskComments(taskId, enabled);
|
const getComments = useGetTaskComments(taskId, enabled);
|
||||||
const createComment = useCreateTaskComment();
|
const createComment = useCreateTaskComment();
|
||||||
|
const multiUpload = useMultiUpload();
|
||||||
|
|
||||||
const comments = getComments.data?.data ?? [];
|
const comments = getComments.data?.data ?? [];
|
||||||
const trimmedContent = content.trim();
|
const trimmedContent = content.trim();
|
||||||
const isSubmitDisabled = createComment.isPending || trimmedContent.length === 0;
|
const isSubmitting = createComment.isPending || multiUpload.isPending;
|
||||||
|
const isSubmitDisabled = isSubmitting || (trimmedContent.length === 0 && files.length === 0);
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleFilesSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (!trimmedContent) return;
|
const selected = Array.from(e.target.files ?? []);
|
||||||
|
if (selected.length === 0) return;
|
||||||
|
|
||||||
|
setFiles((prev) => [...prev, ...selected]);
|
||||||
|
e.target.value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFile = (index: number) => {
|
||||||
|
setFiles((prev) => prev.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (isSubmitDisabled) return;
|
||||||
|
|
||||||
|
let attachments: string[] | undefined;
|
||||||
|
|
||||||
|
if (files.length > 0) {
|
||||||
|
const formData = new FormData();
|
||||||
|
files.forEach((file) => formData.append("files", file));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uploadResult = await multiUpload.mutateAsync(formData);
|
||||||
|
attachments = uploadResult?.data?.map((item: { url: string }) => item?.url).filter(Boolean);
|
||||||
|
} catch (error) {
|
||||||
|
toast((error as ErrorType).response?.data?.error.message[0], "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createComment.mutate(
|
createComment.mutate(
|
||||||
{ content: trimmedContent, taskId },
|
{
|
||||||
|
content: trimmedContent,
|
||||||
|
taskId,
|
||||||
|
...(attachments?.length ? { attachments } : {}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast(t("success"), "success");
|
toast(t("success"), "success");
|
||||||
setContent("");
|
setContent("");
|
||||||
|
setFiles([]);
|
||||||
},
|
},
|
||||||
onError: (error: ErrorType) => {
|
onError: (error: ErrorType) => {
|
||||||
toast(error.response?.data?.error.message[0], "error");
|
toast(error.response?.data?.error.message[0], "error");
|
||||||
@@ -42,21 +80,61 @@ const CommentList: FC<Props> = ({ taskId, enabled = true }) => {
|
|||||||
<div className="text-sm font-bold">{t("taskmanager.task_detail.comments")}</div>
|
<div className="text-sm font-bold">{t("taskmanager.task_detail.comments")}</div>
|
||||||
|
|
||||||
<div className="mt-3 relative bg-white/25 rounded-xl">
|
<div className="mt-3 relative bg-white/25 rounded-xl">
|
||||||
<button
|
<div className="absolute top-2 left-2 z-10 flex items-center gap-1.5">
|
||||||
type="button"
|
<button
|
||||||
onClick={handleSubmit}
|
type="button"
|
||||||
disabled={isSubmitDisabled}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
className="absolute top-2 left-2 z-10 h-7 px-2.5 rounded-lg bg-[#292D32] text-white text-[11px] disabled:opacity-50"
|
disabled={isSubmitting}
|
||||||
>
|
className="size-7 rounded-lg bg-white/70 flex items-center justify-center disabled:opacity-50"
|
||||||
{t("taskmanager.task_detail.add_comment")}
|
aria-label={t("select_file")}
|
||||||
</button>
|
>
|
||||||
|
<Paperclip2 size={16} color="#292D32" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={isSubmitDisabled}
|
||||||
|
className="h-7 px-2.5 rounded-lg bg-[#292D32] text-white text-[11px] disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{t("taskmanager.task_detail.add_comment")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFilesSelected}
|
||||||
|
/>
|
||||||
<textarea
|
<textarea
|
||||||
value={content}
|
value={content}
|
||||||
onChange={(e) => setContent(e.target.value)}
|
onChange={(e) => setContent(e.target.value)}
|
||||||
placeholder={t("taskmanager.task_detail.comment_placeholder")}
|
placeholder={t("taskmanager.task_detail.comment_placeholder")}
|
||||||
rows={3}
|
rows={3}
|
||||||
className="w-full bg-transparent rounded-xl px-4 pt-4 pb-3 pl-20 text-xs outline-none resize-none placeholder:text-description"
|
className="w-full bg-transparent rounded-xl px-4 pt-4 pb-3 pl-28 text-xs outline-none resize-none placeholder:text-description"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{files.length > 0 ? (
|
||||||
|
<div className="px-3 pb-3 flex flex-wrap gap-2">
|
||||||
|
{files.map((file, index) => (
|
||||||
|
<div
|
||||||
|
key={`${file.name}-${file.size}-${index}`}
|
||||||
|
className="flex items-center gap-1.5 max-w-full rounded-lg bg-white/60 px-2 py-1"
|
||||||
|
>
|
||||||
|
<span className="text-[11px] truncate max-w-[160px]">{file.name}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleRemoveFile(index)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="shrink-0 outline-none disabled:opacity-50"
|
||||||
|
aria-label={t("cancel")}
|
||||||
|
>
|
||||||
|
<CloseCircle size={14} color="#FF0000" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{getComments.isPending ? (
|
{getComments.isPending ? (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
|||||||
import { More } from "iconsax-react";
|
import { More } from "iconsax-react";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { usePermissions } from "../../../../hooks/usePermissions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
@@ -10,6 +11,7 @@ type Props = {
|
|||||||
|
|
||||||
const ProjectCardMenu: FC<Props> = ({ onEdit, onDelete }) => {
|
const ProjectCardMenu: FC<Props> = ({ onEdit, onDelete }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
const { canUpdate, canDelete } = usePermissions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
@@ -21,20 +23,24 @@ const ProjectCardMenu: FC<Props> = ({ onEdit, onDelete }) => {
|
|||||||
anchor="bottom start"
|
anchor="bottom start"
|
||||||
className="z-20 mt-1 min-w-[120px] rounded-xl bg-white shadow-md border border-border py-1 text-sm"
|
className="z-20 mt-1 min-w-[120px] rounded-xl bg-white shadow-md border border-border py-1 text-sm"
|
||||||
>
|
>
|
||||||
<button
|
{canUpdate("project") && (
|
||||||
type="button"
|
<button
|
||||||
onClick={onEdit}
|
type="button"
|
||||||
className="w-full px-4 py-2.5 text-right hover:bg-[#F4F5F9] transition-colors"
|
onClick={onEdit}
|
||||||
>
|
className="w-full px-4 py-2.5 text-right hover:bg-[#F4F5F9] transition-colors"
|
||||||
{t("edit")}
|
>
|
||||||
</button>
|
{t("edit")}
|
||||||
<button
|
</button>
|
||||||
type="button"
|
)}
|
||||||
onClick={onDelete}
|
{canDelete("project") && (
|
||||||
className="w-full px-4 py-2.5 text-right text-red-500 hover:bg-red-50 transition-colors"
|
<button
|
||||||
>
|
type="button"
|
||||||
{t("taskmanager.delete_project")}
|
onClick={onDelete}
|
||||||
</button>
|
className="w-full px-4 py-2.5 text-right text-red-500 hover:bg-red-50 transition-colors"
|
||||||
|
>
|
||||||
|
{t("taskmanager.delete_project")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</PopoverPanel>
|
</PopoverPanel>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import Button from "../../../../components/Button";
|
import Button from "../../../../components/Button";
|
||||||
import { Pages } from "../../../../config/Pages";
|
import { Pages } from "../../../../config/Pages";
|
||||||
|
import { usePermissions } from "../../../../hooks/usePermissions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -13,6 +14,7 @@ type Props = {
|
|||||||
|
|
||||||
const ProjectListHeader: FC<Props> = ({ title, workspaceId, onSettingsClick }) => {
|
const ProjectListHeader: FC<Props> = ({ title, workspaceId, onSettingsClick }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
const { canCreate } = usePermissions();
|
||||||
const createProjectLink = workspaceId ? `${Pages.taskmanager.createProject}?workspaceId=${workspaceId}` : Pages.taskmanager.createProject;
|
const createProjectLink = workspaceId ? `${Pages.taskmanager.createProject}?workspaceId=${workspaceId}` : Pages.taskmanager.createProject;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
return (
|
return (
|
||||||
@@ -31,18 +33,20 @@ const ProjectListHeader: FC<Props> = ({ title, workspaceId, onSettingsClick }) =
|
|||||||
<span>برگشت</span>
|
<span>برگشت</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Link to={createProjectLink}>
|
{canCreate("project") && (
|
||||||
<Button
|
<Link to={createProjectLink}>
|
||||||
onClick={onSettingsClick}
|
<Button
|
||||||
className="flex items-center gap-2 whitespace-nowrap px-4"
|
onClick={onSettingsClick}
|
||||||
>
|
className="flex items-center gap-2 whitespace-nowrap px-4"
|
||||||
<Add
|
>
|
||||||
size={18}
|
<Add
|
||||||
color="white"
|
size={18}
|
||||||
/>
|
color="white"
|
||||||
<span>{t("taskmanager.new_project")}</span>
|
/>
|
||||||
</Button>
|
<span>{t("taskmanager.new_project")}</span>
|
||||||
</Link>
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type TaskCommentType = {
|
|||||||
taskId: string;
|
taskId: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
user?: TaskCommentAuthorType;
|
user?: TaskCommentAuthorType;
|
||||||
|
attachments?: string[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
};
|
};
|
||||||
@@ -20,10 +21,12 @@ export type TaskCommentType = {
|
|||||||
export type CreateTaskCommentType = {
|
export type CreateTaskCommentType = {
|
||||||
content: string;
|
content: string;
|
||||||
taskId: string;
|
taskId: string;
|
||||||
|
attachments?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateTaskCommentType = {
|
export type UpdateTaskCommentType = {
|
||||||
content: string;
|
content: string;
|
||||||
|
attachments?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface GetTaskCommentsResponse extends IResponse<TaskCommentType[]> {
|
export interface GetTaskCommentsResponse extends IResponse<TaskCommentType[]> {
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import { Pages } from "../../../config/Pages";
|
|||||||
import { ErrorType } from "../../../helpers/types";
|
import { ErrorType } from "../../../helpers/types";
|
||||||
import { useDeleteWorkspace, useGetWorkspaces } from "./hooks/useWorkspaceData";
|
import { useDeleteWorkspace, useGetWorkspaces } from "./hooks/useWorkspaceData";
|
||||||
import { WorkspaceItemType } from "./types/WorkspaceTypes";
|
import { WorkspaceItemType } from "./types/WorkspaceTypes";
|
||||||
|
import { usePermissions } from "../../../hooks/usePermissions";
|
||||||
|
|
||||||
const WorkspaceList: FC = () => {
|
const WorkspaceList: FC = () => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions();
|
||||||
const getWorkspaces = useGetWorkspaces();
|
const getWorkspaces = useGetWorkspaces();
|
||||||
const deleteWorkspace = useDeleteWorkspace();
|
const deleteWorkspace = useDeleteWorkspace();
|
||||||
const [deleteTarget, setDeleteTarget] = useState<WorkspaceItemType | null>(null);
|
const [deleteTarget, setDeleteTarget] = useState<WorkspaceItemType | null>(null);
|
||||||
@@ -38,17 +40,19 @@ const WorkspaceList: FC = () => {
|
|||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<div className="flex w-full justify-between items-center">
|
<div className="flex w-full justify-between items-center">
|
||||||
<div>{t("taskmanager.workspace_list")}</div>
|
<div>{t("taskmanager.workspace_list")}</div>
|
||||||
<Link to={Pages.taskmanager.createWorkspace}>
|
{canCreate("workspace") && (
|
||||||
<Button className="px-5">
|
<Link to={Pages.taskmanager.createWorkspace}>
|
||||||
<div className="flex gap-2">
|
<Button className="px-5">
|
||||||
<Add
|
<div className="flex gap-2">
|
||||||
className="size-5"
|
<Add
|
||||||
color="#fff"
|
className="size-5"
|
||||||
/>
|
color="#fff"
|
||||||
<div>{t("taskmanager.new_workspace")}</div>
|
/>
|
||||||
</div>
|
<div>{t("taskmanager.new_workspace")}</div>
|
||||||
</Button>
|
</div>
|
||||||
</Link>
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{getWorkspaces.isPending ? (
|
{getWorkspaces.isPending ? (
|
||||||
@@ -92,18 +96,22 @@ const WorkspaceList: FC = () => {
|
|||||||
</Td>
|
</Td>
|
||||||
<Td text="">
|
<Td text="">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link to={Pages.taskmanager.updateWorkspace + item.id}>
|
{canUpdate("workspace") && (
|
||||||
<Edit
|
<Link to={Pages.taskmanager.updateWorkspace + item.id}>
|
||||||
|
<Edit
|
||||||
|
size={20}
|
||||||
|
color="#8C90A3"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{canDelete("workspace") && (
|
||||||
|
<Trash
|
||||||
|
onClick={() => setDeleteTarget(item)}
|
||||||
|
color="#888"
|
||||||
size={20}
|
size={20}
|
||||||
color="#8C90A3"
|
className="cursor-pointer"
|
||||||
/>
|
/>
|
||||||
</Link>
|
)}
|
||||||
<Trash
|
|
||||||
onClick={() => setDeleteTarget(item)}
|
|
||||||
color="#888"
|
|
||||||
size={20}
|
|
||||||
className="cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ import { useCreateCategory, useGetCategories } from './hooks/useTicketData'
|
|||||||
import Textarea from '../../components/Textarea'
|
import Textarea from '../../components/Textarea'
|
||||||
import { ErrorType } from '../../helpers/types'
|
import { ErrorType } from '../../helpers/types'
|
||||||
import { toast } from '../../components/Toast';
|
import { toast } from '../../components/Toast';
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const TicketCategory: FC = () => {
|
const TicketCategory: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate } = usePermissions()
|
||||||
const getGroups = useGetGroups()
|
const getGroups = useGetGroups()
|
||||||
const createCategory = useCreateCategory()
|
const createCategory = useCreateCategory()
|
||||||
const getCategories = useGetCategories()
|
const getCategories = useGetCategories()
|
||||||
@@ -100,6 +102,7 @@ const TicketCategory: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{canCreate('ticket_categories') &&
|
||||||
<div className='bg-white w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
<div className='bg-white w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
||||||
<div className='text-sm'>
|
<div className='text-sm'>
|
||||||
{t('ticket.add_category')}
|
{t('ticket.add_category')}
|
||||||
@@ -187,6 +190,7 @@ const TicketCategory: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div> */}
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
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";
|
||||||
|
import { usePermissions } from "../../../hooks/usePermissions";
|
||||||
|
|
||||||
|
const TicketMastersList: FC = () => {
|
||||||
|
const { t } = useTranslation("global");
|
||||||
|
const { canCreate, canDelete } = usePermissions();
|
||||||
|
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>
|
||||||
|
{canCreate("ticket_masters") && (
|
||||||
|
<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> */}
|
||||||
|
{canDelete("ticket_masters") && (
|
||||||
|
<TrashWithConfrim
|
||||||
|
onDelete={() => handleDelete(item.id)}
|
||||||
|
isLoading={deleteMaster.isPending}
|
||||||
|
color="#8C90A3"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TicketMastersList;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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,11 +7,13 @@ import { Pages } from '../../config/Pages'
|
|||||||
import { Add } from 'iconsax-react'
|
import { Add } from 'iconsax-react'
|
||||||
import Td from '../../components/Td'
|
import Td from '../../components/Td'
|
||||||
import { GroupItemType } from './types/UserTypes'
|
import { GroupItemType } from './types/UserTypes'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const GroupList: FC = () => {
|
const GroupList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { canCreate } = usePermissions()
|
||||||
const getGroups = useGetGroups()
|
const getGroups = useGetGroups()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -20,20 +22,22 @@ const GroupList: FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
{t('user.groups')}
|
{t('user.groups')}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{canCreate('admins') && (
|
||||||
<Button
|
<div>
|
||||||
className='px-5'
|
<Button
|
||||||
onClick={() => navigate(Pages.users.groupList)}
|
className='px-5'
|
||||||
>
|
onClick={() => navigate(Pages.users.groupList)}
|
||||||
<div className='flex gap-2'>
|
>
|
||||||
<Add
|
<div className='flex gap-2'>
|
||||||
className='size-5'
|
<Add
|
||||||
color='#fff'
|
className='size-5'
|
||||||
/>
|
color='#fff'
|
||||||
<div>{t('user.group_add')}</div>
|
/>
|
||||||
</div>
|
<div>{t('user.group_add')}</div>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||||
|
|||||||
+31
-23
@@ -13,10 +13,12 @@ import { toast } from '../../components/Toast';
|
|||||||
import { ErrorType } from '../../helpers/types'
|
import { ErrorType } from '../../helpers/types'
|
||||||
import Pagination from '../../components/Pagination'
|
import Pagination from '../../components/Pagination'
|
||||||
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
const UsersList: FC = () => {
|
const UsersList: FC = () => {
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const [search, setSearch] = useState<string>('')
|
const [search, setSearch] = useState<string>('')
|
||||||
const getUsers = useGetUsers(search, undefined, page)
|
const getUsers = useGetUsers(search, undefined, page)
|
||||||
@@ -40,20 +42,22 @@ const UsersList: FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
{t('user.users_list')}
|
{t('user.users_list')}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{canCreate('admins') && (
|
||||||
<Button
|
<div>
|
||||||
className='px-5'
|
<Button
|
||||||
onClick={() => navigate(Pages.users.create)}
|
className='px-5'
|
||||||
>
|
onClick={() => navigate(Pages.users.create)}
|
||||||
<div className='flex gap-2'>
|
>
|
||||||
<Add
|
<div className='flex gap-2'>
|
||||||
className='size-5'
|
<Add
|
||||||
color='#fff'
|
className='size-5'
|
||||||
/>
|
color='#fff'
|
||||||
<div>{t('user.add_user')}</div>
|
/>
|
||||||
</div>
|
<div>{t('user.add_user')}</div>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex justify-between items-center mt-12'>
|
<div className='flex justify-between items-center mt-12'>
|
||||||
@@ -103,16 +107,20 @@ const UsersList: FC = () => {
|
|||||||
</Td>
|
</Td>
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<Link to={Pages.users.detail + item.id}>
|
{canUpdate('admins') && (
|
||||||
<Eye size={20} color='#8C90A3' />
|
<Link to={Pages.users.detail + item.id}>
|
||||||
</Link>
|
<Eye size={20} color='#8C90A3' />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
<TrashWithConfrim
|
{canDelete('admins') && (
|
||||||
size={20}
|
<TrashWithConfrim
|
||||||
color='#8C90A3'
|
size={20}
|
||||||
onDelete={() => handleDeleteUser(item.id)}
|
color='#8C90A3'
|
||||||
isLoading={deleteUser.isPending}
|
onDelete={() => handleDeleteUser(item.id)}
|
||||||
/>
|
isLoading={deleteUser.isPending}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td text={''} />
|
<Td text={''} />
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import { Link, useNavigate } from 'react-router-dom'
|
|||||||
import { Pages } from '../../config/Pages'
|
import { Pages } from '../../config/Pages'
|
||||||
import { useGetRoles } from './hooks/useUserData'
|
import { useGetRoles } from './hooks/useUserData'
|
||||||
import { RoleItemType } from './types/UserTypes'
|
import { RoleItemType } from './types/UserTypes'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const RolesList: FC = () => {
|
const RolesList: FC = () => {
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate, canUpdate } = usePermissions()
|
||||||
const [search, setSearch] = useState<string>('')
|
const [search, setSearch] = useState<string>('')
|
||||||
const getRoles = useGetRoles(search)
|
const getRoles = useGetRoles(search)
|
||||||
|
|
||||||
@@ -23,20 +25,22 @@ const RolesList: FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
{t('user.roles_list')}
|
{t('user.roles_list')}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{canCreate('admins') && (
|
||||||
<Button
|
<div>
|
||||||
className='px-5'
|
<Button
|
||||||
onClick={() => navigate(Pages.users.roleCreate)}
|
className='px-5'
|
||||||
>
|
onClick={() => navigate(Pages.users.roleCreate)}
|
||||||
<div className='flex gap-2'>
|
>
|
||||||
<Add
|
<div className='flex gap-2'>
|
||||||
className='size-5'
|
<Add
|
||||||
color='#fff'
|
className='size-5'
|
||||||
/>
|
color='#fff'
|
||||||
<div>{t('user.add_role')}</div>
|
/>
|
||||||
</div>
|
<div>{t('user.add_role')}</div>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex justify-end items-center mt-12'>
|
<div className='flex justify-end items-center mt-12'>
|
||||||
@@ -69,9 +73,11 @@ const RolesList: FC = () => {
|
|||||||
<Td text={item.userCount + ''} />
|
<Td text={item.userCount + ''} />
|
||||||
<Td text={item.isAdmin ? t('yes') : t('no')} />
|
<Td text={item.isAdmin ? t('yes') : t('no')} />
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<Link to={Pages.users.roleUpdate + item.id}>
|
{canUpdate('admins') && (
|
||||||
<Eye size={20} color='#8C90A3' />
|
<Link to={Pages.users.roleUpdate + item.id}>
|
||||||
</Link>
|
<Eye size={20} color='#8C90A3' />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 />}
|
||||||
|
|||||||
@@ -39,13 +39,13 @@ const ContentSubMenu: FC = () => {
|
|||||||
title={t("submenu.blog_comments")}
|
title={t("submenu.blog_comments")}
|
||||||
isActive={isActive("/blog/comments")}
|
isActive={isActive("/blog/comments")}
|
||||||
link={Pages.blog.comments}
|
link={Pages.blog.comments}
|
||||||
activeName="blogs"
|
activeName="blog_comments"
|
||||||
/>
|
/>
|
||||||
<SubMenuItem
|
<SubMenuItem
|
||||||
title={t("submenu.blog_category")}
|
title={t("submenu.blog_category")}
|
||||||
isActive={isActive("/blog/category")}
|
isActive={isActive("/blog/category")}
|
||||||
link={Pages.blog.category}
|
link={Pages.blog.category}
|
||||||
activeName="blogs"
|
activeName="blog_categories"
|
||||||
/>
|
/>
|
||||||
<SubMenuItem
|
<SubMenuItem
|
||||||
title={t("sidebar.ads")}
|
title={t("sidebar.ads")}
|
||||||
@@ -69,7 +69,7 @@ const ContentSubMenu: FC = () => {
|
|||||||
title={t("sidebar.sliders")}
|
title={t("sidebar.sliders")}
|
||||||
isActive={isActive("sliders")}
|
isActive={isActive("sliders")}
|
||||||
link={Pages.sliders.list}
|
link={Pages.sliders.list}
|
||||||
activeName="blogs"
|
activeName="sliders"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,7 +39,13 @@ const SupportSubMenu: FC = () => {
|
|||||||
title={t("submenu.ticket_category")}
|
title={t("submenu.ticket_category")}
|
||||||
isActive={isActive("/tickets/category")}
|
isActive={isActive("/tickets/category")}
|
||||||
link={Pages.ticket.category}
|
link={Pages.ticket.category}
|
||||||
activeName="tickets"
|
activeName="ticket_categories"
|
||||||
|
/>
|
||||||
|
<SubMenuItem
|
||||||
|
title={t("submenu.ticket_masters")}
|
||||||
|
isActive={isActive("/tickets/masters")}
|
||||||
|
link={Pages.ticket.masters}
|
||||||
|
activeName="ticket_masters"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user