count
This commit is contained in:
@@ -1,265 +1,379 @@
|
||||
import Tabs from '@/components/Tabs'
|
||||
import { useCallback, useMemo, useState, type FC } from 'react'
|
||||
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 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 { useArchiveInvoice, useGetInvoice } from './hooks/useInvoiceData'
|
||||
import type { InvoiceType } from './types/Types'
|
||||
import ArchiveWithConfirm from '@/components/ArchiveWithConfirm'
|
||||
import { toast } from 'react-toastify'
|
||||
import { extractErrorMessage } from '@/config/func'
|
||||
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 = invoiceListTabs.map((tab) => tab.value)
|
||||
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.PENDING
|
||||
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 statuses = invoiceListTabStatuses[activeTab]
|
||||
const statuses = invoiceListTabStatuses[activeTab];
|
||||
const activeFilterCount = useMemo(
|
||||
() => countActiveFilters(filters),
|
||||
[filters],
|
||||
);
|
||||
const hasActiveFilters = activeFilterCount > 0;
|
||||
|
||||
const setActiveTab = (tab: ProformaInvoiceStatusEnum) => {
|
||||
setPage(1)
|
||||
setSearchParams(
|
||||
(current) => {
|
||||
const next = new URLSearchParams(current)
|
||||
next.set('tab', tab)
|
||||
return next
|
||||
},
|
||||
{ replace: true },
|
||||
)
|
||||
}
|
||||
const setActiveTab = (tab: ProformaInvoiceStatusEnum) => {
|
||||
setPage(1);
|
||||
setSearchParams(
|
||||
(current) => {
|
||||
const next = new URLSearchParams(current);
|
||||
next.set("tab", tab);
|
||||
return next;
|
||||
},
|
||||
{ replace: true },
|
||||
);
|
||||
};
|
||||
|
||||
const { data, isLoading, refetch, isFetching } = useGetInvoice({ page, statuses })
|
||||
const { mutate: archiveInvoice, isPending: isArchiving } = useArchiveInvoice()
|
||||
const handleFiltersChange = useCallback((next: FilterValues) => {
|
||||
setPage(1);
|
||||
setFilters(next);
|
||||
}, []);
|
||||
|
||||
const meta = data?.meta
|
||||
const clearFilters = useCallback(() => {
|
||||
setPage(1);
|
||||
setFilters({ search: null, from: null, to: null });
|
||||
setFiltersKey((key) => key + 1);
|
||||
}, []);
|
||||
|
||||
const handleArchive = useCallback(
|
||||
(id: string) => {
|
||||
archiveInvoice(id, {
|
||||
onSuccess: () => {
|
||||
refetch()
|
||||
toast.success('پیشفاکتور با موفقیت آرشیو شد')
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(extractErrorMessage(error))
|
||||
},
|
||||
})
|
||||
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();
|
||||
|
||||
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 meta = data?.meta;
|
||||
|
||||
const handleArchive = useCallback(
|
||||
(id: string) => {
|
||||
archiveInvoice(id, {
|
||||
onSuccess: () => {
|
||||
refetch();
|
||||
toast.success("پیشفاکتور با موفقیت آرشیو شد");
|
||||
},
|
||||
[archiveInvoice, refetch],
|
||||
)
|
||||
onError: (error) => {
|
||||
toast.error(extractErrorMessage(error));
|
||||
},
|
||||
});
|
||||
},
|
||||
[archiveInvoice, refetch],
|
||||
);
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
let result = data?.data ?? []
|
||||
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 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 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 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
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}, [data?.data, filters.search, filters.date])
|
||||
|
||||
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 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={[...invoiceListTabs]}
|
||||
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: '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: '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>
|
||||
{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={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;
|
||||
|
||||
@@ -9,17 +9,35 @@ export const useCreateInvoice = () => {
|
||||
mutationFn: api.createInvoice,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["invoice"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["invoice-tab-counts"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetInvoice = (params: api.GetInvoicesParams) => {
|
||||
return useQuery({
|
||||
queryKey: ["invoice", params.page, params.statuses?.join(",") ?? ""],
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetInvoiceDetail = (id: string | undefined) => {
|
||||
return useQuery({
|
||||
queryKey: ["invoice", id],
|
||||
@@ -49,6 +67,7 @@ export const useUpdateInvoice = () => {
|
||||
}) => api.updateInvoice(id, params),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["invoice"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["invoice-tab-counts"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -60,6 +79,7 @@ export const useArchiveInvoice = () => {
|
||||
mutationFn: (id: string) => api.archiveInvoice(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["invoice"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["invoice-tab-counts"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,10 +7,22 @@ import type {
|
||||
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) => {
|
||||
@@ -21,16 +33,36 @@ export const createInvoice = async (params: CreatePreInvoiceType) => {
|
||||
export const getInvoice = async (
|
||||
params: GetInvoicesParams = {},
|
||||
): Promise<GetInvoiceResponseType> => {
|
||||
const { page = 1, statuses } = params;
|
||||
const { page = 1, statuses, search, from, to } = params;
|
||||
const { data } = await axios.get<GetInvoiceResponseType>("/admin/invoice", {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user