Compare commits

..

3 Commits

Author SHA1 Message Date
morteza 74fec3d2be side bar
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-23 19:40:08 +03:30
morteza b9bc80acf1 count 2026-07-23 18:47:40 +03:30
morteza 2599eac261 invoice 2026-07-23 18:13:48 +03:30
13 changed files with 583 additions and 268 deletions
+46
View File
@@ -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
+1 -1
View File
@@ -37,7 +37,7 @@ const AdminList: FC = () => {
<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 className="text-[13px] font-light">کاربر جدید</div>
</div>
</Button>
</Link>
+5 -4
View File
@@ -11,7 +11,7 @@ import { getChatSenderName } from '../type/Types'
type Props = {
content: string
attachments?: ChatAttachmentType[]
attachments?: ChatAttachmentType[] | null
senderName?: string
senderLabel?: string
createdAt?: string
@@ -29,7 +29,7 @@ type Props = {
const ChatMessage: FC<Props> = ({
content,
attachments = [],
attachments,
senderName,
senderLabel = 'پشتیبان',
createdAt,
@@ -44,8 +44,9 @@ const ChatMessage: FC<Props> = ({
onDelete,
isDeleting = false,
}) => {
const fileAttachments = attachments.filter((a) => a.type !== 'voice')
const voiceAttachments = attachments.filter((a) => a.type === 'voice')
const safeAttachments = attachments ?? []
const fileAttachments = safeAttachments.filter((a) => a.type !== 'voice')
const voiceAttachments = safeAttachments.filter((a) => a.type === 'voice')
const handleOpenLink = async (key: string) => {
const url = await getPresignedUrl(key)
+1 -1
View File
@@ -29,7 +29,7 @@ export type ChatParentMessageType = {
export type ChatMessageType = {
admin?: ChatParticipantType | null
user?: ChatParticipantType | null
attachments: ChatAttachmentType[]
attachments?: ChatAttachmentType[] | null
content: string
createdAt: string
id: string
@@ -22,7 +22,7 @@ export type WeeklyOrdersPoint = {
};
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;
};
+1 -1
View File
@@ -33,7 +33,7 @@ const Stats: FC = () => {
<StatCard
count={stats?.unconfirmedInvoicesCount}
description={t('home.factureCount')}
to={`${Paths.perfomaInvoice.list}?tab=${ProformaInvoiceStatusEnum.NOT_CONFIRMED}`}
to={`${Paths.perfomaInvoice.list}?tab=${ProformaInvoiceStatusEnum.PENDING}`}
icon={<ReceiptText
size={27}
color={COLORS.primary}
+59 -13
View File
@@ -1,14 +1,17 @@
import { type FC, type ReactNode } from 'react';
import { Link, useParams } from 'react-router-dom';
import { type FC, type ReactNode, useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
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 BackButton from '@/components/BackButton';
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 { formatItemDiscountDisplay } from './utils/invoiceItem';
import type { InvoiceItemType } from './types/Types';
import { toast } from 'react-toastify';
import { extractErrorMessage } from '@/config/func';
const formatAmount = (amount: number) =>
Number(amount).toLocaleString('fa-IR') + ' ریال';
@@ -110,9 +113,26 @@ const InvoiceItemTableRow: FC<{ item: InvoiceItemType }> = ({ item }) => {
const DetailPerfomaInvoice: FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [isArchiveConfirmOpen, setIsArchiveConfirmOpen] = useState(false);
const { data: invoiceData, isLoading, refetch, isFetching, isError } = useGetInvoiceDetail(id);
const { mutate: archiveInvoice, isPending: isArchiving } = useArchiveInvoice();
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) {
return (
<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">
<RefreshButton onClick={() => refetch()} isLoading={isFetching} />
<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' && (
<>
<button
type="button"
onClick={() => setIsArchiveConfirmOpen(true)}
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"
>
<Archive size={18} color="#4B5563" />
آرشیو
</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>
<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="rounded-3xl border border-border/70 bg-white p-6 shadow-sm">
<div className="font-medium text-[#111827]">اطلاعات پیش فاکتور</div>
+358 -233
View File
@@ -1,254 +1,379 @@
import Tabs from '@/components/Tabs'
import { useMemo, useState, type FC } from 'react'
import { ProformaInvoiceStatusEnum } from './enum/InvoiceEnum'
import Filters from '@/components/Filters'
import RefreshButton from '@/components/RefreshButton'
import Table from '@/components/Table'
import { Eye, Add, Edit2 } from 'iconsax-react'
import { Link, useSearchParams } from 'react-router-dom'
import { Paths } from '@/config/Paths'
import moment from 'moment-jalaali'
import type { FilterValues } from '@/components/Filters'
import { useGetInvoice } from './hooks/useInvoiceData'
import type { InvoiceType } from './types/Types'
import Tabs from "@/components/Tabs";
import { useCallback, useMemo, useState, type FC } from "react";
import {
invoiceListTabs,
invoiceListTabStatuses,
ProformaInvoiceStatusEnum,
} from "./enum/InvoiceEnum";
import Filters from "@/components/Filters";
import type { FilterValues } from "@/components/Filters";
import RefreshButton from "@/components/RefreshButton";
import Table from "@/components/Table";
import { Eye, Add, Edit2, ArrowDown2, Filter } from "iconsax-react";
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[] = [
ProformaInvoiceStatusEnum.ALL,
ProformaInvoiceStatusEnum.NOT_CONFIRMED,
ProformaInvoiceStatusEnum.PARTIALLY_CONFIRMED,
ProformaInvoiceStatusEnum.CONFIRMED,
]
const INVOICE_TABS = invoiceListTabs.map((tab) => tab.value);
const isValidInvoiceTab = (value: string | null): value is ProformaInvoiceStatusEnum =>
value !== null && INVOICE_TABS.includes(value as ProformaInvoiceStatusEnum)
const INVOICE_LIST_FILTER_FIELDS = [
{
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 [searchParams, setSearchParams] = useSearchParams()
const tabParam = searchParams.get('tab')
const activeTab = isValidInvoiceTab(tabParam) ? tabParam : ProformaInvoiceStatusEnum.ALL
const [page, setPage] = useState(1)
const [filters, setFilters] = useState<FilterValues>({})
const [searchParams, setSearchParams] = useSearchParams();
const tabParam = searchParams.get("tab");
const activeTab = isValidInvoiceTab(tabParam)
? tabParam
: 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) => {
setSearchParams(
(current) => {
const next = new URLSearchParams(current)
if (tab === ProformaInvoiceStatusEnum.ALL) {
next.delete('tab')
} else {
next.set('tab', tab)
}
return next
},
{ replace: true },
)
}
const statuses = invoiceListTabStatuses[activeTab];
const activeFilterCount = useMemo(
() => countActiveFilters(filters),
[filters],
);
const hasActiveFilters = activeFilterCount > 0;
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 list = data?.data ?? []
const clearFilters = useCallback(() => {
setPage(1);
setFilters({ search: null, from: null, to: null });
setFiltersKey((key) => key + 1);
}, []);
if (activeTab === ProformaInvoiceStatusEnum.CONFIRMED) {
return list.filter((o) => o.confirmStatus === 'confirmed')
}
if (activeTab === ProformaInvoiceStatusEnum.PARTIALLY_CONFIRMED) {
return list.filter((o) => o.confirmStatus === 'partially_confirmed')
}
if (activeTab === ProformaInvoiceStatusEnum.NOT_CONFIRMED) {
return list.filter((o) => o.confirmStatus === 'pending')
}
const { data, isLoading, refetch, isFetching } = useGetInvoice({
page,
statuses,
search: filters.search?.trim() || null,
from: filters.from || null,
to: filters.to || null,
});
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()
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 meta = data?.meta;
const dateFilter = filters.date
if (dateFilter) {
const filterDate = moment(dateFilter, 'jYYYY/jMM/jDD').format('YYYY-MM-DD')
result = result.filter((o) => {
const d = o.createdAt
return d && moment(d).format('YYYY-MM-DD') === filterDate
})
}
const handleArchive = useCallback(
(id: string) => {
archiveInvoice(id, {
onSuccess: () => {
refetch();
toast.success("پیش‌فاکتور با موفقیت آرشیو شد");
},
onError: (error) => {
toast.error(extractErrorMessage(error));
},
});
},
[archiveInvoice, refetch],
);
return result
}, [data?.data, activeTab, filters.search, filters.date])
const getServiceLabel = (item: InvoiceType) => {
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 total = Number(item.total) || 0
const paid = Number(item.paidAmount) || 0
if (total <= 0) return '-'
if (paid >= total) return 'پرداخت شده'
if (paid > 0) return 'پرداخت جزئی'
return 'پرداخت نشده'
}
const getCustomerLabel = (item: InvoiceType) => {
const fullName =
item.user?.firstName || item.user?.lastName
? [item.user.firstName, item.user.lastName].filter(Boolean).join(" ")
: null;
const phone = item.user?.phone;
const getServiceLabel = (item: InvoiceType) => {
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>
)
}
if (!fullName && !phone) return "-";
return (
<div className='mt-5'>
<div className='flex justify-between items-center'>
<h1 className='text-lg font-light'>پیش فاکتورها</h1>
<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>
<div className="flex flex-col gap-0.5">
{fullName && <span>{fullName}</span>}
{phone && <span className="text-xs text-[#8C90A3]">{phone}</span>}
</div>
);
};
<div className='mt-8'>
<Tabs
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>
const formatAmount = (value: number | null | undefined) =>
value != null ? Number(value).toLocaleString("fa-IR") + " ریال" : "-";
<div className='mt-8'>
<Filters
fields={[
{
type: 'input',
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>
return (
<div className="mt-5 space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<h1 className="text-lg font-light">پیش فاکتورها</h1>
</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;
+23 -3
View File
@@ -1,6 +1,26 @@
export const enum ProformaInvoiceStatusEnum {
ALL = 'all',
NOT_CONFIRMED = 'not_confirmed',
PARTIALLY_CONFIRMED = 'partially_confirmed',
PENDING = 'pending',
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
+34 -3
View File
@@ -9,14 +9,32 @@ export const useCreateInvoice = () => {
mutationFn: api.createInvoice,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["invoice"] });
queryClient.invalidateQueries({ queryKey: ["invoice-tab-counts"] });
},
});
};
export const useGetInvoice = (page = 1) => {
export const useGetInvoice = (params: api.GetInvoicesParams) => {
return useQuery({
queryKey: ["invoice", page],
queryFn: () => api.getInvoice(page),
queryKey: [
"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),
onSuccess: () => {
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"] });
},
});
};
+51 -2
View File
@@ -6,19 +6,63 @@ import type {
InvoiceType,
UpdatePreInvoiceType,
} 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) => {
const { data } = await axios.post("/admin/invoice", params);
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", {
params: { page },
params: {
page,
...(statuses?.length ? { statuses: statuses.join(",") } : {}),
...(search ? { search } : {}),
...(from ? { from } : {}),
...(to ? { to } : {}),
},
});
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) => {
const { data } = await axios.get<{ data: InvoiceType }>(`/admin/invoice/${id}`);
return data;
@@ -35,3 +79,8 @@ export const updateInvoice = async (id: string, params: UpdatePreInvoiceType) =>
const { data } = await axios.patch(`/admin/invoice/${id}`, params);
return data;
};
export const archiveInvoice = async (id: string) => {
const { data } = await axios.patch(`/admin/invoice/${id}/archive`);
return data;
};
+1 -1
View File
@@ -104,7 +104,7 @@ export type InvoiceType = {
total: number;
paidAmount: number;
balance: number;
confirmStatus: 'pending' | 'partially_confirmed' | 'confirmed';
status: 'pending' | 'partially_confirmed' | 'confirmed' | 'archived';
enableTax: boolean;
approvalDeadline: string;
attachments: InvoiceAttachmentType[];
+1 -4
View File
@@ -106,9 +106,6 @@ const SideBar: FC = () => {
/>
<SideBarItem
icon={<People size={iconSizeSideBar} color="#4F5260" />}
title={'مشتریان'}
@@ -127,7 +124,7 @@ const SideBar: FC = () => {
<SideBarItem
icon={<User size={iconSizeSideBar} color="#4F5260" />}
title={'مدیران'}
title={'کاربران'}
isActive={isActive('admin')}
link={Paths.admin.list}
activeName='manage_admins'