@fayz-ai/plugin-reports 0.9.1 → 0.11.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 (46) hide show
  1. package/dist/{chunk-2ZFN6ECM.js → chunk-4SPBNVQA.js} +3 -2
  2. package/dist/chunk-4SPBNVQA.js.map +1 -0
  3. package/dist/data/analytics-mock.d.ts.map +1 -1
  4. package/dist/data/analytics-supabase.d.ts.map +1 -1
  5. package/dist/{excel-WE73YE2V.js → excel-KIJTC7OJ.js} +3 -3
  6. package/dist/{excel-WE73YE2V.js.map → excel-KIJTC7OJ.js.map} +1 -1
  7. package/dist/index.d.ts +12 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +367 -112
  10. package/dist/index.js.map +1 -1
  11. package/dist/lib/columns.d.ts.map +1 -1
  12. package/dist/lib/legacy.d.ts.map +1 -1
  13. package/dist/lib/tenant.d.ts +1 -0
  14. package/dist/lib/tenant.d.ts.map +1 -1
  15. package/dist/locales/en.d.ts.map +1 -1
  16. package/dist/locales/pt-BR.d.ts.map +1 -1
  17. package/dist/{pdf-E6DXHFXU.js → pdf-7QQ3U5JQ.js} +3 -3
  18. package/dist/{pdf-E6DXHFXU.js.map → pdf-7QQ3U5JQ.js.map} +1 -1
  19. package/dist/types.d.ts +2 -0
  20. package/dist/types.d.ts.map +1 -1
  21. package/dist/views/AnalyticsAssistant.d.ts +3 -0
  22. package/dist/views/AnalyticsAssistant.d.ts.map +1 -0
  23. package/dist/views/AnalyticsModule.d.ts +10 -0
  24. package/dist/views/AnalyticsModule.d.ts.map +1 -0
  25. package/dist/views/AnalyticsOverview.d.ts.map +1 -1
  26. package/dist/views/AnalyticsReportViewer.d.ts.map +1 -1
  27. package/dist/views/AnalyticsSurface.d.ts +27 -0
  28. package/dist/views/AnalyticsSurface.d.ts.map +1 -1
  29. package/package.json +4 -4
  30. package/dist/chunk-2ZFN6ECM.js.map +0 -1
  31. package/dist/components/ReportCard.d.ts +0 -9
  32. package/dist/components/ReportCard.d.ts.map +0 -1
  33. package/dist/components/ReportFilterBar.d.ts +0 -12
  34. package/dist/components/ReportFilterBar.d.ts.map +0 -1
  35. package/dist/components/ReportSummaryRow.d.ts +0 -10
  36. package/dist/components/ReportSummaryRow.d.ts.map +0 -1
  37. package/dist/context.d.ts +0 -16
  38. package/dist/context.d.ts.map +0 -1
  39. package/dist/hooks/useReportData.d.ts +0 -19
  40. package/dist/hooks/useReportData.d.ts.map +0 -1
  41. package/dist/views/ReportHub.d.ts +0 -8
  42. package/dist/views/ReportHub.d.ts.map +0 -1
  43. package/dist/views/ReportViewer.d.ts +0 -9
  44. package/dist/views/ReportViewer.d.ts.map +0 -1
  45. package/dist/views/ReportsPage.d.ts +0 -8
  46. package/dist/views/ReportsPage.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
- import { reportColumnsToTanstack, formatValuePlain } from './chunk-2ZFN6ECM.js';
2
- import * as React4 from 'react';
3
- import React4__default, { useState, useEffect } from 'react';
1
+ import { reportColumnsToTanstack, formatValuePlain } from './chunk-4SPBNVQA.js';
2
+ import * as React6 from 'react';
3
+ import React6__default, { useState, useEffect } from 'react';
4
4
  import { usePluginRuntime, getAnalyticsCards, registerTranslations, createSafeDataProvider, getSupabaseClientOptional, hashRouterAdapter, useTranslation, exportCSV } from '@fayz-ai/core';
5
- import { AnalyticsContextProvider, collectFilters, AnalyticsControls, AnalyticsGrid, Card, FilterChip, DataTable, Button, SubpageHeader, useAnalyticsCard, useAnalyticsContext, AnalyticsCard, Dropdown, DropdownTrigger, DropdownContent, DropdownItem } from '@fayz-ai/ui';
5
+ import { useOrganizationStore, useShellRailAvailable, useContextualNav, AssistantInvite } from '@fayz-ai/admin';
6
+ import { AnalyticsContextProvider, useModuleLayout, ModulePage, collectFilters, AnalyticsControls, AnalyticsGrid, Card, FilterChip, DataTable, Button, SubpageHeader, useAnalyticsCard, useAnalyticsContext, AnalyticsCard, Dropdown, DropdownTrigger, DropdownContent, DropdownItem } from '@fayz-ai/ui';
6
7
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
8
  import { BarChart3, User, Search, ArrowLeft, Printer, Construction, Download, Radio, ChevronLeft, ChevronRight } from 'lucide-react';
8
9
 
@@ -59,10 +60,10 @@ function matchesFilters(row, filters) {
59
60
  function inRange(row, dateColumn, from, to) {
60
61
  const raw = row[dateColumn];
61
62
  if (raw == null) return true;
62
- const t2 = new Date(String(raw)).getTime();
63
- if (Number.isNaN(t2)) return true;
64
- if (from && t2 < (/* @__PURE__ */ new Date(`${from}T00:00:00`)).getTime()) return false;
65
- if (to && t2 > (/* @__PURE__ */ new Date(`${to}T23:59:59.999`)).getTime()) return false;
63
+ const t = new Date(String(raw)).getTime();
64
+ if (Number.isNaN(t)) return true;
65
+ if (from && t < (/* @__PURE__ */ new Date(`${from}T00:00:00`)).getTime()) return false;
66
+ if (to && t > (/* @__PURE__ */ new Date(`${to}T23:59:59.999`)).getTime()) return false;
66
67
  return true;
67
68
  }
68
69
  function applyMeasure(rows, agg, column) {
@@ -163,7 +164,7 @@ function reservedKeys(card) {
163
164
  ]);
164
165
  }
165
166
  function searchColumns(card) {
166
- const cols = (card.columns ?? []).filter((c) => c.type === "text" || c.type === "email").map((c) => c.key);
167
+ const cols = (card.columns ?? []).filter((c) => c.type === "text" || c.type === "email" || c.type === "person").map((c) => c.key);
167
168
  return cols.length ? cols : void 0;
168
169
  }
169
170
  function camelToSnake(s) {
@@ -204,6 +205,61 @@ async function selectFromView(card, state, db, tenantId, reserved) {
204
205
  total: count ?? 0
205
206
  };
206
207
  }
208
+ function toError(err) {
209
+ if (err instanceof Error) return err;
210
+ if (err && typeof err === "object") {
211
+ const e = err;
212
+ const message = [...new Set(
213
+ [e.message, e.details, e.hint].filter(
214
+ (part) => typeof part === "string" && part.trim().length > 0
215
+ )
216
+ )].join(" \u2014 ");
217
+ if (message) return Object.assign(new Error(message), e.code ? { code: e.code } : {});
218
+ }
219
+ return new Error(String(err));
220
+ }
221
+ function isAggregateCard(card) {
222
+ const { source } = card.query;
223
+ if (source.kind !== "view" || !source.name) return false;
224
+ return (card.query.dimensions?.length ?? 0) > 0 || (card.query.measures?.length ?? 0) > 0;
225
+ }
226
+ function runArgs(card, state, tenantId) {
227
+ const size = state.pageSize || 50;
228
+ const snapshot = card.query.dateColumn === null;
229
+ return {
230
+ source: card.query.source.name,
231
+ tenant_id: tenantId ?? null,
232
+ from: !snapshot && state.dateRange.from ? `${state.dateRange.from}T00:00:00` : null,
233
+ // Inclusive end-of-day: the range picker speaks in days, the column is a
234
+ // timestamp, and closing at midnight loses everything sold that day.
235
+ to: !snapshot && state.dateRange.to ? `${state.dateRange.to}T23:59:59.999` : null,
236
+ dimensions: (card.query.dimensions ?? []).map((d) => ({
237
+ key: d.key,
238
+ ...d.grain && d.grain !== "auto" ? { grain: d.grain } : {}
239
+ })),
240
+ measures: (card.query.measures ?? []).filter((m) => m.agg !== "ratio").map((m) => ({ key: m.key, agg: m.agg, ...m.column ? { column: m.column } : {} })),
241
+ filters: state.filters ?? {},
242
+ search: state.search ?? null,
243
+ search_columns: searchColumns(card) ?? null,
244
+ sort: state.sortBy ?? card.query.defaultSort ?? null,
245
+ dir: state.sortDir ?? card.query.defaultSortDir ?? "desc",
246
+ limit: card.query.limit ?? size,
247
+ offset: card.query.limit ? 0 : ((state.page || 1) - 1) * size
248
+ };
249
+ }
250
+ function toResult(data, card, reserved) {
251
+ const rows = data?.rows ?? [];
252
+ return {
253
+ rows: rows.map((r) => mapRow(r, reserved, card.query.source.columnMap)),
254
+ total: data?.total ?? rows.length,
255
+ summary: data?.summary ?? void 0
256
+ };
257
+ }
258
+ function batchUnavailable(err) {
259
+ const code = typeof err?.code === "string" ? err.code : "";
260
+ if (["42883", "PGRST202"].includes(code)) return true;
261
+ return typeof err?.message === "string" && err.message.includes("analytics_run_batch");
262
+ }
207
263
  function createSupabaseAnalyticsProvider(config) {
208
264
  const resolveTenantId = () => (typeof config.tenantId === "function" ? config.tenantId() : config.tenantId) || void 0;
209
265
  function client() {
@@ -211,18 +267,19 @@ function createSupabaseAnalyticsProvider(config) {
211
267
  if (!supabase) throw new Error("Supabase client not available");
212
268
  return config.schema && config.schema !== "public" ? supabase.schema(config.schema) : supabase;
213
269
  }
270
+ let batchSupported = true;
214
271
  return {
215
272
  async run(card, state) {
216
273
  const { source } = card.query;
217
274
  const reserved = reservedKeys(card);
218
275
  if (source.kind === "fn") {
219
276
  if (!source.run) throw new Error(`analytics: card ${card.id} declares kind 'fn' with no run()`);
220
- const rows2 = await source.run({
277
+ const rows = await source.run({
221
278
  tenantId: resolveTenantId(),
222
279
  dateRange: state.dateRange,
223
280
  filters: state.filters
224
281
  });
225
- return aggregateRows(rows2, card, state);
282
+ return aggregateRows(rows, card, state);
226
283
  }
227
284
  if (!source.name) throw new Error(`analytics: card ${card.id} has no source name`);
228
285
  const isDetail = (card.query.dimensions?.length ?? 0) === 0 && (card.query.measures?.length ?? 0) === 0;
@@ -237,42 +294,80 @@ function createSupabaseAnalyticsProvider(config) {
237
294
  ...Object.fromEntries(Object.entries(state.filters ?? {}).map(([k, v]) => [`p_${k}`, v]))
238
295
  });
239
296
  if (error2) throw error2;
240
- const rows2 = Array.isArray(data2) ? data2 : data2?.rows ?? [];
297
+ const rows = Array.isArray(data2) ? data2 : data2?.rows ?? [];
241
298
  return {
242
- rows: rows2.map((r) => mapRow(r, reserved, source.columnMap)),
243
- total: Array.isArray(data2) ? rows2.length : data2?.total ?? rows2.length,
299
+ rows: rows.map((r) => mapRow(r, reserved, source.columnMap)),
300
+ total: Array.isArray(data2) ? rows.length : data2?.total ?? rows.length,
244
301
  summary: Array.isArray(data2) ? void 0 : data2?.summary
245
302
  };
246
303
  }
247
- const size = state.pageSize || 50;
248
- const snapshot = card.query.dateColumn === null;
304
+ const args = runArgs(card, state, resolveTenantId());
249
305
  const { data, error } = await client().rpc("analytics_run", {
250
- p_source: source.name,
251
- p_tenant_id: resolveTenantId() ?? null,
252
- p_from: !snapshot && state.dateRange.from ? `${state.dateRange.from}T00:00:00` : null,
253
- // Inclusive end-of-day: the range picker speaks in days, the column is
254
- // a timestamp, and closing at midnight loses everything sold that day.
255
- p_to: !snapshot && state.dateRange.to ? `${state.dateRange.to}T23:59:59.999` : null,
256
- p_dimensions: (card.query.dimensions ?? []).map((d) => ({
257
- key: d.key,
258
- ...d.grain && d.grain !== "auto" ? { grain: d.grain } : {}
259
- })),
260
- p_measures: (card.query.measures ?? []).filter((m) => m.agg !== "ratio").map((m) => ({ key: m.key, agg: m.agg, ...m.column ? { column: m.column } : {} })),
261
- p_filters: state.filters ?? {},
262
- p_search: state.search ?? null,
263
- p_search_columns: searchColumns(card) ?? null,
264
- p_sort: state.sortBy ?? card.query.defaultSort ?? null,
265
- p_dir: state.sortDir ?? card.query.defaultSortDir ?? "desc",
266
- p_limit: card.query.limit ?? size,
267
- p_offset: card.query.limit ? 0 : ((state.page || 1) - 1) * size
306
+ p_source: args.source,
307
+ p_tenant_id: args.tenant_id,
308
+ p_from: args.from,
309
+ p_to: args.to,
310
+ p_dimensions: args.dimensions,
311
+ p_measures: args.measures,
312
+ p_filters: args.filters,
313
+ p_search: args.search,
314
+ p_search_columns: args.search_columns,
315
+ p_sort: args.sort,
316
+ p_dir: args.dir,
317
+ p_limit: args.limit,
318
+ p_offset: args.offset
268
319
  });
269
320
  if (error) throw error;
270
- const rows = data?.rows ?? [];
271
- return {
272
- rows: rows.map((r) => mapRow(r, reserved, source.columnMap)),
273
- total: data?.total ?? rows.length,
274
- summary: data?.summary ?? void 0
275
- };
321
+ return toResult(data, card, reserved);
322
+ },
323
+ // -----------------------------------------------------------------------
324
+ // The batched path.
325
+ //
326
+ // Every aggregate card in the batch becomes ONE analytics_run_batch call;
327
+ // detail listings and 'fn' cards, which the batch function cannot express,
328
+ // still go one by one — but a dashboard is all aggregates, so opening
329
+ // /analytics went from forty-one requests to one.
330
+ //
331
+ // Results keep their INDEX. The caller matched a promise to a position, and
332
+ // returning a compacted array would silently hand card 7 card 9's numbers.
333
+ // -----------------------------------------------------------------------
334
+ async runMany(requests) {
335
+ const out = new Array(requests.length);
336
+ const batchable = [];
337
+ for (let i = 0; i < requests.length; i++) {
338
+ if (batchSupported && isAggregateCard(requests[i].card)) batchable.push(i);
339
+ }
340
+ const singles = requests.map((req, i) => ({ req, i })).filter(({ i }) => !batchable.includes(i)).map(({ req, i }) => this.run(req.card, req.state).then((r) => {
341
+ out[i] = r;
342
+ }).catch((e) => {
343
+ out[i] = toError(e);
344
+ }));
345
+ if (batchable.length > 0) {
346
+ const tenantId = resolveTenantId();
347
+ const payload = batchable.map((i) => runArgs(requests[i].card, requests[i].state, tenantId));
348
+ const { data, error } = await client().rpc("analytics_run_batch", { p_requests: payload });
349
+ if (error && batchUnavailable(error)) {
350
+ batchSupported = false;
351
+ await Promise.all([
352
+ ...singles,
353
+ ...batchable.map((i) => this.run(requests[i].card, requests[i].state).then((r) => {
354
+ out[i] = r;
355
+ }).catch((e) => {
356
+ out[i] = toError(e);
357
+ }))
358
+ ]);
359
+ return out;
360
+ }
361
+ if (error) throw error;
362
+ const results = Array.isArray(data) ? data : [];
363
+ batchable.forEach((cardIndex, slot) => {
364
+ const entry = results[slot];
365
+ const { card } = requests[cardIndex];
366
+ out[cardIndex] = entry?.error ? Object.assign(toError(entry.error), { code: entry.code }) : toResult(entry, card, reservedKeys(card));
367
+ });
368
+ }
369
+ await Promise.all(singles);
370
+ return out;
276
371
  }
277
372
  };
278
373
  }
@@ -286,10 +381,10 @@ function seeded(seed) {
286
381
  }
287
382
  return () => {
288
383
  h += 1831565813;
289
- let t2 = h;
290
- t2 = Math.imul(t2 ^ t2 >>> 15, t2 | 1);
291
- t2 ^= t2 + Math.imul(t2 ^ t2 >>> 7, t2 | 61);
292
- return ((t2 ^ t2 >>> 14) >>> 0) / 4294967296;
384
+ let t = h;
385
+ t = Math.imul(t ^ t >>> 15, t | 1);
386
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
387
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
293
388
  };
294
389
  }
295
390
  var SAMPLE_LABELS = {
@@ -338,21 +433,37 @@ function createMockAnalyticsProvider() {
338
433
  return row;
339
434
  });
340
435
  return aggregateRows(rows, card, state);
436
+ },
437
+ // There is no transport to save a round trip on, so this is just the loop.
438
+ // It exists because the mock is reached through createSafeDataProvider,
439
+ // whose Proxy answers EVERY property with a function — `provider.runMany`
440
+ // is truthy even for a provider that never declared it, and the caller's
441
+ // feature check therefore cannot detect its absence. Implementing it is how
442
+ // the mock stops being a landmine behind that Proxy.
443
+ async runMany(requests) {
444
+ return Promise.all(requests.map((r) => this.run(r.card, r.state).catch((e) => e instanceof Error ? e : new Error(String(e)))));
341
445
  }
342
446
  };
343
447
  }
344
-
345
- // src/lib/tenant.ts
346
- var t;
448
+ var override;
449
+ function setReportsTenantId(id) {
450
+ override = id;
451
+ }
347
452
  function getReportsTenantId() {
348
- return t;
453
+ if (override) return override;
454
+ try {
455
+ return useOrganizationStore.getState().currentOrg?.id;
456
+ } catch {
457
+ return void 0;
458
+ }
349
459
  }
350
460
 
351
461
  // src/locales/en.ts
352
462
  var en = {
353
463
  "reports.pageTitle": "Reports",
354
464
  "reports.pageSubtitle": "Access complete reports for analysis and decision making",
355
- "reports.allReports": "All Reports",
465
+ "reports.overview": "Overview",
466
+ "reports.allReports": "All reports",
356
467
  "reports.dateRange": "Date Range",
357
468
  "reports.from": "From",
358
469
  "reports.to": "To",
@@ -374,6 +485,17 @@ var en = {
374
485
  "reports.generate": "Generate",
375
486
  "reports.emptyStateTitle": "Ready to generate",
376
487
  "reports.emptyStateDescription": "Select a date range and filters, then click Generate to view the report.",
488
+ // The assistant's invitation on /analytics. It opens on a question, not on a
489
+ // grid: the numbers are already downstairs, and what the owner actually wants
490
+ // is to know what they mean.
491
+ "reports.assistant.persona": "Growth Analyst",
492
+ "reports.assistant.headline": "Hi {{name}}, what would you like to learn today?",
493
+ "reports.assistant.pitch": "Let's find the opportunities hiding in your business",
494
+ "reports.assistant.description": "Tell me your goals and I'll help build a plan to hit them. I read the same data these reports do, and I'll point at what changed and what to do about it.",
495
+ "reports.assistant.placeholder": "Build a custom report...",
496
+ "reports.assistant.suggestion.0": "How is my business doing this month?",
497
+ "reports.assistant.suggestion.1": "What's growing and what's falling?",
498
+ "reports.assistant.suggestion.2": "Where am I losing money?",
377
499
  "reports.unavailableTitle": "Coming soon",
378
500
  "reports.unavailableDescription": "This report is not available yet. It will be enabled in a future update."
379
501
  };
@@ -382,7 +504,8 @@ var en = {
382
504
  var ptBR = {
383
505
  "reports.pageTitle": "Relat\xF3rios",
384
506
  "reports.pageSubtitle": "Acesse relat\xF3rios completos para an\xE1lise e tomada de decis\xE3o",
385
- "reports.allReports": "Todos os Relat\xF3rios",
507
+ "reports.overview": "Vis\xE3o geral",
508
+ "reports.allReports": "Todos os relat\xF3rios",
386
509
  "reports.dateRange": "Per\xEDodo",
387
510
  "reports.from": "De",
388
511
  "reports.to": "At\xE9",
@@ -404,6 +527,14 @@ var ptBR = {
404
527
  "reports.generate": "Gerar",
405
528
  "reports.emptyStateTitle": "Pronto para gerar",
406
529
  "reports.emptyStateDescription": "Selecione o per\xEDodo e os filtros desejados, depois clique em Gerar para visualizar o relat\xF3rio.",
530
+ "reports.assistant.persona": "Analista de Crescimento",
531
+ "reports.assistant.headline": "Oi {{name}}, o que voc\xEA quer entender hoje?",
532
+ "reports.assistant.pitch": "Vamos achar as oportunidades escondidas no seu neg\xF3cio",
533
+ "reports.assistant.description": "Me conte sua meta e eu ajudo a montar o plano para bater. Leio os mesmos dados destes relat\xF3rios e aponto o que mudou e o que fazer a respeito.",
534
+ "reports.assistant.placeholder": "Pe\xE7a um relat\xF3rio do seu jeito...",
535
+ "reports.assistant.suggestion.0": "Como est\xE1 meu neg\xF3cio neste m\xEAs?",
536
+ "reports.assistant.suggestion.1": "O que est\xE1 crescendo e o que est\xE1 caindo?",
537
+ "reports.assistant.suggestion.2": "Onde estou perdendo dinheiro?",
407
538
  "reports.unavailableTitle": "Em breve",
408
539
  "reports.unavailableDescription": "Este relat\xF3rio ainda n\xE3o est\xE1 dispon\xEDvel. Ser\xE1 habilitado em uma atualiza\xE7\xE3o futura."
409
540
  };
@@ -459,6 +590,7 @@ function reportDefToCard(report, createdBy) {
459
590
  sortable: c.sortable,
460
591
  currency: c.currency,
461
592
  idKey: c.idKey,
593
+ personKind: c.personKind,
462
594
  renderCell: c.renderCell,
463
595
  visible: c.visible,
464
596
  align: c.align,
@@ -493,8 +625,8 @@ function markViewed(cardId) {
493
625
  for (const listener of listeners) listener(next);
494
626
  }
495
627
  function useLastViewed() {
496
- const [map, setMap] = React4.useState(() => read());
497
- React4.useEffect(() => {
628
+ const [map, setMap] = React6.useState(() => read());
629
+ React6.useEffect(() => {
498
630
  listeners.add(setMap);
499
631
  return () => {
500
632
  listeners.delete(setMap);
@@ -504,16 +636,16 @@ function useLastViewed() {
504
636
  }
505
637
  var ANALYTICS_BASE = "/analytics";
506
638
  var REPORTS_BASE = `${ANALYTICS_BASE}/reports`;
507
- var ConfigContext = React4__default.createContext(null);
639
+ var ConfigContext = React6__default.createContext(null);
508
640
  function useAnalyticsConfig() {
509
- const ctx = React4__default.useContext(ConfigContext);
641
+ const ctx = React6__default.useContext(ConfigContext);
510
642
  if (!ctx) throw new Error("useAnalyticsConfig must be used inside <AnalyticsSurface>");
511
643
  return ctx;
512
644
  }
513
645
  function useAnalyticsCards() {
514
646
  const runtime = usePluginRuntime();
515
647
  const { cards, createdBy } = useAnalyticsConfig();
516
- return React4__default.useMemo(() => {
648
+ return React6__default.useMemo(() => {
517
649
  const byId = /* @__PURE__ */ new Map();
518
650
  for (const card of getAnalyticsCards(runtime)) byId.set(card.id, card);
519
651
  for (const card of cards) byId.set(card.id, card);
@@ -522,7 +654,7 @@ function useAnalyticsCards() {
522
654
  }
523
655
  function useOverviewCards() {
524
656
  const all = useAnalyticsCards();
525
- return React4__default.useMemo(
657
+ return React6__default.useMemo(
526
658
  () => all.filter((c) => c.dashboard && c.dashboard.defaultVisible !== false).sort((a, b) => (a.dashboard?.order ?? 0) - (b.dashboard?.order ?? 0)),
527
659
  [all]
528
660
  );
@@ -530,10 +662,14 @@ function useOverviewCards() {
530
662
  function cardColumns(card) {
531
663
  if (card.columns?.length) return card.columns;
532
664
  return [
533
- ...(card.query.dimensions ?? []).map((d) => ({
665
+ // A hidden dimension is group-by only — the id a person column reads from,
666
+ // never a column of UUIDs next to the name it belongs to.
667
+ ...(card.query.dimensions ?? []).filter((d) => !d.hidden).map((d) => ({
534
668
  key: d.key,
535
669
  label: d.label,
536
- type: d.format ?? (d.grain ? "date" : "text"),
670
+ type: d.person ? "person" : d.format ?? (d.grain ? "date" : "text"),
671
+ idKey: d.person?.idKey,
672
+ personKind: d.person?.kind,
537
673
  align: "left"
538
674
  })),
539
675
  ...(card.query.measures ?? []).map((m) => ({
@@ -549,7 +685,7 @@ function navigateAnalytics(path) {
549
685
  hashRouterAdapter().navigate(path);
550
686
  }
551
687
  function AnalyticsSurface({ config, provider, children }) {
552
- const openCard = React4__default.useCallback((card) => {
688
+ const openCard = React6__default.useCallback((card) => {
553
689
  markViewed(card.id);
554
690
  navigateAnalytics(`${REPORTS_BASE}/${encodeURIComponent(card.id)}`);
555
691
  }, []);
@@ -567,22 +703,148 @@ function AnalyticsSurface({ config, provider, children }) {
567
703
  }
568
704
  ) });
569
705
  }
706
+ function AnalyticsAssistant() {
707
+ const { assistant } = useAnalyticsConfig();
708
+ const t = useTranslation();
709
+ const suggestions = React6__default.useMemo(
710
+ () => assistant?.suggestions ?? [0, 1, 2].map((i) => ({ label: t(`reports.assistant.suggestion.${i}`) })),
711
+ [assistant?.suggestions, t]
712
+ );
713
+ if (!assistant) return null;
714
+ return /* @__PURE__ */ jsx(
715
+ AssistantInvite,
716
+ {
717
+ id: "analytics",
718
+ auto: assistant.auto ?? false,
719
+ persona: assistant.persona ?? t("reports.assistant.persona"),
720
+ headline: assistant.headline ?? t("reports.assistant.headline"),
721
+ pitch: assistant.pitch ?? t("reports.assistant.pitch"),
722
+ description: assistant.description ?? t("reports.assistant.description"),
723
+ placeholder: assistant.placeholder ?? t("reports.assistant.placeholder"),
724
+ suggestions
725
+ }
726
+ );
727
+ }
728
+ var reportRoute = (cardId) => `${REPORTS_BASE}/${encodeURIComponent(cardId)}`;
729
+ function groupCards(cards) {
730
+ const groups = /* @__PURE__ */ new Map();
731
+ for (const card of cards) {
732
+ const list = groups.get(card.category);
733
+ if (list) list.push(card);
734
+ else groups.set(card.category, [card]);
735
+ }
736
+ return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([category, list]) => [category, [...list].sort((a, b) => a.name.localeCompare(b.name))]);
737
+ }
738
+ function useAnalyticsTree() {
739
+ const t = useTranslation();
740
+ const { labels, showLiveView } = useAnalyticsConfig();
741
+ const cards = useAnalyticsCards();
742
+ return React6__default.useMemo(() => ({
743
+ title: labels.analytics,
744
+ // Only the overview. The searchable catalogue (/analytics/reports) is still
745
+ // routed — bookmarks and the legacy /reports link resolve — but it is not
746
+ // listed: the tree below already IS the catalogue, and a link named "all
747
+ // reports" sitting above the reports reads as a different, fuller list.
748
+ fixed: [
749
+ { id: "overview", label: labels.overview ?? t("reports.overview"), icon: "BarChart3", route: ANALYTICS_BASE, view: "overview" }
750
+ ],
751
+ groups: groupCards(cards),
752
+ live: showLiveView ? { id: "live", label: labels.liveView, icon: "Radio", route: `${ANALYTICS_BASE}/live`, view: "live" } : null
753
+ }), [labels, showLiveView, cards, t]);
754
+ }
755
+ function AnalyticsModule({ view, cardId, children }) {
756
+ const variant = useModuleLayout();
757
+ const tree = useAnalyticsTree();
758
+ const activeId = cardId ? decodeURIComponent(cardId) : void 0;
759
+ const shellRail = useShellRailAvailable();
760
+ const claimsRail = shellRail ?? variant !== "rail";
761
+ const entries = React6__default.useMemo(() => [
762
+ ...tree.fixed.map((f) => ({
763
+ id: `analytics:${f.id}`,
764
+ label: f.label,
765
+ icon: f.icon,
766
+ route: f.route,
767
+ section: "main"
768
+ })),
769
+ ...tree.groups.flatMap(([category, list]) => list.map((card) => ({
770
+ id: `analytics:${card.id}`,
771
+ label: card.name,
772
+ icon: card.icon ?? "FileText",
773
+ route: reportRoute(card.id),
774
+ section: "main",
775
+ // Consecutive items sharing a group render under one heading — the same
776
+ // mechanism that heads the settings rail's plugin runs.
777
+ group: category
778
+ }))),
779
+ ...tree.live ? [{
780
+ id: "analytics:live",
781
+ label: tree.live.label,
782
+ icon: tree.live.icon,
783
+ route: tree.live.route,
784
+ section: "main"
785
+ }] : []
786
+ ], [tree]);
787
+ useContextualNav(claimsRail ? { base: ANALYTICS_BASE, entries, label: tree.title } : null);
788
+ const nav = React6__default.useMemo(() => {
789
+ if (claimsRail) return [];
790
+ return [
791
+ ...tree.fixed.map((f) => ({
792
+ id: f.id,
793
+ label: f.label,
794
+ icon: f.icon,
795
+ active: view === f.view,
796
+ onClick: () => navigateAnalytics(f.route)
797
+ })),
798
+ ...tree.groups.map(([category, list]) => ({
799
+ id: `group:${category}`,
800
+ label: category,
801
+ children: list.map((card) => ({
802
+ id: card.id,
803
+ label: card.name,
804
+ active: view === "report" && card.id === activeId,
805
+ onClick: () => navigateAnalytics(reportRoute(card.id))
806
+ }))
807
+ })),
808
+ ...tree.live ? [{
809
+ id: tree.live.id,
810
+ label: tree.live.label,
811
+ icon: tree.live.icon,
812
+ active: view === "live",
813
+ onClick: () => navigateAnalytics(tree.live.route)
814
+ }] : []
815
+ ];
816
+ }, [tree, view, activeId, claimsRail]);
817
+ return /* @__PURE__ */ jsxs(
818
+ ModulePage,
819
+ {
820
+ title: tree.title,
821
+ nav,
822
+ navVariant: claimsRail ? "sidebar" : "rail",
823
+ showHeader: false,
824
+ viewKey: view === "report" ? `report:${activeId}` : view,
825
+ children: [
826
+ /* @__PURE__ */ jsx(AnalyticsAssistant, {}),
827
+ /* @__PURE__ */ jsx("div", { className: "md:pb-16", children })
828
+ ]
829
+ }
830
+ );
831
+ }
570
832
  function AnalyticsOverview() {
571
833
  const { labels, showHeader } = useAnalyticsConfig();
572
834
  const cards = useOverviewCards();
573
- const filters = React4__default.useMemo(() => collectFilters(cards), [cards]);
574
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
835
+ const filters = React6__default.useMemo(() => collectFilters(cards), [cards]);
836
+ return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
575
837
  showHeader ? /* @__PURE__ */ jsxs("div", { children: [
576
838
  /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold tracking-tight", children: labels.analytics }),
577
839
  /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: labels.overviewSubtitle })
578
840
  ] }) : null,
579
- /* @__PURE__ */ jsx(AnalyticsControls, { filters, className: "sticky top-0 z-10 -mx-1 bg-background/95 px-1 py-2 backdrop-blur" }),
841
+ /* @__PURE__ */ jsx(AnalyticsControls, { filters, className: "sticky top-3 z-20" }),
580
842
  cards.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center rounded-xl border border-dashed py-16 text-center", children: [
581
843
  /* @__PURE__ */ jsx("div", { className: "mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: /* @__PURE__ */ jsx(BarChart3, { className: "h-5 w-5 text-muted-foreground" }) }),
582
844
  /* @__PURE__ */ jsx("p", { className: "font-medium", children: labels.emptyOverviewTitle }),
583
845
  /* @__PURE__ */ jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: labels.emptyOverviewDescription })
584
846
  ] }) : /* @__PURE__ */ jsx(AnalyticsGrid, { cards })
585
- ] });
847
+ ] }) });
586
848
  }
587
849
  function formatViewed(iso, locale, never) {
588
850
  if (!iso) return never;
@@ -594,18 +856,18 @@ function ReportsListPage() {
594
856
  const { labels, currency, showHeader } = useAnalyticsConfig();
595
857
  const cards = useAnalyticsCards();
596
858
  const lastViewed = useLastViewed();
597
- const [search, setSearch] = React4__default.useState("");
598
- const [category, setCategory] = React4__default.useState();
599
- const [author, setAuthor] = React4__default.useState();
600
- const categoryOptions = React4__default.useMemo(
859
+ const [search, setSearch] = React6__default.useState("");
860
+ const [category, setCategory] = React6__default.useState();
861
+ const [author, setAuthor] = React6__default.useState();
862
+ const categoryOptions = React6__default.useMemo(
601
863
  () => [...new Set(cards.map((c) => c.category))].sort((a, b) => a.localeCompare(b)).map((c) => ({ value: c, label: c })),
602
864
  [cards]
603
865
  );
604
- const authorOptions = React4__default.useMemo(
866
+ const authorOptions = React6__default.useMemo(
605
867
  () => [...new Set(cards.map((c) => c.createdBy ?? "\u2014"))].sort((a, b) => a.localeCompare(b)).map((a) => ({ value: a, label: a })),
606
868
  [cards]
607
869
  );
608
- const rows = React4__default.useMemo(() => {
870
+ const rows = React6__default.useMemo(() => {
609
871
  const q = search.trim().toLowerCase();
610
872
  return cards.filter((card) => {
611
873
  if (category && card.category !== category) return false;
@@ -614,7 +876,7 @@ function ReportsListPage() {
614
876
  return card.name.toLowerCase().includes(q) || card.description?.toLowerCase().includes(q) || card.category.toLowerCase().includes(q);
615
877
  });
616
878
  }, [cards, search, category, author]);
617
- const columns = React4__default.useMemo(() => [
879
+ const columns = React6__default.useMemo(() => [
618
880
  {
619
881
  id: "name",
620
882
  accessorFn: (card) => card.name,
@@ -730,33 +992,33 @@ function exportToCsv(columns, data, filename, currency) {
730
992
  exportCSV(csvColumns, data, filename);
731
993
  }
732
994
  function ExportDropdown({ columns, data, filename, currency, title }) {
733
- const t2 = useTranslation();
995
+ const t = useTranslation();
734
996
  const [excelAvailable, setExcelAvailable] = useState(false);
735
997
  const [pdfAvailable, setPdfAvailable] = useState(false);
736
998
  useEffect(() => {
737
- import('./excel-WE73YE2V.js').then((m) => m.isExcelAvailable()).then(setExcelAvailable).catch(() => {
999
+ import('./excel-KIJTC7OJ.js').then((m) => m.isExcelAvailable()).then(setExcelAvailable).catch(() => {
738
1000
  });
739
- import('./pdf-E6DXHFXU.js').then((m) => m.isPdfAvailable()).then(setPdfAvailable).catch(() => {
1001
+ import('./pdf-7QQ3U5JQ.js').then((m) => m.isPdfAvailable()).then(setPdfAvailable).catch(() => {
740
1002
  });
741
1003
  }, []);
742
1004
  const handleCsv = () => exportToCsv(columns, data, filename, currency);
743
1005
  const handleExcel = async () => {
744
- const { exportToExcel } = await import('./excel-WE73YE2V.js');
1006
+ const { exportToExcel } = await import('./excel-KIJTC7OJ.js');
745
1007
  await exportToExcel(columns, data, filename, currency);
746
1008
  };
747
1009
  const handlePdf = async () => {
748
- const { exportToPdf } = await import('./pdf-E6DXHFXU.js');
1010
+ const { exportToPdf } = await import('./pdf-7QQ3U5JQ.js');
749
1011
  await exportToPdf(columns, data, filename, { title, currency });
750
1012
  };
751
1013
  return /* @__PURE__ */ jsxs(Dropdown, { children: [
752
1014
  /* @__PURE__ */ jsx(DropdownTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", children: [
753
1015
  /* @__PURE__ */ jsx(Download, { className: "h-4 w-4 mr-1.5" }),
754
- t2("reports.export")
1016
+ t("reports.export")
755
1017
  ] }) }),
756
1018
  /* @__PURE__ */ jsxs(DropdownContent, { align: "end", children: [
757
- /* @__PURE__ */ jsx(DropdownItem, { onClick: handleCsv, children: t2("reports.exportCsv") }),
758
- excelAvailable && /* @__PURE__ */ jsx(DropdownItem, { onClick: handleExcel, children: t2("reports.exportExcel") }),
759
- pdfAvailable && /* @__PURE__ */ jsx(DropdownItem, { onClick: handlePdf, children: t2("reports.exportPdf") })
1019
+ /* @__PURE__ */ jsx(DropdownItem, { onClick: handleCsv, children: t("reports.exportCsv") }),
1020
+ excelAvailable && /* @__PURE__ */ jsx(DropdownItem, { onClick: handleExcel, children: t("reports.exportExcel") }),
1021
+ pdfAvailable && /* @__PURE__ */ jsx(DropdownItem, { onClick: handlePdf, children: t("reports.exportPdf") })
760
1022
  ] })
761
1023
  ] });
762
1024
  }
@@ -813,8 +1075,8 @@ function ViewerBody({ card }) {
813
1075
  const { labels, currency } = useAnalyticsConfig();
814
1076
  const { state, setPage, setPageSize } = useAnalyticsContext();
815
1077
  const { result, loading, error, engineMissing } = useAnalyticsCard(card);
816
- const columns = React4__default.useMemo(() => cardColumns(card), [card]);
817
- const tanstackColumns = React4__default.useMemo(
1078
+ const columns = React6__default.useMemo(() => cardColumns(card), [card]);
1079
+ const tanstackColumns = React6__default.useMemo(
818
1080
  () => reportColumnsToTanstack(columns, currency.code),
819
1081
  [columns, currency.code]
820
1082
  );
@@ -871,7 +1133,7 @@ function AnalyticsReportViewer({ cardId }) {
871
1133
  const { labels } = useAnalyticsConfig();
872
1134
  const cards = useAnalyticsCards();
873
1135
  const card = cards.find((c) => c.id === decodeURIComponent(cardId ?? ""));
874
- React4__default.useEffect(() => {
1136
+ React6__default.useEffect(() => {
875
1137
  if (card) markViewed(card.id);
876
1138
  }, [card?.id]);
877
1139
  if (!card) {
@@ -888,8 +1150,8 @@ function AnalyticsReportViewer({ cardId }) {
888
1150
  SubpageHeader,
889
1151
  {
890
1152
  title: card.name,
891
- parentLabel: labels.reports,
892
- onBack: () => navigateAnalytics(REPORTS_BASE)
1153
+ parentLabel: labels.analytics,
1154
+ onBack: () => navigateAnalytics(ANALYTICS_BASE)
893
1155
  }
894
1156
  ),
895
1157
  card.description ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: card.description }) : null,
@@ -908,7 +1170,7 @@ function AnalyticsReportViewer({ cardId }) {
908
1170
  function ViewerActions({ card }) {
909
1171
  const { currency } = useAnalyticsConfig();
910
1172
  const { result } = useAnalyticsCard(card);
911
- const columns = React4__default.useMemo(() => cardColumns(card), [card]);
1173
+ const columns = React6__default.useMemo(() => cardColumns(card), [card]);
912
1174
  if (!result?.rows?.length) return null;
913
1175
  return /* @__PURE__ */ jsxs(Fragment, { children: [
914
1176
  /* @__PURE__ */ jsx(
@@ -996,7 +1258,7 @@ async function fetchViaView(reportDef, query, getClient, resolveTenantId, tenant
996
1258
  }
997
1259
  }
998
1260
  if (query.search) {
999
- const searchableCols = reportDef.columns.filter((c) => c.type === "text" || c.type === "email").map((c) => camelToSnake2(c.key));
1261
+ const searchableCols = reportDef.columns.filter((c) => c.type === "text" || c.type === "email" || c.type === "person").map((c) => camelToSnake2(c.key));
1000
1262
  if (searchableCols.length > 0) {
1001
1263
  const term = `%${query.search}%`;
1002
1264
  const orClause = searchableCols.map((col) => `${col}.ilike.${term}`).join(",");
@@ -1099,7 +1361,9 @@ function resolveConfig(options) {
1099
1361
  defaultPageSize: options.defaultPageSize ?? 50,
1100
1362
  showHeader: options.showHeader === true,
1101
1363
  defaultRange: options.defaultRange ?? "last_30_days",
1102
- defaultCompare: options.defaultCompare ?? "previous_period"
1364
+ defaultCompare: options.defaultCompare ?? "previous_period",
1365
+ showLiveView: options.showLiveView === true,
1366
+ assistant: options.assistant === false ? null : options.assistant ?? {}
1103
1367
  };
1104
1368
  }
1105
1369
  function createAnalyticsPlugin(options = {}) {
@@ -1109,10 +1373,14 @@ function createAnalyticsPlugin(options = {}) {
1109
1373
  () => createSupabaseAnalyticsProvider({ tenantId: () => getReportsTenantId() }),
1110
1374
  () => createMockAnalyticsProvider()
1111
1375
  );
1112
- const page2 = (Body) => (props) => React4__default.createElement(
1376
+ const page2 = (Body, view) => (props) => React6__default.createElement(
1113
1377
  AnalyticsSurface,
1114
1378
  { config, provider },
1115
- React4__default.createElement(Body, props)
1379
+ React6__default.createElement(
1380
+ AnalyticsModule,
1381
+ { view, cardId: props?.cardId },
1382
+ React6__default.createElement(Body, props)
1383
+ )
1116
1384
  );
1117
1385
  const READ = { feature: "reports", action: "read" };
1118
1386
  const position = options.navPosition ?? 9;
@@ -1125,23 +1393,7 @@ function createAnalyticsPlugin(options = {}) {
1125
1393
  route: ANALYTICS_BASE,
1126
1394
  icon: "BarChart3",
1127
1395
  permission: READ
1128
- },
1129
- {
1130
- section,
1131
- position: position + 0.1,
1132
- label: config.labels.reports,
1133
- route: REPORTS_BASE,
1134
- icon: "FileText",
1135
- permission: READ
1136
- },
1137
- ...options.showLiveView ? [{
1138
- section,
1139
- position: position + 0.2,
1140
- label: config.labels.liveView,
1141
- route: `${ANALYTICS_BASE}/live`,
1142
- icon: "Radio",
1143
- permission: READ
1144
- }] : []
1396
+ }
1145
1397
  ];
1146
1398
  return {
1147
1399
  id: "reports",
@@ -1157,8 +1409,8 @@ function createAnalyticsPlugin(options = {}) {
1157
1409
  ],
1158
1410
  navigation: nav,
1159
1411
  routes: [
1160
- { path: ANALYTICS_BASE, component: page2(AnalyticsOverview), permission: READ },
1161
- { path: REPORTS_BASE, component: page2(ReportsListPage), permission: READ },
1412
+ { path: ANALYTICS_BASE, component: page2(AnalyticsOverview, "overview"), permission: READ },
1413
+ { path: REPORTS_BASE, component: page2(ReportsListPage, "reports"), permission: READ },
1162
1414
  // Registered before the list would ever shadow it: matchRoute scores
1163
1415
  // static segments above params, so /analytics/reports still wins its own
1164
1416
  // exact path and only /analytics/reports/<id> lands here.
@@ -1166,14 +1418,17 @@ function createAnalyticsPlugin(options = {}) {
1166
1418
  // (the card's name), so it claims the shell bar itself via PageHeader.
1167
1419
  // Declaring both gave the page two stacked back-crumbs saying the same
1168
1420
  // thing, one of them generic.
1169
- { path: `${REPORTS_BASE}/:cardId`, component: page2(AnalyticsReportViewer), permission: READ },
1170
- { path: `${ANALYTICS_BASE}/live`, component: page2(LiveViewPage), permission: READ },
1421
+ { path: `${REPORTS_BASE}/:cardId`, component: page2(AnalyticsReportViewer, "report"), permission: READ },
1422
+ { path: `${ANALYTICS_BASE}/live`, component: page2(LiveViewPage, "live"), permission: READ },
1171
1423
  // The old top-level route kept as a redirect target: bookmarks, saved
1172
1424
  // links and the five apps still pointing at /reports must not 404.
1173
- { path: "/reports", component: page2(ReportsListPage), permission: READ }
1425
+ { path: "/reports", component: page2(ReportsListPage, "reports"), permission: READ }
1174
1426
  ],
1175
1427
  widgets: [],
1176
1428
  registries: [],
1429
+ // Measurement, not acquisition — what the Fayz plugin page lists for
1430
+ // Analytics.
1431
+ integrations: ["google-analytics", "hotjar"],
1177
1432
  locales: reportsLocales
1178
1433
  };
1179
1434
  }
@@ -1194,6 +1449,6 @@ function createReportsPlugin(options) {
1194
1449
  });
1195
1450
  }
1196
1451
 
1197
- export { ANALYTICS_BASE, AnalyticsOverview, AnalyticsReportViewer, AnalyticsSurface, REPORTS_BASE, ReportsListPage, aggregateRows, cardColumns, createAnalyticsPlugin, createMockAnalyticsProvider, createMockReportProvider, createReportsPlugin, createSupabaseAnalyticsProvider, createSupabaseReportProvider, markViewed, reportDefToCard, reportDefsToCards, useAnalyticsCards, useAnalyticsConfig, useLastViewed, useOverviewCards };
1452
+ export { ANALYTICS_BASE, AnalyticsModule, AnalyticsOverview, AnalyticsReportViewer, AnalyticsSurface, REPORTS_BASE, ReportsListPage, aggregateRows, cardColumns, createAnalyticsPlugin, createMockAnalyticsProvider, createMockReportProvider, createReportsPlugin, createSupabaseAnalyticsProvider, createSupabaseReportProvider, getReportsTenantId, markViewed, reportDefToCard, reportDefsToCards, setReportsTenantId, useAnalyticsCards, useAnalyticsConfig, useLastViewed, useOverviewCards };
1198
1453
  //# sourceMappingURL=index.js.map
1199
1454
  //# sourceMappingURL=index.js.map