From 388d8992f87f06224dcd1f8177d094248301fb12 Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 12 Sep 2026 20:40:33 +0200 Subject: [PATCH] feat: add CSV/JSON export, advanced bulk filters & selection to Catalog Manager - Admin Audit Log: CSV/JSON export via ?format=query param - Catalog Manager: club_only / have_offer filters (multi-filter support) - Offer discovery: refactored discoverOffers() to accept filters object - Translations added for new filter labels - Test updates for all modified paths --- .../catalog-items-table.tsx | 101 +++++++++++++++--- src/app/admin/logs/audit/audit-table.tsx | 19 +++- .../api/admin/logs/audit/export/route.test.ts | 15 ++- src/app/api/admin/logs/audit/export/route.ts | 38 +++++-- .../catalog/domain/offer-discovery.test.ts | 72 ++++++++++--- .../catalog/domain/offer-discovery.ts | 87 +++++++++++---- src/i18n/commandocentrum-messages.test.ts | 1 - src/messages/en.json | 8 +- 8 files changed, 279 insertions(+), 62 deletions(-) diff --git a/src/app/admin/catalog/[id]/catalog-items-table/catalog-items-table.tsx b/src/app/admin/catalog/[id]/catalog-items-table/catalog-items-table.tsx index 4093bce4..9d09306c 100644 --- a/src/app/admin/catalog/[id]/catalog-items-table/catalog-items-table.tsx +++ b/src/app/admin/catalog/[id]/catalog-items-table/catalog-items-table.tsx @@ -195,13 +195,19 @@ export function CatalogItemsTable({ // ── Search / Filter ───────────────────────────────────────────── const [searchQuery, setSearchQuery] = useState(initialSearch); - const [offerFilter, setOfferFilter] = useState("all"); + const [filters, setFilters] = useState>({ + offerFilter: "all", + clubOnlyFilter: "all", + haveOfferFilter: "all", + }); + const { items: filteredItems, counts: filterCounts } = useMemo( - () => discoverOffers(items, searchQuery, offerFilter), - [items, searchQuery, offerFilter], + () => discoverOffers(items, searchQuery, filters), + [items, searchQuery, filters], ); const hasDiscoveryFilter = - Boolean(searchQuery.trim()) || offerFilter !== "all"; + Boolean(searchQuery.trim()) || + Object.values(filters).some((f) => f !== "all"); // ── Move items dialog ─────────────────────────────────────────── const [moveOpen, setMoveOpen] = useState(false); @@ -225,12 +231,13 @@ export function CatalogItemsTable({ ); const orderedFilteredItems = useMemo(() => { - if (searchQuery.trim()) return filteredItems; + if (searchQuery.trim() || Object.values(filters).some((f) => f !== "all")) + return filteredItems; const orderMap = new Map(itemOrder.map((id, idx) => [id, idx])); - return [...filteredItems].sort( - (a, b) => (orderMap.get(a.id) ?? 0) - (orderMap.get(b.id) ?? 0), - ); - }, [filteredItems, itemOrder, searchQuery]); + return filteredItems + .slice() + .sort((a, b) => (orderMap.get(a.id) ?? 0) - (orderMap.get(b.id) ?? 0)); + }, [filteredItems, itemOrder, searchQuery, filters]); // ── Selection ──────────────────────────────────────────────────── const [selected, setSelected] = useState>(new Set()); @@ -265,13 +272,17 @@ export function CatalogItemsTable({ setSearchQuery(query); setSelected(new Set()); } - function updateOfferFilter(value: string) { - setOfferFilter(value as OfferFilter); + function updateFilter(key: string, value: string) { + setFilters((prev) => ({ ...prev, [key]: value })); setSelected(new Set()); } function resetDiscovery() { setSearchQuery(""); - setOfferFilter("all"); + setFilters({ + offerFilter: "all", + clubOnlyFilter: "all", + haveOfferFilter: "all", + }); setSelected(new Set()); } @@ -1187,18 +1198,21 @@ export function CatalogItemsTable({ onClear={() => updateSearch("")} /> - updateFilter("offerFilter", v)} + > - {discovery(`filters.${offerFilter}`)} ( - {filterCounts[offerFilter]}) + {discovery(`filters.${filters.offerFilter}`)} ( + {filterCounts[filters.offerFilter as OfferFilter]}) - {OFFER_FILTERS.map((filter) => ( + {OFFER_FILTERS.slice(0, 5).map((filter) => ( {discovery(`filters.${filter}`)} ({filterCounts[filter]} ) @@ -1206,6 +1220,59 @@ export function CatalogItemsTable({ ))} + + + + + {hasDiscoveryFilter && (