store profile status updated
This commit is contained in:
188
src/features/store-admin/StoreOpenControl.tsx
Normal file
188
src/features/store-admin/StoreOpenControl.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Switch } from '@astryxdesign/core/Switch';
|
||||
import { Button } from '@astryxdesign/core/Button';
|
||||
import { errorMessage } from '@/api/client';
|
||||
import { tenantsApi } from '@/api/tenants';
|
||||
import { queryKeys } from '@/queries/keys';
|
||||
|
||||
/**
|
||||
* Open or close this branch for the day.
|
||||
*
|
||||
* ── Why it sits in the page header ──────────────────────────────────────────
|
||||
*
|
||||
* This is reached for in a hurry — the power has gone, somebody is ill, the
|
||||
* delivery never came — and usually on a phone. A control for that cannot be
|
||||
* two taps into a Settings tab. Everything else on this page is edited once a
|
||||
* year; this is the one thing a shopkeeper touches on a bad morning.
|
||||
*
|
||||
* ── Closing is confirmed, opening is not ────────────────────────────────────
|
||||
*
|
||||
* Closing stops the shop taking money, and a mis-tap doing that silently is
|
||||
* the failure worth preventing. Opening again is harmless, so it is one tap.
|
||||
* The confirmation is also where the reopening date is offered, because
|
||||
* "closed until Monday" is the answer most people actually want and nobody
|
||||
* thinks to ask for it before they have pressed close.
|
||||
*
|
||||
* ── Not shown to a branch user ──────────────────────────────────────────────
|
||||
*
|
||||
* Gated by the caller. A branch user runs one shop floor; whether the business
|
||||
* trades today is the owner's call, and Nearle's.
|
||||
*/
|
||||
export function StoreOpenControl({
|
||||
tenantid,
|
||||
locationid,
|
||||
isOpen,
|
||||
closedUntil,
|
||||
}: {
|
||||
tenantid: number;
|
||||
locationid: number;
|
||||
/** The stored flag, not the computed one — this control sets what it shows. */
|
||||
isOpen: boolean;
|
||||
closedUntil?: string | undefined;
|
||||
}) {
|
||||
const client = useQueryClient();
|
||||
|
||||
/*
|
||||
What this control believes it is set to.
|
||||
|
||||
Local, because the record behind this page is fetched once and does not
|
||||
change under it. Without this the switch would read the same value before
|
||||
and after a tap, and a save that worked would look exactly like one that
|
||||
did not.
|
||||
*/
|
||||
const [open, setOpen] = useState(isOpen);
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
const [until, setUntil] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Re-seed if the page refetches with a different answer, but never while the
|
||||
// confirmation is up — that would move the switch under the shopkeeper's hand.
|
||||
useEffect(() => {
|
||||
if (!confirming) setOpen(isOpen);
|
||||
}, [isOpen, confirming]);
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: (next: { isopen: boolean; closeduntil?: string }) =>
|
||||
tenantsApi.setStoreOpen({ tenantid, locationid, ...next }),
|
||||
onMutate: () => setError(null),
|
||||
onSuccess: (_data, next) => {
|
||||
setOpen(next.isopen);
|
||||
setConfirming(false);
|
||||
setUntil('');
|
||||
void client.invalidateQueries({ queryKey: queryKeys.tenants.all });
|
||||
},
|
||||
// A close that fails silently is the worst outcome here: the shopkeeper
|
||||
// walks away believing the shop is shut while it keeps taking orders.
|
||||
onError: (cause) => {
|
||||
setOpen(isOpen);
|
||||
setError(errorMessage(cause));
|
||||
},
|
||||
});
|
||||
|
||||
const shownClosedUntil = (closedUntil ?? '').slice(0, 10);
|
||||
|
||||
return (
|
||||
<div style={{ display: 'grid', gap: 6, justifyItems: 'end' }}>
|
||||
<Switch
|
||||
label={open ? 'Open today' : 'Closed today'}
|
||||
description={
|
||||
open
|
||||
? 'Customers can order from this branch'
|
||||
: shownClosedUntil
|
||||
? `Customers cannot order. Reopens on ${friendly(shownClosedUntil)}`
|
||||
: 'Customers cannot order from this branch'
|
||||
}
|
||||
value={open}
|
||||
onChange={(next: boolean) => {
|
||||
if (next) {
|
||||
// Reopening is harmless, so it happens on the tap.
|
||||
save.mutate({ isopen: true });
|
||||
} else {
|
||||
setOpen(false);
|
||||
setConfirming(true);
|
||||
}
|
||||
}}
|
||||
isLoading={save.isPending}
|
||||
size="sm"
|
||||
labelSpacing="spread"
|
||||
/>
|
||||
|
||||
{confirming ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gap: 8,
|
||||
padding: '11px 13px',
|
||||
borderRadius: 11,
|
||||
border: '1px solid var(--color-line)',
|
||||
background: 'var(--color-surface)',
|
||||
justifyItems: 'start',
|
||||
minWidth: 260,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 12.5, color: 'var(--color-ink-3)' }}>
|
||||
Customers will not be able to order from this branch.
|
||||
</span>
|
||||
|
||||
<label style={{ fontSize: 12, color: 'var(--color-ink-4)', display: 'grid', gap: 4 }}>
|
||||
Reopens on <span style={{ opacity: 0.75 }}>(leave blank to stay closed)</span>
|
||||
<input
|
||||
id="store-reopens-on"
|
||||
type="date"
|
||||
value={until}
|
||||
min={today()}
|
||||
onChange={(event) => setUntil(event.target.value)}
|
||||
style={{
|
||||
padding: '6px 9px',
|
||||
fontSize: 13,
|
||||
border: '1px solid var(--color-line)',
|
||||
borderRadius: 7,
|
||||
background: 'var(--color-surface)',
|
||||
color: 'var(--color-ink-1)',
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Button
|
||||
label="Keep open"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setConfirming(false);
|
||||
setOpen(true);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label={save.isPending ? 'Closing…' : 'Close the branch'}
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
isLoading={save.isPending}
|
||||
isDisabled={save.isPending}
|
||||
onClick={() =>
|
||||
save.mutate({ isopen: false, ...(until ? { closeduntil: until } : {}) })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<span style={{ fontSize: 12, color: 'var(--color-error, #d64545)' }}>{error}</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function today(): string {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/** "2026-10-12" → "12 Oct". Falls back to the raw value rather than printing
|
||||
"Invalid Date" beside a shop's trading status. */
|
||||
function friendly(date: string): string {
|
||||
const parsed = new Date(`${date}T00:00:00`);
|
||||
if (Number.isNaN(parsed.getTime())) return date;
|
||||
return parsed.toLocaleDateString(undefined, { day: 'numeric', month: 'short' });
|
||||
}
|
||||
Reference in New Issue
Block a user