Skip to content

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]                        |
+---------------------------------------------+
ElementStyle
Denomination labelfont-mono, 14px, text-1, left
Count inputfont-mono, 14px, centered, width 64px, border, rounded-lg
Subtotalfont-mono, 14px, text-2, right
Grand totalfont-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

VarianceStyleIcon
Zerotext-success, "Pas"CheckCircle green
Within tolerance (< Rp 5.000)text-warningAlertTriangle amber
Over tolerancetext-danger, boldAlertCircle 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

EventSoundHaptic
Shift openedSuccess chimeMedium vibration
Shift closed (no variance)Success chimeMedium vibration
Shift closed (with variance)Warning toneHeavy 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

RetailOS - Sistem ERP Retail Modern untuk Indonesia