feat(site-scope): exempt the Sites list, add a per-row scope action
This commit is contained in:
parent
5edea14105
commit
729118d5dd
3 changed files with 61 additions and 2 deletions
|
|
@ -12,6 +12,8 @@ import {
|
|||
} from "@/lib/metrics";
|
||||
import { Topbar } from "@/components/layout/sidebar";
|
||||
import { SitesView, type SiteSummary } from "./sites-view";
|
||||
import { SetScopeButton } from "@/components/layout/set-scope-button";
|
||||
import { getSiteScope, type SearchParams } from "@/lib/site-scope";
|
||||
import {
|
||||
AddSiteButton,
|
||||
type SiteTemplateOption,
|
||||
|
|
@ -19,9 +21,20 @@ import {
|
|||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function SitesPage() {
|
||||
export default async function SitesPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<SearchParams>;
|
||||
}) {
|
||||
const pb = await pbAdmin();
|
||||
|
||||
// Sites list is intentionally exempt from scope (see plan §12):
|
||||
// scoping it to one site would leave a one-row comparison table.
|
||||
// We still read the allowed list so we can hide sites the user may
|
||||
// not see, when PocketBase users land.
|
||||
const scope = await getSiteScope(await searchParams);
|
||||
const allowedIds = new Set(scope.allowed.map((s) => s.id));
|
||||
|
||||
const [
|
||||
sites,
|
||||
fixedCosts,
|
||||
|
|
@ -117,7 +130,9 @@ export default async function SitesPage() {
|
|||
incomeBySite.set(c.site, list);
|
||||
}
|
||||
|
||||
const summaries: SiteSummary[] = sites.map((s) => {
|
||||
const summaries: SiteSummary[] = sites
|
||||
.filter((s) => allowedIds.has(s.id))
|
||||
.map((s) => {
|
||||
const siteFuel = fuelBySite.get(s.id) ?? [];
|
||||
|
||||
const spend = computeSiteSpend({
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from "lucide-react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { SetScopeButton } from "@/components/layout/set-scope-button";
|
||||
import { cn, formatCurrency, formatNumber } from "@/lib/utils";
|
||||
|
||||
export type SiteStatus = "just-started" | "under" | "on-track" | "over";
|
||||
|
|
@ -239,6 +240,13 @@ function SiteCard({ site }: { site: SiteSummary }) {
|
|||
{formatCurrency(site.projectedProfit)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Per-row scope action. Sits at the bottom of the card, on its
|
||||
own row, so it never shares a hit target with the row's own
|
||||
`Link` to the detail page. */}
|
||||
<div className="flex items-center justify-end pt-1">
|
||||
<SetScopeButton siteId={site.id} siteName={site.name} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
|||
36
src/components/layout/set-scope-button.tsx
Normal file
36
src/components/layout/set-scope-button.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
"use client";
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { Crosshair } from "lucide-react";
|
||||
import { SITE_SCOPE_COOKIE, SITE_SCOPE_PARAM } from "@/lib/site-scope";
|
||||
|
||||
/**
|
||||
* Sets the global scope to one site from the Sites list. Deliberately
|
||||
* NOT wired to the row's link: navigating must never silently mutate
|
||||
* scope, so choosing a scope is its own explicit action.
|
||||
*/
|
||||
export function SetScopeButton({ siteId, siteName }: { siteId: string; siteName: string }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const params = useSearchParams();
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`View only ${siteName}`}
|
||||
title={`View only ${siteName}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
document.cookie = `${SITE_SCOPE_COOKIE}=${siteId}; path=/; max-age=31536000; samesite=lax`;
|
||||
const next = new URLSearchParams(params.toString());
|
||||
next.set(SITE_SCOPE_PARAM, siteId);
|
||||
router.replace(`${pathname}?${next.toString()}`, { scroll: false });
|
||||
}}
|
||||
className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<Crosshair className="h-3.5 w-3.5" />
|
||||
View only this site
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue