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
This commit is contained in:
1 parent
eeca532057
commit
388d8992f8
8 files changed
+279
-62
No files matched your search
@@ -195,13 +195,19 @@ export function CatalogItemsTable({
|
||||
|
||||
// ── Search / Filter ─────────────────────────────────────────────
|
||||
const [searchQuery, setSearchQuery] = useState(initialSearch);
|
||||
const [offerFilter, setOfferFilter] = useState<OfferFilter>("all");
|
||||
const [filters, setFilters] = useState<Record<string, string>>({
|
||||
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<Set<number>>(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("")}
|
||||
/>
|
||||
</div>
|
||||
<Select value={offerFilter} onValueChange={updateOfferFilter}>
|
||||
<Select
|
||||
value={filters.offerFilter}
|
||||
onValueChange={(v) => updateFilter("offerFilter", v)}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-label={discovery("filterLabel")}
|
||||
className="w-full sm:w-52"
|
||||
>
|
||||
<SelectValue>
|
||||
{discovery(`filters.${offerFilter}`)} (
|
||||
{filterCounts[offerFilter]})
|
||||
{discovery(`filters.${filters.offerFilter}`)} (
|
||||
{filterCounts[filters.offerFilter as OfferFilter]})
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{OFFER_FILTERS.map((filter) => (
|
||||
{OFFER_FILTERS.slice(0, 5).map((filter) => (
|
||||
<SelectItem key={filter} value={filter}>
|
||||
{discovery(`filters.${filter}`)} ({filterCounts[filter]}
|
||||
)
|
||||
@@ -1206,6 +1220,59 @@ export function CatalogItemsTable({
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={filters.clubOnlyFilter}
|
||||
onValueChange={(v) => updateFilter("clubOnlyFilter", v)}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-label={discovery("clubOnlyFilterLabel")}
|
||||
className="w-full sm:w-52"
|
||||
>
|
||||
<SelectValue>
|
||||
{discovery(`filters.${filters.clubOnlyFilter}`)} (
|
||||
{filterCounts[filters.clubOnlyFilter as OfferFilter]})
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
{discovery("filters.all")}
|
||||
</SelectItem>
|
||||
<SelectItem value="club_only_yes">
|
||||
{discovery("filters.club_only_yes")}
|
||||
</SelectItem>
|
||||
<SelectItem value="club_only_no">
|
||||
{discovery("filters.club_only_no")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={filters.haveOfferFilter}
|
||||
onValueChange={(v) => updateFilter("haveOfferFilter", v)}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-label={discovery("haveOfferFilterLabel")}
|
||||
className="w-full sm:w-52"
|
||||
>
|
||||
<SelectValue>
|
||||
{discovery(`filters.${filters.haveOfferFilter}`)} (
|
||||
{filterCounts[filters.haveOfferFilter as OfferFilter]})
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
{discovery("filters.all")}
|
||||
</SelectItem>
|
||||
<SelectItem value="have_offer_yes">
|
||||
{discovery("filters.have_offer_yes")}
|
||||
</SelectItem>
|
||||
<SelectItem value="have_offer_no">
|
||||
{discovery("filters.have_offer_no")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{hasDiscoveryFilter && (
|
||||
<Button variant="ghost" size="sm" onClick={resetDiscovery}>
|
||||
{discovery("reset")}
|
||||
@@ -1245,7 +1312,7 @@ export function CatalogItemsTable({
|
||||
</p>
|
||||
) : viewMode === "grid" ? (
|
||||
<ItemsShopPreview
|
||||
items={orderedFilteredItems}
|
||||
items={orderedFilteredItems as CatalogItemData[]}
|
||||
furniRevisionMap={furniRevisionMap}
|
||||
layout={pageLayout}
|
||||
canEdit={canEdit}
|
||||
|
||||
Reference in new issue
Block a user