Compare commits

..

3 Commits

Author SHA1 Message Date
morteza f1d0a661e7 orders
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-23 19:40:17 +03:30
morteza 1bea0ed3ec count 2026-07-23 18:47:48 +03:30
morteza cea411b912 invoice 2026-07-23 18:13:39 +03:30
10 changed files with 141 additions and 68 deletions
+7 -5
View File
@@ -17,13 +17,15 @@ export type StatusConfig = {
*/
export const statusRegistry: Record<string, StatusConfig> = {
// --- ProformaInvoiceStatusEnum ---
[ProformaInvoiceStatusEnum.ALL]: { label: 'همه', variant: 'neutral' },
[ProformaInvoiceStatusEnum.NOT_CONFIRMED]: { label: 'تایید نشده', variant: 'warning' },
[ProformaInvoiceStatusEnum.PARTIALLY_CONFIRMED]: { label: 'تایید جزئی', variant: 'info' },
[ProformaInvoiceStatusEnum.PENDING]: { label: 'در انتظار تایید', variant: 'warning' },
[ProformaInvoiceStatusEnum.CONFIRMED]: { label: 'تایید شده', variant: 'success' },
[ProformaInvoiceStatusEnum.ARCHIVED]: { label: 'آرشیو شده', variant: 'neutral' },
// --- Invoice confirm status (from API; partially_confirmed/confirmed covered by enum above) ---
pending: { label: 'تایید نشده', variant: 'warning' },
// --- Invoice confirm status (from API) ---
pending: { label: 'در انتظار تایید', variant: 'warning' },
partially_confirmed: { label: 'تایید جزئی', variant: 'info' },
confirmed: { label: 'تایید شده', variant: 'success' },
archived: { label: 'آرشیو شده', variant: 'neutral' },
// --- OrderStatusEnum ---
[OrderStatusEnum.CREATED]: { label: 'ایجاد شده', variant: 'info' },
+5 -4
View File
@@ -10,7 +10,7 @@ import { getChatSenderName } from '../type/Types'
type Props = {
content: string
attachments?: ChatAttachmentType[]
attachments?: ChatAttachmentType[] | null
senderName?: string
senderLabel?: string
createdAt?: string
@@ -25,7 +25,7 @@ type Props = {
const ChatMessage: FC<Props> = ({
content,
attachments = [],
attachments,
senderName,
senderLabel = 'پشتیبان',
createdAt,
@@ -37,8 +37,9 @@ const ChatMessage: FC<Props> = ({
parentSenderLabel,
onReply,
}) => {
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
+1 -1
View File
@@ -33,7 +33,7 @@ const Stats: FC = () => {
<StatCard
count={data?.data.invoicesCount}
description={t('home.factureCount')}
to={`${Paths.proformaInvoice}?tab=${ProformaInvoiceStatusEnum.NOT_CONFIRMED}`}
to={`${Paths.proformaInvoice}?tab=${ProformaInvoiceStatusEnum.PENDING}`}
icon={<ReceiptText
size={27}
color={COLORS.primary}
+54 -45
View File
@@ -1,10 +1,14 @@
import Tabs from '@/components/Tabs'
import { useMemo, useState, type FC } from 'react'
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
import { ProformaInvoiceStatusEnum } from './enum/InvoiceEnum'
import {
invoiceListTabs,
invoiceListTabStatuses,
ProformaInvoiceStatusEnum,
} from './enum/InvoiceEnum'
import Filters from '@/components/Filters'
import Table from '@/components/Table'
import { useGetInvoices } from './hooks/useInvoiceData'
import { useGetInvoices, useGetInvoiceTabCounts } from './hooks/useInvoiceData'
import type { Invoice } from './types/InvoiceTypes'
import type { ColumnType } from '@/components/types/TableTypes'
import type { FilterValues } from '@/components/Filters'
@@ -14,11 +18,10 @@ import moment from 'moment-jalaali'
import StatusWithText from '@/components/StatusWithText'
import { Eye } from 'iconsax-react'
const INVOICE_TABS = invoiceListTabs.map((tab) => tab.value)
const isInvoiceTab = (value: string | null): value is ProformaInvoiceStatusEnum =>
value === ProformaInvoiceStatusEnum.ALL ||
value === ProformaInvoiceStatusEnum.NOT_CONFIRMED ||
value === ProformaInvoiceStatusEnum.PARTIALLY_CONFIRMED ||
value === ProformaInvoiceStatusEnum.CONFIRMED
value !== null && INVOICE_TABS.includes(value as ProformaInvoiceStatusEnum)
const getServiceNames = (invoice: Invoice): string => {
const names = [...new Set(invoice.items.map((item) => item.product.title))]
@@ -27,29 +30,53 @@ const getServiceNames = (invoice: Invoice): string => {
const ProformaInvoice: FC = () => {
const navigate = useNavigate()
const [searchParams] = useSearchParams()
const [searchParams, setSearchParams] = useSearchParams()
const tabFromQuery = searchParams.get('tab')
const [activeTab, setActiveTab] = useState<ProformaInvoiceStatusEnum>(
isInvoiceTab(tabFromQuery) ? tabFromQuery : ProformaInvoiceStatusEnum.ALL,
)
const activeTab = isInvoiceTab(tabFromQuery)
? tabFromQuery
: ProformaInvoiceStatusEnum.PENDING
const [filters, setFilters] = useState<FilterValues>({})
const { data, isPending } = useGetInvoices()
const statuses = invoiceListTabStatuses[activeTab]
const setActiveTab = (tab: ProformaInvoiceStatusEnum) => {
setSearchParams(
(current) => {
const next = new URLSearchParams(current)
next.set('tab', tab)
return next
},
{ replace: true },
)
}
const { data, isPending } = useGetInvoices({ statuses })
const { data: tabCounts } = useGetInvoiceTabCounts()
const invoices = useMemo(() => data?.data ?? [], [data?.data])
const tabsWithCount = useMemo(
() =>
invoiceListTabs.map((tab) => {
const total =
tab.value === ProformaInvoiceStatusEnum.PENDING
? (tabCounts?.pending ?? 0)
: tab.value === ProformaInvoiceStatusEnum.CONFIRMED
? (tabCounts?.confirmed ?? 0)
: (tabCounts?.archived ?? 0)
return {
...tab,
label:
total > 0
? `${tab.label} (${total.toLocaleString('fa-IR')})`
: tab.label,
}
}),
[tabCounts],
)
const filteredInvoices = useMemo(() => {
let result = invoices
// فیلتر تب
if (activeTab === ProformaInvoiceStatusEnum.NOT_CONFIRMED) {
result = result.filter((inv) => inv.confirmStatus === 'pending')
} else if (activeTab === ProformaInvoiceStatusEnum.PARTIALLY_CONFIRMED) {
result = result.filter((inv) => inv.confirmStatus === 'partially_confirmed')
} else if (activeTab === ProformaInvoiceStatusEnum.CONFIRMED) {
result = result.filter((inv) => inv.confirmStatus === 'confirmed')
}
// فیلتر جستجو
const search = filters.search?.toString().trim()
if (search) {
const lower = search.toLowerCase()
@@ -62,7 +89,6 @@ const ProformaInvoice: FC = () => {
)
}
// فیلتر تاریخ
const dateFilter = filters.date?.toString()
if (dateFilter) {
result = result.filter((inv) => {
@@ -72,7 +98,7 @@ const ProformaInvoice: FC = () => {
}
return result
}, [invoices, activeTab, filters])
}, [invoices, filters])
const columns: ColumnType<Invoice>[] = [
{
@@ -101,15 +127,16 @@ const ProformaInvoice: FC = () => {
render: (item) => <span>{NumberFormat(item.total)} ریال</span>,
},
{
key: 'confirmStatus',
key: 'status',
title: 'وضعیت تایید',
render: (item) => {
const statusMap = {
confirmed: { variant: 'success' as const, text: 'تایید شده' },
partially_confirmed: { variant: 'info' as const, text: 'تایید جزئی' },
pending: { variant: 'warning' as const, text: 'تایید نشده' },
pending: { variant: 'warning' as const, text: 'در انتظار تایید' },
archived: { variant: 'info' as const, text: 'آرشیو شده' },
}
const status = statusMap[item.confirmStatus] ?? statusMap.pending
const status = statusMap[item.status] ?? statusMap.pending
return (
<StatusWithText
variant={status.variant}
@@ -118,19 +145,6 @@ const ProformaInvoice: FC = () => {
)
},
},
{
key: 'paymentStatus',
title: 'وضعیت پرداخت',
render: (item) => {
const isPaid = item.paidAmount >= item.total && item.total > 0
return (
<StatusWithText
variant={isPaid ? 'success' : 'info'}
text={isPaid ? 'پرداخت شده' : 'پرداخت نشده'}
/>
)
},
},
{
key: 'action',
title: '',
@@ -157,12 +171,7 @@ const ProformaInvoice: FC = () => {
<div className="mt-8">
<Tabs
items={[
{ label: 'همه', value: ProformaInvoiceStatusEnum.ALL },
{ label: 'تایید نشده', value: ProformaInvoiceStatusEnum.NOT_CONFIRMED },
{ label: 'تایید جزئی', value: ProformaInvoiceStatusEnum.PARTIALLY_CONFIRMED },
{ label: 'تایید شده', value: ProformaInvoiceStatusEnum.CONFIRMED },
]}
items={tabsWithCount}
activeTab={activeTab}
onTabChange={(tab) => setActiveTab(tab as ProformaInvoiceStatusEnum)}
/>
+24 -4
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
+19 -4
View File
@@ -1,10 +1,18 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as api from "../service/InvoiceService";
export const useGetInvoices = () => {
export const useGetInvoices = (params: api.GetInvoicesParams) => {
return useQuery({
queryKey: ["invoices"],
queryFn: api.getInvoices,
queryKey: ["invoices", params.statuses?.join(",") ?? ""],
queryFn: () => api.getInvoices(params),
});
};
export const useGetInvoiceTabCounts = () => {
return useQuery({
queryKey: ["invoice-tab-counts"],
queryFn: () => api.getInvoiceTabCounts(),
refetchInterval: 20_000,
});
};
@@ -17,7 +25,14 @@ export const useGetInvoiceDetail = (id: string) => {
};
export const useConfirmInvoiceItem = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.confrimInvoiceItem,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["invoices"] });
queryClient.invalidateQueries({ queryKey: ["invoice"] });
queryClient.invalidateQueries({ queryKey: ["invoice-tab-counts"] });
},
});
};
+28 -2
View File
@@ -3,12 +3,38 @@ import type {
InvoicesResponseType,
InvoiceDetailResponseType,
} from "../types/InvoiceTypes";
import type { InvoiceConfirmStatus } from "../enum/InvoiceEnum";
import type { BaseResponse } from "@/shared/types/Types";
export const getInvoices = async (): Promise<InvoicesResponseType> => {
const { data } = await axios.get<InvoicesResponseType>("/public/invoice");
export type GetInvoicesParams = {
statuses?: InvoiceConfirmStatus[];
};
export type InvoiceTabCountsResponseType = {
pending: number;
confirmed: number;
archived: number;
};
export const getInvoices = async (
params: GetInvoicesParams = {},
): Promise<InvoicesResponseType> => {
const { statuses } = params;
const { data } = await axios.get<InvoicesResponseType>("/public/invoice", {
params: {
...(statuses?.length ? { statuses: statuses.join(",") } : {}),
},
});
return data;
};
export const getInvoiceTabCounts = async (): Promise<InvoiceTabCountsResponseType> => {
const { data } = await axios.get<BaseResponse<InvoiceTabCountsResponseType>>(
"/public/invoice/tab-counts",
);
return data.data;
};
export const getInvoiceDetail = async (
id: string,
): Promise<InvoiceDetailResponseType> => {
+1 -1
View File
@@ -55,7 +55,7 @@ export type Invoice = {
total: number;
paidAmount: number;
balance: number;
confirmStatus: 'pending' | 'partially_confirmed' | 'confirmed';
status: 'pending' | 'partially_confirmed' | 'confirmed' | 'archived';
enableTax: boolean;
approvalDeadline: string;
attachments: unknown[];
+1 -1
View File
@@ -34,7 +34,7 @@ const MyOrders: FC = () => {
items={[
{ label: 'همه', value: TabMyOrdersEnum.ALL },
{ label: 'در حال انجام', value: TabMyOrdersEnum.IN_PROGRESS },
{ label: 'تکمیل داده شده', value: TabMyOrdersEnum.DELIVERED },
{ label: 'تکمیل شده', value: TabMyOrdersEnum.DELIVERED },
{ label: 'کنسل شده', value: TabMyOrdersEnum.CANCELLED },
]}
activeTab={activeTab}