Shift Flow
The shift flow pattern manages cashier shift open and close procedures in the POS application. It includes cashier authentication, cash counting, and variance reporting.
Buka Shift Baru
RS
Rina Sari
Kasir — Shift Pagi
Kas Awal
Rp 500.000
Rp 1.000.000
Rp 1.500.000
Rp 2.000.000
Shift Open Flow
Step 1: Cashier Selection
+---------------------------------------------+
| Buka Shift Baru |
| |
| Pilih kasir: |
| +---+ +---+ +---+ +---+ |
| |Ava| |Ava| |Ava| |Ava| |
| |img| |img| |img| |img| |
| +---+ +---+ +---+ +---+ |
| Rina Budi Dewi Anto |
| |
| [Authenticate with fingerprint] |
+---------------------------------------------+- Cashier avatars: 64px circles with name below
- Selected state: green border ring (3px), slight scale-up (1.05)
- Authentication: WebAuthn biometric prompt (fingerprint/face)
Step 2: Starting Cash
+---------------------------------------------+
| Kas Awal |
| |
| Preset amounts: |
| [Rp 200.000] [Rp 300.000] [Rp 500.000] |
| |
| Atau masukkan jumlah: |
| [Rp _______________] |
| |
| [Buka Shift] |
+---------------------------------------------+- Preset buttons:
bg-inset,border,rounded-lg,font-mono, 14px - Selected preset:
bg-green-light,border-brand - Custom input:
font-mono, 16px - Open button: full-width primary green
Shift Close Flow
Step 1: Denomination Helper
The denomination helper lets cashiers count each bill and coin type:
+---------------------------------------------+
| Hitung Kas |
| |
| Rp 100.000 x [ 5 ] = Rp 500.000 |
| Rp 50.000 x [ 3 ] = Rp 150.000 |
| Rp 20.000 x [ 8 ] = Rp 160.000 |
| Rp 10.000 x [ 4 ] = Rp 40.000 |
| Rp 5.000 x [ 2 ] = Rp 10.000 |
| Rp 2.000 x [ 1 ] = Rp 2.000 |
| Rp 1.000 x [ 0 ] = Rp 0 |
| ────────── |
| Total Dihitung: Rp 862.000 |
| |
| [Lanjut] |
+---------------------------------------------+| Element | Style |
|---|---|
| Denomination label | font-mono, 14px, text-1, left |
| Count input | font-mono, 14px, centered, width 64px, border, rounded-lg |
| Subtotal | font-mono, 14px, text-2, right |
| Grand total | font-mono, 18px, font-bold, text-1 |
Count inputs use + / - stepper buttons on mobile, direct number entry on desktop.
Step 2: Cash Summary
+---------------------------------------------+
| Ringkasan Shift |
| |
| Kas Awal Rp 300.000 |
| Penjualan Tunai Rp 562.000 |
| Pengeluaran Rp 0 |
| ────────── |
| Seharusnya Rp 862.000 |
| Dihitung Rp 860.000 |
| ────────── |
| Selisih Rp -2.000 [!] |
| |
| Catatan: [_______________________] |
| |
| [Tutup Shift] |
+---------------------------------------------+Variance Display
| Variance | Style | Icon |
|---|---|---|
| Zero | text-success, "Pas" | CheckCircle green |
| Within tolerance (< Rp 5.000) | text-warning | AlertTriangle amber |
| Over tolerance | text-danger, bold | AlertCircle red |
Tolerance threshold is configurable per store.
React Component
tsx
interface Denomination {
value: number
label: string
}
const IDR_DENOMINATIONS: Denomination[] = [
{ value: 100000, label: 'Rp 100.000' },
{ value: 50000, label: 'Rp 50.000' },
{ value: 20000, label: 'Rp 20.000' },
{ value: 10000, label: 'Rp 10.000' },
{ value: 5000, label: 'Rp 5.000' },
{ value: 2000, label: 'Rp 2.000' },
{ value: 1000, label: 'Rp 1.000' },
{ value: 500, label: 'Rp 500' },
{ value: 200, label: 'Rp 200' },
{ value: 100, label: 'Rp 100' },
]
export function DenominationHelper({ onComplete }: { onComplete: (total: number) => void }) {
const [counts, setCounts] = useState<Record<number, number>>(
Object.fromEntries(IDR_DENOMINATIONS.map(d => [d.value, 0]))
)
const total = IDR_DENOMINATIONS.reduce((sum, d) => sum + d.value * (counts[d.value] ?? 0), 0)
return (
<div className="space-y-3">
{IDR_DENOMINATIONS.map(d => (
<div key={d.value} className="flex items-center gap-3">
<span className="font-mono text-sm text-1 w-24">{d.label}</span>
<span className="text-2 text-sm">x</span>
<input
type="number"
min={0}
value={counts[d.value]}
onChange={e => setCounts(prev => ({ ...prev, [d.value]: parseInt(e.target.value) || 0 }))}
className="w-16 text-center font-mono text-sm border border-border rounded-lg py-1.5"
/>
<span className="text-sm text-2">=</span>
<span className="font-mono text-sm text-2 flex-1 text-right">
Rp {(d.value * (counts[d.value] ?? 0)).toLocaleString('id-ID')}
</span>
</div>
))}
<div className="border-t border-border pt-3 flex justify-between items-center">
<span className="font-semibold text-1">Total Dihitung</span>
<span className="font-mono text-lg font-bold text-1">
Rp {total.toLocaleString('id-ID')}
</span>
</div>
<button
onClick={() => onComplete(total)}
className="w-full py-3 bg-brand text-white font-semibold rounded-xl hover:bg-brand-hover transition-colors"
>
Lanjut
</button>
</div>
)
}Sound & Haptics
| Event | Sound | Haptic |
|---|---|---|
| Shift opened | Success chime | Medium vibration |
| Shift closed (no variance) | Success chime | Medium vibration |
| Shift closed (with variance) | Warning tone | Heavy vibration |
Security
Shift close always requires the same cashier who opened the shift to authenticate via WebAuthn. A supervisor override is available with separate authentication and audit logging.
Don't
- Don't allow closing a shift without counting cash
- Don't hide the variance amount --- transparency prevents disputes
- Don't skip the denomination helper on mobile --- it catches counting errors