import { useEffect, useMemo, useState } from 'react' import { CreditCard, Eye, RotateCcw, Search, ShoppingBag, Trash2 } from 'lucide-react' import { useLocale } from '@meshkee/dashboard-ui' import { Breadcrumbs } from '../components/Breadcrumbs' import { PageTitle } from '../components/PageTitle' import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal' import { EmptyTableRow } from '../components/EmptyState' import { OrderItemsModal } from '../components/OrderItemsModal' import { OrderStepModal } from '../components/OrderStepModal' import { OrderTransactionsModal } from '../components/OrderTransactionsModal' import { Pagination } from '../components/Pagination' import { Tooltip } from '../components/Tooltip' import { useToast } from '../context/ToastContext' import { useT } from '../i18n/useT' import { ApiError, isAbortError } from '../lib/api' import { formatCellForDisplay } from '../lib/cellNumber' import { listOrders, removeOrder, type Order, type OrdersListResponse, type OrderSource, } from '../services/orderService' import { getSettings, DEFAULT_ORDER_PROCESS_STEPS, type OrderProcessStep, } from '../services/settingsService' import { formatIrtInput, formatIrtPrice, parseIrtInput } from '../utils/irtPrice' import { stepBadgeStyle } from '../utils/stepColors' import filterStyles from '../components/ListFiltersPanel.module.css' import pageStyles from '../components/PageContent.module.css' import styles from './OrdersPage.module.css' const PAGE_SIZE = 24 const COLUMN_COUNT = 8 interface AppliedFilters { orderNumber?: string customerQuery?: string dateFrom?: string dateTo?: string minTotal?: number maxTotal?: number } function displayName(order: Order) { const name = [order.customer.firstName, order.customer.lastName].filter(Boolean).join(' ').trim() return name || '—' } function formatDateTime(value: string, locale: 'en' | 'fa') { const d = new Date(value) if (Number.isNaN(d.getTime())) return { date: value, time: '' } const intlLocale = locale === 'fa' ? 'fa-IR' : 'en-US' return { date: d.toLocaleDateString(intlLocale, { year: 'numeric', month: 'short', day: '2-digit' }), time: d.toLocaleTimeString(intlLocale, { hour: '2-digit', minute: '2-digit' }), } } function totalItemQuantity(order: Order) { return order.items.reduce((sum, item) => sum + item.quantity, 0) } function defaultFaForLabel(label: string | null | undefined) { const trimmed = label?.trim() if (!trimmed) return undefined return DEFAULT_ORDER_PROCESS_STEPS.find( (step) => step.label.toLowerCase() === trimmed.toLowerCase(), )?.labelFa } function stepLabel( steps: OrderProcessStep[], processStepId: string, processStepLabel?: string | null, processStepLabelFa?: string | null, locale: 'en' | 'fa' = 'en', ) { const step = steps.find((item) => item.id === processStepId) ?? DEFAULT_ORDER_PROCESS_STEPS.find((item) => item.id === processStepId) if (locale === 'fa') { let fa = processStepLabelFa?.trim() || step?.labelFa?.trim() if (fa && processStepLabel?.trim() && fa === processStepLabel.trim()) { fa = undefined } fa = fa || defaultFaForLabel(processStepLabel) || defaultFaForLabel(step?.label) if (fa) return fa } if (processStepLabel?.trim()) return processStepLabel.trim() return step?.label ?? processStepId } function sourceClass(source: OrderSource) { switch (source) { case 'admin': return styles.sourceOperator case 'app': return styles.sourceApplication case 'website': default: return styles.sourceWebsite } } function stepColor( steps: OrderProcessStep[], processStepId: string, processStepColor?: string | null, ) { if (processStepColor?.trim()) return processStepColor.trim() const index = steps.findIndex((step) => step.id === processStepId) const step = index >= 0 ? steps[index] : steps[0] if (step?.color) return step.color if (typeof document !== 'undefined') { const themed = getComputedStyle(document.documentElement) .getPropertyValue('--primary') .trim() if (themed) return themed } return '#3b82f6' } export function OrdersPage() { const t = useT() const { locale } = useLocale() const { showToast } = useToast() const [data, setData] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [appliedFilters, setAppliedFilters] = useState({}) const [page, setPage] = useState(1) const [draftOrderId, setDraftOrderId] = useState('') const [draftCustomer, setDraftCustomer] = useState('') const [draftDateFrom, setDraftDateFrom] = useState('') const [draftDateTo, setDraftDateTo] = useState('') const [draftMinCost, setDraftMinCost] = useState('') const [draftMaxCost, setDraftMaxCost] = useState('') const [viewOrder, setViewOrder] = useState(null) const [transactionsOrder, setTransactionsOrder] = useState(null) const [stepOrder, setStepOrder] = useState(null) const [processSteps, setProcessSteps] = useState(DEFAULT_ORDER_PROCESS_STEPS) const [removeTarget, setRemoveTarget] = useState(null) const [removing, setRemoving] = useState(false) function sourceLabel(source: OrderSource) { switch (source) { case 'admin': return t('orders.source.operator') case 'app': return t('orders.source.app') case 'website': default: return t('orders.source.website') } } useEffect(() => { const controller = new AbortController() async function loadSteps() { try { const data = await getSettings(controller.signal) setProcessSteps(data.settings.store.orderProcessSteps) } catch (err) { if (isAbortError(err) || controller.signal.aborted) return } } void loadSteps() return () => controller.abort() }, []) useEffect(() => { const controller = new AbortController() async function load() { setLoading(true) setError('') try { const result = await listOrders( { page, pageSize: PAGE_SIZE, ...appliedFilters, }, controller.signal, ) if (controller.signal.aborted) return setData(result) } catch (err) { if (isAbortError(err) || controller.signal.aborted) return setError(err instanceof ApiError ? err.message : t('orders.error.load')) } finally { if (!controller.signal.aborted) setLoading(false) } } void load() return () => { controller.abort() } }, [ page, appliedFilters.orderNumber, appliedFilters.customerQuery, appliedFilters.dateFrom, appliedFilters.dateTo, appliedFilters.minTotal, appliedFilters.maxTotal, t, ]) const totalPages = useMemo(() => { const total = data?.total ?? 0 return Math.max(1, Math.ceil(total / PAGE_SIZE)) }, [data?.total]) const showingFrom = useMemo(() => { if (!data || data.total === 0) return 0 return (page - 1) * PAGE_SIZE + 1 }, [data, page]) const showingTo = useMemo(() => { if (!data) return 0 return Math.min(data.total, page * PAGE_SIZE) }, [data, page]) function applyFilters() { const minTotal = parseIrtInput(draftMinCost) const maxTotal = parseIrtInput(draftMaxCost) if (minTotal !== null && maxTotal !== null && minTotal > maxTotal) { setError(t('orders.error.minMax')) return } setPage(1) setAppliedFilters({ ...(draftOrderId.trim() ? { orderNumber: draftOrderId.trim() } : {}), ...(draftCustomer.trim() ? { customerQuery: draftCustomer.trim() } : {}), ...(draftDateFrom ? { dateFrom: draftDateFrom } : {}), ...(draftDateTo ? { dateTo: draftDateTo } : {}), ...(minTotal !== null ? { minTotal } : {}), ...(maxTotal !== null ? { maxTotal } : {}), }) } function clearFilters() { setDraftOrderId('') setDraftCustomer('') setDraftDateFrom('') setDraftDateTo('') setDraftMinCost('') setDraftMaxCost('') setPage(1) setAppliedFilters({}) setError('') } function handleStepSaved(updated: Order) { setData((prev) => { if (!prev) return prev return { ...prev, items: prev.items.map((item) => (item.id === updated.id ? updated : item)), } }) } async function confirmRemove() { if (!removeTarget) return setRemoving(true) setError('') try { await removeOrder(removeTarget.id) setData((prev) => { if (!prev) return prev return { ...prev, total: Math.max(0, prev.total - 1), items: prev.items.filter((item) => item.id !== removeTarget.id), } }) showToast(t('orders.removed', { orderNumber: removeTarget.orderNumber }), 'success') setRemoveTarget(null) } catch (err) { setError(err instanceof ApiError ? err.message : t('orders.error.remove')) } finally { setRemoving(false) } } return (
{t('orders.title')}

{t('orders.subtitle')}

{t('orders.filters')}
{ e.preventDefault(); applyFilters() }}>
setDraftOrderId(e.target.value)} placeholder={t('orders.filter.orderId')} aria-label={t('orders.filter.orderId')} />
setDraftCustomer(e.target.value)} placeholder={t('orders.filter.customer')} aria-label={t('orders.filter.customer')} autoComplete="off" />
setDraftDateFrom(e.target.value)} aria-label={t('orders.filter.dateFrom')} title={t('orders.filter.dateFrom')} />
setDraftDateTo(e.target.value)} aria-label={t('orders.filter.dateTo')} title={t('orders.filter.dateTo')} />
setDraftMinCost(formatIrtInput(e.target.value))} placeholder={t('orders.filter.minCost')} aria-label={t('orders.filter.minCost')} autoComplete="off" />
setDraftMaxCost(formatIrtInput(e.target.value))} placeholder={t('orders.filter.maxCost')} aria-label={t('orders.filter.maxCost')} autoComplete="off" />
{t('orders.listTitle')}
{data ? ( data.total > 0 ? ( t('orders.showing', { from: showingFrom, to: showingTo, total: data.total, }) ) : ( t('orders.none') ) ) : ( ' ' )}
{error &&
{error}
} {loading && ( )} {!loading && data?.items?.length === 0 && ( )} {!loading && data?.items?.map((order) => { const { date, time } = formatDateTime(order.createdAt, locale) const itemQty = totalItemQuantity(order) const currentStepLabel = stepLabel( processSteps, order.processStepId ?? processSteps[0]?.id ?? 'processing', order.processStepLabel, order.processStepLabelFa, locale, ) return ( ) })}
{t('orders.col.orderId')} {t('orders.col.customer')} {t('orders.col.items')} {t('orders.col.total')} {t('orders.col.date')} {t('orders.col.step')} {t('orders.col.source')} {t('orders.col.actions')}
{t('orders.loading')}
{order.orderNumber}
{displayName(order)}
{formatCellForDisplay(order.customer.cellNumber)}
{itemQty > 0 ? itemQty : 0} {formatIrtPrice(order.total)}
{date}
{time &&
{time}
}
{sourceLabel(order.source)}
{t('orders.pageMeta', { page, totalPages, pageSize: PAGE_SIZE, total: data?.total ?? 0, })}
setViewOrder(null)} /> setTransactionsOrder(null)} /> setStepOrder(null)} onSaved={handleStepSaved} /> setRemoveTarget(null)} onConfirm={() => void confirmRemove()} />
) }