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 <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-10-02 22:22:24 +03:30
co-authored by Cursor
parent bc1c5f0747
commit 41f21999d2
4 changed files with 221 additions and 37 deletions
+22 -2
View File
@@ -1236,6 +1236,10 @@ const en = {
'orders.search': 'Search', 'orders.search': 'Search',
'orders.clearFilters': 'Clear filters', 'orders.clearFilters': 'Clear filters',
'orders.listTitle': 'Order list', '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.showing': 'Showing {from} - {to} of {total}',
'orders.none': 'No orders', 'orders.none': 'No orders',
'orders.empty': 'No results found.', 'orders.empty': 'No results found.',
@@ -1244,12 +1248,18 @@ const en = {
'orders.error.minMax': 'Minimum cost cannot be greater than maximum cost.', 'orders.error.minMax': 'Minimum cost cannot be greater than maximum cost.',
'orders.error.remove': 'Unable to remove order.', 'orders.error.remove': 'Unable to remove order.',
'orders.removed': 'Order {orderNumber} has been removed.', '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.orderId': 'Order ID',
'orders.col.customer': 'Customer', 'orders.col.customer': 'Customer',
'orders.col.items': 'Items', 'orders.col.items': 'Items',
'orders.col.total': 'Total cost', 'orders.col.total': 'Total cost',
'orders.col.date': 'Date & time', '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.source': 'Registered by',
'orders.col.actions': 'Actions', 'orders.col.actions': 'Actions',
'orders.pageMeta': 'Page {page} / {totalPages} · {pageSize} per page · {total} total', 'orders.pageMeta': 'Page {page} / {totalPages} · {pageSize} per page · {total} total',
@@ -4116,6 +4126,10 @@ const fa: Record<MessageKey, string> = {
'orders.search': 'جستجو', 'orders.search': 'جستجو',
'orders.clearFilters': 'پاک کردن فیلترها', 'orders.clearFilters': 'پاک کردن فیلترها',
'orders.listTitle': 'فهرست سفارش‌ها', 'orders.listTitle': 'فهرست سفارش‌ها',
'orders.tabsAria': 'فهرست‌های سفارش',
'orders.tab.paid': 'سفارش‌های موفق',
'orders.tab.all': 'همه سفارش‌ها',
'orders.emptyPaid': 'هنوز سفارش پرداخت‌شده‌ای وجود ندارد.',
'orders.showing': 'نمایش {from} تا {to} از {total}', 'orders.showing': 'نمایش {from} تا {to} از {total}',
'orders.none': 'بدون سفارش', 'orders.none': 'بدون سفارش',
'orders.empty': 'نتیجه‌ای یافت نشد.', 'orders.empty': 'نتیجه‌ای یافت نشد.',
@@ -4124,12 +4138,18 @@ const fa: Record<MessageKey, string> = {
'orders.error.minMax': 'حداقل مبلغ نمی‌تواند از حداکثر بیشتر باشد.', 'orders.error.minMax': 'حداقل مبلغ نمی‌تواند از حداکثر بیشتر باشد.',
'orders.error.remove': 'حذف سفارش ممکن نشد.', 'orders.error.remove': 'حذف سفارش ممکن نشد.',
'orders.removed': 'سفارش {orderNumber} حذف شد.', 'orders.removed': 'سفارش {orderNumber} حذف شد.',
'orders.payment.paid': 'پرداخت شده',
'orders.payment.unpaid': 'پرداخت نشده',
'orders.payment.failed': 'ناموفق',
'orders.fulfillment.new': 'جدید',
'orders.fulfillment.ready': 'آماده پردازش',
'orders.col.orderId': 'شماره سفارش', 'orders.col.orderId': 'شماره سفارش',
'orders.col.customer': 'مشتری', 'orders.col.customer': 'مشتری',
'orders.col.items': 'اقلام', 'orders.col.items': 'اقلام',
'orders.col.total': 'مبلغ کل', 'orders.col.total': 'مبلغ کل',
'orders.col.date': 'تاریخ و ساعت', 'orders.col.date': 'تاریخ و ساعت',
'orders.col.step': 'وضعیت', 'orders.col.payment': 'وضعیت پرداخت',
'orders.col.step': 'وضعیت سفارش',
'orders.col.source': 'ثبت‌شده توسط', 'orders.col.source': 'ثبت‌شده توسط',
'orders.col.actions': 'عملیات', 'orders.col.actions': 'عملیات',
'orders.pageMeta': 'صفحه {page} / {totalPages} · {pageSize} در هر صفحه · {total} کل', 'orders.pageMeta': 'صفحه {page} / {totalPages} · {pageSize} در هر صفحه · {total} کل',
+89 -18
View File
@@ -19,19 +19,60 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 12px;
border-bottom: 1px solid rgba(148, 163, 184, 0.2); 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 { .tableHeaderTitle {
font-size: 14px; font-size: 14px;
font-weight: 700; font-weight: 700;
color: var(--text-primary); 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 { .meta {
color: var(--text-secondary); color: var(--text-secondary);
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
white-space: nowrap;
} }
.table { .table {
@@ -41,35 +82,39 @@
} }
.colOrderId { .colOrderId {
width: 14%;
}
.colCustomer {
width: 16%;
}
.colItems {
width: 8%;
}
.colTotal {
width: 11%;
}
.colDate {
width: 12%; width: 12%;
} }
.colStep { .colCustomer {
width: 14%; width: 14%;
} }
.colItems {
width: 6%;
}
.colTotal {
width: 10%;
}
.colDate {
width: 11%;
}
.colPayment {
width: 11%;
}
.colStep {
width: 12%;
}
.colSource { .colSource {
width: 10%; width: 10%;
} }
.colActions { .colActions {
width: 128px; width: 120px;
} }
.th, .th,
@@ -159,6 +204,32 @@
background: rgba(139, 92, 246, 0.12); 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 { .stepBadge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
+104 -16
View File
@@ -18,6 +18,7 @@ import {
listOrders, listOrders,
removeOrder, removeOrder,
type Order, type Order,
type OrderPaymentStatus,
type OrdersListResponse, type OrdersListResponse,
type OrderSource, type OrderSource,
} from '../services/orderService' } from '../services/orderService'
@@ -33,7 +34,9 @@ import pageStyles from '../components/PageContent.module.css'
import styles from './OrdersPage.module.css' import styles from './OrdersPage.module.css'
const PAGE_SIZE = 24 const PAGE_SIZE = 24
const COLUMN_COUNT = 8 const COLUMN_COUNT = 9
type OrdersTab = 'paid' | 'all'
interface AppliedFilters { interface AppliedFilters {
orderNumber?: string orderNumber?: string
@@ -98,6 +101,14 @@ function stepLabel(
return step?.label ?? processStepId 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) { function sourceClass(source: OrderSource) {
switch (source) { switch (source) {
case 'admin': case 'admin':
@@ -154,6 +165,7 @@ export function OrdersPage() {
const [processSteps, setProcessSteps] = useState<OrderProcessStep[]>(DEFAULT_ORDER_PROCESS_STEPS) const [processSteps, setProcessSteps] = useState<OrderProcessStep[]>(DEFAULT_ORDER_PROCESS_STEPS)
const [removeTarget, setRemoveTarget] = useState<Order | null>(null) const [removeTarget, setRemoveTarget] = useState<Order | null>(null)
const [removing, setRemoving] = useState(false) const [removing, setRemoving] = useState(false)
const [ordersTab, setOrdersTab] = useState<OrdersTab>('paid')
function sourceLabel(source: OrderSource) { function sourceLabel(source: OrderSource) {
switch (source) { 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(() => { useEffect(() => {
const controller = new AbortController() const controller = new AbortController()
@@ -195,6 +251,7 @@ export function OrdersPage() {
page, page,
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
...appliedFilters, ...appliedFilters,
...(ordersTab === 'paid' ? { paymentStatus: 'paid' } : {}),
}, },
controller.signal, controller.signal,
) )
@@ -215,6 +272,7 @@ export function OrdersPage() {
} }
}, [ }, [
page, page,
ordersTab,
appliedFilters.orderNumber, appliedFilters.orderNumber,
appliedFilters.customerQuery, appliedFilters.customerQuery,
appliedFilters.dateFrom, appliedFilters.dateFrom,
@@ -416,7 +474,37 @@ export function OrdersPage() {
<div className={styles.tablePanel}> <div className={styles.tablePanel}>
<div className={styles.tableWrap}> <div className={styles.tableWrap}>
<div className={styles.tableHeader}> <div className={styles.tableHeader}>
<div className={styles.tableHeaderMain}>
<div className={styles.tableHeaderTitle}>{t('orders.listTitle')}</div> <div className={styles.tableHeaderTitle}>{t('orders.listTitle')}</div>
<div className={styles.tabs} role="tablist" aria-label={t('orders.tabsAria')}>
<button
type="button"
role="tab"
aria-selected={ordersTab === 'paid'}
className={`${styles.tab} ${ordersTab === 'paid' ? styles.tabActive : ''}`}
onClick={() => {
if (ordersTab === 'paid') return
setOrdersTab('paid')
setPage(1)
}}
>
{t('orders.tab.paid')}
</button>
<button
type="button"
role="tab"
aria-selected={ordersTab === 'all'}
className={`${styles.tab} ${ordersTab === 'all' ? styles.tabActive : ''}`}
onClick={() => {
if (ordersTab === 'all') return
setOrdersTab('all')
setPage(1)
}}
>
{t('orders.tab.all')}
</button>
</div>
</div>
<div className={styles.meta}> <div className={styles.meta}>
{data ? ( {data ? (
data.total > 0 ? ( data.total > 0 ? (
@@ -443,6 +531,7 @@ export function OrdersPage() {
<col className={styles.colItems} /> <col className={styles.colItems} />
<col className={styles.colTotal} /> <col className={styles.colTotal} />
<col className={styles.colDate} /> <col className={styles.colDate} />
<col className={styles.colPayment} />
<col className={styles.colStep} /> <col className={styles.colStep} />
<col className={styles.colSource} /> <col className={styles.colSource} />
<col className={styles.colActions} /> <col className={styles.colActions} />
@@ -454,6 +543,7 @@ export function OrdersPage() {
<th className={styles.th}>{t('orders.col.items')}</th> <th className={styles.th}>{t('orders.col.items')}</th>
<th className={styles.th}>{t('orders.col.total')}</th> <th className={styles.th}>{t('orders.col.total')}</th>
<th className={styles.th}>{t('orders.col.date')}</th> <th className={styles.th}>{t('orders.col.date')}</th>
<th className={styles.th}>{t('orders.col.payment')}</th>
<th className={styles.th}>{t('orders.col.step')}</th> <th className={styles.th}>{t('orders.col.step')}</th>
<th className={styles.th}>{t('orders.col.source')}</th> <th className={styles.th}>{t('orders.col.source')}</th>
<th className={`${styles.th} ${styles.thActions}`}>{t('orders.col.actions')}</th> <th className={`${styles.th} ${styles.thActions}`}>{t('orders.col.actions')}</th>
@@ -472,7 +562,9 @@ export function OrdersPage() {
<EmptyTableRow <EmptyTableRow
colSpan={COLUMN_COUNT} colSpan={COLUMN_COUNT}
icon={ShoppingBag} icon={ShoppingBag}
message={t('orders.empty')} message={
ordersTab === 'paid' ? t('orders.emptyPaid') : t('orders.empty')
}
/> />
)} )}
@@ -480,13 +572,8 @@ export function OrdersPage() {
data?.items?.map((order) => { data?.items?.map((order) => {
const { date, time } = formatDateTime(order.createdAt, locale) const { date, time } = formatDateTime(order.createdAt, locale)
const itemQty = totalItemQuantity(order) const itemQty = totalItemQuantity(order)
const currentStepLabel = stepLabel( const paymentStatus = derivePaymentStatus(order)
processSteps, const currentStepLabel = orderStatusLabel(order)
order.processStepId ?? processSteps[0]?.id ?? 'processing',
order.processStepLabel,
order.processStepLabelFa,
locale,
)
return ( return (
<tr key={order.id}> <tr key={order.id}>
@@ -507,17 +594,18 @@ export function OrdersPage() {
<div className={styles.dateTime}>{date}</div> <div className={styles.dateTime}>{date}</div>
{time && <div className={styles.dateTimeSub}>{time}</div>} {time && <div className={styles.dateTimeSub}>{time}</div>}
</td> </td>
<td className={styles.td}>
<span
className={`${styles.paymentBadge} ${paymentStatusClass(paymentStatus)}`}
>
{paymentStatusLabel(paymentStatus)}
</span>
</td>
<td className={styles.td}> <td className={styles.td}>
<button <button
type="button" type="button"
className={styles.stepBadge} className={styles.stepBadge}
style={stepBadgeStyle( style={stepBadgeStyle(orderStatusColor(order))}
stepColor(
processSteps,
order.processStepId ?? processSteps[0]?.id ?? 'processing',
order.processStepColor,
),
)}
onClick={() => setStepOrder(order)} onClick={() => setStepOrder(order)}
aria-label={t('orders.changeStep', { step: currentStepLabel })} aria-label={t('orders.changeStep', { step: currentStepLabel })}
> >
@@ -61,6 +61,8 @@ export interface OrderTransaction {
updatedAt: string updatedAt: string
} }
export type OrderPaymentStatus = 'paid' | 'unpaid' | 'failed'
export interface Order { export interface Order {
id: string id: string
businessId: string businessId: string
@@ -70,6 +72,7 @@ export interface Order {
processStepLabel?: string | null processStepLabel?: string | null
processStepLabelFa?: string | null processStepLabelFa?: string | null
processStepColor?: string | null processStepColor?: string | null
paymentStatus?: OrderPaymentStatus
source: OrderSource source: OrderSource
subtotal: number subtotal: number
shippingTotal: number shippingTotal: number
@@ -104,6 +107,7 @@ export interface ListOrdersParams {
dateTo?: string dateTo?: string
minTotal?: number minTotal?: number
maxTotal?: number maxTotal?: number
paymentStatus?: OrderPaymentStatus
} }
export interface CreateAdminOrderPayment { export interface CreateAdminOrderPayment {
@@ -144,6 +148,7 @@ export async function listOrders(params: ListOrdersParams = {}, signal?: AbortSi
if (params.dateTo) q.set('dateTo', params.dateTo) if (params.dateTo) q.set('dateTo', params.dateTo)
if (params.minTotal !== undefined) q.set('minTotal', String(params.minTotal)) if (params.minTotal !== undefined) q.set('minTotal', String(params.minTotal))
if (params.maxTotal !== undefined) q.set('maxTotal', String(params.maxTotal)) if (params.maxTotal !== undefined) q.set('maxTotal', String(params.maxTotal))
if (params.paymentStatus) q.set('paymentStatus', params.paymentStatus)
const query = q.toString() const query = q.toString()
const path = `${businessPath()}${query ? `?${query}` : ''}` const path = `${businessPath()}${query ? `?${query}` : ''}`