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.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<MessageKey, string> = {
'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<MessageKey, string> = {
'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} کل',
+89 -18
View File
@@ -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;
+104 -16
View File
@@ -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<OrderProcessStep[]>(DEFAULT_ORDER_PROCESS_STEPS)
const [removeTarget, setRemoveTarget] = useState<Order | null>(null)
const [removing, setRemoving] = useState(false)
const [ordersTab, setOrdersTab] = useState<OrdersTab>('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() {
<div className={styles.tablePanel}>
<div className={styles.tableWrap}>
<div className={styles.tableHeader}>
<div className={styles.tableHeaderMain}>
<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}>
{data ? (
data.total > 0 ? (
@@ -443,6 +531,7 @@ export function OrdersPage() {
<col className={styles.colItems} />
<col className={styles.colTotal} />
<col className={styles.colDate} />
<col className={styles.colPayment} />
<col className={styles.colStep} />
<col className={styles.colSource} />
<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.total')}</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.source')}</th>
<th className={`${styles.th} ${styles.thActions}`}>{t('orders.col.actions')}</th>
@@ -472,7 +562,9 @@ export function OrdersPage() {
<EmptyTableRow
colSpan={COLUMN_COUNT}
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) => {
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 (
<tr key={order.id}>
@@ -507,17 +594,18 @@ export function OrdersPage() {
<div className={styles.dateTime}>{date}</div>
{time && <div className={styles.dateTimeSub}>{time}</div>}
</td>
<td className={styles.td}>
<span
className={`${styles.paymentBadge} ${paymentStatusClass(paymentStatus)}`}
>
{paymentStatusLabel(paymentStatus)}
</span>
</td>
<td className={styles.td}>
<button
type="button"
className={styles.stepBadge}
style={stepBadgeStyle(
stepColor(
processSteps,
order.processStepId ?? processSteps[0]?.id ?? 'processing',
order.processStepColor,
),
)}
style={stepBadgeStyle(orderStatusColor(order))}
onClick={() => setStepOrder(order)}
aria-label={t('orders.changeStep', { step: currentStepLabel })}
>
@@ -61,6 +61,8 @@ export interface OrderTransaction {
updatedAt: string
}
export type OrderPaymentStatus = 'paid' | 'unpaid' | 'failed'
export interface Order {
id: string
businessId: string
@@ -70,6 +72,7 @@ export interface Order {
processStepLabel?: string | null
processStepLabelFa?: string | null
processStepColor?: string | null
paymentStatus?: OrderPaymentStatus
source: OrderSource
subtotal: number
shippingTotal: number
@@ -104,6 +107,7 @@ export interface ListOrdersParams {
dateTo?: string
minTotal?: number
maxTotal?: number
paymentStatus?: OrderPaymentStatus
}
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.minTotal !== undefined) q.set('minTotal', String(params.minTotal))
if (params.maxTotal !== undefined) q.set('maxTotal', String(params.maxTotal))
if (params.paymentStatus) q.set('paymentStatus', params.paymentStatus)
const query = q.toString()
const path = `${businessPath()}${query ? `?${query}` : ''}`