@flowselections/dashboard-erp-kwekers 1.0.1

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 (150) hide show
  1. package/dist-lib/_core-safelist.d.ts +2 -0
  2. package/dist-lib/_core-safelist.d.ts.map +1 -0
  3. package/dist-lib/_core-safelist.js +15 -0
  4. package/dist-lib/components/DashboardErpLogo.d.ts +6 -0
  5. package/dist-lib/components/DashboardErpLogo.d.ts.map +1 -0
  6. package/dist-lib/components/DashboardErpLogo.js +4 -0
  7. package/dist-lib/components/dashboard/AiSettingsCard.d.ts +2 -0
  8. package/dist-lib/components/dashboard/AiSettingsCard.d.ts.map +1 -0
  9. package/dist-lib/components/dashboard/AiSettingsCard.js +37 -0
  10. package/dist-lib/components/dashboard/AlertHistoryPanel.d.ts +4 -0
  11. package/dist-lib/components/dashboard/AlertHistoryPanel.d.ts.map +1 -0
  12. package/dist-lib/components/dashboard/AlertHistoryPanel.js +18 -0
  13. package/dist-lib/components/dashboard/BlockEditorDialog.d.ts +15 -0
  14. package/dist-lib/components/dashboard/BlockEditorDialog.d.ts.map +1 -0
  15. package/dist-lib/components/dashboard/BlockEditorDialog.js +89 -0
  16. package/dist-lib/components/dashboard/BlockRenderer.d.ts +5 -0
  17. package/dist-lib/components/dashboard/BlockRenderer.d.ts.map +1 -0
  18. package/dist-lib/components/dashboard/BlockRenderer.js +25 -0
  19. package/dist-lib/components/dashboard/DashboardBuilderPage.d.ts +4 -0
  20. package/dist-lib/components/dashboard/DashboardBuilderPage.d.ts.map +1 -0
  21. package/dist-lib/components/dashboard/DashboardBuilderPage.js +49 -0
  22. package/dist-lib/components/dashboard/DashboardErpLayoutPage.d.ts +2 -0
  23. package/dist-lib/components/dashboard/DashboardErpLayoutPage.d.ts.map +1 -0
  24. package/dist-lib/components/dashboard/DashboardErpLayoutPage.js +30 -0
  25. package/dist-lib/components/dashboard/DashboardSummary.d.ts +9 -0
  26. package/dist-lib/components/dashboard/DashboardSummary.d.ts.map +1 -0
  27. package/dist-lib/components/dashboard/DashboardSummary.js +136 -0
  28. package/dist-lib/components/dashboard/DashboardViewPage.d.ts +3 -0
  29. package/dist-lib/components/dashboard/DashboardViewPage.d.ts.map +1 -0
  30. package/dist-lib/components/dashboard/DashboardViewPage.js +37 -0
  31. package/dist-lib/components/dashboard/FilterBar.d.ts +17 -0
  32. package/dist-lib/components/dashboard/FilterBar.d.ts.map +1 -0
  33. package/dist-lib/components/dashboard/FilterBar.js +66 -0
  34. package/dist-lib/components/dashboard/QuickAddDialog.d.ts +15 -0
  35. package/dist-lib/components/dashboard/QuickAddDialog.d.ts.map +1 -0
  36. package/dist-lib/components/dashboard/QuickAddDialog.js +168 -0
  37. package/dist-lib/components/dashboard/SuggestionBanner.d.ts +15 -0
  38. package/dist-lib/components/dashboard/SuggestionBanner.d.ts.map +1 -0
  39. package/dist-lib/components/dashboard/SuggestionBanner.js +13 -0
  40. package/dist-lib/components/dashboard/TileGallery.d.ts +17 -0
  41. package/dist-lib/components/dashboard/TileGallery.d.ts.map +1 -0
  42. package/dist-lib/components/dashboard/TileGallery.js +60 -0
  43. package/dist-lib/components/dashboard/ai/AiChatPanel.d.ts +6 -0
  44. package/dist-lib/components/dashboard/ai/AiChatPanel.d.ts.map +1 -0
  45. package/dist-lib/components/dashboard/ai/AiChatPanel.js +57 -0
  46. package/dist-lib/components/dashboard/ai/BlockGeneratorDialog.d.ts +14 -0
  47. package/dist-lib/components/dashboard/ai/BlockGeneratorDialog.d.ts.map +1 -0
  48. package/dist-lib/components/dashboard/ai/BlockGeneratorDialog.js +61 -0
  49. package/dist-lib/components/dashboard/blocks/AiAnalysisBlock.d.ts +5 -0
  50. package/dist-lib/components/dashboard/blocks/AiAnalysisBlock.d.ts.map +1 -0
  51. package/dist-lib/components/dashboard/blocks/AiAnalysisBlock.js +58 -0
  52. package/dist-lib/components/dashboard/blocks/AlertBlock.d.ts +5 -0
  53. package/dist-lib/components/dashboard/blocks/AlertBlock.d.ts.map +1 -0
  54. package/dist-lib/components/dashboard/blocks/AlertBlock.js +107 -0
  55. package/dist-lib/components/dashboard/blocks/BlockShell.d.ts +10 -0
  56. package/dist-lib/components/dashboard/blocks/BlockShell.d.ts.map +1 -0
  57. package/dist-lib/components/dashboard/blocks/BlockShell.js +39 -0
  58. package/dist-lib/components/dashboard/blocks/ChartBlock.d.ts +5 -0
  59. package/dist-lib/components/dashboard/blocks/ChartBlock.d.ts.map +1 -0
  60. package/dist-lib/components/dashboard/blocks/ChartBlock.js +31 -0
  61. package/dist-lib/components/dashboard/blocks/ForecastBlock.d.ts +5 -0
  62. package/dist-lib/components/dashboard/blocks/ForecastBlock.d.ts.map +1 -0
  63. package/dist-lib/components/dashboard/blocks/ForecastBlock.js +86 -0
  64. package/dist-lib/components/dashboard/blocks/KpiBlock.d.ts +5 -0
  65. package/dist-lib/components/dashboard/blocks/KpiBlock.d.ts.map +1 -0
  66. package/dist-lib/components/dashboard/blocks/KpiBlock.js +17 -0
  67. package/dist-lib/components/dashboard/blocks/TableBlock.d.ts +5 -0
  68. package/dist-lib/components/dashboard/blocks/TableBlock.d.ts.map +1 -0
  69. package/dist-lib/components/dashboard/blocks/TableBlock.js +12 -0
  70. package/dist-lib/components/dashboard/types.d.ts +129 -0
  71. package/dist-lib/components/dashboard/types.d.ts.map +1 -0
  72. package/dist-lib/components/dashboard/types.js +1 -0
  73. package/dist-lib/hooks/useAlertEvents.d.ts +9 -0
  74. package/dist-lib/hooks/useAlertEvents.d.ts.map +1 -0
  75. package/dist-lib/hooks/useAlertEvents.js +32 -0
  76. package/dist-lib/hooks/useBlockData.d.ts +10 -0
  77. package/dist-lib/hooks/useBlockData.d.ts.map +1 -0
  78. package/dist-lib/hooks/useBlockData.js +130 -0
  79. package/dist-lib/hooks/useDashboardBlocks.d.ts +25 -0
  80. package/dist-lib/hooks/useDashboardBlocks.d.ts.map +1 -0
  81. package/dist-lib/hooks/useDashboardBlocks.js +66 -0
  82. package/dist-lib/hooks/useDashboardPages.d.ts +10 -0
  83. package/dist-lib/hooks/useDashboardPages.d.ts.map +1 -0
  84. package/dist-lib/hooks/useDashboardPages.js +56 -0
  85. package/dist-lib/hooks/useSchema.d.ts +13 -0
  86. package/dist-lib/hooks/useSchema.d.ts.map +1 -0
  87. package/dist-lib/hooks/useSchema.js +23 -0
  88. package/dist-lib/hooks/useUserRoles.d.ts +5 -0
  89. package/dist-lib/hooks/useUserRoles.d.ts.map +1 -0
  90. package/dist-lib/hooks/useUserRoles.js +23 -0
  91. package/dist-lib/index.d.ts +17 -0
  92. package/dist-lib/index.d.ts.map +1 -0
  93. package/dist-lib/index.js +47 -0
  94. package/dist-lib/integrations/supabase/auth-attacher.d.ts +2 -0
  95. package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -0
  96. package/dist-lib/integrations/supabase/auth-attacher.js +12 -0
  97. package/dist-lib/integrations/supabase/auth-middleware.d.ts +2555 -0
  98. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -0
  99. package/dist-lib/integrations/supabase/auth-middleware.js +52 -0
  100. package/dist-lib/integrations/supabase/client.d.ts +2551 -0
  101. package/dist-lib/integrations/supabase/client.d.ts.map +1 -0
  102. package/dist-lib/integrations/supabase/client.js +13 -0
  103. package/dist-lib/integrations/supabase/client.server.d.ts +2551 -0
  104. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -0
  105. package/dist-lib/integrations/supabase/client.server.js +30 -0
  106. package/dist-lib/integrations/supabase/types.d.ts +2685 -0
  107. package/dist-lib/integrations/supabase/types.d.ts.map +1 -0
  108. package/dist-lib/integrations/supabase/types.js +8 -0
  109. package/dist-lib/lib/ai-gateway.server.d.ts +2 -0
  110. package/dist-lib/lib/ai-gateway.server.d.ts.map +1 -0
  111. package/dist-lib/lib/ai-gateway.server.js +11 -0
  112. package/dist-lib/lib/ai-insights.functions.d.ts +10271 -0
  113. package/dist-lib/lib/ai-insights.functions.d.ts.map +1 -0
  114. package/dist-lib/lib/ai-insights.functions.js +118 -0
  115. package/dist-lib/lib/aiSettings.d.ts +22 -0
  116. package/dist-lib/lib/aiSettings.d.ts.map +1 -0
  117. package/dist-lib/lib/aiSettings.js +107 -0
  118. package/dist-lib/lib/dashboard-eval.functions.d.ts +5130 -0
  119. package/dist-lib/lib/dashboard-eval.functions.d.ts.map +1 -0
  120. package/dist-lib/lib/dashboard-eval.functions.js +87 -0
  121. package/dist-lib/lib/dashboard.functions.d.ts +2562 -0
  122. package/dist-lib/lib/dashboard.functions.d.ts.map +1 -0
  123. package/dist-lib/lib/dashboard.functions.js +21 -0
  124. package/dist-lib/lib/dashboardTemplates.d.ts +11 -0
  125. package/dist-lib/lib/dashboardTemplates.d.ts.map +1 -0
  126. package/dist-lib/lib/dashboardTemplates.js +44 -0
  127. package/dist-lib/lib/format.d.ts +20 -0
  128. package/dist-lib/lib/format.d.ts.map +1 -0
  129. package/dist-lib/lib/format.js +121 -0
  130. package/dist-lib/lib/query.functions.d.ts +2583 -0
  131. package/dist-lib/lib/query.functions.d.ts.map +1 -0
  132. package/dist-lib/lib/query.functions.js +85 -0
  133. package/dist-lib/lib/quickBlocks.d.ts +28 -0
  134. package/dist-lib/lib/quickBlocks.d.ts.map +1 -0
  135. package/dist-lib/lib/quickBlocks.js +297 -0
  136. package/dist-lib/lib/roles.functions.d.ts +2556 -0
  137. package/dist-lib/lib/roles.functions.d.ts.map +1 -0
  138. package/dist-lib/lib/roles.functions.js +14 -0
  139. package/dist-lib/lib/suggestions.d.ts +8 -0
  140. package/dist-lib/lib/suggestions.d.ts.map +1 -0
  141. package/dist-lib/lib/suggestions.js +41 -0
  142. package/dist-lib/lib/utils.d.ts +3 -0
  143. package/dist-lib/lib/utils.d.ts.map +1 -0
  144. package/dist-lib/lib/utils.js +5 -0
  145. package/dist-lib/lib/validationSchemas.d.ts +15 -0
  146. package/dist-lib/lib/validationSchemas.d.ts.map +1 -0
  147. package/dist-lib/lib/validationSchemas.js +25 -0
  148. package/dist-lib/styles.css +1 -0
  149. package/package.json +95 -0
  150. package/public/flowselections-assets/template-module/README.md +15 -0
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=_core-safelist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_core-safelist.d.ts","sourceRoot":"","sources":["../src/_core-safelist.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,CAAC"}
@@ -0,0 +1,15 @@
1
+ // Forceert Tailwind classes die uit @flowselections/core komen maar niet
2
+ // voorkomen in de eigen broncode van deze module.
3
+ // Zonder dit bestand worden deze classes weggeoptimaliseerd op de
4
+ // Lovable productie build.
5
+ const _safelist = [
6
+ // Layout — Sidebar en shell structuur
7
+ 'h-full', 'flex-col', 'flex-1', 'min-h-screen', 'ml-64', 'pt-16',
8
+ 'items-start', 'items-center', 'justify-between', 'border-t',
9
+ 'w-64', 'h-screen', 'h-20', 'overflow-y-auto',
10
+ // Grid — InstellingenPage
11
+ 'grid', 'lg:grid-cols-2', 'gap-6', 'p-6', 'space-y-6',
12
+ // Toggle/Switch — aan/uit zichtbaarheid
13
+ 'data-[state=checked]:bg-primary', 'data-[state=unchecked]:bg-input',
14
+ ];
15
+ export {};
@@ -0,0 +1,6 @@
1
+ interface Props {
2
+ className?: string;
3
+ }
4
+ export declare function DashboardErpLogo({ className }: Props): import("react").JSX.Element;
5
+ export {};
6
+ //# sourceMappingURL=DashboardErpLogo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DashboardErpLogo.d.ts","sourceRoot":"","sources":["../../src/components/DashboardErpLogo.tsx"],"names":[],"mappings":"AAAA,UAAU,KAAK;IAAG,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE;AAEtC,wBAAgB,gBAAgB,CAAC,EAAE,SAAS,EAAE,EAAE,KAAK,+BASpD"}
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function DashboardErpLogo({ className }) {
3
+ return (_jsxs("svg", { viewBox: "0 0 40 40", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: className, "aria-hidden": "true", children: [_jsx("rect", { width: "40", height: "40", rx: "8", fill: "currentColor", opacity: "0.12" }), _jsx("path", { d: "M10 28V18l5-4 5 4v10", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M22 28V12l4-3 4 3v16", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M6 30h28", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })] }));
4
+ }
@@ -0,0 +1,2 @@
1
+ export declare function AiSettingsCard(): import("react").JSX.Element;
2
+ //# sourceMappingURL=AiSettingsCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AiSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/components/dashboard/AiSettingsCard.tsx"],"names":[],"mappings":"AAuCA,wBAAgB,cAAc,gCAuG7B"}
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Card, CardContent, CardHeader, CardTitle, Switch, Checkbox, Button } from '@flowselections/core';
3
+ import { Sparkles } from 'lucide-react';
4
+ import { useAiSettings, } from '../../lib/aiSettings';
5
+ const TYPE_LABELS = {
6
+ dashboard: { label: 'Dashboard-samenvattingen', example: '"De omzet stijgt met 12% t.o.v. de vorige periode."' },
7
+ voorraad: { label: 'Voorraad-samenvattingen', example: '"5 producten dreigen binnen 7 dagen uitverkocht te raken."' },
8
+ verkoop: { label: 'Verkoop-samenvattingen', example: '"Productgroep Kamerplanten laat de grootste groei zien."' },
9
+ klanten: { label: 'Klant-samenvattingen', example: '"3 klanten hebben deze maand aanzienlijk minder besteld."' },
10
+ forecast: { label: 'Forecast-samenvattingen', example: '"Op basis van de huidige trend wordt 8% omzetgroei verwacht."' },
11
+ risico: { label: 'Risico-meldingen', example: '"Voorraad van productgroep X raakt binnen 14 dagen op."' },
12
+ dagelijks: { label: 'Dagelijkse samenvatting', example: 'Elke dag een korte stand van zaken.' },
13
+ wekelijks: { label: 'Wekelijkse samenvatting', example: 'Elke week een overzicht van trends.' },
14
+ maandelijks: { label: 'Maandelijkse samenvatting', example: 'Elke maand een uitgebreide analyse.' },
15
+ };
16
+ const FOCUS_LABELS = {
17
+ omzet: 'Omzet', voorraad: 'Voorraad', orders: 'Orders', klanten: 'Klanten',
18
+ producten: 'Producten', inkoop: 'Inkoop', leveranciers: 'Leveranciers',
19
+ forecasts: 'Forecasts', risicos: "Risico's", marge: 'Winst & marge',
20
+ };
21
+ const DETAILS = [
22
+ { id: 'kort', label: 'Kort', hint: '1 tot 3 regels' },
23
+ { id: 'normaal', label: 'Normaal', hint: 'Korte managementsamenvatting' },
24
+ { id: 'uitgebreid', label: 'Uitgebreid', hint: 'Volledige analyse met aanbevelingen' },
25
+ ];
26
+ const PERSONALITIES = [
27
+ { id: 'zakelijk', label: 'Zakelijk' },
28
+ { id: 'management', label: 'Management' },
29
+ { id: 'operationeel', label: 'Operationeel' },
30
+ { id: 'kort', label: 'Kort & bondig' },
31
+ { id: 'advies', label: 'Adviesgericht' },
32
+ { id: 'analytisch', label: 'Analytisch' },
33
+ ];
34
+ export function AiSettingsCard() {
35
+ const [s, set] = useAiSettings();
36
+ return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "flex items-center gap-2", children: [_jsx(Sparkles, { className: "w-4 h-4 text-primary" }), " AI-samenvattingen"] }) }), _jsxs(CardContent, { className: "space-y-6", children: [_jsxs("div", { className: "flex items-center justify-between rounded-lg border p-3", children: [_jsxs("div", { children: [_jsx("div", { className: "font-medium text-sm", children: "AI-samenvattingen inschakelen" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Wanneer uit: geen samenvattingen, analyses of automatische inzichten." })] }), _jsx(Switch, { checked: s.enabled, onCheckedChange: (v) => set({ ...s, enabled: !!v }) })] }), _jsxs("div", { className: s.enabled ? '' : 'opacity-50 pointer-events-none', children: [_jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "text-sm font-medium", children: "Soorten samenvattingen" }), _jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-2", children: Object.keys(TYPE_LABELS).map((k) => (_jsxs("label", { className: "flex items-start gap-3 rounded-md border p-3 cursor-pointer", children: [_jsx(Switch, { checked: s.types[k], onCheckedChange: (v) => set({ ...s, types: { ...s.types, [k]: !!v } }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "text-sm font-medium", children: TYPE_LABELS[k].label }), _jsx("div", { className: "text-xs text-muted-foreground italic", children: TYPE_LABELS[k].example })] })] }, k))) })] }), _jsxs("div", { className: "space-y-2 mt-6", children: [_jsx("div", { className: "text-sm font-medium", children: "Detailniveau" }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-3 gap-2", children: DETAILS.map((d) => (_jsxs(Button, { type: "button", variant: s.detail === d.id ? 'default' : 'outline', onClick: () => set({ ...s, detail: d.id }), className: "h-auto py-3 flex flex-col items-start", children: [_jsx("span", { className: "font-medium", children: d.label }), _jsx("span", { className: "text-xs opacity-80", children: d.hint })] }, d.id))) })] }), _jsxs("div", { className: "space-y-2 mt-6", children: [_jsx("div", { className: "text-sm font-medium", children: "Focusgebieden" }), _jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-2", children: Object.keys(FOCUS_LABELS).map((k) => (_jsxs("label", { className: "flex items-center gap-2 rounded-md border p-2 cursor-pointer", children: [_jsx(Checkbox, { checked: s.focus[k], onCheckedChange: (v) => set({ ...s, focus: { ...s.focus, [k]: !!v } }) }), _jsx("span", { className: "text-sm", children: FOCUS_LABELS[k] })] }, k))) })] }), _jsxs("div", { className: "space-y-2 mt-6", children: [_jsx("div", { className: "text-sm font-medium", children: "AI-persoonlijkheid" }), _jsx("div", { className: "flex flex-wrap gap-2", children: PERSONALITIES.map((p) => (_jsx(Button, { type: "button", size: "sm", variant: s.personality === p.id ? 'default' : 'outline', onClick: () => set({ ...s, personality: p.id }), children: p.label }, p.id))) })] }), _jsx("p", { className: "text-xs text-muted-foreground mt-6", children: "Tip: per dashboard kun je de AI-samenvatting tonen of verbergen via het oogje rechtsboven de samenvatting." })] })] })] }));
37
+ }
@@ -0,0 +1,4 @@
1
+ export declare function AlertHistoryPanel({ pageId }: {
2
+ pageId: string;
3
+ }): import("react").JSX.Element | null;
4
+ //# sourceMappingURL=AlertHistoryPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AlertHistoryPanel.d.ts","sourceRoot":"","sources":["../../../src/components/dashboard/AlertHistoryPanel.tsx"],"names":[],"mappings":"AAUA,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,sCAuC/D"}
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Card, CardContent, CardHeader, CardTitle, Button, Badge } from '@flowselections/core';
3
+ import { Bell, Check, Trash2 } from 'lucide-react';
4
+ import { useAlertEvents } from '../../hooks/useAlertEvents';
5
+ const SEV_COLOR = {
6
+ info: 'bg-blue-500/15 text-blue-700 dark:text-blue-300 border-blue-500/30',
7
+ warning: 'bg-amber-500/15 text-amber-700 dark:text-amber-300 border-amber-500/30',
8
+ critical: 'bg-destructive/15 text-destructive border-destructive/30',
9
+ };
10
+ export function AlertHistoryPanel({ pageId }) {
11
+ const { events, loading, acknowledge, remove } = useAlertEvents(pageId);
12
+ const open = events.filter(e => !e.acknowledged);
13
+ if (loading)
14
+ return null;
15
+ if (!events.length)
16
+ return null;
17
+ return (_jsxs(Card, { className: "mb-4 mx-2", children: [_jsx(CardHeader, { className: "pb-2 flex flex-row items-center justify-between space-y-0", children: _jsxs(CardTitle, { className: "text-sm font-medium flex items-center gap-2", children: [_jsx(Bell, { className: "w-4 h-4" }), " Alert-historie", open.length > 0 && _jsxs(Badge, { variant: "destructive", className: "ml-1", children: [open.length, " open"] })] }) }), _jsx(CardContent, { className: "space-y-2 max-h-64 overflow-auto", children: events.map(ev => (_jsxs("div", { className: `flex items-center justify-between gap-2 rounded border p-2 text-xs ${SEV_COLOR[ev.severity] ?? ''} ${ev.acknowledged ? 'opacity-60' : ''}`, children: [_jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "font-medium truncate", children: ev.message || 'Drempel overschreden' }), _jsxs("div", { className: "opacity-70", children: ["Waarde ", ev.value ?? '—', " \u00B7 drempel ", ev.threshold ?? '—', " \u00B7 ", new Date(ev.created_at).toLocaleString('nl-NL')] })] }), _jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [!ev.acknowledged && (_jsx(Button, { size: "icon", variant: "ghost", className: "h-6 w-6", onClick: () => acknowledge(ev.id), title: "Markeer als gezien", children: _jsx(Check, { className: "w-3.5 h-3.5" }) })), _jsx(Button, { size: "icon", variant: "ghost", className: "h-6 w-6", onClick: () => remove(ev.id), title: "Verwijderen", children: _jsx(Trash2, { className: "w-3.5 h-3.5" }) })] })] }, ev.id))) })] }));
18
+ }
@@ -0,0 +1,15 @@
1
+ import type { AnyBlockConfig, BlockRow, BlockType, BlockPermissions } from './types';
2
+ interface Props {
3
+ open: boolean;
4
+ onOpenChange: (v: boolean) => void;
5
+ initial?: BlockRow | null;
6
+ onSave: (input: {
7
+ type: BlockType;
8
+ title: string;
9
+ config: AnyBlockConfig;
10
+ permissions: BlockPermissions;
11
+ }) => void | Promise<void>;
12
+ }
13
+ export declare function BlockEditorDialog({ open, onOpenChange, initial, onSave }: Props): import("react").JSX.Element;
14
+ export {};
15
+ //# sourceMappingURL=BlockEditorDialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BlockEditorDialog.d.ts","sourceRoot":"","sources":["../../../src/components/dashboard/BlockEditorDialog.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,SAAS,EAAsF,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEzK,UAAU,KAAK;IACb,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,OAAO,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,cAAc,CAAC;QAAC,WAAW,EAAE,gBAAgB,CAAA;KAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpI;AAcD,wBAAgB,iBAAiB,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,KAAK,+BA8X/E"}
@@ -0,0 +1,89 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, toast, Switch, } from '@flowselections/core';
4
+ import { Sparkles, Loader2 } from 'lucide-react';
5
+ import { useServerFn } from '@tanstack/react-start';
6
+ import { useSchema } from '../../hooks/useSchema';
7
+ import { proposeBlockConfig } from '../../lib/ai-insights.functions';
8
+ const AGGS = ['count', 'sum', 'avg', 'min', 'max'];
9
+ const TYPES = [
10
+ { value: 'kpi', label: 'KPI' },
11
+ { value: 'table', label: 'Tabel' },
12
+ { value: 'chart', label: 'Grafiek' },
13
+ { value: 'ai_analysis', label: 'AI Analyse' },
14
+ { value: 'forecast', label: 'Forecast' },
15
+ { value: 'alert', label: 'Alert' },
16
+ ];
17
+ const OPERATORS = ['>', '>=', '<', '<=', '==', '!='];
18
+ export function BlockEditorDialog({ open, onOpenChange, initial, onSave }) {
19
+ const { tables, loading } = useSchema();
20
+ const [type, setType] = useState(initial?.type ?? 'kpi');
21
+ const [title, setTitle] = useState(initial?.title ?? '');
22
+ const [config, setConfig] = useState(initial?.config ?? { table: '', agg: 'count' });
23
+ const [aiPrompt, setAiPrompt] = useState('');
24
+ const [aiBusy, setAiBusy] = useState(false);
25
+ const [permissions, setPermissions] = useState(initial?.permissions ?? { can_view_ai: true, can_edit: true });
26
+ const propose = useServerFn(proposeBlockConfig);
27
+ useEffect(() => {
28
+ if (open) {
29
+ setType(initial?.type ?? 'kpi');
30
+ setTitle(initial?.title ?? '');
31
+ setConfig(initial?.config ?? { table: '', agg: 'count' });
32
+ setPermissions(initial?.permissions ?? { can_view_ai: true, can_edit: true });
33
+ }
34
+ }, [open, initial]);
35
+ const tableNames = useMemo(() => tables.map(t => t.name), [tables]);
36
+ const currentCols = useMemo(() => {
37
+ const t = tables.find(t => t.name === config.table);
38
+ return t?.columns ?? [];
39
+ }, [tables, config]);
40
+ function patchCfg(p) {
41
+ setConfig(prev => ({ ...prev, ...p }));
42
+ }
43
+ function handleTypeChange(v) {
44
+ setType(v);
45
+ const t = config.table ?? '';
46
+ if (v === 'kpi')
47
+ setConfig({ table: t, agg: 'count' });
48
+ if (v === 'table')
49
+ setConfig({ table: t, columns: [], limit: 50 });
50
+ if (v === 'chart')
51
+ setConfig({ kind: 'line', table: t, xColumn: '', yColumn: '', agg: 'sum', limit: 100 });
52
+ if (v === 'ai_analysis')
53
+ setConfig({ table: t, columns: [], limit: 100 });
54
+ if (v === 'forecast')
55
+ setConfig({ table: t, xColumn: '', yColumn: '', agg: 'sum', horizon: 6, history: 200 });
56
+ if (v === 'alert')
57
+ setConfig({ table: t, agg: 'count', operator: '>', threshold: 0, severity: 'warning' });
58
+ }
59
+ async function generateWithAi() {
60
+ if (!aiPrompt.trim())
61
+ return;
62
+ setAiBusy(true);
63
+ try {
64
+ const res = await propose({ data: { prompt: aiPrompt, schema: tables } });
65
+ if (!res.proposal) {
66
+ toast.error(res.note || 'Geen voorstel');
67
+ return;
68
+ }
69
+ const p = res.proposal;
70
+ if (p.type)
71
+ setType(p.type);
72
+ if (p.title)
73
+ setTitle(p.title);
74
+ if (p.config)
75
+ setConfig(p.config);
76
+ toast.success('Voorstel ingevuld — pas aan en sla op');
77
+ }
78
+ catch (e) {
79
+ toast.error('Fout: ' + (e?.message ?? 'onbekend'));
80
+ }
81
+ finally {
82
+ setAiBusy(false);
83
+ }
84
+ }
85
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-2xl", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: initial ? 'Blok bewerken' : 'Nieuw blok' }) }), !initial && (_jsxs("div", { className: "rounded-lg border border-primary/30 bg-primary/5 p-3 space-y-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-primary", children: [_jsx(Sparkles, { className: "w-4 h-4" }), " Genereer met AI"] }), _jsx(Textarea, { value: aiPrompt, onChange: (e) => setAiPrompt(e.target.value), placeholder: "bijv. Toon een staafgrafiek van de omzet per maand uit tabel orders", rows: 2 }), _jsx("div", { className: "flex justify-end", children: _jsxs(Button, { size: "sm", onClick: generateWithAi, disabled: aiBusy || !aiPrompt.trim(), children: [aiBusy ? _jsx(Loader2, { className: "w-3.5 h-3.5 mr-2 animate-spin" }) : _jsx(Sparkles, { className: "w-3.5 h-3.5 mr-2" }), "Voorstel maken"] }) })] })), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Type" }), _jsxs(Select, { value: type, onValueChange: (v) => handleTypeChange(v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: TYPES.map(t => _jsx(SelectItem, { value: t.value, children: t.label }, t.value)) })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Titel" }), _jsx(Input, { value: title, onChange: (e) => setTitle(e.target.value), placeholder: "bijv. Aantal orders" })] }), _jsxs("div", { className: "space-y-2 col-span-2", children: [_jsx(Label, { children: "Tabel (databron)" }), _jsxs(Select, { value: config.table ?? '', onValueChange: (v) => patchCfg({ table: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: loading ? 'Laden…' : 'Kies een tabel' }) }), _jsx(SelectContent, { className: "max-h-72", children: tableNames.map(n => _jsx(SelectItem, { value: n, children: n }, n)) })] })] }), type === 'kpi' && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Aggregatie" }), _jsxs(Select, { value: config.agg, onValueChange: (v) => patchCfg({ agg: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: AGGS.map(a => _jsx(SelectItem, { value: a, children: a.toUpperCase() }, a)) })] })] }), config.agg !== 'count' && (_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Kolom" }), _jsxs(Select, { value: config.column ?? '', onValueChange: (v) => patchCfg({ column: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies kolom" }) }), _jsx(SelectContent, { children: currentCols.map(c => _jsx(SelectItem, { value: c.name, children: c.name }, c.name)) })] })] })), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Formaat" }), _jsxs(Select, { value: config.format ?? 'number', onValueChange: (v) => patchCfg({ format: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "number", children: "Getal" }), _jsx(SelectItem, { value: "currency", children: "\u20AC Bedrag" }), _jsx(SelectItem, { value: "percent", children: "Percentage" })] })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Subtitel" }), _jsx(Input, { value: config.subtitle ?? '', onChange: (e) => patchCfg({ subtitle: e.target.value }) })] })] })), type === 'table' && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-2 col-span-2", children: [_jsx(Label, { children: "Kolommen (komma-gescheiden, leeg = alles)" }), _jsx(Input, { value: (config.columns ?? []).join(','), onChange: (e) => patchCfg({ columns: e.target.value.split(',').map(s => s.trim()).filter(Boolean) }), placeholder: "bijv. id,name,created_at" })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Sorteer op" }), _jsxs(Select, { value: config.orderBy ?? '', onValueChange: (v) => patchCfg({ orderBy: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kolom" }) }), _jsx(SelectContent, { children: currentCols.map(c => _jsx(SelectItem, { value: c.name, children: c.name }, c.name)) })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Limiet" }), _jsx(Input, { type: "number", value: config.limit ?? 50, onChange: (e) => patchCfg({ limit: Number(e.target.value) }) })] })] })), type === 'chart' && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Soort" }), _jsxs(Select, { value: config.kind, onValueChange: (v) => patchCfg({ kind: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "line", children: "Lijn" }), _jsx(SelectItem, { value: "bar", children: "Staaf" }), _jsx(SelectItem, { value: "area", children: "Vlak" }), _jsx(SelectItem, { value: "stacked-bar", children: "Gestapelde staaf" }), _jsx(SelectItem, { value: "pie", children: "Taart" }), _jsx(SelectItem, { value: "donut", children: "Donut" }), _jsx(SelectItem, { value: "scatter", children: "Scatter" }), _jsx(SelectItem, { value: "heatmap", children: "Heatmap" }), _jsx(SelectItem, { value: "funnel", children: "Funnel" })] })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Aggregatie (Y)" }), _jsxs(Select, { value: config.agg, onValueChange: (v) => patchCfg({ agg: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: AGGS.map(a => _jsx(SelectItem, { value: a, children: a.toUpperCase() }, a)) })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "X-as kolom" }), _jsxs(Select, { value: config.xColumn, onValueChange: (v) => patchCfg({ xColumn: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies X" }) }), _jsx(SelectContent, { children: currentCols.map(c => _jsx(SelectItem, { value: c.name, children: c.name }, c.name)) })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Y-as kolom" }), _jsxs(Select, { value: config.yColumn, onValueChange: (v) => patchCfg({ yColumn: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies Y" }) }), _jsx(SelectContent, { children: currentCols.map(c => _jsx(SelectItem, { value: c.name, children: c.name }, c.name)) })] })] })] })), type === 'ai_analysis' && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-2 col-span-2", children: [_jsx(Label, { children: "Kolommen (komma-gescheiden, leeg = alles)" }), _jsx(Input, { value: (config.columns ?? []).join(','), onChange: (e) => patchCfg({ columns: e.target.value.split(',').map(s => s.trim()).filter(Boolean) }), placeholder: "bijv. id,status,created_at" })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Sample-grootte" }), _jsx(Input, { type: "number", value: config.limit ?? 100, onChange: (e) => patchCfg({ limit: Number(e.target.value) }) })] }), _jsxs("div", { className: "space-y-2 col-span-2", children: [_jsx(Label, { children: "Extra vraag voor de AI (optioneel)" }), _jsx(Input, { value: config.question ?? '', onChange: (e) => patchCfg({ question: e.target.value }), placeholder: "bijv. Welke artikelen blijven achter?" })] })] })), type === 'forecast' && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "X-as (periode/datum)" }), _jsxs(Select, { value: config.xColumn, onValueChange: (v) => patchCfg({ xColumn: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies kolom" }) }), _jsx(SelectContent, { children: currentCols.map(c => _jsx(SelectItem, { value: c.name, children: c.name }, c.name)) })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Y-as (waarde)" }), _jsxs(Select, { value: config.yColumn, onValueChange: (v) => patchCfg({ yColumn: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies kolom" }) }), _jsx(SelectContent, { children: currentCols.map(c => _jsx(SelectItem, { value: c.name, children: c.name }, c.name)) })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Aggregatie" }), _jsxs(Select, { value: config.agg, onValueChange: (v) => patchCfg({ agg: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: AGGS.map(a => _jsx(SelectItem, { value: a, children: a.toUpperCase() }, a)) })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Horizon (periodes)" }), _jsx(Input, { type: "number", value: config.horizon ?? 6, onChange: (e) => patchCfg({ horizon: Number(e.target.value) }) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Historie-rijen" }), _jsx(Input, { type: "number", value: config.history ?? 200, onChange: (e) => patchCfg({ history: Number(e.target.value) }) })] }), _jsxs("div", { className: "space-y-2 col-span-2", children: [_jsx(Label, { children: "Extra context (optioneel)" }), _jsx(Input, { value: config.question ?? '', onChange: (e) => patchCfg({ question: e.target.value }), placeholder: "bijv. Houd rekening met seizoenseffect" })] })] })), type === 'alert' && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Aggregatie" }), _jsxs(Select, { value: config.agg, onValueChange: (v) => patchCfg({ agg: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: AGGS.map(a => _jsx(SelectItem, { value: a, children: a.toUpperCase() }, a)) })] })] }), config.agg !== 'count' && (_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Kolom" }), _jsxs(Select, { value: config.column ?? '', onValueChange: (v) => patchCfg({ column: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies kolom" }) }), _jsx(SelectContent, { children: currentCols.map(c => _jsx(SelectItem, { value: c.name, children: c.name }, c.name)) })] })] })), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Operator" }), _jsxs(Select, { value: config.operator, onValueChange: (v) => patchCfg({ operator: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: OPERATORS.map(o => _jsx(SelectItem, { value: o, children: o }, o)) })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Drempelwaarde" }), _jsx(Input, { type: "number", value: config.threshold ?? 0, onChange: (e) => patchCfg({ threshold: Number(e.target.value) }) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Ernst" }), _jsxs(Select, { value: config.severity ?? 'warning', onValueChange: (v) => patchCfg({ severity: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "info", children: "Info" }), _jsx(SelectItem, { value: "warning", children: "Waarschuwing" }), _jsx(SelectItem, { value: "critical", children: "Kritiek" })] })] })] }), _jsxs("div", { className: "space-y-2 col-span-2", children: [_jsx(Label, { children: "Boodschap bij trigger" }), _jsx(Input, { value: config.message ?? '', onChange: (e) => patchCfg({ message: e.target.value }), placeholder: "bijv. Voorraad onder kritieke grens" })] })] }))] }), _jsxs("div", { className: "rounded-lg border bg-muted/30 p-3 space-y-2", children: [_jsx("div", { className: "text-sm font-medium", children: "Rechten" }), _jsxs("div", { className: "flex items-center justify-between text-sm", children: [_jsxs(Label, { className: "flex flex-col gap-0.5", children: [_jsx("span", { children: "AI-analyse zichtbaar" }), _jsx("span", { className: "text-xs text-muted-foreground font-normal", children: "Toon AI-inzichten op dit blok" })] }), _jsx(Switch, { checked: permissions.can_view_ai !== false, onCheckedChange: (v) => setPermissions(p => ({ ...p, can_view_ai: v })) })] }), _jsxs("div", { className: "flex items-center justify-between text-sm", children: [_jsxs(Label, { className: "flex flex-col gap-0.5", children: [_jsx("span", { children: "Bewerkbaar" }), _jsx("span", { className: "text-xs text-muted-foreground font-normal", children: "Mag in de builder worden aangepast" })] }), _jsx(Switch, { checked: permissions.can_edit !== false, onCheckedChange: (v) => setPermissions(p => ({ ...p, can_edit: v })) })] }), _jsxs("div", { className: "space-y-1 pt-2 border-t", children: [_jsx(Label, { className: "text-sm", children: "Zichtbaar voor rollen (komma-gescheiden, leeg = iedereen)" }), _jsx(Input, { placeholder: "bijv. admin, manager, sales", value: (permissions.viewer_roles ?? []).join(', '), onChange: (e) => setPermissions(p => ({
86
+ ...p,
87
+ viewer_roles: e.target.value.split(',').map(s => s.trim()).filter(Boolean),
88
+ })) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Server-side controle: alleen gebruikers met \u00E9\u00E9n van deze rollen (via has_role) zien dit blok." })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), children: "Annuleren" }), _jsx(Button, { onClick: async () => { await onSave({ type, title, config, permissions }); onOpenChange(false); }, children: "Opslaan" })] })] }) }));
89
+ }
@@ -0,0 +1,5 @@
1
+ import type { BlockRow } from './types';
2
+ export declare function BlockRenderer({ block }: {
3
+ block: BlockRow;
4
+ }): import("react").JSX.Element;
5
+ //# sourceMappingURL=BlockRenderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BlockRenderer.d.ts","sourceRoot":"","sources":["../../../src/components/dashboard/BlockRenderer.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,QAAQ,CAAA;CAAE,+BAoB3D"}
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Lock } from 'lucide-react';
3
+ import { KpiBlock } from './blocks/KpiBlock';
4
+ import { TableBlock } from './blocks/TableBlock';
5
+ import { ChartBlock } from './blocks/ChartBlock';
6
+ import { AiAnalysisBlock } from './blocks/AiAnalysisBlock';
7
+ import { ForecastBlock } from './blocks/ForecastBlock';
8
+ import { AlertBlock } from './blocks/AlertBlock';
9
+ import { useUserRoles } from '../../hooks/useUserRoles';
10
+ export function BlockRenderer({ block }) {
11
+ const { roles, loading } = useUserRoles();
12
+ const required = block.permissions?.viewer_roles ?? [];
13
+ if (required.length > 0 && !loading && !required.some(r => roles.includes(r))) {
14
+ return (_jsxs("div", { className: "h-full flex flex-col items-center justify-center text-xs text-muted-foreground border rounded-lg bg-muted/30 p-4 gap-2", children: [_jsx(Lock, { className: "w-4 h-4" }), "Geen toegang tot dit blok"] }));
15
+ }
16
+ switch (block.type) {
17
+ case 'kpi': return _jsx(KpiBlock, { block: block });
18
+ case 'table': return _jsx(TableBlock, { block: block });
19
+ case 'chart': return _jsx(ChartBlock, { block: block });
20
+ case 'ai_analysis': return _jsx(AiAnalysisBlock, { block: block });
21
+ case 'forecast': return _jsx(ForecastBlock, { block: block });
22
+ case 'alert': return _jsx(AlertBlock, { block: block });
23
+ default: return _jsxs("div", { className: "p-4 text-sm text-muted-foreground", children: ["Onbekend bloktype: ", block.type] });
24
+ }
25
+ }
@@ -0,0 +1,4 @@
1
+ import 'react-grid-layout/css/styles.css';
2
+ import 'react-resizable/css/styles.css';
3
+ export declare function DashboardBuilderPage(): import("react").JSX.Element;
4
+ //# sourceMappingURL=DashboardBuilderPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DashboardBuilderPage.d.ts","sourceRoot":"","sources":["../../../src/components/dashboard/DashboardBuilderPage.tsx"],"names":[],"mappings":"AAeA,OAAO,kCAAkC,CAAC;AAC1C,OAAO,gCAAgC,CAAC;AAExC,wBAAgB,oBAAoB,gCA2InC"}
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { useParams, useNavigate } from '@tanstack/react-router';
4
+ import { Button } from '@flowselections/core';
5
+ import { Save, Trash2, Pencil, Sparkles, Settings, MessageSquare } from 'lucide-react';
6
+ import GridLayout from 'react-grid-layout/legacy';
7
+ import { useDashboardPages } from '../../hooks/useDashboardPages';
8
+ import { useDashboardBlocks } from '../../hooks/useDashboardBlocks';
9
+ import { BlockRenderer } from './BlockRenderer';
10
+ import { BlockEditorDialog } from './BlockEditorDialog';
11
+ import { BlockGeneratorDialog } from './ai/BlockGeneratorDialog';
12
+ import { QuickAddDialog } from './QuickAddDialog';
13
+ import { SuggestionBanner } from './SuggestionBanner';
14
+ import { TileGallery } from './TileGallery';
15
+ import 'react-grid-layout/css/styles.css';
16
+ import 'react-resizable/css/styles.css';
17
+ export function DashboardBuilderPage() {
18
+ const params = useParams({ strict: false });
19
+ const pageId = params.pageId ?? null;
20
+ const navigate = useNavigate();
21
+ const { pages } = useDashboardPages();
22
+ const page = pages.find(p => p.id === pageId);
23
+ const { blocks, create, update, remove, updateLayouts } = useDashboardBlocks(pageId);
24
+ const [editorOpen, setEditorOpen] = useState(false);
25
+ const [editing, setEditing] = useState(null);
26
+ const [genOpen, setGenOpen] = useState(false);
27
+ const [quickOpen, setQuickOpen] = useState(false);
28
+ const layout = blocks.map(b => ({
29
+ i: b.id,
30
+ x: b.layout?.x ?? 0,
31
+ y: b.layout?.y ?? 0,
32
+ w: b.layout?.w ?? 4,
33
+ h: b.layout?.h ?? 4,
34
+ }));
35
+ async function handleLayoutChange(next) {
36
+ const patch = next.map(l => ({ id: l.i, layout: { x: l.x, y: l.y, w: l.w, h: l.h } }));
37
+ await updateLayouts(patch);
38
+ }
39
+ return (_jsxs("div", { className: "p-4", children: [_jsxs("div", { className: "flex items-center justify-between mb-4 px-2", children: [_jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Bewerken" }), _jsx("h1", { className: "text-xl font-semibold", children: page?.name ?? 'Dashboard' })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => { setEditing(null); setEditorOpen(true); }, title: "Geavanceerd: zelf instellen", children: _jsx(Settings, { className: "w-4 h-4" }) }), _jsxs(Button, { variant: "outline", onClick: () => pageId && navigate({ to: '/dashboard-erp-kwekers/$pageId', params: { pageId } }), children: [_jsx(Save, { className: "w-4 h-4 mr-2" }), " Klaar"] })] })] }), _jsx("div", { className: "px-2", children: _jsx(TileGallery, { onAdd: async ({ type, title, config }) => { await create({ type, title, config }); } }) }), blocks.length === 0 ? (_jsx("div", { className: "p-8 text-center border-2 border-dashed rounded-lg mx-2 text-muted-foreground", children: "Nog geen blokken. Kies hierboven een kant-en-klare tegel, of vraag de AI rechtsonder om een voorstel." })) : (_jsx(GridLayout, { className: "layout", layout: layout, cols: 12, rowHeight: 60, width: 1200, onLayoutChange: handleLayoutChange, draggableHandle: ".drag-handle", children: blocks.map(b => (_jsxs("div", { className: "relative group", children: [_jsx("div", { className: "drag-handle absolute top-1 left-1 right-1 h-6 cursor-move opacity-0 group-hover:opacity-100 transition bg-muted/50 rounded text-xs flex items-center justify-center text-muted-foreground z-10", children: "\u22EE\u22EE slepen" }), _jsxs("div", { className: "absolute top-1 right-1 flex gap-1 opacity-0 group-hover:opacity-100 transition z-10", children: [_jsx(Button, { size: "icon", variant: "secondary", className: "h-7 w-7", onClick: () => { setEditing(b); setEditorOpen(true); }, children: _jsx(Pencil, { className: "w-3.5 h-3.5" }) }), _jsx(Button, { size: "icon", variant: "destructive", className: "h-7 w-7", onClick: () => remove(b.id), children: _jsx(Trash2, { className: "w-3.5 h-3.5" }) })] }), _jsx(BlockRenderer, { block: b })] }, b.id))) })), blocks.length > 0 && (_jsx("div", { className: "px-2 mt-4", children: _jsx(SuggestionBanner, { block: blocks[blocks.length - 1], onAdd: async ({ type, title, config }) => { await create({ type, title, config }); } }) })), _jsxs("div", { className: "fixed bottom-6 right-6 z-40 flex flex-col items-end gap-2", children: [_jsxs(Button, { onClick: () => setQuickOpen(true), className: "shadow-lg rounded-full h-14 pl-4 pr-5", children: [_jsx(Sparkles, { className: "w-5 h-5 mr-2" }), "AI-voorstel"] }), _jsxs(Button, { variant: "secondary", size: "sm", onClick: () => setGenOpen(true), className: "shadow-md rounded-full", children: [_jsx(MessageSquare, { className: "w-4 h-4 mr-2" }), "AI-chat"] })] }), _jsx(QuickAddDialog, { open: quickOpen, onOpenChange: setQuickOpen, onAdd: async ({ type, title, config }) => { await create({ type, title, config }); }, onAdvanced: () => { setEditing(null); setEditorOpen(true); } }), _jsx(BlockEditorDialog, { open: editorOpen, onOpenChange: setEditorOpen, initial: editing, onSave: async ({ type, title, config, permissions }) => {
40
+ if (editing) {
41
+ await update(editing.id, { title, config, permissions });
42
+ }
43
+ else {
44
+ await create({ type, title, config, permissions });
45
+ }
46
+ } }), _jsx(BlockGeneratorDialog, { open: genOpen, onOpenChange: setGenOpen, onSave: async ({ type, title, config, permissions }) => {
47
+ await create({ type, title, config, permissions });
48
+ } })] }));
49
+ }
@@ -0,0 +1,2 @@
1
+ export declare function DashboardErpLayoutPage(): import("react").JSX.Element;
2
+ //# sourceMappingURL=DashboardErpLayoutPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DashboardErpLayoutPage.d.ts","sourceRoot":"","sources":["../../../src/components/dashboard/DashboardErpLayoutPage.tsx"],"names":[],"mappings":"AAOA,wBAAgB,sBAAsB,gCAkErC"}
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Outlet, useParams, useNavigate, Link } from '@tanstack/react-router';
4
+ import { Button, Input, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@flowselections/core';
5
+ import { Plus, LayoutGrid } from 'lucide-react';
6
+ import { useDashboardPages } from '../../hooks/useDashboardPages';
7
+ import { AiChatPanel } from './ai/AiChatPanel';
8
+ export function DashboardErpLayoutPage() {
9
+ const { pages, loading, create } = useDashboardPages();
10
+ const params = useParams({ strict: false });
11
+ const navigate = useNavigate();
12
+ const [openNew, setOpenNew] = useState(false);
13
+ const [name, setName] = useState('');
14
+ const activeId = params.pageId ?? pages[0]?.id;
15
+ async function handleCreate() {
16
+ if (!name.trim())
17
+ return;
18
+ const page = await create(name.trim());
19
+ setOpenNew(false);
20
+ setName('');
21
+ if (page)
22
+ navigate({ to: '/dashboard-erp-kwekers/$pageId', params: { pageId: page.id } });
23
+ }
24
+ return (_jsxs("div", { className: "flex flex-col h-full", children: [_jsx("div", { className: "border-b bg-card", children: _jsxs("div", { className: "px-6 pt-3 flex items-end gap-1 overflow-x-auto", children: [loading && _jsx("div", { className: "text-sm text-muted-foreground pb-3", children: "Dashboards laden\u2026" }), !loading && pages.length === 0 && (_jsx("div", { className: "text-sm text-muted-foreground pb-3", children: "Nog geen dashboards." })), pages.map(p => {
25
+ const isActive = p.id === activeId;
26
+ return (_jsxs(Link, { to: '/dashboard-erp-kwekers/$pageId', params: { pageId: p.id }, className: `px-4 py-2 text-sm rounded-t-md border-b-2 -mb-px whitespace-nowrap ${isActive
27
+ ? 'border-primary text-foreground font-medium bg-background'
28
+ : 'border-transparent text-muted-foreground hover:text-foreground'}`, children: [_jsx(LayoutGrid, { className: "inline w-3.5 h-3.5 mr-1.5 -mt-0.5" }), p.name] }, p.id));
29
+ }), _jsxs(Button, { variant: "ghost", size: "sm", className: "ml-2 mb-1", onClick: () => setOpenNew(true), children: [_jsx(Plus, { className: "w-4 h-4 mr-1" }), " Nieuw dashboard"] })] }) }), _jsx("div", { className: "flex-1 overflow-auto", children: _jsx(Outlet, {}) }), _jsx(AiChatPanel, { pageId: activeId ?? null }), _jsx(Dialog, { open: openNew, onOpenChange: setOpenNew, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Nieuw dashboard" }) }), _jsx(Input, { value: name, onChange: (e) => setName(e.target.value), placeholder: "Bijv. Voorraad overzicht", autoFocus: true }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setOpenNew(false), children: "Annuleren" }), _jsx(Button, { onClick: handleCreate, children: "Aanmaken" })] })] }) })] }));
30
+ }
@@ -0,0 +1,9 @@
1
+ import type { BlockRow } from './types';
2
+ interface Props {
3
+ pageId?: string;
4
+ pageName: string;
5
+ blocks: BlockRow[];
6
+ }
7
+ export declare function DashboardSummary({ pageId, pageName, blocks }: Props): import("react").JSX.Element | null;
8
+ export {};
9
+ //# sourceMappingURL=DashboardSummary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DashboardSummary.d.ts","sourceRoot":"","sources":["../../../src/components/dashboard/DashboardSummary.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAuC,MAAM,SAAS,CAAC;AAE7E,UAAU,KAAK;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,QAAQ,EAAE,CAAC;CACpB;AA+CD,wBAAgB,gBAAgB,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,KAAK,sCAqHnE"}
@@ -0,0 +1,136 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useState } from 'react';
3
+ import { Card, CardContent, Button, supabase, toast } from '@flowselections/core';
4
+ import { Sparkles, RefreshCw, Loader2, FileText, Eye, EyeOff } from 'lucide-react';
5
+ import { useServerFn } from '@tanstack/react-start';
6
+ import ReactMarkdown from 'react-markdown';
7
+ import { summarizeDashboard } from '../../lib/ai-insights.functions';
8
+ import { saveDailyReport, listDailyReports } from '../../lib/dashboard-eval.functions';
9
+ import { useAiSettings, useDashboardAiVisible, buildAiInstruction } from '../../lib/aiSettings';
10
+ async function snapshotBlock(b) {
11
+ const title = b.title || b.type;
12
+ try {
13
+ if (b.type === 'kpi') {
14
+ const c = b.config;
15
+ if (!c.table)
16
+ return { title, type: b.type };
17
+ if (c.agg === 'count') {
18
+ const { count } = await supabase.from(c.table).select('*', { count: 'exact', head: true });
19
+ return { title, type: b.type, table: c.table, value: count ?? 0 };
20
+ }
21
+ if (!c.column)
22
+ return { title, type: b.type, table: c.table, value: 0 };
23
+ const { data } = await supabase.from(c.table).select(c.column).limit(5000);
24
+ const nums = (data ?? []).map(r => Number(r?.[c.column])).filter(n => !Number.isNaN(n));
25
+ let value = 0;
26
+ if (nums.length) {
27
+ switch (c.agg) {
28
+ case 'sum':
29
+ value = nums.reduce((a, b) => a + b, 0);
30
+ break;
31
+ case 'avg':
32
+ value = nums.reduce((a, b) => a + b, 0) / nums.length;
33
+ break;
34
+ case 'min':
35
+ value = Math.min(...nums);
36
+ break;
37
+ case 'max':
38
+ value = Math.max(...nums);
39
+ break;
40
+ }
41
+ }
42
+ return { title, type: b.type, table: c.table, value };
43
+ }
44
+ if (b.type === 'table') {
45
+ const c = b.config;
46
+ if (!c.table)
47
+ return { title, type: b.type };
48
+ const cols = c.columns && c.columns.length ? c.columns.join(',') : '*';
49
+ const { data } = await supabase.from(c.table).select(cols).limit(Math.min(c.limit ?? 20, 20));
50
+ return { title, type: b.type, table: c.table, rows: Array.isArray(data) ? data : [] };
51
+ }
52
+ if (b.type === 'chart') {
53
+ const c = b.config;
54
+ if (!c.table)
55
+ return { title, type: b.type };
56
+ const { data } = await supabase.from(c.table).select(`${c.xColumn},${c.yColumn}`).limit(100);
57
+ return { title, type: b.type, table: c.table, rows: Array.isArray(data) ? data.slice(0, 20) : [] };
58
+ }
59
+ }
60
+ catch {
61
+ // ignore
62
+ }
63
+ return { title, type: b.type };
64
+ }
65
+ export function DashboardSummary({ pageId, pageName, blocks }) {
66
+ const [settings] = useAiSettings();
67
+ const [visible, setVisible] = useDashboardAiVisible(pageId);
68
+ const run = useServerFn(summarizeDashboard);
69
+ const save = useServerFn(saveDailyReport);
70
+ const listReports = useServerFn(listDailyReports);
71
+ const [text, setText] = useState('');
72
+ const [loading, setLoading] = useState(false);
73
+ const [error, setError] = useState(null);
74
+ const [saving, setSaving] = useState(false);
75
+ const [reports, setReports] = useState([]);
76
+ useEffect(() => {
77
+ if (!pageId)
78
+ return;
79
+ (async () => {
80
+ try {
81
+ const res = await listReports({ data: { pageId } });
82
+ setReports(Array.isArray(res?.reports) ? res.reports.slice(0, 3) : []);
83
+ }
84
+ catch { /* niet-fataal */ }
85
+ })();
86
+ }, [pageId, listReports]);
87
+ const summarize = useCallback(async () => {
88
+ if (!settings.enabled || !settings.types.dashboard)
89
+ return;
90
+ const dataBlocks = blocks.filter(b => b.type !== 'ai_analysis');
91
+ if (!dataBlocks.length)
92
+ return;
93
+ setLoading(true);
94
+ setError(null);
95
+ try {
96
+ const snapshots = await Promise.all(dataBlocks.map(snapshotBlock));
97
+ const res = await run({
98
+ data: { pageName, blocks: snapshots, instruction: buildAiInstruction(settings) },
99
+ });
100
+ setText(res.text);
101
+ }
102
+ catch (e) {
103
+ setError(e?.message ?? 'Fout bij samenvatting');
104
+ }
105
+ finally {
106
+ setLoading(false);
107
+ }
108
+ }, [run, pageName, blocks, settings]);
109
+ useEffect(() => { if (visible)
110
+ summarize(); /* eslint-disable-next-line */ }, [pageName, blocks.length, settings.enabled, settings.detail, settings.personality, visible]);
111
+ if (!blocks.length)
112
+ return null;
113
+ if (!settings.enabled || !settings.types.dashboard)
114
+ return null;
115
+ if (!visible) {
116
+ return (_jsx("div", { className: "mb-2 mx-2 flex justify-end", children: _jsxs(Button, { size: "sm", variant: "ghost", onClick: () => setVisible(true), className: "h-7 text-xs gap-1", children: [_jsx(Eye, { className: "w-3.5 h-3.5" }), " AI-samenvatting tonen"] }) }));
117
+ }
118
+ async function handleSave() {
119
+ if (!pageId || !text)
120
+ return;
121
+ setSaving(true);
122
+ try {
123
+ await save({ data: { pageId, summary: text } });
124
+ toast.success('Rapport opgeslagen');
125
+ const res = await listReports({ data: { pageId } });
126
+ setReports(Array.isArray(res?.reports) ? res.reports.slice(0, 3) : []);
127
+ }
128
+ catch (e) {
129
+ toast.error('Opslaan mislukt: ' + (e?.message ?? ''));
130
+ }
131
+ finally {
132
+ setSaving(false);
133
+ }
134
+ }
135
+ return (_jsx(Card, { className: "mb-4 mx-2 border-primary/20 bg-gradient-to-br from-primary/5 to-transparent", children: _jsxs(CardContent, { className: "py-4 px-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-primary", children: [_jsx(Sparkles, { className: "w-4 h-4" }), " AI-samenvatting"] }), _jsxs("div", { className: "flex items-center gap-1 -mt-1", children: [_jsx(Button, { size: "icon", variant: "ghost", onClick: () => setVisible(false), className: "h-7 w-7", title: "Verbergen op dit dashboard", children: _jsx(EyeOff, { className: "w-3.5 h-3.5" }) }), pageId && text && (_jsx(Button, { size: "sm", variant: "ghost", onClick: handleSave, disabled: saving, className: "h-7 text-xs", children: saving ? 'Opslaan…' : 'Bewaar als dagrapport' })), _jsx(Button, { size: "icon", variant: "ghost", onClick: summarize, disabled: loading, className: "h-7 w-7", children: loading ? _jsx(Loader2, { className: "w-3.5 h-3.5 animate-spin" }) : _jsx(RefreshCw, { className: "w-3.5 h-3.5" }) })] })] }), _jsxs("div", { className: "mt-2 text-sm", children: [error && _jsx("div", { className: "text-destructive", children: error }), loading && !text && _jsx("div", { className: "text-muted-foreground", children: "AI leest het dashboard\u2026" }), text && (_jsx("div", { className: "prose prose-sm dark:prose-invert max-w-none", children: _jsx(ReactMarkdown, { children: text }) }))] }), reports.length > 0 && (_jsxs("div", { className: "mt-3 pt-3 border-t border-primary/10", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs font-medium text-muted-foreground mb-2", children: [_jsx(FileText, { className: "w-3.5 h-3.5" }), " Recente dagrapporten"] }), _jsx("div", { className: "space-y-2", children: reports.map(r => (_jsxs("details", { className: "text-xs", children: [_jsx("summary", { className: "cursor-pointer text-muted-foreground hover:text-foreground", children: new Date(r.created_at).toLocaleString('nl-NL') }), _jsx("div", { className: "mt-1 pl-3 text-foreground/80", children: r.summary })] }, r.id))) })] }))] }) }));
136
+ }
@@ -0,0 +1,3 @@
1
+ import 'react-grid-layout/css/styles.css';
2
+ export declare function DashboardViewPage(): import("react").JSX.Element;
3
+ //# sourceMappingURL=DashboardViewPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DashboardViewPage.d.ts","sourceRoot":"","sources":["../../../src/components/dashboard/DashboardViewPage.tsx"],"names":[],"mappings":"AAUA,OAAO,kCAAkC,CAAC;AAE1C,wBAAgB,iBAAiB,gCAMhC"}