Compare commits

...

13 Commits

Author SHA1 Message Date
hamid zarghami 6b96ecf9d9 refer ticket
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-26 14:57:27 +03:30
hamid zarghami 4bb164be0f fix render column
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-26 14:39:55 +03:30
hamid zarghami 2f71b4c96c edit check list item 2026-07-26 14:38:46 +03:30
hamid zarghami cabf248c0e workspace users
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-26 10:05:58 +03:30
hamid zarghami 1990e73368 permisisions
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-26 08:59:39 +03:30
hamid zarghami 185961c162 permission 2026-07-26 08:45:44 +03:30
hamid zarghami b60a38e21d ticket master 2026-07-25 16:36:35 +03:30
hamid zarghami f53b5d8fe1 fix design bug 2026-07-25 15:53:16 +03:30
hamid zarghami f5da44d9cf attachment comment 2026-07-25 15:49:02 +03:30
hamid zarghami 77475f09cd project users
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-25 15:28:09 +03:30
hamid zarghami 9413cf03c9 out side click fix 2026-07-25 12:52:49 +03:30
hamid zarghami b2f9166c22 tooltip 2026-07-25 12:46:57 +03:30
hamid zarghami 7fedfdfbfd padding left for task description 2026-07-25 12:25:48 +03:30
74 changed files with 1856 additions and 702 deletions
+3
View File
@@ -36,6 +36,9 @@ export const Pages = {
create: "/tickets/create", create: "/tickets/create",
detail: "/tickets/messages/", detail: "/tickets/messages/",
category: "/tickets/category", category: "/tickets/category",
masters: "/tickets/masters",
mastersCreate: "/tickets/masters/create",
mastersUpdate: "/tickets/masters/update/",
}, },
announcement: { announcement: {
list: "/announcement", list: "/announcement",
+80 -4
View File
@@ -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[],
+28
View File
@@ -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
View File
@@ -143,6 +143,7 @@
"representative_list": "لیست نمایندگان", "representative_list": "لیست نمایندگان",
"ticket_list": "لیست تیکت ها", "ticket_list": "لیست تیکت ها",
"ticket_category": "دسته بندی تیکت ها", "ticket_category": "دسته بندی تیکت ها",
"ticket_masters": "مستر",
"send_ticket": "ارسال تیکت", "send_ticket": "ارسال تیکت",
"category": "دسته بندی", "category": "دسته بندی",
"user_list": "لیست کاربران", "user_list": "لیست کاربران",
@@ -377,7 +378,10 @@
"column": "ستون", "column": "ستون",
"select_workspace_required": "فضای کاری را انتخاب کنید", "select_workspace_required": "فضای کاری را انتخاب کنید",
"select_project_required": "پروژه را انتخاب کنید", "select_project_required": "پروژه را انتخاب کنید",
"select_column_required": "ستون را انتخاب کنید" "select_column_required": "ستون را انتخاب کنید",
"masters": "مستر",
"add_master": "افزودن مستر",
"edit_master": "ویرایش مستر"
}, },
"active": "فعال", "active": "فعال",
"inactive": "غیرفعال", "inactive": "غیرفعال",
+23 -15
View File
@@ -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>
+31 -23
View File
@@ -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>
+65 -57
View File
@@ -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
View File
@@ -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>
+3 -1
View File
@@ -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
View File
@@ -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>
) )
+5 -1
View File
@@ -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
View File
@@ -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>
+29 -21
View File
@@ -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
View File
@@ -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>
+5 -1
View File
@@ -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
View File
@@ -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>
{ {
+3 -1
View File
@@ -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
+13 -7
View File
@@ -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
View File
@@ -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>
{ {
+13 -7
View File
@@ -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
View File
@@ -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}>
+8 -4
View File
@@ -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
View File
@@ -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>
+8 -4
View File
@@ -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
View File
@@ -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
+51 -47
View File
@@ -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();
@@ -52,7 +54,7 @@ const Column: FC<Props> = ({
const items = tasksData?.pages.flatMap((page) => page.data?.items ?? []) ?? []; const items = tasksData?.pages.flatMap((page) => page.data?.items ?? []) ?? [];
return [...items] return [...items]
.sort((a, b) => (a.order ?? a.priority ?? 0) - (b.order ?? b.priority ?? 0)) .sort((a, b) => (a.priority ?? a.order ?? 0) - (b.priority ?? b.order ?? 0))
.map((task, index) => mapTaskItemToTask(task as TaskItemType, column.id, index)); .map((task, index) => mapTaskItemToTask(task as TaskItemType, column.id, index));
}, [tasksData?.pages, column.id]); }, [tasksData?.pages, column.id]);
@@ -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>
); );
+26 -20
View File
@@ -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>
</> </>
)} )}
+3 -3
View File
@@ -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}
@@ -5,11 +5,22 @@ type Props = {
src?: string | null; src?: string | null;
alt?: string; alt?: string;
className?: string; className?: string;
showTooltip?: boolean;
}; };
const UserAvatar: FC<Props> = ({ src, alt = "", className = "size-6" }) => ( const UserAvatar: FC<Props> = ({ src, alt = "", className = "size-6", showTooltip = true }) => (
<div className={`${className} rounded-full overflow-hidden shrink-0 bg-[#D0D0D0]`}> <div className={`relative group/avatar ${className} rounded-full overflow-visible shrink-0`}>
<img src={src || AvatarImage} alt={alt} className="size-full object-cover" /> <div className="size-full rounded-full overflow-hidden bg-[#D0D0D0]">
<img src={src || AvatarImage} alt={alt} className="size-full object-cover" />
</div>
{showTooltip && alt ? (
<span
role="tooltip"
className="pointer-events-none absolute bottom-full left-1/2 z-20 mb-1.5 -translate-x-1/2 whitespace-nowrap rounded-md bg-[#292D32] px-2 py-1 text-[10px] text-white opacity-0 transition-opacity group-hover/avatar:opacity-100"
>
{alt}
</span>
) : null}
</div> </div>
); );
@@ -28,7 +28,7 @@ const TaskDetailDescription: FC<Props> = ({ value = "", onChange, onSave, isSavi
onChange={(e) => onChange?.(e.target.value)} onChange={(e) => onChange?.(e.target.value)}
placeholder={t("taskmanager.task_detail.description_placeholder")} placeholder={t("taskmanager.task_detail.description_placeholder")}
rows={5} rows={5}
className="w-full bg-transparent rounded-xl px-4 pt-4 pb-3 text-xs outline-none resize-none placeholder:text-description" className="w-full bg-transparent rounded-xl px-4 pt-4 pb-3 pl-20 text-xs outline-none resize-none placeholder:text-description"
/> />
</div> </div>
</div> </div>
@@ -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>
@@ -199,6 +199,7 @@ const TaskDetailModal: FC<Props> = ({ open, task, columns, projectId, onClose, o
onTabChange={handleTabChange} onTabChange={handleTabChange}
taskDetail={taskDetail} taskDetail={taskDetail}
taskId={task.id} taskId={task.id}
projectId={taskDetail?.projectId ?? projectId}
/> />
</div> </div>
@@ -61,8 +61,10 @@ const TaskDetailPopoverPanel: FC<Props> = ({ anchorRef, children, className, wid
onClose(); onClose();
}; };
document.addEventListener("pointerdown", handlePointerDown); // Capture: DefaulModal stops pointerdown propagation on bubble, so outside-click
return () => document.removeEventListener("pointerdown", handlePointerDown); // would never reach a document bubble listener while the modal is open.
document.addEventListener("pointerdown", handlePointerDown, true);
return () => document.removeEventListener("pointerdown", handlePointerDown, true);
}, [anchorRef, onClose]); }, [anchorRef, onClose]);
if (!position) return null; if (!position) return null;
@@ -1,7 +1,6 @@
import { type FC, useEffect, useMemo, useState } from "react"; import { type FC, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from '../../../../components/Toast'; import { toast } from '../../../../components/Toast';
import { useGetUsersInfinite } from "../../../users/hooks/useUserData";
import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover"; import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover";
import TaskDetailDatePopover from "./date/TaskDetailDatePopover"; import TaskDetailDatePopover from "./date/TaskDetailDatePopover";
import { parseApiDate, toApiDate } from "./date/utils"; import { parseApiDate, toApiDate } from "./date/utils";
@@ -15,7 +14,6 @@ import type { TaskDetailTab } from "./types";
import TaskDetailUsersPopover from "./users/TaskDetailUsersPopover"; import TaskDetailUsersPopover from "./users/TaskDetailUsersPopover";
import TaskDetailTimePopover from "./time/TaskDetailTimePopover"; import TaskDetailTimePopover from "./time/TaskDetailTimePopover";
import type { TaskUser } from "./users/types"; import type { TaskUser } from "./users/types";
import type { UserItemType } from "../../../users/types/UserTypes";
import type { TaskDetailType } from "../../task/types/TaskTypes"; import type { TaskDetailType } from "../../task/types/TaskTypes";
import { import {
useCreateCheckListItem, useCreateCheckListItem,
@@ -24,6 +22,8 @@ import {
useDeleteTaskRemark, useDeleteTaskRemark,
useUpdateTask, useUpdateTask,
} from "../../task/hooks/useTaskData"; } from "../../task/hooks/useTaskData";
import { useGetProjectUsers } from "../../project/hooks/useProjectData";
import type { ProjectUserType } from "../../project/types/ProjectTypes";
import { useSingleUpload } from "../../../service/hooks/useServiceData"; import { useSingleUpload } from "../../../service/hooks/useServiceData";
import { ErrorType } from "../../../../helpers/types"; import { ErrorType } from "../../../../helpers/types";
@@ -32,9 +32,10 @@ type Props = {
onTabChange: (tab: TaskDetailTab) => void; onTabChange: (tab: TaskDetailTab) => void;
taskDetail?: TaskDetailType; taskDetail?: TaskDetailType;
taskId: string; taskId: string;
projectId: string;
}; };
const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, taskId }) => { const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, taskId, projectId }) => {
const { t } = useTranslation("global"); const { t } = useTranslation("global");
const createTaskRemark = useCreateTaskRemark(); const createTaskRemark = useCreateTaskRemark();
const deleteTaskRemark = useDeleteTaskRemark(); const deleteTaskRemark = useDeleteTaskRemark();
@@ -44,14 +45,7 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
const updateTask = useUpdateTask(); const updateTask = useUpdateTask();
const isUsersOpen = activeTab === "users"; const isUsersOpen = activeTab === "users";
const [userSearch, setUserSearch] = useState(""); const [userSearch, setUserSearch] = useState("");
const [debouncedUserSearch, setDebouncedUserSearch] = useState(""); const { data: usersData, isPending: isUsersPending } = useGetProjectUsers(projectId, isUsersOpen);
const {
data: usersData,
isPending: isUsersPending,
isFetchingNextPage: isFetchingNextUsersPage,
fetchNextPage: fetchNextUsersPage,
hasNextPage: hasNextUsersPage,
} = useGetUsersInfinite(debouncedUserSearch, undefined, isUsersOpen);
const [labelsView, setLabelsView] = useState<LabelsView>("list"); const [labelsView, setLabelsView] = useState<LabelsView>("list");
const [labels, setLabels] = useState<TaskLabel[]>([]); const [labels, setLabels] = useState<TaskLabel[]>([]);
const [selectedLabelIds, setSelectedLabelIds] = useState<Set<string>>(new Set()); const [selectedLabelIds, setSelectedLabelIds] = useState<Set<string>>(new Set());
@@ -59,14 +53,6 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
const [startDate, setStartDate] = useState<DateObject | null>(null); const [startDate, setStartDate] = useState<DateObject | null>(null);
const [endDate, setEndDate] = useState<DateObject | null>(null); const [endDate, setEndDate] = useState<DateObject | null>(null);
useEffect(() => {
const timeoutId = window.setTimeout(() => {
setDebouncedUserSearch(userSearch.trim());
}, 300);
return () => window.clearTimeout(timeoutId);
}, [userSearch]);
useEffect(() => { useEffect(() => {
if (!taskDetail) return; if (!taskDetail) return;
@@ -91,16 +77,13 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
}, [taskDetail]); }, [taskDetail]);
const selectedLabels = labels.filter((label) => selectedLabelIds.has(label.id)); const selectedLabels = labels.filter((label) => selectedLabelIds.has(label.id));
const users = useMemo((): TaskUser[] => { const projectUsers = useMemo((): TaskUser[] => {
const apiUsers: TaskUser[] = const apiUsers: TaskUser[] =
usersData?.pages.flatMap( usersData?.data?.map((user: ProjectUserType) => ({
(page) => id: user.id,
page.data?.users?.map((user: UserItemType) => ({ name: `${user.firstName} ${user.lastName}`.trim(),
id: user.id, avatar: user.profilePic,
name: `${user.firstName} ${user.lastName}`.trim(), })) ?? [];
avatar: user.profilePic,
})) ?? [],
) ?? [];
const taskUsers: TaskUser[] = const taskUsers: TaskUser[] =
taskDetail?.users?.map((user) => ({ taskDetail?.users?.map((user) => ({
id: user.id, id: user.id,
@@ -123,20 +106,21 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
}); });
return Array.from(userMap.values()); return Array.from(userMap.values());
}, [usersData?.pages, taskDetail?.users]); }, [usersData?.data, taskDetail?.users]);
const listUsers = useMemo((): TaskUser[] => { const listUsers = useMemo((): TaskUser[] => {
return ( const query = userSearch.trim().toLowerCase();
usersData?.pages.flatMap( const apiUsers: TaskUser[] =
(page) => usersData?.data?.map((user: ProjectUserType) => ({
page.data?.users?.map((user: UserItemType) => ({ id: user.id,
id: user.id, name: `${user.firstName} ${user.lastName}`.trim(),
name: `${user.firstName} ${user.lastName}`.trim(), avatar: user.profilePic,
avatar: user.profilePic, })) ?? [];
})) ?? [],
) ?? [] if (!query) return apiUsers;
);
}, [usersData?.pages]); return apiUsers.filter((user) => user.name.toLowerCase().includes(query));
const selectedUsers = users.filter((user) => selectedUserIds.has(user.id)); }, [usersData?.data, userSearch]);
const selectedUsers = projectUsers.filter((user) => selectedUserIds.has(user.id));
const isLabelsOpen = activeTab === "labels"; const isLabelsOpen = activeTab === "labels";
const isChecklistOpen = activeTab === "checklist"; const isChecklistOpen = activeTab === "checklist";
const isAttachmentOpen = activeTab === "attachment"; const isAttachmentOpen = activeTab === "attachment";
@@ -382,11 +366,6 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
onToggle={handleUserToggle} onToggle={handleUserToggle}
isLoading={isUsersPending} isLoading={isUsersPending}
isSubmitting={updateTask.isPending} isSubmitting={updateTask.isPending}
hasNextPage={hasNextUsersPage}
isFetchingNextPage={isFetchingNextUsersPage}
onLoadMore={() => {
void fetchNextUsersPage();
}}
search={userSearch} search={userSearch}
onSearchChange={setUserSearch} onSearchChange={setUserSearch}
/> />
@@ -1,17 +1,83 @@
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { More } from "iconsax-react"; import { More } from "iconsax-react";
import { type FC } from "react"; import { type FC, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import TaskDetailLabelCheckbox from "../labels/TaskDetailLabelCheckbox"; import TaskDetailLabelCheckbox from "../labels/TaskDetailLabelCheckbox";
type Props = { type Props = {
title: string; title: string;
checked: boolean; checked: boolean;
onToggle: () => void; onToggle: () => void;
onEdit: () => void; onSave: (title: string) => void;
onDelete: () => void; onDelete: () => void;
isSaving?: boolean;
}; };
const ChecklistItem: FC<Props> = ({ title, checked, onToggle, onEdit, onDelete }) => { const ChecklistItem: FC<Props> = ({ title, checked, onToggle, onSave, onDelete, isSaving = false }) => {
const { t } = useTranslation("global");
const [isEditing, setIsEditing] = useState(false);
const [editTitle, setEditTitle] = useState(title);
useEffect(() => {
if (!isEditing) {
setEditTitle(title);
}
}, [title, isEditing]);
const handleSave = () => {
const trimmed = editTitle.trim();
if (!trimmed || trimmed === title) {
setIsEditing(false);
setEditTitle(title);
return;
}
onSave(trimmed);
setIsEditing(false);
};
const handleCancel = () => {
setIsEditing(false);
setEditTitle(title);
};
if (isEditing) {
return (
<div className="flex gap-1 items-center">
<TaskDetailLabelCheckbox checked={checked} onToggle={onToggle} />
<div className="flex-1 flex gap-2 items-center">
<input
value={editTitle}
onChange={(e) => setEditTitle(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") handleSave();
if (e.key === "Escape") handleCancel();
}}
autoFocus
disabled={isSaving}
className="flex-1 h-[27px] px-2 text-xs bg-white/50 rounded outline-none"
/>
<button
type="button"
onClick={handleSave}
disabled={!editTitle.trim() || isSaving}
className="h-7 px-3 rounded-lg bg-[#292D32] text-white text-[11px] disabled:opacity-50"
>
{t("save")}
</button>
<button
type="button"
onClick={handleCancel}
disabled={isSaving}
className="h-7 px-3 rounded-lg bg-white/50 text-[#292D32] text-[11px] border border-white disabled:opacity-50"
>
{t("cancel")}
</button>
</div>
</div>
);
}
return ( return (
<div className="flex gap-1 h-[27px] items-center"> <div className="flex gap-1 h-[27px] items-center">
<TaskDetailLabelCheckbox checked={checked} onToggle={onToggle} /> <TaskDetailLabelCheckbox checked={checked} onToggle={onToggle} />
@@ -24,14 +90,32 @@ const ChecklistItem: FC<Props> = ({ title, checked, onToggle, onEdit, onDelete }
<More size={20} color="#000" /> <More size={20} color="#000" />
</PopoverButton> </PopoverButton>
<PopoverPanel anchor="bottom end" className="z-[80] mt-1 rounded-[10px] bg-white shadow-md text-right p-3"> <PopoverPanel anchor="bottom end" className="z-[80] mt-1 rounded-[10px] bg-white shadow-md text-right p-3">
<button type="button" onClick={onEdit} className="w-full text-sm text-right"> {({ close }) => (
ویرایش <>
</button> <button
type="button"
onClick={() => {
close();
setIsEditing(true);
}}
className="w-full text-sm text-right"
>
ویرایش
</button>
<button type="button" onClick={onDelete} className="w-full mt-3 text-sm text-right text-[#FF0000]"> <button
پاک کردن type="button"
</button> onClick={() => {
close();
onDelete();
}}
className="w-full mt-3 text-sm text-right text-[#FF0000]"
>
پاک کردن
</button>
</>
)}
</PopoverPanel> </PopoverPanel>
</Popover> </Popover>
</div> </div>
@@ -2,7 +2,7 @@ import { Edit } from "iconsax-react";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import ModalConfrim from "../../../../../components/ModalConfrim"; import ModalConfrim from "../../../../../components/ModalConfrim";
import { toast } from '../../../../../components/Toast'; import { toast } from "../../../../../components/Toast";
import ProgressBar from "../../../../../components/ProgressBar"; import ProgressBar from "../../../../../components/ProgressBar";
import { ErrorType } from "../../../../../helpers/types"; import { ErrorType } from "../../../../../helpers/types";
import { useDeleteCheckListItem, useUpdateCheckListItem } from "../../../task/hooks/useTaskData"; import { useDeleteCheckListItem, useUpdateCheckListItem } from "../../../task/hooks/useTaskData";
@@ -37,12 +37,23 @@ const CheckLists = ({
const doneCount = completedCheckListItemCount ?? items.filter((item) => item.checked).length; const doneCount = completedCheckListItemCount ?? items.filter((item) => item.checked).length;
const progress = totalCount > 0 ? Math.round((doneCount / totalCount) * 100) : 0; const progress = totalCount > 0 ? Math.round((doneCount / totalCount) * 100) : 0;
const handleToggle = (id: string, checked: boolean) => { const handleToggle = (id: string, title: string, checked: boolean) => {
updateCheckListItem.mutate( updateCheckListItem.mutate(
{ id, params: { isDone: !checked }, taskId }, { id, params: { title, isDone: !checked, taskId }, taskId },
{ {
onError: (error: ErrorType) => { onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0], 'error'); toast(error.response?.data?.error.message[0], "error");
},
},
);
};
const handleSave = (id: string, title: string, checked: boolean) => {
updateCheckListItem.mutate(
{ id, params: { title, isDone: checked, taskId }, taskId },
{
onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0], "error");
}, },
}, },
); );
@@ -58,7 +69,7 @@ const CheckLists = ({
setDeleteItemId(null); setDeleteItemId(null);
}, },
onError: (error: ErrorType) => { onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0], 'error'); toast(error.response?.data?.error.message[0], "error");
}, },
}, },
); );
@@ -84,9 +95,10 @@ const CheckLists = ({
key={item.id} key={item.id}
title={item.title} title={item.title}
checked={item.checked} checked={item.checked}
onToggle={() => handleToggle(item.id, item.checked)} onToggle={() => handleToggle(item.id, item.title, item.checked)}
onEdit={() => {}} onSave={(title) => handleSave(item.id, title, item.checked)}
onDelete={() => setDeleteItemId(item.id)} onDelete={() => setDeleteItemId(item.id)}
isSaving={updateCheckListItem.isPending}
/> />
))} ))}
</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);
}; };
@@ -69,7 +130,7 @@ const CommentItem: FC<Props> = ({ comment, taskId }) => {
return ( return (
<div className="bg-white/25 rounded-xl p-3"> <div className="bg-white/25 rounded-xl p-3">
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<UserAvatar src={comment.user?.profilePic} alt={authorName} className="size-7" /> <UserAvatar src={comment.user?.profilePic} alt={authorName} className="size-7" showTooltip={false} />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
@@ -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 ? (
@@ -72,7 +72,7 @@ const TaskDetailUsersList: FC<Props> = ({
onToggle={() => onToggle(user.id)} onToggle={() => onToggle(user.id)}
ariaLabel={user.name} ariaLabel={user.name}
/> />
<UserAvatar src={user.avatar} alt={user.name} /> <UserAvatar src={user.avatar} alt={user.name} showTooltip={false} />
<div className="text-[10px]">{user.name}</div> <div className="text-[10px]">{user.name}</div>
</div> </div>
); );
@@ -3,10 +3,14 @@ import { FC, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Select from "../../../../components/Select"; import Select from "../../../../components/Select";
import SwitchComponent from "../../../../components/Switch"; import SwitchComponent from "../../../../components/Switch";
import { useGetUsers } from "../../../users/hooks/useUserData"; import {
import { UserItemType } from "../../../users/types/UserTypes"; useGetWorkspaces,
import { useGetWorkspaces } from "../../workspace/hooks/useWorkspaceData"; useGetWorkspaceUsers,
import { WorkspaceItemType } from "../../workspace/types/WorkspaceTypes"; } from "../../workspace/hooks/useWorkspaceData";
import {
WorkspaceItemType,
WorkspaceUserType,
} from "../../workspace/types/WorkspaceTypes";
import WorkspaceColorPicker from "../../workspace/components/WorkspaceColorPicker"; import WorkspaceColorPicker from "../../workspace/components/WorkspaceColorPicker";
import UserAvatar from "../../components/UserAvatar"; import UserAvatar from "../../components/UserAvatar";
@@ -67,8 +71,8 @@ const CreateProjectSidebar: FC<Props> = ({
className, className,
}) => { }) => {
const { t } = useTranslation("global"); const { t } = useTranslation("global");
const getUsers = useGetUsers("");
const getWorkspaces = useGetWorkspaces(); const getWorkspaces = useGetWorkspaces();
const getWorkspaceUsers = useGetWorkspaceUsers(workspaceId);
const [userSelectValue, setUserSelectValue] = useState(""); const [userSelectValue, setUserSelectValue] = useState("");
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
@@ -82,19 +86,24 @@ const CreateProjectSidebar: FC<Props> = ({
value: workspace.id, value: workspace.id,
})); }));
const userItems = const workspaceUsers: WorkspaceUserType[] = Array.isArray(
getUsers.data?.data?.users getWorkspaceUsers.data?.data,
?.filter((user: UserItemType) => !selectedUsers.some((selected) => selected.id === user.id)) )
.map((user: UserItemType) => ({ ? getWorkspaceUsers.data.data
label: `${user.firstName} ${user.lastName}`, : (getWorkspaceUsers.data?.data?.users ?? []);
value: user.id,
})) ?? []; const userItems = workspaceUsers
.filter((user) => !selectedUsers.some((selected) => selected.id === user.id))
.map((user) => ({
label: `${user.firstName} ${user.lastName}`,
value: user.id,
}));
const handleUserSelect = (e: React.ChangeEvent<HTMLSelectElement>) => { const handleUserSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value; const value = e.target.value;
if (!value) return; if (!value) return;
const user = getUsers.data?.data?.users?.find((item: UserItemType) => item.id === value); const user = workspaceUsers.find((item) => item.id === value);
if (user) { if (user) {
onSelectedUsersChange([ onSelectedUsersChange([
@@ -140,14 +149,26 @@ const CreateProjectSidebar: FC<Props> = ({
placeholder={t("taskmanager.select_workspace")} placeholder={t("taskmanager.select_workspace")}
items={workspaceItems} items={workspaceItems}
value={workspaceId} value={workspaceId}
onChange={(e) => onWorkspaceIdChange(e.target.value)} onChange={(e) => {
onWorkspaceIdChange(e.target.value);
onSelectedUsersChange([]);
setUserSelectValue("");
}}
className="border" className="border"
error_text={workspaceError ?? ""} error_text={workspaceError ?? ""}
/> />
</div> </div>
<div className="mt-8"> <div className="mt-8">
<Select label={t("taskmanager.users")} placeholder={t("taskmanager.select_user")} items={userItems} value={userSelectValue} onChange={handleUserSelect} className="border" /> <Select
label={t("taskmanager.users")}
placeholder={t("taskmanager.select_user")}
items={userItems}
value={userSelectValue}
onChange={handleUserSelect}
className="border"
disabled={!workspaceId}
/>
<div className="mt-3 border border-dashed border-border rounded-xl p-4 flex gap-2 flex-wrap min-h-[52px]"> <div className="mt-3 border border-dashed border-border rounded-xl p-4 flex gap-2 flex-wrap min-h-[52px]">
{selectedUsers.length === 0 ? ( {selectedUsers.length === 0 ? (
@@ -155,7 +176,7 @@ const CreateProjectSidebar: FC<Props> = ({
) : ( ) : (
selectedUsers.map((user) => ( selectedUsers.map((user) => (
<div key={user.id} className="bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg"> <div key={user.id} className="bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg">
<UserAvatar src={user.profilePic} alt={user.name} /> <UserAvatar src={user.profilePic} alt={user.name} showTooltip={false} />
<div>{user.name}</div> <div>{user.name}</div>
<CloseCircle variant="Bold" className="size-4 cursor-pointer" color="red" onClick={() => handleRemoveUser(user.id)} /> <CloseCircle variant="Bold" className="size-4 cursor-pointer" color="red" onClick={() => handleRemoveUser(user.id)} />
</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 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>
); );
@@ -26,6 +26,14 @@ export const useGetProject = (id: string) => {
}); });
}; };
export const useGetProjectUsers = (id: string, enabled = true) => {
return useQuery({
queryKey: ["project-users", id],
queryFn: () => api.getProjectUsers(id),
enabled: !!id && enabled,
});
};
export const useCreateProject = () => { export const useCreateProject = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -49,6 +57,9 @@ export const useUpdateProject = () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["project-detail", variables.id], queryKey: ["project-detail", variables.id],
}); });
queryClient.invalidateQueries({
queryKey: ["project-users", variables.id],
});
}, },
}); });
}; };
@@ -1,5 +1,5 @@
import axios from "../../../../config/axios"; import axios from "../../../../config/axios";
import { CreateProjectType, GetProjectResponse, UpdateProjectType } from "../types/ProjectTypes"; import { CreateProjectType, GetProjectResponse, GetProjectUsersResponse, UpdateProjectType } from "../types/ProjectTypes";
export const getProjectsByWorkspace = async (workspaceId: string) => { export const getProjectsByWorkspace = async (workspaceId: string) => {
const { data } = await axios.get(`/task-manager/projects/user/workspace/${workspaceId}`); const { data } = await axios.get(`/task-manager/projects/user/workspace/${workspaceId}`);
@@ -16,6 +16,11 @@ export const getProject = async (id: string): Promise<GetProjectResponse> => {
return data; return data;
}; };
export const getProjectUsers = async (id: string): Promise<GetProjectUsersResponse> => {
const { data } = await axios.get(`/task-manager/projects/${id}/users`);
return data;
};
export const createProject = async (params: CreateProjectType) => { export const createProject = async (params: CreateProjectType) => {
const { data } = await axios.post(`/task-manager/projects`, params); const { data } = await axios.post(`/task-manager/projects`, params);
return data; return data;
@@ -53,3 +53,7 @@ export type ProjectWithPhasesType = {
export interface GetProjectResponse extends IResponse<ProjectWithPhasesType> { export interface GetProjectResponse extends IResponse<ProjectWithPhasesType> {
statusCode: number; statusCode: number;
} }
export interface GetProjectUsersResponse extends IResponse<ProjectUserType[]> {
statusCode: number;
}
@@ -159,7 +159,13 @@ export const useUpdateCheckListItem = () => {
if (!current?.data?.checkListItems) return current; if (!current?.data?.checkListItems) return current;
const checkListItems = current.data.checkListItems.map((item) => const checkListItems = current.data.checkListItems.map((item) =>
item.id === id ? { ...item, isDone: params.isDone } : item, item.id === id
? {
...item,
...(params.isDone !== undefined ? { isDone: params.isDone } : {}),
...(params.title !== undefined ? { title: params.title } : {}),
}
: item,
); );
const completedCheckListItemCount = checkListItems.filter((item) => item.isDone).length; const completedCheckListItemCount = checkListItems.filter((item) => item.isDone).length;
@@ -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[]> {
@@ -83,7 +83,9 @@ export interface CreateCheckListItemResponse extends IResponse<{ checkListItem:
} }
export type UpdateCheckListItemType = { export type UpdateCheckListItemType = {
isDone: boolean; title?: string;
isDone?: boolean;
taskId?: string;
}; };
export interface UpdateCheckListItemResponse extends IResponse<{ checkListItem: TaskChecklistItemType }> { export interface UpdateCheckListItemResponse extends IResponse<{ checkListItem: TaskChecklistItemType }> {
@@ -4,7 +4,7 @@ import type { TaskItemType } from "../types/TaskTypes";
export const mapTaskItemToTask = (task: TaskItemType, taskPhaseId: string, fallbackOrder: number): Task => ({ export const mapTaskItemToTask = (task: TaskItemType, taskPhaseId: string, fallbackOrder: number): Task => ({
id: task.id, id: task.id,
columnId: task.taskPhaseId ?? taskPhaseId, columnId: task.taskPhaseId ?? taskPhaseId,
order: task.order ?? task.priority ?? fallbackOrder, order: task.priority ?? task.order ?? fallbackOrder,
title: task.title, title: task.title,
tag: task.tag ?? "", tag: task.tag ?? "",
dateRange: task.dateRange ?? "", dateRange: task.dateRange ?? "",
@@ -1,6 +1,5 @@
import type { TaskItemType } from "../task/types/TaskTypes"; import type { TaskItemType } from "../task/types/TaskTypes";
import { getTaskSortOrder } from "./getTaskSortOrder";
const getTaskSortOrder = (task: TaskItemType) => task.order ?? task.priority ?? 0;
export const getPriorityDestinationTaskId = ( export const getPriorityDestinationTaskId = (
items: TaskItemType[], items: TaskItemType[],
@@ -9,16 +8,19 @@ export const getPriorityDestinationTaskId = (
removeSource = true, removeSource = true,
): string | null => { ): string | null => {
const sorted = [...items].sort((a, b) => getTaskSortOrder(a) - getTaskSortOrder(b)); const sorted = [...items].sort((a, b) => getTaskSortOrder(a) - getTaskSortOrder(b));
const sourceIndex = sorted.findIndex((item) => item.id === sourceId);
if (removeSource && sourceIndex === destIndex) return null; if (!removeSource) {
if (sorted.length === 0) return null;
const list = removeSource ? sorted.filter((item) => item.id !== sourceId) : sorted; if (destIndex >= sorted.length) return sorted[sorted.length - 1].id;
if (list.length === 0) return null; return sorted[destIndex].id;
if (destIndex >= list.length) {
return list[list.length - 1].id;
} }
return list[destIndex].id; const sourceIndex = sorted.findIndex((item) => item.id === sourceId);
if (sourceIndex === -1 || sourceIndex === destIndex) return null;
// Dest is the task currently at the drop index (مقصد), not after removing source.
const destTask = sorted[destIndex];
if (!destTask || destTask.id === sourceId) return null;
return destTask.id;
}; };
@@ -0,0 +1,4 @@
import type { TaskItemType } from "../task/types/TaskTypes";
export const getTaskSortOrder = (task: Pick<TaskItemType, "priority" | "order">) =>
task.priority ?? task.order ?? 0;
@@ -1,6 +1,5 @@
import type { TaskItemType } from "../task/types/TaskTypes"; import type { TaskItemType } from "../task/types/TaskTypes";
import { getTaskSortOrder } from "./getTaskSortOrder";
const getTaskSortOrder = (task: TaskItemType) => task.order ?? task.priority ?? 0;
export const reorderTaskItems = ( export const reorderTaskItems = (
tasks: TaskItemType[], tasks: TaskItemType[],
+29 -21
View File
@@ -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>
@@ -3,7 +3,7 @@ import { CloseCircle } from "iconsax-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import SwitchComponent from "../../../../components/Switch"; import SwitchComponent from "../../../../components/Switch";
import Select from "../../../../components/Select"; import Select from "../../../../components/Select";
import { useGetUsers } from "../../../users/hooks/useUserData"; import { useGetAdminsListAll } from "../../../users/hooks/useUserData";
import { UserItemType } from "../../../users/types/UserTypes"; import { UserItemType } from "../../../users/types/UserTypes";
import WorkspaceColorPicker from "./WorkspaceColorPicker"; import WorkspaceColorPicker from "./WorkspaceColorPicker";
import UserAvatar from "../../components/UserAvatar"; import UserAvatar from "../../components/UserAvatar";
@@ -32,27 +32,25 @@ const CreateWorkspaceSidebar: FC<Props> = ({
onSelectedUsersChange, onSelectedUsersChange,
}) => { }) => {
const { t } = useTranslation("global"); const { t } = useTranslation("global");
const getUsers = useGetUsers(""); const getAdmins = useGetAdminsListAll();
const [userSelectValue, setUserSelectValue] = useState(""); const [userSelectValue, setUserSelectValue] = useState("");
const userItems = const admins: UserItemType[] = getAdmins.data?.data ?? [];
getUsers.data?.data?.users
?.filter( const userItems = admins
(user: UserItemType) => .filter(
!selectedUsers.some((selected) => selected.id === user.id), (user) => !selectedUsers.some((selected) => selected.id === user.id),
) )
.map((user: UserItemType) => ({ .map((user) => ({
label: `${user.firstName} ${user.lastName}`, label: `${user.firstName} ${user.lastName}`,
value: user.id, value: user.id,
})) ?? []; }));
const handleUserSelect = (e: React.ChangeEvent<HTMLSelectElement>) => { const handleUserSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value; const value = e.target.value;
if (!value) return; if (!value) return;
const user = getUsers.data?.data?.users?.find( const user = admins.find((item) => item.id === value);
(item: UserItemType) => item.id === value,
);
if (user) { if (user) {
onSelectedUsersChange([ onSelectedUsersChange([
@@ -113,7 +111,7 @@ const CreateWorkspaceSidebar: FC<Props> = ({
key={user.id} key={user.id}
className="bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg" className="bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg"
> >
<UserAvatar src={user.profilePic} alt={user.name} /> <UserAvatar src={user.profilePic} alt={user.name} showTooltip={false} />
<div>{user.name}</div> <div>{user.name}</div>
<CloseCircle <CloseCircle
variant="Bold" variant="Bold"
@@ -20,6 +20,14 @@ export const useGetWorkspaceDetail = (id: string) => {
}); });
}; };
export const useGetWorkspaceUsers = (id: string) => {
return useQuery({
queryKey: ["workspace-users", id],
queryFn: () => api.getWorkspaceUsers(id),
enabled: !!id,
});
};
export const useCreateWorkspace = () => { export const useCreateWorkspace = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -14,6 +14,11 @@ export const getWorkspaceDetail = async (id: string) => {
return data; return data;
}; };
export const getWorkspaceUsers = async (id: string) => {
const { data } = await axios.get(`/task-manager/workspaces/${id}/users`);
return data;
};
export const createWorkspace = async (params: CreateWorkspaceType) => { export const createWorkspace = async (params: CreateWorkspaceType) => {
const { data } = await axios.post(`/task-manager/workspaces`, params); const { data } = await axios.post(`/task-manager/workspaces`, params);
return data; return data;
+4
View File
@@ -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>
) )
+62 -64
View File
@@ -1,75 +1,73 @@
import { FC, useState } from 'react' import { FC, useState } from "react";
import { useGetUsers } from '../../users/hooks/useUserData' import { useTranslation } from "react-i18next";
import { useTranslation } from 'react-i18next' import { useParams } from "react-router-dom";
import { UserItemType } from '../../users/types/UserTypes' import Button from "../../../components/Button";
import CheckBoxComponent from '../../../components/CheckBoxComponent' import CheckBoxComponent from "../../../components/CheckBoxComponent";
import { useParams } from 'react-router-dom' import { toast } from "../../../components/Toast";
import { useReferTicket } from '../hooks/useTicketData' import { ErrorType } from "../../../helpers/types";
import Button from '../../../components/Button' import { useGetAdminsListAll } from "../../users/hooks/useUserData";
import { toast } from '../../../components/Toast'; import { UserItemType } from "../../users/types/UserTypes";
import { ErrorType } from '../../../helpers/types' import { useReferTicket } from "../hooks/useTicketData";
type Props = { type Props = {
refetch: () => void refetch: () => void;
} };
const ReferTicket: FC<Props> = ({ refetch }) => { const ReferTicket: FC<Props> = ({ refetch }) => {
const { t } = useTranslation("global");
const { id } = useParams();
const [userId, setUserId] = useState<string>("");
const getUsers = useGetAdminsListAll();
const referTicket = useReferTicket();
const { t } = useTranslation('global') const handleReferTicket = () => {
const { id } = useParams() referTicket.mutate(
const [userId, setUserId] = useState<string>('') { id: id as string, userId },
const getUsers = useGetUsers('', 'tickets') {
const referTicket = useReferTicket() onSuccess: () => {
refetch();
toast(t("ticket.refer_ticket_success"), "success");
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0], "error");
},
},
);
};
const handleReferTicket = () => { return (
referTicket.mutate({ id: id as string, userId }, { <div className={"bg-white p-6 rounded-3xl mt-8"}>
onSuccess: () => { <div className="flex justify-between">
refetch() <div className="text-sm">{t("ticket.refer_ticket")}</div>
toast(t('ticket.refer_ticket_success'), 'success') </div>
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0], 'error')
}
})
}
return ( <div className="mt-4">
<div className={'bg-white p-6 rounded-3xl mt-8'}> {getUsers.data?.data?.map((item: UserItemType) => (
<div className='flex justify-between'> <div
<div className='text-sm'> key={item.id}
{t('ticket.refer_ticket')} className="flex gap-2 items-center"
</div> >
</div> <div>
<CheckBoxComponent
<div className="mt-4"> checked={userId === item.id}
{ onChange={(e) => setUserId(e.target.checked ? item.id : "")}
getUsers.data?.data?.users?.map((item: UserItemType) => ( />
<div key={item.id} className='flex gap-2 items-center'>
<div>
<CheckBoxComponent
checked={userId === item.id}
onChange={(e) => setUserId(e.target.checked ? item.id : '')}
/>
</div>
<div className='text-xs leading-5'>
{item.firstName + ' ' + item.lastName}
</div>
</div>
))
}
<div className='flex justify-end mt-4'>
<Button
label={t('ticket.refer_ticket')}
onClick={handleReferTicket}
isLoading={referTicket.isPending}
className='w-fit px-7'
/>
</div>
</div> </div>
<div className="text-xs leading-5">{item.firstName + " " + item.lastName}</div>
</div>
))}
<div className="flex justify-end mt-4">
<Button
label={t("ticket.refer_ticket")}
onClick={handleReferTicket}
isLoading={referTicket.isPending}
className="w-fit px-7"
/>
</div> </div>
) </div>
} </div>
);
};
export default ReferTicket export default ReferTicket;
+51
View File
@@ -4,6 +4,7 @@ import {
AddMessageTicketType, AddMessageTicketType,
CreateCategoryType, CreateCategoryType,
CreateTicketAdminType, CreateTicketAdminType,
CreateTicketMasterType,
} from "../types/TicketTypes"; } from "../types/TicketTypes";
export const useGetTickets = ( export const useGetTickets = (
@@ -78,3 +79,53 @@ export const useCreateTicket = () => {
api.createTicket(variables), api.createTicket(variables),
}); });
}; };
export const useGetMasters = () => {
return useQuery({
queryKey: ["ticket-masters"],
queryFn: () => api.getMasters(),
});
};
export const useGetMasterById = (id: string) => {
return useQuery({
queryKey: ["ticket-master", id],
queryFn: () => api.getMasterById(id),
enabled: !!id,
});
};
export const useCreateMaster = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (variables: CreateTicketMasterType) =>
api.createMaster(variables),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["ticket-masters"] });
},
});
};
export const useUpdateMaster = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (variables: { id: string; params: CreateTicketMasterType }) =>
api.updateMaster(variables.id, variables.params),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["ticket-masters"] });
},
});
};
export const useDeleteMaster = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => api.deleteMaster(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["ticket-masters"] });
},
});
};
+72
View File
@@ -0,0 +1,72 @@
import { useFormik } from "formik";
import { FC } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import * as Yup from "yup";
import Button from "../../../components/Button";
import Select from "../../../components/Select";
import { toast } from "../../../components/Toast";
import { Pages } from "../../../config/Pages";
import { ErrorType } from "../../../helpers/types";
import { useGetAdminsListAll } from "../../users/hooks/useUserData";
import { UserItemType } from "../../users/types/UserTypes";
import { useCreateMaster } from "../hooks/useTicketData";
import { CreateTicketMasterType } from "../types/TicketTypes";
const CreateTicketMaster: FC = () => {
const { t } = useTranslation("global");
const navigate = useNavigate();
const getAdmins = useGetAdminsListAll();
const createMaster = useCreateMaster();
const formik = useFormik<CreateTicketMasterType>({
initialValues: {
userId: "",
},
validationSchema: Yup.object({
userId: Yup.string().required(t("errors.required")),
}),
onSubmit: (values) => {
createMaster.mutate(values, {
onSuccess: () => {
toast(t("success"), "success");
navigate(Pages.ticket.masters);
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0], "error");
},
});
},
});
return (
<div className="mt-4">
<div>{t("ticket.add_master")}</div>
<div className="bg-white mt-8 py-8 xl:px-10 px-4 rounded-3xl max-w-xl">
<Select
label={t("ticket.user")}
placeholder={t("select")}
className="border"
items={getAdmins.data?.data?.map((item: UserItemType) => ({
label: `${item.firstName} ${item.lastName}`,
value: item.id,
}))}
{...formik.getFieldProps("userId")}
error_text={formik.touched.userId && formik.errors.userId ? formik.errors.userId : ""}
/>
<div className="mt-8 flex justify-end">
<Button
label={t("submit")}
className="w-fit px-7"
onClick={() => formik.handleSubmit()}
isLoading={createMaster.isPending}
/>
</div>
</div>
</div>
);
};
export default CreateTicketMaster;
+100
View File
@@ -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;
+89
View File
@@ -0,0 +1,89 @@
import { useFormik } from "formik";
import { FC, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
import * as Yup from "yup";
import Button from "../../../components/Button";
import PageLoading from "../../../components/PageLoading";
import Select from "../../../components/Select";
import { toast } from "../../../components/Toast";
import { Pages } from "../../../config/Pages";
import { ErrorType } from "../../../helpers/types";
import { useGetAdminsListAll } from "../../users/hooks/useUserData";
import { UserItemType } from "../../users/types/UserTypes";
import { useGetMasterById, useUpdateMaster } from "../hooks/useTicketData";
import { CreateTicketMasterType } from "../types/TicketTypes";
const UpdateTicketMaster: FC = () => {
const { t } = useTranslation("global");
const { id } = useParams();
const navigate = useNavigate();
const getAdmins = useGetAdminsListAll();
const getMaster = useGetMasterById(id ?? "");
const updateMaster = useUpdateMaster();
const formik = useFormik<CreateTicketMasterType>({
initialValues: {
userId: "",
},
validationSchema: Yup.object({
userId: Yup.string().required(t("errors.required")),
}),
onSubmit: (values) => {
updateMaster.mutate(
{ id: id as string, params: values },
{
onSuccess: () => {
toast(t("success"), "success");
navigate(Pages.ticket.masters);
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0], "error");
},
},
);
},
});
useEffect(() => {
const master = getMaster.data?.data?.master ?? getMaster.data?.data?.ticketMaster;
if (master?.userId) {
formik.setFieldValue("userId", master.userId);
}
}, [getMaster.data]);
if (getMaster.isPending) {
return <PageLoading />;
}
return (
<div className="mt-4">
<div>{t("ticket.edit_master")}</div>
<div className="bg-white mt-8 py-8 xl:px-10 px-4 rounded-3xl max-w-xl">
<Select
label={t("ticket.user")}
placeholder={t("select")}
className="border"
items={getAdmins.data?.data?.map((item: UserItemType) => ({
label: `${item.firstName} ${item.lastName}`,
value: item.id,
}))}
{...formik.getFieldProps("userId")}
error_text={formik.touched.userId && formik.errors.userId ? formik.errors.userId : ""}
/>
<div className="mt-8 flex justify-end">
<Button
label={t("submit")}
className="w-fit px-7"
onClick={() => formik.handleSubmit()}
isLoading={updateMaster.isPending}
/>
</div>
</div>
</div>
);
};
export default UpdateTicketMaster;
+26
View File
@@ -3,6 +3,7 @@ import {
AddMessageTicketType, AddMessageTicketType,
CreateCategoryType, CreateCategoryType,
CreateTicketAdminType, CreateTicketAdminType,
CreateTicketMasterType,
} from "../types/TicketTypes"; } from "../types/TicketTypes";
export const getTickets = async ( export const getTickets = async (
@@ -63,3 +64,28 @@ export const createTicket = async (params: CreateTicketAdminType) => {
const { data } = await axios.post(`/tickets/admin`, params); const { data } = await axios.post(`/tickets/admin`, params);
return data; return data;
}; };
export const getMasters = async () => {
const { data } = await axios.get(`/tickets/masters`);
return data;
};
export const getMasterById = async (id: string) => {
const { data } = await axios.get(`/tickets/masters/${id}`);
return data;
};
export const createMaster = async (params: CreateTicketMasterType) => {
const { data } = await axios.post(`/tickets/masters`, params);
return data;
};
export const updateMaster = async (id: string, params: CreateTicketMasterType) => {
const { data } = await axios.patch(`/tickets/masters/${id}`, params);
return data;
};
export const deleteMaster = async (id: string) => {
const { data } = await axios.delete(`/tickets/masters/${id}`);
return data;
};
+18
View File
@@ -89,3 +89,21 @@ export type CreateTicketAdminType = {
attachmentUrls?: string[]; attachmentUrls?: string[];
userId: string; userId: string;
}; };
export type CreateTicketMasterType = {
userId: string;
};
export type TicketMasterItemType = {
id: string;
userId: string;
user?: {
id: string;
firstName: string;
lastName: string;
email?: string;
phone?: string;
};
createdAt?: string;
updatedAt?: string;
};
+18 -14
View File
@@ -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
View File
@@ -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={''} />
+23 -17
View File
@@ -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>
) )
+7
View File
@@ -54,6 +54,13 @@ export const useGetUsers = (search: string, permission?: string, page = 1) => {
}); });
}; };
export const useGetAdminsListAll = () => {
return useQuery({
queryKey: ["admins-list-all"],
queryFn: () => api.getAdminsListAll(),
});
};
export const useGetUsersInfinite = (search: string, permission?: string, enabled = true) => { export const useGetUsersInfinite = (search: string, permission?: string, enabled = true) => {
return useInfiniteQuery({ return useInfiniteQuery({
queryKey: ["users-infinite", search, permission], queryKey: ["users-infinite", search, permission],
+5
View File
@@ -42,6 +42,11 @@ export const getUsers = async (search: string, permission?: string, page = 1) =>
return data; return data;
}; };
export const getAdminsListAll = async () => {
const { data } = await axios.get(`/users/admins-list/all`);
return data;
};
export const getUserDetail = async (id: string) => { export const getUserDetail = async (id: string) => {
const { data } = await axios.get(`/users/admins/${id}`); const { data } = await axios.get(`/users/admins/${id}`);
return data; return data;
+15
View File
@@ -89,6 +89,9 @@ import TicketCategory from "../pages/ticket/Category";
import CreateTicket from "../pages/ticket/CreateTicket"; import CreateTicket from "../pages/ticket/CreateTicket";
import TicketDetail from "../pages/ticket/Detail"; import TicketDetail from "../pages/ticket/Detail";
import TicketList from "../pages/ticket/TicketList"; import TicketList from "../pages/ticket/TicketList";
import CreateTicketMaster from "../pages/ticket/masters/Create";
import TicketMastersList from "../pages/ticket/masters/List";
import UpdateTicketMaster from "../pages/ticket/masters/Update";
import TransactionList from "../pages/transaction/List"; import TransactionList from "../pages/transaction/List";
import CreateUser from "../pages/users/Create"; import CreateUser from "../pages/users/Create";
import GroupCreate from "../pages/users/GroupCreate"; import GroupCreate from "../pages/users/GroupCreate";
@@ -169,6 +172,18 @@ const MainRouter: FC = () => {
path={Pages.ticket.category} path={Pages.ticket.category}
element={<TicketCategory />} element={<TicketCategory />}
/> />
<Route
path={Pages.ticket.masters}
element={<TicketMastersList />}
/>
<Route
path={Pages.ticket.mastersCreate}
element={<CreateTicketMaster />}
/>
<Route
path={Pages.ticket.mastersUpdate + ":id"}
element={<UpdateTicketMaster />}
/>
<Route <Route
path={Pages.ticket.detail + ":id"} path={Pages.ticket.detail + ":id"}
element={<TicketDetail />} element={<TicketDetail />}
+3 -3
View File
@@ -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>
+7 -1
View File
@@ -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>