From 41f21999d2756b8ec280687be9ccedb9f30c41b2 Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Fri, 2 Oct 2026 22:22:24 +0330 Subject: [PATCH] Show payment and order status with paid/all order tabs. Default to successful (paid) orders, keep fulfillment status editable, and display gateway payment separately. Co-authored-by: Cursor --- apps/business/src/i18n/messages.ts | 24 +++- apps/business/src/pages/OrdersPage.module.css | 107 ++++++++++++--- apps/business/src/pages/OrdersPage.tsx | 122 +++++++++++++++--- apps/business/src/services/orderService.ts | 5 + 4 files changed, 221 insertions(+), 37 deletions(-) diff --git a/apps/business/src/i18n/messages.ts b/apps/business/src/i18n/messages.ts index 6f1620c..ec4b823 100644 --- a/apps/business/src/i18n/messages.ts +++ b/apps/business/src/i18n/messages.ts @@ -1236,6 +1236,10 @@ const en = { 'orders.search': 'Search', 'orders.clearFilters': 'Clear filters', 'orders.listTitle': 'Order list', + 'orders.tabsAria': 'Order lists', + 'orders.tab.paid': 'Successful orders', + 'orders.tab.all': 'All orders', + 'orders.emptyPaid': 'No paid orders yet.', 'orders.showing': 'Showing {from} - {to} of {total}', 'orders.none': 'No orders', 'orders.empty': 'No results found.', @@ -1244,12 +1248,18 @@ const en = { 'orders.error.minMax': 'Minimum cost cannot be greater than maximum cost.', 'orders.error.remove': 'Unable to remove order.', 'orders.removed': 'Order {orderNumber} has been removed.', + 'orders.payment.paid': 'Paid', + 'orders.payment.unpaid': 'Unpaid', + 'orders.payment.failed': 'Failed', + 'orders.fulfillment.new': 'New', + 'orders.fulfillment.ready': 'Ready to process', 'orders.col.orderId': 'Order ID', 'orders.col.customer': 'Customer', 'orders.col.items': 'Items', 'orders.col.total': 'Total cost', 'orders.col.date': 'Date & time', - 'orders.col.step': 'Step', + 'orders.col.payment': 'Payment', + 'orders.col.step': 'Order status', 'orders.col.source': 'Registered by', 'orders.col.actions': 'Actions', 'orders.pageMeta': 'Page {page} / {totalPages} · {pageSize} per page · {total} total', @@ -4116,6 +4126,10 @@ const fa: Record = { 'orders.search': 'جستجو', 'orders.clearFilters': 'پاک کردن فیلترها', 'orders.listTitle': 'فهرست سفارش‌ها', + 'orders.tabsAria': 'فهرست‌های سفارش', + 'orders.tab.paid': 'سفارش‌های موفق', + 'orders.tab.all': 'همه سفارش‌ها', + 'orders.emptyPaid': 'هنوز سفارش پرداخت‌شده‌ای وجود ندارد.', 'orders.showing': 'نمایش {from} تا {to} از {total}', 'orders.none': 'بدون سفارش', 'orders.empty': 'نتیجه‌ای یافت نشد.', @@ -4124,12 +4138,18 @@ const fa: Record = { 'orders.error.minMax': 'حداقل مبلغ نمی‌تواند از حداکثر بیشتر باشد.', 'orders.error.remove': 'حذف سفارش ممکن نشد.', 'orders.removed': 'سفارش {orderNumber} حذف شد.', + 'orders.payment.paid': 'پرداخت شده', + 'orders.payment.unpaid': 'پرداخت نشده', + 'orders.payment.failed': 'ناموفق', + 'orders.fulfillment.new': 'جدید', + 'orders.fulfillment.ready': 'آماده پردازش', 'orders.col.orderId': 'شماره سفارش', 'orders.col.customer': 'مشتری', 'orders.col.items': 'اقلام', 'orders.col.total': 'مبلغ کل', 'orders.col.date': 'تاریخ و ساعت', - 'orders.col.step': 'وضعیت', + 'orders.col.payment': 'وضعیت پرداخت', + 'orders.col.step': 'وضعیت سفارش', 'orders.col.source': 'ثبت‌شده توسط', 'orders.col.actions': 'عملیات', 'orders.pageMeta': 'صفحه {page} / {totalPages} · {pageSize} در هر صفحه · {total} کل', diff --git a/apps/business/src/pages/OrdersPage.module.css b/apps/business/src/pages/OrdersPage.module.css index fe455db..9d25a6c 100644 --- a/apps/business/src/pages/OrdersPage.module.css +++ b/apps/business/src/pages/OrdersPage.module.css @@ -19,19 +19,60 @@ display: flex; align-items: center; justify-content: space-between; + gap: 12px; border-bottom: 1px solid rgba(148, 163, 184, 0.2); } +.tableHeaderMain { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px 16px; + min-width: 0; +} + .tableHeaderTitle { font-size: 14px; font-weight: 700; color: var(--text-primary); } +.tabs { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px; + border-radius: 999px; + background: rgba(148, 163, 184, 0.14); +} + +.tab { + border: 0; + background: transparent; + color: var(--text-secondary); + font-size: 12px; + font-weight: 700; + padding: 6px 12px; + border-radius: 999px; + cursor: pointer; + transition: background 0.15s, color 0.15s; +} + +.tab:hover { + color: var(--text-primary); +} + +.tabActive { + background: var(--surface, #fff); + color: var(--text-primary); + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08); +} + .meta { color: var(--text-secondary); font-size: 12px; font-weight: 500; + white-space: nowrap; } .table { @@ -41,35 +82,39 @@ } .colOrderId { - width: 14%; -} - -.colCustomer { - width: 16%; -} - -.colItems { - width: 8%; -} - -.colTotal { - width: 11%; -} - -.colDate { width: 12%; } -.colStep { +.colCustomer { width: 14%; } +.colItems { + width: 6%; +} + +.colTotal { + width: 10%; +} + +.colDate { + width: 11%; +} + +.colPayment { + width: 11%; +} + +.colStep { + width: 12%; +} + .colSource { width: 10%; } .colActions { - width: 128px; + width: 120px; } .th, @@ -159,6 +204,32 @@ background: rgba(139, 92, 246, 0.12); } +.paymentBadge { + display: inline-flex; + align-items: center; + max-width: 100%; + padding: 4px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + white-space: nowrap; +} + +.paymentPaid { + color: #047857; + background: rgba(16, 185, 129, 0.12); +} + +.paymentUnpaid { + color: #64748b; + background: rgba(100, 116, 139, 0.14); +} + +.paymentFailed { + color: #b91c1c; + background: rgba(239, 68, 68, 0.12); +} + .stepBadge { display: inline-flex; align-items: center; diff --git a/apps/business/src/pages/OrdersPage.tsx b/apps/business/src/pages/OrdersPage.tsx index 889715e..89476c8 100644 --- a/apps/business/src/pages/OrdersPage.tsx +++ b/apps/business/src/pages/OrdersPage.tsx @@ -18,6 +18,7 @@ import { listOrders, removeOrder, type Order, + type OrderPaymentStatus, type OrdersListResponse, type OrderSource, } from '../services/orderService' @@ -33,7 +34,9 @@ import pageStyles from '../components/PageContent.module.css' import styles from './OrdersPage.module.css' const PAGE_SIZE = 24 -const COLUMN_COUNT = 8 +const COLUMN_COUNT = 9 + +type OrdersTab = 'paid' | 'all' interface AppliedFilters { orderNumber?: string @@ -98,6 +101,14 @@ function stepLabel( return step?.label ?? processStepId } +function derivePaymentStatus(order: Order): OrderPaymentStatus { + if (order.paymentStatus) return order.paymentStatus + const txs = order.transactions ?? [] + if (txs.some((tx) => tx.status === 'completed')) return 'paid' + if (txs.length > 0 && txs.every((tx) => tx.status === 'failed')) return 'failed' + return 'unpaid' +} + function sourceClass(source: OrderSource) { switch (source) { case 'admin': @@ -154,6 +165,7 @@ export function OrdersPage() { const [processSteps, setProcessSteps] = useState(DEFAULT_ORDER_PROCESS_STEPS) const [removeTarget, setRemoveTarget] = useState(null) const [removing, setRemoving] = useState(false) + const [ordersTab, setOrdersTab] = useState('paid') function sourceLabel(source: OrderSource) { switch (source) { @@ -167,6 +179,50 @@ export function OrdersPage() { } } + function paymentStatusLabel(status: OrderPaymentStatus) { + switch (status) { + case 'paid': + return t('orders.payment.paid') + case 'failed': + return t('orders.payment.failed') + case 'unpaid': + default: + return t('orders.payment.unpaid') + } + } + + function paymentStatusClass(status: OrderPaymentStatus) { + switch (status) { + case 'paid': + return styles.paymentPaid + case 'failed': + return styles.paymentFailed + case 'unpaid': + default: + return styles.paymentUnpaid + } + } + + function orderStatusLabel(order: Order) { + const stepId = order.processStepId ?? processSteps[0]?.id ?? 'processing' + if (stepId === 'awaiting-payment') return t('orders.fulfillment.new') + if (stepId === 'payment-successful') return t('orders.fulfillment.ready') + return stepLabel( + processSteps, + stepId, + order.processStepLabel, + order.processStepLabelFa, + locale, + ) + } + + function orderStatusColor(order: Order) { + const stepId = order.processStepId ?? processSteps[0]?.id ?? 'processing' + if (stepId === 'awaiting-payment') return '#64748B' + if (stepId === 'payment-successful') return '#3B82F6' + return stepColor(processSteps, stepId, order.processStepColor) + } + useEffect(() => { const controller = new AbortController() @@ -195,6 +251,7 @@ export function OrdersPage() { page, pageSize: PAGE_SIZE, ...appliedFilters, + ...(ordersTab === 'paid' ? { paymentStatus: 'paid' } : {}), }, controller.signal, ) @@ -215,6 +272,7 @@ export function OrdersPage() { } }, [ page, + ordersTab, appliedFilters.orderNumber, appliedFilters.customerQuery, appliedFilters.dateFrom, @@ -416,7 +474,37 @@ export function OrdersPage() {
-
{t('orders.listTitle')}
+
+
{t('orders.listTitle')}
+
+ + +
+
{data ? ( data.total > 0 ? ( @@ -443,6 +531,7 @@ export function OrdersPage() { + @@ -454,6 +543,7 @@ export function OrdersPage() { {t('orders.col.items')} {t('orders.col.total')} {t('orders.col.date')} + {t('orders.col.payment')} {t('orders.col.step')} {t('orders.col.source')} {t('orders.col.actions')} @@ -472,7 +562,9 @@ export function OrdersPage() { )} @@ -480,13 +572,8 @@ export function OrdersPage() { 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, - ) + const paymentStatus = derivePaymentStatus(order) + const currentStepLabel = orderStatusLabel(order) return ( @@ -507,17 +594,18 @@ export function OrdersPage() {
{date}
{time &&
{time}
} + + + {paymentStatusLabel(paymentStatus)} + +