Compare commits
7 Commits
a5ac1d6a24
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0369553c39 | |||
| 3676688849 | |||
| 7d4e2865bb | |||
| 8563508674 | |||
| 74fec3d2be | |||
| b9bc80acf1 | |||
| 2599eac261 |
@@ -0,0 +1,46 @@
|
|||||||
|
import { Archive } from 'iconsax-react'
|
||||||
|
import { type FC, useState } from 'react'
|
||||||
|
import ModalConfrim from './ModalConfrim'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onArchive: () => void
|
||||||
|
isloading?: boolean
|
||||||
|
colorIcon?: string
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ArchiveWithConfirm: FC<Props> = ({
|
||||||
|
onArchive,
|
||||||
|
isloading = false,
|
||||||
|
colorIcon,
|
||||||
|
title = 'آرشیو',
|
||||||
|
}) => {
|
||||||
|
const [isConfirm, setIsConfirm] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title={title}
|
||||||
|
onClick={() => setIsConfirm(true)}
|
||||||
|
className="inline-flex"
|
||||||
|
>
|
||||||
|
<Archive className="size-5" color={colorIcon || '#8C90A3'} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ModalConfrim
|
||||||
|
isOpen={isConfirm}
|
||||||
|
close={() => setIsConfirm(false)}
|
||||||
|
onConfrim={() => {
|
||||||
|
onArchive()
|
||||||
|
setIsConfirm(false)
|
||||||
|
}}
|
||||||
|
isloading={isloading}
|
||||||
|
title_header="آرشیو پیشفاکتور"
|
||||||
|
label="آیا از آرشیو کردن این پیشفاکتور مطمئن هستید؟"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ArchiveWithConfirm
|
||||||
@@ -8,8 +8,8 @@ export const Paths = {
|
|||||||
create: '/product/create',
|
create: '/product/create',
|
||||||
update: '/product/update/',
|
update: '/product/update/',
|
||||||
category: {
|
category: {
|
||||||
create: '/product/category/create',
|
|
||||||
list: '/product/category/list',
|
list: '/product/category/list',
|
||||||
|
children: '/product/category/list/',
|
||||||
update: '/product/category/update/',
|
update: '/product/category/update/',
|
||||||
},
|
},
|
||||||
attribute: {
|
attribute: {
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { Paths } from '@/config/Paths'
|
||||||
|
|
||||||
|
/** Permission names aligned with API PermissionEnum. */
|
||||||
|
export const Permissions = {
|
||||||
|
DASHBOARD: 'dashboard',
|
||||||
|
VIEW_ORDERS: 'view_orders',
|
||||||
|
VIEW_ASSIGNED_ORDERS: 'view_assigned_orders',
|
||||||
|
VIEW_INVOICES: 'view_invoices',
|
||||||
|
VIEW_REQUESTS: 'view_requests',
|
||||||
|
MANAGE_PAYMENTS: 'manage_payments',
|
||||||
|
VIEW_USERS: 'view_users',
|
||||||
|
VIEW_PRINT_FORM: 'view_print_form',
|
||||||
|
MANAGE_ADMINS: 'manage_admins',
|
||||||
|
MANAGE_ROLES: 'manage_roles',
|
||||||
|
MANAGE_PRODUCTS: 'manage_products',
|
||||||
|
MANAGE_TICKETS: 'manage_tickets',
|
||||||
|
MANAGE_ANNOUNCEMENTS: 'manage_announcements',
|
||||||
|
MANAGE_CRITICISMS: 'manage_criticisms',
|
||||||
|
MANAGE_LEARNINGS: 'manage_learnings',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type PermissionName = (typeof Permissions)[keyof typeof Permissions]
|
||||||
|
|
||||||
|
export const ORDER_VIEW_PERMISSIONS: PermissionName[] = [
|
||||||
|
Permissions.VIEW_ORDERS,
|
||||||
|
Permissions.VIEW_ASSIGNED_ORDERS,
|
||||||
|
]
|
||||||
|
|
||||||
|
type LandingRoute = {
|
||||||
|
permissions: PermissionName | PermissionName[]
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sidebar-ordered landing candidates when choosing a post-login / fallback path. */
|
||||||
|
const LANDING_ROUTES: LandingRoute[] = [
|
||||||
|
{ permissions: Permissions.DASHBOARD, path: Paths.home },
|
||||||
|
{ permissions: ORDER_VIEW_PERMISSIONS, path: Paths.order.list },
|
||||||
|
{ permissions: Permissions.VIEW_INVOICES, path: Paths.perfomaInvoice.list },
|
||||||
|
{ permissions: Permissions.VIEW_REQUESTS, path: Paths.requests.list },
|
||||||
|
{ permissions: Permissions.MANAGE_PAYMENTS, path: Paths.payments.list },
|
||||||
|
{ permissions: Permissions.VIEW_USERS, path: Paths.users.list },
|
||||||
|
{ permissions: Permissions.VIEW_PRINT_FORM, path: Paths.print.list },
|
||||||
|
{ permissions: Permissions.MANAGE_ADMINS, path: Paths.admin.list },
|
||||||
|
{ permissions: Permissions.MANAGE_PRODUCTS, path: Paths.product.list },
|
||||||
|
{ permissions: Permissions.MANAGE_TICKETS, path: Paths.tickets.list },
|
||||||
|
{ permissions: Permissions.MANAGE_ANNOUNCEMENTS, path: Paths.announcement.list },
|
||||||
|
{ permissions: Permissions.MANAGE_CRITICISMS, path: Paths.criticisms.list },
|
||||||
|
{ permissions: Permissions.MANAGE_LEARNINGS, path: Paths.learning.list },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const hasPermission = (
|
||||||
|
userPermissions: string[],
|
||||||
|
required: string | string[],
|
||||||
|
): boolean => {
|
||||||
|
const requiredList = Array.isArray(required) ? required : [required]
|
||||||
|
return requiredList.some((permission) => userPermissions.includes(permission))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** First permitted app path, or profile if none match. */
|
||||||
|
export const getDefaultAdminPath = (permissions: string[]): string => {
|
||||||
|
for (const route of LANDING_ROUTES) {
|
||||||
|
if (hasPermission(permissions, route.permissions)) {
|
||||||
|
return route.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Paths.profile
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { getToken, removeRefreshToken, removeToken } from "./func";
|
import { getToken, removeRefreshToken, removeToken } from "./func";
|
||||||
|
import { useAuthStore } from "@/pages/auth/store/AuthStore";
|
||||||
|
|
||||||
type SessionAuthStore = {
|
type SessionAuthStore = {
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
@@ -14,6 +15,7 @@ export const useSessionAuth = create<SessionAuthStore>((set) => ({
|
|||||||
removeToken();
|
removeToken();
|
||||||
removeRefreshToken();
|
removeRefreshToken();
|
||||||
window.isRefreshTokenExpired = false;
|
window.isRefreshTokenExpired = false;
|
||||||
|
useAuthStore.getState().reset();
|
||||||
set({ isAuthenticated: false });
|
set({ isAuthenticated: false });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const AdminList: FC = () => {
|
|||||||
<Button className="w-fit px-6">
|
<Button className="w-fit px-6">
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
<AddSquare size={18} color="black" />
|
<AddSquare size={18} color="black" />
|
||||||
<div className="text-[13px] font-light">ادمین جدید</div>
|
<div className="text-[13px] font-light">کاربر جدید</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import { useLoginWithOtp, useOtpVerify } from '../hooks/useAuthData'
|
|||||||
import { extractErrorMessage, setRefreshToken } from '../../../config/func'
|
import { extractErrorMessage, setRefreshToken } from '../../../config/func'
|
||||||
import { setToken } from '../../../config/func'
|
import { setToken } from '../../../config/func'
|
||||||
import { toast } from '@/shared/toast'
|
import { toast } from '@/shared/toast'
|
||||||
import { Paths } from '@/config/Paths'
|
|
||||||
import { useSessionAuth } from '@/config/sessionAuth'
|
import { useSessionAuth } from '@/config/sessionAuth'
|
||||||
import { appNavigate } from '@/config/navigation'
|
import { appNavigate } from '@/config/navigation'
|
||||||
|
import { getDefaultAdminPath } from '@/config/permissions'
|
||||||
|
import type { BaseResponse } from '@/shared/types/Types'
|
||||||
|
import type { OtpVerifyResponseType } from '../types/AuthTypes'
|
||||||
|
|
||||||
const LoginStep2: FC = () => {
|
const LoginStep2: FC = () => {
|
||||||
|
|
||||||
@@ -39,11 +41,12 @@ const LoginStep2: FC = () => {
|
|||||||
otp: values.otp
|
otp: values.otp
|
||||||
}
|
}
|
||||||
otpVerify.mutate(params, {
|
otpVerify.mutate(params, {
|
||||||
onSuccess(data) {
|
onSuccess(data: BaseResponse<OtpVerifyResponseType>) {
|
||||||
setToken(data?.data?.tokens?.accessToken?.token)
|
setToken(data?.data?.tokens?.accessToken?.token)
|
||||||
setRefreshToken(data?.data?.tokens?.refreshToken?.token)
|
setRefreshToken(data?.data?.tokens?.refreshToken?.token)
|
||||||
useSessionAuth.getState().setAuthenticated(true)
|
useSessionAuth.getState().setAuthenticated(true)
|
||||||
appNavigate(Paths.home, { replace: true })
|
const permissions = data?.data?.admin?.permissions ?? []
|
||||||
|
appNavigate(getDefaultAdminPath(permissions), { replace: true })
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
toast(extractErrorMessage(error), 'error')
|
toast(extractErrorMessage(error), 'error')
|
||||||
|
|||||||
@@ -1,21 +1,28 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { type AuthStoreType } from "../../auth/types/AuthTypes";
|
import { type AuthStoreType } from "../../auth/types/AuthTypes";
|
||||||
|
|
||||||
export const useAuthStore = create<AuthStoreType>((set) => ({
|
const initialState = {
|
||||||
phone: "",
|
phone: "",
|
||||||
|
email: "",
|
||||||
|
stepLogin: 1,
|
||||||
|
devOtpCode: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuthStore = create<AuthStoreType>((set) => ({
|
||||||
|
...initialState,
|
||||||
setPhone(value) {
|
setPhone(value) {
|
||||||
set({ phone: value });
|
set({ phone: value });
|
||||||
},
|
},
|
||||||
email: "",
|
|
||||||
setEmail(value) {
|
setEmail(value) {
|
||||||
set({ email: value });
|
set({ email: value });
|
||||||
},
|
},
|
||||||
stepLogin: 1,
|
|
||||||
setStepLogin(value) {
|
setStepLogin(value) {
|
||||||
set({ stepLogin: value });
|
set({ stepLogin: value });
|
||||||
},
|
},
|
||||||
devOtpCode: "",
|
|
||||||
setDevOtpCode(value) {
|
setDevOtpCode(value) {
|
||||||
set({ devOtpCode: value });
|
set({ devOtpCode: value });
|
||||||
},
|
},
|
||||||
|
reset() {
|
||||||
|
set({ ...initialState });
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type AuthStoreType = {
|
|||||||
/** TODO: remove before production — dev-only OTP from API response */
|
/** TODO: remove before production — dev-only OTP from API response */
|
||||||
devOtpCode: string;
|
devOtpCode: string;
|
||||||
setDevOtpCode: (value: string) => void;
|
setDevOtpCode: (value: string) => void;
|
||||||
|
reset: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LoginWithPasswordType = {
|
export type LoginWithPasswordType = {
|
||||||
@@ -32,6 +33,22 @@ export type OtpVerifyType = {
|
|||||||
otp: string;
|
otp: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AdminLoginResponseType = {
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
phone: string;
|
||||||
|
role: string;
|
||||||
|
permissions: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OtpVerifyResponseType = {
|
||||||
|
tokens: {
|
||||||
|
accessToken: { token: string };
|
||||||
|
refreshToken: { token: string };
|
||||||
|
};
|
||||||
|
admin: AdminLoginResponseType;
|
||||||
|
};
|
||||||
|
|
||||||
export type CheckHasAccountType = {
|
export type CheckHasAccountType = {
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { getChatSenderName } from '../type/Types'
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
content: string
|
content: string
|
||||||
attachments?: ChatAttachmentType[]
|
attachments?: ChatAttachmentType[] | null
|
||||||
senderName?: string
|
senderName?: string
|
||||||
senderLabel?: string
|
senderLabel?: string
|
||||||
createdAt?: string
|
createdAt?: string
|
||||||
@@ -29,7 +29,7 @@ type Props = {
|
|||||||
|
|
||||||
const ChatMessage: FC<Props> = ({
|
const ChatMessage: FC<Props> = ({
|
||||||
content,
|
content,
|
||||||
attachments = [],
|
attachments,
|
||||||
senderName,
|
senderName,
|
||||||
senderLabel = 'پشتیبان',
|
senderLabel = 'پشتیبان',
|
||||||
createdAt,
|
createdAt,
|
||||||
@@ -44,8 +44,9 @@ const ChatMessage: FC<Props> = ({
|
|||||||
onDelete,
|
onDelete,
|
||||||
isDeleting = false,
|
isDeleting = false,
|
||||||
}) => {
|
}) => {
|
||||||
const fileAttachments = attachments.filter((a) => a.type !== 'voice')
|
const safeAttachments = attachments ?? []
|
||||||
const voiceAttachments = attachments.filter((a) => a.type === 'voice')
|
const fileAttachments = safeAttachments.filter((a) => a.type !== 'voice')
|
||||||
|
const voiceAttachments = safeAttachments.filter((a) => a.type === 'voice')
|
||||||
|
|
||||||
const handleOpenLink = async (key: string) => {
|
const handleOpenLink = async (key: string) => {
|
||||||
const url = await getPresignedUrl(key)
|
const url = await getPresignedUrl(key)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export type ChatParentMessageType = {
|
|||||||
export type ChatMessageType = {
|
export type ChatMessageType = {
|
||||||
admin?: ChatParticipantType | null
|
admin?: ChatParticipantType | null
|
||||||
user?: ChatParticipantType | null
|
user?: ChatParticipantType | null
|
||||||
attachments: ChatAttachmentType[]
|
attachments?: ChatAttachmentType[] | null
|
||||||
content: string
|
content: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
id: string
|
id: string
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export type WeeklyOrdersPoint = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getDashboardCounts = async (): Promise<BaseResponse<DashboardCountsType>> => {
|
export const getDashboardCounts = async (): Promise<BaseResponse<DashboardCountsType>> => {
|
||||||
const { data } = await axios.get<BaseResponse<DashboardCountsType>>('/admin/dashboard/counts');
|
const { data } = await axios.get<BaseResponse<DashboardCountsType>>('/admin/menu/counts');
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const Stats: FC = () => {
|
|||||||
<StatCard
|
<StatCard
|
||||||
count={stats?.unconfirmedInvoicesCount}
|
count={stats?.unconfirmedInvoicesCount}
|
||||||
description={t('home.factureCount')}
|
description={t('home.factureCount')}
|
||||||
to={`${Paths.perfomaInvoice.list}?tab=${ProformaInvoiceStatusEnum.NOT_CONFIRMED}`}
|
to={`${Paths.perfomaInvoice.list}?tab=${ProformaInvoiceStatusEnum.PENDING}`}
|
||||||
icon={<ReceiptText
|
icon={<ReceiptText
|
||||||
size={27}
|
size={27}
|
||||||
color={COLORS.primary}
|
color={COLORS.primary}
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { type FC, type ReactNode } from 'react';
|
import { type FC, type ReactNode, useState } from 'react';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||||
import moment from 'moment-jalaali';
|
import moment from 'moment-jalaali';
|
||||||
import { Calendar, Edit, Profile2User, ReceiptText, WalletMoney } from 'iconsax-react';
|
import { Archive, Calendar, Edit, Profile2User, ReceiptText, WalletMoney } from 'iconsax-react';
|
||||||
import { Paths } from '@/config/Paths';
|
import { Paths } from '@/config/Paths';
|
||||||
import BackButton from '@/components/BackButton';
|
import BackButton from '@/components/BackButton';
|
||||||
import RefreshButton from '@/components/RefreshButton';
|
import RefreshButton from '@/components/RefreshButton';
|
||||||
import { useGetInvoiceDetail } from './hooks/useInvoiceData';
|
import ModalConfrim from '@/components/ModalConfrim';
|
||||||
|
import { useArchiveInvoice, useGetInvoiceDetail } from './hooks/useInvoiceData';
|
||||||
import InvoicePaymentsSection from './components/InvoicePaymentsSection';
|
import InvoicePaymentsSection from './components/InvoicePaymentsSection';
|
||||||
import { formatItemDiscountDisplay } from './utils/invoiceItem';
|
import { formatItemDiscountDisplay } from './utils/invoiceItem';
|
||||||
import type { InvoiceItemType } from './types/Types';
|
import type { InvoiceItemType } from './types/Types';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { extractErrorMessage } from '@/config/func';
|
||||||
|
|
||||||
const formatAmount = (amount: number) =>
|
const formatAmount = (amount: number) =>
|
||||||
Number(amount).toLocaleString('fa-IR') + ' ریال';
|
Number(amount).toLocaleString('fa-IR') + ' ریال';
|
||||||
@@ -110,9 +113,26 @@ const InvoiceItemTableRow: FC<{ item: InvoiceItemType }> = ({ item }) => {
|
|||||||
|
|
||||||
const DetailPerfomaInvoice: FC = () => {
|
const DetailPerfomaInvoice: FC = () => {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [isArchiveConfirmOpen, setIsArchiveConfirmOpen] = useState(false);
|
||||||
const { data: invoiceData, isLoading, refetch, isFetching, isError } = useGetInvoiceDetail(id);
|
const { data: invoiceData, isLoading, refetch, isFetching, isError } = useGetInvoiceDetail(id);
|
||||||
|
const { mutate: archiveInvoice, isPending: isArchiving } = useArchiveInvoice();
|
||||||
const invoice = invoiceData?.data;
|
const invoice = invoiceData?.data;
|
||||||
|
|
||||||
|
const handleArchive = () => {
|
||||||
|
if (!id) return;
|
||||||
|
archiveInvoice(id, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsArchiveConfirmOpen(false);
|
||||||
|
toast.success('پیشفاکتور با موفقیت آرشیو شد');
|
||||||
|
navigate(Paths.perfomaInvoice.list);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-5 flex min-h-[200px] items-center justify-center">
|
<div className="mt-5 flex min-h-[200px] items-center justify-center">
|
||||||
@@ -154,18 +174,44 @@ const DetailPerfomaInvoice: FC = () => {
|
|||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<RefreshButton onClick={() => refetch()} isLoading={isFetching} />
|
<RefreshButton onClick={() => refetch()} isLoading={isFetching} />
|
||||||
<Link to={Paths.perfomaInvoice.update + invoice.id}>
|
{invoice.status !== 'archived' && (
|
||||||
<button
|
<>
|
||||||
type="button"
|
<button
|
||||||
className="flex items-center gap-2 rounded-xl bg-primary px-5 py-2.5 text-sm font-medium hover:opacity-90"
|
type="button"
|
||||||
>
|
onClick={() => setIsArchiveConfirmOpen(true)}
|
||||||
<Edit size={18} color="black" />
|
className="flex items-center gap-2 rounded-xl border border-border px-5 py-2.5 text-sm font-medium text-[#4B5563] transition-colors hover:bg-gray-50"
|
||||||
ویرایش
|
>
|
||||||
</button>
|
<Archive size={18} color="#4B5563" />
|
||||||
</Link>
|
آرشیو
|
||||||
|
</button>
|
||||||
|
<Link to={Paths.perfomaInvoice.update + invoice.id}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-2 rounded-xl bg-primary px-5 py-2.5 text-sm font-medium hover:opacity-90"
|
||||||
|
>
|
||||||
|
<Edit size={18} color="black" />
|
||||||
|
ویرایش
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{invoice.status === 'archived' && (
|
||||||
|
<span className="inline-flex items-center rounded-full bg-gray-100 px-3 py-1.5 text-xs text-gray-700">
|
||||||
|
آرشیو شده
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ModalConfrim
|
||||||
|
isOpen={isArchiveConfirmOpen}
|
||||||
|
close={() => setIsArchiveConfirmOpen(false)}
|
||||||
|
onConfrim={handleArchive}
|
||||||
|
isloading={isArchiving}
|
||||||
|
title_header="آرشیو پیشفاکتور"
|
||||||
|
label="آیا از آرشیو کردن این پیشفاکتور مطمئن هستید؟"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-6 xl:grid-cols-[minmax(0,1fr)_320px]">
|
<div className="grid grid-cols-1 gap-6 xl:grid-cols-[minmax(0,1fr)_320px]">
|
||||||
<div className="rounded-3xl border border-border/70 bg-white p-6 shadow-sm">
|
<div className="rounded-3xl border border-border/70 bg-white p-6 shadow-sm">
|
||||||
<div className="font-medium text-[#111827]">اطلاعات پیش فاکتور</div>
|
<div className="font-medium text-[#111827]">اطلاعات پیش فاکتور</div>
|
||||||
|
|||||||
@@ -1,254 +1,379 @@
|
|||||||
import Tabs from '@/components/Tabs'
|
import Tabs from "@/components/Tabs";
|
||||||
import { useMemo, useState, type FC } from 'react'
|
import { useCallback, useMemo, useState, type FC } from "react";
|
||||||
import { ProformaInvoiceStatusEnum } from './enum/InvoiceEnum'
|
import {
|
||||||
import Filters from '@/components/Filters'
|
invoiceListTabs,
|
||||||
import RefreshButton from '@/components/RefreshButton'
|
invoiceListTabStatuses,
|
||||||
import Table from '@/components/Table'
|
ProformaInvoiceStatusEnum,
|
||||||
import { Eye, Add, Edit2 } from 'iconsax-react'
|
} from "./enum/InvoiceEnum";
|
||||||
import { Link, useSearchParams } from 'react-router-dom'
|
import Filters from "@/components/Filters";
|
||||||
import { Paths } from '@/config/Paths'
|
import type { FilterValues } from "@/components/Filters";
|
||||||
import moment from 'moment-jalaali'
|
import RefreshButton from "@/components/RefreshButton";
|
||||||
import type { FilterValues } from '@/components/Filters'
|
import Table from "@/components/Table";
|
||||||
import { useGetInvoice } from './hooks/useInvoiceData'
|
import { Eye, Add, Edit2, ArrowDown2, Filter } from "iconsax-react";
|
||||||
import type { InvoiceType } from './types/Types'
|
import { Link, useSearchParams } from "react-router-dom";
|
||||||
|
import { Paths } from "@/config/Paths";
|
||||||
|
import moment from "moment-jalaali";
|
||||||
|
import { useArchiveInvoice, useGetInvoice, useGetInvoiceTabCounts } from "./hooks/useInvoiceData";
|
||||||
|
import type { InvoiceType } from "./types/Types";
|
||||||
|
import ArchiveWithConfirm from "@/components/ArchiveWithConfirm";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { extractErrorMessage } from "@/config/func";
|
||||||
|
import { clx } from "@/helpers/utils";
|
||||||
|
|
||||||
const INVOICE_TABS: ProformaInvoiceStatusEnum[] = [
|
const INVOICE_TABS = invoiceListTabs.map((tab) => tab.value);
|
||||||
ProformaInvoiceStatusEnum.ALL,
|
|
||||||
ProformaInvoiceStatusEnum.NOT_CONFIRMED,
|
|
||||||
ProformaInvoiceStatusEnum.PARTIALLY_CONFIRMED,
|
|
||||||
ProformaInvoiceStatusEnum.CONFIRMED,
|
|
||||||
]
|
|
||||||
|
|
||||||
const isValidInvoiceTab = (value: string | null): value is ProformaInvoiceStatusEnum =>
|
const INVOICE_LIST_FILTER_FIELDS = [
|
||||||
value !== null && INVOICE_TABS.includes(value as ProformaInvoiceStatusEnum)
|
{
|
||||||
|
name: "search",
|
||||||
|
type: "input" as const,
|
||||||
|
placeholder: "جستجو (شماره سفارش / مشتری)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "from",
|
||||||
|
type: "date" as const,
|
||||||
|
placeholder: "از تاریخ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "to",
|
||||||
|
type: "date" as const,
|
||||||
|
placeholder: "تا تاریخ",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const isValidInvoiceTab = (
|
||||||
|
value: string | null,
|
||||||
|
): value is ProformaInvoiceStatusEnum =>
|
||||||
|
value !== null && INVOICE_TABS.includes(value as ProformaInvoiceStatusEnum);
|
||||||
|
|
||||||
|
const countActiveFilters = (filters: FilterValues) =>
|
||||||
|
[filters.search, filters.from, filters.to].filter(Boolean).length;
|
||||||
|
|
||||||
const ProformaInvoice: FC = () => {
|
const ProformaInvoice: FC = () => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const tabParam = searchParams.get('tab')
|
const tabParam = searchParams.get("tab");
|
||||||
const activeTab = isValidInvoiceTab(tabParam) ? tabParam : ProformaInvoiceStatusEnum.ALL
|
const activeTab = isValidInvoiceTab(tabParam)
|
||||||
const [page, setPage] = useState(1)
|
? tabParam
|
||||||
const [filters, setFilters] = useState<FilterValues>({})
|
: ProformaInvoiceStatusEnum.PENDING;
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [filters, setFilters] = useState<FilterValues>({});
|
||||||
|
const [isFiltersExpanded, setIsFiltersExpanded] = useState(false);
|
||||||
|
const [filtersKey, setFiltersKey] = useState(0);
|
||||||
|
|
||||||
const setActiveTab = (tab: ProformaInvoiceStatusEnum) => {
|
const statuses = invoiceListTabStatuses[activeTab];
|
||||||
setSearchParams(
|
const activeFilterCount = useMemo(
|
||||||
(current) => {
|
() => countActiveFilters(filters),
|
||||||
const next = new URLSearchParams(current)
|
[filters],
|
||||||
if (tab === ProformaInvoiceStatusEnum.ALL) {
|
);
|
||||||
next.delete('tab')
|
const hasActiveFilters = activeFilterCount > 0;
|
||||||
} else {
|
|
||||||
next.set('tab', tab)
|
|
||||||
}
|
|
||||||
return next
|
|
||||||
},
|
|
||||||
{ replace: true },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data, isLoading, refetch, isFetching } = useGetInvoice(page)
|
const setActiveTab = (tab: ProformaInvoiceStatusEnum) => {
|
||||||
|
setPage(1);
|
||||||
|
setSearchParams(
|
||||||
|
(current) => {
|
||||||
|
const next = new URLSearchParams(current);
|
||||||
|
next.set("tab", tab);
|
||||||
|
return next;
|
||||||
|
},
|
||||||
|
{ replace: true },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const meta = data?.meta
|
const handleFiltersChange = useCallback((next: FilterValues) => {
|
||||||
|
setPage(1);
|
||||||
|
setFilters(next);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const clearFilters = useCallback(() => {
|
||||||
const list = data?.data ?? []
|
setPage(1);
|
||||||
|
setFilters({ search: null, from: null, to: null });
|
||||||
|
setFiltersKey((key) => key + 1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (activeTab === ProformaInvoiceStatusEnum.CONFIRMED) {
|
const { data, isLoading, refetch, isFetching } = useGetInvoice({
|
||||||
return list.filter((o) => o.confirmStatus === 'confirmed')
|
page,
|
||||||
}
|
statuses,
|
||||||
if (activeTab === ProformaInvoiceStatusEnum.PARTIALLY_CONFIRMED) {
|
search: filters.search?.trim() || null,
|
||||||
return list.filter((o) => o.confirmStatus === 'partially_confirmed')
|
from: filters.from || null,
|
||||||
}
|
to: filters.to || null,
|
||||||
if (activeTab === ProformaInvoiceStatusEnum.NOT_CONFIRMED) {
|
});
|
||||||
return list.filter((o) => o.confirmStatus === 'pending')
|
const { data: tabCounts } = useGetInvoiceTabCounts({
|
||||||
}
|
search: filters.search?.trim() || null,
|
||||||
|
from: filters.from || null,
|
||||||
|
to: filters.to || null,
|
||||||
|
});
|
||||||
|
const { mutate: archiveInvoice, isPending: isArchiving } =
|
||||||
|
useArchiveInvoice();
|
||||||
|
|
||||||
let result = list
|
const tabCountByTab: Record<ProformaInvoiceStatusEnum, number> = useMemo(
|
||||||
|
() => ({
|
||||||
|
[ProformaInvoiceStatusEnum.PENDING]: tabCounts?.pending ?? 0,
|
||||||
|
[ProformaInvoiceStatusEnum.CONFIRMED]: tabCounts?.confirmed ?? 0,
|
||||||
|
[ProformaInvoiceStatusEnum.ARCHIVED]: tabCounts?.archived ?? 0,
|
||||||
|
}),
|
||||||
|
[tabCounts],
|
||||||
|
);
|
||||||
|
const tabsWithCount = useMemo(
|
||||||
|
() =>
|
||||||
|
invoiceListTabs.map((tab) => {
|
||||||
|
const total = tabCountByTab[tab.value] ?? 0;
|
||||||
|
return {
|
||||||
|
...tab,
|
||||||
|
label: total > 0 ? `${tab.label} (${total.toLocaleString("fa-IR")})` : tab.label,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[tabCountByTab],
|
||||||
|
);
|
||||||
|
|
||||||
const search = (filters.search ?? '').trim()
|
const meta = data?.meta;
|
||||||
if (search) {
|
|
||||||
const lower = search.toLowerCase()
|
|
||||||
result = result.filter(
|
|
||||||
(o) =>
|
|
||||||
String(o.invoiceNumber).includes(search) ||
|
|
||||||
o.user?.firstName?.toLowerCase().includes(lower) ||
|
|
||||||
o.user?.lastName?.toLowerCase().includes(lower) ||
|
|
||||||
o.user?.phone?.includes(search)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const dateFilter = filters.date
|
const handleArchive = useCallback(
|
||||||
if (dateFilter) {
|
(id: string) => {
|
||||||
const filterDate = moment(dateFilter, 'jYYYY/jMM/jDD').format('YYYY-MM-DD')
|
archiveInvoice(id, {
|
||||||
result = result.filter((o) => {
|
onSuccess: () => {
|
||||||
const d = o.createdAt
|
refetch();
|
||||||
return d && moment(d).format('YYYY-MM-DD') === filterDate
|
toast.success("پیشفاکتور با موفقیت آرشیو شد");
|
||||||
})
|
},
|
||||||
}
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[archiveInvoice, refetch],
|
||||||
|
);
|
||||||
|
|
||||||
return result
|
const getServiceLabel = (item: InvoiceType) => {
|
||||||
}, [data?.data, activeTab, filters.search, filters.date])
|
if (!item.items?.length) return "-";
|
||||||
|
if (item.items.length === 1) return item.items[0].product?.title ?? "-";
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
{item.items.map((line) => (
|
||||||
|
<span key={line.id}>
|
||||||
|
{line.product?.title ?? "-"} ×{" "}
|
||||||
|
{line.quantity.toLocaleString("fa-IR")}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const getPaymentStatus = (item: InvoiceType) => {
|
const getCustomerLabel = (item: InvoiceType) => {
|
||||||
const total = Number(item.total) || 0
|
const fullName =
|
||||||
const paid = Number(item.paidAmount) || 0
|
item.user?.firstName || item.user?.lastName
|
||||||
if (total <= 0) return '-'
|
? [item.user.firstName, item.user.lastName].filter(Boolean).join(" ")
|
||||||
if (paid >= total) return 'پرداخت شده'
|
: null;
|
||||||
if (paid > 0) return 'پرداخت جزئی'
|
const phone = item.user?.phone;
|
||||||
return 'پرداخت نشده'
|
|
||||||
}
|
|
||||||
|
|
||||||
const getServiceLabel = (item: InvoiceType) => {
|
if (!fullName && !phone) return "-";
|
||||||
if (!item.items?.length) return '-'
|
|
||||||
if (item.items.length === 1) return item.items[0].product?.title ?? '-'
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col gap-0.5'>
|
|
||||||
{item.items.map((line) => (
|
|
||||||
<span key={line.id}>
|
|
||||||
{line.product?.title ?? '-'} ({line.quantity.toLocaleString('fa-IR')})
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-5'>
|
<div className="flex flex-col gap-0.5">
|
||||||
<div className='flex justify-between items-center'>
|
{fullName && <span>{fullName}</span>}
|
||||||
<h1 className='text-lg font-light'>پیش فاکتورها</h1>
|
{phone && <span className="text-xs text-[#8C90A3]">{phone}</span>}
|
||||||
<div className='flex items-center gap-3'>
|
</div>
|
||||||
<RefreshButton onClick={() => refetch()} isLoading={isFetching} />
|
);
|
||||||
<Link to={Paths.perfomaInvoice.create}>
|
};
|
||||||
<button className='flex items-center gap-2 px-4 py-2 bg-primary text-black text-sm font-medium rounded-xl hover:opacity-90 transition-opacity'>
|
|
||||||
<Add size={18} />
|
|
||||||
پیش فاکتور جدید
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-8'>
|
const formatAmount = (value: number | null | undefined) =>
|
||||||
<Tabs
|
value != null ? Number(value).toLocaleString("fa-IR") + " ریال" : "-";
|
||||||
items={[
|
|
||||||
{ label: 'همه', value: ProformaInvoiceStatusEnum.ALL },
|
|
||||||
{ label: 'تایید نشده', value: ProformaInvoiceStatusEnum.NOT_CONFIRMED },
|
|
||||||
{ label: 'تایید جزئی', value: ProformaInvoiceStatusEnum.PARTIALLY_CONFIRMED },
|
|
||||||
{ label: 'تایید شده', value: ProformaInvoiceStatusEnum.CONFIRMED },
|
|
||||||
]}
|
|
||||||
activeTab={activeTab}
|
|
||||||
onTabChange={(tab) => setActiveTab(tab as ProformaInvoiceStatusEnum)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-8'>
|
return (
|
||||||
<Filters
|
<div className="mt-5 space-y-6">
|
||||||
fields={[
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
{
|
<div>
|
||||||
type: 'input',
|
<h1 className="text-lg font-light">پیش فاکتورها</h1>
|
||||||
name: 'search',
|
|
||||||
placeholder: 'جستجو (شماره سفارش / مشتری)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'date',
|
|
||||||
name: 'date',
|
|
||||||
placeholder: 'تاریخ',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onChange={setFilters}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-8'>
|
|
||||||
<Table
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
key: 'invoiceNumber',
|
|
||||||
title: 'شماره',
|
|
||||||
render: (item) => item.invoiceNumber,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'service',
|
|
||||||
title: 'خدمت',
|
|
||||||
className: '!whitespace-normal',
|
|
||||||
render: (item) => getServiceLabel(item),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'issueDate',
|
|
||||||
title: 'تاریخ صدور',
|
|
||||||
render: (item) =>
|
|
||||||
item.createdAt
|
|
||||||
? moment(item.createdAt).format('jYYYY/jMM/jDD')
|
|
||||||
: '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'lastConfirmDate',
|
|
||||||
title: 'آخرین مهلت تایید',
|
|
||||||
render: (item) =>
|
|
||||||
item.approvalDeadline
|
|
||||||
? moment(item.approvalDeadline).format('jYYYY/jMM/jDD')
|
|
||||||
: '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'total',
|
|
||||||
title: 'مجموع',
|
|
||||||
render: (item) =>
|
|
||||||
item.total
|
|
||||||
? Number(item.total).toLocaleString('fa-IR') + ' ریال'
|
|
||||||
: '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'confirmStatus',
|
|
||||||
title: 'وضعیت تایید',
|
|
||||||
render: (item) => {
|
|
||||||
const statusMap = {
|
|
||||||
confirmed: { className: 'bg-green-100 text-green-800', text: 'تایید شده' },
|
|
||||||
partially_confirmed: { className: 'bg-blue-100 text-blue-800', text: 'تایید جزئی' },
|
|
||||||
pending: { className: 'bg-amber-100 text-amber-800', text: 'تایید نشده' },
|
|
||||||
}
|
|
||||||
const status = statusMap[item.confirmStatus] ?? statusMap.pending
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={`inline-flex h-6 items-center rounded-full px-2.5 text-xs ${status.className}`}
|
|
||||||
>
|
|
||||||
{status.text}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'paymentStatus',
|
|
||||||
title: 'وضعیت پرداخت',
|
|
||||||
render: (item) => getPaymentStatus(item),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'actions',
|
|
||||||
title: '',
|
|
||||||
render: (item) => (
|
|
||||||
<div className='flex gap-2 items-center'>
|
|
||||||
<Link to={Paths.perfomaInvoice.detail + item.id}>
|
|
||||||
<Eye size={20} color='#8C90A3' />
|
|
||||||
</Link>
|
|
||||||
<Link to={Paths.perfomaInvoice.update + item.id}>
|
|
||||||
<Edit2 size={20} color='#0037FF' />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
data={filteredData}
|
|
||||||
isLoading={isLoading}
|
|
||||||
noDataMessage='پیش فاکتوری یافت نشد.'
|
|
||||||
pagination={
|
|
||||||
meta && meta.totalPages > 1
|
|
||||||
? {
|
|
||||||
currentPage: meta.page,
|
|
||||||
totalPages: meta.totalPages,
|
|
||||||
onPageChange: setPage,
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
<div className="flex items-center gap-3">
|
||||||
}
|
<RefreshButton onClick={() => refetch()} isLoading={isFetching} />
|
||||||
|
<Link to={Paths.perfomaInvoice.create}>
|
||||||
|
<button className="flex items-center gap-2 px-4 py-2 bg-primary text-black text-sm font-medium rounded-xl hover:opacity-90 transition-opacity">
|
||||||
|
<Add size={18} />
|
||||||
|
پیش فاکتور جدید
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
export default ProformaInvoice
|
<Tabs
|
||||||
|
items={tabsWithCount}
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={(tab) => setActiveTab(tab as ProformaInvoiceStatusEnum)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="overflow-hidden rounded-3xl bg-white">
|
||||||
|
<div className="flex items-center justify-between gap-3 p-5 md:px-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsFiltersExpanded((prev) => !prev)}
|
||||||
|
className="flex flex-1 items-center gap-2 text-right"
|
||||||
|
>
|
||||||
|
<Filter size={18} color="#8C90A3" />
|
||||||
|
<span className="text-sm font-medium text-gray-700">فیلترها</span>
|
||||||
|
{hasActiveFilters && (
|
||||||
|
<span className="inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-xs font-medium text-black">
|
||||||
|
{activeFilterCount.toLocaleString("fa-IR")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<ArrowDown2
|
||||||
|
size={18}
|
||||||
|
color="#8C90A3"
|
||||||
|
className={clx(
|
||||||
|
"mr-auto transition-transform duration-200",
|
||||||
|
isFiltersExpanded && "rotate-180",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{hasActiveFilters && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={clearFilters}
|
||||||
|
className="shrink-0 text-xs text-[#0037FF] hover:underline"
|
||||||
|
>
|
||||||
|
پاک کردن فیلترها
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isFiltersExpanded && (
|
||||||
|
<div className="space-y-5 border-t border-gray-100 px-5 pb-5 pt-4 md:px-6 md:pb-6">
|
||||||
|
<Filters
|
||||||
|
key={filtersKey}
|
||||||
|
fields={INVOICE_LIST_FILTER_FIELDS}
|
||||||
|
initialValues={filters}
|
||||||
|
onChange={handleFiltersChange}
|
||||||
|
className="w-full"
|
||||||
|
fieldClassName="grid w-full grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
key: "invoiceNumber",
|
||||||
|
title: "شماره",
|
||||||
|
render: (item) => item.invoiceNumber,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "customer",
|
||||||
|
title: "مشتری",
|
||||||
|
className: "!whitespace-normal",
|
||||||
|
render: (item) => getCustomerLabel(item),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "service",
|
||||||
|
title: "آیتم ها",
|
||||||
|
className: "!whitespace-normal",
|
||||||
|
render: (item) => getServiceLabel(item),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "issueDate",
|
||||||
|
title: "تاریخ صدور",
|
||||||
|
render: (item) =>
|
||||||
|
item.createdAt
|
||||||
|
? moment(item.createdAt).format("jYYYY/jMM/jDD")
|
||||||
|
: "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "lastConfirmDate",
|
||||||
|
title: "آخرین مهلت تایید",
|
||||||
|
render: (item) =>
|
||||||
|
item.approvalDeadline
|
||||||
|
? moment(item.approvalDeadline).format("jYYYY/jMM/jDD")
|
||||||
|
: "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "total",
|
||||||
|
title: "مالی",
|
||||||
|
className: "!whitespace-normal",
|
||||||
|
render: (item) => (
|
||||||
|
<table className="w-full">
|
||||||
|
<tr>
|
||||||
|
<td className="text-xs text-[#8C90A3]">مجموع : </td>
|
||||||
|
<td className="text-left">{formatAmount(item.total)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="text-xs text-[#8C90A3]">پرداخت : </td>
|
||||||
|
<td className="text-left">{formatAmount(item.paidAmount)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="text-xs text-[#8C90A3]">مانده: </td>
|
||||||
|
<td className="text-left">{formatAmount(item.balance)}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "status",
|
||||||
|
title: "وضعیت",
|
||||||
|
render: (item) => {
|
||||||
|
const statusMap = {
|
||||||
|
confirmed: {
|
||||||
|
className: "bg-green-100 text-green-800",
|
||||||
|
text: "تایید شده",
|
||||||
|
},
|
||||||
|
partially_confirmed: {
|
||||||
|
className: "bg-blue-100 text-blue-800",
|
||||||
|
text: "تایید جزئی",
|
||||||
|
},
|
||||||
|
pending: {
|
||||||
|
className: "bg-amber-100 text-amber-800",
|
||||||
|
text: "در انتظار تایید",
|
||||||
|
},
|
||||||
|
archived: {
|
||||||
|
className: "bg-gray-100 text-gray-700",
|
||||||
|
text: "آرشیو شده",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const status = statusMap[item.status] ?? statusMap.pending;
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`inline-flex h-6 items-center rounded-full px-2.5 text-xs ${status.className}`}
|
||||||
|
>
|
||||||
|
{status.text}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "actions",
|
||||||
|
title: "",
|
||||||
|
render: (item) => (
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<Link to={Paths.perfomaInvoice.detail + item.id}>
|
||||||
|
<Eye size={20} color="#8C90A3" />
|
||||||
|
</Link>
|
||||||
|
{item.status !== "archived" && (
|
||||||
|
<>
|
||||||
|
<Link to={Paths.perfomaInvoice.update + item.id}>
|
||||||
|
<Edit2 size={20} color="#0037FF" />
|
||||||
|
</Link>
|
||||||
|
<ArchiveWithConfirm
|
||||||
|
onArchive={() => handleArchive(item.id)}
|
||||||
|
isloading={isArchiving}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
data={data?.data}
|
||||||
|
isLoading={isLoading}
|
||||||
|
noDataMessage="پیش فاکتوری یافت نشد."
|
||||||
|
pagination={
|
||||||
|
meta && meta.totalPages > 1
|
||||||
|
? {
|
||||||
|
currentPage: meta.page,
|
||||||
|
totalPages: meta.totalPages,
|
||||||
|
onPageChange: setPage,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProformaInvoice;
|
||||||
|
|||||||
@@ -1,6 +1,26 @@
|
|||||||
export const enum ProformaInvoiceStatusEnum {
|
export const enum ProformaInvoiceStatusEnum {
|
||||||
ALL = 'all',
|
PENDING = 'pending',
|
||||||
NOT_CONFIRMED = 'not_confirmed',
|
|
||||||
PARTIALLY_CONFIRMED = 'partially_confirmed',
|
|
||||||
CONFIRMED = 'confirmed',
|
CONFIRMED = 'confirmed',
|
||||||
}
|
ARCHIVED = 'archived',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InvoiceConfirmStatus =
|
||||||
|
| 'pending'
|
||||||
|
| 'partially_confirmed'
|
||||||
|
| 'confirmed'
|
||||||
|
| 'archived'
|
||||||
|
|
||||||
|
export const invoiceListTabStatuses: Record<
|
||||||
|
ProformaInvoiceStatusEnum,
|
||||||
|
InvoiceConfirmStatus[]
|
||||||
|
> = {
|
||||||
|
[ProformaInvoiceStatusEnum.PENDING]: ['pending'],
|
||||||
|
[ProformaInvoiceStatusEnum.CONFIRMED]: ['partially_confirmed', 'confirmed'],
|
||||||
|
[ProformaInvoiceStatusEnum.ARCHIVED]: ['archived'],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const invoiceListTabs = [
|
||||||
|
{ label: 'در انتظار تایید', value: ProformaInvoiceStatusEnum.PENDING },
|
||||||
|
{ label: 'تایید شده', value: ProformaInvoiceStatusEnum.CONFIRMED },
|
||||||
|
{ label: 'آرشیو شده', value: ProformaInvoiceStatusEnum.ARCHIVED },
|
||||||
|
] as const
|
||||||
|
|||||||
@@ -9,14 +9,32 @@ export const useCreateInvoice = () => {
|
|||||||
mutationFn: api.createInvoice,
|
mutationFn: api.createInvoice,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoice"] });
|
queryClient.invalidateQueries({ queryKey: ["invoice"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["invoice-tab-counts"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetInvoice = (page = 1) => {
|
export const useGetInvoice = (params: api.GetInvoicesParams) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["invoice", page],
|
queryKey: [
|
||||||
queryFn: () => api.getInvoice(page),
|
"invoice",
|
||||||
|
params.page,
|
||||||
|
params.statuses?.join(",") ?? "",
|
||||||
|
params.search ?? "",
|
||||||
|
params.from ?? "",
|
||||||
|
params.to ?? "",
|
||||||
|
],
|
||||||
|
queryFn: () => api.getInvoice(params),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetInvoiceTabCounts = (
|
||||||
|
params?: api.GetInvoiceTabCountsParams,
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["invoice-tab-counts", params],
|
||||||
|
queryFn: () => api.getInvoiceTabCounts(params),
|
||||||
|
refetchInterval: 20_000,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,6 +67,19 @@ export const useUpdateInvoice = () => {
|
|||||||
}) => api.updateInvoice(id, params),
|
}) => api.updateInvoice(id, params),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoice"] });
|
queryClient.invalidateQueries({ queryKey: ["invoice"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["invoice-tab-counts"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useArchiveInvoice = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => api.archiveInvoice(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["invoice"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["invoice-tab-counts"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,19 +6,63 @@ import type {
|
|||||||
InvoiceType,
|
InvoiceType,
|
||||||
UpdatePreInvoiceType,
|
UpdatePreInvoiceType,
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
|
import type { InvoiceConfirmStatus } from "../enum/InvoiceEnum";
|
||||||
|
import type { BaseResponse } from "@/shared/types/Types";
|
||||||
|
|
||||||
|
export type GetInvoicesParams = {
|
||||||
|
page?: number;
|
||||||
|
statuses?: InvoiceConfirmStatus[];
|
||||||
|
search?: string | null;
|
||||||
|
from?: string | null;
|
||||||
|
to?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetInvoiceTabCountsParams = Omit<GetInvoicesParams, "page" | "statuses">;
|
||||||
|
|
||||||
|
export type InvoiceTabCountsResponseType = {
|
||||||
|
pending: number;
|
||||||
|
confirmed: number;
|
||||||
|
archived: number;
|
||||||
|
};
|
||||||
|
|
||||||
export const createInvoice = async (params: CreatePreInvoiceType) => {
|
export const createInvoice = async (params: CreatePreInvoiceType) => {
|
||||||
const { data } = await axios.post("/admin/invoice", params);
|
const { data } = await axios.post("/admin/invoice", params);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getInvoice = async (page = 1): Promise<GetInvoiceResponseType> => {
|
export const getInvoice = async (
|
||||||
|
params: GetInvoicesParams = {},
|
||||||
|
): Promise<GetInvoiceResponseType> => {
|
||||||
|
const { page = 1, statuses, search, from, to } = params;
|
||||||
const { data } = await axios.get<GetInvoiceResponseType>("/admin/invoice", {
|
const { data } = await axios.get<GetInvoiceResponseType>("/admin/invoice", {
|
||||||
params: { page },
|
params: {
|
||||||
|
page,
|
||||||
|
...(statuses?.length ? { statuses: statuses.join(",") } : {}),
|
||||||
|
...(search ? { search } : {}),
|
||||||
|
...(from ? { from } : {}),
|
||||||
|
...(to ? { to } : {}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getInvoiceTabCounts = async (
|
||||||
|
params: GetInvoiceTabCountsParams = {},
|
||||||
|
): Promise<InvoiceTabCountsResponseType> => {
|
||||||
|
const { search, from, to } = params;
|
||||||
|
const { data } = await axios.get<BaseResponse<InvoiceTabCountsResponseType>>(
|
||||||
|
"/admin/invoice/tab-counts",
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
...(search ? { search } : {}),
|
||||||
|
...(from ? { from } : {}),
|
||||||
|
...(to ? { to } : {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
export const getInvoiceById = async (id: string) => {
|
export const getInvoiceById = async (id: string) => {
|
||||||
const { data } = await axios.get<{ data: InvoiceType }>(`/admin/invoice/${id}`);
|
const { data } = await axios.get<{ data: InvoiceType }>(`/admin/invoice/${id}`);
|
||||||
return data;
|
return data;
|
||||||
@@ -35,3 +79,8 @@ export const updateInvoice = async (id: string, params: UpdatePreInvoiceType) =>
|
|||||||
const { data } = await axios.patch(`/admin/invoice/${id}`, params);
|
const { data } = await axios.patch(`/admin/invoice/${id}`, params);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const archiveInvoice = async (id: string) => {
|
||||||
|
const { data } = await axios.patch(`/admin/invoice/${id}/archive`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export type InvoiceType = {
|
|||||||
total: number;
|
total: number;
|
||||||
paidAmount: number;
|
paidAmount: number;
|
||||||
balance: number;
|
balance: number;
|
||||||
confirmStatus: 'pending' | 'partially_confirmed' | 'confirmed';
|
status: 'pending' | 'partially_confirmed' | 'confirmed' | 'archived';
|
||||||
enableTax: boolean;
|
enableTax: boolean;
|
||||||
approvalDeadline: string;
|
approvalDeadline: string;
|
||||||
attachments: InvoiceAttachmentType[];
|
attachments: InvoiceAttachmentType[];
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { toast } from 'react-toastify';
|
|||||||
import type { ErrorType } from '@/helpers/types';
|
import type { ErrorType } from '@/helpers/types';
|
||||||
import { extractErrorMessage } from '@/config/func';
|
import { extractErrorMessage } from '@/config/func';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Paths } from '@/config/Paths'
|
||||||
|
import BackButton from '@/components/BackButton'
|
||||||
|
|
||||||
const CreateProduct: FC = () => {
|
const CreateProduct: FC = () => {
|
||||||
|
|
||||||
@@ -71,8 +73,10 @@ const CreateProduct: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex justify-between items-center'>
|
||||||
|
<div className='flex items-center gap-4'>
|
||||||
<h1 className='text-lg font-light'>محصول جدید</h1>
|
<BackButton to={Paths.product.list} />
|
||||||
|
<h1 className='text-lg font-light'>محصول جدید</h1>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
className='w-fit px-6'
|
className='w-fit px-6'
|
||||||
onClick={() => formik.handleSubmit()}
|
onClick={() => formik.handleSubmit()}
|
||||||
|
|||||||
+29
-23
@@ -1,7 +1,6 @@
|
|||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
import Filters, { type FilterValues } from '@/components/Filters'
|
import Filters, { type FilterValues } from '@/components/Filters'
|
||||||
import Table from '@/components/Table'
|
import Table from '@/components/Table'
|
||||||
import Tabs from '@/components/Tabs'
|
|
||||||
import { AddSquare, Edit, More2 } from 'iconsax-react'
|
import { AddSquare, Edit, More2 } from 'iconsax-react'
|
||||||
import { type FC, useMemo, useState } from 'react'
|
import { type FC, useMemo, useState } from 'react'
|
||||||
import { useDeleteProduct, useDuplicateProduct, useGetCategory, useGetProducts } from './hooks/useProductData'
|
import { useDeleteProduct, useDuplicateProduct, useGetCategory, useGetProducts } from './hooks/useProductData'
|
||||||
@@ -11,39 +10,44 @@ import TrashWithConfrim from '@/components/TrashWithConfrim'
|
|||||||
import CopyWithConfirm from '@/components/CopyWithConfirm'
|
import CopyWithConfirm from '@/components/CopyWithConfirm'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { extractErrorMessage } from '@/config/func'
|
import { extractErrorMessage } from '@/config/func'
|
||||||
|
import type { CategoryType } from './types/Types'
|
||||||
|
|
||||||
|
const flattenCategoryOptions = (categories: CategoryType[]) => {
|
||||||
|
const options: { label: string; value: string }[] = []
|
||||||
|
|
||||||
|
for (const category of categories) {
|
||||||
|
options.push({ label: category.title, value: category.id })
|
||||||
|
for (const child of category.children ?? []) {
|
||||||
|
options.push({ label: `— ${child.title}`, value: child.id })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
const ProductList: FC = () => {
|
const ProductList: FC = () => {
|
||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const [search, setSearch] = useState<string>('')
|
const [search, setSearch] = useState<string>('')
|
||||||
const [categoryId, setCategoryId] = useState<string>('')
|
const [categoryId, setCategoryId] = useState<string>('')
|
||||||
|
|
||||||
const { data: categoriesData } = useGetCategory({ limit: 1000 })
|
const { data: categoriesData } = useGetCategory({ limit: 1000, parentId: 'null' })
|
||||||
const { data: products, refetch } = useGetProducts({
|
const { data: products, refetch } = useGetProducts({
|
||||||
page,
|
page,
|
||||||
search: search || undefined,
|
search: search || undefined,
|
||||||
categoryId: categoryId || undefined,
|
categoryId: categoryId || undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const categoryTabs = useMemo(
|
const categoryOptions = useMemo(
|
||||||
() => [
|
() => flattenCategoryOptions((categoriesData?.data ?? []) as CategoryType[]),
|
||||||
{ label: 'همه', value: '' },
|
|
||||||
...(categoriesData?.data?.map((category) => ({
|
|
||||||
label: category.title,
|
|
||||||
value: category.id,
|
|
||||||
})) ?? []),
|
|
||||||
],
|
|
||||||
[categoriesData?.data],
|
[categoriesData?.data],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleFiltersChange = (filters: FilterValues) => {
|
const handleFiltersChange = (filters: FilterValues) => {
|
||||||
setSearch((filters.search as string) ?? '')
|
setSearch(filters.search ?? '')
|
||||||
|
setCategoryId(filters.categoryId ?? '')
|
||||||
setPage(1)
|
setPage(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCategoryTabChange = (tab: string) => {
|
|
||||||
setCategoryId(tab)
|
|
||||||
setPage(1)
|
|
||||||
}
|
|
||||||
const { mutate: deleteProduct, isPending: isDeleting } = useDeleteProduct()
|
const { mutate: deleteProduct, isPending: isDeleting } = useDeleteProduct()
|
||||||
const { mutate: duplicateProduct, isPending: isDuplicating } = useDuplicateProduct()
|
const { mutate: duplicateProduct, isPending: isDuplicating } = useDuplicateProduct()
|
||||||
const [duplicatingId, setDuplicatingId] = useState<string | null>(null)
|
const [duplicatingId, setDuplicatingId] = useState<string | null>(null)
|
||||||
@@ -98,14 +102,6 @@ const ProductList: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-8'>
|
|
||||||
<Tabs
|
|
||||||
items={categoryTabs}
|
|
||||||
activeTab={categoryId}
|
|
||||||
onTabChange={handleCategoryTabChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-8'>
|
<div className='mt-8'>
|
||||||
<Filters
|
<Filters
|
||||||
fields={[
|
fields={[
|
||||||
@@ -113,6 +109,12 @@ const ProductList: FC = () => {
|
|||||||
name: 'search',
|
name: 'search',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: 'جستجوی محصول'
|
placeholder: 'جستجوی محصول'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'categoryId',
|
||||||
|
type: 'select',
|
||||||
|
placeholder: 'دستهبندی',
|
||||||
|
options: categoryOptions,
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
onChange={handleFiltersChange}
|
onChange={handleFiltersChange}
|
||||||
@@ -152,6 +154,10 @@ const ProductList: FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'order',
|
||||||
|
title: 'ترتیب',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
title: '',
|
title: '',
|
||||||
|
|||||||
@@ -3,39 +3,45 @@ import { useFormik } from 'formik'
|
|||||||
import { useState, type FC } from 'react'
|
import { useState, type FC } from 'react'
|
||||||
import type { CreateCategoryType } from '../types/Types'
|
import type { CreateCategoryType } from '../types/Types'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import { useCreateCategory, useGetCategory } from '../hooks/useProductData'
|
import { useCreateCategory } from '../hooks/useProductData'
|
||||||
import Select from '@/components/Select'
|
|
||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
import { AddSquare } from 'iconsax-react'
|
import { AddSquare, TickCircle } from 'iconsax-react'
|
||||||
import UploadBox from '@/components/UploadBox'
|
import UploadBox from '@/components/UploadBox'
|
||||||
import SwitchComponent from '@/components/Switch'
|
import SwitchComponent from '@/components/Switch'
|
||||||
import { useSingleUpload } from '@/pages/uploader/hooks/useUploader'
|
import { useSingleUpload } from '@/pages/uploader/hooks/useUploader'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { extractErrorMessage } from '@/config/func'
|
import { extractErrorMessage } from '@/config/func'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import DefaulModal from '@/components/DefaulModal'
|
||||||
import { Paths } from '@/config/Paths'
|
|
||||||
import BackButton from '@/components/BackButton'
|
|
||||||
|
|
||||||
const ProductCategory: FC = () => {
|
type Props = {
|
||||||
|
parentId?: string
|
||||||
|
refetch: () => void
|
||||||
|
}
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const CreateCategory: FC<Props> = ({ parentId, refetch }) => {
|
||||||
const { data } = useGetCategory()
|
|
||||||
const { mutate, isPending } = useCreateCategory()
|
const { mutate, isPending } = useCreateCategory()
|
||||||
const { mutate: upload, isPending: isUploading } = useSingleUpload()
|
const { mutate: upload, isPending: isUploading } = useSingleUpload()
|
||||||
|
|
||||||
|
const [showModal, setShowModal] = useState(false)
|
||||||
const [file, setFile] = useState<File>()
|
const [file, setFile] = useState<File>()
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setShowModal(false)
|
||||||
|
setFile(undefined)
|
||||||
|
formik.resetForm()
|
||||||
|
}
|
||||||
|
|
||||||
const handleSave = (values: CreateCategoryType) => {
|
const handleSave = (values: CreateCategoryType) => {
|
||||||
const payload: CreateCategoryType = {
|
const payload: CreateCategoryType = {
|
||||||
...values,
|
...values,
|
||||||
parentId: values.parentId || undefined,
|
parentId: parentId || undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
mutate(payload, {
|
mutate(payload, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('با موفقیت ذخیره شد')
|
toast.success('با موفقیت ذخیره شد')
|
||||||
navigate(Paths.product.category.list)
|
refetch()
|
||||||
|
handleClose()
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(extractErrorMessage(error))
|
toast.error(extractErrorMessage(error))
|
||||||
@@ -69,55 +75,39 @@ const ProductCategory: FC = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-5'>
|
<div>
|
||||||
<div className='flex justify-between items-center'>
|
<Button
|
||||||
<div className='flex items-center gap-4'>
|
className='w-fit px-6'
|
||||||
<BackButton to={Paths.product.category.list} />
|
onClick={() => setShowModal(true)}
|
||||||
<h1 className='text-lg font-light'>دسته جدید</h1>
|
>
|
||||||
|
<div className='flex gap-1.5'>
|
||||||
|
<AddSquare size={18} color='black' />
|
||||||
|
<div className='text-[13px] font-light'>دستهبندی جدید</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
</Button>
|
||||||
className='w-fit px-6'
|
|
||||||
isLoading={isPending || isUploading}
|
|
||||||
onClick={() => formik.handleSubmit()}
|
|
||||||
>
|
|
||||||
<div className='flex gap-1.5'>
|
|
||||||
<AddSquare size={18} color='black' />
|
|
||||||
<div className='text-[13px] font-light'>ساخت دسته</div>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className='mt-8 bg-white p-6 rounded-3xl '>
|
|
||||||
|
|
||||||
<div>
|
<DefaulModal
|
||||||
|
open={showModal}
|
||||||
|
close={handleClose}
|
||||||
|
isHeader
|
||||||
|
title_header='دسته جدید'
|
||||||
|
>
|
||||||
|
<div className='mt-5'>
|
||||||
<UploadBox
|
<UploadBox
|
||||||
label='تصویر دسته (اختیاری)'
|
label='تصویر دسته (اختیاری)'
|
||||||
onChange={(files) => setFile(files[0])}
|
onChange={(files) => setFile(files[0])}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='rowTwoInput mt-6'>
|
<div className='mt-5'>
|
||||||
<Input
|
<Input
|
||||||
label='عنوان'
|
label='عنوان'
|
||||||
{...formik.getFieldProps('title')}
|
{...formik.getFieldProps('title')}
|
||||||
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
|
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
|
||||||
label='انتخاب والد درسته'
|
|
||||||
placeholder='اختیاری'
|
|
||||||
items={data?.data?.map((item) => {
|
|
||||||
return {
|
|
||||||
label: item.title,
|
|
||||||
value: item.id + ''
|
|
||||||
}
|
|
||||||
}) || []}
|
|
||||||
onChange={formik.handleChange}
|
|
||||||
name='parentId'
|
|
||||||
value={formik.values.parentId || ''}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-6'>
|
<div className='mt-5'>
|
||||||
<Input
|
<Input
|
||||||
label='ترتیب اولویت'
|
label='ترتیب اولویت'
|
||||||
{...formik.getFieldProps('order')}
|
{...formik.getFieldProps('order')}
|
||||||
@@ -125,15 +115,31 @@ const ProductCategory: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-6'>
|
<div className='mt-5'>
|
||||||
<SwitchComponent
|
<SwitchComponent
|
||||||
active={formik.values.isActive}
|
active={formik.values.isActive}
|
||||||
onChange={(value) => formik.setFieldValue('isActive', value)}
|
onChange={(value) => formik.setFieldValue('isActive', value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div className='mt-6 flex justify-end'>
|
||||||
|
<Button
|
||||||
|
className='w-fit px-6'
|
||||||
|
isLoading={isPending || isUploading}
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2 items-center'>
|
||||||
|
<TickCircle
|
||||||
|
size={18}
|
||||||
|
color='black'
|
||||||
|
/>
|
||||||
|
<div>ذخیره</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DefaulModal>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProductCategory
|
export default CreateCategory
|
||||||
|
|||||||
@@ -1,24 +1,34 @@
|
|||||||
import { type FC, useMemo } from 'react'
|
import { type FC, useMemo } from 'react'
|
||||||
import PresignedImage from '@/components/PresignedImage'
|
import PresignedImage from '@/components/PresignedImage'
|
||||||
import { useDeleteCategory, useGetCategory } from '../hooks/useProductData'
|
import { useDeleteCategory, useGetCategory, useGetCategoryDetail } from '../hooks/useProductData'
|
||||||
import Table from '@/components/Table'
|
import Table from '@/components/Table'
|
||||||
import type { ColumnType, RowDataType } from '@/components/types/TableTypes'
|
import type { ColumnType, RowDataType } from '@/components/types/TableTypes'
|
||||||
import type { CategoryType } from '../types/Types'
|
import type { CategoryType } from '../types/Types'
|
||||||
import Button from '@/components/Button'
|
import { ArrowLeft2, Edit } from 'iconsax-react'
|
||||||
import { AddSquare, Edit } from 'iconsax-react'
|
|
||||||
import TrashWithConfrim from '@/components/TrashWithConfrim'
|
import TrashWithConfrim from '@/components/TrashWithConfrim'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { extractErrorMessage } from '@/config/func'
|
import { extractErrorMessage } from '@/config/func'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
import { Paths } from '@/config/Paths'
|
import { Paths } from '@/config/Paths'
|
||||||
import BackButton from '@/components/BackButton'
|
import BackButton from '@/components/BackButton'
|
||||||
|
import CreateCategory from './Create'
|
||||||
|
|
||||||
const CategoryList: FC = () => {
|
const CategoryList: FC = () => {
|
||||||
|
const { parentId } = useParams<{ parentId?: string }>()
|
||||||
|
|
||||||
const { data, refetch } = useGetCategory()
|
const { data, refetch } = useGetCategory({
|
||||||
|
limit: 1000,
|
||||||
|
parentId: parentId ?? 'null',
|
||||||
|
})
|
||||||
|
const { data: parentDetail } = useGetCategoryDetail(parentId ?? '')
|
||||||
const { mutate, isPending } = useDeleteCategory()
|
const { mutate, isPending } = useDeleteCategory()
|
||||||
const categories = (data?.data ?? []) as (CategoryType & RowDataType)[]
|
const categories = (data?.data ?? []) as (CategoryType & RowDataType)[]
|
||||||
|
|
||||||
|
const parentCategory = parentDetail?.data
|
||||||
|
const backTo = parentCategory?.parent?.id
|
||||||
|
? Paths.product.category.children + parentCategory.parent.id
|
||||||
|
: Paths.product.category.list
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = (id: string) => {
|
||||||
mutate(id, {
|
mutate(id, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -39,19 +49,34 @@ const CategoryList: FC = () => {
|
|||||||
{
|
{
|
||||||
title: 'عنوان',
|
title: 'عنوان',
|
||||||
key: 'title',
|
key: 'title',
|
||||||
|
render: (item) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={Paths.product.category.children + item.id}
|
||||||
|
className='inline-flex items-center gap-1.5 text-sm text-black hover:opacity-70'
|
||||||
|
>
|
||||||
|
<span>{item.title}</span>
|
||||||
|
<ArrowLeft2 size={16} color='#8C90A3' />
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'والد',
|
title: 'زیر دستهها',
|
||||||
key: 'parent',
|
|
||||||
render: (item) => item.parent?.title || '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'تعداد زیر دسته',
|
|
||||||
key: 'children',
|
key: 'children',
|
||||||
render: (item) => <div>{item.children?.length || 0}</div>
|
render: (item) => {
|
||||||
|
if (!item.children?.length) return '0'
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col gap-0.5'>
|
||||||
|
{item.children.map((child) => (
|
||||||
|
<span key={child.id}>{child.title}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'اولویت',
|
title: 'ترتیب',
|
||||||
key: 'order',
|
key: 'order',
|
||||||
render: (item) => item.order ?? '-',
|
render: (item) => item.order ?? '-',
|
||||||
},
|
},
|
||||||
@@ -75,33 +100,30 @@ const CategoryList: FC = () => {
|
|||||||
}
|
}
|
||||||
], [isPending])
|
], [isPending])
|
||||||
|
|
||||||
|
const title = parentCategory
|
||||||
|
? `زیر دستههای «${parentCategory.title}»`
|
||||||
|
: 'دسته بندی محصولات'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex justify-between items-center'>
|
||||||
<div className='flex items-center gap-4'>
|
<div className='flex items-center gap-4'>
|
||||||
<BackButton to={Paths.product.list} />
|
<BackButton to={parentId ? backTo : Paths.product.list} />
|
||||||
<h1 className='text-lg font-light'>دسته بندی محصولات</h1>
|
<h1 className='text-lg font-light'>{title}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link to={Paths.product.category.create}>
|
<CreateCategory parentId={parentId} refetch={refetch} />
|
||||||
<Button className='w-fit px-6'>
|
|
||||||
<div className='flex gap-1.5'>
|
|
||||||
<AddSquare size={18} color='black' />
|
|
||||||
<div className='text-[13px] font-light'>دستهبندی جدید</div>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-8'>
|
<div className='mt-8'>
|
||||||
<Table<CategoryType & RowDataType>
|
<Table<CategoryType & RowDataType>
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={categories}
|
data={categories}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CategoryList
|
export default CategoryList
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { type FC, type SelectHTMLAttributes } from 'react'
|
import { type FC, type SelectHTMLAttributes, useMemo } from 'react'
|
||||||
import { useGetCategory } from '../hooks/useProductData'
|
import { useGetCategory } from '../hooks/useProductData'
|
||||||
import Select from '@/components/Select'
|
import Select from '@/components/Select'
|
||||||
|
import type { CategoryType } from '../types/Types'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
error_text?: string,
|
error_text?: string,
|
||||||
@@ -9,6 +10,19 @@ type Props = {
|
|||||||
clearable?: boolean,
|
clearable?: boolean,
|
||||||
} & SelectHTMLAttributes<HTMLSelectElement>
|
} & SelectHTMLAttributes<HTMLSelectElement>
|
||||||
|
|
||||||
|
const flattenCategoryOptions = (categories: CategoryType[]) => {
|
||||||
|
const options: { label: string; value: string }[] = []
|
||||||
|
|
||||||
|
for (const category of categories) {
|
||||||
|
options.push({ label: category.title, value: category.id })
|
||||||
|
for (const child of category.children ?? []) {
|
||||||
|
options.push({ label: `— ${child.title}`, value: child.id })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
const CategoriesSelect: FC<Props> = ({
|
const CategoriesSelect: FC<Props> = ({
|
||||||
isDisableShowLable,
|
isDisableShowLable,
|
||||||
placeholder,
|
placeholder,
|
||||||
@@ -16,8 +30,12 @@ const CategoriesSelect: FC<Props> = ({
|
|||||||
clearable = true,
|
clearable = true,
|
||||||
...selectProps
|
...selectProps
|
||||||
}) => {
|
}) => {
|
||||||
|
const { data: categories } = useGetCategory({ limit: 1000, parentId: 'null' })
|
||||||
|
|
||||||
const { data: categories } = useGetCategory()
|
const items = useMemo(
|
||||||
|
() => flattenCategoryOptions((categories?.data ?? []) as CategoryType[]),
|
||||||
|
[categories?.data],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
@@ -25,15 +43,10 @@ const CategoriesSelect: FC<Props> = ({
|
|||||||
placeholder={placeholder || 'انتخاب'}
|
placeholder={placeholder || 'انتخاب'}
|
||||||
error_text={error_text}
|
error_text={error_text}
|
||||||
clearable={clearable}
|
clearable={clearable}
|
||||||
items={categories?.data?.map((item) => {
|
items={items}
|
||||||
return {
|
|
||||||
label: item.title,
|
|
||||||
value: item.id + ''
|
|
||||||
}
|
|
||||||
}) || []}
|
|
||||||
{...selectProps}
|
{...selectProps}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CategoriesSelect
|
export default CategoriesSelect
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ export const useGetCategory = (params?: api.GetCategoryParams) => {
|
|||||||
const page = params?.page ?? 1;
|
const page = params?.page ?? 1;
|
||||||
const limit = params?.limit;
|
const limit = params?.limit;
|
||||||
const search = params?.search;
|
const search = params?.search;
|
||||||
|
const parentId = params?.parentId;
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["category", page, limit, search],
|
queryKey: ["category", page, limit, search, parentId],
|
||||||
queryFn: () => api.getCategory({ page, limit, search }),
|
queryFn: () => api.getCategory({ page, limit, search, parentId }),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export type GetCategoryParams = {
|
|||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
|
parentId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCategory = async (params?: GetCategoryParams) => {
|
export const getCategory = async (params?: GetCategoryParams) => {
|
||||||
@@ -13,6 +14,7 @@ export const getCategory = async (params?: GetCategoryParams) => {
|
|||||||
page: params?.page,
|
page: params?.page,
|
||||||
limit: params?.limit,
|
limit: params?.limit,
|
||||||
search: params?.search || undefined,
|
search: params?.search || undefined,
|
||||||
|
parentId: params?.parentId || undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
+70
-55
@@ -1,8 +1,13 @@
|
|||||||
import { type FC } from "react";
|
import { type FC, type ReactNode } from "react";
|
||||||
import { Route, Routes, useLocation } from "react-router-dom";
|
import { Route, Routes, useLocation } from "react-router-dom";
|
||||||
import Home from "../pages/home/Home";
|
import Home from "../pages/home/Home";
|
||||||
import AppLayout from "../shared/AppLayout";
|
import AppLayout from "../shared/AppLayout";
|
||||||
|
import PermissionGuard from "../shared/PermissionGuard";
|
||||||
import { Paths } from "@/config/Paths";
|
import { Paths } from "@/config/Paths";
|
||||||
|
import {
|
||||||
|
ORDER_VIEW_PERMISSIONS,
|
||||||
|
Permissions,
|
||||||
|
} from "@/config/permissions";
|
||||||
import ProformaInvoice from "@/pages/invoice/ProformaInvoice";
|
import ProformaInvoice from "@/pages/invoice/ProformaInvoice";
|
||||||
import CreateInvoice from "@/pages/invoice/Create";
|
import CreateInvoice from "@/pages/invoice/Create";
|
||||||
import RequestList from "@/pages/requests/RequestList";
|
import RequestList from "@/pages/requests/RequestList";
|
||||||
@@ -23,7 +28,6 @@ import LearningList from "@/pages/learning/List";
|
|||||||
import LearningCreate from "@/pages/learning/Create";
|
import LearningCreate from "@/pages/learning/Create";
|
||||||
import LearningCategory from "@/pages/learning/Category";
|
import LearningCategory from "@/pages/learning/Category";
|
||||||
import CategoryList from "@/pages/product/category/List";
|
import CategoryList from "@/pages/product/category/List";
|
||||||
import CreateCategory from "@/pages/product/category/Create";
|
|
||||||
import UpdateCategory from "@/pages/product/category/Update";
|
import UpdateCategory from "@/pages/product/category/Update";
|
||||||
import UpdateProduct from "@/pages/product/Update";
|
import UpdateProduct from "@/pages/product/Update";
|
||||||
import CreateAttribute from "@/pages/product/attribute/Create";
|
import CreateAttribute from "@/pages/product/attribute/Create";
|
||||||
@@ -59,6 +63,13 @@ import TicketList from "@/pages/ticket/TicketList";
|
|||||||
import TicketDetail from "@/pages/ticket/Detail";
|
import TicketDetail from "@/pages/ticket/Detail";
|
||||||
import PaymentsList from "@/pages/payment/List";
|
import PaymentsList from "@/pages/payment/List";
|
||||||
|
|
||||||
|
const withPermission = (
|
||||||
|
permission: string | string[],
|
||||||
|
element: ReactNode,
|
||||||
|
) => (
|
||||||
|
<PermissionGuard permission={permission}>{element}</PermissionGuard>
|
||||||
|
);
|
||||||
|
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const isPrintPreview = /\/order\/print\/[^/]+\/preview/.test(location.pathname)
|
const isPrintPreview = /\/order\/print\/[^/]+\/preview/.test(location.pathname)
|
||||||
@@ -74,74 +85,78 @@ const MainRouter: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={withPermission(Permissions.DASHBOARD, <Home />)} />
|
||||||
<Route path={Paths.home} element={<Home />} />
|
<Route path={Paths.home} element={withPermission(Permissions.DASHBOARD, <Home />)} />
|
||||||
<Route path={Paths.profile} element={<Profile />} />
|
<Route path={Paths.profile} element={<Profile />} />
|
||||||
<Route path={Paths.perfomaInvoice.list} element={<ProformaInvoice />} />
|
|
||||||
<Route path={Paths.payments.list} element={<PaymentsList />} />
|
|
||||||
<Route path={Paths.perfomaInvoice.create} element={<CreateInvoice />} />
|
|
||||||
<Route path={Paths.perfomaInvoice.detail + ":id"} element={<DetailPerfomaInvoice />} />
|
|
||||||
<Route path={Paths.perfomaInvoice.update + ":id"} element={<UpdateInvoice />} />
|
|
||||||
<Route path={Paths.requests.list} element={<RequestList />} />
|
|
||||||
<Route path={Paths.requests.detail + ":id"} element={<RequestDetail />} />
|
|
||||||
<Route path={Paths.product.list} element={<ProductList />} />
|
|
||||||
<Route path={Paths.product.create} element={<CreateProduct />} />
|
|
||||||
<Route path={Paths.product.update + ":id"} element={<UpdateProduct />} />
|
|
||||||
<Route path={Paths.product.attribute.create + ":id"} element={<CreateAttribute />} />
|
|
||||||
<Route path={Paths.product.attribute.list + ":id"} element={<AttributeList />} />
|
|
||||||
<Route path={Paths.product.attribute.update + ":id"} element={<UpdateAttribute />} />
|
|
||||||
<Route path={Paths.product.attributeValue.list + ":id"} element={<AttributeValues />} />
|
|
||||||
<Route path={Paths.product.category.list} element={<CategoryList />} />
|
|
||||||
<Route path={Paths.product.category.create} element={<CreateCategory />} />
|
|
||||||
<Route path={Paths.product.category.update + ":id"} element={<UpdateCategory />} />
|
|
||||||
|
|
||||||
<Route path={Paths.formBuilder.list + ":id/:type"} element={<FormBuilderList />} />
|
<Route path={Paths.perfomaInvoice.list} element={withPermission(Permissions.VIEW_INVOICES, <ProformaInvoice />)} />
|
||||||
<Route path={Paths.formBuilder.create + ":id/:type"} element={<FormBuilderCreate />} />
|
<Route path={Paths.perfomaInvoice.create} element={withPermission(Permissions.VIEW_INVOICES, <CreateInvoice />)} />
|
||||||
<Route path={Paths.formBuilder.update + ":id"} element={<FormBuilderUpdate />} />
|
<Route path={Paths.perfomaInvoice.detail + ":id"} element={withPermission(Permissions.VIEW_INVOICES, <DetailPerfomaInvoice />)} />
|
||||||
<Route path={Paths.formBuilder.values + ":id"} element={<FormBuilderValues />} />
|
<Route path={Paths.perfomaInvoice.update + ":id"} element={withPermission(Permissions.VIEW_INVOICES, <UpdateInvoice />)} />
|
||||||
|
|
||||||
<Route path={Paths.order.list} element={<OrdersList />} />
|
<Route path={Paths.payments.list} element={withPermission(Permissions.MANAGE_PAYMENTS, <PaymentsList />)} />
|
||||||
<Route path={Paths.order.details + ":id"} element={<OrderDetail />} />
|
|
||||||
<Route path={Paths.order.create} element={<NewOrder />} />
|
|
||||||
<Route path={Paths.order.edit + ":id"} element={<EditOrder />} />
|
|
||||||
<Route path={Paths.convertToOrder} element={<ConvertToOrders />} />
|
|
||||||
<Route path={Paths.orderPrint + ':id/preview'} element={<OrderPrintPreview />} />
|
|
||||||
<Route path={Paths.orderPrint + ':id'} element={<OrderPrint />} />
|
|
||||||
|
|
||||||
<Route path={Paths.tickets.list} element={<TicketList />} />
|
<Route path={Paths.requests.list} element={withPermission(Permissions.VIEW_REQUESTS, <RequestList />)} />
|
||||||
<Route path={Paths.tickets.detail + ":id"} element={<TicketDetail />} />
|
<Route path={Paths.requests.detail + ":id"} element={withPermission(Permissions.VIEW_REQUESTS, <RequestDetail />)} />
|
||||||
|
|
||||||
<Route path={Paths.print.list} element={<SectionList />} />
|
<Route path={Paths.product.list} element={withPermission(Permissions.MANAGE_PRODUCTS, <ProductList />)} />
|
||||||
<Route path={Paths.print.create} element={<CreateSection />} />
|
<Route path={Paths.product.create} element={withPermission(Permissions.MANAGE_PRODUCTS, <CreateProduct />)} />
|
||||||
<Route path={Paths.print.update + ":id"} element={<UpdateSection />} />
|
<Route path={Paths.product.update + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <UpdateProduct />)} />
|
||||||
<Route path={Paths.print.form + ":itemId/:orderId"} element={<PrintForm />} />
|
<Route path={Paths.product.attribute.create + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <CreateAttribute />)} />
|
||||||
|
<Route path={Paths.product.attribute.list + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <AttributeList />)} />
|
||||||
|
<Route path={Paths.product.attribute.update + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <UpdateAttribute />)} />
|
||||||
|
<Route path={Paths.product.attributeValue.list + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <AttributeValues />)} />
|
||||||
|
<Route path={Paths.product.category.list} element={withPermission(Permissions.MANAGE_PRODUCTS, <CategoryList />)} />
|
||||||
|
<Route path={Paths.product.category.children + ":parentId"} element={withPermission(Permissions.MANAGE_PRODUCTS, <CategoryList />)} />
|
||||||
|
<Route path={Paths.product.category.update + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <UpdateCategory />)} />
|
||||||
|
|
||||||
<Route path={Paths.users.list} element={<UsersList />} />
|
<Route path={Paths.formBuilder.list + ":id/:type"} element={withPermission(Permissions.MANAGE_PRODUCTS, <FormBuilderList />)} />
|
||||||
<Route path={Paths.users.create} element={<CreateUser />} />
|
<Route path={Paths.formBuilder.create + ":id/:type"} element={withPermission(Permissions.MANAGE_PRODUCTS, <FormBuilderCreate />)} />
|
||||||
<Route path={Paths.users.update + ":id"} element={<UpdateUser />} />
|
<Route path={Paths.formBuilder.update + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <FormBuilderUpdate />)} />
|
||||||
|
<Route path={Paths.formBuilder.values + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <FormBuilderValues />)} />
|
||||||
|
|
||||||
<Route path={Paths.admin.list} element={<AdminList />} />
|
<Route path={Paths.order.list} element={withPermission(ORDER_VIEW_PERMISSIONS, <OrdersList />)} />
|
||||||
<Route path={Paths.admin.create} element={<CreateAdmin />} />
|
<Route path={Paths.order.details + ":id"} element={withPermission(ORDER_VIEW_PERMISSIONS, <OrderDetail />)} />
|
||||||
<Route path={Paths.admin.update + ":id"} element={<EditAdmin />} />
|
<Route path={Paths.order.create} element={withPermission(ORDER_VIEW_PERMISSIONS, <NewOrder />)} />
|
||||||
|
<Route path={Paths.order.edit + ":id"} element={withPermission(ORDER_VIEW_PERMISSIONS, <EditOrder />)} />
|
||||||
|
<Route path={Paths.convertToOrder} element={withPermission(ORDER_VIEW_PERMISSIONS, <ConvertToOrders />)} />
|
||||||
|
<Route path={Paths.orderPrint + ':id/preview'} element={withPermission(ORDER_VIEW_PERMISSIONS, <OrderPrintPreview />)} />
|
||||||
|
<Route path={Paths.orderPrint + ':id'} element={withPermission(ORDER_VIEW_PERMISSIONS, <OrderPrint />)} />
|
||||||
|
|
||||||
<Route path={Paths.role.list} element={<RoleList />} />
|
<Route path={Paths.tickets.list} element={withPermission(Permissions.MANAGE_TICKETS, <TicketList />)} />
|
||||||
<Route path={Paths.role.create} element={<CreateRole />} />
|
<Route path={Paths.tickets.detail + ":id"} element={withPermission(Permissions.MANAGE_TICKETS, <TicketDetail />)} />
|
||||||
<Route path={Paths.role.update + ":id"} element={<UpdateRole />} />
|
|
||||||
|
<Route path={Paths.print.list} element={withPermission(Permissions.VIEW_PRINT_FORM, <SectionList />)} />
|
||||||
|
<Route path={Paths.print.create} element={withPermission(Permissions.VIEW_PRINT_FORM, <CreateSection />)} />
|
||||||
|
<Route path={Paths.print.update + ":id"} element={withPermission(Permissions.VIEW_PRINT_FORM, <UpdateSection />)} />
|
||||||
|
<Route path={Paths.print.form + ":itemId/:orderId"} element={withPermission(Permissions.VIEW_PRINT_FORM, <PrintForm />)} />
|
||||||
|
|
||||||
|
<Route path={Paths.users.list} element={withPermission(Permissions.VIEW_USERS, <UsersList />)} />
|
||||||
|
<Route path={Paths.users.create} element={withPermission(Permissions.VIEW_USERS, <CreateUser />)} />
|
||||||
|
<Route path={Paths.users.update + ":id"} element={withPermission(Permissions.VIEW_USERS, <UpdateUser />)} />
|
||||||
|
|
||||||
|
<Route path={Paths.admin.list} element={withPermission(Permissions.MANAGE_ADMINS, <AdminList />)} />
|
||||||
|
<Route path={Paths.admin.create} element={withPermission(Permissions.MANAGE_ADMINS, <CreateAdmin />)} />
|
||||||
|
<Route path={Paths.admin.update + ":id"} element={withPermission(Permissions.MANAGE_ADMINS, <EditAdmin />)} />
|
||||||
|
|
||||||
|
<Route path={Paths.role.list} element={withPermission(Permissions.MANAGE_ROLES, <RoleList />)} />
|
||||||
|
<Route path={Paths.role.create} element={withPermission(Permissions.MANAGE_ROLES, <CreateRole />)} />
|
||||||
|
<Route path={Paths.role.update + ":id"} element={withPermission(Permissions.MANAGE_ROLES, <UpdateRole />)} />
|
||||||
|
|
||||||
<Route path={Paths.features.list} element={<FeaturesList />} />
|
<Route path={Paths.features.list} element={<FeaturesList />} />
|
||||||
<Route path={Paths.features.create} element={<CreateFeature />} />
|
<Route path={Paths.features.create} element={<CreateFeature />} />
|
||||||
<Route path={Paths.service.print} element={<PrintService />} />
|
<Route path={Paths.service.print} element={<PrintService />} />
|
||||||
|
|
||||||
<Route path={Paths.announcement.list} element={<AnnouncementList />} />
|
<Route path={Paths.announcement.list} element={withPermission(Permissions.MANAGE_ANNOUNCEMENTS, <AnnouncementList />)} />
|
||||||
<Route path={Paths.announcement.detail + ":id"} element={<AnnouncementUpdate />} />
|
<Route path={Paths.announcement.detail + ":id"} element={withPermission(Permissions.MANAGE_ANNOUNCEMENTS, <AnnouncementUpdate />)} />
|
||||||
<Route path={Paths.announcement.create} element={<AnnouncementCreate />} />
|
<Route path={Paths.announcement.create} element={withPermission(Permissions.MANAGE_ANNOUNCEMENTS, <AnnouncementCreate />)} />
|
||||||
|
|
||||||
<Route path={Paths.criticisms.list} element={<CriticismsList />} />
|
<Route path={Paths.criticisms.list} element={withPermission(Permissions.MANAGE_CRITICISMS, <CriticismsList />)} />
|
||||||
<Route path={Paths.criticisms.detail + ":id"} element={<CriticismsDetail />} />
|
<Route path={Paths.criticisms.detail + ":id"} element={withPermission(Permissions.MANAGE_CRITICISMS, <CriticismsDetail />)} />
|
||||||
|
|
||||||
<Route path={Paths.learning.list} element={<LearningList />} />
|
<Route path={Paths.learning.list} element={withPermission(Permissions.MANAGE_LEARNINGS, <LearningList />)} />
|
||||||
<Route path={Paths.learning.create} element={<LearningCreate />} />
|
<Route path={Paths.learning.create} element={withPermission(Permissions.MANAGE_LEARNINGS, <LearningCreate />)} />
|
||||||
<Route path={Paths.learning.category} element={<LearningCategory />} />
|
<Route path={Paths.learning.category} element={withPermission(Permissions.MANAGE_LEARNINGS, <LearningCategory />)} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { type FC, type ReactNode } from 'react'
|
||||||
|
import { Navigate } from 'react-router-dom'
|
||||||
|
import { useGetAdminMe } from '@/pages/admin/hooks/useAdminData'
|
||||||
|
import {
|
||||||
|
getDefaultAdminPath,
|
||||||
|
hasPermission,
|
||||||
|
} from '@/config/permissions'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
permission: string | string[]
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocks route access when the current admin lacks the required permission(s).
|
||||||
|
* Pass an array to allow access if the user has any of them.
|
||||||
|
*/
|
||||||
|
const PermissionGuard: FC<Props> = ({ permission, children }) => {
|
||||||
|
const { data: adminMe, isLoading } = useGetAdminMe()
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissions =
|
||||||
|
adminMe?.data?.role?.permissions?.map((p) => p.name) ?? []
|
||||||
|
|
||||||
|
if (!hasPermission(permissions, permission)) {
|
||||||
|
return <Navigate to={getDefaultAdminPath(permissions)} replace />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PermissionGuard
|
||||||
@@ -66,7 +66,7 @@ const SideBar: FC = () => {
|
|||||||
title={t('sidebar.mainPage')}
|
title={t('sidebar.mainPage')}
|
||||||
isActive={isActive('/home')}
|
isActive={isActive('/home')}
|
||||||
link={Paths.home}
|
link={Paths.home}
|
||||||
activeName=''
|
activeName='dashboard'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SideBarItem
|
<SideBarItem
|
||||||
@@ -106,9 +106,6 @@ const SideBar: FC = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<SideBarItem
|
<SideBarItem
|
||||||
icon={<People size={iconSizeSideBar} color="#4F5260" />}
|
icon={<People size={iconSizeSideBar} color="#4F5260" />}
|
||||||
title={'مشتریان'}
|
title={'مشتریان'}
|
||||||
@@ -127,7 +124,7 @@ const SideBar: FC = () => {
|
|||||||
|
|
||||||
<SideBarItem
|
<SideBarItem
|
||||||
icon={<User size={iconSizeSideBar} color="#4F5260" />}
|
icon={<User size={iconSizeSideBar} color="#4F5260" />}
|
||||||
title={'مدیران'}
|
title={'کاربران'}
|
||||||
isActive={isActive('admin')}
|
isActive={isActive('admin')}
|
||||||
link={Paths.admin.list}
|
link={Paths.admin.list}
|
||||||
activeName='manage_admins'
|
activeName='manage_admins'
|
||||||
|
|||||||
Reference in New Issue
Block a user