Approval Flow
The approval flow pattern handles multi-step review and authorization of items such as purchase orders, adjustments, refunds, and expense claims.
RS
Rina Sari
Pengajuan penyesuaian stok — 12 item, total Rp 2.450.000
States
| State | Badge | Color | Next Actions |
|---|---|---|---|
pending | Menunggu | Amber bg, amber text | Approve, Reject |
approved | Disetujui | Green bg, green text | --- |
rejected | Ditolak | Red bg, red text | Resubmit (if allowed) |
in_review | Direview | Blue bg, blue text | Approve, Reject |
Anatomy
The approval flow consists of three sections:
1. Summary Header
A card at the top showing the request summary:
+----------------------------------------------------------+
| PO-2026-0342 Status: [Menunggu] |
| Supplier: PT Maju Bersama |
| Total: Rp 24.500.000 | Dibuat: 22 Mar 2026 |
| Dibuat oleh: Rina Sari |
+----------------------------------------------------------+2. Items List
A data table showing the line items being approved. Each row has:
- Item description (text)
- Quantity (mono, right-aligned)
- Unit price (mono, right-aligned)
- Total (mono, right-aligned, bold)
3. Action Bar
Fixed at the bottom or inline, containing:
- Reject button: destructive ghost style (red text, no fill)
- Approve button: primary filled (green)
- Optional Note text area that expands on click
Usage
| App | Context |
|---|---|
| ho-finance | Purchase order approval, expense claims, adjustments |
| store-admin | Stock adjustment approval, petty cash requests |
| stock-app-web | Transfer requests, return authorizations |
React Component
tsx
interface ApprovalItem {
id: string
description: string
qty: number
unitPrice: number
total: number
}
interface ApprovalFlowProps {
requestId: string
status: 'pending' | 'approved' | 'rejected' | 'in_review'
items: ApprovalItem[]
submittedBy: string
submittedAt: string
onApprove: (note?: string) => void
onReject: (note: string) => void
}
export function ApprovalFlow({ requestId, status, items, submittedBy, submittedAt, onApprove, onReject }: ApprovalFlowProps) {
const [note, setNote] = useState('')
const [showNote, setShowNote] = useState(false)
const total = items.reduce((sum, item) => sum + item.total, 0)
return (
<div className="space-y-4">
{/* Summary Header */}
<div className="bg-card border border-border rounded-xl p-5">
<div className="flex items-center justify-between mb-3">
<h2 className="font-mono text-lg font-semibold">{requestId}</h2>
<StatusBadge status={status} />
</div>
<div className="grid grid-cols-2 gap-2 text-sm text-2">
<p>Dibuat oleh: <span className="text-1 font-medium">{submittedBy}</span></p>
<p>Tanggal: <span className="font-mono">{submittedAt}</span></p>
<p>Total: <span className="font-mono font-semibold text-1">Rp {total.toLocaleString('id-ID')}</span></p>
</div>
</div>
{/* Items Table */}
<div className="border border-border rounded-xl overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="bg-inset">
<th className="text-left px-4 h-10 text-[11px] uppercase tracking-wider text-2">Item</th>
<th className="text-right px-4 text-[11px] uppercase tracking-wider text-2">Qty</th>
<th className="text-right px-4 text-[11px] uppercase tracking-wider text-2">Harga</th>
<th className="text-right px-4 text-[11px] uppercase tracking-wider text-2">Total</th>
</tr>
</thead>
<tbody>
{items.map(item => (
<tr key={item.id} className="border-b border-border">
<td className="px-4 py-3">{item.description}</td>
<td className="px-4 py-3 text-right font-mono">{item.qty}</td>
<td className="px-4 py-3 text-right font-mono">Rp {item.unitPrice.toLocaleString('id-ID')}</td>
<td className="px-4 py-3 text-right font-mono font-semibold">Rp {item.total.toLocaleString('id-ID')}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Action Bar */}
{status === 'pending' && (
<div className="flex items-center justify-end gap-3 pt-2">
<button
onClick={() => note ? onReject(note) : setShowNote(true)}
className="px-4 py-2 text-sm font-semibold text-danger hover:bg-red-50 rounded-lg transition-colors"
>
Tolak
</button>
<button
onClick={() => onApprove(note || undefined)}
className="px-4 py-2 text-sm font-semibold text-white bg-brand hover:bg-brand-hover rounded-lg transition-colors"
>
Setujui
</button>
</div>
)}
</div>
)
}Multi-Level Approval
For requests requiring multiple approvers, show an approval timeline:
[1] Store Manager --- Disetujui (22 Mar, 09:15) [check]
[2] Area Manager --- Menunggu [clock]
[3] Finance Director --- Belum [gray dot]Each level shows: role, status, timestamp (if acted on), and icon.
Notifications
Pending approvals should trigger push notifications via the RetailOS notification system. The approver sees a badge count on their portal sidebar.
Don't
- Don't allow approval without viewing all line items
- Don't hide the reject reason field --- always require a note for rejections
- Don't auto-approve based on amount thresholds without explicit configuration