Skip to content

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
Menunggu

States

StateBadgeColorNext Actions
pendingMenungguAmber bg, amber textApprove, Reject
approvedDisetujuiGreen bg, green text---
rejectedDitolakRed bg, red textResubmit (if allowed)
in_reviewDireviewBlue bg, blue textApprove, 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

AppContext
ho-financePurchase order approval, expense claims, adjustments
store-adminStock adjustment approval, petty cash requests
stock-app-webTransfer 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

RetailOS - Sistem ERP Retail Modern untuk Indonesia