admin login issue
This commit is contained in:
125
src/features/admin/components/DeleteCompanyDialog.tsx
Normal file
125
src/features/admin/components/DeleteCompanyDialog.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
'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 {Button} from '@astryxdesign/core/Button';
|
||||
import {Banner} from '@astryxdesign/core/Banner';
|
||||
import {companyRepository} from '@/features/admin/repositories/companyRepository';
|
||||
import type {Company} from '@/features/admin/types/company';
|
||||
|
||||
/**
|
||||
* Delete a company. Permanently.
|
||||
*
|
||||
* ── Why the typed slug ───────────────────────────────────────────────────
|
||||
* The platform requires the body to repeat the company's slug, and this dialog
|
||||
* asks for it rather than filling it in. That is the whole safeguard: a
|
||||
* confirm button can be clicked by muscle memory on the wrong row, and typing
|
||||
* "tenext-retail" cannot. The button stays disabled until it matches exactly —
|
||||
* no trimming of case, because the slug is lowercase upstream and a console
|
||||
* that quietly "fixes" what was typed is not confirming anything.
|
||||
*
|
||||
* ── Why the suspended check is NOT enforced here ─────────────────────────
|
||||
* The platform refuses to delete an active company (`409 still_active`), and
|
||||
* this dialog does not pre-empt that. It says so plainly, and the button is
|
||||
* still live: an active company is sent, and the platform's real 409 is what
|
||||
* appears. A console that blocked the request itself would eventually disagree
|
||||
* with the server about what is deletable, and that disagreement surfaces as a
|
||||
* delete that looked like it worked and did not happen.
|
||||
*
|
||||
* ── What is actually destroyed ───────────────────────────────────────────
|
||||
* Stored face images go from object storage first — a failure there is
|
||||
* `502 storage_error` and nothing else is touched — then the shop PCs' broker
|
||||
* logins, then every row by cascade: face templates, visits, users, sessions,
|
||||
* cameras. The data is biometric. There is no undo and no backup to ask for.
|
||||
*/
|
||||
export function DeleteCompanyDialog({
|
||||
company,
|
||||
onClose,
|
||||
onDeleted,
|
||||
}: {
|
||||
company: Company;
|
||||
onClose: () => void;
|
||||
onDeleted: () => void;
|
||||
}) {
|
||||
const [confirm, setConfirm] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const matches = confirm === company.slug;
|
||||
|
||||
async function submit() {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
|
||||
const res = await companyRepository.remove(company.id, confirm);
|
||||
setBusy(false);
|
||||
|
||||
if (!res.ok) {
|
||||
// `still_active` and `storage_error` both name a condition the operator
|
||||
// can act on — suspend it first, or retry once storage is healthy.
|
||||
setError(res.message ?? 'Could not delete that company.');
|
||||
return;
|
||||
}
|
||||
|
||||
onDeleted();
|
||||
onClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen
|
||||
onOpenChange={(open) => (open ? undefined : onClose())}
|
||||
purpose="required"
|
||||
width={460}
|
||||
aria-label="Delete company"
|
||||
>
|
||||
<VStack gap={4} width="100%">
|
||||
<DialogHeader
|
||||
title="Delete company?"
|
||||
onOpenChange={(open) => (open ? undefined : onClose())}
|
||||
/>
|
||||
|
||||
{error ? <Banner status="error" title={error} /> : null}
|
||||
|
||||
<Banner
|
||||
status="error"
|
||||
title="This cannot be undone"
|
||||
description={`Deletes ${company.name}, its ${company.sites} ${
|
||||
company.sites === 1 ? 'shop' : 'shops'
|
||||
}, its ${company.users} ${
|
||||
company.users === 1 ? 'account' : 'accounts'
|
||||
}, and every stored face image, visit and camera. The data is biometric and there is no backup to restore from.`}
|
||||
/>
|
||||
|
||||
{company.isActive ? (
|
||||
<Banner
|
||||
status="warning"
|
||||
title="Still active"
|
||||
description="The platform will refuse this until the company is suspended. Suspend it first, then come back."
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<TextInput
|
||||
label="Type the company slug to confirm"
|
||||
value={confirm}
|
||||
onChange={setConfirm}
|
||||
placeholder={company.slug}
|
||||
description={`Exactly: ${company.slug}`}
|
||||
/>
|
||||
|
||||
<HStack gap={2} hAlign="end">
|
||||
<Button variant="secondary" onClick={onClose} label="Cancel" />
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => void submit()}
|
||||
isDisabled={!matches || busy}
|
||||
isLoading={busy}
|
||||
label="Delete company"
|
||||
/>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user