From 3e8f5fb0c7f6f7c7ce1a2b190e18af86666b153f Mon Sep 17 00:00:00 2001 From: "Claude (auto-fix)" Date: Wed, 24 Jun 2026 13:18:00 +0530 Subject: [PATCH] feat(history): add Accounting Code search filter to PO History PO History had no way to narrow by accounting code. Add an "Accounting Code" filter (the shared type-to-search combobox) alongside Cost Centre, backed by the PO-level account already included in the query. - history/page.tsx: read `accountId` searchParam, fetch selectable leaf accounting codes (active, no children) via buildAccountGroups, apply `where.accountId`, thread the param into pagination + export links, and surface an Accounting Code column for context. - history-filters.tsx: new SearchableSelect control wired into buildParams/apply/clear/hasFilters like the Cost Centre select. - api/reports/export: apply the same `accountId` filter so CSV/PDF export respects the on-screen filter. - tests/staging: verify picking a code drives an `accountId` query param. Fixes #121 Co-Authored-By: Claude Opus 4.8 (1M context) --- App/app/(portal)/history/history-filters.tsx | 21 ++++++++++++--- App/app/(portal)/history/page.tsx | 22 +++++++++++++--- App/app/api/reports/export/route.ts | 2 ++ .../issue-121-history-accounting-code.spec.ts | 26 +++++++++++++++++++ 4 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 App/tests/staging/issue-121-history-accounting-code.spec.ts diff --git a/App/app/(portal)/history/history-filters.tsx b/App/app/(portal)/history/history-filters.tsx index 6ee2598..d9fe99f 100644 --- a/App/app/(portal)/history/history-filters.tsx +++ b/App/app/(portal)/history/history-filters.tsx @@ -2,6 +2,8 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useRef, useState } from "react"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import type { AccountGroup } from "@/app/(portal)/po/new/new-po-form"; const STATUSES = [ { value: "DRAFT", label: "Draft" }, @@ -19,11 +21,12 @@ const STATUSES = [ interface Props { vessels: { id: string; name: string }[]; + accounts: AccountGroup[]; perPageOptions: number[]; defaultPerPage: number; } -export function HistoryFilters({ vessels, perPageOptions, defaultPerPage }: Props) { +export function HistoryFilters({ vessels, accounts, perPageOptions, defaultPerPage }: Props) { const router = useRouter(); const sp = useSearchParams(); @@ -36,6 +39,7 @@ export function HistoryFilters({ vessels, perPageOptions, defaultPerPage }: Prop const [approvedFrom, setApprovedFrom] = useState(sp.get("approvedFrom") ?? ""); const [approvedTo, setApprovedTo] = useState(sp.get("approvedTo") ?? ""); const [vesselId, setVesselId] = useState(sp.get("vesselId") ?? ""); + const [accountId, setAccountId] = useState(sp.get("accountId") ?? ""); const [statuses, setStatuses] = useState(sp.getAll("status")); const [statusOpen, setStatusOpen] = useState(false); const statusRef = useRef(null); @@ -64,6 +68,7 @@ export function HistoryFilters({ vessels, perPageOptions, defaultPerPage }: Prop if (approvedFrom) params.set("approvedFrom", approvedFrom); if (approvedTo) params.set("approvedTo", approvedTo); if (vesselId) params.set("vesselId", vesselId); + if (accountId) params.set("accountId", accountId); for (const s of statuses) params.append("status", s); if (nextPerPage !== defaultPerPage) params.set("perPage", String(nextPerPage)); return params; @@ -78,14 +83,14 @@ export function HistoryFilters({ vessels, perPageOptions, defaultPerPage }: Prop } function clear() { - setDateFrom(""); setDateTo(""); setApprovedFrom(""); setApprovedTo(""); setVesselId(""); setStatuses([]); + setDateFrom(""); setDateTo(""); setApprovedFrom(""); setApprovedTo(""); setVesselId(""); setAccountId(""); setStatuses([]); const params = new URLSearchParams(); if (perPage !== defaultPerPage) params.set("perPage", String(perPage)); const qs = params.toString(); router.push(qs ? `/history?${qs}` : "/history"); } - const hasFilters = dateFrom || dateTo || approvedFrom || approvedTo || vesselId || statuses.length > 0; + const hasFilters = dateFrom || dateTo || approvedFrom || approvedTo || vesselId || accountId || statuses.length > 0; const statusLabel = statuses.length === 0 @@ -125,6 +130,16 @@ export function HistoryFilters({ vessels, perPageOptions, defaultPerPage }: Prop {vessels.map((v) => )} +
+ + +
- +
@@ -150,6 +162,7 @@ export default async function HistoryPage({ searchParams }: Props) { PO Number Title Cost Centre + Accounting Code Submitter Status Amount @@ -169,6 +182,9 @@ export default async function HistoryPage({ searchParams }: Props) { {po.title} {po.vessel.name} + + {po.account.code} {po.account.name} + {po.submitter.name} diff --git a/App/app/api/reports/export/route.ts b/App/app/api/reports/export/route.ts index ee82eae..3fa2fbc 100644 --- a/App/app/api/reports/export/route.ts +++ b/App/app/api/reports/export/route.ts @@ -30,6 +30,7 @@ export async function GET(request: NextRequest) { const approvedFrom = sp.get("approvedFrom"); const approvedTo = sp.get("approvedTo"); const vesselId = sp.get("vesselId"); + const accountId = sp.get("accountId"); const statuses = sp.getAll("status").filter(Boolean); const where: NonNullable[0]>["where"] = {}; @@ -54,6 +55,7 @@ export async function GET(request: NextRequest) { where.approvedAt = approvedAt; } if (vesselId) where.vesselId = vesselId; + if (accountId) where.accountId = accountId; if (statuses.length > 0) where.status = { in: statuses as POStatus[] }; const orders = await db.purchaseOrder.findMany({ diff --git a/App/tests/staging/issue-121-history-accounting-code.spec.ts b/App/tests/staging/issue-121-history-accounting-code.spec.ts new file mode 100644 index 0000000..4ddc57e --- /dev/null +++ b/App/tests/staging/issue-121-history-accounting-code.spec.ts @@ -0,0 +1,26 @@ +import { test, expect } from "@playwright/test"; +import { USERS, login } from "./helpers"; + +/** + * Issue #121 — PO History gains an Accounting Code filter. The control is the + * shared type-to-search combobox; picking a code and applying narrows the list + * via an `accountId` query param (mirrored by the CSV/PDF export links). + */ +test("#121 history can be filtered by accounting code", async ({ page }) => { + await login(page, USERS.MANAGER); + await page.goto("/history"); + + // The Accounting Code control is present (its trigger shows the empty label). + const trigger = page.getByRole("button", { name: "All accounting codes" }); + await expect(trigger).toBeVisible(); + + // Open it and pick the first accounting code from the searchable list. + await trigger.click(); + await expect(page.getByPlaceholder("Type code or name…")).toBeVisible(); + await page.locator(".max-h-72 button").first().click(); + + await page.getByRole("button", { name: "Apply" }).click(); + + // Applying the filter drives an accountId query param. + await expect(page).toHaveURL(/accountId=/); +});