feat(admin): update configuration and documentation for order processing
- Removed .env.example file as it is no longer needed. - Updated next.config.js to include turbopack configuration. - Added detailed implementation plans for order processing and UI design in new markdown files. - Introduced API usage guide for background removal service. This commit enhances the order processing backend and UI design documentation, ensuring clarity and improved configuration management.
This commit is contained in:
488
apps/admin/docs/07-orders-ui-design-plan.md
Normal file
488
apps/admin/docs/07-orders-ui-design-plan.md
Normal file
@@ -0,0 +1,488 @@
|
||||
# Orders UI — Design Plan (Admin Dashboard)
|
||||
|
||||
**Audience:** Senior software engineers
|
||||
**Scope:** Orders UI only — list page, detail page, all admin actions. No new backend work.
|
||||
**Backend reference:** `06-order-processing-backend-implementation-plan.md`
|
||||
**Design constraint:** Match the existing admin design language exactly (ShadCN only, HugeIcons, same layout shell, same patterns as Products page).
|
||||
|
||||
---
|
||||
|
||||
## 1. Design philosophy
|
||||
|
||||
Three principles guide every decision here:
|
||||
|
||||
1. **Single responsibility per component.** Each component owns one concern: a card, a table, a button, a timeline. No component manages both data display and action side-effects unless it is a leaf action component.
|
||||
2. **Status-driven UI.** The order's `status` field is the single source of truth. What you see, what buttons appear, what badges render — all computed from status. No hidden state, no guessing.
|
||||
3. **Consistency over novelty.** Use the exact same patterns already in the codebase: `rounded-lg border` for table wrappers, `text-xl font-semibold` headings, `usePaginatedQuery` + "Load more", `AlertDialog` for destructive actions, `toast` (Sonner) for feedback.
|
||||
|
||||
---
|
||||
|
||||
## 2. Route structure
|
||||
|
||||
```
|
||||
/orders → OrdersPage (list)
|
||||
/orders/[id] → OrderDetailPage (detail)
|
||||
```
|
||||
|
||||
The detail page is a full Next.js route (`app/(dashboard)/orders/[id]/page.tsx`), not a dialog. Orders are complex enough to deserve their own page; a dialog would feel cramped.
|
||||
|
||||
---
|
||||
|
||||
## 3. Status vocabulary
|
||||
|
||||
Both pages share the same two badge components. Define color intent once, reuse everywhere.
|
||||
|
||||
### 3.1 Order status badges
|
||||
|
||||
| Status | Badge variant | Tailwind override | Meaning |
|
||||
|--------|--------------|-------------------|---------|
|
||||
| `pending` | `secondary` | — | Payment not yet confirmed |
|
||||
| `confirmed` | `outline` | `border-blue-500 text-blue-600` | Paid, awaiting fulfilment |
|
||||
| `processing` | `outline` | `border-violet-500 text-violet-600` | Label created, packing |
|
||||
| `shipped` | `outline` | `border-indigo-500 text-indigo-600` | In transit |
|
||||
| `delivered` | `default` | `bg-green-600` | Delivered |
|
||||
| `cancelled` | `destructive` | — | Cancelled |
|
||||
| `refunded` | `secondary` | `line-through` | Refunded |
|
||||
|
||||
### 3.2 Payment status badges
|
||||
|
||||
| Status | Badge variant | Tailwind override |
|
||||
|--------|--------------|-------------------|
|
||||
| `pending` | `secondary` | — |
|
||||
| `paid` | `default` | `bg-green-600` |
|
||||
| `failed` | `destructive` | — |
|
||||
| `refunded` | `secondary` | — |
|
||||
|
||||
Both mappings live in `apps/admin/src/components/orders/shared/statusConfig.ts` — a plain TypeScript constants file, no JSX.
|
||||
|
||||
---
|
||||
|
||||
## 4. Orders list page
|
||||
|
||||
**File:** `apps/admin/src/app/(dashboard)/orders/page.tsx`
|
||||
|
||||
### 4.1 Layout (top to bottom)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Orders │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ [🔍 Search by order # or email…] [Status ▾] │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ Order # │ Customer │ Date │ Status │ Total │ │
|
||||
│ │──────────┼──────────┼──────┼────────┼───────│ │
|
||||
│ │ ORD-AB12 │ Jane Doe │ … │ [Conf] │ £42 │ │
|
||||
│ └─────────────────────────────────────────────┘ │
|
||||
│ 12 loaded [Load more] │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 Data fetching
|
||||
|
||||
- **Default (no filter, no search):** `usePaginatedQuery(api.orders.listAll, {}, { initialNumItems: 25 })`
|
||||
- **Status filter active:** `usePaginatedQuery(api.orders.listAll, { status }, { initialNumItems: 25 })`
|
||||
- Pagination: "Load more" button + count footer — identical to ProductsPage.
|
||||
- No search query for now (orders list does not have a full-text search index). Search by order number or email is client-side filtered from the loaded page; add a note in the component that server-side search can be added when an index exists.
|
||||
|
||||
### 4.3 Table columns
|
||||
|
||||
| Column | Notes |
|
||||
|--------|-------|
|
||||
| Order # | `font-mono text-xs`; clicking the row navigates to detail |
|
||||
| Customer | `order.email`; `text-muted-foreground` |
|
||||
| Date | `createdAt` formatted short; `text-xs text-muted-foreground` |
|
||||
| Status | `<OrderStatusBadge status={order.status} />` |
|
||||
| Payment | `<OrderPaymentBadge status={order.paymentStatus} />` |
|
||||
| Total | `formatPrice(order.total)` right-aligned |
|
||||
|
||||
No actions menu column — clicking anywhere on the row navigates to the detail page. Keeps the list clean. A chevron icon on the right of each row communicates navigability.
|
||||
|
||||
### 4.4 Extracted components
|
||||
|
||||
| Component | File | Responsibility |
|
||||
|-----------|------|---------------|
|
||||
| `OrderStatusBadge` | `components/orders/shared/OrderStatusBadge.tsx` | Renders one `<Badge>` for order status |
|
||||
| `OrderPaymentBadge` | `components/orders/shared/OrderPaymentBadge.tsx` | Renders one `<Badge>` for payment status |
|
||||
| `OrdersTableSkeleton` | inline in list page | 10 skeleton rows, same columns |
|
||||
|
||||
### 4.5 Empty and loading states
|
||||
|
||||
- **Loading:** 10 skeleton rows with matching column widths.
|
||||
- **Empty (no orders):** Centered row: *"No orders yet."*
|
||||
- **Empty (filter applied):** *"No orders with status 'confirmed'."*
|
||||
|
||||
---
|
||||
|
||||
## 5. Order detail page
|
||||
|
||||
**File:** `apps/admin/src/app/(dashboard)/orders/[id]/page.tsx`
|
||||
|
||||
This page uses `useQuery(api.orders.getById, { id })` and `useQuery(api.orders.getTimeline, { orderId: id })` to load both the order and its timeline in parallel.
|
||||
|
||||
### 5.1 Layout
|
||||
|
||||
Two-column layout on `md+`, stacked on mobile. The left column is wider (about 60%) and holds the primary data. The right column holds contextual cards.
|
||||
|
||||
```
|
||||
┌──────────────────────┬───────────────────┐
|
||||
│ │ │
|
||||
│ ← Orders │ Customer │
|
||||
│ ORD-AB12 [Conf] │ Jane Doe │
|
||||
│ 14 Jan 2026 │ jane@example.com │
|
||||
│ ├───────────────────┤
|
||||
│ [Admin actions bar] │ Shipping address │
|
||||
│ │ 123 Main St… │
|
||||
├──────────────────────┤ │
|
||||
│ Order items ├───────────────────┤
|
||||
│ ────────────────── │ Fulfilment │
|
||||
│ Product Qty Total │ Carrier: DPD UK │
|
||||
│ … │ Service: Next Day│
|
||||
├──────────────────────┤ [Create Label] │
|
||||
│ Financials ├───────────────────┤
|
||||
│ Subtotal £38.00 │ │
|
||||
│ Shipping £4.99 │ (empty if no │
|
||||
│ Total £42.99 │ tracking yet) │
|
||||
├──────────────────────┤ │
|
||||
│ Timeline │ │
|
||||
│ ○ Order confirmed │ │
|
||||
│ ○ Label created │ │
|
||||
│ ○ Delivered │ │
|
||||
└──────────────────────┴───────────────────┘
|
||||
```
|
||||
|
||||
On mobile, right column cards stack below the left column, in this order: Customer → Shipping → Fulfilment.
|
||||
|
||||
### 5.2 Loading state
|
||||
|
||||
All cards render as `Skeleton` blocks while data is `undefined`. Each card has its own skeleton shape so the layout does not shift when data loads. No full-page spinner.
|
||||
|
||||
### 5.3 Component breakdown
|
||||
|
||||
Every component below has **one job**. None of them fetch data themselves — the page fetches and passes props down.
|
||||
|
||||
---
|
||||
|
||||
#### 5.3.1 `OrderPageHeader`
|
||||
|
||||
**File:** `components/orders/detail/OrderPageHeader.tsx`
|
||||
**Job:** Back link, order number, date, status badges.
|
||||
|
||||
```
|
||||
← Orders ORD-AB12 [Confirmed] [Paid] 14 Jan 2026, 10:22
|
||||
```
|
||||
|
||||
Props: `orderNumber`, `createdAt`, `status`, `paymentStatus`.
|
||||
No actions here. Badges are read-only. Headings use `text-xl font-semibold`.
|
||||
|
||||
---
|
||||
|
||||
#### 5.3.2 `OrderActionsBar`
|
||||
|
||||
**File:** `components/orders/detail/OrderActionsBar.tsx`
|
||||
**Job:** Render the correct set of action buttons based on the order's current status. This is the only component that "knows" which actions are valid — it reads `status`, `paymentStatus`, `trackingNumber`, `returnRequestedAt`, `returnReceivedAt` and decides what to render.
|
||||
|
||||
Action matrix:
|
||||
|
||||
| Condition | Buttons shown |
|
||||
|-----------|--------------|
|
||||
| `status === "confirmed"` and no `trackingNumber` | **Create Label** (primary), **Update Status** (outline) |
|
||||
| `status === "confirmed"` and `trackingNumber` exists | **Update Status** (outline) |
|
||||
| `status === "confirmed"` and `returnRequestedAt` and no `returnReceivedAt` | **Mark Return Received** (outline), **Update Status** |
|
||||
| `status === "confirmed"` and `returnReceivedAt` and `paymentStatus !== "refunded"` | **Issue Refund** (destructive), **Update Status** |
|
||||
| `status === "processing"` or `"shipped"` | **Update Status** only |
|
||||
| `status === "delivered"` and `paymentStatus !== "refunded"` | **Issue Refund** (destructive), **Update Status** |
|
||||
| `status === "cancelled"` or `"refunded"` | Read-only — no buttons |
|
||||
|
||||
**`OrderActionsBar` does not implement the action logic itself.** It renders the individual action components below. It is purely a conditional layout shell.
|
||||
|
||||
---
|
||||
|
||||
#### 5.3.3 `CreateLabelButton`
|
||||
|
||||
**File:** `components/orders/actions/CreateLabelButton.tsx`
|
||||
**Job:** Call `api.fulfillmentActions.createShippingLabel`, show loading spinner, surface structured errors.
|
||||
|
||||
- Uses `useAction(api.fulfillmentActions.createShippingLabel)`.
|
||||
- On click: sets loading, calls action, on success shows `toast.success("Label created. Tracking: {trackingNumber}")`, on structured error shows `toast.error(result.message)`, on thrown error shows generic toast.
|
||||
- Button label: "Create Label" → "Creating…" while loading.
|
||||
- Disabled while loading.
|
||||
- No AlertDialog — creating a label is not destructive.
|
||||
|
||||
---
|
||||
|
||||
#### 5.3.4 `UpdateStatusDialog`
|
||||
|
||||
**File:** `components/orders/actions/UpdateStatusDialog.tsx`
|
||||
**Job:** A `Dialog` containing a `Select` of valid next statuses. On confirm, calls `api.orders.updateStatus`.
|
||||
|
||||
- Triggered by "Update Status" button in `OrderActionsBar`.
|
||||
- Select shows all statuses except the current one.
|
||||
- Submit button shows spinner while mutating.
|
||||
- On success: `toast.success("Status updated to {status}")`.
|
||||
|
||||
---
|
||||
|
||||
#### 5.3.5 `MarkReturnReceivedButton`
|
||||
|
||||
**File:** `components/orders/actions/MarkReturnReceivedButton.tsx`
|
||||
**Job:** Call `api.orders.markReturnReceived` with an `AlertDialog` confirmation.
|
||||
|
||||
- AlertDialog title: *"Mark return as received?"*
|
||||
- Description: *"Confirm that the returned items have arrived. You can then issue a refund."*
|
||||
- Uses `useMutation(api.orders.markReturnReceived)`.
|
||||
- Shows spinner on the confirm button while loading.
|
||||
|
||||
---
|
||||
|
||||
#### 5.3.6 `IssueRefundButton`
|
||||
|
||||
**File:** `components/orders/actions/IssueRefundButton.tsx`
|
||||
**Job:** Call `api.returnActions.issueRefund` with an `AlertDialog` confirmation.
|
||||
|
||||
- AlertDialog title: *"Issue full refund?"*
|
||||
- Description: *"A full refund of £{order.total} will be sent to the customer via Stripe. This cannot be undone."*
|
||||
- Uses `useAction(api.returnActions.issueRefund)`.
|
||||
- Confirm button: destructive variant, "Issue Refund" → "Refunding…".
|
||||
- On success: `toast.success("Refund issued")`.
|
||||
- On error: `toast.error(err.message)`.
|
||||
|
||||
---
|
||||
|
||||
#### 5.3.7 `OrderItemsCard`
|
||||
|
||||
**File:** `components/orders/detail/OrderItemsCard.tsx`
|
||||
**Job:** Display the line items in a `Card` with a bordered table inside.
|
||||
|
||||
Columns: Product name (+ variant name below in `text-xs text-muted-foreground`), SKU (`font-mono text-xs`), Qty, Unit price, Total.
|
||||
No actions. Read-only.
|
||||
|
||||
```
|
||||
┌─ Order items ────────────────────────────────────┐
|
||||
│ Product SKU Qty Unit Total │
|
||||
│ Royal Canin… │
|
||||
│ Adult 2kg SKU-001 2 £19.00 £38.00 │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 5.3.8 `OrderFinancialsCard`
|
||||
|
||||
**File:** `components/orders/detail/OrderFinancialsCard.tsx`
|
||||
**Job:** Render the price breakdown.
|
||||
|
||||
Uses `InfoRow`-style layout (label left, value right) inside a `Card`. Subtotal, shipping, discount (if > 0), tax (if > 0), and a `Separator` before the bold total.
|
||||
|
||||
---
|
||||
|
||||
#### 5.3.9 `CustomerCard`
|
||||
|
||||
**File:** `components/orders/detail/CustomerCard.tsx`
|
||||
**Job:** Display the customer name and email.
|
||||
|
||||
`Card` with `CardHeader` (title "Customer") and `CardContent`. Email is a `mailto:` link styled as `text-sm text-muted-foreground hover:underline`.
|
||||
|
||||
---
|
||||
|
||||
#### 5.3.10 `ShippingAddressCard`
|
||||
|
||||
**File:** `components/orders/detail/ShippingAddressCard.tsx`
|
||||
**Job:** Render the `shippingAddressSnapshot` in a `Card`.
|
||||
|
||||
Displays each address line in `text-sm`, country in `text-xs text-muted-foreground`. No editing — it's a snapshot.
|
||||
|
||||
---
|
||||
|
||||
#### 5.3.11 `FulfilmentCard`
|
||||
|
||||
**File:** `components/orders/detail/FulfilmentCard.tsx`
|
||||
**Job:** Shipping service info + tracking state. No action buttons — those live in `OrderActionsBar`.
|
||||
|
||||
Three visual states:
|
||||
|
||||
**State A — No label yet (status: `confirmed`, no `trackingNumber`):**
|
||||
```
|
||||
┌─ Fulfilment ──────────────────────────────────┐
|
||||
│ Carrier DPD UK │
|
||||
│ Service Next Day │
|
||||
│ Method Express │
|
||||
│ │
|
||||
│ No label created yet. │
|
||||
└───────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**State B — Label created, in transit:**
|
||||
```
|
||||
┌─ Fulfilment ──────────────────────────────────┐
|
||||
│ Carrier DPD UK │
|
||||
│ Service Next Day │
|
||||
│ Tracking 1Z999AA1… [↗ Track] │
|
||||
│ Status TRANSIT │
|
||||
│ Est. delivery 18 Jan 2026 │
|
||||
│ Label [↗ Download] │
|
||||
└───────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**State C — Delivered:**
|
||||
```
|
||||
┌─ Fulfilment ──────────────────────────────────┐
|
||||
│ Carrier DPD UK │
|
||||
│ Tracking 1Z999AA1… [↗ Track] │
|
||||
│ Status DELIVERED │
|
||||
│ Delivered 17 Jan 2026, 14:35 │
|
||||
└───────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The `trackingStatus` field drives the displayed status string. Tracking and label URLs open in a new tab.
|
||||
|
||||
---
|
||||
|
||||
#### 5.3.12 `OrderTimelineCard`
|
||||
|
||||
**File:** `components/orders/detail/OrderTimelineCard.tsx`
|
||||
**Job:** Render `orderTimelineEvents` as a vertical stepper/timeline.
|
||||
|
||||
Receives the `events` array as a prop (fetched by the page).
|
||||
|
||||
Each event row:
|
||||
```
|
||||
○ Status changed: confirmed → processing 14 Jan · 10:35
|
||||
by admin
|
||||
```
|
||||
|
||||
Event type → human label mapping:
|
||||
|
||||
| `eventType` | Display label |
|
||||
|-------------|--------------|
|
||||
| `status_change` | "Status changed: {fromStatus} → {toStatus}" |
|
||||
| `label_created` | "Shipping label created" + tracking number from payload |
|
||||
| `tracking_update` | "Tracking update: {status}" |
|
||||
| `customer_cancel` | "Customer requested cancellation" |
|
||||
| `return_requested` | "Customer requested return" |
|
||||
| `return_received` | "Return marked as received" |
|
||||
| `refund` | "Refund issued" |
|
||||
|
||||
The dot (`○`) color matches the status badge color for `status_change` events; neutral for all others.
|
||||
|
||||
Loading state: 3–4 skeleton rows.
|
||||
Empty state: *"No activity yet."*
|
||||
|
||||
---
|
||||
|
||||
## 6. Shared utilities
|
||||
|
||||
**File:** `components/orders/shared/statusConfig.ts`
|
||||
|
||||
```typescript
|
||||
// Order status → { label, badgeVariant, className }
|
||||
// Payment status → { label, badgeVariant, className }
|
||||
// Event type → human label
|
||||
// Timeline dot color by toStatus
|
||||
```
|
||||
|
||||
Keep all display constants here. Both pages and all badge components import from this single file. No duplication.
|
||||
|
||||
---
|
||||
|
||||
## 7. File tree
|
||||
|
||||
```
|
||||
apps/admin/src/
|
||||
├── app/(dashboard)/
|
||||
│ └── orders/
|
||||
│ ├── page.tsx ← Orders list page
|
||||
│ └── [id]/
|
||||
│ └── page.tsx ← Order detail page
|
||||
└── components/
|
||||
└── orders/
|
||||
├── shared/
|
||||
│ ├── statusConfig.ts ← Constants only, no JSX
|
||||
│ ├── OrderStatusBadge.tsx
|
||||
│ └── OrderPaymentBadge.tsx
|
||||
├── detail/
|
||||
│ ├── OrderPageHeader.tsx
|
||||
│ ├── OrderActionsBar.tsx ← Layout shell, no action logic
|
||||
│ ├── OrderItemsCard.tsx
|
||||
│ ├── OrderFinancialsCard.tsx
|
||||
│ ├── CustomerCard.tsx
|
||||
│ ├── ShippingAddressCard.tsx
|
||||
│ ├── FulfilmentCard.tsx
|
||||
│ └── OrderTimelineCard.tsx
|
||||
└── actions/
|
||||
├── CreateLabelButton.tsx
|
||||
├── UpdateStatusDialog.tsx
|
||||
├── MarkReturnReceivedButton.tsx
|
||||
└── IssueRefundButton.tsx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Data flow
|
||||
|
||||
```
|
||||
OrderDetailPage (page.tsx)
|
||||
│
|
||||
│ useQuery(api.orders.getById, { id }) → order
|
||||
│ useQuery(api.orders.getTimeline, { orderId }) → events
|
||||
│
|
||||
├── OrderPageHeader (order)
|
||||
├── OrderActionsBar (order)
|
||||
│ ├── CreateLabelButton (orderId, order.status, order.trackingNumber)
|
||||
│ ├── UpdateStatusDialog (orderId, order.status)
|
||||
│ ├── MarkReturnReceivedButton (orderId)
|
||||
│ └── IssueRefundButton (orderId, order.total, order.currency)
|
||||
│
|
||||
├── [left column]
|
||||
│ ├── OrderItemsCard (order.items)
|
||||
│ ├── OrderFinancialsCard (order)
|
||||
│ └── OrderTimelineCard (events)
|
||||
│
|
||||
└── [right column]
|
||||
├── CustomerCard (order.email, order.userId)
|
||||
├── ShippingAddressCard (order.shippingAddressSnapshot)
|
||||
└── FulfilmentCard (order)
|
||||
```
|
||||
|
||||
All queries are read-only in the page. Action components call mutations/actions themselves and handle their own loading/error state. The page never passes mutation functions as props.
|
||||
|
||||
---
|
||||
|
||||
## 9. Implementation order
|
||||
|
||||
| Step | What to build | Why first |
|
||||
|------|---------------|-----------|
|
||||
| 1 | `statusConfig.ts`, `OrderStatusBadge`, `OrderPaymentBadge` | Shared atoms used by both pages |
|
||||
| 2 | Orders list page | Validates data fetching works, gives navigation to detail |
|
||||
| 3 | `OrderPageHeader`, `OrderItemsCard`, `OrderFinancialsCard` | Core detail page scaffold — read-only |
|
||||
| 4 | `CustomerCard`, `ShippingAddressCard`, `FulfilmentCard` | Right column — all read-only |
|
||||
| 5 | `OrderTimelineCard` | Needs timeline query wired |
|
||||
| 6 | `UpdateStatusDialog` | Lowest-risk action — any status |
|
||||
| 7 | `CreateLabelButton` | Most impactful Phase 1 action |
|
||||
| 8 | `MarkReturnReceivedButton`, `IssueRefundButton` | Phase 3/4 actions |
|
||||
| 9 | `OrderActionsBar` | Wire all actions together with condition logic |
|
||||
|
||||
Build read-only views first, add write actions last. This way the detail page is useful even before all actions are wired.
|
||||
|
||||
---
|
||||
|
||||
## 10. Design decisions and rationale
|
||||
|
||||
**Why a full page for detail, not a dialog?**
|
||||
Orders have 5+ cards, a timeline, and multiple action flows. A dialog forces vertical scrolling with no space for the two-column layout. A page also makes deep-linking (e.g. linking from a customer email) trivial.
|
||||
|
||||
**Why does `OrderActionsBar` not implement actions?**
|
||||
If it did, adding or changing one action would require modifying a large, complex component. With leaf action components, each action is independently testable and replaceable.
|
||||
|
||||
**Why no search on the list page?**
|
||||
The `orders` table has no full-text search index. Client-side filtering over 25 loaded rows is fine for a small operation, but it does not scale past the first page. A future iteration can add a `by_order_number` or `by_email` query when the need is clear.
|
||||
|
||||
**Why `useAction` for `createShippingLabel` and `issueRefund`?**
|
||||
Both are Convex actions (they call external APIs). `useAction` from `convex/react` is the correct hook — not `useMutation`.
|
||||
|
||||
**Why show `trackingStatus` in `FulfilmentCard` rather than a separate card?**
|
||||
Tracking info and fulfilment info are tightly related; an admin needs them together. Separating them would force eye-scanning across two cards to understand the delivery state.
|
||||
|
||||
---
|
||||
|
||||
*Document version: 1.0. UI only — no new backend work required.*
|
||||
Reference in New Issue
Block a user