Files
Behavision/desktop/frontend/src/views/Customers.jsx
Suriyakumarvijayanayagam 7021d5d2f5 The shop app gets its help panel, and its last two old screens catch up
Ask Behavision: a panel beside any screen that talks to the head-office
assistant as the signed-in user - setup questions and 'is my shop
working' answered by the same thing, without leaving the app. The
assistant's prompt now knows how the product is set up (installation
codes, adding a camera, what a placement verdict means, the model
download on first run), so it is the help and not only the analyst. A
PC running on its own has nobody to ask and gets the essentials as text.

Cameras and Customers were still on the pre-redesign markup - the add
camera drawer ran off the right edge of the window because it used a
class the new stylesheet never sized. Both are rebuilt: cameras as
picture-led cards with connection and 'proven' as two separate claims
and a placement check laid out as the two steps it is; the customer
record as a proper sheet.

mock.js renders the app in a browser with fake bindings
(?mock=fresh|standalone|claimed, dev server only), so a screen can be
put in front of somebody without a Windows build. It is how these were
reviewed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
2026-09-19 13:22:16 +05:30

100 lines
3.9 KiB
JavaScript

import { useState } from 'react'
import { api } from '../bridge.js'
import { usePolled, fmtDate } from '../hooks.js'
import * as Icon from '../ui/icons.jsx'
import CustomerForm from './CustomerForm.jsx'
// The customer database, and the form staff fill in when someone walks in.
export default function Customers({ session }) {
const [query, setQuery] = useState('')
const [selected, setSelected] = useState(null)
const { data, error, reload } = usePolled(
() => api.customers(query, 200), 30000, [query])
const rows = data ?? []
const named = rows.filter(r => r.has_profile).length
return (
<div className="page">
<header className="pagehead">
<div>
<h2>Customers</h2>
<p>Everyone this business has recognised. Fill in details once and they are known at every store.</p>
</div>
<label className="search">
<Icon.Search size={15} />
<input placeholder="Search by name or phone" value={query} onChange={e => setQuery(e.target.value)} />
</label>
</header>
{error && <div className="err"><Icon.Warning size={15} />{error}</div>}
<div className="metrics" style={{ marginBottom: 'var(--sp-4)' }}>
<Metric k="Known people" v={rows.length || '—'} />
<Metric k="With details" v={named || '—'} s={rows.length ? `${Math.round(100 * named / rows.length)}% captured` : null} />
<Metric k="Returning" v={rows.filter(r => r.visit_count > 1).length || '—'} />
<Metric k="With consent" v={rows.filter(r => r.has_consent).length || '—'} />
</div>
<div className="panel">
{rows.length === 0
? <div className="empty tall">
<Icon.NoFaces size={34} />
<b>No customers yet</b>
<p>They appear here the first time a camera recognises them. Click one to add a name and phone number.</p>
</div>
: <div className="tablewrap">
<table>
<thead>
<tr>
<th>Customer</th><th>Phone</th>
<th className="num">Visits</th>
<th>First seen</th><th>Last seen</th><th>Details</th>
</tr>
</thead>
<tbody>
{rows.map(c => (
<tr key={c.id} className="click" onClick={() => setSelected(c)}>
<td>
{c.full_name || <span className="note">{c.label}</span>}
{/* Only beside a name a human typed: the auto label
already IS the number ("Visitor 13"), so showing
both reads as two identifiers for one person. */}
{c.full_name && c.ref &&
<span className="note"> · {c.ref}</span>}
</td>
<td className="mono">{c.phone || '—'}</td>
<td className="num">{c.visit_count}</td>
<td>{fmtDate(c.first_seen_at)}</td>
<td>{fmtDate(c.last_seen_at)}</td>
<td>
{c.has_profile
? <span className="pill ok"><i className="dot ok" />captured</span>
: <span className="pill warn"><i className="dot warn" />needed</span>}
</td>
</tr>
))}
</tbody>
</table>
</div>}
</div>
{selected && (
<CustomerForm customer={selected} session={session}
onClose={() => setSelected(null)}
onSaved={() => { setSelected(null); reload() }} />
)}
</div>
)
}
function Metric({ k, v, s }) {
return (
<div className="metric">
<div className="metric-k">{k}</div>
<div className="metric-v">{v}</div>
{s && <div className="metric-s">{s}</div>}
</div>
)
}