diff --git a/frontend/src/components/sites/SiteTable.tsx b/frontend/src/components/sites/SiteTable.tsx new file mode 100644 index 0000000..3e875a4 --- /dev/null +++ b/frontend/src/components/sites/SiteTable.tsx @@ -0,0 +1,268 @@ +import { useState } from 'react' +import { Link } from '@tanstack/react-router' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { ChevronUp, ChevronDown, ChevronsUpDown, MapPin, Pencil, Trash2 } from 'lucide-react' +import { sitesApi, type SiteResponse } from '@/lib/api' +import { useAuth, canWrite } from '@/lib/auth' +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { TableSkeleton } from '@/components/ui/page-skeleton' +import { EmptyState } from '@/components/ui/empty-state' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from '@/components/ui/dialog' + +interface SiteTableProps { + tenantId: string + search: string + onCreateClick: () => void + onEditClick: (site: SiteResponse) => void +} + +type SortField = 'name' | 'device_count' | 'online_percent' +type SortDir = 'asc' | 'desc' + +interface SortHeaderProps { + column: SortField + label: string + currentSort: SortField + currentDir: SortDir + onSort: (col: SortField) => void + className?: string +} + +function SortHeader({ column, label, currentSort, currentDir, onSort, className }: SortHeaderProps) { + const isActive = currentSort === column + const ariaSortValue: 'ascending' | 'descending' | 'none' = isActive + ? (currentDir === 'asc' ? 'ascending' : 'descending') + : 'none' + + return ( + + + + ) +} + +export function SiteTable({ tenantId, search, onCreateClick, onEditClick }: SiteTableProps) { + const { user } = useAuth() + const queryClient = useQueryClient() + const showActions = canWrite(user) + + const [sortBy, setSortBy] = useState('name') + const [sortDir, setSortDir] = useState('asc') + const [deleteTarget, setDeleteTarget] = useState(null) + + const { data, isLoading } = useQuery({ + queryKey: ['sites', tenantId], + queryFn: () => sitesApi.list(tenantId), + }) + + const deleteMutation = useMutation({ + mutationFn: (siteId: string) => sitesApi.delete(tenantId, siteId), + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: ['sites', tenantId] }) + setDeleteTarget(null) + }, + }) + + function handleSort(col: SortField) { + if (col === sortBy) { + setSortDir((d) => (d === 'asc' ? 'desc' : 'asc')) + } else { + setSortBy(col) + setSortDir('asc') + } + } + + if (isLoading) { + return + } + + if (!data || data.sites.length === 0) { + return ( + + ) + } + + // Filter by search + const filtered = data.sites.filter((site) => + site.name.toLowerCase().includes(search.toLowerCase()), + ) + + // Sort + const sorted = [...filtered].sort((a, b) => { + const dir = sortDir === 'asc' ? 1 : -1 + if (sortBy === 'name') return a.name.localeCompare(b.name) * dir + if (sortBy === 'device_count') return (a.device_count - b.device_count) * dir + if (sortBy === 'online_percent') return (a.online_percent - b.online_percent) * dir + return 0 + }) + + const sortProps = { currentSort: sortBy, currentDir: sortDir, onSort: handleSort } + const colCount = showActions ? 6 : 5 + + return ( + <> +
+
+ + + + + + + + + {showActions && ( + + )} + + + + {sorted.map((site) => ( + + + + + + + {showActions && ( + + )} + + ))} + + {/* Unassigned devices row */} + + + + + +
+ Address + + Alerts + + Actions +
+ + {site.name} + + + {site.address ?? '--'} + + {site.device_count} + + = 90 + ? 'text-green-500' + : site.online_percent >= 50 + ? 'text-yellow-500' + : 'text-red-500', + )} + > + {site.device_count > 0 ? `${site.online_percent.toFixed(0)}%` : '--'} + + + {site.alert_count > 0 ? ( + + {site.alert_count} + + ) : ( + 0 + )} + +
+ + +
+
+ Unassigned + + {data.unassigned_count} + +
+
+
+ + {/* Delete confirmation dialog */} + { if (!open) setDeleteTarget(null) }}> + + + Delete site? + + Are you sure you want to delete "{deleteTarget?.name}"?{' '} + {deleteTarget?.device_count ?? 0} device(s) will become unassigned. + + + + + + + + + + ) +} diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 09e6850..aab3666 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -16,8 +16,10 @@ import { Route as LoginRouteImport } from './routes/login' import { Route as ForgotPasswordRouteImport } from './routes/forgot-password' import { Route as AuthenticatedRouteImport } from './routes/_authenticated' import { Route as AuthenticatedIndexRouteImport } from './routes/_authenticated/index' +import { Route as AuthenticatedWirelessRouteImport } from './routes/_authenticated/wireless' import { Route as AuthenticatedVpnRouteImport } from './routes/_authenticated/vpn' import { Route as AuthenticatedTransparencyRouteImport } from './routes/_authenticated/transparency' +import { Route as AuthenticatedTrafficRouteImport } from './routes/_authenticated/traffic' import { Route as AuthenticatedTopologyRouteImport } from './routes/_authenticated/topology' import { Route as AuthenticatedTemplatesRouteImport } from './routes/_authenticated/templates' import { Route as AuthenticatedSetupRouteImport } from './routes/_authenticated/setup' @@ -38,7 +40,9 @@ import { Route as AuthenticatedTenantsIndexRouteImport } from './routes/_authent import { Route as AuthenticatedSettingsApiKeysRouteImport } from './routes/_authenticated/settings.api-keys' import { Route as AuthenticatedTenantsTenantIdIndexRouteImport } from './routes/_authenticated/tenants/$tenantId/index' import { Route as AuthenticatedTenantsTenantIdUsersRouteImport } from './routes/_authenticated/tenants/$tenantId/users' +import { Route as AuthenticatedTenantsTenantIdSitesIndexRouteImport } from './routes/_authenticated/tenants/$tenantId/sites/index' import { Route as AuthenticatedTenantsTenantIdDevicesIndexRouteImport } from './routes/_authenticated/tenants/$tenantId/devices/index' +import { Route as AuthenticatedTenantsTenantIdSitesSiteIdRouteImport } from './routes/_authenticated/tenants/$tenantId/sites/$siteId' import { Route as AuthenticatedTenantsTenantIdDevicesScanRouteImport } from './routes/_authenticated/tenants/$tenantId/devices/scan' import { Route as AuthenticatedTenantsTenantIdDevicesAdoptRouteImport } from './routes/_authenticated/tenants/$tenantId/devices/adopt' import { Route as AuthenticatedTenantsTenantIdDevicesAddRouteImport } from './routes/_authenticated/tenants/$tenantId/devices/add' @@ -78,6 +82,11 @@ const AuthenticatedIndexRoute = AuthenticatedIndexRouteImport.update({ path: '/', getParentRoute: () => AuthenticatedRoute, } as any) +const AuthenticatedWirelessRoute = AuthenticatedWirelessRouteImport.update({ + id: '/wireless', + path: '/wireless', + getParentRoute: () => AuthenticatedRoute, +} as any) const AuthenticatedVpnRoute = AuthenticatedVpnRouteImport.update({ id: '/vpn', path: '/vpn', @@ -89,6 +98,11 @@ const AuthenticatedTransparencyRoute = path: '/transparency', getParentRoute: () => AuthenticatedRoute, } as any) +const AuthenticatedTrafficRoute = AuthenticatedTrafficRouteImport.update({ + id: '/traffic', + path: '/traffic', + getParentRoute: () => AuthenticatedRoute, +} as any) const AuthenticatedTopologyRoute = AuthenticatedTopologyRouteImport.update({ id: '/topology', path: '/topology', @@ -198,12 +212,24 @@ const AuthenticatedTenantsTenantIdUsersRoute = path: '/tenants/$tenantId/users', getParentRoute: () => AuthenticatedRoute, } as any) +const AuthenticatedTenantsTenantIdSitesIndexRoute = + AuthenticatedTenantsTenantIdSitesIndexRouteImport.update({ + id: '/tenants/$tenantId/sites/', + path: '/tenants/$tenantId/sites/', + getParentRoute: () => AuthenticatedRoute, + } as any) const AuthenticatedTenantsTenantIdDevicesIndexRoute = AuthenticatedTenantsTenantIdDevicesIndexRouteImport.update({ id: '/tenants/$tenantId/devices/', path: '/tenants/$tenantId/devices/', getParentRoute: () => AuthenticatedRoute, } as any) +const AuthenticatedTenantsTenantIdSitesSiteIdRoute = + AuthenticatedTenantsTenantIdSitesSiteIdRouteImport.update({ + id: '/tenants/$tenantId/sites/$siteId', + path: '/tenants/$tenantId/sites/$siteId', + getParentRoute: () => AuthenticatedRoute, + } as any) const AuthenticatedTenantsTenantIdDevicesScanRoute = AuthenticatedTenantsTenantIdDevicesScanRouteImport.update({ id: '/tenants/$tenantId/devices/scan', @@ -252,8 +278,10 @@ export interface FileRoutesByFullPath { '/setup': typeof AuthenticatedSetupRoute '/templates': typeof AuthenticatedTemplatesRoute '/topology': typeof AuthenticatedTopologyRoute + '/traffic': typeof AuthenticatedTrafficRoute '/transparency': typeof AuthenticatedTransparencyRoute '/vpn': typeof AuthenticatedVpnRoute + '/wireless': typeof AuthenticatedWirelessRoute '/settings/api-keys': typeof AuthenticatedSettingsApiKeysRoute '/tenants/': typeof AuthenticatedTenantsIndexRoute '/tenants/$tenantId/users': typeof AuthenticatedTenantsTenantIdUsersRoute @@ -262,7 +290,9 @@ export interface FileRoutesByFullPath { '/tenants/$tenantId/devices/add': typeof AuthenticatedTenantsTenantIdDevicesAddRoute '/tenants/$tenantId/devices/adopt': typeof AuthenticatedTenantsTenantIdDevicesAdoptRoute '/tenants/$tenantId/devices/scan': typeof AuthenticatedTenantsTenantIdDevicesScanRoute + '/tenants/$tenantId/sites/$siteId': typeof AuthenticatedTenantsTenantIdSitesSiteIdRoute '/tenants/$tenantId/devices/': typeof AuthenticatedTenantsTenantIdDevicesIndexRoute + '/tenants/$tenantId/sites/': typeof AuthenticatedTenantsTenantIdSitesIndexRoute } export interface FileRoutesByTo { '/forgot-password': typeof ForgotPasswordRoute @@ -286,8 +316,10 @@ export interface FileRoutesByTo { '/setup': typeof AuthenticatedSetupRoute '/templates': typeof AuthenticatedTemplatesRoute '/topology': typeof AuthenticatedTopologyRoute + '/traffic': typeof AuthenticatedTrafficRoute '/transparency': typeof AuthenticatedTransparencyRoute '/vpn': typeof AuthenticatedVpnRoute + '/wireless': typeof AuthenticatedWirelessRoute '/': typeof AuthenticatedIndexRoute '/settings/api-keys': typeof AuthenticatedSettingsApiKeysRoute '/tenants': typeof AuthenticatedTenantsIndexRoute @@ -297,7 +329,9 @@ export interface FileRoutesByTo { '/tenants/$tenantId/devices/add': typeof AuthenticatedTenantsTenantIdDevicesAddRoute '/tenants/$tenantId/devices/adopt': typeof AuthenticatedTenantsTenantIdDevicesAdoptRoute '/tenants/$tenantId/devices/scan': typeof AuthenticatedTenantsTenantIdDevicesScanRoute + '/tenants/$tenantId/sites/$siteId': typeof AuthenticatedTenantsTenantIdSitesSiteIdRoute '/tenants/$tenantId/devices': typeof AuthenticatedTenantsTenantIdDevicesIndexRoute + '/tenants/$tenantId/sites': typeof AuthenticatedTenantsTenantIdSitesIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -323,8 +357,10 @@ export interface FileRoutesById { '/_authenticated/setup': typeof AuthenticatedSetupRoute '/_authenticated/templates': typeof AuthenticatedTemplatesRoute '/_authenticated/topology': typeof AuthenticatedTopologyRoute + '/_authenticated/traffic': typeof AuthenticatedTrafficRoute '/_authenticated/transparency': typeof AuthenticatedTransparencyRoute '/_authenticated/vpn': typeof AuthenticatedVpnRoute + '/_authenticated/wireless': typeof AuthenticatedWirelessRoute '/_authenticated/': typeof AuthenticatedIndexRoute '/_authenticated/settings/api-keys': typeof AuthenticatedSettingsApiKeysRoute '/_authenticated/tenants/': typeof AuthenticatedTenantsIndexRoute @@ -334,7 +370,9 @@ export interface FileRoutesById { '/_authenticated/tenants/$tenantId/devices/add': typeof AuthenticatedTenantsTenantIdDevicesAddRoute '/_authenticated/tenants/$tenantId/devices/adopt': typeof AuthenticatedTenantsTenantIdDevicesAdoptRoute '/_authenticated/tenants/$tenantId/devices/scan': typeof AuthenticatedTenantsTenantIdDevicesScanRoute + '/_authenticated/tenants/$tenantId/sites/$siteId': typeof AuthenticatedTenantsTenantIdSitesSiteIdRoute '/_authenticated/tenants/$tenantId/devices/': typeof AuthenticatedTenantsTenantIdDevicesIndexRoute + '/_authenticated/tenants/$tenantId/sites/': typeof AuthenticatedTenantsTenantIdSitesIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -361,8 +399,10 @@ export interface FileRouteTypes { | '/setup' | '/templates' | '/topology' + | '/traffic' | '/transparency' | '/vpn' + | '/wireless' | '/settings/api-keys' | '/tenants/' | '/tenants/$tenantId/users' @@ -371,7 +411,9 @@ export interface FileRouteTypes { | '/tenants/$tenantId/devices/add' | '/tenants/$tenantId/devices/adopt' | '/tenants/$tenantId/devices/scan' + | '/tenants/$tenantId/sites/$siteId' | '/tenants/$tenantId/devices/' + | '/tenants/$tenantId/sites/' fileRoutesByTo: FileRoutesByTo to: | '/forgot-password' @@ -395,8 +437,10 @@ export interface FileRouteTypes { | '/setup' | '/templates' | '/topology' + | '/traffic' | '/transparency' | '/vpn' + | '/wireless' | '/' | '/settings/api-keys' | '/tenants' @@ -406,7 +450,9 @@ export interface FileRouteTypes { | '/tenants/$tenantId/devices/add' | '/tenants/$tenantId/devices/adopt' | '/tenants/$tenantId/devices/scan' + | '/tenants/$tenantId/sites/$siteId' | '/tenants/$tenantId/devices' + | '/tenants/$tenantId/sites' id: | '__root__' | '/_authenticated' @@ -431,8 +477,10 @@ export interface FileRouteTypes { | '/_authenticated/setup' | '/_authenticated/templates' | '/_authenticated/topology' + | '/_authenticated/traffic' | '/_authenticated/transparency' | '/_authenticated/vpn' + | '/_authenticated/wireless' | '/_authenticated/' | '/_authenticated/settings/api-keys' | '/_authenticated/tenants/' @@ -442,7 +490,9 @@ export interface FileRouteTypes { | '/_authenticated/tenants/$tenantId/devices/add' | '/_authenticated/tenants/$tenantId/devices/adopt' | '/_authenticated/tenants/$tenantId/devices/scan' + | '/_authenticated/tenants/$tenantId/sites/$siteId' | '/_authenticated/tenants/$tenantId/devices/' + | '/_authenticated/tenants/$tenantId/sites/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -505,6 +555,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedIndexRouteImport parentRoute: typeof AuthenticatedRoute } + '/_authenticated/wireless': { + id: '/_authenticated/wireless' + path: '/wireless' + fullPath: '/wireless' + preLoaderRoute: typeof AuthenticatedWirelessRouteImport + parentRoute: typeof AuthenticatedRoute + } '/_authenticated/vpn': { id: '/_authenticated/vpn' path: '/vpn' @@ -519,6 +576,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedTransparencyRouteImport parentRoute: typeof AuthenticatedRoute } + '/_authenticated/traffic': { + id: '/_authenticated/traffic' + path: '/traffic' + fullPath: '/traffic' + preLoaderRoute: typeof AuthenticatedTrafficRouteImport + parentRoute: typeof AuthenticatedRoute + } '/_authenticated/topology': { id: '/_authenticated/topology' path: '/topology' @@ -659,6 +723,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedTenantsTenantIdUsersRouteImport parentRoute: typeof AuthenticatedRoute } + '/_authenticated/tenants/$tenantId/sites/': { + id: '/_authenticated/tenants/$tenantId/sites/' + path: '/tenants/$tenantId/sites' + fullPath: '/tenants/$tenantId/sites/' + preLoaderRoute: typeof AuthenticatedTenantsTenantIdSitesIndexRouteImport + parentRoute: typeof AuthenticatedRoute + } '/_authenticated/tenants/$tenantId/devices/': { id: '/_authenticated/tenants/$tenantId/devices/' path: '/tenants/$tenantId/devices' @@ -666,6 +737,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedTenantsTenantIdDevicesIndexRouteImport parentRoute: typeof AuthenticatedRoute } + '/_authenticated/tenants/$tenantId/sites/$siteId': { + id: '/_authenticated/tenants/$tenantId/sites/$siteId' + path: '/tenants/$tenantId/sites/$siteId' + fullPath: '/tenants/$tenantId/sites/$siteId' + preLoaderRoute: typeof AuthenticatedTenantsTenantIdSitesSiteIdRouteImport + parentRoute: typeof AuthenticatedRoute + } '/_authenticated/tenants/$tenantId/devices/scan': { id: '/_authenticated/tenants/$tenantId/devices/scan' path: '/tenants/$tenantId/devices/scan' @@ -727,8 +805,10 @@ interface AuthenticatedRouteChildren { AuthenticatedSetupRoute: typeof AuthenticatedSetupRoute AuthenticatedTemplatesRoute: typeof AuthenticatedTemplatesRoute AuthenticatedTopologyRoute: typeof AuthenticatedTopologyRoute + AuthenticatedTrafficRoute: typeof AuthenticatedTrafficRoute AuthenticatedTransparencyRoute: typeof AuthenticatedTransparencyRoute AuthenticatedVpnRoute: typeof AuthenticatedVpnRoute + AuthenticatedWirelessRoute: typeof AuthenticatedWirelessRoute AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute AuthenticatedTenantsIndexRoute: typeof AuthenticatedTenantsIndexRoute AuthenticatedTenantsTenantIdUsersRoute: typeof AuthenticatedTenantsTenantIdUsersRoute @@ -737,7 +817,9 @@ interface AuthenticatedRouteChildren { AuthenticatedTenantsTenantIdDevicesAddRoute: typeof AuthenticatedTenantsTenantIdDevicesAddRoute AuthenticatedTenantsTenantIdDevicesAdoptRoute: typeof AuthenticatedTenantsTenantIdDevicesAdoptRoute AuthenticatedTenantsTenantIdDevicesScanRoute: typeof AuthenticatedTenantsTenantIdDevicesScanRoute + AuthenticatedTenantsTenantIdSitesSiteIdRoute: typeof AuthenticatedTenantsTenantIdSitesSiteIdRoute AuthenticatedTenantsTenantIdDevicesIndexRoute: typeof AuthenticatedTenantsTenantIdDevicesIndexRoute + AuthenticatedTenantsTenantIdSitesIndexRoute: typeof AuthenticatedTenantsTenantIdSitesIndexRoute } const AuthenticatedRouteChildren: AuthenticatedRouteChildren = { @@ -757,8 +839,10 @@ const AuthenticatedRouteChildren: AuthenticatedRouteChildren = { AuthenticatedSetupRoute: AuthenticatedSetupRoute, AuthenticatedTemplatesRoute: AuthenticatedTemplatesRoute, AuthenticatedTopologyRoute: AuthenticatedTopologyRoute, + AuthenticatedTrafficRoute: AuthenticatedTrafficRoute, AuthenticatedTransparencyRoute: AuthenticatedTransparencyRoute, AuthenticatedVpnRoute: AuthenticatedVpnRoute, + AuthenticatedWirelessRoute: AuthenticatedWirelessRoute, AuthenticatedIndexRoute: AuthenticatedIndexRoute, AuthenticatedTenantsIndexRoute: AuthenticatedTenantsIndexRoute, AuthenticatedTenantsTenantIdUsersRoute: @@ -773,8 +857,12 @@ const AuthenticatedRouteChildren: AuthenticatedRouteChildren = { AuthenticatedTenantsTenantIdDevicesAdoptRoute, AuthenticatedTenantsTenantIdDevicesScanRoute: AuthenticatedTenantsTenantIdDevicesScanRoute, + AuthenticatedTenantsTenantIdSitesSiteIdRoute: + AuthenticatedTenantsTenantIdSitesSiteIdRoute, AuthenticatedTenantsTenantIdDevicesIndexRoute: AuthenticatedTenantsTenantIdDevicesIndexRoute, + AuthenticatedTenantsTenantIdSitesIndexRoute: + AuthenticatedTenantsTenantIdSitesIndexRoute, } const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren( diff --git a/frontend/src/routes/_authenticated/tenants/$tenantId/sites/$siteId.tsx b/frontend/src/routes/_authenticated/tenants/$tenantId/sites/$siteId.tsx new file mode 100644 index 0000000..e131bee --- /dev/null +++ b/frontend/src/routes/_authenticated/tenants/$tenantId/sites/$siteId.tsx @@ -0,0 +1,125 @@ +import { createFileRoute, Link } from '@tanstack/react-router' +import { useQuery } from '@tanstack/react-query' +import { sitesApi } from '@/lib/api' +import { MapPin, ArrowLeft } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { cn } from '@/lib/utils' + +export const Route = createFileRoute('/_authenticated/tenants/$tenantId/sites/$siteId')({ + component: SiteDetailPage, +}) + +function SiteDetailPage() { + const { tenantId, siteId } = Route.useParams() + + const { data: site, isLoading } = useQuery({ + queryKey: ['sites', tenantId, siteId], + queryFn: () => sitesApi.get(tenantId, siteId), + }) + + if (isLoading) { + return ( +
+
+
+
+ ) + } + + if (!site) { + return
Site not found
+ } + + return ( +
+ {/* Header with back link */} +
+ + + +
+ + {/* Site info card */} +
+
+ +

{site.name}

+
+ +
+ {site.address && ( +
+ Address: + {site.address} +
+ )} + {site.latitude != null && site.longitude != null && ( +
+ Coordinates: + + {site.latitude}, {site.longitude} + +
+ )} + {site.elevation != null && ( +
+ Elevation: + {site.elevation} m +
+ )} + {site.notes && ( +
+ Notes: +

{site.notes}

+
+ )} +
+
+ + {/* Health stats summary */} +
+
+

{site.device_count}

+

Devices

+
+
+

{site.online_count}

+

Online

+
+
+

= 90 + ? 'text-green-500' + : site.online_percent >= 50 + ? 'text-yellow-500' + : 'text-red-500', + )} + > + {site.online_percent.toFixed(0)}% +

+

Online %

+
+
+

0 && 'text-red-500')}> + {site.alert_count} +

+

Alerts

+
+
+ + {/* Placeholder for device list -- Phase 14 will add full site dashboard */} +
+

Assigned Devices

+

+ {site.device_count > 0 + ? `${site.device_count} device${site.device_count !== 1 ? 's' : ''} assigned to this site. Full device list coming in site dashboard.` + : 'No devices assigned to this site yet. Assign devices from the fleet page.'} +

+
+
+ ) +} diff --git a/frontend/src/routes/_authenticated/tenants/$tenantId/sites/index.tsx b/frontend/src/routes/_authenticated/tenants/$tenantId/sites/index.tsx new file mode 100644 index 0000000..cc658a5 --- /dev/null +++ b/frontend/src/routes/_authenticated/tenants/$tenantId/sites/index.tsx @@ -0,0 +1,51 @@ +import { createFileRoute } from '@tanstack/react-router' +import { useState } from 'react' +import { Plus } from 'lucide-react' +import { SiteTable } from '@/components/sites/SiteTable' +import { SiteFormDialog } from '@/components/sites/SiteFormDialog' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import type { SiteResponse } from '@/lib/api' + +export const Route = createFileRoute('/_authenticated/tenants/$tenantId/sites/')({ + component: SitesPage, +}) + +function SitesPage() { + const { tenantId } = Route.useParams() + const [search, setSearch] = useState('') + const [createOpen, setCreateOpen] = useState(false) + const [editSite, setEditSite] = useState(null) + + return ( +
+
+

Sites

+ +
+ setSearch(e.target.value)} + className="max-w-xs" + /> + setCreateOpen(true)} + onEditClick={setEditSite} + /> + + { + if (!open) setEditSite(null) + }} + tenantId={tenantId} + site={editSite} + /> +
+ ) +}