13e3444258
The page forced the card to fill the viewport via h-[calc(100vh-...)], leaving awkward empty space when few bins were present. Drop the fixed height so the card sizes to its tallest column and let the page scroll naturally if the bin grid overflows. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
119 lines
3.7 KiB
TypeScript
119 lines
3.7 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { parseAsString, useQueryState } from 'nuqs';
|
|
import { ChevronRight, MapPin } from 'lucide-react';
|
|
import { PageHeader } from '../components/layout/PageHeader.js';
|
|
import { SiteRoomTree } from '../components/locations/SiteRoomTree.js';
|
|
import { BinGrid } from '../components/locations/BinGrid.js';
|
|
import { listSites } from '../lib/api/sites.js';
|
|
import { listRooms } from '../lib/api/rooms.js';
|
|
import { queryKeys } from '../lib/queryKeys.js';
|
|
import { useAuth } from '../contexts/AuthContext.js';
|
|
|
|
export default function Locations() {
|
|
const { user } = useAuth();
|
|
const canEdit = user?.role === 'ADMIN';
|
|
|
|
const [siteId, setSiteId] = useQueryState('site', parseAsString);
|
|
const [roomId, setRoomId] = useQueryState('room', parseAsString);
|
|
|
|
const handleSite = (id: string) => {
|
|
void setSiteId(id || null);
|
|
void setRoomId(null);
|
|
};
|
|
const handleRoom = (id: string) => {
|
|
void setRoomId(id || null);
|
|
};
|
|
|
|
const sites = useQuery({
|
|
queryKey: queryKeys.sites.list({ pageSize: 100 }),
|
|
queryFn: () => listSites({ pageSize: 100 }),
|
|
});
|
|
const rooms = useQuery({
|
|
queryKey: queryKeys.rooms.list({ siteId, pageSize: 100 }),
|
|
queryFn: () => listRooms({ siteId: siteId!, pageSize: 100 }),
|
|
enabled: Boolean(siteId),
|
|
});
|
|
|
|
const siteName = useMemo(
|
|
() => sites.data?.data.find((s) => s.id === siteId)?.name,
|
|
[sites.data, siteId],
|
|
);
|
|
const roomName = useMemo(
|
|
() => rooms.data?.data.find((r) => r.id === roomId)?.name,
|
|
[rooms.data, roomId],
|
|
);
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<PageHeader
|
|
title="Locations"
|
|
description="Sites → Rooms → Bins. Pick a room to see its bins."
|
|
/>
|
|
<div className="grid grid-cols-[18rem_1fr] overflow-hidden rounded-lg border border-border bg-card">
|
|
<div className="border-r border-border">
|
|
<SiteRoomTree
|
|
siteId={siteId}
|
|
roomId={roomId}
|
|
onSelectSite={handleSite}
|
|
onSelectRoom={handleRoom}
|
|
canEdit={canEdit}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<Breadcrumb siteName={siteName} roomName={roomName} />
|
|
<div className="flex-1">
|
|
{roomId ? (
|
|
<BinGrid roomId={roomId} canEdit={canEdit} />
|
|
) : (
|
|
<EmptyPane siteSelected={Boolean(siteId)} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Breadcrumb({
|
|
siteName,
|
|
roomName,
|
|
}: {
|
|
siteName: string | undefined;
|
|
roomName: string | undefined;
|
|
}) {
|
|
return (
|
|
<div className="flex items-center gap-1.5 border-b border-border px-4 py-2 text-sm text-muted-foreground">
|
|
{siteName ? (
|
|
<>
|
|
<span className="text-foreground">{siteName}</span>
|
|
{roomName && (
|
|
<>
|
|
<ChevronRight className="h-3.5 w-3.5 opacity-60" />
|
|
<span className="text-foreground">{roomName}</span>
|
|
</>
|
|
)}
|
|
</>
|
|
) : (
|
|
<span>Select a site to begin.</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function EmptyPane({ siteSelected }: { siteSelected: boolean }) {
|
|
return (
|
|
<div className="flex h-full items-center justify-center p-8">
|
|
<div className="flex max-w-sm flex-col items-center gap-2 rounded-lg border border-dashed border-border bg-muted/30 px-8 py-10 text-center">
|
|
<MapPin className="h-6 w-6 text-muted-foreground" />
|
|
<p className="text-sm font-medium">
|
|
{siteSelected ? 'Pick a room' : 'Pick a site and room'}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
Bins live inside rooms. Expand a site in the tree and choose a room to manage its bins.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|