A shop can be renamed and, while empty, removed - from head office
The display name was always meant to be editable and the slug frozen;
until now neither had a way in. PATCH /api/sites/{site} takes a name
and a timezone (manager and above), DELETE removes an empty shop
(owner). The shop drawer in head office gets both, with the short name
shown read-only and the reason beside it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
This commit is contained in:
@@ -206,6 +206,8 @@ export const api = {
|
||||
|
||||
sites: () => send('GET', '/api/sites'),
|
||||
createSite: (input) => send('POST', '/api/sites', input),
|
||||
updateSite: (site, input) => send('PATCH', `/api/sites/${encodeURIComponent(site)}`, input),
|
||||
deleteSite: (site) => send('DELETE', `/api/sites/${encodeURIComponent(site)}`),
|
||||
|
||||
// The live arrivals feed. `cursor` is opaque and must be echoed back.
|
||||
arrivals: (params) => send('GET', '/api/visits' + qs(params)),
|
||||
|
||||
@@ -568,3 +568,7 @@ button.ghost.danger:hover { border-color: var(--bad); }
|
||||
letter-spacing: .04em; text-transform: uppercase; }
|
||||
|
||||
.ask-btn .mark { width: 18px; height: 18px; }
|
||||
|
||||
.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||||
button.ghost.danger { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 40%, transparent); }
|
||||
input[readonly] { opacity: .6; }
|
||||
|
||||
@@ -8,7 +8,7 @@ import { api } from '../api.js'
|
||||
// recognising almost nobody. Every step names what to do when it fails, and the
|
||||
// shop is only "working" when all of them pass: a partial pass is not a working
|
||||
// shop, and calling it one is how that site got signed off.
|
||||
export default function SiteCheck({ site, onClose }) {
|
||||
export default function SiteCheck({ site, user, onClose, onChanged }) {
|
||||
const [result, setResult] = useState(null)
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
@@ -89,6 +89,7 @@ export default function SiteCheck({ site, onClose }) {
|
||||
)}
|
||||
|
||||
<ClaimPC site={site} />
|
||||
<ShopSettings site={site} user={user} onChanged={onChanged} onClose={onClose} />
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
@@ -169,3 +170,74 @@ function expiry(iso) {
|
||||
if (days <= 0) return 'today'
|
||||
return days === 1 ? 'tomorrow' : `in ${days} days`
|
||||
}
|
||||
|
||||
// The shop's own details: rename, timezone, and - for a shop opened by mistake
|
||||
// - removal. The short name is shown but not editable: it is what the shop PC
|
||||
// calls itself and a segment of the broker topic, so renaming it would orphan
|
||||
// both. The display name is what people read, and a system that cannot fix a
|
||||
// typo in a shop's name has confused the two.
|
||||
function ShopSettings({ site, user, onChanged, onClose }) {
|
||||
const [name, setName] = useState(site.name)
|
||||
const [tz, setTz] = useState(site.timezone || 'Asia/Kolkata')
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [confirming, setConfirming] = useState(false)
|
||||
const canManage = user?.role === 'owner' || user?.role === 'manager'
|
||||
const isOwner = user?.role === 'owner'
|
||||
if (!canManage) return null
|
||||
const dirty = name.trim() !== site.name || tz.trim() !== (site.timezone || 'Asia/Kolkata')
|
||||
|
||||
const save = async (e) => {
|
||||
e.preventDefault()
|
||||
setBusy(true); setError('')
|
||||
try {
|
||||
await api.updateSite(site.slug || site.site_id, { name: name.trim(), timezone: tz.trim() })
|
||||
onChanged?.()
|
||||
} catch (err) { setError(err.message) } finally { setBusy(false) }
|
||||
}
|
||||
const remove = async () => {
|
||||
setBusy(true); setError('')
|
||||
try {
|
||||
await api.deleteSite(site.slug || site.site_id)
|
||||
onChanged?.(); onClose?.()
|
||||
} catch (err) { setError(err.message); setConfirming(false) } finally { setBusy(false) }
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="claim">
|
||||
<h3>Shop details</h3>
|
||||
<form onSubmit={save}>
|
||||
<label className="field">
|
||||
<span>Name</span>
|
||||
<input value={name} onChange={e => setName(e.target.value)} required />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>Short name</span>
|
||||
<input value={site.slug} readOnly />
|
||||
<span className="hint">Fixed: the shop PC and the broker are keyed on it.</span>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>Timezone</span>
|
||||
<input value={tz} onChange={e => setTz(e.target.value)} />
|
||||
</label>
|
||||
{error && <p className="error" role="alert">{error}</p>}
|
||||
<div className="row">
|
||||
<button className="primary" disabled={busy || !dirty}>{busy ? 'Saving…' : 'Save'}</button>
|
||||
{isOwner && !confirming && (
|
||||
<button type="button" className="ghost danger" onClick={() => setConfirming(true)}>Remove this shop…</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
{confirming && (
|
||||
<div className="banner warn" style={{ marginTop: 12 }}>
|
||||
<b>Remove {site.name}?</b>
|
||||
<p className="sub">Only possible while it has no cameras and no visits. A shop with history is kept.</p>
|
||||
<div className="row">
|
||||
<button className="primary" disabled={busy} onClick={remove}>{busy ? 'Removing…' : 'Yes, remove it'}</button>
|
||||
<button className="ghost" onClick={() => setConfirming(false)}>Keep it</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export default function Sites({ user }) {
|
||||
{/* Reachable per shop, at last. The smoke test used to hang off a single
|
||||
button on the camera screen that always checked sites[0], so with two
|
||||
shops the second could not be checked at all. */}
|
||||
{checking && <SiteCheck site={checking} onClose={() => setChecking(null)} />}
|
||||
{checking && <SiteCheck site={checking} user={user} onClose={() => setChecking(null)} onChanged={reload} />}
|
||||
{opening && <NewShop onClose={() => setOpening(false)}
|
||||
onCreated={() => { setOpening(false); reload() }} />}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user