feat(history): allow filtering PO history by multiple statuses

The PO history page previously allowed only a single status filter. This
enhances it to accept multiple statuses that are OR-ed together (e.g.
Closed + Approved shows all POs in either state), as requested.

- Status filter is now a multi-select checkbox dropdown that serialises
  selections as repeated `status` query params.
- History page and the reports export endpoint read all `status` values
  and query with `status: { in: [...] }` (OR semantics).
- Single-status and no-status cases remain unchanged.

Verified OR-query semantics against the test DB and confirmed both routes
compile and respond. type-check passes for the changed files.

Fixes #31

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude (auto-fix) 2026-06-19 11:53:42 +05:30
parent 7daf3091bc
commit e94c7f99a3
3 changed files with 55 additions and 16 deletions

View file

@ -1,10 +1,9 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
const STATUSES = [
{ value: "", label: "All statuses" },
{ value: "DRAFT", label: "Draft" },
{ value: "SUBMITTED", label: "Submitted" },
{ value: "MGR_REVIEW", label: "Pending Approval" },
@ -28,23 +27,48 @@ export function HistoryFilters({ vessels }: Props) {
const [dateFrom, setDateFrom] = useState(sp.get("dateFrom") ?? "");
const [dateTo, setDateTo] = useState(sp.get("dateTo") ?? "");
const [vesselId, setVesselId] = useState(sp.get("vesselId") ?? "");
const [status, setStatus] = useState(sp.get("status") ?? "");
const [statuses, setStatuses] = useState<string[]>(sp.getAll("status"));
const [statusOpen, setStatusOpen] = useState(false);
const statusRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function onClick(e: MouseEvent) {
if (statusRef.current && !statusRef.current.contains(e.target as Node)) {
setStatusOpen(false);
}
}
document.addEventListener("mousedown", onClick);
return () => document.removeEventListener("mousedown", onClick);
}, []);
function toggleStatus(value: string) {
setStatuses((prev) =>
prev.includes(value) ? prev.filter((s) => s !== value) : [...prev, value]
);
}
function apply() {
const params = new URLSearchParams();
if (dateFrom) params.set("dateFrom", dateFrom);
if (dateTo) params.set("dateTo", dateTo);
if (vesselId) params.set("vesselId", vesselId);
if (status) params.set("status", status);
for (const s of statuses) params.append("status", s);
router.push(`/history?${params.toString()}`);
}
function clear() {
setDateFrom(""); setDateTo(""); setVesselId(""); setStatus("");
setDateFrom(""); setDateTo(""); setVesselId(""); setStatuses([]);
router.push("/history");
}
const hasFilters = dateFrom || dateTo || vesselId || status;
const hasFilters = dateFrom || dateTo || vesselId || statuses.length > 0;
const statusLabel =
statuses.length === 0
? "All statuses"
: statuses.length === 1
? (STATUSES.find((s) => s.value === statuses[0])?.label ?? statuses[0])
: `${statuses.length} statuses`;
return (
<div className="mb-4 rounded-lg border border-neutral-200 bg-white p-4">
@ -67,12 +91,26 @@ export function HistoryFilters({ vessels }: Props) {
{vessels.map((v) => <option key={v.id} value={v.id}>{v.name}</option>)}
</select>
</div>
<div>
<div className="relative" ref={statusRef}>
<label className="block text-xs font-medium text-neutral-600 mb-1">Status</label>
<select value={status} onChange={(e) => setStatus(e.target.value)}
className="w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20">
{STATUSES.map((s) => <option key={s.value} value={s.value}>{s.label}</option>)}
</select>
<button type="button" onClick={() => setStatusOpen((o) => !o)}
className="flex w-full items-center justify-between rounded-lg border border-neutral-300 px-3 py-2 text-left text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20">
<span className={statuses.length === 0 ? "text-neutral-500" : "text-neutral-900"}>{statusLabel}</span>
<svg className="ml-2 h-4 w-4 shrink-0 text-neutral-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fillRule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.17l3.71-3.94a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clipRule="evenodd" />
</svg>
</button>
{statusOpen && (
<div className="absolute z-10 mt-1 max-h-64 w-full overflow-auto rounded-lg border border-neutral-200 bg-white py-1 shadow-lg">
{STATUSES.map((s) => (
<label key={s.value} className="flex cursor-pointer items-center gap-2 px-3 py-1.5 text-sm hover:bg-neutral-50">
<input type="checkbox" checked={statuses.includes(s.value)} onChange={() => toggleStatus(s.value)}
className="h-4 w-4 rounded border-neutral-300 text-primary-600 focus:ring-primary-500/20" />
<span className="text-neutral-700">{s.label}</span>
</label>
))}
</div>
)}
</div>
</div>
<div className="mt-3 flex items-center gap-2">

View file

@ -17,7 +17,7 @@ interface Props {
dateFrom?: string;
dateTo?: string;
vesselId?: string;
status?: string;
status?: string | string[];
}>;
}
@ -41,7 +41,8 @@ export default async function HistoryPage({ searchParams }: Props) {
where.createdAt = createdAt;
}
if (vesselId) where.vesselId = vesselId;
if (status) where.status = status as POStatus;
const statuses = (Array.isArray(status) ? status : status ? [status] : []).filter(Boolean);
if (statuses.length > 0) where.status = { in: statuses as POStatus[] };
const [orders, vessels] = await Promise.all([
db.purchaseOrder.findMany({
@ -57,7 +58,7 @@ export default async function HistoryPage({ searchParams }: Props) {
if (dateFrom) exportParams.set("dateFrom", dateFrom);
if (dateTo) exportParams.set("dateTo", dateTo);
if (vesselId) exportParams.set("vesselId", vesselId);
if (status) exportParams.set("status", status);
for (const s of statuses) exportParams.append("status", s);
return (
<div>

View file

@ -25,7 +25,7 @@ export async function GET(request: NextRequest) {
const dateFrom = sp.get("dateFrom");
const dateTo = sp.get("dateTo");
const vesselId = sp.get("vesselId");
const status = sp.get("status");
const statuses = sp.getAll("status").filter(Boolean);
const where: NonNullable<Parameters<typeof db.purchaseOrder.findMany>[0]>["where"] = {};
if (dateFrom || dateTo) {
@ -39,7 +39,7 @@ export async function GET(request: NextRequest) {
where.createdAt = createdAt;
}
if (vesselId) where.vesselId = vesselId;
if (status) where.status = status as POStatus;
if (statuses.length > 0) where.status = { in: statuses as POStatus[] };
const orders = await db.purchaseOrder.findMany({
where,