Files
loyaly_cutomerweb/src/features/settings/components/StoreManagement.tsx
2026-09-25 16:31:10 +05:30

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>
);
}