222 lines
6.9 KiB
TypeScript
222 lines
6.9 KiB
TypeScript
'use client';
|
|
|
|
import {useState} from 'react';
|
|
import {proportional, pixel} from '@astryxdesign/core/Table';
|
|
import type {TableColumn} from '@astryxdesign/core/Table';
|
|
import {VStack, HStack} from '@astryxdesign/core/Layout';
|
|
import {Text} from '@astryxdesign/core/Text';
|
|
import {Button} from '@astryxdesign/core/Button';
|
|
import {StatusDot} from '@astryxdesign/core/StatusDot';
|
|
import {DropdownMenu} from '@astryxdesign/core/DropdownMenu';
|
|
import {Icon} from '@astryxdesign/core/Icon';
|
|
import {useToast} from '@astryxdesign/core/Toast';
|
|
import {PanelCard} from '@/shared/components/patterns/PanelCard';
|
|
import {ResponsiveTable} from '@/shared/components/patterns/ResponsiveTable';
|
|
import {SkeletonRows} from '@/shared/components/patterns/LoadingState';
|
|
import {EmptyPanel} from '@/shared/components/patterns/EmptyPanel';
|
|
import {useWorkspace} from '@/shared/providers/WorkspaceProvider';
|
|
import {useSession} from '@/features/auth/providers/SessionProvider';
|
|
import {hasRole} from '@/features/auth/services/roles';
|
|
import {useSites} from '@/features/stores/hooks/useSites';
|
|
import {OpenShopDialog} from '@/features/stores/components/OpenShopDialog';
|
|
import {RenameShopDialog} from '@/features/stores/components/RenameShopDialog';
|
|
import {RemoveShopDialog} from '@/features/stores/components/RemoveShopDialog';
|
|
import type {Site} from '@/features/stores/types/site';
|
|
|
|
type SiteRow = Site & Record<string, unknown>;
|
|
|
|
/**
|
|
* The company's shops, from the platform — open, rename, remove.
|
|
*
|
|
* This screen used to be a hardcoded list of five Bengaluru shops held in
|
|
* local state: adding one "worked" until the page reloaded. It now reads
|
|
* GET /api/sites and writes through POST / PATCH / DELETE on the same
|
|
* resource, and every write re-reads rather than splicing local state.
|
|
*
|
|
* Who is offered what mirrors the platform's own rules — opening and removing
|
|
* a shop is the owner's, renaming is a manager's too — but the platform is
|
|
* what enforces them; hiding a button is courtesy, not security.
|
|
*
|
|
* Cameras and the shop PC are set up per shop on the Store page, not here:
|
|
* this is the directory, that is the equipment.
|
|
*/
|
|
export function StoreManagement() {
|
|
const toast = useToast();
|
|
const sites = useSites();
|
|
const {refreshStores} = useWorkspace();
|
|
const {user} = useSession();
|
|
const isOwner = hasRole(user?.role, 'owner');
|
|
const isManager = hasRole(user?.role, 'manager');
|
|
|
|
const [opening, setOpening] = useState(false);
|
|
const [renaming, setRenaming] = useState<Site | null>(null);
|
|
const [removing, setRemoving] = useState<Site | null>(null);
|
|
|
|
/** The settings table and the global switcher each hold a copy; refresh both. */
|
|
function reload() {
|
|
sites.refetch();
|
|
refreshStores();
|
|
}
|
|
|
|
const columns: TableColumn<SiteRow>[] = [
|
|
{
|
|
key: 'name',
|
|
header: 'Shop',
|
|
width: proportional(2),
|
|
renderCell: (row) => (
|
|
<VStack gap={0}>
|
|
<Text size="sm" weight="medium">
|
|
{row.name}
|
|
</Text>
|
|
<Text size="sm" color="secondary" className="font-mono">
|
|
{row.id}
|
|
</Text>
|
|
</VStack>
|
|
),
|
|
},
|
|
{
|
|
key: 'timezone',
|
|
header: 'Timezone',
|
|
width: proportional(1.2),
|
|
renderCell: (row) => (
|
|
<Text size="sm" color="secondary">
|
|
{row.timezone || '—'}
|
|
</Text>
|
|
),
|
|
},
|
|
{
|
|
key: 'isOnline',
|
|
header: 'Shop PC',
|
|
width: pixel(150),
|
|
renderCell: (row) => (
|
|
<HStack gap={1.5} vAlign="center">
|
|
<StatusDot
|
|
variant={row.isOnline === null ? 'neutral' : row.isOnline ? 'success' : 'error'}
|
|
label={row.isOnline === null ? 'Not reported' : row.isOnline ? 'Online' : 'Offline'}
|
|
/>
|
|
<Text size="sm" color="secondary">
|
|
{row.isOnline === null ? 'Not reported' : row.isOnline ? 'Online' : 'Offline'}
|
|
</Text>
|
|
</HStack>
|
|
),
|
|
},
|
|
{
|
|
key: 'camerasUp',
|
|
header: 'Cameras',
|
|
width: pixel(110),
|
|
renderCell: (row) => (
|
|
<Text size="sm" weight="medium">
|
|
{row.camerasUp === null || row.camerasTotal === null
|
|
? '—'
|
|
: `${row.camerasUp}/${row.camerasTotal} up`}
|
|
</Text>
|
|
),
|
|
},
|
|
{
|
|
key: 'actions',
|
|
header: 'Actions',
|
|
align: 'center',
|
|
width: pixel(140),
|
|
renderCell: (row) =>
|
|
isManager ? (
|
|
<HStack hAlign="center">
|
|
<DropdownMenu
|
|
button={{
|
|
variant: 'secondary',
|
|
size: 'sm',
|
|
label: 'Manage',
|
|
icon: <Icon icon="moreHorizontal" size="sm" />,
|
|
}}
|
|
menuWidth={180}
|
|
items={[
|
|
{label: 'Rename', onClick: () => setRenaming(row)},
|
|
...(isOwner
|
|
? [
|
|
{type: 'divider' as const},
|
|
{label: 'Remove shop', onClick: () => setRemoving(row)},
|
|
]
|
|
: []),
|
|
]}
|
|
/>
|
|
</HStack>
|
|
) : null,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<VStack gap={5}>
|
|
<PanelCard
|
|
title="Shops"
|
|
subtitle="Every shop in your company. Cameras and the shop PC are set up on the Store page."
|
|
actions={
|
|
isOwner ? (
|
|
<Button
|
|
variant="primary"
|
|
size="sm"
|
|
label="Open a shop"
|
|
onClick={() => setOpening(true)}
|
|
/>
|
|
) : undefined
|
|
}
|
|
resource={sites}
|
|
loading={<SkeletonRows count={4} height={52} />}
|
|
empty={
|
|
<EmptyPanel
|
|
icon="stores"
|
|
title="No shops yet"
|
|
description={
|
|
isOwner
|
|
? 'Open your first shop, then add its cameras on the Store page.'
|
|
: 'Your company has no shops yet. The owner opens them.'
|
|
}
|
|
/>
|
|
}
|
|
>
|
|
{(rows) => (
|
|
<ResponsiveTable
|
|
columns={columns}
|
|
data={rows as SiteRow[]}
|
|
idKey="id"
|
|
primaryKey="name"
|
|
/>
|
|
)}
|
|
</PanelCard>
|
|
|
|
{opening ? (
|
|
<OpenShopDialog
|
|
onClose={() => setOpening(false)}
|
|
onOpened={() => {
|
|
setOpening(false);
|
|
toast({body: 'Shop opened. Add its cameras on the Store page.'});
|
|
reload();
|
|
}}
|
|
/>
|
|
) : null}
|
|
|
|
{renaming ? (
|
|
<RenameShopDialog
|
|
site={renaming}
|
|
onClose={() => setRenaming(null)}
|
|
onSaved={() => {
|
|
setRenaming(null);
|
|
toast({body: 'Shop updated'});
|
|
reload();
|
|
}}
|
|
/>
|
|
) : null}
|
|
|
|
{removing ? (
|
|
<RemoveShopDialog
|
|
site={removing}
|
|
onClose={() => setRemoving(null)}
|
|
onRemoved={() => {
|
|
setRemoving(null);
|
|
toast({body: 'Shop removed'});
|
|
reload();
|
|
}}
|
|
/>
|
|
) : null}
|
|
</VStack>
|
|
);
|
|
}
|