@xiwebdesign/xicms-core 7.23.0 → 7.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/dist/admin/AdminApp.js +2 -1
  2. package/dist/admin/AdminApp.js.map +1 -1
  3. package/dist/admin/CoreVersion.d.ts +6 -0
  4. package/dist/admin/CoreVersion.js +42 -1
  5. package/dist/admin/CoreVersion.js.map +1 -1
  6. package/dist/admin/SuperadminPanel.js +19 -3
  7. package/dist/admin/SuperadminPanel.js.map +1 -1
  8. package/dist/admin/dashboard/DashboardPanel.js +46 -9
  9. package/dist/admin/dashboard/DashboardPanel.js.map +1 -1
  10. package/dist/admin/forms/FormsPanel.d.ts +9 -0
  11. package/dist/admin/forms/FormsPanel.js +102 -16
  12. package/dist/admin/forms/FormsPanel.js.map +1 -1
  13. package/dist/admin/forms/gezien.d.ts +3 -0
  14. package/dist/admin/forms/gezien.js +22 -0
  15. package/dist/admin/forms/gezien.js.map +1 -0
  16. package/dist/admin/lib/kleur.d.ts +32 -0
  17. package/dist/admin/lib/kleur.js +60 -0
  18. package/dist/admin/lib/kleur.js.map +1 -0
  19. package/dist/admin/lib/pagesClient.d.ts +32 -0
  20. package/dist/admin/lib/pagesClient.js +60 -1
  21. package/dist/admin/lib/pagesClient.js.map +1 -1
  22. package/dist/admin/lib/seoHulp.d.ts +28 -0
  23. package/dist/admin/lib/seoHulp.js +33 -0
  24. package/dist/admin/lib/seoHulp.js.map +1 -0
  25. package/dist/admin/media/MediaManager.js +133 -51
  26. package/dist/admin/media/MediaManager.js.map +1 -1
  27. package/dist/admin/pages/PageEditor.d.ts +14 -0
  28. package/dist/admin/pages/PageEditor.js +192 -123
  29. package/dist/admin/pages/PageEditor.js.map +1 -1
  30. package/dist/admin/pages/PagesPanel.js +88 -40
  31. package/dist/admin/pages/PagesPanel.js.map +1 -1
  32. package/dist/admin/settings/SettingsPanel.js +100 -19
  33. package/dist/admin/settings/SettingsPanel.js.map +1 -1
  34. package/dist/admin/shop/OrdersTab.js +171 -3
  35. package/dist/admin/shop/OrdersTab.js.map +1 -1
  36. package/dist/admin/shop/ProductsTab.js +1 -1
  37. package/dist/admin/shop/ProductsTab.js.map +1 -1
  38. package/dist/admin/shop/ShopPanel.js +5 -2
  39. package/dist/admin/shop/ShopPanel.js.map +1 -1
  40. package/dist/admin/stats/StatsPanel.js +30 -2
  41. package/dist/admin/stats/StatsPanel.js.map +1 -1
  42. package/dist/admin/stats/lib.d.ts +14 -1
  43. package/dist/admin/stats/lib.js +46 -2
  44. package/dist/admin/stats/lib.js.map +1 -1
  45. package/dist/admin/visual/VisualEditor.js +80 -4
  46. package/dist/admin/visual/VisualEditor.js.map +1 -1
  47. package/dist/admin.css +1 -1
  48. package/dist/blocks/builtins/FormBlockRender.js +8 -2
  49. package/dist/blocks/builtins/FormBlockRender.js.map +1 -1
  50. package/dist/blocks/builtins/lib/submitForm.d.ts +3 -1
  51. package/dist/blocks/builtins/lib/submitForm.js +2 -2
  52. package/dist/blocks/builtins/lib/submitForm.js.map +1 -1
  53. package/dist/components/EditBar.d.ts +3 -1
  54. package/dist/components/EditBar.js +30 -4
  55. package/dist/components/EditBar.js.map +1 -1
  56. package/dist/components/News.js +8 -4
  57. package/dist/components/News.js.map +1 -1
  58. package/dist/components/PageView.d.ts +5 -0
  59. package/dist/components/PageView.js +24 -2
  60. package/dist/components/PageView.js.map +1 -1
  61. package/dist/components/ResponsiveImage.d.ts +1 -1
  62. package/dist/components/ResponsiveImage.js +3 -1
  63. package/dist/components/ResponsiveImage.js.map +1 -1
  64. package/dist/components/site/SiteNav.d.ts +29 -0
  65. package/dist/components/site/SiteNav.js +82 -0
  66. package/dist/components/site/SiteNav.js.map +1 -0
  67. package/dist/content/types.d.ts +8 -0
  68. package/dist/index.d.ts +3 -1
  69. package/dist/index.js +8 -1
  70. package/dist/index.js.map +1 -1
  71. package/dist/media/ImageContext.d.ts +1 -6
  72. package/dist/media/ImageContext.js +6 -1
  73. package/dist/media/ImageContext.js.map +1 -1
  74. package/dist/releaseNotes.d.ts +15 -0
  75. package/dist/releaseNotes.js +28 -0
  76. package/dist/releaseNotes.js.map +1 -0
  77. package/dist/schema/mail.d.ts +20 -0
  78. package/dist/schema/mail.js +14 -0
  79. package/dist/schema/mail.js.map +1 -1
  80. package/dist/schema/settings.d.ts +105 -0
  81. package/dist/schema/settings.js +45 -5
  82. package/dist/schema/settings.js.map +1 -1
  83. package/dist/schema/shop.d.ts +2 -0
  84. package/dist/schema/shop.js +12 -2
  85. package/dist/schema/shop.js.map +1 -1
  86. package/dist/schema/siteConfig.d.ts +9 -0
  87. package/dist/server/blockData.js +6 -2
  88. package/dist/server/blockData.js.map +1 -1
  89. package/dist/server/draft.d.ts +11 -0
  90. package/dist/server/draft.js +20 -1
  91. package/dist/server/draft.js.map +1 -1
  92. package/dist/server/index.d.ts +3 -1
  93. package/dist/server/index.js +7 -1
  94. package/dist/server/index.js.map +1 -1
  95. package/dist/server/pages.js +4 -1
  96. package/dist/server/pages.js.map +1 -1
  97. package/dist/server/previewToken.d.ts +34 -0
  98. package/dist/server/previewToken.js +44 -0
  99. package/dist/server/previewToken.js.map +1 -0
  100. package/dist/server/redirects.d.ts +15 -0
  101. package/dist/server/redirects.js +15 -0
  102. package/dist/server/redirects.js.map +1 -0
  103. package/dist/styles.css +110 -0
  104. package/dist/version.js +1 -1
  105. package/package.json +1 -1
@@ -17,7 +17,7 @@ import { PanelIcon } from "./ui/PanelIcon.js";
17
17
  import { builtinPanels } from "./panelRegistry.js";
18
18
  import { AdminNavContext } from "./navContext.js";
19
19
  import { AdminHelp } from "./AdminHelp.js";
20
- import { CoreVersion } from "./CoreVersion.js";
20
+ import { CoreVersion, WatIsNieuw } from "./CoreVersion.js";
21
21
  function roleAllows(role, min) {
22
22
  return min === "siteadmin" ? canManageSite(role) : canEdit(role);
23
23
  }
@@ -100,6 +100,7 @@ function AdminApp({
100
100
  )
101
101
  ] }),
102
102
  /* @__PURE__ */ jsxs("div", { className: "grid gap-6 p-4 md:p-8", children: [
103
+ /* @__PURE__ */ jsx(WatIsNieuw, {}),
103
104
  panels.map((p) => /* @__PURE__ */ jsx(TabsContent, { value: sleutel(p), className: "pt-0", children: /* @__PURE__ */ jsx(p.Panel, { siteId }) }, sleutel(p))),
104
105
  /* @__PURE__ */ jsx(AdminHelp, {})
105
106
  ] })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/admin/AdminApp.tsx"],"sourcesContent":["\"use client\";\n\nimport { registerBuiltinSchemas } from \"../blocks/builtinSchemas\";\n// Schema's zijn lazy (7.13): dit is een editor-chunk, dus hier horen ze thuis.\nregisterBuiltinSchemas();\nimport { useState } from \"react\";\nimport { useAuth } from \"../auth/AuthProvider\";\nimport { LoginForm } from \"../auth/LoginForm\";\nimport { useSiteConfig } from \"../config\";\nimport { useModules } from \"../modules/context\";\nimport { canEdit, canManageSite, type Role } from \"../auth/roles\";\nimport { Tabs, TabsContent } from \"./ui/tabs\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport { ToastProvider } from \"./ui/toast\";\nimport { ConfirmHost } from \"./ui/ConfirmDialog\";\nimport { Button } from \"./ui/primitives\";\nimport { PanelIcon } from \"./ui/PanelIcon\";\nimport { builtinPanels, type PanelDefinition } from \"./panelRegistry\";\nimport { AdminNavContext } from \"./navContext\";\nimport { AdminHelp } from \"./AdminHelp\";\nimport { CoreVersion } from \"./CoreVersion\";\n\n/**\n * Rol-bewaakt beheer (XiCMS 3.0). De secties komen uit de paneel-registry, gefilterd op de actieve\n * modules (`SiteConfig.modules`/`features`) en de rol van de gebruiker. Een module met een eigen\n * paneel (ook klant-modules via `extraPanels`) verschijnt automatisch.\n *\n * VORM: een schermvullend werkblad met een vaste zijbalk, geen kaart in de pagina. Het beheer viel\n * hiervoor binnen de site-chrome — nav erboven, footer eronder, cookiebanner eroverheen — en las\n * daardoor als een blok ergens op de website in plaats van als gereedschap. De site-chrome leeft nu\n * in `app/(site)/layout.tsx`; /beheer valt daarbuiten.\n *\n * De zijbalk verving de horizontale tabstrip omdat die bij zeven of meer panelen al omsloeg naar een\n * tweede regel. Verticaal is er bovendien ruimte voor een icoon naast het woord, en `orientation`\n * houdt de pijltjestoetsen correct (Radix).\n *\n * Wikkel in `.xicms-admin` zodat de Tailwind-stijlen uit `@xiwebdesign/xicms-core/admin.css` scoped\n * blijven en niet naar de site lekken. Importeer die CSS alleen op de /beheer-route.\n */\nfunction roleAllows(role: Role | null, min: PanelDefinition[\"minRole\"]): boolean {\n return min === \"siteadmin\" ? canManageSite(role) : canEdit(role);\n}\n\n/** Gecentreerde boodschap voor de toestanden waarin er (nog) geen beheer te tonen is. */\nfunction Melding({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"xicms-admin__midden grid place-items-center p-6\">\n <div className=\"w-full max-w-md rounded-xl border border-slate-200 bg-white p-6 text-sm shadow-sm\">{children}</div>\n </div>\n );\n}\n\nexport function AdminApp({\n extraPanels = [],\n embedded = false,\n}: {\n extraPanels?: PanelDefinition[];\n /**\n * Draait het beheer BINNEN de site-schil (header, logo, menu, footer eromheen) in plaats van als\n * schermvullend werkblad op een kale pagina?\n *\n * Het verschil is niet cosmetisch. Los rekent het werkblad met het VENSTER: `min-h-screen`, en een\n * zijbalk die met `sticky top-0` aan de bovenkant van het scherm plakt. Zet je daar een\n * site-header boven, dan loopt de pagina 100vh + de hoogte van die header, en plakt de zijbalk\n * onder de header vast in plaats van erlangs. Ingebed laat de zijbalk daarom los van het venster\n * en wordt hij een balk over de volle breedte — dat vecht niet met de navigatie erboven.\n */\n embedded?: boolean;\n}) {\n const { user, role, loading, configured, signOut } = useAuth();\n const { name, siteId } = useSiteConfig();\n const { activeIds } = useModules();\n\n // Panelen van actieve modules waar de rol toegang toe heeft, gesorteerd op volgorde.\n const panels = [...builtinPanels, ...extraPanels]\n .filter((p) => activeIds.has(p.moduleId) && roleAllows(role, p.minRole))\n .sort((a, b) => a.order - b.order);\n /** Sleutel van een paneel in de tabbladen: eigen `id` als die er is, anders de module-id. */\n const sleutel = (p: PanelDefinition) => p.id ?? p.moduleId;\n const firstTab = panels[0] && sleutel(panels[0]);\n // Gecontroleerd i.p.v. `defaultValue`: het werkblad toont de naam van de sectie waar je in zit.\n // Zonder die kop lees je alleen aan de zijbalk-markering af waar je bent, en dat is te weinig\n // houvast op een breed scherm waar de zijbalk ver van je blik af staat.\n const [actief, setActief] = useState<string | undefined>(firstTab);\n const actievePanel = panels.find((p) => sleutel(p) === (actief ?? firstTab));\n\n return (\n <div className={embedded ? \"xicms-admin xicms-admin--ingebed\" : \"xicms-admin\"}>\n <ToastProvider>\n <ConfirmHost>\n {!configured ? (\n <Melding>\n Firebase is nog niet geconfigureerd. Vul de <code>NEXT_PUBLIC_FIREBASE_*</code>-variabelen in\n om het beheer aan te zetten.\n </Melding>\n ) : loading ? (\n <Melding>\n <span className=\"text-slate-500\">Bezig met laden…</span>\n </Melding>\n ) : !user ? (\n <div className=\"xicms-admin__midden grid place-items-center p-6\">\n <div className=\"w-full max-w-sm\">\n <p className=\"mb-4 text-center text-sm text-slate-500\">Beheer · {name}</p>\n {/*\n Na inloggen naar de HOMEPAGINA, niet naar dit scherm.\n\n Dat is een sturende keuze, geen detail. Wie op het beheer uitkomt gaat daar\n rondkijken en bewerkt zijn teksten in formulieren. Wie op zijn eigen site uitkomt\n ziet de beheerbalk over de pagina heen en past de tekst aan op de plek waar hij\n staat — en dat is waar XiCMS voor gemaakt is. Het beheer blijft één klik weg, via\n \"Beheren\" in die balk, voor het werk dat er wél thuishoort.\n\n Ook hier en niet alleen op /login: je komt op /beheer terecht via een bladwijzer of\n een oude link, en dan gold die keuze ineens niet.\n */}\n <LoginForm onSuccess={() => { window.location.replace(\"/\"); }} />\n </div>\n </div>\n ) : !canEdit(role) ? (\n <Melding>\n Je bent ingelogd als <strong>{user.email}</strong>, maar hebt geen beheerrechten voor deze site.\n <div className=\"mt-3\">\n <Button variant=\"secondary\" size=\"sm\" onClick={() => void signOut()}>Uitloggen</Button>\n </div>\n </Melding>\n ) : panels.length === 0 ? (\n <Melding>Geen actieve beheer-modules voor deze site.</Melding>\n ) : (\n <AdminNavContext.Provider value={setActief}>\n <Tabs value={actief ?? firstTab} onValueChange={setActief} orientation=\"vertical\" className=\"xicms-admin__raster md:grid md:grid-cols-[248px_1fr]\">\n {/* ── Zijbalk ─────────────────────────────────────────────────────────── */}\n <div\n className=\"xicms-admin__rail flex flex-col gap-4 border-b border-slate-200 p-4 md:sticky md:top-0 md:h-screen md:border-r md:border-b-0\"\n style={{ background: \"var(--admin-sidebar)\" }}\n >\n <div className=\"min-w-0 px-2 pt-1\">\n <p className=\"truncate text-sm font-semibold\">{name}</p>\n <p className=\"text-xs text-slate-500\">Beheer</p>\n </div>\n\n <TabsPrimitive.List aria-label=\"Beheer-secties\" className=\"xicms-admin__lijst -mx-1 flex gap-1 overflow-x-auto md:flex-col md:overflow-visible\">\n {panels.map((p) => (\n <TabsPrimitive.Trigger\n key={sleutel(p)}\n value={sleutel(p)}\n className=\"flex shrink-0 items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium text-slate-600 hover:bg-slate-200 hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none data-[state=active]:bg-slate-100 data-[state=active]:text-slate-900 data-[state=active]:shadow-[inset_2px_0_0_0_var(--color-brand-500)]\"\n >\n {/* Op paneel-sleutel, niet module-id: Menu, Bouwstenen en Start delen de\n content-module maar zijn drie verschillende plekken met drie vormen. */}\n <PanelIcon id={sleutel(p)} />\n {p.label}\n </TabsPrimitive.Trigger>\n ))}\n </TabsPrimitive.List>\n\n {/* Voet van de zijbalk: wie ben ik, welke engine draait er, en eruit. Stond eerder als\n vier losse regels op elkaar gepakt — e-mail, rol, versie en een onderstreepte link\n liepen visueel in elkaar over. Nu drie duidelijke lagen met lucht ertussen, en\n Uitloggen als een echte knop in plaats van een tekstlink. */}\n <div className=\"xicms-admin__voet mt-auto hidden min-w-0 flex-col gap-3 border-t border-slate-200 pt-3 md:flex\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-xs font-medium text-slate-600\" title={user.email ?? undefined}>{user.email}</p>\n <p className=\"text-xs text-slate-400\">{role}</p>\n </div>\n <CoreVersion />\n <Button variant=\"secondary\" size=\"sm\" onClick={() => void signOut()}>\n Uitloggen\n </Button>\n </div>\n </div>\n\n {/* ── Werkblad ────────────────────────────────────────────────────────── */}\n <div className=\"min-w-0\">\n <header className=\"xicms-admin__kop sticky top-0 z-10 flex items-center justify-between gap-4 border-b border-slate-200 px-4 py-3 md:px-8\" style={{ background: \"var(--admin-canvas)\" }}>\n <h1 className=\"truncate text-base font-semibold\">{actievePanel?.label ?? \"Beheer\"}</h1>\n <a\n className=\"inline-flex h-8 shrink-0 items-center rounded-lg border border-slate-200 px-3 text-sm hover:bg-slate-100 focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none\"\n href=\"/\"\n >\n Bekijk site ↗\n </a>\n </header>\n <div className=\"grid gap-6 p-4 md:p-8\">\n {panels.map((p) => (\n <TabsContent key={sleutel(p)} value={sleutel(p)} className=\"pt-0\">\n <p.Panel siteId={siteId} />\n </TabsContent>\n ))}\n <AdminHelp />\n </div>\n </div>\n </Tabs>\n </AdminNavContext.Provider>\n )}\n </ConfirmHost>\n </ToastProvider>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/admin/AdminApp.tsx"],"sourcesContent":["\"use client\";\n\nimport { registerBuiltinSchemas } from \"../blocks/builtinSchemas\";\n// Schema's zijn lazy (7.13): dit is een editor-chunk, dus hier horen ze thuis.\nregisterBuiltinSchemas();\nimport { useState } from \"react\";\nimport { useAuth } from \"../auth/AuthProvider\";\nimport { LoginForm } from \"../auth/LoginForm\";\nimport { useSiteConfig } from \"../config\";\nimport { useModules } from \"../modules/context\";\nimport { canEdit, canManageSite, type Role } from \"../auth/roles\";\nimport { Tabs, TabsContent } from \"./ui/tabs\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport { ToastProvider } from \"./ui/toast\";\nimport { ConfirmHost } from \"./ui/ConfirmDialog\";\nimport { Button } from \"./ui/primitives\";\nimport { PanelIcon } from \"./ui/PanelIcon\";\nimport { builtinPanels, type PanelDefinition } from \"./panelRegistry\";\nimport { AdminNavContext } from \"./navContext\";\nimport { AdminHelp } from \"./AdminHelp\";\nimport { CoreVersion, WatIsNieuw } from \"./CoreVersion\";\n\n/**\n * Rol-bewaakt beheer (XiCMS 3.0). De secties komen uit de paneel-registry, gefilterd op de actieve\n * modules (`SiteConfig.modules`/`features`) en de rol van de gebruiker. Een module met een eigen\n * paneel (ook klant-modules via `extraPanels`) verschijnt automatisch.\n *\n * VORM: een schermvullend werkblad met een vaste zijbalk, geen kaart in de pagina. Het beheer viel\n * hiervoor binnen de site-chrome — nav erboven, footer eronder, cookiebanner eroverheen — en las\n * daardoor als een blok ergens op de website in plaats van als gereedschap. De site-chrome leeft nu\n * in `app/(site)/layout.tsx`; /beheer valt daarbuiten.\n *\n * De zijbalk verving de horizontale tabstrip omdat die bij zeven of meer panelen al omsloeg naar een\n * tweede regel. Verticaal is er bovendien ruimte voor een icoon naast het woord, en `orientation`\n * houdt de pijltjestoetsen correct (Radix).\n *\n * Wikkel in `.xicms-admin` zodat de Tailwind-stijlen uit `@xiwebdesign/xicms-core/admin.css` scoped\n * blijven en niet naar de site lekken. Importeer die CSS alleen op de /beheer-route.\n */\nfunction roleAllows(role: Role | null, min: PanelDefinition[\"minRole\"]): boolean {\n return min === \"siteadmin\" ? canManageSite(role) : canEdit(role);\n}\n\n/** Gecentreerde boodschap voor de toestanden waarin er (nog) geen beheer te tonen is. */\nfunction Melding({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"xicms-admin__midden grid place-items-center p-6\">\n <div className=\"w-full max-w-md rounded-xl border border-slate-200 bg-white p-6 text-sm shadow-sm\">{children}</div>\n </div>\n );\n}\n\nexport function AdminApp({\n extraPanels = [],\n embedded = false,\n}: {\n extraPanels?: PanelDefinition[];\n /**\n * Draait het beheer BINNEN de site-schil (header, logo, menu, footer eromheen) in plaats van als\n * schermvullend werkblad op een kale pagina?\n *\n * Het verschil is niet cosmetisch. Los rekent het werkblad met het VENSTER: `min-h-screen`, en een\n * zijbalk die met `sticky top-0` aan de bovenkant van het scherm plakt. Zet je daar een\n * site-header boven, dan loopt de pagina 100vh + de hoogte van die header, en plakt de zijbalk\n * onder de header vast in plaats van erlangs. Ingebed laat de zijbalk daarom los van het venster\n * en wordt hij een balk over de volle breedte — dat vecht niet met de navigatie erboven.\n */\n embedded?: boolean;\n}) {\n const { user, role, loading, configured, signOut } = useAuth();\n const { name, siteId } = useSiteConfig();\n const { activeIds } = useModules();\n\n // Panelen van actieve modules waar de rol toegang toe heeft, gesorteerd op volgorde.\n const panels = [...builtinPanels, ...extraPanels]\n .filter((p) => activeIds.has(p.moduleId) && roleAllows(role, p.minRole))\n .sort((a, b) => a.order - b.order);\n /** Sleutel van een paneel in de tabbladen: eigen `id` als die er is, anders de module-id. */\n const sleutel = (p: PanelDefinition) => p.id ?? p.moduleId;\n const firstTab = panels[0] && sleutel(panels[0]);\n // Gecontroleerd i.p.v. `defaultValue`: het werkblad toont de naam van de sectie waar je in zit.\n // Zonder die kop lees je alleen aan de zijbalk-markering af waar je bent, en dat is te weinig\n // houvast op een breed scherm waar de zijbalk ver van je blik af staat.\n const [actief, setActief] = useState<string | undefined>(firstTab);\n const actievePanel = panels.find((p) => sleutel(p) === (actief ?? firstTab));\n\n return (\n <div className={embedded ? \"xicms-admin xicms-admin--ingebed\" : \"xicms-admin\"}>\n <ToastProvider>\n <ConfirmHost>\n {!configured ? (\n <Melding>\n Firebase is nog niet geconfigureerd. Vul de <code>NEXT_PUBLIC_FIREBASE_*</code>-variabelen in\n om het beheer aan te zetten.\n </Melding>\n ) : loading ? (\n <Melding>\n <span className=\"text-slate-500\">Bezig met laden…</span>\n </Melding>\n ) : !user ? (\n <div className=\"xicms-admin__midden grid place-items-center p-6\">\n <div className=\"w-full max-w-sm\">\n <p className=\"mb-4 text-center text-sm text-slate-500\">Beheer · {name}</p>\n {/*\n Na inloggen naar de HOMEPAGINA, niet naar dit scherm.\n\n Dat is een sturende keuze, geen detail. Wie op het beheer uitkomt gaat daar\n rondkijken en bewerkt zijn teksten in formulieren. Wie op zijn eigen site uitkomt\n ziet de beheerbalk over de pagina heen en past de tekst aan op de plek waar hij\n staat — en dat is waar XiCMS voor gemaakt is. Het beheer blijft één klik weg, via\n \"Beheren\" in die balk, voor het werk dat er wél thuishoort.\n\n Ook hier en niet alleen op /login: je komt op /beheer terecht via een bladwijzer of\n een oude link, en dan gold die keuze ineens niet.\n */}\n <LoginForm onSuccess={() => { window.location.replace(\"/\"); }} />\n </div>\n </div>\n ) : !canEdit(role) ? (\n <Melding>\n Je bent ingelogd als <strong>{user.email}</strong>, maar hebt geen beheerrechten voor deze site.\n <div className=\"mt-3\">\n <Button variant=\"secondary\" size=\"sm\" onClick={() => void signOut()}>Uitloggen</Button>\n </div>\n </Melding>\n ) : panels.length === 0 ? (\n <Melding>Geen actieve beheer-modules voor deze site.</Melding>\n ) : (\n <AdminNavContext.Provider value={setActief}>\n <Tabs value={actief ?? firstTab} onValueChange={setActief} orientation=\"vertical\" className=\"xicms-admin__raster md:grid md:grid-cols-[248px_1fr]\">\n {/* ── Zijbalk ─────────────────────────────────────────────────────────── */}\n <div\n className=\"xicms-admin__rail flex flex-col gap-4 border-b border-slate-200 p-4 md:sticky md:top-0 md:h-screen md:border-r md:border-b-0\"\n style={{ background: \"var(--admin-sidebar)\" }}\n >\n <div className=\"min-w-0 px-2 pt-1\">\n <p className=\"truncate text-sm font-semibold\">{name}</p>\n <p className=\"text-xs text-slate-500\">Beheer</p>\n </div>\n\n <TabsPrimitive.List aria-label=\"Beheer-secties\" className=\"xicms-admin__lijst -mx-1 flex gap-1 overflow-x-auto md:flex-col md:overflow-visible\">\n {panels.map((p) => (\n <TabsPrimitive.Trigger\n key={sleutel(p)}\n value={sleutel(p)}\n className=\"flex shrink-0 items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium text-slate-600 hover:bg-slate-200 hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none data-[state=active]:bg-slate-100 data-[state=active]:text-slate-900 data-[state=active]:shadow-[inset_2px_0_0_0_var(--color-brand-500)]\"\n >\n {/* Op paneel-sleutel, niet module-id: Menu, Bouwstenen en Start delen de\n content-module maar zijn drie verschillende plekken met drie vormen. */}\n <PanelIcon id={sleutel(p)} />\n {p.label}\n </TabsPrimitive.Trigger>\n ))}\n </TabsPrimitive.List>\n\n {/* Voet van de zijbalk: wie ben ik, welke engine draait er, en eruit. Stond eerder als\n vier losse regels op elkaar gepakt — e-mail, rol, versie en een onderstreepte link\n liepen visueel in elkaar over. Nu drie duidelijke lagen met lucht ertussen, en\n Uitloggen als een echte knop in plaats van een tekstlink. */}\n <div className=\"xicms-admin__voet mt-auto hidden min-w-0 flex-col gap-3 border-t border-slate-200 pt-3 md:flex\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-xs font-medium text-slate-600\" title={user.email ?? undefined}>{user.email}</p>\n <p className=\"text-xs text-slate-400\">{role}</p>\n </div>\n <CoreVersion />\n <Button variant=\"secondary\" size=\"sm\" onClick={() => void signOut()}>\n Uitloggen\n </Button>\n </div>\n </div>\n\n {/* ── Werkblad ────────────────────────────────────────────────────────── */}\n <div className=\"min-w-0\">\n <header className=\"xicms-admin__kop sticky top-0 z-10 flex items-center justify-between gap-4 border-b border-slate-200 px-4 py-3 md:px-8\" style={{ background: \"var(--admin-canvas)\" }}>\n <h1 className=\"truncate text-base font-semibold\">{actievePanel?.label ?? \"Beheer\"}</h1>\n <a\n className=\"inline-flex h-8 shrink-0 items-center rounded-lg border border-slate-200 px-3 text-sm hover:bg-slate-100 focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none\"\n href=\"/\"\n >\n Bekijk site ↗\n </a>\n </header>\n <div className=\"grid gap-6 p-4 md:p-8\">\n <WatIsNieuw />\n {panels.map((p) => (\n <TabsContent key={sleutel(p)} value={sleutel(p)} className=\"pt-0\">\n <p.Panel siteId={siteId} />\n </TabsContent>\n ))}\n <AdminHelp />\n </div>\n </div>\n </Tabs>\n </AdminNavContext.Provider>\n )}\n </ConfirmHost>\n </ToastProvider>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -11,3 +11,9 @@
11
11
  export declare function CoreVersion({ compact }: {
12
12
  compact?: boolean;
13
13
  }): import("react").JSX.Element;
14
+ /**
15
+ * "Wat is nieuw" (7.24): de release-notitie van de huidige engine, één keer per minor, in het
16
+ * werkblad. Features die je bouwt zonder dat de klant het hoort, bestaan voor die klant niet.
17
+ * Wegklikken onthoudt de versie (localStorage); de volgende minor toont de volgende notitie.
18
+ */
19
+ export declare function WatIsNieuw(): import("react").JSX.Element | null;
@@ -2,6 +2,7 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useState } from "react";
4
4
  import { CORE_VERSION, CORE_PACKAGE, isNieuwer } from "../version.js";
5
+ import { releaseNoteVoor } from "../releaseNotes.js";
5
6
  function CoreVersion({ compact = false }) {
6
7
  const [laatste, setLaatste] = useState(null);
7
8
  useEffect(() => {
@@ -32,7 +33,47 @@ function CoreVersion({ compact = false }) {
32
33
  ] }) : laatste ? /* @__PURE__ */ jsx("span", { className: "text-slate-500", children: "Bijgewerkt" }) : null
33
34
  ] });
34
35
  }
36
+ function WatIsNieuw() {
37
+ const note = releaseNoteVoor(CORE_VERSION);
38
+ const sleutel = `xicms-nieuw-gezien:${note?.versie ?? ""}`;
39
+ const [toon, setToon] = useState(false);
40
+ useEffect(() => {
41
+ if (!note) return;
42
+ try {
43
+ setToon(localStorage.getItem(sleutel) !== "1");
44
+ } catch {
45
+ setToon(true);
46
+ }
47
+ }, [note, sleutel]);
48
+ if (!note || !toon) return null;
49
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-brand-200 bg-brand-50 p-4 text-sm text-slate-700", role: "status", children: [
50
+ /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [
51
+ /* @__PURE__ */ jsxs("strong", { className: "text-slate-800", children: [
52
+ "Nieuw in het beheer (",
53
+ note.versie,
54
+ ")"
55
+ ] }),
56
+ /* @__PURE__ */ jsx(
57
+ "button",
58
+ {
59
+ type: "button",
60
+ className: "text-xs text-slate-500 hover:underline",
61
+ onClick: () => {
62
+ try {
63
+ localStorage.setItem(sleutel, "1");
64
+ } catch {
65
+ }
66
+ setToon(false);
67
+ },
68
+ children: "Gelezen"
69
+ }
70
+ )
71
+ ] }),
72
+ /* @__PURE__ */ jsx("ul", { className: "grid list-disc gap-1 pl-5", children: note.punten.map((p) => /* @__PURE__ */ jsx("li", { children: p }, p)) })
73
+ ] });
74
+ }
35
75
  export {
36
- CoreVersion
76
+ CoreVersion,
77
+ WatIsNieuw
37
78
  };
38
79
  //# sourceMappingURL=CoreVersion.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/admin/CoreVersion.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { CORE_VERSION, CORE_PACKAGE, isNieuwer } from \"../version\";\n\n/**\n * Toont op welke engine deze site draait, en of er een nieuwere is.\n *\n * De laatste versie komt rechtstreeks van de npm-registry. Dat is een publiek, CORS-vrij endpoint\n * en het is de enige bron die niet kan verouderen — een waarde die we zelf ergens zouden bijhouden\n * loopt per definitie achter op de release die hem zou moeten bijwerken.\n *\n * Faalt stil: geen netwerk of een registry die hapert mag het beheer niet in de weg zitten. Je ziet\n * dan alleen de eigen versie, wat nog steeds het antwoord is op de vraag \"waar draai ik op\".\n */\nexport function CoreVersion({ compact = false }: { compact?: boolean }) {\n const [laatste, setLaatste] = useState<string | null>(null);\n\n useEffect(() => {\n const ctrl = new AbortController();\n fetch(`https://registry.npmjs.org/${CORE_PACKAGE}/latest`, { signal: ctrl.signal })\n .then((r) => (r.ok ? r.json() : null))\n .then((d: { version?: string } | null) => {\n if (typeof d?.version === \"string\") setLaatste(d.version);\n })\n .catch(() => {});\n return () => ctrl.abort();\n }, []);\n\n const update = laatste ? isNieuwer(laatste, CORE_VERSION) : false;\n\n if (compact) {\n return (\n <span className=\"font-mono text-[11px] text-slate-500\">\n core {CORE_VERSION}\n {update && <span className=\"ml-1 text-amber-600\">→ {laatste}</span>}\n </span>\n );\n }\n\n return (\n <div className=\"flex flex-wrap items-center gap-2 text-xs\">\n <span className=\"text-slate-500\">Engine</span>\n <span className=\"rounded px-1.5 py-0.5 font-mono text-slate-700\" style={{ background: \"var(--color-background-muted)\" }}>\n {CORE_VERSION}\n </span>\n {update ? (\n <span className=\"text-amber-700\">\n Nieuwer beschikbaar: <span className=\"font-mono\">{laatste}</span>\n </span>\n ) : laatste ? (\n <span className=\"text-slate-500\">Bijgewerkt</span>\n ) : null}\n </div>\n );\n}\n"],"mappings":";AAmCmB,SAOb,KAPa;AAjCnB,SAAS,WAAW,gBAAgB;AACpC,SAAS,cAAc,cAAc,iBAAiB;AAY/C,SAAS,YAAY,EAAE,UAAU,MAAM,GAA0B;AACtE,QAAM,CAAC,SAAS,UAAU,IAAI,SAAwB,IAAI;AAE1D,YAAU,MAAM;AACd,UAAM,OAAO,IAAI,gBAAgB;AACjC,UAAM,8BAA8B,YAAY,WAAW,EAAE,QAAQ,KAAK,OAAO,CAAC,EAC/E,KAAK,CAAC,MAAO,EAAE,KAAK,EAAE,KAAK,IAAI,IAAK,EACpC,KAAK,CAAC,MAAmC;AACxC,UAAI,OAAO,GAAG,YAAY,SAAU,YAAW,EAAE,OAAO;AAAA,IAC1D,CAAC,EACA,MAAM,MAAM;AAAA,IAAC,CAAC;AACjB,WAAO,MAAM,KAAK,MAAM;AAAA,EAC1B,GAAG,CAAC,CAAC;AAEL,QAAM,SAAS,UAAU,UAAU,SAAS,YAAY,IAAI;AAE5D,MAAI,SAAS;AACX,WACE,qBAAC,UAAK,WAAU,wCAAuC;AAAA;AAAA,MAC/C;AAAA,MACL,UAAU,qBAAC,UAAK,WAAU,uBAAsB;AAAA;AAAA,QAAG;AAAA,SAAQ;AAAA,OAC9D;AAAA,EAEJ;AAEA,SACE,qBAAC,SAAI,WAAU,6CACb;AAAA,wBAAC,UAAK,WAAU,kBAAiB,oBAAM;AAAA,IACvC,oBAAC,UAAK,WAAU,kDAAiD,OAAO,EAAE,YAAY,gCAAgC,GACnH,wBACH;AAAA,IACC,SACC,qBAAC,UAAK,WAAU,kBAAiB;AAAA;AAAA,MACV,oBAAC,UAAK,WAAU,aAAa,mBAAQ;AAAA,OAC5D,IACE,UACF,oBAAC,UAAK,WAAU,kBAAiB,wBAAU,IACzC;AAAA,KACN;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/admin/CoreVersion.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { CORE_VERSION, CORE_PACKAGE, isNieuwer } from \"../version\";\nimport { releaseNoteVoor } from \"../releaseNotes\";\n\n/**\n * Toont op welke engine deze site draait, en of er een nieuwere is.\n *\n * De laatste versie komt rechtstreeks van de npm-registry. Dat is een publiek, CORS-vrij endpoint\n * en het is de enige bron die niet kan verouderen — een waarde die we zelf ergens zouden bijhouden\n * loopt per definitie achter op de release die hem zou moeten bijwerken.\n *\n * Faalt stil: geen netwerk of een registry die hapert mag het beheer niet in de weg zitten. Je ziet\n * dan alleen de eigen versie, wat nog steeds het antwoord is op de vraag \"waar draai ik op\".\n */\nexport function CoreVersion({ compact = false }: { compact?: boolean }) {\n const [laatste, setLaatste] = useState<string | null>(null);\n\n useEffect(() => {\n const ctrl = new AbortController();\n fetch(`https://registry.npmjs.org/${CORE_PACKAGE}/latest`, { signal: ctrl.signal })\n .then((r) => (r.ok ? r.json() : null))\n .then((d: { version?: string } | null) => {\n if (typeof d?.version === \"string\") setLaatste(d.version);\n })\n .catch(() => {});\n return () => ctrl.abort();\n }, []);\n\n const update = laatste ? isNieuwer(laatste, CORE_VERSION) : false;\n\n if (compact) {\n return (\n <span className=\"font-mono text-[11px] text-slate-500\">\n core {CORE_VERSION}\n {update && <span className=\"ml-1 text-amber-600\">→ {laatste}</span>}\n </span>\n );\n }\n\n return (\n <div className=\"flex flex-wrap items-center gap-2 text-xs\">\n <span className=\"text-slate-500\">Engine</span>\n <span className=\"rounded px-1.5 py-0.5 font-mono text-slate-700\" style={{ background: \"var(--color-background-muted)\" }}>\n {CORE_VERSION}\n </span>\n {update ? (\n <span className=\"text-amber-700\">\n Nieuwer beschikbaar: <span className=\"font-mono\">{laatste}</span>\n </span>\n ) : laatste ? (\n <span className=\"text-slate-500\">Bijgewerkt</span>\n ) : null}\n </div>\n );\n}\n\n/**\n * \"Wat is nieuw\" (7.24): de release-notitie van de huidige engine, één keer per minor, in het\n * werkblad. Features die je bouwt zonder dat de klant het hoort, bestaan voor die klant niet.\n * Wegklikken onthoudt de versie (localStorage); de volgende minor toont de volgende notitie.\n */\nexport function WatIsNieuw() {\n const note = releaseNoteVoor(CORE_VERSION);\n const sleutel = `xicms-nieuw-gezien:${note?.versie ?? \"\"}`;\n const [toon, setToon] = useState(false);\n useEffect(() => {\n if (!note) return;\n try {\n setToon(localStorage.getItem(sleutel) !== \"1\");\n } catch {\n setToon(true);\n }\n }, [note, sleutel]);\n if (!note || !toon) return null;\n return (\n <div className=\"rounded-xl border border-brand-200 bg-brand-50 p-4 text-sm text-slate-700\" role=\"status\">\n <div className=\"mb-2 flex items-center justify-between gap-2\">\n <strong className=\"text-slate-800\">Nieuw in het beheer ({note.versie})</strong>\n <button\n type=\"button\"\n className=\"text-xs text-slate-500 hover:underline\"\n onClick={() => {\n try {\n localStorage.setItem(sleutel, \"1\");\n } catch {\n /* opslag geweigerd */\n }\n setToon(false);\n }}\n >\n Gelezen\n </button>\n </div>\n <ul className=\"grid list-disc gap-1 pl-5\">\n {note.punten.map((p) => (\n <li key={p}>{p}</li>\n ))}\n </ul>\n </div>\n );\n}\n"],"mappings":";AAoCmB,SAOb,KAPa;AAlCnB,SAAS,WAAW,gBAAgB;AACpC,SAAS,cAAc,cAAc,iBAAiB;AACtD,SAAS,uBAAuB;AAYzB,SAAS,YAAY,EAAE,UAAU,MAAM,GAA0B;AACtE,QAAM,CAAC,SAAS,UAAU,IAAI,SAAwB,IAAI;AAE1D,YAAU,MAAM;AACd,UAAM,OAAO,IAAI,gBAAgB;AACjC,UAAM,8BAA8B,YAAY,WAAW,EAAE,QAAQ,KAAK,OAAO,CAAC,EAC/E,KAAK,CAAC,MAAO,EAAE,KAAK,EAAE,KAAK,IAAI,IAAK,EACpC,KAAK,CAAC,MAAmC;AACxC,UAAI,OAAO,GAAG,YAAY,SAAU,YAAW,EAAE,OAAO;AAAA,IAC1D,CAAC,EACA,MAAM,MAAM;AAAA,IAAC,CAAC;AACjB,WAAO,MAAM,KAAK,MAAM;AAAA,EAC1B,GAAG,CAAC,CAAC;AAEL,QAAM,SAAS,UAAU,UAAU,SAAS,YAAY,IAAI;AAE5D,MAAI,SAAS;AACX,WACE,qBAAC,UAAK,WAAU,wCAAuC;AAAA;AAAA,MAC/C;AAAA,MACL,UAAU,qBAAC,UAAK,WAAU,uBAAsB;AAAA;AAAA,QAAG;AAAA,SAAQ;AAAA,OAC9D;AAAA,EAEJ;AAEA,SACE,qBAAC,SAAI,WAAU,6CACb;AAAA,wBAAC,UAAK,WAAU,kBAAiB,oBAAM;AAAA,IACvC,oBAAC,UAAK,WAAU,kDAAiD,OAAO,EAAE,YAAY,gCAAgC,GACnH,wBACH;AAAA,IACC,SACC,qBAAC,UAAK,WAAU,kBAAiB;AAAA;AAAA,MACV,oBAAC,UAAK,WAAU,aAAa,mBAAQ;AAAA,OAC5D,IACE,UACF,oBAAC,UAAK,WAAU,kBAAiB,wBAAU,IACzC;AAAA,KACN;AAEJ;AAOO,SAAS,aAAa;AAC3B,QAAM,OAAO,gBAAgB,YAAY;AACzC,QAAM,UAAU,sBAAsB,MAAM,UAAU,EAAE;AACxD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,QAAI;AACF,cAAQ,aAAa,QAAQ,OAAO,MAAM,GAAG;AAAA,IAC/C,QAAQ;AACN,cAAQ,IAAI;AAAA,IACd;AAAA,EACF,GAAG,CAAC,MAAM,OAAO,CAAC;AAClB,MAAI,CAAC,QAAQ,CAAC,KAAM,QAAO;AAC3B,SACE,qBAAC,SAAI,WAAU,6EAA4E,MAAK,UAC9F;AAAA,yBAAC,SAAI,WAAU,gDACb;AAAA,2BAAC,YAAO,WAAU,kBAAiB;AAAA;AAAA,QAAsB,KAAK;AAAA,QAAO;AAAA,SAAC;AAAA,MACtE;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,WAAU;AAAA,UACV,SAAS,MAAM;AACb,gBAAI;AACF,2BAAa,QAAQ,SAAS,GAAG;AAAA,YACnC,QAAQ;AAAA,YAER;AACA,oBAAQ,KAAK;AAAA,UACf;AAAA,UACD;AAAA;AAAA,MAED;AAAA,OACF;AAAA,IACA,oBAAC,QAAG,WAAU,6BACX,eAAK,OAAO,IAAI,CAAC,MAChB,oBAAC,QAAY,eAAJ,CAAM,CAChB,GACH;AAAA,KACF;AAEJ;","names":[]}
@@ -52,11 +52,12 @@ function SuperadminPanel({ siteId }) {
52
52
  ] })
53
53
  ] }),
54
54
  /* @__PURE__ */ jsxs(Card, { children: [
55
- /* @__PURE__ */ jsx("h3", { className: "mb-3 text-base font-semibold", children: "Beheeraccount" }),
55
+ /* @__PURE__ */ jsx("h3", { className: "mb-1 text-base font-semibold", children: "Beheeraccount" }),
56
+ /* @__PURE__ */ jsx("p", { className: "mb-3 text-xs text-slate-500", children: "Alleen jij maakt accounts aan; siteadmins kunnen dat niet. De gebruiker krijgt een uitnodiging met inloglink gemaild (geen wachtwoord nodig \u2014 de login is wachtwoordloos)." }),
56
57
  /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
57
58
  /* @__PURE__ */ jsx(Field, { label: "siteId", children: /* @__PURE__ */ jsx(Input, { value: account.siteId, onChange: (e) => setAccount({ ...account, siteId: e.target.value }) }) }),
58
59
  /* @__PURE__ */ jsx(Field, { label: "E-mail", children: /* @__PURE__ */ jsx(Input, { type: "email", value: account.email, onChange: (e) => setAccount({ ...account, email: e.target.value }) }) }),
59
- /* @__PURE__ */ jsx(Field, { label: "Tijdelijk wachtwoord", children: /* @__PURE__ */ jsx(Input, { value: account.password, onChange: (e) => setAccount({ ...account, password: e.target.value }) }) }),
60
+ /* @__PURE__ */ jsx(Field, { label: "Wachtwoord (optioneel)", hint: "Leeg = uitnodiging met inloglink. Alleen invullen als iemand per se met een wachtwoord wil inloggen.", children: /* @__PURE__ */ jsx(Input, { value: account.password, onChange: (e) => setAccount({ ...account, password: e.target.value }), autoComplete: "off" }) }),
60
61
  /* @__PURE__ */ jsx(Field, { label: "Rol", children: /* @__PURE__ */ jsx(
61
62
  Select,
62
63
  {
@@ -68,7 +69,22 @@ function SuperadminPanel({ siteId }) {
68
69
  ]
69
70
  }
70
71
  ) }),
71
- /* @__PURE__ */ jsx(Button, { disabled: busy || !account.siteId || !account.email, onClick: () => run(() => superadminCall("createSiteAccount")(account), "Account aangemaakt"), children: "Account aanmaken" })
72
+ /* @__PURE__ */ jsx(
73
+ Button,
74
+ {
75
+ disabled: busy || !account.siteId || !account.email,
76
+ onClick: () => run(
77
+ () => superadminCall("createSiteAccount")({
78
+ ...account,
79
+ // Leeg wachtwoord NIET meesturen: de callable maakt dan een wachtwoordloos account en
80
+ // mailt een uitnodiging.
81
+ password: account.password.trim() || void 0
82
+ }),
83
+ "Account aangemaakt"
84
+ ),
85
+ children: account.password.trim() ? "Account aanmaken" : "Uitnodigen"
86
+ }
87
+ )
72
88
  ] })
73
89
  ] }),
74
90
  /* @__PURE__ */ jsxs(Card, { children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/admin/SuperadminPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { collection, getDocs, limit, orderBy, query, where } from \"firebase/firestore\";\nimport { callable } from \"../firebase/functionsClient\";\nimport { getDb, getFirebaseApp } from \"../firebase/client\";\nimport { Button, Card, Field, Input } from \"./ui/primitives\";\nimport { Select } from \"./ui/select\";\nimport { useToast } from \"./ui/toast\";\n\n/** Callable van dit paneel: zoekt de app op en geeft de standaard `{ message }`-respons terug. */\nfunction superadminCall<T>(name: string) {\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet geconfigureerd.\");\n return callable<T, { message?: string }>(app, name);\n}\n\n/** Superadmin: sites/accounts aanmaken, wachtwoord-reset versturen en audit-log inzien. */\nexport function SuperadminPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const [site, setSite] = useState({ siteId: \"\", name: \"\", domain: \"\" });\n const [account, setAccount] = useState({ siteId: \"\", email: \"\", password: \"\", role: \"siteadmin\" });\n const [resetEmail, setResetEmail] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const [audit, setAudit] = useState<{ id: string; action?: string; target?: string; actorUid?: string }[]>([]);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n const q = query(\n collection(db, \"audit_log\"),\n where(\"siteId\", \"in\", [siteId, \"*\"]),\n orderBy(\"at\", \"desc\"),\n limit(20),\n );\n getDocs(q)\n .then((snap) => setAudit(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Record<string, string>) }))))\n .catch(() => setAudit([]));\n }, [siteId]);\n\n async function run(fn: () => Promise<{ data: { message?: string } }>, ok: string) {\n setBusy(true);\n try {\n const res = await fn();\n toast({ title: res.data.message ?? ok, tone: \"success\" });\n } catch (e) {\n toast({ title: \"Mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n return (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Nieuwe site</h3>\n <div className=\"grid gap-3\">\n <Field label=\"siteId\"><Input value={site.siteId} onChange={(e) => setSite({ ...site, siteId: e.target.value })} placeholder=\"bv. bakkerij-jansen\" /></Field>\n <Field label=\"Naam\"><Input value={site.name} onChange={(e) => setSite({ ...site, name: e.target.value })} /></Field>\n <Field label=\"Domein\"><Input value={site.domain} onChange={(e) => setSite({ ...site, domain: e.target.value })} /></Field>\n <Button disabled={busy || !site.siteId} onClick={() => run(() => superadminCall<typeof site>(\"createSite\")(site), \"Site aangemaakt\")}>\n Site aanmaken\n </Button>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Beheeraccount</h3>\n <div className=\"grid gap-3\">\n <Field label=\"siteId\"><Input value={account.siteId} onChange={(e) => setAccount({ ...account, siteId: e.target.value })} /></Field>\n <Field label=\"E-mail\"><Input type=\"email\" value={account.email} onChange={(e) => setAccount({ ...account, email: e.target.value })} /></Field>\n <Field label=\"Tijdelijk wachtwoord\"><Input value={account.password} onChange={(e) => setAccount({ ...account, password: e.target.value })} /></Field>\n <Field label=\"Rol\">\n <Select\n value={account.role}\n onValueChange={(v) => setAccount({ ...account, role: v })}\n options={[\n { value: \"siteadmin\", label: \"siteadmin (volledig)\" },\n { value: \"editor\", label: \"editor (alleen content)\" },\n ]}\n />\n </Field>\n <Button disabled={busy || !account.siteId || !account.email} onClick={() => run(() => superadminCall<typeof account>(\"createSiteAccount\")(account), \"Account aangemaakt\")}>\n Account aanmaken\n </Button>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Wachtwoord-reset versturen</h3>\n <div className=\"grid gap-3\">\n <Field label=\"E-mail\"><Input type=\"email\" value={resetEmail} onChange={(e) => setResetEmail(e.target.value)} /></Field>\n <Button variant=\"secondary\" disabled={busy || !resetEmail} onClick={() => run(() => superadminCall<{ email: string }>(\"requestPasswordReset\")({ email: resetEmail }), \"Reset-link verstuurd\")}>\n Reset-link versturen\n </Button>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Audit-log (laatste 20)</h3>\n {audit.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Geen recente acties (of geen rechten/index).</p>\n ) : (\n <ul className=\"text-sm text-slate-600\">\n {audit.map((a) => (\n <li key={a.id} className=\"border-b border-slate-100 py-1 last:border-0\">\n <span className=\"font-medium\">{a.action}</span> · {a.target} <span className=\"text-slate-400\">({a.actorUid?.slice(0, 6)})</span>\n </li>\n ))}\n </ul>\n )}\n </Card>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/admin/SuperadminPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { collection, getDocs, limit, orderBy, query, where } from \"firebase/firestore\";\nimport { callable } from \"../firebase/functionsClient\";\nimport { getDb, getFirebaseApp } from \"../firebase/client\";\nimport { Button, Card, Field, Input } from \"./ui/primitives\";\nimport { Select } from \"./ui/select\";\nimport { useToast } from \"./ui/toast\";\n\n/** Callable van dit paneel: zoekt de app op en geeft de standaard `{ message }`-respons terug. */\nfunction superadminCall<T>(name: string) {\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet geconfigureerd.\");\n return callable<T, { message?: string }>(app, name);\n}\n\n/** Superadmin: sites/accounts aanmaken, wachtwoord-reset versturen en audit-log inzien. */\nexport function SuperadminPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const [site, setSite] = useState({ siteId: \"\", name: \"\", domain: \"\" });\n const [account, setAccount] = useState({ siteId: \"\", email: \"\", password: \"\", role: \"siteadmin\" });\n const [resetEmail, setResetEmail] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const [audit, setAudit] = useState<{ id: string; action?: string; target?: string; actorUid?: string }[]>([]);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n const q = query(\n collection(db, \"audit_log\"),\n where(\"siteId\", \"in\", [siteId, \"*\"]),\n orderBy(\"at\", \"desc\"),\n limit(20),\n );\n getDocs(q)\n .then((snap) => setAudit(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Record<string, string>) }))))\n .catch(() => setAudit([]));\n }, [siteId]);\n\n async function run(fn: () => Promise<{ data: { message?: string } }>, ok: string) {\n setBusy(true);\n try {\n const res = await fn();\n toast({ title: res.data.message ?? ok, tone: \"success\" });\n } catch (e) {\n toast({ title: \"Mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n return (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Nieuwe site</h3>\n <div className=\"grid gap-3\">\n <Field label=\"siteId\"><Input value={site.siteId} onChange={(e) => setSite({ ...site, siteId: e.target.value })} placeholder=\"bv. bakkerij-jansen\" /></Field>\n <Field label=\"Naam\"><Input value={site.name} onChange={(e) => setSite({ ...site, name: e.target.value })} /></Field>\n <Field label=\"Domein\"><Input value={site.domain} onChange={(e) => setSite({ ...site, domain: e.target.value })} /></Field>\n <Button disabled={busy || !site.siteId} onClick={() => run(() => superadminCall<typeof site>(\"createSite\")(site), \"Site aangemaakt\")}>\n Site aanmaken\n </Button>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Beheeraccount</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n Alleen jij maakt accounts aan; siteadmins kunnen dat niet. De gebruiker krijgt een\n uitnodiging met inloglink gemaild (geen wachtwoord nodig — de login is wachtwoordloos).\n </p>\n <div className=\"grid gap-3\">\n <Field label=\"siteId\"><Input value={account.siteId} onChange={(e) => setAccount({ ...account, siteId: e.target.value })} /></Field>\n <Field label=\"E-mail\"><Input type=\"email\" value={account.email} onChange={(e) => setAccount({ ...account, email: e.target.value })} /></Field>\n <Field label=\"Wachtwoord (optioneel)\" hint=\"Leeg = uitnodiging met inloglink. Alleen invullen als iemand per se met een wachtwoord wil inloggen.\">\n <Input value={account.password} onChange={(e) => setAccount({ ...account, password: e.target.value })} autoComplete=\"off\" />\n </Field>\n <Field label=\"Rol\">\n <Select\n value={account.role}\n onValueChange={(v) => setAccount({ ...account, role: v })}\n options={[\n { value: \"siteadmin\", label: \"siteadmin (volledig)\" },\n { value: \"editor\", label: \"editor (alleen content)\" },\n ]}\n />\n </Field>\n <Button\n disabled={busy || !account.siteId || !account.email}\n onClick={() =>\n run(\n () => superadminCall<Omit<typeof account, \"password\"> & { password?: string }>(\"createSiteAccount\")({\n ...account,\n // Leeg wachtwoord NIET meesturen: de callable maakt dan een wachtwoordloos account en\n // mailt een uitnodiging.\n password: account.password.trim() || undefined,\n }),\n \"Account aangemaakt\",\n )\n }\n >\n {account.password.trim() ? \"Account aanmaken\" : \"Uitnodigen\"}\n </Button>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Wachtwoord-reset versturen</h3>\n <div className=\"grid gap-3\">\n <Field label=\"E-mail\"><Input type=\"email\" value={resetEmail} onChange={(e) => setResetEmail(e.target.value)} /></Field>\n <Button variant=\"secondary\" disabled={busy || !resetEmail} onClick={() => run(() => superadminCall<{ email: string }>(\"requestPasswordReset\")({ email: resetEmail }), \"Reset-link verstuurd\")}>\n Reset-link versturen\n </Button>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Audit-log (laatste 20)</h3>\n {audit.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Geen recente acties (of geen rechten/index).</p>\n ) : (\n <ul className=\"text-sm text-slate-600\">\n {audit.map((a) => (\n <li key={a.id} className=\"border-b border-slate-100 py-1 last:border-0\">\n <span className=\"font-medium\">{a.action}</span> · {a.target} <span className=\"text-slate-400\">({a.actorUid?.slice(0, 6)})</span>\n </li>\n ))}\n </ul>\n )}\n </Card>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -9,7 +9,8 @@ import {
9
9
  getDocs,
10
10
  limit,
11
11
  orderBy,
12
- query
12
+ query,
13
+ where
13
14
  } from "firebase/firestore";
14
15
  import { getDb, getFirebaseApp } from "../../firebase/client.js";
15
16
  import { callable } from "../../firebase/functionsClient.js";
@@ -22,6 +23,8 @@ import { useToast } from "../ui/toast.js";
22
23
  import { useAdminNav } from "../navContext.js";
23
24
  import { BalkjesLijst, DagStaven } from "../ui/charts.js";
24
25
  import { dagenInMaand, dagReeks, deltaTovVorige, huidigMaandId, maandLabel, topPaden } from "../stats/lib.js";
26
+ import { paginaToestand } from "../lib/pagesClient.js";
27
+ import { formulierenGezienSinds } from "../forms/gezien.js";
25
28
  function updatedMs(v) {
26
29
  if (v && typeof v.toDate === "function") return v.toDate().getTime();
27
30
  if (typeof v === "string") {
@@ -61,6 +64,8 @@ function DashboardPanel({ siteId }) {
61
64
  const heeftShop = activeIds.has("shop");
62
65
  const magInstellen = canManageSite(role);
63
66
  const [paginas, setPaginas] = useState(null);
67
+ const [nietLive, setNietLive] = useState([]);
68
+ const [teVerzenden, setTeVerzenden] = useState(0);
64
69
  const [maand, setMaand] = useState(void 0);
65
70
  const [vorigeMaand, setVorigeMaand] = useState(null);
66
71
  const [orders, setOrders] = useState(null);
@@ -77,11 +82,11 @@ function DashboardPanel({ siteId }) {
77
82
  setFormulieren([]);
78
83
  return;
79
84
  }
80
- getDocs(collection(db, "sites", siteId, "drafts")).then(
81
- (snap) => setPaginas(
82
- snap.docs.map((d) => ({ id: d.id, ...d.data() })).sort((a, b) => updatedMs(b.updatedAt) - updatedMs(a.updatedAt)).slice(0, 5)
83
- )
84
- ).catch(() => setPaginas([]));
85
+ getDocs(collection(db, "sites", siteId, "drafts")).then((snap) => {
86
+ const alle = snap.docs.map((d) => ({ id: d.id, ...d.data() })).filter((p) => !p.trashed).sort((a, b) => updatedMs(b.updatedAt) - updatedMs(a.updatedAt));
87
+ setPaginas(alle.slice(0, 5));
88
+ setNietLive(alle.filter((p) => paginaToestand({ ...p, blocks: [] }) === "gewijzigd"));
89
+ }).catch(() => setPaginas([]));
85
90
  const maandId = huidigMaandId();
86
91
  const [j, m] = maandId.split("-").map(Number);
87
92
  const vd = new Date(Date.UTC(j, (m ?? 1) - 2, 1));
@@ -90,6 +95,7 @@ function DashboardPanel({ siteId }) {
90
95
  getDoc(doc(db, "sites", siteId, "stats", vorigeId)).then((snap) => setVorigeMaand(snap.exists() ? snap.data() : null)).catch(() => setVorigeMaand(null));
91
96
  if (heeftShop) {
92
97
  getDocs(query(collection(db, "sites", siteId, "orders"), orderBy("createdAt", "desc"), limit(5))).then((snap) => setOrders(snap.docs.map((d) => ({ id: d.id, ...d.data() })))).catch(() => setOrders([]));
98
+ getCountFromServer(query(collection(db, "sites", siteId, "orders"), where("status", "==", "paid"))).then((c) => setTeVerzenden(c.data().count)).catch(() => setTeVerzenden(0));
93
99
  } else {
94
100
  setOrders([]);
95
101
  }
@@ -98,11 +104,14 @@ function DashboardPanel({ siteId }) {
98
104
  const uit = await Promise.all(
99
105
  forms.map(async (f) => {
100
106
  const subs = collection(db, "sites", siteId, "forms", f.id, "submissions");
101
- const [count, laatste] = await Promise.all([
107
+ const sinds = formulierenGezienSinds(siteId, f.id);
108
+ const [count, laatste, nieuw] = await Promise.all([
102
109
  getCountFromServer(subs).then((c) => c.data().count).catch(() => 0),
103
- getDocs(query(subs, orderBy("createdAt", "desc"), limit(1))).then((s) => s.docs[0]?.data()?.createdAt).catch(() => void 0)
110
+ getDocs(query(subs, orderBy("createdAt", "desc"), limit(1))).then((s) => s.docs[0]?.data()?.createdAt).catch(() => void 0),
111
+ // Nieuw sinds je laatste bezoek aan dit formulier (moment staat lokaal, zie forms/gezien.ts).
112
+ sinds ? getCountFromServer(query(subs, where("createdAt", ">", sinds))).then((c) => c.data().count).catch(() => 0) : Promise.resolve(0)
104
113
  ]);
105
- return { id: f.id, name: f.data().name ?? f.id, aantal: count, laatste };
114
+ return { id: f.id, name: f.data().name ?? f.id, aantal: count, laatste, nieuw };
106
115
  })
107
116
  );
108
117
  setFormulieren(uit);
@@ -171,6 +180,34 @@ function DashboardPanel({ siteId }) {
171
180
  ] })
172
181
  ] })
173
182
  ] }),
183
+ (() => {
184
+ const nieuweInzendingen = (formulieren ?? []).reduce((som, f) => som + f.nieuw, 0);
185
+ const items = [];
186
+ if (nietLive.length > 0) {
187
+ items.push({
188
+ tekst: nietLive.length === 1 ? `1 pagina heeft wijzigingen die nog niet live staan (${nietLive[0].title ?? nietLive[0].path})` : `${nietLive.length} pagina's hebben wijzigingen die nog niet live staan`,
189
+ tab: "pages",
190
+ tabLabel: "Naar Pagina's"
191
+ });
192
+ }
193
+ if (nieuweInzendingen > 0) {
194
+ items.push({ tekst: nieuweInzendingen === 1 ? "1 nieuwe inzending" : `${nieuweInzendingen} nieuwe inzendingen`, tab: "forms", tabLabel: "Naar Formulieren" });
195
+ }
196
+ if (heeftShop && teVerzenden > 0) {
197
+ items.push({ tekst: teVerzenden === 1 ? "1 betaalde bestelling wacht op verzending" : `${teVerzenden} betaalde bestellingen wachten op verzending`, tab: "shop", tabLabel: "Naar Webwinkel" });
198
+ }
199
+ if (items.length === 0) return null;
200
+ return /* @__PURE__ */ jsxs(Card, { children: [
201
+ /* @__PURE__ */ jsx("h2", { className: "mb-2 text-sm font-semibold text-slate-800", children: "Wacht op jou" }),
202
+ /* @__PURE__ */ jsx("ul", { className: "grid gap-1.5", children: items.map((it) => /* @__PURE__ */ jsxs("li", { className: "flex flex-wrap items-center justify-between gap-2 rounded-lg bg-amber-50 px-3 py-2 text-sm text-amber-900", children: [
203
+ /* @__PURE__ */ jsx("span", { children: it.tekst }),
204
+ /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => gaNaar(it.tab), className: "text-xs font-medium text-brand-700 hover:underline", children: [
205
+ it.tabLabel,
206
+ " \u2192"
207
+ ] })
208
+ ] }, it.tab)) })
209
+ ] });
210
+ })(),
174
211
  /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [
175
212
  /* @__PURE__ */ jsxs(Card, { children: [
176
213
  /* @__PURE__ */ jsx(WidgetKop, { titel: "Bezoek deze maand", tab: "stats", tabLabel: "Statistieken" }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/dashboard/DashboardPanel.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Startpagina van /beheer (7.16).\n *\n * /beheer opende recht in het pagina-overzicht: een lijst technische titels als eerste indruk, en\n * nergens een plek die uitlegt hoe het systeem in elkaar zit. Deze startpagina doet drie dingen:\n * uitleggen hoe je de site bewerkt (op de site zelf, niet hier), in één oogopslag tonen wat er\n * gebeurt (bezoek, bestellingen, inzendingen, laatst bewerkt), en snelkoppelingen bieden die de\n * siteadmin zelf kan instellen.\n *\n * Bewust eenmalige reads (geen live listeners): dit is een overzicht bij binnenkomst, geen\n * monitor. Elke widget vangt zijn eigen fouten — een dichtgezette collectie (rules, module uit)\n * mag nooit de hele startpagina blokkeren.\n */\nimport { useEffect, useMemo, useState } from \"react\";\nimport {\n collection,\n doc,\n getCountFromServer,\n getDoc,\n getDocs,\n limit,\n orderBy,\n query,\n} from \"firebase/firestore\";\nimport type { Timestamp } from \"firebase/firestore\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { callable } from \"../../firebase/functionsClient\";\nimport { useAuth } from \"../../auth/AuthProvider\";\nimport { canManageSite } from \"../../auth/roles\";\nimport { useModules } from \"../../modules/context\";\nimport { formatCents } from \"../../schema/shop\";\nimport { Badge, Button, Card, Input } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { useAdminNav } from \"../navContext\";\nimport { BalkjesLijst, DagStaven } from \"../ui/charts\";\nimport { dagenInMaand, dagReeks, deltaTovVorige, huidigMaandId, maandLabel, topPaden, type MaandStat as StatsMaand } from \"../stats/lib\";\n\ninterface RecentePagina {\n id: string;\n title?: string;\n path?: string;\n /** Timestamp uit de editor, ISO-string uit import-scripts — zie updatedMs(). */\n updatedAt?: unknown;\n}\ntype MaandStat = Omit<StatsMaand, \"id\">;\ninterface RecenteOrder {\n id: string;\n status?: string;\n createdAt?: Timestamp;\n customer?: { name?: string };\n amount?: { totalCents?: number };\n}\ninterface FormOverzicht {\n id: string;\n name: string;\n aantal: number;\n laatste?: Timestamp;\n}\ninterface Snelkoppeling {\n label: string;\n href: string;\n}\n\n/**\n * `updatedAt` is in de praktijk een Firestore-Timestamp (editor-saves) ÓF een ISO-string\n * (import-/migratiescripts schrijven via REST) — live aangetroffen op christelijkkraamcadeau,\n * dóór elkaar in dezelfde collectie. Firestore sorteert die in aparte typegroepen, dus een\n * server-side orderBy geeft dan een fout beeld; we normaliseren en sorteren daarom client-side.\n */\nfunction updatedMs(v: unknown): number {\n if (v && typeof (v as Timestamp).toDate === \"function\") return (v as Timestamp).toDate().getTime();\n if (typeof v === \"string\") {\n const t = Date.parse(v);\n return Number.isNaN(t) ? 0 : t;\n }\n return 0;\n}\n\nfunction fmtDatum(v: unknown): string {\n const ms = updatedMs(v);\n if (!ms) return \"—\";\n return new Date(ms).toLocaleString(\"nl-NL\", { dateStyle: \"short\", timeStyle: \"short\" });\n}\n\n/** Kop van een widget met een \"ga ernaartoe\"-pijl die naar het bijbehorende paneel springt. */\nfunction WidgetKop({ titel, tab, tabLabel }: { titel: string; tab?: string; tabLabel?: string }) {\n const gaNaar = useAdminNav();\n return (\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"text-sm font-semibold text-slate-800\">{titel}</h2>\n {tab && (\n <button\n type=\"button\"\n onClick={() => gaNaar(tab)}\n className=\"shrink-0 text-xs font-medium text-brand-700 hover:underline focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none\"\n >\n {tabLabel ?? \"Bekijk alles\"} →\n </button>\n )}\n </div>\n );\n}\n\nexport function DashboardPanel({ siteId }: { siteId: string }) {\n const { role } = useAuth();\n const { activeIds } = useModules();\n const { toast } = useToast();\n const gaNaar = useAdminNav();\n const heeftShop = activeIds.has(\"shop\");\n const magInstellen = canManageSite(role);\n\n const [paginas, setPaginas] = useState<RecentePagina[] | null>(null);\n const [maand, setMaand] = useState<MaandStat | null | undefined>(undefined);\n const [vorigeMaand, setVorigeMaand] = useState<MaandStat | null>(null);\n const [orders, setOrders] = useState<RecenteOrder[] | null>(null);\n const [formulieren, setFormulieren] = useState<FormOverzicht[] | null>(null);\n const [links, setLinks] = useState<Snelkoppeling[]>([]);\n const [linksBewerken, setLinksBewerken] = useState<Snelkoppeling[] | null>(null);\n const [linksOpslaan, setLinksOpslaan] = useState(false);\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setPaginas([]);\n setMaand(null);\n setOrders([]);\n setFormulieren([]);\n return;\n }\n\n // Laatst bewerkte pagina's. Bewust GEEN server-side orderBy op updatedAt: dat veld is een\n // mengsel van Timestamps en ISO-strings (zie updatedMs) en Firestore sorteert per typegroep.\n // Drafts-collecties zijn klein; alles ophalen en zelf sorteren is hier de betrouwbare weg.\n getDocs(collection(db, \"sites\", siteId, \"drafts\"))\n .then((snap) =>\n setPaginas(\n snap.docs\n .map((d) => ({ id: d.id, ...(d.data() as Omit<RecentePagina, \"id\">) }))\n .sort((a, b) => updatedMs(b.updatedAt) - updatedMs(a.updatedAt))\n .slice(0, 5),\n ),\n )\n .catch(() => setPaginas([]));\n\n // Bezoek deze maand + vorige maand (voor de eerlijke delta) — zelfde bron als Statistieken.\n const maandId = huidigMaandId();\n const [j, m] = maandId.split(\"-\").map(Number);\n const vd = new Date(Date.UTC(j, (m ?? 1) - 2, 1));\n const vorigeId = `${vd.getUTCFullYear()}-${String(vd.getUTCMonth() + 1).padStart(2, \"0\")}`;\n getDoc(doc(db, \"sites\", siteId, \"stats\", maandId))\n .then((snap) => setMaand(snap.exists() ? (snap.data() as MaandStat) : null))\n .catch(() => setMaand(null));\n getDoc(doc(db, \"sites\", siteId, \"stats\", vorigeId))\n .then((snap) => setVorigeMaand(snap.exists() ? (snap.data() as MaandStat) : null))\n .catch(() => setVorigeMaand(null));\n\n // Laatste bestellingen — alleen als de shop-module actief is.\n if (heeftShop) {\n getDocs(query(collection(db, \"sites\", siteId, \"orders\"), orderBy(\"createdAt\", \"desc\"), limit(5)))\n .then((snap) => setOrders(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<RecenteOrder, \"id\">) }))))\n .catch(() => setOrders([]));\n } else {\n setOrders([]);\n }\n\n // Formulieren: naam + aantal inzendingen + laatste inzending (aggregatie-count, geen\n // volledige submissions-download). Max vier formulieren — de widget is een signaal, geen lijst.\n getDocs(collection(db, \"sites\", siteId, \"forms\"))\n .then(async (snap) => {\n const forms = snap.docs.slice(0, 4);\n const uit = await Promise.all(\n forms.map(async (f) => {\n const subs = collection(db, \"sites\", siteId, \"forms\", f.id, \"submissions\");\n const [count, laatste] = await Promise.all([\n getCountFromServer(subs).then((c) => c.data().count).catch(() => 0),\n getDocs(query(subs, orderBy(\"createdAt\", \"desc\"), limit(1)))\n .then((s) => (s.docs[0]?.data() as { createdAt?: Timestamp } | undefined)?.createdAt)\n .catch(() => undefined),\n ]);\n return { id: f.id, name: (f.data() as { name?: string }).name ?? f.id, aantal: count, laatste };\n }),\n );\n setFormulieren(uit);\n })\n .catch(() => setFormulieren([]));\n\n // Snelkoppelingen uit settings/site (editor mag lezen; instellen is siteadmin-werk).\n getDoc(doc(db, \"sites\", siteId, \"settings\", \"site\"))\n .then((snap) => {\n const d = snap.exists() ? (snap.data() as { dashboardLinks?: Snelkoppeling[] }) : {};\n setLinks(Array.isArray(d.dashboardLinks) ? d.dashboardLinks : []);\n })\n .catch(() => setLinks([]));\n }, [siteId, heeftShop]);\n\n const vandaagDag = new Date().getUTCDate();\n const dagen = useMemo(() => dagReeks(maand ? { id: \"\", ...maand } : undefined, dagenInMaand(huidigMaandId())), [maand]);\n const delta = useMemo(\n () =>\n maand && vorigeMaand\n ? deltaTovVorige({ id: \"\", ...maand }, { id: \"\", ...vorigeMaand }, vandaagDag)\n : null,\n [maand, vorigeMaand, vandaagDag],\n );\n const paden = useMemo(() => topPaden(maand ? { id: \"\", ...maand } : undefined, 3), [maand]);\n\n async function slaLinksOp() {\n if (!linksBewerken) return;\n const schoon = linksBewerken.filter((l) => l.label.trim() && l.href.trim());\n const app = getFirebaseApp();\n if (!app) return;\n setLinksOpslaan(true);\n try {\n // Zelfde route als de rest van de instellingen: de saveSiteSettings-callable (merge).\n const fn = callable(app, \"saveSiteSettings\");\n await fn({ siteId, settings: { dashboardLinks: schoon } });\n setLinks(schoon);\n setLinksBewerken(null);\n toast({ title: \"Snelkoppelingen opgeslagen\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setLinksOpslaan(false);\n }\n }\n\n return (\n <div className=\"grid gap-4\">\n {/* ── Zo werkt het ── */}\n <Card>\n <h2 className=\"text-sm font-semibold text-slate-800\">Zo werkt je site</h2>\n <div className=\"mt-2 grid gap-2 text-sm text-slate-600 md:grid-cols-3\">\n <p>\n <strong className=\"text-slate-800\">1 · Bewerk op de site zelf.</strong> Klik rechtsboven op{\" \"}\n <em>Bekijk site</em> en kies <em>Bewerken</em> in de zwarte balk. Elk blok krijgt een{\" \"}\n potlood (tekst) en een tandwiel (instellingen) — je typt gewoon in de pagina.\n </p>\n <p>\n <strong className=\"text-slate-800\">2 · Publiceer als het goed is.</strong> Alles wat je\n wijzigt is eerst een <em>werkversie</em>: bezoekers zien er niets van. Pas de knop{\" \"}\n <em>Publiceren</em> (rechtsonder) zet het live — en <em>Verwerpen</em> gooit het weg.\n </p>\n <p>\n <strong className=\"text-slate-800\">3 · De rest staat hier.</strong> Nieuwe pagina&apos;s, het\n menu, foto&apos;s, formulieren{heeftShop ? \", de webwinkel\" : \"\"} en statistieken vind je in\n de secties hiernaast. Onderaan de zijbalk staat ook een korte helptekst.\n </p>\n </div>\n </Card>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n {/* ── Statistieken ── */}\n <Card>\n <WidgetKop titel=\"Bezoek deze maand\" tab=\"stats\" tabLabel=\"Statistieken\" />\n {maand === undefined ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : maand === null ? (\n <p className=\"text-sm text-slate-500\">Nog geen bezoekersdata deze maand.</p>\n ) : (\n <div className=\"grid gap-4\">\n {/* Hero-cijfer met context: het totaal leidt, vandaag en de delta lopen mee. */}\n <div className=\"flex flex-wrap items-baseline gap-x-5 gap-y-1\">\n <p>\n <span className=\"text-3xl font-semibold text-slate-900\">{(maand.total ?? 0).toLocaleString(\"nl-NL\")}</span>{\" \"}\n <span className=\"text-xs text-slate-500\">weergaven in {maandLabel(huidigMaandId())}</span>\n </p>\n <p>\n <span className=\"text-lg font-semibold text-slate-700\">{(dagen[vandaagDag - 1] ?? 0).toLocaleString(\"nl-NL\")}</span>{\" \"}\n <span className=\"text-xs text-slate-500\">vandaag</span>\n </p>\n {delta != null && Number.isFinite(delta) && (\n <p className={`text-xs font-medium ${delta >= 0 ? \"text-emerald-700\" : \"text-red-700\"}`}>\n <span aria-hidden=\"true\">{delta >= 0 ? \"▲\" : \"▼\"}</span> {Math.abs(Math.round(delta))}%\n <span className=\"font-normal text-slate-500\"> t.o.v. vorige maand</span>\n </p>\n )}\n </div>\n <DagStaven\n waarden={dagen}\n accentIndex={vandaagDag - 1}\n hoogte={110}\n titel={`Weergaven per dag in ${maandLabel(huidigMaandId(), true)}`}\n />\n {paden.length > 0 && <BalkjesLijst items={paden} />}\n </div>\n )}\n </Card>\n\n {/* ── Laatst bewerkte pagina's ── */}\n <Card>\n <WidgetKop titel=\"Laatst bewerkt\" tab=\"pages\" tabLabel=\"Pagina's\" />\n {paginas === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : paginas.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen bewerkte pagina&apos;s.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {paginas.map((p) => (\n <li key={p.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"pages\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n title={`${p.title ?? p.id} — ${p.path ?? \"\"}`}\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate text-slate-800\">{p.title ?? p.id}</span>\n <span className=\"block truncate text-xs text-slate-400\">{p.path}</span>\n </span>\n <span className=\"shrink-0 text-xs text-slate-400\">{fmtDatum(p.updatedAt)}</span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n\n {/* ── Bestellingen (alleen met shop) ── */}\n {heeftShop && (\n <Card>\n <WidgetKop titel=\"Laatste bestellingen\" tab=\"shop\" tabLabel=\"Webwinkel\" />\n {orders === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : orders.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen bestellingen.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {orders.map((o) => (\n <li key={o.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"shop\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate text-slate-800\">{o.customer?.name ?? \"Onbekend\"}</span>\n <span className=\"block text-xs text-slate-400\">{fmtDatum(o.createdAt)}</span>\n </span>\n <span className=\"flex shrink-0 items-center gap-2\">\n {typeof o.amount?.totalCents === \"number\" && (\n <span className=\"text-sm font-medium text-slate-700\">{formatCents(o.amount.totalCents)}</span>\n )}\n <Badge tone={o.status === \"paid\" || o.status === \"shipped\" ? \"green\" : \"amber\"}>\n {o.status ?? \"?\"}\n </Badge>\n </span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n )}\n\n {/* ── Formulieren ── */}\n <Card>\n <WidgetKop titel=\"Formulieren\" tab=\"forms\" tabLabel=\"Formulieren\" />\n {formulieren === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : formulieren.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen formulieren.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {formulieren.map((f) => (\n <li key={f.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"forms\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n >\n <span className=\"truncate text-slate-800\" title={f.name}>{f.name}</span>\n <span className=\"shrink-0 text-xs text-slate-500\">\n {f.aantal} {f.aantal === 1 ? \"inzending\" : \"inzendingen\"}\n {f.laatste ? ` · laatste ${fmtDatum(f.laatste)}` : \"\"}\n </span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n\n {/* ── Snelkoppelingen ── */}\n <Card>\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"text-sm font-semibold text-slate-800\">Snelkoppelingen</h2>\n {magInstellen && linksBewerken === null && (\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setLinksBewerken(links.length ? [...links] : [{ label: \"\", href: \"\" }])}>\n Instellen\n </Button>\n )}\n </div>\n {linksBewerken ? (\n <div className=\"grid gap-2\">\n <div className=\"grid grid-cols-[1fr_1.4fr_auto] gap-2 text-xs font-medium text-slate-500\">\n <span>Naam</span>\n <span>Adres</span>\n <span aria-hidden=\"true\" />\n </div>\n {linksBewerken.map((l, i) => (\n <div key={i} className=\"grid grid-cols-[1fr_1.4fr_auto] gap-2\">\n <Input value={l.label} placeholder=\"bv. Webwinkel\" aria-label=\"Naam van de snelkoppeling\"\n onChange={(e) => setLinksBewerken(linksBewerken.map((x, j) => (j === i ? { ...x, label: e.target.value } : x)))} />\n <Input value={l.href} placeholder=\"/webwinkel of https://…\" aria-label=\"Adres van de snelkoppeling\"\n onChange={(e) => setLinksBewerken(linksBewerken.map((x, j) => (j === i ? { ...x, href: e.target.value } : x)))} />\n <Button variant=\"ghost\" size=\"sm\" aria-label=\"Verwijderen\"\n onClick={() => setLinksBewerken(linksBewerken.filter((_, j) => j !== i))}>✕</Button>\n </div>\n ))}\n <div className=\"flex items-center justify-between gap-2\">\n <Button variant=\"secondary\" size=\"sm\" disabled={linksBewerken.length >= 12}\n onClick={() => setLinksBewerken([...linksBewerken, { label: \"\", href: \"\" }])}>\n + Snelkoppeling\n </Button>\n <div className=\"flex gap-2\">\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setLinksBewerken(null)}>Annuleren</Button>\n <Button size=\"sm\" disabled={linksOpslaan} onClick={() => void slaLinksOp()}>\n {linksOpslaan ? \"Bezig…\" : \"Opslaan\"}\n </Button>\n </div>\n </div>\n </div>\n ) : links.length === 0 ? (\n <p className=\"text-sm text-slate-500\">\n {magInstellen\n ? \"Nog geen snelkoppelingen. Zet hier de plekken waar je vaak moet zijn — je webwinkel, een pagina, een extern systeem.\"\n : \"Nog geen snelkoppelingen ingesteld.\"}\n </p>\n ) : (\n <ul className=\"flex flex-wrap gap-2\">\n {links.map((l, i) => (\n <li key={i}>\n <a\n href={l.href}\n target={/^https?:\\/\\//i.test(l.href) ? \"_blank\" : undefined}\n rel=\"noreferrer\"\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-slate-200 px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-100\"\n >\n {l.label} <span aria-hidden=\"true\" className=\"text-slate-400\">↗</span>\n </a>\n </li>\n ))}\n </ul>\n )}\n </Card>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/dashboard/DashboardPanel.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Startpagina van /beheer (7.16).\n *\n * /beheer opende recht in het pagina-overzicht: een lijst technische titels als eerste indruk, en\n * nergens een plek die uitlegt hoe het systeem in elkaar zit. Deze startpagina doet drie dingen:\n * uitleggen hoe je de site bewerkt (op de site zelf, niet hier), in één oogopslag tonen wat er\n * gebeurt (bezoek, bestellingen, inzendingen, laatst bewerkt), en snelkoppelingen bieden die de\n * siteadmin zelf kan instellen.\n *\n * Bewust eenmalige reads (geen live listeners): dit is een overzicht bij binnenkomst, geen\n * monitor. Elke widget vangt zijn eigen fouten — een dichtgezette collectie (rules, module uit)\n * mag nooit de hele startpagina blokkeren.\n */\nimport { useEffect, useMemo, useState } from \"react\";\nimport {\n collection,\n doc,\n getCountFromServer,\n getDoc,\n getDocs,\n limit,\n orderBy,\n query,\n where,\n} from \"firebase/firestore\";\nimport type { Timestamp } from \"firebase/firestore\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { callable } from \"../../firebase/functionsClient\";\nimport { useAuth } from \"../../auth/AuthProvider\";\nimport { canManageSite } from \"../../auth/roles\";\nimport { useModules } from \"../../modules/context\";\nimport { formatCents } from \"../../schema/shop\";\nimport { Badge, Button, Card, Input } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { useAdminNav } from \"../navContext\";\nimport { BalkjesLijst, DagStaven } from \"../ui/charts\";\nimport { dagenInMaand, dagReeks, deltaTovVorige, huidigMaandId, maandLabel, topPaden, type MaandStat as StatsMaand } from \"../stats/lib\";\nimport { paginaToestand } from \"../lib/pagesClient\";\nimport { formulierenGezienSinds } from \"../forms/gezien\";\n\ninterface RecentePagina {\n id: string;\n title?: string;\n path?: string;\n /** Timestamp uit de editor, ISO-string uit import-scripts — zie updatedMs(). */\n updatedAt?: unknown;\n}\ntype MaandStat = Omit<StatsMaand, \"id\">;\ninterface RecenteOrder {\n id: string;\n status?: string;\n createdAt?: Timestamp;\n customer?: { name?: string };\n amount?: { totalCents?: number };\n}\ninterface FormOverzicht {\n id: string;\n name: string;\n aantal: number;\n laatste?: Timestamp;\n /** 7.24: inzendingen sinds je het formulier voor het laatst opende. */\n nieuw: number;\n}\ninterface Snelkoppeling {\n label: string;\n href: string;\n}\n\n/**\n * `updatedAt` is in de praktijk een Firestore-Timestamp (editor-saves) ÓF een ISO-string\n * (import-/migratiescripts schrijven via REST) — live aangetroffen op christelijkkraamcadeau,\n * dóór elkaar in dezelfde collectie. Firestore sorteert die in aparte typegroepen, dus een\n * server-side orderBy geeft dan een fout beeld; we normaliseren en sorteren daarom client-side.\n */\nfunction updatedMs(v: unknown): number {\n if (v && typeof (v as Timestamp).toDate === \"function\") return (v as Timestamp).toDate().getTime();\n if (typeof v === \"string\") {\n const t = Date.parse(v);\n return Number.isNaN(t) ? 0 : t;\n }\n return 0;\n}\n\nfunction fmtDatum(v: unknown): string {\n const ms = updatedMs(v);\n if (!ms) return \"—\";\n return new Date(ms).toLocaleString(\"nl-NL\", { dateStyle: \"short\", timeStyle: \"short\" });\n}\n\n/** Kop van een widget met een \"ga ernaartoe\"-pijl die naar het bijbehorende paneel springt. */\nfunction WidgetKop({ titel, tab, tabLabel }: { titel: string; tab?: string; tabLabel?: string }) {\n const gaNaar = useAdminNav();\n return (\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"text-sm font-semibold text-slate-800\">{titel}</h2>\n {tab && (\n <button\n type=\"button\"\n onClick={() => gaNaar(tab)}\n className=\"shrink-0 text-xs font-medium text-brand-700 hover:underline focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none\"\n >\n {tabLabel ?? \"Bekijk alles\"} →\n </button>\n )}\n </div>\n );\n}\n\nexport function DashboardPanel({ siteId }: { siteId: string }) {\n const { role } = useAuth();\n const { activeIds } = useModules();\n const { toast } = useToast();\n const gaNaar = useAdminNav();\n const heeftShop = activeIds.has(\"shop\");\n const magInstellen = canManageSite(role);\n\n const [paginas, setPaginas] = useState<RecentePagina[] | null>(null);\n /** 7.24: pagina's met wijzigingen die nog niet live staan — de \"wacht op jou\"-teller. */\n const [nietLive, setNietLive] = useState<RecentePagina[]>([]);\n const [teVerzenden, setTeVerzenden] = useState<number>(0);\n const [maand, setMaand] = useState<MaandStat | null | undefined>(undefined);\n const [vorigeMaand, setVorigeMaand] = useState<MaandStat | null>(null);\n const [orders, setOrders] = useState<RecenteOrder[] | null>(null);\n const [formulieren, setFormulieren] = useState<FormOverzicht[] | null>(null);\n const [links, setLinks] = useState<Snelkoppeling[]>([]);\n const [linksBewerken, setLinksBewerken] = useState<Snelkoppeling[] | null>(null);\n const [linksOpslaan, setLinksOpslaan] = useState(false);\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setPaginas([]);\n setMaand(null);\n setOrders([]);\n setFormulieren([]);\n return;\n }\n\n // Laatst bewerkte pagina's. Bewust GEEN server-side orderBy op updatedAt: dat veld is een\n // mengsel van Timestamps en ISO-strings (zie updatedMs) en Firestore sorteert per typegroep.\n // Drafts-collecties zijn klein; alles ophalen en zelf sorteren is hier de betrouwbare weg.\n getDocs(collection(db, \"sites\", siteId, \"drafts\"))\n .then((snap) => {\n const alle = snap.docs\n .map((d) => ({ id: d.id, ...(d.data() as Omit<RecentePagina, \"id\"> & { published?: boolean; lastPublishedAt?: unknown; trashed?: boolean; blocks?: unknown[] }) }))\n .filter((p) => !p.trashed)\n .sort((a, b) => updatedMs(b.updatedAt) - updatedMs(a.updatedAt));\n setPaginas(alle.slice(0, 5));\n // Zelfde regel als de badge in Pagina's: gepubliceerd, en daarna opnieuw opgeslagen.\n setNietLive(alle.filter((p) => paginaToestand({ ...p, blocks: [] } as never) === \"gewijzigd\"));\n })\n .catch(() => setPaginas([]));\n\n // Bezoek deze maand + vorige maand (voor de eerlijke delta) — zelfde bron als Statistieken.\n const maandId = huidigMaandId();\n const [j, m] = maandId.split(\"-\").map(Number);\n const vd = new Date(Date.UTC(j, (m ?? 1) - 2, 1));\n const vorigeId = `${vd.getUTCFullYear()}-${String(vd.getUTCMonth() + 1).padStart(2, \"0\")}`;\n getDoc(doc(db, \"sites\", siteId, \"stats\", maandId))\n .then((snap) => setMaand(snap.exists() ? (snap.data() as MaandStat) : null))\n .catch(() => setMaand(null));\n getDoc(doc(db, \"sites\", siteId, \"stats\", vorigeId))\n .then((snap) => setVorigeMaand(snap.exists() ? (snap.data() as MaandStat) : null))\n .catch(() => setVorigeMaand(null));\n\n // Laatste bestellingen — alleen als de shop-module actief is.\n if (heeftShop) {\n getDocs(query(collection(db, \"sites\", siteId, \"orders\"), orderBy(\"createdAt\", \"desc\"), limit(5)))\n .then((snap) => setOrders(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<RecenteOrder, \"id\">) }))))\n .catch(() => setOrders([]));\n // Betaald maar nog niet verzonden: dat is werk dat op de winkelier wacht.\n getCountFromServer(query(collection(db, \"sites\", siteId, \"orders\"), where(\"status\", \"==\", \"paid\")))\n .then((c) => setTeVerzenden(c.data().count))\n .catch(() => setTeVerzenden(0));\n } else {\n setOrders([]);\n }\n\n // Formulieren: naam + aantal inzendingen + laatste inzending (aggregatie-count, geen\n // volledige submissions-download). Max vier formulieren — de widget is een signaal, geen lijst.\n getDocs(collection(db, \"sites\", siteId, \"forms\"))\n .then(async (snap) => {\n const forms = snap.docs.slice(0, 4);\n const uit = await Promise.all(\n forms.map(async (f) => {\n const subs = collection(db, \"sites\", siteId, \"forms\", f.id, \"submissions\");\n const sinds = formulierenGezienSinds(siteId, f.id);\n const [count, laatste, nieuw] = await Promise.all([\n getCountFromServer(subs).then((c) => c.data().count).catch(() => 0),\n getDocs(query(subs, orderBy(\"createdAt\", \"desc\"), limit(1)))\n .then((s) => (s.docs[0]?.data() as { createdAt?: Timestamp } | undefined)?.createdAt)\n .catch(() => undefined),\n // Nieuw sinds je laatste bezoek aan dit formulier (moment staat lokaal, zie forms/gezien.ts).\n sinds\n ? getCountFromServer(query(subs, where(\"createdAt\", \">\", sinds))).then((c) => c.data().count).catch(() => 0)\n : Promise.resolve(0),\n ]);\n return { id: f.id, name: (f.data() as { name?: string }).name ?? f.id, aantal: count, laatste, nieuw };\n }),\n );\n setFormulieren(uit);\n })\n .catch(() => setFormulieren([]));\n\n // Snelkoppelingen uit settings/site (editor mag lezen; instellen is siteadmin-werk).\n getDoc(doc(db, \"sites\", siteId, \"settings\", \"site\"))\n .then((snap) => {\n const d = snap.exists() ? (snap.data() as { dashboardLinks?: Snelkoppeling[] }) : {};\n setLinks(Array.isArray(d.dashboardLinks) ? d.dashboardLinks : []);\n })\n .catch(() => setLinks([]));\n }, [siteId, heeftShop]);\n\n const vandaagDag = new Date().getUTCDate();\n const dagen = useMemo(() => dagReeks(maand ? { id: \"\", ...maand } : undefined, dagenInMaand(huidigMaandId())), [maand]);\n const delta = useMemo(\n () =>\n maand && vorigeMaand\n ? deltaTovVorige({ id: \"\", ...maand }, { id: \"\", ...vorigeMaand }, vandaagDag)\n : null,\n [maand, vorigeMaand, vandaagDag],\n );\n const paden = useMemo(() => topPaden(maand ? { id: \"\", ...maand } : undefined, 3), [maand]);\n\n async function slaLinksOp() {\n if (!linksBewerken) return;\n const schoon = linksBewerken.filter((l) => l.label.trim() && l.href.trim());\n const app = getFirebaseApp();\n if (!app) return;\n setLinksOpslaan(true);\n try {\n // Zelfde route als de rest van de instellingen: de saveSiteSettings-callable (merge).\n const fn = callable(app, \"saveSiteSettings\");\n await fn({ siteId, settings: { dashboardLinks: schoon } });\n setLinks(schoon);\n setLinksBewerken(null);\n toast({ title: \"Snelkoppelingen opgeslagen\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setLinksOpslaan(false);\n }\n }\n\n return (\n <div className=\"grid gap-4\">\n {/* ── Zo werkt het ── */}\n <Card>\n <h2 className=\"text-sm font-semibold text-slate-800\">Zo werkt je site</h2>\n <div className=\"mt-2 grid gap-2 text-sm text-slate-600 md:grid-cols-3\">\n <p>\n <strong className=\"text-slate-800\">1 · Bewerk op de site zelf.</strong> Klik rechtsboven op{\" \"}\n <em>Bekijk site</em> en kies <em>Bewerken</em> in de zwarte balk. Elk blok krijgt een{\" \"}\n potlood (tekst) en een tandwiel (instellingen) — je typt gewoon in de pagina.\n </p>\n <p>\n <strong className=\"text-slate-800\">2 · Publiceer als het goed is.</strong> Alles wat je\n wijzigt is eerst een <em>werkversie</em>: bezoekers zien er niets van. Pas de knop{\" \"}\n <em>Publiceren</em> (rechtsonder) zet het live — en <em>Verwerpen</em> gooit het weg.\n </p>\n <p>\n <strong className=\"text-slate-800\">3 · De rest staat hier.</strong> Nieuwe pagina&apos;s, het\n menu, foto&apos;s, formulieren{heeftShop ? \", de webwinkel\" : \"\"} en statistieken vind je in\n de secties hiernaast. Onderaan de zijbalk staat ook een korte helptekst.\n </p>\n </div>\n </Card>\n\n {/* ── Wacht op jou (7.24) ──\n Eén tegel met wat er van de beheerder verwacht wordt: wijzigingen die nog niet live staan,\n nieuwe inzendingen, bestellingen die de deur uit moeten. Dát is waarvoor iemand inlogt;\n de overzichten hieronder zijn context. Verschijnt alleen als er iets ligt. */}\n {(() => {\n const nieuweInzendingen = (formulieren ?? []).reduce((som, f) => som + f.nieuw, 0);\n const items: { tekst: string; tab: string; tabLabel: string }[] = [];\n if (nietLive.length > 0) {\n items.push({\n tekst: nietLive.length === 1\n ? `1 pagina heeft wijzigingen die nog niet live staan (${nietLive[0].title ?? nietLive[0].path})`\n : `${nietLive.length} pagina's hebben wijzigingen die nog niet live staan`,\n tab: \"pages\",\n tabLabel: \"Naar Pagina's\",\n });\n }\n if (nieuweInzendingen > 0) {\n items.push({ tekst: nieuweInzendingen === 1 ? \"1 nieuwe inzending\" : `${nieuweInzendingen} nieuwe inzendingen`, tab: \"forms\", tabLabel: \"Naar Formulieren\" });\n }\n if (heeftShop && teVerzenden > 0) {\n items.push({ tekst: teVerzenden === 1 ? \"1 betaalde bestelling wacht op verzending\" : `${teVerzenden} betaalde bestellingen wachten op verzending`, tab: \"shop\", tabLabel: \"Naar Webwinkel\" });\n }\n if (items.length === 0) return null;\n return (\n <Card>\n <h2 className=\"mb-2 text-sm font-semibold text-slate-800\">Wacht op jou</h2>\n <ul className=\"grid gap-1.5\">\n {items.map((it) => (\n <li key={it.tab} className=\"flex flex-wrap items-center justify-between gap-2 rounded-lg bg-amber-50 px-3 py-2 text-sm text-amber-900\">\n <span>{it.tekst}</span>\n <button type=\"button\" onClick={() => gaNaar(it.tab)} className=\"text-xs font-medium text-brand-700 hover:underline\">\n {it.tabLabel} →\n </button>\n </li>\n ))}\n </ul>\n </Card>\n );\n })()}\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n {/* ── Statistieken ── */}\n <Card>\n <WidgetKop titel=\"Bezoek deze maand\" tab=\"stats\" tabLabel=\"Statistieken\" />\n {maand === undefined ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : maand === null ? (\n <p className=\"text-sm text-slate-500\">Nog geen bezoekersdata deze maand.</p>\n ) : (\n <div className=\"grid gap-4\">\n {/* Hero-cijfer met context: het totaal leidt, vandaag en de delta lopen mee. */}\n <div className=\"flex flex-wrap items-baseline gap-x-5 gap-y-1\">\n <p>\n <span className=\"text-3xl font-semibold text-slate-900\">{(maand.total ?? 0).toLocaleString(\"nl-NL\")}</span>{\" \"}\n <span className=\"text-xs text-slate-500\">weergaven in {maandLabel(huidigMaandId())}</span>\n </p>\n <p>\n <span className=\"text-lg font-semibold text-slate-700\">{(dagen[vandaagDag - 1] ?? 0).toLocaleString(\"nl-NL\")}</span>{\" \"}\n <span className=\"text-xs text-slate-500\">vandaag</span>\n </p>\n {delta != null && Number.isFinite(delta) && (\n <p className={`text-xs font-medium ${delta >= 0 ? \"text-emerald-700\" : \"text-red-700\"}`}>\n <span aria-hidden=\"true\">{delta >= 0 ? \"▲\" : \"▼\"}</span> {Math.abs(Math.round(delta))}%\n <span className=\"font-normal text-slate-500\"> t.o.v. vorige maand</span>\n </p>\n )}\n </div>\n <DagStaven\n waarden={dagen}\n accentIndex={vandaagDag - 1}\n hoogte={110}\n titel={`Weergaven per dag in ${maandLabel(huidigMaandId(), true)}`}\n />\n {paden.length > 0 && <BalkjesLijst items={paden} />}\n </div>\n )}\n </Card>\n\n {/* ── Laatst bewerkte pagina's ── */}\n <Card>\n <WidgetKop titel=\"Laatst bewerkt\" tab=\"pages\" tabLabel=\"Pagina's\" />\n {paginas === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : paginas.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen bewerkte pagina&apos;s.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {paginas.map((p) => (\n <li key={p.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"pages\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n title={`${p.title ?? p.id} — ${p.path ?? \"\"}`}\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate text-slate-800\">{p.title ?? p.id}</span>\n <span className=\"block truncate text-xs text-slate-400\">{p.path}</span>\n </span>\n <span className=\"shrink-0 text-xs text-slate-400\">{fmtDatum(p.updatedAt)}</span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n\n {/* ── Bestellingen (alleen met shop) ── */}\n {heeftShop && (\n <Card>\n <WidgetKop titel=\"Laatste bestellingen\" tab=\"shop\" tabLabel=\"Webwinkel\" />\n {orders === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : orders.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen bestellingen.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {orders.map((o) => (\n <li key={o.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"shop\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate text-slate-800\">{o.customer?.name ?? \"Onbekend\"}</span>\n <span className=\"block text-xs text-slate-400\">{fmtDatum(o.createdAt)}</span>\n </span>\n <span className=\"flex shrink-0 items-center gap-2\">\n {typeof o.amount?.totalCents === \"number\" && (\n <span className=\"text-sm font-medium text-slate-700\">{formatCents(o.amount.totalCents)}</span>\n )}\n <Badge tone={o.status === \"paid\" || o.status === \"shipped\" ? \"green\" : \"amber\"}>\n {o.status ?? \"?\"}\n </Badge>\n </span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n )}\n\n {/* ── Formulieren ── */}\n <Card>\n <WidgetKop titel=\"Formulieren\" tab=\"forms\" tabLabel=\"Formulieren\" />\n {formulieren === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : formulieren.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen formulieren.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {formulieren.map((f) => (\n <li key={f.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"forms\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n >\n <span className=\"truncate text-slate-800\" title={f.name}>{f.name}</span>\n <span className=\"shrink-0 text-xs text-slate-500\">\n {f.aantal} {f.aantal === 1 ? \"inzending\" : \"inzendingen\"}\n {f.laatste ? ` · laatste ${fmtDatum(f.laatste)}` : \"\"}\n </span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n\n {/* ── Snelkoppelingen ── */}\n <Card>\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"text-sm font-semibold text-slate-800\">Snelkoppelingen</h2>\n {magInstellen && linksBewerken === null && (\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setLinksBewerken(links.length ? [...links] : [{ label: \"\", href: \"\" }])}>\n Instellen\n </Button>\n )}\n </div>\n {linksBewerken ? (\n <div className=\"grid gap-2\">\n <div className=\"grid grid-cols-[1fr_1.4fr_auto] gap-2 text-xs font-medium text-slate-500\">\n <span>Naam</span>\n <span>Adres</span>\n <span aria-hidden=\"true\" />\n </div>\n {linksBewerken.map((l, i) => (\n <div key={i} className=\"grid grid-cols-[1fr_1.4fr_auto] gap-2\">\n <Input value={l.label} placeholder=\"bv. Webwinkel\" aria-label=\"Naam van de snelkoppeling\"\n onChange={(e) => setLinksBewerken(linksBewerken.map((x, j) => (j === i ? { ...x, label: e.target.value } : x)))} />\n <Input value={l.href} placeholder=\"/webwinkel of https://…\" aria-label=\"Adres van de snelkoppeling\"\n onChange={(e) => setLinksBewerken(linksBewerken.map((x, j) => (j === i ? { ...x, href: e.target.value } : x)))} />\n <Button variant=\"ghost\" size=\"sm\" aria-label=\"Verwijderen\"\n onClick={() => setLinksBewerken(linksBewerken.filter((_, j) => j !== i))}>✕</Button>\n </div>\n ))}\n <div className=\"flex items-center justify-between gap-2\">\n <Button variant=\"secondary\" size=\"sm\" disabled={linksBewerken.length >= 12}\n onClick={() => setLinksBewerken([...linksBewerken, { label: \"\", href: \"\" }])}>\n + Snelkoppeling\n </Button>\n <div className=\"flex gap-2\">\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setLinksBewerken(null)}>Annuleren</Button>\n <Button size=\"sm\" disabled={linksOpslaan} onClick={() => void slaLinksOp()}>\n {linksOpslaan ? \"Bezig…\" : \"Opslaan\"}\n </Button>\n </div>\n </div>\n </div>\n ) : links.length === 0 ? (\n <p className=\"text-sm text-slate-500\">\n {magInstellen\n ? \"Nog geen snelkoppelingen. Zet hier de plekken waar je vaak moet zijn — je webwinkel, een pagina, een extern systeem.\"\n : \"Nog geen snelkoppelingen ingesteld.\"}\n </p>\n ) : (\n <ul className=\"flex flex-wrap gap-2\">\n {links.map((l, i) => (\n <li key={i}>\n <a\n href={l.href}\n target={/^https?:\\/\\//i.test(l.href) ? \"_blank\" : undefined}\n rel=\"noreferrer\"\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-slate-200 px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-100\"\n >\n {l.label} <span aria-hidden=\"true\" className=\"text-slate-400\">↗</span>\n </a>\n </li>\n ))}\n </ul>\n )}\n </Card>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,3 +1,12 @@
1
+ import type { Timestamp } from "firebase/firestore";
1
2
  export declare function FormsPanel({ siteId }: {
2
3
  siteId: string;
3
4
  }): import("react").JSX.Element;
5
+ type Inzending = {
6
+ id: string;
7
+ values: Record<string, unknown>;
8
+ createdAt?: Timestamp;
9
+ };
10
+ /** CSV met Excel-vriendelijke opmaak: BOM, puntkomma, alles gequoot. Kolommen = de velden van het formulier. */
11
+ export declare function inzendingenCsv(velden: string[], subs: Inzending[]): string;
12
+ export {};