diff --git a/src/app/admin/catalog/catalog-actions.tsx b/src/app/admin/catalog/catalog-actions.tsx index 7b128e6c..d63b1f64 100644 --- a/src/app/admin/catalog/catalog-actions.tsx +++ b/src/app/admin/catalog/catalog-actions.tsx @@ -1,7 +1,8 @@ "use client"; import { Loader2, Plus } from "lucide-react"; -import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; import { toast } from "sonner"; import { createCatalogPage } from "@/actions/catalog"; import { createBcPage } from "@/actions/catalog-bc"; @@ -26,18 +27,40 @@ import { CATALOG_LAYOUTS } from "@/lib/catalog-layouts"; interface CatalogActionsProps { catalogType?: "normal" | "bc"; + /** Prefill parent and auto-open create dialog (from ?newPageParent=). */ + initialParentId?: string | null; } export function CatalogActions({ catalogType = "normal", + initialParentId = null, }: CatalogActionsProps) { const { run, isPending } = useServerAction(); + const router = useRouter(); const [open, setOpen] = useState(false); const [caption, setCaption] = useState(""); const [parentId, setParentId] = useState("-1"); const [pageLayout, setPageLayout] = useState("default_3x3"); const isBc = catalogType === "bc"; + useEffect(() => { + if (initialParentId == null || initialParentId === "") return; + const n = Number.parseInt(initialParentId, 10); + if (!Number.isFinite(n)) return; + setParentId(String(n)); + setOpen(true); + + if (typeof window === "undefined") return; + const url = new URL(window.location.href); + if (url.searchParams.has("newPageParent")) { + url.searchParams.delete("newPageParent"); + const qs = url.searchParams.toString(); + router.replace(`${url.pathname}${qs ? `?${qs}` : ""}`, { + scroll: false, + }); + } + }, [initialParentId, router]); + function handleCreate() { if (!caption.trim()) { toast.error("Caption is required"); diff --git a/src/app/admin/catalog/page.tsx b/src/app/admin/catalog/page.tsx index 04964678..d4f4059c 100644 --- a/src/app/admin/catalog/page.tsx +++ b/src/app/admin/catalog/page.tsx @@ -212,6 +212,11 @@ export default async function CatalogPagesPage({ ({ id: p.id, caption: p.caption, @@ -221,7 +226,12 @@ export default async function CatalogPagesPage({ }))} /> )} - {canEdit && } + {canEdit && ( + + )} {viewMode === "tree" ? ( @@ -398,6 +408,11 @@ export default async function CatalogPagesPage({ ({ id: p.id, caption: p.caption, @@ -407,7 +422,9 @@ export default async function CatalogPagesPage({ }))} /> )} - {canEdit && } + {canEdit && ( + + )} diff --git a/src/components/admin/catalog-manager/catalog-manager-dialog.tsx b/src/components/admin/catalog-manager/catalog-manager-dialog.tsx index da5f54f9..2fffaa27 100644 --- a/src/components/admin/catalog-manager/catalog-manager-dialog.tsx +++ b/src/components/admin/catalog-manager/catalog-manager-dialog.tsx @@ -13,7 +13,7 @@ import { BreadcrumbBar } from "./breadcrumb-bar"; import { InlineEditor, type PageUpdateData } from "./inline-editor"; import { RootTabs } from "./root-tabs"; import { SortableTree } from "./sortable-tree"; -import { StatsBar } from "./stats-bar"; +import { type CatalogStatsTotals, StatsBar } from "./stats-bar"; import { CatalogManagerProvider, type CatalogManagerMode, @@ -29,10 +29,12 @@ export function CatalogManagerDialog({ initialRoots = [], catalogType = "normal", canEdit = false, + totals = null, }: { initialRoots?: RootTab[]; catalogType?: CatalogManagerMode; canEdit?: boolean; + totals?: CatalogStatsTotals | null; }) { const [open, setOpen] = useState(false); const [mounted, setMounted] = useState(false); @@ -89,6 +91,7 @@ export function CatalogManagerDialog({ open={open} onOpenChange={setOpen} initialRoots={initialRoots} + totals={totals} /> @@ -104,10 +107,12 @@ function CatalogManagerInner({ open, onOpenChange, initialRoots, + totals, }: { open: boolean; onOpenChange: (v: boolean) => void; initialRoots: RootTab[]; + totals?: CatalogStatsTotals | null; }) { const { state, dispatch, catalogType, canEdit } = useCatalogManager(); const catQs = catalogTreeQs(catalogType); @@ -300,7 +305,7 @@ function CatalogManagerInner({ - + ); } diff --git a/src/components/admin/catalog-manager/root-tabs.tsx b/src/components/admin/catalog-manager/root-tabs.tsx index fee80070..00a1055f 100644 --- a/src/components/admin/catalog-manager/root-tabs.tsx +++ b/src/components/admin/catalog-manager/root-tabs.tsx @@ -127,7 +127,7 @@ export function RootTabs({ if (!canEdit) return; const ok = await confirm({ title: "Delete root page", - description: `Delete root page "${tab.caption}"? Children will be reparented.`, + description: `Delete root page "${tab.caption}"? Child pages (if any) will be moved to root; items on this page will be deleted.`, confirmLabel: "Delete", }); if (!ok) return; diff --git a/src/components/admin/catalog-manager/sortable-tree.tsx b/src/components/admin/catalog-manager/sortable-tree.tsx index d20e9cd4..a873532b 100644 --- a/src/components/admin/catalog-manager/sortable-tree.tsx +++ b/src/components/admin/catalog-manager/sortable-tree.tsx @@ -557,9 +557,17 @@ export function SortableTree({ const handleDelete = async (id: number) => { if (!canEdit) return; const node = nodes.get(id); + const childInfo = + node && node.childCount > 0 + ? ` Its ${node.childCount} child page(s) will be moved to the parent.` + : ""; + const itemInfo = + node && node.itemCount > 0 + ? ` ${node.itemCount} item(s) on this page will be deleted.` + : ""; const ok = await confirm({ title: "Delete page", - description: `Delete "${node?.caption ?? id}"? Children will be reparented.`, + description: `Delete "${node?.caption ?? id}"?${childInfo}${itemInfo}`, confirmLabel: "Delete", }); if (!ok) return; @@ -700,19 +708,41 @@ export function SortableTree({ const handleBulkDelete = async () => { if (!canEdit) return; const ids = [...multiSelectedIds]; + let childTotal = 0; + let itemTotal = 0; + for (const id of ids) { + const node = nodes.get(id); + if (!node) continue; + childTotal += node.childCount; + itemTotal += node.itemCount; + } + const childInfo = + childTotal > 0 + ? ` About ${childTotal} child page(s) will be moved to parents.` + : ""; + const itemInfo = + itemTotal > 0 + ? ` About ${itemTotal} item(s) on selected pages will be deleted.` + : ""; const ok = await confirm({ title: "Delete pages", - description: `Delete ${ids.length} page(s)? Children will be reparented.`, + description: `Delete ${ids.length} page(s)?${childInfo}${itemInfo}`, confirmLabel: "Delete", }); if (!ok) return; const del = catalogType === "bc" ? deleteBcTreePage : deleteTreePage; - await Promise.allSettled( + const results = await Promise.allSettled( ids.map((id) => del({ pageId: id, mode: "reparent" })), ); + const failed = results.filter((r) => r.status === "rejected").length; + const okCount = ids.length - failed; dispatch({ type: "CLEAR_MULTI_SELECT" }); await loadFullTree(); - toast.success(`${ids.length} page(s) deleted`); + if (failed > 0) { + toast.error(`${failed} delete(s) failed; ${okCount} succeeded`); + } else { + toast.success(`${okCount} page(s) deleted`); + } onTreeMutation(); }; diff --git a/src/components/admin/catalog-manager/stats-bar.tsx b/src/components/admin/catalog-manager/stats-bar.tsx index 0f768b63..19186a19 100644 --- a/src/components/admin/catalog-manager/stats-bar.tsx +++ b/src/components/admin/catalog-manager/stats-bar.tsx @@ -1,45 +1,50 @@ "use client"; import { FolderTree, Keyboard, Package, ToggleRight } from "lucide-react"; -import { useMemo } from "react"; import { useCatalogManager } from "./use-catalog-store"; -export function StatsBar() { - const { state } = useCatalogManager(); +export interface CatalogStatsTotals { + totalPages: number; + enabledPages: number; + totalItems: number; +} - const stats = useMemo(() => { - const nodes = Array.from(state.nodes.values()); - const enabled = nodes.filter((n) => n.enabled === "1").length; - const totalItems = nodes.reduce((sum, n) => sum + n.itemCount, 0); - return { - rootTabs: state.rootPages.length, - totalPages: nodes.length, - enabled, - totalItems, - }; - }, [state.nodes, state.rootPages]); +export function StatsBar({ totals }: { totals?: CatalogStatsTotals | null }) { + const { state } = useCatalogManager(); + const selected = state.multiSelectedIds.size; + const rootTabs = state.rootPages.length; + + const pages = totals?.totalPages ?? state.nodes.size; + const enabled = totals?.enabledPages ?? null; + const items = totals?.totalItems ?? null; + const loadedOnly = !totals; return (
- {stats.rootTabs} root + {rootTabs} root - {stats.totalPages} pages - - - {stats.enabled} enabled + + {pages} pages + {loadedOnly ? " loaded" : ""} - - - {stats.totalItems} items - - {state.multiSelectedIds.size > 0 && ( - - {state.multiSelectedIds.size} selected + {enabled !== null && ( + + + {enabled} enabled )} + {items !== null && ( + + + {items} items + + )} + {selected > 0 && ( + {selected} selected + )}
diff --git a/src/components/admin/catalog-tree.tsx b/src/components/admin/catalog-tree.tsx index 56589eb9..292fb59e 100644 --- a/src/components/admin/catalog-tree.tsx +++ b/src/components/admin/catalog-tree.tsx @@ -728,7 +728,7 @@ function TreeItem({ e.stopPropagation(); router.push( catalogType === "bc" - ? `/admin/catalog/builder-club/${node.id}` + ? `/admin/catalog?catalog=bc&newPageParent=${node.id}` : `/admin/catalog?newPageParent=${node.id}`, ); }} @@ -872,7 +872,7 @@ function TreeItem({ onClick={() => router.push( catalogType === "bc" - ? `/admin/catalog/builder-club/${node.id}` + ? `/admin/catalog?catalog=bc&newPageParent=${node.id}` : `/admin/catalog?newPageParent=${node.id}`, ) } diff --git a/src/messages/nl.json b/src/messages/nl.json index cf911bd4..ee89a022 100644 --- a/src/messages/nl.json +++ b/src/messages/nl.json @@ -1246,6 +1246,9 @@ "subtitle": "Beheer pakketten en bestellingen", "packages": "Pakketten", "orders": "Bestellingen", + "recentOrders": "Recente bestellingen", + "paidOrders": "Betaalde bestellingen", + "ordersHint": "Laatste PayPal-transacties", "newPackage": "+ Nieuw pakket", "edit": "Bewerken", "delete": "Verwijderen", @@ -1256,6 +1259,11 @@ "colId": "ID", "colName": "Naam", "colPrice": "Prijs", + "colCredits": "Credits", + "colDuckets": "Duckets", + "colDiamonds": "Diamanten", + "colRank": "Rang", + "colPosition": "Positie", "colCurrency": "Valuta", "colType": "Type", "colSold": "Verkocht", @@ -1264,6 +1272,9 @@ "colOrderId": "Bestelling ID", "colCustomer": "Klant", "colPackage": "Pakket", + "colTransaction": "Transactie", + "colUser": "Gebruiker", + "colDescription": "Beschrijving", "colAmount": "Bedrag", "colStatus": "Status", "colDate": "Datum", @@ -1299,12 +1310,20 @@ "transactions": { "title": "Transacties", "subtitle": "PayPal betalingslogboek", + "totalTransactions": "Totaal transacties", + "pagePaid": "Betaald (deze pagina)", + "pageFailed": "Mislukt (deze pagina)", + "searchPlaceholder": "Zoek op gebruiker, transactie-ID of pakket…", + "presetPaid": "Betaald", + "presetFailed": "Mislukt", "noTransactions": "Nog geen transacties", "colId": "ID", "colUser": "Gebruiker", + "colPayer": "Betaler", "colAmount": "Bedrag", "colCurrency": "Valuta", "colPackage": "Pakket", + "colTransaction": "Transactie-ID", "colStatus": "Status", "colDate": "Datum", "colActions": "Acties", @@ -1324,16 +1343,30 @@ "vouchers": { "title": "Winkel Vouchers", "subtitle": "Beheer inwisselbare vouchercodes", + "vouchers": "Vouchers", + "active": "Actief", + "usedUp": "Opgebruikt", + "expired": "Verlopen", "newVoucher": "+ Nieuwe voucher", "edit": "Bewerken", "delete": "Verwijderen", "noVouchers": "Nog geen vouchers", + "confirmDeleteTitle": "Voucher verwijderen", "confirmDelete": "Voucher \"{code}\" verwijderen?", + "created": "Voucher aangemaakt", + "deleted": "Voucher verwijderd", + "createError": "Kon voucher niet aanmaken", + "deleteError": "Kon voucher niet verwijderen", + "statusActive": "Actief", + "statusUsedUp": "Opgebruikt", + "statusExpired": "Verlopen", "colId": "ID", "colCode": "Code", "colValue": "Waarde", + "colAmount": "Bedrag", "colType": "Type", "colUses": "Gebruik / Max", + "colStatus": "Status", "colExpires": "Verloopt", "colCreated": "Aangemaakt", "colActions": "Acties", @@ -1341,6 +1374,8 @@ "code": "Code", "codePlaceholder": "Bijv. ZOMER2025", "value": "Waarde", + "amount": "Aantal credits", + "amountPlaceholder": "bijv. 100", "type": "Type", "typeCredits": "Credits", "typeDuckets": "Duckets", @@ -1355,6 +1390,7 @@ "maxUsesPlaceholder": "0 = onbeperkt", "expiresAt": "Verloopt op", "enabled": "Ingeschakeld", + "create": "Voucher aanmaken", "save": "Voucher opslaan", "saving": "Opslaan…", "cancel": "Annuleren"