Files
Behavision/desktop/frontend/src/views/CustomerForm.jsx
Suriyakumarvijayanayagam 3598d8e9c0 The shop PC's avatar had the same V1 collision
Fixed on the web arrivals feed and not here, which is the failure this
codebase already warns about: two surfaces disagreeing about one fact.
Taking the first letter of each word of "Visitor 13" gives "V1" - and so
do "Visitor 10" and "Visitor 15", so three different customers wear the
same badge and it reads as the V-1 reference for a fourth.

Shows the number itself, same rule as the web app. customerRef, not
ref: React reserves that prop name and it would never arrive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
2026-09-07 12:45:38 +05:30

184 lines
7.5 KiB
JavaScript

import { useState } from 'react'
import { api, message } from '../bridge.js'
import CustomerPhoto, { useCustomerPhoto } from './CustomerPhoto.jsx'
import VisitHistory from './VisitHistory.jsx'
import EraseCustomer from './EraseCustomer.jsx'
// The in-store form. Two jobs in one sheet: capture who this person is, and
// record what they bought — because staff have the customer in front of them
// once, and asking them to open a second screen means the sale never gets
// recorded.
export default function CustomerForm({ customer, session, onClose, onSaved }) {
const [f, setF] = useState({
full_name: customer.full_name ?? '',
phone: customer.phone ?? '',
email: customer.email ?? '',
gender: '',
date_of_birth: '',
notes: '',
consent: customer.has_consent ?? false,
})
const [purchase, setPurchase] = useState({ amount: '', items: '', notes: '' })
const [busy, setBusy] = useState(false)
const [error, setError] = useState(null)
const [erasing, setErasing] = useState(false)
const [photo, setPhoto] = useState(null)
const fetched = useCustomerPhoto(customer.id)
const shown = photo ?? fetched
// Erasure destroys a record permanently, so it is a manager's decision. The
// server enforces this too — this only avoids offering a button that would
// come back 403.
const role = session?.user?.role
const canErase = ['admin', 'owner', 'manager'].includes(role)
const set = k => e => setF({ ...f, [k]: e.target.value })
async function save(e) {
e.preventDefault()
setBusy(true); setError(null)
try {
await api.saveProfile({ visitor_id: customer.id, ...f })
const amount = parseFloat(purchase.amount)
if (!isNaN(amount) && amount > 0) {
const items = purchase.items.split(',').map(s => s.trim()).filter(Boolean)
await api.recordPurchase(customer.id, amount, items, purchase.notes)
}
onSaved()
} catch (err) {
setError(message(err))
} finally {
setBusy(false)
}
}
return (
<div className="drawer" onMouseDown={e => e.target === e.currentTarget && onClose()}>
<div className="panel">
<div className="who">
<CustomerPhoto photo={shown} name={customer.full_name || customer.label}
customerRef={customer.ref}
onBroken={() => setPhoto({ available: false,
reason: 'The photo could not be loaded.' })} />
<div className="grow">
<h3>{customer.full_name || customer.label}</h3>
<p className="note">
{customer.visit_count} visit{customer.visit_count === 1 ? '' : 's'}
{customer.last_seen_at && ` · last seen ${new Date(customer.last_seen_at).toLocaleDateString()}`}
</p>
{shown && !shown.available && shown.reason &&
<p className="note">{shown.reason}</p>}
</div>
<button type="button" className="btn sm" onClick={onClose}>Close</button>
</div>
<form onSubmit={save}>
{error && <div className="err">{error}</div>}
<div className="card" style={{ marginBottom: 14 }}>
<h3>Customer details</h3>
<label className="field">
<span>Full name</span>
<input value={f.full_name} onChange={set('full_name')} autoFocus />
</label>
<div className="fieldrow">
<label className="field">
<span>Phone</span>
<input value={f.phone} onChange={set('phone')} inputMode="tel" />
</label>
<label className="field">
<span>Email</span>
<input value={f.email} onChange={set('email')} type="email" />
</label>
</div>
<div className="fieldrow">
<label className="field">
<span>Gender</span>
<select value={f.gender} onChange={set('gender')}>
<option value="">Not recorded</option>
<option>Female</option><option>Male</option><option>Other</option>
</select>
</label>
<label className="field">
<span>Date of birth</span>
<input type="date" value={f.date_of_birth} onChange={set('date_of_birth')} />
</label>
</div>
<label className="field">
<span>Notes</span>
<textarea value={f.notes} onChange={set('notes')}
placeholder="Preferences, sizes, anything worth remembering" />
</label>
</div>
<div className="card" style={{ marginBottom: 14 }}>
<h3>Purchase (optional)</h3>
<div className="fieldrow">
<label className="field">
<span>Amount</span>
<input value={purchase.amount} inputMode="decimal" placeholder="0.00"
onChange={e => setPurchase({ ...purchase, amount: e.target.value })} />
</label>
<label className="field">
<span>Items</span>
<input value={purchase.items} placeholder="shirt, belt"
onChange={e => setPurchase({ ...purchase, items: e.target.value })} />
</label>
</div>
<p className="note">Leave the amount blank if they did not buy anything —
a visit without a sale is still worth recording.</p>
</div>
<div className="card" style={{ marginBottom: 16 }}>
<h3>Consent</h3>
<label style={{ display: 'flex', gap: 10, alignItems: 'flex-start',
fontSize: 13, cursor: 'pointer' }}>
<input type="checkbox" checked={f.consent} style={{ marginTop: 3 }}
onChange={e => setF({ ...f, consent: e.target.checked })} />
<span>This customer agreed to us keeping their details and
recognising them on future visits.</span>
</label>
<p className="note" style={{ marginTop: 10 }}>
Recorded with the date and who collected it. They can withdraw it
at any time, which erases their face data.
</p>
</div>
<div className="card" style={{ marginBottom: 14 }}>
<h3>Visits</h3>
<VisitHistory customer={customer} />
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button className="btn primary" disabled={busy}>
{busy ? 'Saving…' : 'Save'}
</button>
<button type="button" className="btn" onClick={onClose}>Cancel</button>
</div>
{canErase && (
<div className="card danger-zone">
<h3>At the customer's request</h3>
{erasing
? <EraseCustomer customer={customer}
onCancel={() => setErasing(false)}
onDone={onSaved} />
: <>
<p className="note">
Erase this person's face data, photo and details. Their
past visits stay in your footfall figures, without their
name.
</p>
<button type="button" className="btn danger"
onClick={() => setErasing(true)}>
Erase this customer…
</button>
</>}
</div>
)}
</form>
</div>
</div>
)
}