Files
Vector/apps/web/src/pages/Locations.tsx
T
josh 13e3444258
CI / Lint · Typecheck · Test · Build (push) Successful in 47s
CI / Playwright (smoke) (push) Has been skipped
CI / Build & push images (push) Successful in 1m4s
fix(locations): size card to content instead of full viewport
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>
2026-04-17 14:38:34 -04:00

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