@fayz-ai/plugin-reports 0.9.1 → 0.10.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.
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { reportColumnsToTanstack, formatValuePlain } from './chunk-2ZFN6ECM.js';
2
- import * as React4 from 'react';
3
- import React4__default, { useState, useEffect } from 'react';
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';
2
+ import * as React5 from 'react';
3
+ import React5__default, { useState, useEffect } from 'react';
4
+ import { usePluginRuntime, getAnalyticsCards, useTranslation, registerTranslations, createSafeDataProvider, getSupabaseClientOptional, hashRouterAdapter, exportCSV } from '@fayz-ai/core';
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) {
@@ -204,6 +205,48 @@ async function selectFromView(card, state, db, tenantId, reserved) {
204
205
  total: count ?? 0
205
206
  };
206
207
  }
208
+ function isAggregateCard(card) {
209
+ const { source } = card.query;
210
+ if (source.kind !== "view" || !source.name) return false;
211
+ return (card.query.dimensions?.length ?? 0) > 0 || (card.query.measures?.length ?? 0) > 0;
212
+ }
213
+ function runArgs(card, state, tenantId) {
214
+ const size = state.pageSize || 50;
215
+ const snapshot = card.query.dateColumn === null;
216
+ return {
217
+ source: card.query.source.name,
218
+ tenant_id: tenantId ?? null,
219
+ from: !snapshot && state.dateRange.from ? `${state.dateRange.from}T00:00:00` : null,
220
+ // Inclusive end-of-day: the range picker speaks in days, the column is a
221
+ // timestamp, and closing at midnight loses everything sold that day.
222
+ to: !snapshot && state.dateRange.to ? `${state.dateRange.to}T23:59:59.999` : null,
223
+ dimensions: (card.query.dimensions ?? []).map((d) => ({
224
+ key: d.key,
225
+ ...d.grain && d.grain !== "auto" ? { grain: d.grain } : {}
226
+ })),
227
+ measures: (card.query.measures ?? []).filter((m) => m.agg !== "ratio").map((m) => ({ key: m.key, agg: m.agg, ...m.column ? { column: m.column } : {} })),
228
+ filters: state.filters ?? {},
229
+ search: state.search ?? null,
230
+ search_columns: searchColumns(card) ?? null,
231
+ sort: state.sortBy ?? card.query.defaultSort ?? null,
232
+ dir: state.sortDir ?? card.query.defaultSortDir ?? "desc",
233
+ limit: card.query.limit ?? size,
234
+ offset: card.query.limit ? 0 : ((state.page || 1) - 1) * size
235
+ };
236
+ }
237
+ function toResult(data, card, reserved) {
238
+ const rows = data?.rows ?? [];
239
+ return {
240
+ rows: rows.map((r) => mapRow(r, reserved, card.query.source.columnMap)),
241
+ total: data?.total ?? rows.length,
242
+ summary: data?.summary ?? void 0
243
+ };
244
+ }
245
+ function batchUnavailable(err) {
246
+ const code = typeof err?.code === "string" ? err.code : "";
247
+ if (["42883", "PGRST202"].includes(code)) return true;
248
+ return typeof err?.message === "string" && err.message.includes("analytics_run_batch");
249
+ }
207
250
  function createSupabaseAnalyticsProvider(config) {
208
251
  const resolveTenantId = () => (typeof config.tenantId === "function" ? config.tenantId() : config.tenantId) || void 0;
209
252
  function client() {
@@ -211,18 +254,19 @@ function createSupabaseAnalyticsProvider(config) {
211
254
  if (!supabase) throw new Error("Supabase client not available");
212
255
  return config.schema && config.schema !== "public" ? supabase.schema(config.schema) : supabase;
213
256
  }
257
+ let batchSupported = true;
214
258
  return {
215
259
  async run(card, state) {
216
260
  const { source } = card.query;
217
261
  const reserved = reservedKeys(card);
218
262
  if (source.kind === "fn") {
219
263
  if (!source.run) throw new Error(`analytics: card ${card.id} declares kind 'fn' with no run()`);
220
- const rows2 = await source.run({
264
+ const rows = await source.run({
221
265
  tenantId: resolveTenantId(),
222
266
  dateRange: state.dateRange,
223
267
  filters: state.filters
224
268
  });
225
- return aggregateRows(rows2, card, state);
269
+ return aggregateRows(rows, card, state);
226
270
  }
227
271
  if (!source.name) throw new Error(`analytics: card ${card.id} has no source name`);
228
272
  const isDetail = (card.query.dimensions?.length ?? 0) === 0 && (card.query.measures?.length ?? 0) === 0;
@@ -237,42 +281,80 @@ function createSupabaseAnalyticsProvider(config) {
237
281
  ...Object.fromEntries(Object.entries(state.filters ?? {}).map(([k, v]) => [`p_${k}`, v]))
238
282
  });
239
283
  if (error2) throw error2;
240
- const rows2 = Array.isArray(data2) ? data2 : data2?.rows ?? [];
284
+ const rows = Array.isArray(data2) ? data2 : data2?.rows ?? [];
241
285
  return {
242
- rows: rows2.map((r) => mapRow(r, reserved, source.columnMap)),
243
- total: Array.isArray(data2) ? rows2.length : data2?.total ?? rows2.length,
286
+ rows: rows.map((r) => mapRow(r, reserved, source.columnMap)),
287
+ total: Array.isArray(data2) ? rows.length : data2?.total ?? rows.length,
244
288
  summary: Array.isArray(data2) ? void 0 : data2?.summary
245
289
  };
246
290
  }
247
- const size = state.pageSize || 50;
248
- const snapshot = card.query.dateColumn === null;
291
+ const args = runArgs(card, state, resolveTenantId());
249
292
  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
293
+ p_source: args.source,
294
+ p_tenant_id: args.tenant_id,
295
+ p_from: args.from,
296
+ p_to: args.to,
297
+ p_dimensions: args.dimensions,
298
+ p_measures: args.measures,
299
+ p_filters: args.filters,
300
+ p_search: args.search,
301
+ p_search_columns: args.search_columns,
302
+ p_sort: args.sort,
303
+ p_dir: args.dir,
304
+ p_limit: args.limit,
305
+ p_offset: args.offset
268
306
  });
269
307
  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
- };
308
+ return toResult(data, card, reserved);
309
+ },
310
+ // -----------------------------------------------------------------------
311
+ // The batched path.
312
+ //
313
+ // Every aggregate card in the batch becomes ONE analytics_run_batch call;
314
+ // detail listings and 'fn' cards, which the batch function cannot express,
315
+ // still go one by one — but a dashboard is all aggregates, so opening
316
+ // /analytics went from forty-one requests to one.
317
+ //
318
+ // Results keep their INDEX. The caller matched a promise to a position, and
319
+ // returning a compacted array would silently hand card 7 card 9's numbers.
320
+ // -----------------------------------------------------------------------
321
+ async runMany(requests) {
322
+ const out = new Array(requests.length);
323
+ const batchable = [];
324
+ for (let i = 0; i < requests.length; i++) {
325
+ if (batchSupported && isAggregateCard(requests[i].card)) batchable.push(i);
326
+ }
327
+ const singles = requests.map((req, i) => ({ req, i })).filter(({ i }) => !batchable.includes(i)).map(({ req, i }) => this.run(req.card, req.state).then((r) => {
328
+ out[i] = r;
329
+ }).catch((e) => {
330
+ out[i] = e instanceof Error ? e : new Error(String(e));
331
+ }));
332
+ if (batchable.length > 0) {
333
+ const tenantId = resolveTenantId();
334
+ const payload = batchable.map((i) => runArgs(requests[i].card, requests[i].state, tenantId));
335
+ const { data, error } = await client().rpc("analytics_run_batch", { p_requests: payload });
336
+ if (error && batchUnavailable(error)) {
337
+ batchSupported = false;
338
+ await Promise.all([
339
+ ...singles,
340
+ ...batchable.map((i) => this.run(requests[i].card, requests[i].state).then((r) => {
341
+ out[i] = r;
342
+ }).catch((e) => {
343
+ out[i] = e instanceof Error ? e : new Error(String(e));
344
+ }))
345
+ ]);
346
+ return out;
347
+ }
348
+ if (error) throw error;
349
+ const results = Array.isArray(data) ? data : [];
350
+ batchable.forEach((cardIndex, slot) => {
351
+ const entry = results[slot];
352
+ const { card } = requests[cardIndex];
353
+ out[cardIndex] = entry?.error ? Object.assign(new Error(String(entry.error)), { code: entry.code }) : toResult(entry, card, reservedKeys(card));
354
+ });
355
+ }
356
+ await Promise.all(singles);
357
+ return out;
276
358
  }
277
359
  };
278
360
  }
@@ -286,10 +368,10 @@ function seeded(seed) {
286
368
  }
287
369
  return () => {
288
370
  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;
371
+ let t = h;
372
+ t = Math.imul(t ^ t >>> 15, t | 1);
373
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
374
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
293
375
  };
294
376
  }
295
377
  var SAMPLE_LABELS = {
@@ -338,21 +420,37 @@ function createMockAnalyticsProvider() {
338
420
  return row;
339
421
  });
340
422
  return aggregateRows(rows, card, state);
423
+ },
424
+ // There is no transport to save a round trip on, so this is just the loop.
425
+ // It exists because the mock is reached through createSafeDataProvider,
426
+ // whose Proxy answers EVERY property with a function — `provider.runMany`
427
+ // is truthy even for a provider that never declared it, and the caller's
428
+ // feature check therefore cannot detect its absence. Implementing it is how
429
+ // the mock stops being a landmine behind that Proxy.
430
+ async runMany(requests) {
431
+ return Promise.all(requests.map((r) => this.run(r.card, r.state).catch((e) => e instanceof Error ? e : new Error(String(e)))));
341
432
  }
342
433
  };
343
434
  }
344
-
345
- // src/lib/tenant.ts
346
- var t;
435
+ var override;
436
+ function setReportsTenantId(id) {
437
+ override = id;
438
+ }
347
439
  function getReportsTenantId() {
348
- return t;
440
+ if (override) return override;
441
+ try {
442
+ return useOrganizationStore.getState().currentOrg?.id;
443
+ } catch {
444
+ return void 0;
445
+ }
349
446
  }
350
447
 
351
448
  // src/locales/en.ts
352
449
  var en = {
353
450
  "reports.pageTitle": "Reports",
354
451
  "reports.pageSubtitle": "Access complete reports for analysis and decision making",
355
- "reports.allReports": "All Reports",
452
+ "reports.overview": "Overview",
453
+ "reports.allReports": "All reports",
356
454
  "reports.dateRange": "Date Range",
357
455
  "reports.from": "From",
358
456
  "reports.to": "To",
@@ -374,6 +472,17 @@ var en = {
374
472
  "reports.generate": "Generate",
375
473
  "reports.emptyStateTitle": "Ready to generate",
376
474
  "reports.emptyStateDescription": "Select a date range and filters, then click Generate to view the report.",
475
+ // The assistant's invitation on /analytics. It opens on a question, not on a
476
+ // grid: the numbers are already downstairs, and what the owner actually wants
477
+ // is to know what they mean.
478
+ "reports.assistant.persona": "Growth Analyst",
479
+ "reports.assistant.headline": "Hi {{name}}, what would you like to learn today?",
480
+ "reports.assistant.pitch": "Let's find the opportunities hiding in your business",
481
+ "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.",
482
+ "reports.assistant.placeholder": "Build a custom report...",
483
+ "reports.assistant.suggestion.0": "How is my business doing this month?",
484
+ "reports.assistant.suggestion.1": "What's growing and what's falling?",
485
+ "reports.assistant.suggestion.2": "Where am I losing money?",
377
486
  "reports.unavailableTitle": "Coming soon",
378
487
  "reports.unavailableDescription": "This report is not available yet. It will be enabled in a future update."
379
488
  };
@@ -382,7 +491,8 @@ var en = {
382
491
  var ptBR = {
383
492
  "reports.pageTitle": "Relat\xF3rios",
384
493
  "reports.pageSubtitle": "Acesse relat\xF3rios completos para an\xE1lise e tomada de decis\xE3o",
385
- "reports.allReports": "Todos os Relat\xF3rios",
494
+ "reports.overview": "Vis\xE3o geral",
495
+ "reports.allReports": "Todos os relat\xF3rios",
386
496
  "reports.dateRange": "Per\xEDodo",
387
497
  "reports.from": "De",
388
498
  "reports.to": "At\xE9",
@@ -404,6 +514,14 @@ var ptBR = {
404
514
  "reports.generate": "Gerar",
405
515
  "reports.emptyStateTitle": "Pronto para gerar",
406
516
  "reports.emptyStateDescription": "Selecione o per\xEDodo e os filtros desejados, depois clique em Gerar para visualizar o relat\xF3rio.",
517
+ "reports.assistant.persona": "Analista de Crescimento",
518
+ "reports.assistant.headline": "Oi {{name}}, o que voc\xEA quer entender hoje?",
519
+ "reports.assistant.pitch": "Vamos achar as oportunidades escondidas no seu neg\xF3cio",
520
+ "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.",
521
+ "reports.assistant.placeholder": "Pe\xE7a um relat\xF3rio do seu jeito...",
522
+ "reports.assistant.suggestion.0": "Como est\xE1 meu neg\xF3cio neste m\xEAs?",
523
+ "reports.assistant.suggestion.1": "O que est\xE1 crescendo e o que est\xE1 caindo?",
524
+ "reports.assistant.suggestion.2": "Onde estou perdendo dinheiro?",
407
525
  "reports.unavailableTitle": "Em breve",
408
526
  "reports.unavailableDescription": "Este relat\xF3rio ainda n\xE3o est\xE1 dispon\xEDvel. Ser\xE1 habilitado em uma atualiza\xE7\xE3o futura."
409
527
  };
@@ -493,8 +611,8 @@ function markViewed(cardId) {
493
611
  for (const listener of listeners) listener(next);
494
612
  }
495
613
  function useLastViewed() {
496
- const [map, setMap] = React4.useState(() => read());
497
- React4.useEffect(() => {
614
+ const [map, setMap] = React5.useState(() => read());
615
+ React5.useEffect(() => {
498
616
  listeners.add(setMap);
499
617
  return () => {
500
618
  listeners.delete(setMap);
@@ -504,16 +622,16 @@ function useLastViewed() {
504
622
  }
505
623
  var ANALYTICS_BASE = "/analytics";
506
624
  var REPORTS_BASE = `${ANALYTICS_BASE}/reports`;
507
- var ConfigContext = React4__default.createContext(null);
625
+ var ConfigContext = React5__default.createContext(null);
508
626
  function useAnalyticsConfig() {
509
- const ctx = React4__default.useContext(ConfigContext);
627
+ const ctx = React5__default.useContext(ConfigContext);
510
628
  if (!ctx) throw new Error("useAnalyticsConfig must be used inside <AnalyticsSurface>");
511
629
  return ctx;
512
630
  }
513
631
  function useAnalyticsCards() {
514
632
  const runtime = usePluginRuntime();
515
633
  const { cards, createdBy } = useAnalyticsConfig();
516
- return React4__default.useMemo(() => {
634
+ return React5__default.useMemo(() => {
517
635
  const byId = /* @__PURE__ */ new Map();
518
636
  for (const card of getAnalyticsCards(runtime)) byId.set(card.id, card);
519
637
  for (const card of cards) byId.set(card.id, card);
@@ -522,7 +640,7 @@ function useAnalyticsCards() {
522
640
  }
523
641
  function useOverviewCards() {
524
642
  const all = useAnalyticsCards();
525
- return React4__default.useMemo(
643
+ return React5__default.useMemo(
526
644
  () => all.filter((c) => c.dashboard && c.dashboard.defaultVisible !== false).sort((a, b) => (a.dashboard?.order ?? 0) - (b.dashboard?.order ?? 0)),
527
645
  [all]
528
646
  );
@@ -549,7 +667,7 @@ function navigateAnalytics(path) {
549
667
  hashRouterAdapter().navigate(path);
550
668
  }
551
669
  function AnalyticsSurface({ config, provider, children }) {
552
- const openCard = React4__default.useCallback((card) => {
670
+ const openCard = React5__default.useCallback((card) => {
553
671
  markViewed(card.id);
554
672
  navigateAnalytics(`${REPORTS_BASE}/${encodeURIComponent(card.id)}`);
555
673
  }, []);
@@ -567,21 +685,141 @@ function AnalyticsSurface({ config, provider, children }) {
567
685
  }
568
686
  ) });
569
687
  }
688
+ var reportRoute = (cardId) => `${REPORTS_BASE}/${encodeURIComponent(cardId)}`;
689
+ function groupCards(cards) {
690
+ const groups = /* @__PURE__ */ new Map();
691
+ for (const card of cards) {
692
+ const list = groups.get(card.category);
693
+ if (list) list.push(card);
694
+ else groups.set(card.category, [card]);
695
+ }
696
+ return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([category, list]) => [category, [...list].sort((a, b) => a.name.localeCompare(b.name))]);
697
+ }
698
+ function useAnalyticsTree() {
699
+ const t = useTranslation();
700
+ const { labels, showLiveView } = useAnalyticsConfig();
701
+ const cards = useAnalyticsCards();
702
+ return React5__default.useMemo(() => ({
703
+ title: labels.analytics,
704
+ // Only the overview. The searchable catalogue (/analytics/reports) is still
705
+ // routed — bookmarks and the legacy /reports link resolve — but it is not
706
+ // listed: the tree below already IS the catalogue, and a link named "all
707
+ // reports" sitting above the reports reads as a different, fuller list.
708
+ fixed: [
709
+ { id: "overview", label: labels.overview ?? t("reports.overview"), icon: "BarChart3", route: ANALYTICS_BASE, view: "overview" }
710
+ ],
711
+ groups: groupCards(cards),
712
+ live: showLiveView ? { id: "live", label: labels.liveView, icon: "Radio", route: `${ANALYTICS_BASE}/live`, view: "live" } : null
713
+ }), [labels, showLiveView, cards, t]);
714
+ }
715
+ function AnalyticsModule({ view, cardId, children }) {
716
+ const variant = useModuleLayout();
717
+ const tree = useAnalyticsTree();
718
+ const activeId = cardId ? decodeURIComponent(cardId) : void 0;
719
+ const shellRail = useShellRailAvailable();
720
+ const claimsRail = shellRail ?? variant !== "rail";
721
+ const entries = React5__default.useMemo(() => [
722
+ ...tree.fixed.map((f) => ({
723
+ id: `analytics:${f.id}`,
724
+ label: f.label,
725
+ icon: f.icon,
726
+ route: f.route,
727
+ section: "main"
728
+ })),
729
+ ...tree.groups.flatMap(([category, list]) => list.map((card) => ({
730
+ id: `analytics:${card.id}`,
731
+ label: card.name,
732
+ icon: card.icon ?? "FileText",
733
+ route: reportRoute(card.id),
734
+ section: "main",
735
+ // Consecutive items sharing a group render under one heading — the same
736
+ // mechanism that heads the settings rail's plugin runs.
737
+ group: category
738
+ }))),
739
+ ...tree.live ? [{
740
+ id: "analytics:live",
741
+ label: tree.live.label,
742
+ icon: tree.live.icon,
743
+ route: tree.live.route,
744
+ section: "main"
745
+ }] : []
746
+ ], [tree]);
747
+ useContextualNav(claimsRail ? { base: ANALYTICS_BASE, entries, label: tree.title } : null);
748
+ const nav = React5__default.useMemo(() => {
749
+ if (claimsRail) return [];
750
+ return [
751
+ ...tree.fixed.map((f) => ({
752
+ id: f.id,
753
+ label: f.label,
754
+ icon: f.icon,
755
+ active: view === f.view,
756
+ onClick: () => navigateAnalytics(f.route)
757
+ })),
758
+ ...tree.groups.map(([category, list]) => ({
759
+ id: `group:${category}`,
760
+ label: category,
761
+ children: list.map((card) => ({
762
+ id: card.id,
763
+ label: card.name,
764
+ active: view === "report" && card.id === activeId,
765
+ onClick: () => navigateAnalytics(reportRoute(card.id))
766
+ }))
767
+ })),
768
+ ...tree.live ? [{
769
+ id: tree.live.id,
770
+ label: tree.live.label,
771
+ icon: tree.live.icon,
772
+ active: view === "live",
773
+ onClick: () => navigateAnalytics(tree.live.route)
774
+ }] : []
775
+ ];
776
+ }, [tree, view, activeId, claimsRail]);
777
+ return /* @__PURE__ */ jsx(
778
+ ModulePage,
779
+ {
780
+ title: tree.title,
781
+ nav,
782
+ navVariant: claimsRail ? "sidebar" : "rail",
783
+ showHeader: false,
784
+ viewKey: view === "report" ? `report:${activeId}` : view,
785
+ children
786
+ }
787
+ );
788
+ }
570
789
  function AnalyticsOverview() {
571
- const { labels, showHeader } = useAnalyticsConfig();
790
+ const { labels, showHeader, assistant } = useAnalyticsConfig();
572
791
  const cards = useOverviewCards();
573
- const filters = React4__default.useMemo(() => collectFilters(cards), [cards]);
574
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
575
- showHeader ? /* @__PURE__ */ jsxs("div", { children: [
576
- /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold tracking-tight", children: labels.analytics }),
577
- /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: labels.overviewSubtitle })
578
- ] }) : null,
579
- /* @__PURE__ */ jsx(AnalyticsControls, { filters, className: "sticky top-0 z-10 -mx-1 bg-background/95 px-1 py-2 backdrop-blur" }),
580
- 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
- /* @__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
- /* @__PURE__ */ jsx("p", { className: "font-medium", children: labels.emptyOverviewTitle }),
583
- /* @__PURE__ */ jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: labels.emptyOverviewDescription })
584
- ] }) : /* @__PURE__ */ jsx(AnalyticsGrid, { cards })
792
+ const filters = React5__default.useMemo(() => collectFilters(cards), [cards]);
793
+ const t = useTranslation();
794
+ const suggestions = React5__default.useMemo(
795
+ () => assistant?.suggestions ?? [0, 1, 2].map((i) => ({ label: t(`reports.assistant.suggestion.${i}`) })),
796
+ [assistant?.suggestions, t]
797
+ );
798
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
799
+ assistant && /* @__PURE__ */ jsx(
800
+ AssistantInvite,
801
+ {
802
+ id: "analytics",
803
+ persona: assistant.persona ?? t("reports.assistant.persona"),
804
+ headline: assistant.headline ?? t("reports.assistant.headline"),
805
+ pitch: assistant.pitch ?? t("reports.assistant.pitch"),
806
+ description: assistant.description ?? t("reports.assistant.description"),
807
+ placeholder: assistant.placeholder ?? t("reports.assistant.placeholder"),
808
+ suggestions
809
+ }
810
+ ),
811
+ /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
812
+ showHeader ? /* @__PURE__ */ jsxs("div", { children: [
813
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold tracking-tight", children: labels.analytics }),
814
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: labels.overviewSubtitle })
815
+ ] }) : null,
816
+ /* @__PURE__ */ jsx(AnalyticsControls, { filters, className: "sticky top-3 z-20" }),
817
+ cards.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center rounded-xl border border-dashed py-16 text-center", children: [
818
+ /* @__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" }) }),
819
+ /* @__PURE__ */ jsx("p", { className: "font-medium", children: labels.emptyOverviewTitle }),
820
+ /* @__PURE__ */ jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: labels.emptyOverviewDescription })
821
+ ] }) : /* @__PURE__ */ jsx(AnalyticsGrid, { cards })
822
+ ] })
585
823
  ] });
586
824
  }
587
825
  function formatViewed(iso, locale, never) {
@@ -594,18 +832,18 @@ function ReportsListPage() {
594
832
  const { labels, currency, showHeader } = useAnalyticsConfig();
595
833
  const cards = useAnalyticsCards();
596
834
  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(
835
+ const [search, setSearch] = React5__default.useState("");
836
+ const [category, setCategory] = React5__default.useState();
837
+ const [author, setAuthor] = React5__default.useState();
838
+ const categoryOptions = React5__default.useMemo(
601
839
  () => [...new Set(cards.map((c) => c.category))].sort((a, b) => a.localeCompare(b)).map((c) => ({ value: c, label: c })),
602
840
  [cards]
603
841
  );
604
- const authorOptions = React4__default.useMemo(
842
+ const authorOptions = React5__default.useMemo(
605
843
  () => [...new Set(cards.map((c) => c.createdBy ?? "\u2014"))].sort((a, b) => a.localeCompare(b)).map((a) => ({ value: a, label: a })),
606
844
  [cards]
607
845
  );
608
- const rows = React4__default.useMemo(() => {
846
+ const rows = React5__default.useMemo(() => {
609
847
  const q = search.trim().toLowerCase();
610
848
  return cards.filter((card) => {
611
849
  if (category && card.category !== category) return false;
@@ -614,7 +852,7 @@ function ReportsListPage() {
614
852
  return card.name.toLowerCase().includes(q) || card.description?.toLowerCase().includes(q) || card.category.toLowerCase().includes(q);
615
853
  });
616
854
  }, [cards, search, category, author]);
617
- const columns = React4__default.useMemo(() => [
855
+ const columns = React5__default.useMemo(() => [
618
856
  {
619
857
  id: "name",
620
858
  accessorFn: (card) => card.name,
@@ -730,7 +968,7 @@ function exportToCsv(columns, data, filename, currency) {
730
968
  exportCSV(csvColumns, data, filename);
731
969
  }
732
970
  function ExportDropdown({ columns, data, filename, currency, title }) {
733
- const t2 = useTranslation();
971
+ const t = useTranslation();
734
972
  const [excelAvailable, setExcelAvailable] = useState(false);
735
973
  const [pdfAvailable, setPdfAvailable] = useState(false);
736
974
  useEffect(() => {
@@ -751,12 +989,12 @@ function ExportDropdown({ columns, data, filename, currency, title }) {
751
989
  return /* @__PURE__ */ jsxs(Dropdown, { children: [
752
990
  /* @__PURE__ */ jsx(DropdownTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", children: [
753
991
  /* @__PURE__ */ jsx(Download, { className: "h-4 w-4 mr-1.5" }),
754
- t2("reports.export")
992
+ t("reports.export")
755
993
  ] }) }),
756
994
  /* @__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") })
995
+ /* @__PURE__ */ jsx(DropdownItem, { onClick: handleCsv, children: t("reports.exportCsv") }),
996
+ excelAvailable && /* @__PURE__ */ jsx(DropdownItem, { onClick: handleExcel, children: t("reports.exportExcel") }),
997
+ pdfAvailable && /* @__PURE__ */ jsx(DropdownItem, { onClick: handlePdf, children: t("reports.exportPdf") })
760
998
  ] })
761
999
  ] });
762
1000
  }
@@ -813,8 +1051,8 @@ function ViewerBody({ card }) {
813
1051
  const { labels, currency } = useAnalyticsConfig();
814
1052
  const { state, setPage, setPageSize } = useAnalyticsContext();
815
1053
  const { result, loading, error, engineMissing } = useAnalyticsCard(card);
816
- const columns = React4__default.useMemo(() => cardColumns(card), [card]);
817
- const tanstackColumns = React4__default.useMemo(
1054
+ const columns = React5__default.useMemo(() => cardColumns(card), [card]);
1055
+ const tanstackColumns = React5__default.useMemo(
818
1056
  () => reportColumnsToTanstack(columns, currency.code),
819
1057
  [columns, currency.code]
820
1058
  );
@@ -871,7 +1109,7 @@ function AnalyticsReportViewer({ cardId }) {
871
1109
  const { labels } = useAnalyticsConfig();
872
1110
  const cards = useAnalyticsCards();
873
1111
  const card = cards.find((c) => c.id === decodeURIComponent(cardId ?? ""));
874
- React4__default.useEffect(() => {
1112
+ React5__default.useEffect(() => {
875
1113
  if (card) markViewed(card.id);
876
1114
  }, [card?.id]);
877
1115
  if (!card) {
@@ -888,8 +1126,8 @@ function AnalyticsReportViewer({ cardId }) {
888
1126
  SubpageHeader,
889
1127
  {
890
1128
  title: card.name,
891
- parentLabel: labels.reports,
892
- onBack: () => navigateAnalytics(REPORTS_BASE)
1129
+ parentLabel: labels.analytics,
1130
+ onBack: () => navigateAnalytics(ANALYTICS_BASE)
893
1131
  }
894
1132
  ),
895
1133
  card.description ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: card.description }) : null,
@@ -908,7 +1146,7 @@ function AnalyticsReportViewer({ cardId }) {
908
1146
  function ViewerActions({ card }) {
909
1147
  const { currency } = useAnalyticsConfig();
910
1148
  const { result } = useAnalyticsCard(card);
911
- const columns = React4__default.useMemo(() => cardColumns(card), [card]);
1149
+ const columns = React5__default.useMemo(() => cardColumns(card), [card]);
912
1150
  if (!result?.rows?.length) return null;
913
1151
  return /* @__PURE__ */ jsxs(Fragment, { children: [
914
1152
  /* @__PURE__ */ jsx(
@@ -1099,7 +1337,9 @@ function resolveConfig(options) {
1099
1337
  defaultPageSize: options.defaultPageSize ?? 50,
1100
1338
  showHeader: options.showHeader === true,
1101
1339
  defaultRange: options.defaultRange ?? "last_30_days",
1102
- defaultCompare: options.defaultCompare ?? "previous_period"
1340
+ defaultCompare: options.defaultCompare ?? "previous_period",
1341
+ showLiveView: options.showLiveView === true,
1342
+ assistant: options.assistant === false ? null : options.assistant ?? {}
1103
1343
  };
1104
1344
  }
1105
1345
  function createAnalyticsPlugin(options = {}) {
@@ -1109,10 +1349,14 @@ function createAnalyticsPlugin(options = {}) {
1109
1349
  () => createSupabaseAnalyticsProvider({ tenantId: () => getReportsTenantId() }),
1110
1350
  () => createMockAnalyticsProvider()
1111
1351
  );
1112
- const page2 = (Body) => (props) => React4__default.createElement(
1352
+ const page2 = (Body, view) => (props) => React5__default.createElement(
1113
1353
  AnalyticsSurface,
1114
1354
  { config, provider },
1115
- React4__default.createElement(Body, props)
1355
+ React5__default.createElement(
1356
+ AnalyticsModule,
1357
+ { view, cardId: props?.cardId },
1358
+ React5__default.createElement(Body, props)
1359
+ )
1116
1360
  );
1117
1361
  const READ = { feature: "reports", action: "read" };
1118
1362
  const position = options.navPosition ?? 9;
@@ -1125,23 +1369,7 @@ function createAnalyticsPlugin(options = {}) {
1125
1369
  route: ANALYTICS_BASE,
1126
1370
  icon: "BarChart3",
1127
1371
  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
- }] : []
1372
+ }
1145
1373
  ];
1146
1374
  return {
1147
1375
  id: "reports",
@@ -1157,8 +1385,8 @@ function createAnalyticsPlugin(options = {}) {
1157
1385
  ],
1158
1386
  navigation: nav,
1159
1387
  routes: [
1160
- { path: ANALYTICS_BASE, component: page2(AnalyticsOverview), permission: READ },
1161
- { path: REPORTS_BASE, component: page2(ReportsListPage), permission: READ },
1388
+ { path: ANALYTICS_BASE, component: page2(AnalyticsOverview, "overview"), permission: READ },
1389
+ { path: REPORTS_BASE, component: page2(ReportsListPage, "reports"), permission: READ },
1162
1390
  // Registered before the list would ever shadow it: matchRoute scores
1163
1391
  // static segments above params, so /analytics/reports still wins its own
1164
1392
  // exact path and only /analytics/reports/<id> lands here.
@@ -1166,14 +1394,17 @@ function createAnalyticsPlugin(options = {}) {
1166
1394
  // (the card's name), so it claims the shell bar itself via PageHeader.
1167
1395
  // Declaring both gave the page two stacked back-crumbs saying the same
1168
1396
  // 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 },
1397
+ { path: `${REPORTS_BASE}/:cardId`, component: page2(AnalyticsReportViewer, "report"), permission: READ },
1398
+ { path: `${ANALYTICS_BASE}/live`, component: page2(LiveViewPage, "live"), permission: READ },
1171
1399
  // The old top-level route kept as a redirect target: bookmarks, saved
1172
1400
  // links and the five apps still pointing at /reports must not 404.
1173
- { path: "/reports", component: page2(ReportsListPage), permission: READ }
1401
+ { path: "/reports", component: page2(ReportsListPage, "reports"), permission: READ }
1174
1402
  ],
1175
1403
  widgets: [],
1176
1404
  registries: [],
1405
+ // Measurement, not acquisition — what the Fayz plugin page lists for
1406
+ // Analytics.
1407
+ integrations: ["google-analytics", "hotjar"],
1177
1408
  locales: reportsLocales
1178
1409
  };
1179
1410
  }
@@ -1194,6 +1425,6 @@ function createReportsPlugin(options) {
1194
1425
  });
1195
1426
  }
1196
1427
 
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 };
1428
+ 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
1429
  //# sourceMappingURL=index.js.map
1199
1430
  //# sourceMappingURL=index.js.map