feat(collection-map): drop date filter; thread ready-gate into assign dialog
This commit is contained in:
parent
b5cd061da1
commit
8c21347ecc
2 changed files with 46 additions and 103 deletions
|
|
@ -68,31 +68,37 @@ describe("splitSupplierBucket", () => {
|
|||
|
||||
describe("collection-map toolbar shape contract", () => {
|
||||
// The toolbar lives in page.tsx. We snapshot the literal labels that must
|
||||
// remain visible (date + quick chips + Refresh + overlays) and the labels
|
||||
// that must NOT come back (the deleted filter chips + Drivers card).
|
||||
// These are static source-text assertions: a future PR that re-adds the
|
||||
// filter chips won't pass the pipeline, even if the chips "look right".
|
||||
// remain visible (Refresh) and the labels / state / JSX that must NOT
|
||||
// come back (the deleted date picker + quick chips + filter chips +
|
||||
// Drivers card). These are static source-text assertions: a future PR
|
||||
// that re-adds removed controls won't pass the pipeline, even if the
|
||||
// controls "look right".
|
||||
const src = fs.readFileSync(path.resolve(__dirname, "./page.tsx"), "utf8");
|
||||
|
||||
it("renders the locked toolbar labels", () => {
|
||||
// Quick-chip labels.
|
||||
expect(src).toMatch(/Today/);
|
||||
expect(src).toMatch(/Tomorrow/);
|
||||
expect(src).toMatch(/\+3 days/);
|
||||
expect(src).toMatch(/Next week/);
|
||||
// Overlay buttons + manual refresh.
|
||||
it("renders only the Refresh control in the page toolbar", () => {
|
||||
expect(src).toMatch(/Refresh/);
|
||||
// Quick-chip labels deleted in 2026-07-12 (date-filter removal).
|
||||
expect(src).not.toMatch(/Tomorrow/);
|
||||
expect(src).not.toMatch(/\+3 days/);
|
||||
expect(src).not.toMatch(/Next week/);
|
||||
// Map chrome (not page toolbar) — keep these locked in too so the
|
||||
// overlay buttons don't regress.
|
||||
expect(src).toMatch(/Traffic/);
|
||||
expect(src).toMatch(/Fullscreen|Exit fullscreen|Minimize2/);
|
||||
expect(src).toMatch(/Refresh/);
|
||||
});
|
||||
|
||||
it("does not re-introduce the deleted filter chips", () => {
|
||||
// State + JSX for the deleted chips — must not silently come back.
|
||||
it("does not re-introduce the deleted filter chips, search, or Drivers card", () => {
|
||||
expect(src).not.toMatch(/setStatusFilter|statusFilter|StatusFilter/);
|
||||
expect(src).not.toMatch(/setReadyFilter|readyFilter/);
|
||||
// Search input + placeholder.
|
||||
expect(src).not.toMatch(/Search PO, supplier/);
|
||||
// Drivers sidebar card heading.
|
||||
expect(src).not.toMatch(/>\s*Drivers\s*</);
|
||||
});
|
||||
|
||||
it("does not re-introduce the date state or date bounds in fetchRows", () => {
|
||||
// State setter must be gone.
|
||||
expect(src).not.toMatch(/useState\(ymdToday\(\)\)/);
|
||||
// The day-window filter string must be gone from both queries.
|
||||
expect(src).not.toMatch(/assigned_at >= "\$\{date\}T00:00:00\.000Z"/);
|
||||
expect(src).not.toMatch(/assigned_at <= "\$\{date\}T23:59:59\.999Z"/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -36,13 +36,12 @@ import {
|
|||
useMap,
|
||||
useMapEvents,
|
||||
} from "react-leaflet";
|
||||
import { Calendar, ExternalLink, MapPin, RefreshCw, Truck, User as UserIcon, Users, X, Layers } from "lucide-react";
|
||||
import { ExternalLink, MapPin, RefreshCw, Truck, User as UserIcon, Users, X, Layers } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Maximize2, Minimize2 } from "lucide-react";
|
||||
import { AssignDriverDialog, type AssignDriverSubmitPayload } from "@/components/ui/assign-driver-dialog";
|
||||
import { getPocketBase } from "@/lib/pocketbase";
|
||||
|
|
@ -146,22 +145,6 @@ type LookupUser = Pick<UserType, "id" | "name" | "email">;
|
|||
* Helpers
|
||||
* ========================================================================= */
|
||||
|
||||
function ymdToday(): string {
|
||||
const now = new Date();
|
||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function ymdForDate(d: Date): string {
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function addDays(ymd: string, days: number): string {
|
||||
const [y, m, d] = ymd.split("-").map(Number);
|
||||
const date = new Date(Date.UTC(y, m - 1, d));
|
||||
date.setUTCDate(date.getUTCDate() + days);
|
||||
return ymdForDate(date);
|
||||
}
|
||||
|
||||
function formatDateLabel(ymd: string): string {
|
||||
const [y, m, d] = ymd.split("-").map(Number);
|
||||
const date = new Date(y, m - 1, d);
|
||||
|
|
@ -283,7 +266,10 @@ export default function CollectionMapPage() {
|
|||
const [items, setItems] = useState<StopItem[]>([]);
|
||||
const [suppliersWithoutCoords, setSuppliersWithoutCoords] = useState<string[]>([]);
|
||||
|
||||
const [date, setDate] = useState(ymdToday());
|
||||
// The page no longer exposes a date filter; the map shows every
|
||||
// assigned + collection_pool item regardless of `assigned_at`. The
|
||||
// assign dialog still picks a date internally ("when is the pickup")
|
||||
// — that's a per-row concern, not a page-level filter.
|
||||
// Traffic overlay toggle — default OFF to conserve the TomTom
|
||||
// freemium 2,500-tile/day budget. Operators can flip it on for a
|
||||
// single planning session; Leaflet caches tiles client-side so
|
||||
|
|
@ -307,23 +293,10 @@ export default function CollectionMapPage() {
|
|||
setLoading(true);
|
||||
try {
|
||||
const pb = getPocketBase();
|
||||
// Build a UTC ISO 8601 day window. `assigned_at` is stored at
|
||||
// "YYYY-MM-DDT00:00:00.000Z" (see /api/v1/collection-map/items/[id]/assign),
|
||||
// so the bounds need the same "T" and trailing "Z" or string
|
||||
// comparison puts `T` (0x54) before the space (0x20) and the bound
|
||||
// excludes every real row.
|
||||
const dayStart = `${date}T00:00:00.000Z`;
|
||||
const dayEnd = `${date}T23:59:59.999Z`;
|
||||
// Union of:
|
||||
// (a) assigned items whose assigned_at lands on the picked date,
|
||||
// (b) un-dated pool items (collection_pool + empty assigned_at) so
|
||||
// the operator can plan them visually on the map.
|
||||
// Items scheduled for a *future* date stay hidden until that date
|
||||
// — the map's date picker still means "the day I'm planning for".
|
||||
const filter = [
|
||||
`(item_status = "assigned" || item_status = "collection_pool")`,
|
||||
`((assigned_at >= "${dayStart}" && assigned_at <= "${dayEnd}") || (assigned_at = "" || assigned_at = null))`,
|
||||
].join(" && ");
|
||||
// The map shows every assigned + collection_pool row regardless of
|
||||
// `assigned_at` — operators plan across all open pickups, not just
|
||||
// today's. The page no longer exposes a date filter.
|
||||
const filter = `(item_status = "assigned" || item_status = "collection_pool")`;
|
||||
|
||||
const rows = (await pb.collection("order_items").getFullList({
|
||||
filter,
|
||||
|
|
@ -367,7 +340,7 @@ export default function CollectionMapPage() {
|
|||
// Also collect items whose suppliers have NO coordinates — show in
|
||||
// the sidebar so the operator knows they exist but can't be mapped.
|
||||
const unmappedRows = (await pb.collection("order_items").getFullList({
|
||||
filter: `(item_status = "assigned" || item_status = "collection_pool") && ((assigned_at >= "${date}T00:00:00.000Z" && assigned_at <= "${date}T23:59:59.999Z") || assigned_at = "" || assigned_at = null)`,
|
||||
filter: `(item_status = "assigned" || item_status = "collection_pool")`,
|
||||
expand: "purchase_order.supplier",
|
||||
fields: "id,purchase_order",
|
||||
$autoCancel: false,
|
||||
|
|
@ -388,7 +361,8 @@ export default function CollectionMapPage() {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [date]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const fetchUsers = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -521,9 +495,7 @@ export default function CollectionMapPage() {
|
|||
/* -- assign dialog -------------------------------------------------------- */
|
||||
const openAssignDialog = useCallback(
|
||||
(row: StopItem) => {
|
||||
const initialDate = row.assigned_at
|
||||
? row.assigned_at.slice(0, 10)
|
||||
: date;
|
||||
const initialDate = row.assigned_at ? row.assigned_at.slice(0, 10) : "";
|
||||
setAssignDialog({
|
||||
open: true,
|
||||
row,
|
||||
|
|
@ -531,7 +503,8 @@ export default function CollectionMapPage() {
|
|||
initialDate,
|
||||
});
|
||||
},
|
||||
[date]
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
const submitAssign = useCallback(
|
||||
|
|
@ -599,7 +572,7 @@ export default function CollectionMapPage() {
|
|||
<CardContent>
|
||||
<p className="text-3xl font-bold">{stopCount}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
For {formatDateLabel(date)} across {stopsWithCoords.length} supplier{stopsWithCoords.length === 1 ? "" : "s"}
|
||||
Across {stopsWithCoords.length} supplier{stopsWithCoords.length === 1 ? "" : "s"}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -624,50 +597,11 @@ export default function CollectionMapPage() {
|
|||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/* Date picker */}
|
||||
<div className="relative">
|
||||
<Calendar className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
type="date"
|
||||
value={date}
|
||||
onValueChange={setDate}
|
||||
className="h-9 w-[170px] pl-9 text-sm"
|
||||
aria-label="Scheduled date"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{[
|
||||
{ label: "Today", offset: 0 },
|
||||
{ label: "Tomorrow", offset: 1 },
|
||||
{ label: "+3 days", offset: 3 },
|
||||
{ label: "Next week", offset: 7 },
|
||||
].map((q) => {
|
||||
const target = addDays(ymdToday(), q.offset);
|
||||
const active = target === date;
|
||||
return (
|
||||
<Button
|
||||
key={q.label}
|
||||
type="button"
|
||||
size="sm"
|
||||
variant={active ? "default" : "secondary"}
|
||||
onClick={() => setDate(target)}
|
||||
className="h-9"
|
||||
>
|
||||
{q.label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button variant="outline" size="sm" className="h-9" onClick={fetchRows} disabled={loading}>
|
||||
<RefreshCw className={cn("mr-1.5 h-4 w-4", loading && "animate-spin")} />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
<Button variant="outline" size="sm" className="h-9" onClick={fetchRows} disabled={loading}>
|
||||
<RefreshCw className={cn("mr-1.5 h-4 w-4", loading && "animate-spin")} />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid items-start gap-5 xl:grid-cols-[minmax(0,1fr)_320px]">
|
||||
|
|
@ -907,6 +841,9 @@ export default function CollectionMapPage() {
|
|||
onSubmit={submitAssign}
|
||||
initialDriverId={assignDialog.initialDriverId}
|
||||
initialDate={assignDialog.initialDate}
|
||||
requireReadyForCollection
|
||||
readyForCollection={assignDialog.row?.ready_for_collection ?? false}
|
||||
busy={loading}
|
||||
eyebrow={
|
||||
assignDialog.row ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
|
|
|
|||
Loading…
Reference in a new issue