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:
co-authored by
Cursor
parent
bc1c5f0747
commit
41f21999d2
@@ -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;
|
||||
|
||||
@@ -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 })}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user