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
+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;
+105 -17
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.tableHeaderTitle}>{t('orders.listTitle')}</div>
<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 })}
>