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
This commit is contained in:
2026-09-19 13:22:16 +05:30
parent 4c62fc0ef3
commit 7021d5d2f5
17 changed files with 677 additions and 253 deletions

View File

@@ -1,6 +1,7 @@
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.
@@ -15,34 +16,32 @@ export default function Customers({ session }) {
return (
<div className="page">
<header>
<h2>Customers</h2>
<p>Everyone this business has recognised. Fill in details once and they
are known at every store.</p>
<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">{error}</div>}
{error && <div className="err"><Icon.Warning size={15} />{error}</div>}
<div className="grid cols-4" style={{ marginBottom: 16 }}>
<Stat label="Known people" value={rows.length || '—'} />
<Stat label="With details" value={named || '—'}
sub={rows.length ? `${Math.round(100 * named / rows.length)}% captured` : null} />
<Stat label="Returning" value={rows.filter(r => r.visit_count > 1).length || '—'} />
<Stat label="With consent" value={rows.filter(r => r.has_consent).length || '—'} />
<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="card">
<div style={{ display: 'flex', gap: 10, marginBottom: 12 }}>
<input className="field" style={{ flex: 1, margin: 0, background: 'var(--ground)',
border: '1px solid var(--line)', borderRadius: 6, padding: '8px 10px' }}
placeholder="Search by name or phone"
value={query} onChange={e => setQuery(e.target.value)} />
<button className="btn" onClick={reload}>Refresh</button>
</div>
<div className="panel">
{rows.length === 0
? <div className="empty">
No customers yet. They appear here the first time a camera sees them.
? <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>
@@ -89,11 +88,12 @@ export default function Customers({ session }) {
)
}
function Stat({ label, value, sub }) {
function Metric({ k, v, s }) {
return (
<div className="card stat">
<h3>{label}</h3><div className="value">{value}</div>
{sub && <div className="sub">{sub}</div>}
<div className="metric">
<div className="metric-k">{k}</div>
<div className="metric-v">{v}</div>
{s && <div className="metric-s">{s}</div>}
</div>
)
}