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:
2026-09-19 16:11:07 +05:30
parent 4ac08e5a85
commit f88d441bbf
13 changed files with 210 additions and 18 deletions

View File

@@ -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>
)
}

View File

@@ -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() }} />}
</>