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
@@ -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} کل',
|
||||
|
||||
@@ -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.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}` : ''}`
|
||||
|
||||
Reference in New Issue
Block a user