first commit
This commit is contained in:
221
src/features/settings/components/StoreManagement.tsx
Normal file
221
src/features/settings/components/StoreManagement.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user