'use client'; import { useState } from 'react'; import { useRouter } from 'next/navigation'; import { AlertTriangle, Camera, CheckCircle2, X } from 'lucide-react'; import { shrinkImage } from '@/lib/shrink-image'; import { rand } from '@/lib/cash-float'; /** * Photograph a cash-card slip, fill in what it was for, and it comes off the card's balance. * The first time ever, the card's opening balance is asked for. */ export function SlipCapture({ hasOpening, balance, costCodes, suppliers }: { hasOpening: boolean; balance: number; costCodes: string[]; suppliers: string[] }) { const router = useRouter(); const [photo, setPhoto] = useState(null); const [preview, setPreview] = useState(''); const [supplier, setSupplier] = useState(''); const [costCode, setCostCode] = useState(''); const [comment, setComment] = useState(''); const [amount, setAmount] = useState(''); const [opening, setOpening] = useState(''); const [busy, setBusy] = useState(false); const [error, setError] = useState(''); const [message, setMessage] = useState(''); async function picked(file: File | undefined) { if (!file) return; const small = await shrinkImage(file, 'slip.jpg', 1800); setPhoto(small); setPreview(URL.createObjectURL(small)); setSupplier(''); setCostCode(''); setComment(''); setAmount(''); setError(''); setMessage(''); } function close() { if (!busy) { setPhoto(null); setPreview(''); } } async function saveOpening(e: React.FormEvent) { e.preventDefault(); setBusy(true); setError(''); try { const res = await fetch('/api/clock/cash-float/opening', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: opening }) }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || 'Could not save.'); router.refresh(); } catch (err) { setError(err instanceof Error ? err.message : 'Could not save.'); } finally { setBusy(false); } } async function saveSlip(e: React.FormEvent) { e.preventDefault(); if (!photo) return; setBusy(true); setError(''); try { const body = new FormData(); body.set('slip_photo', photo); body.set('supplier', supplier); body.set('cost_code', costCode); body.set('comment', comment); body.set('amount', amount); const res = await fetch('/api/clock/slip', { method: 'POST', body }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || 'Could not save.'); setMessage(`Slip for ${rand(data.amount)} saved. Card balance: ${rand(data.balance)}.`); setPhoto(null); setPreview(''); router.refresh(); navigator.vibrate?.(120); } catch (err) { setError(err instanceof Error ? err.message : 'Could not save.'); } finally { setBusy(false); } } return

MEGA PLANT

Slips

{message &&

{message}

} {!hasOpening ?

First time: set up the card

What is the balance on the card right now? Every slip you photograph will come off this amount.

setOpening(e.target.value)} placeholder="0.00"/> {error &&

{error}

}
: <>

Card balance

{rand(balance)}

{ void picked(e.target.files?.[0]); e.target.value = ''; }}/>

Take a clear photo of the slip. You will then fill in the supplier, cost code, comments and amount.

} {photo &&
e.stopPropagation()} onSubmit={saveSlip} role="dialog" aria-modal="true" aria-label="Slip details" className="w-full max-w-md max-h-[94svh] overflow-y-auto rounded-t-2xl sm:rounded-2xl bg-surface p-4 sm:p-5 shadow-xl">

Slip details

Photo of the slip setSupplier(e.target.value)} maxLength={200} autoComplete="off" placeholder="e.g. Builders Warehouse"/> {suppliers.map(s => setCostCode(e.target.value)} maxLength={100} autoComplete="off" autoCapitalize="characters" placeholder="e.g. TLB04"/> {costCodes.map(c => setAmount(e.target.value)} placeholder="0.00"/>