feat(site-scope): scope the Plant pages, drop their project dropdowns

This commit is contained in:
DEls 2026-09-11 23:14:08 +02:00
parent fe55a5d7df
commit 77ec2ec697
6 changed files with 156 additions and 114 deletions

View file

@ -1,4 +1,5 @@
import { pbAdmin } from "@/lib/pb";
import { composeFilter, filterOpt, getSiteScope, siteFilter, type SearchParams } from "@/lib/site-scope";
import {
PlantView,
type PlantRow,
@ -9,7 +10,13 @@ import type { PlantSiteOption } from "@/components/add-plant-button";
export const dynamic = "force-dynamic";
export default async function PlantPage() {
export default async function PlantPage({
searchParams,
}: {
searchParams: Promise<SearchParams>;
}) {
const scope = await getSiteScope(await searchParams);
const siteF = siteFilter(scope);
const pb = await pbAdmin();
// Plant bucket includes legacy empty-unit-type rows. The fuel page
@ -18,16 +25,17 @@ export default async function PlantPage() {
const [allVehicles, allFuelLogs, allSites] = await Promise.all([
pb.collection("vehicles").getFullList({
sort: "description",
perPage: 5000,
filter: `unit_type="plant" || unit_type="" || unit_type=null`,
filter: composeFilter(siteF, `unit_type="plant" || unit_type="" || unit_type=null`),
}),
pb.collection("fuel_logs").getFullList({
sort: "-date_logged",
perPage: 5000,
...filterOpt(siteF),
}),
// Sites is a lookup table for project_code display, not a data
// read — left unfiltered so the Add-plant dialog can still show
// every site even when this page is scoped.
pb.collection("sites").getFullList({
sort: "project_code",
perPage: 5000,
fields: "id,project_code,name",
}),
]);
@ -145,7 +153,7 @@ export default async function PlantPage() {
};
});
const projectCodes = Array.from(new Set(rows.map((r) => r.projectCode))).sort();
const projectCodes: string[] = []; // Project column visibility is controlled by showSiteColumn; the list is now derived server-side.
const sites: PlantSiteOption[] = allSites.map((s) => ({
id: s.id,
@ -179,6 +187,7 @@ export default async function PlantPage() {
logs={logs}
projectCodes={projectCodes}
sites={sites}
showSiteColumn={scope.mode === "all"}
kpis={kpis}
vehicleBasePath="/dashboard/plant/equipment"
title="Equipment (L/h)"

View file

@ -135,6 +135,7 @@ export function PlantView({
logs,
projectCodes,
sites,
showSiteColumn,
kpis,
vehicleBasePath,
title,
@ -144,13 +145,13 @@ export function PlantView({
logs: PlantLogRow[];
projectCodes: string[];
sites: PlantSiteOption[];
showSiteColumn: boolean;
kpis: PlantKpis;
vehicleBasePath: string;
title: string;
subtitle?: string;
}) {
const router = useRouter();
const [projectFilter, setProjectFilter] = React.useState<string>("all");
const [search, setSearch] = React.useState("");
const [page, setPage] = React.useState(1);
// Independent pagination for the bottom "Plant log entries" table.
@ -184,15 +185,14 @@ export function PlantView({
const q = search.trim().toLowerCase();
return rows.filter((r) => {
if (removedIds.has(r.id)) return false;
if (projectFilter !== "all" && r.projectCode !== projectFilter) return false;
if (q && !r.description.toLowerCase().includes(q)) return false;
return true;
});
}, [rows, projectFilter, search, removedIds]);
}, [rows, search, removedIds]);
React.useEffect(() => {
setPage(1);
}, [projectFilter, search]);
}, [search]);
const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
const pageRows = React.useMemo(
@ -249,7 +249,7 @@ export function PlantView({
}
}
const filtersActive = projectFilter !== "all" || search !== "";
const filtersActive = search !== "";
// Inline-edit hook for the description column. The page-level toggle
// controls this state via the EditModeToggle rendered in the Topbar.
@ -392,31 +392,11 @@ export function PlantView({
/>
</div>
<div className="flex items-center gap-1.5">
<span className="flex items-center gap-1 text-xs text-muted-foreground">
<Filter className="h-3.5 w-3.5" /> Project
</span>
<Select value={projectFilter} onValueChange={setProjectFilter}>
<SelectTrigger className="w-[160px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All projects</SelectItem>
{projectCodes.map((c) => (
<SelectItem key={c} value={c}>
{c}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{filtersActive && (
<Button
variant="ghost"
size="sm"
onClick={() => {
setProjectFilter("all");
setSearch("");
}}
>
@ -461,6 +441,7 @@ export function PlantView({
}}
onDelete={edit.editing ? () => void handleDelete(r) : null}
deleting={deletingId === r.id}
showSiteColumn={showSiteColumn}
/>
</li>
))}
@ -472,7 +453,7 @@ export function PlantView({
<TableRow>
<TableHead>Description</TableHead>
<TableHead>QR code</TableHead>
<TableHead>Project</TableHead>
{showSiteColumn && <TableHead>Project</TableHead>}
<TableHead className="text-right">Hours</TableHead>
<TableHead className="text-right">Value</TableHead>
<TableHead className="text-right">Logs</TableHead>
@ -545,11 +526,13 @@ export function PlantView({
<span className="text-xs text-muted-foreground">—</span>
)}
</TableCell>
<TableCell>
<Badge variant="outline" className="font-mono text-xs">
{r.projectCode || "—"}
</Badge>
</TableCell>
{showSiteColumn && (
<TableCell>
<Badge variant="outline" className="font-mono text-xs">
{r.projectCode || "—"}
</Badge>
</TableCell>
)}
<TableCell className="text-right tabular-nums">
{r.totalHours > 0
? `${formatNumber(r.totalHours, { maximumFractionDigits: 1 })} h`
@ -647,7 +630,7 @@ export function PlantView({
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Vehicle</TableHead>
<TableHead>Project</TableHead>
{showSiteColumn && <TableHead>Project</TableHead>}
<TableHead className="text-right">Hour meter</TableHead>
<TableHead className="text-right">Hours</TableHead>
<TableHead className="text-right">Fuel (L)</TableHead>
@ -667,11 +650,13 @@ export function PlantView({
<TableCell>
<div className="font-medium">{l.vehicleDescription}</div>
</TableCell>
<TableCell>
<Badge variant="outline" className="font-mono text-xs">
{l.projectCode || "—"}
</Badge>
</TableCell>
{showSiteColumn && (
<TableCell>
<Badge variant="outline" className="font-mono text-xs">
{l.projectCode || "—"}
</Badge>
</TableCell>
)}
<TableCell className="text-right tabular-nums">
{formatNumber(l.odoReading)}
</TableCell>
@ -834,6 +819,7 @@ function PlantMobileCard({
edit,
onDelete,
deleting,
showSiteColumn,
}: {
r: PlantRow;
href: string;
@ -847,6 +833,7 @@ function PlantMobileCard({
};
onDelete: (() => void) | null;
deleting: boolean;
showSiteColumn: boolean;
}) {
return (
<div
@ -894,9 +881,11 @@ function PlantMobileCard({
</div>
<div className="flex items-center justify-between gap-2 text-xs text-muted-foreground">
<div className="flex min-w-0 flex-wrap items-center gap-1.5">
<Badge variant="outline" className="font-mono text-[10px]">
{r.projectCode || "—"}
</Badge>
{showSiteColumn && (
<Badge variant="outline" className="font-mono text-[10px]">
{r.projectCode || "—"}
</Badge>
)}
{r.qrCodeId && (
<span className="font-mono text-[10px]">QR {r.qrCodeId}</span>
)}

View file

@ -3,23 +3,38 @@ import { Topbar } from "@/components/layout/sidebar";
import { ImportFuelLogsButton } from "@/components/import-fuel-logs-button";
import { resolveFuelPrice } from "@/lib/metrics";
import { loadFuelPrices } from "@/lib/fuel-prices";
import { filterOpt, getSiteScope, siteFilter, siteFilterAllowingUnassigned, type SearchParams } from "@/lib/site-scope";
import { FuelLogsView, type FuelLogRow } from "./fuel-logs-view";
import { deriveProjectPeriodLabel } from "@/lib/project-calendar";
export const dynamic = "force-dynamic";
export default async function FuelLogsPage() {
export default async function FuelLogsPage({
searchParams,
}: {
searchParams: Promise<SearchParams>;
}) {
const scope = await getSiteScope(await searchParams);
const siteF = siteFilter(scope);
const pb = await pbAdmin();
const [logs, vehicles, sites, documents] = await Promise.all([
pb.collection("fuel_logs").getFullList({
sort: "-date_logged",
expand: "vehicle,fuel_sheet",
...filterOpt(siteF),
}),
pb.collection("vehicles").getFullList(),
// Sites is a lookup / Sites dropdown data — kept unfiltered.
pb.collection("sites").getFullList({ sort: "name" }),
pb.collection("fuel_sheets").getFullList({
fields: "id,processing_status",
...filterOpt(
// A pump sheet legitimately spans sites and carries site="" by
// design (src/lib/pb-types.ts:353-358). A plain siteFilter would
// make every multi-site sheet vanish from upload status.
siteFilterAllowingUnassigned(scope),
),
}),
]);

View file

@ -12,19 +12,27 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { loadFuelPrices } from "@/lib/fuel-prices";
import { resolveFuelPrice } from "@/lib/metrics";
import { filterOpt, getSiteScope, composeFilter, siteFilter, type SearchParams, type SiteScope } from "@/lib/site-scope";
import { formatCurrency, formatNumber } from "@/lib/utils";
export const dynamic = "force-dynamic";
async function loadPlantSummary() {
async function loadPlantSummary(scope: SiteScope) {
const pb = await pbAdmin();
const siteF = siteFilter(scope);
const now = new Date();
const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
const monthStartIso = monthStart.toISOString().slice(0, 10);
const [allVehicles, allFuelLogs, fuelPrices] = await Promise.all([
pb.collection("vehicles").getFullList({ sort: "description" }),
pb.collection("fuel_logs").getFullList({ sort: "-date_logged" }),
pb.collection("vehicles").getFullList({
sort: "description",
filter: composeFilter(siteF, `unit_type="fuel" || unit_type=null || unit_type=""`),
}),
pb.collection("fuel_logs").getFullList({
sort: "-date_logged",
...filterOpt(siteF),
}),
loadFuelPrices(),
]);
@ -94,8 +102,13 @@ async function loadPlantSummary() {
};
}
export default async function PlantLanding() {
const summary = await loadPlantSummary();
export default async function PlantLanding({
searchParams,
}: {
searchParams: Promise<SearchParams>;
}) {
const scope = await getSiteScope(await searchParams);
const summary = await loadPlantSummary(scope);
return (
<>

View file

@ -195,7 +195,7 @@ const DISMISS_ERROR_COPY: Record<string, string> = {
export function FuelView({
rows,
vehicles,
projectCodes,
showSiteColumn,
kpis,
vehicleBasePath,
title,
@ -203,7 +203,7 @@ export function FuelView({
}: {
rows: FuelRow[];
vehicles: VehicleSummary[];
projectCodes: string[];
showSiteColumn: boolean;
kpis: FuelKpis;
vehicleBasePath: string;
title: string;
@ -214,7 +214,6 @@ export function FuelView({
[vehicleBasePath],
);
const router = useRouter();
const [projectFilter, setProjectFilter] = React.useState<string>("all");
const [statusFilter, setStatusFilter] = React.useState<string>("all");
const [search, setSearch] = React.useState("");
const [page, setPage] = React.useState(1);
@ -231,7 +230,6 @@ export function FuelView({
const filtered = React.useMemo(() => {
const q = search.trim().toLowerCase();
return rows.filter((r) => {
if (projectFilter !== "all" && r.projectCode !== projectFilter) return false;
if (statusFilter !== "all") {
if (statusFilter === "flagged" && r.status !== "warn" && r.status !== "alert") return false;
if (statusFilter !== "flagged" && r.status !== statusFilter) return false;
@ -239,17 +237,16 @@ export function FuelView({
if (q && !r.vehicleDescription.toLowerCase().includes(q)) return false;
return true;
});
}, [rows, projectFilter, statusFilter, search]);
}, [rows, statusFilter, search]);
// Vehicles shown in the cards — same filters as the table apply
const filteredVehicles = React.useMemo(() => {
const q = search.trim().toLowerCase();
return vehicles.filter((v) => {
if (projectFilter !== "all" && v.projectCode !== projectFilter) return false;
if (q && !v.description.toLowerCase().includes(q)) return false;
return true;
});
}, [vehicles, projectFilter, search]);
}, [vehicles, search]);
const vehiclesWithOverlay = React.useMemo(
() =>
@ -276,7 +273,7 @@ export function FuelView({
React.useEffect(() => {
setPage(1);
setFleetPage(1);
}, [projectFilter, statusFilter, search]);
}, [statusFilter, search]);
const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
const pageRows = React.useMemo(
@ -297,8 +294,7 @@ export function FuelView({
[filteredVehicles, fleetPage],
);
const filtersActive =
projectFilter !== "all" || statusFilter !== "all" || search !== "";
const filtersActive = statusFilter !== "all" || search !== "";
// Inline-edit hook for vehicle descriptions. Toggle lives in the
// Topbar's right slot (next to the Import button); banner shows
@ -468,24 +464,7 @@ export function FuelView({
/>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Filter className="h-3.5 w-3.5" /> Project
</span>
<Select value={projectFilter} onValueChange={setProjectFilter}>
<SelectTrigger className="w-[160px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All projects</SelectItem>
{projectCodes.map((c) => (
<SelectItem key={c} value={c}>
{c}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
@ -512,7 +491,6 @@ export function FuelView({
variant="ghost"
size="sm"
onClick={() => {
setProjectFilter("all");
setStatusFilter("all");
setSearch("");
}}
@ -565,6 +543,7 @@ export function FuelView({
onDismiss={handleDismiss}
busy={busyId === v.id}
disabled={busyId !== null}
showSiteColumn={showSiteColumn}
/>
</li>
))}
@ -579,6 +558,7 @@ export function FuelView({
onDismiss={handleDismiss}
busy={busyId === v.id}
disabled={busyId !== null}
showSiteColumn={showSiteColumn}
/>
</li>
))}
@ -622,6 +602,7 @@ export function FuelView({
unitType: edit.valueOf(v.id, "unitType", v.unitType),
onUnitTypeChange: (next) => edit.setValue(v.id, "unitType", next),
}}
showSiteColumn={showSiteColumn}
/>
</li>
))}
@ -633,7 +614,7 @@ export function FuelView({
<TableRow>
<TableHead>Description</TableHead>
<TableHead>QR code</TableHead>
<TableHead>Project</TableHead>
{showSiteColumn && <TableHead>Project</TableHead>}
<TableHead className="text-right">Litres</TableHead>
<TableHead className="text-right">Spend</TableHead>
<TableHead className="text-right">Txns</TableHead>
@ -659,6 +640,7 @@ export function FuelView({
edit.isDirty(v.id) &&
edit.valueOf(v.id, "unitType", v.unitType) !== v.unitType,
}}
showSiteColumn={showSiteColumn}
/>
))}
</TableBody>
@ -702,7 +684,7 @@ export function FuelView({
) : (
pageRows.map((r) => (
<li key={r.id}>
<TransactionMobileCard r={r} href={vehicleHref(r.vehicleId)} />
<TransactionMobileCard r={r} href={vehicleHref(r.vehicleId)} showSiteColumn={showSiteColumn} />
</li>
))
)}
@ -713,7 +695,7 @@ export function FuelView({
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Project</TableHead>
{showSiteColumn && <TableHead>Project</TableHead>}
<TableHead>Vehicle</TableHead>
<TableHead className="text-right">Odo / meter</TableHead>
<TableHead className="text-right">Litres</TableHead>
@ -743,11 +725,13 @@ export function FuelView({
<TableCell className="text-xs tabular-nums">
{r.date ? format(parseISO(r.date), "yyyy-MM-dd") : "—"}
</TableCell>
<TableCell>
<Badge variant="outline" className="font-mono text-xs">
{r.projectCode || "—"}
</Badge>
</TableCell>
{showSiteColumn && (
<TableCell>
<Badge variant="outline" className="font-mono text-xs">
{r.projectCode || "—"}
</Badge>
</TableCell>
)}
<TableCell>
<Link
href={vehicleHref(r.vehicleId)}
@ -823,12 +807,14 @@ function AlertCard({
onDismiss,
busy,
disabled,
showSiteColumn,
}: {
v: VehicleSummary;
href: string;
onDismiss: (id: string) => void;
busy: boolean;
disabled: boolean;
showSiteColumn: boolean;
}) {
const dominant = v.alert > 0 ? "alert" : "warn";
const toneClasses =
@ -860,9 +846,11 @@ function AlertCard({
QR {v.qrCodeId}
</p>
)}
<Badge variant="outline" className="mt-2 font-mono text-xs">
{v.projectCode || "—"}
</Badge>
{showSiteColumn && (
<Badge variant="outline" className="mt-2 font-mono text-xs">
{v.projectCode || "—"}
</Badge>
)}
</div>
<ArrowRight
className="mt-1 h-4 w-4 shrink-0 text-muted-foreground"
@ -924,12 +912,14 @@ function AlertListRow({
onDismiss,
busy,
disabled,
showSiteColumn,
}: {
v: VehicleSummary;
href: string;
onDismiss: (id: string) => void;
busy: boolean;
disabled: boolean;
showSiteColumn: boolean;
}) {
const dominant = v.alert > 0 ? "alert" : "warn";
const stripeColor =
@ -951,9 +941,11 @@ function AlertListRow({
QR {v.qrCodeId}
</span>
)}
<Badge variant="outline" className="shrink-0 font-mono text-[10px]">
{v.projectCode || "—"}
</Badge>
{showSiteColumn && (
<Badge variant="outline" className="shrink-0 font-mono text-[10px]">
{v.projectCode || "—"}
</Badge>
)}
</div>
<div className="flex shrink-0 items-center gap-1.5">
{v.alert > 0 && (
@ -1010,6 +1002,7 @@ function FleetMobileCard({
v,
href,
edit,
showSiteColumn,
}: {
v: VehicleSummary;
href: string;
@ -1021,6 +1014,7 @@ function FleetMobileCard({
unitType: "fuel" | "plant";
onUnitTypeChange: (next: "fuel" | "plant") => void;
};
showSiteColumn: boolean;
}) {
return (
edit.editing ? (
@ -1049,9 +1043,11 @@ function FleetMobileCard({
</div>
<div className="flex items-center justify-between text-xs text-muted-foreground">
<div className="flex min-w-0 items-center gap-2">
<Badge variant="outline" className="font-mono text-[10px]">
{v.projectCode || "—"}
</Badge>
{showSiteColumn && (
<Badge variant="outline" className="font-mono text-[10px]">
{v.projectCode || "—"}
</Badge>
)}
{v.fuelLimits && (
<span className="whitespace-nowrap font-medium tabular-nums" title={v.fuelLimits}>{v.fuelLimits}</span>
)}
@ -1098,9 +1094,11 @@ function FleetMobileCard({
</div>
<div className="flex items-center justify-between text-xs text-muted-foreground">
<div className="flex min-w-0 items-center gap-2">
<Badge variant="outline" className="font-mono text-[10px]">
{v.projectCode || "—"}
</Badge>
{showSiteColumn && (
<Badge variant="outline" className="font-mono text-[10px]">
{v.projectCode || "—"}
</Badge>
)}
<UnitTypePill value={v.unitType} />
{v.fuelLimits && (
<span className="whitespace-nowrap font-medium tabular-nums" title={v.fuelLimits}>{v.fuelLimits}</span>
@ -1164,7 +1162,7 @@ function DescriptionInput({
);
}
function TransactionMobileCard({ r, href }: { r: FuelRow; href: string }) {
function TransactionMobileCard({ r, href, showSiteColumn }: { r: FuelRow; href: string; showSiteColumn: boolean }) {
return (
<Link
href={href}
@ -1186,7 +1184,7 @@ function TransactionMobileCard({ r, href }: { r: FuelRow; href: string }) {
<span className="tabular-nums">
{r.date ? format(parseISO(r.date), "yyyy-MM-dd") : "—"}
</span>
{r.projectCode && (
{showSiteColumn && r.projectCode && (
<>
<span aria-hidden>·</span>
<Badge variant="outline" className="font-mono text-[10px]">
@ -1224,6 +1222,7 @@ function FleetRow({
v,
href,
edit,
showSiteColumn,
}: {
v: VehicleSummary;
href: string;
@ -1236,6 +1235,7 @@ function FleetRow({
onUnitTypeChange: (next: "fuel" | "plant") => void;
dirtyUnitType: boolean;
};
showSiteColumn: boolean;
}) {
const router = useRouter();
const handleNav = React.useCallback(() => router.push(href), [router, href]);
@ -1286,11 +1286,13 @@ function FleetRow({
<span className="text-xs text-muted-foreground">—</span>
)}
</TableCell>
<TableCell>
<Badge variant="outline" className="font-mono text-xs">
{v.projectCode || "—"}
</Badge>
</TableCell>
{showSiteColumn && (
<TableCell>
<Badge variant="outline" className="font-mono text-xs">
{v.projectCode || "—"}
</Badge>
</TableCell>
)}
<TableCell className="text-right tabular-nums">
{v.totalLitres > 0
? `${formatNumber(v.totalLitres, { maximumFractionDigits: 1 })} L`

View file

@ -1,6 +1,13 @@
import { pbAdmin } from "@/lib/pb";
import { loadFuelPrices } from "@/lib/fuel-prices";
import { parseFuelLimit } from "@/lib/fuel-variance";
import {
composeFilter,
filterOpt,
getSiteScope,
siteFilter,
type SearchParams,
} from "@/lib/site-scope";
import {
computeFuelLogStatuses,
resolveFuelPrice,
@ -11,7 +18,13 @@ import { FuelView, type FuelRow, type VehicleSummary } from "./fuel-view";
export const dynamic = "force-dynamic";
export default async function FuelPage() {
export default async function FuelPage({
searchParams,
}: {
searchParams: Promise<SearchParams>;
}) {
const scope = await getSiteScope(await searchParams);
const siteF = siteFilter(scope);
const pb = await pbAdmin();
const [vehicles, fuelLogs, fuelPrices, sites] = await Promise.all([
@ -20,10 +33,13 @@ export default async function FuelPage() {
// appear on the Fleet page where the user expects them)
pb.collection("vehicles").getFullList({
sort: "description",
filter: `unit_type="fuel" || unit_type=null || unit_type=""`,
filter: composeFilter(siteF, `unit_type="fuel" || unit_type=null || unit_type=""`),
}),
pb.collection("fuel_logs").getFullList({ expand: "vehicle" }),
pb.collection("fuel_logs").getFullList({ expand: "vehicle", ...filterOpt(siteF) }),
loadFuelPrices(),
// Sites is a lookup table for project_code display, not a data
// read — left unfiltered so project_code resolution still works
// when this page is scoped to a single site.
pb.collection("sites").getFullList({ fields: "id,project_code" }),
]);
@ -164,13 +180,11 @@ export default async function FuelPage() {
};
});
const projectCodes = Array.from(new Set(rows.map((r) => r.projectCode))).sort();
return (
<FuelView
rows={rows}
vehicles={vehicleSummaries}
projectCodes={projectCodes}
showSiteColumn={scope.mode === "all"}
kpis={{
vehicles: vehicles.length,
totalLitres,