first commit
This commit is contained in:
111
src/features/engagement/components/RecordActivityDialog.tsx
Normal file
111
src/features/engagement/components/RecordActivityDialog.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
'use client';
|
||||
|
||||
import {useState} from 'react';
|
||||
import {Dialog, DialogHeader} from '@astryxdesign/core/Dialog';
|
||||
import {VStack, HStack} from '@astryxdesign/core/Layout';
|
||||
import {TextInput} from '@astryxdesign/core/TextInput';
|
||||
import {Selector} from '@astryxdesign/core/Selector';
|
||||
import {Button} from '@astryxdesign/core/Button';
|
||||
import {Banner} from '@astryxdesign/core/Banner';
|
||||
import {useWorkspace} from '@/shared/providers/WorkspaceProvider';
|
||||
import {engagementRepository} from '@/features/engagement/repositories/engagementRepository';
|
||||
import type {ActivityRow} from '@/features/engagement/types/engagement';
|
||||
|
||||
const NO_SHOP = '__none__';
|
||||
|
||||
/**
|
||||
* Record that somebody took part in an activity — a selfie, a spin, a
|
||||
* challenge. The cameras cannot observe these, so without this the activity
|
||||
* panels stay at zero forever.
|
||||
*
|
||||
* ── One id per real-world event ──────────────────────────────────────────
|
||||
* `sourceEventId` is minted ONCE, when the dialog opens, and reused by every
|
||||
* retry of this submission. The platform de-duplicates on it, so tapping
|
||||
* "Record" again after a timeout cannot count the same spin twice. A new
|
||||
* dialog is a new event and gets a new id.
|
||||
*/
|
||||
export function RecordActivityDialog({
|
||||
activities,
|
||||
onClose,
|
||||
onRecorded,
|
||||
}: {
|
||||
activities: ActivityRow[];
|
||||
onClose: () => void;
|
||||
onRecorded: () => void;
|
||||
}) {
|
||||
const {stores, storeId} = useWorkspace();
|
||||
const live = activities.filter((a) => a.isActive);
|
||||
const [sourceEventId] = useState(() => crypto.randomUUID());
|
||||
const [kind, setKind] = useState(live[0]?.kind ?? '');
|
||||
const [site, setSite] = useState(storeId !== 'all' ? storeId : NO_SHOP);
|
||||
const [customer, setCustomer] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
async function submit() {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
const res = await engagementRepository.recordEvent({
|
||||
kind,
|
||||
sourceEventId,
|
||||
site: site === NO_SHOP ? undefined : site,
|
||||
visitorId: customer.trim() || undefined,
|
||||
});
|
||||
setBusy(false);
|
||||
if (!res.ok) {
|
||||
setError(res.message ?? 'Could not record that.');
|
||||
return;
|
||||
}
|
||||
onRecorded();
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen
|
||||
onOpenChange={(open) => (open ? undefined : onClose())}
|
||||
purpose="required"
|
||||
width={440}
|
||||
aria-label="Record an activity"
|
||||
>
|
||||
<VStack gap={4} width="100%">
|
||||
<DialogHeader
|
||||
title="Record an activity"
|
||||
onOpenChange={(open) => (open ? undefined : onClose())}
|
||||
/>
|
||||
<Selector
|
||||
label="Activity"
|
||||
value={kind}
|
||||
onChange={(v) => setKind(v)}
|
||||
options={live.map((a) => ({value: a.kind, label: a.name}))}
|
||||
/>
|
||||
<Selector
|
||||
label="Shop"
|
||||
value={site}
|
||||
onChange={(v) => setSite(v)}
|
||||
options={[
|
||||
{value: NO_SHOP, label: 'Not at a shop'},
|
||||
...stores.filter((s) => s.id !== 'all').map((s) => ({value: s.id, label: s.name})),
|
||||
]}
|
||||
/>
|
||||
<TextInput
|
||||
label="Customer number"
|
||||
value={customer}
|
||||
onChange={setCustomer}
|
||||
placeholder="V-42"
|
||||
isOptional
|
||||
description="Leave blank for somebody the cameras have not recognised — it still counts, but cannot appear in the impact figures."
|
||||
/>
|
||||
{error ? <Banner status="error" title={error} /> : null}
|
||||
<HStack gap={2} hAlign="end">
|
||||
<Button variant="secondary" onClick={onClose} label="Cancel" />
|
||||
<Button
|
||||
onClick={() => void submit()}
|
||||
isDisabled={!kind || busy}
|
||||
isLoading={busy}
|
||||
label="Record"
|
||||
/>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user