@fayz-ai/plugin-reports 0.9.0 → 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.
Files changed (79) hide show
  1. package/README.md +4 -4
  2. package/dist/{chunk-LODXZ2SC.js → chunk-2ZFN6ECM.js} +3 -3
  3. package/dist/chunk-2ZFN6ECM.js.map +1 -0
  4. package/dist/data/aggregate.d.ts +14 -0
  5. package/dist/data/aggregate.d.ts.map +1 -0
  6. package/dist/data/analytics-mock.d.ts +3 -0
  7. package/dist/data/analytics-mock.d.ts.map +1 -0
  8. package/dist/data/analytics-supabase.d.ts +7 -0
  9. package/dist/data/analytics-supabase.d.ts.map +1 -0
  10. package/dist/{excel-QHUSM6IH.js → excel-WE73YE2V.js} +3 -3
  11. package/dist/{excel-QHUSM6IH.js.map → excel-WE73YE2V.js.map} +1 -1
  12. package/dist/index.d.ts +73 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1207 -654
  15. package/dist/index.js.map +1 -1
  16. package/dist/lib/lastViewed.d.ts +5 -0
  17. package/dist/lib/lastViewed.d.ts.map +1 -0
  18. package/dist/lib/legacy.d.ts +5 -0
  19. package/dist/lib/legacy.d.ts.map +1 -0
  20. package/dist/lib/tenant.d.ts +1 -0
  21. package/dist/lib/tenant.d.ts.map +1 -1
  22. package/dist/locales/en.d.ts.map +1 -1
  23. package/dist/locales/pt-BR.d.ts.map +1 -1
  24. package/dist/{pdf-HETKN26Y.js → pdf-E6DXHFXU.js} +3 -3
  25. package/dist/{pdf-HETKN26Y.js.map → pdf-E6DXHFXU.js.map} +1 -1
  26. package/dist/views/AnalyticsModule.d.ts +10 -0
  27. package/dist/views/AnalyticsModule.d.ts.map +1 -0
  28. package/dist/views/AnalyticsOverview.d.ts +3 -0
  29. package/dist/views/AnalyticsOverview.d.ts.map +1 -0
  30. package/dist/views/AnalyticsReportViewer.d.ts +5 -0
  31. package/dist/views/AnalyticsReportViewer.d.ts.map +1 -0
  32. package/dist/views/AnalyticsSurface.d.ts +97 -0
  33. package/dist/views/AnalyticsSurface.d.ts.map +1 -0
  34. package/dist/views/LiveViewPage.d.ts +3 -0
  35. package/dist/views/LiveViewPage.d.ts.map +1 -0
  36. package/dist/views/ReportsListPage.d.ts +3 -0
  37. package/dist/views/ReportsListPage.d.ts.map +1 -0
  38. package/package.json +5 -7
  39. package/dist/ReportsContext.d.ts +0 -16
  40. package/dist/ReportsContext.d.ts.map +0 -1
  41. package/dist/ReportsPage.d.ts +0 -8
  42. package/dist/ReportsPage.d.ts.map +0 -1
  43. package/dist/chunk-LODXZ2SC.js.map +0 -1
  44. package/dist/components/ReportCard.d.ts +0 -9
  45. package/dist/components/ReportCard.d.ts.map +0 -1
  46. package/dist/components/ReportFilterBar.d.ts +0 -12
  47. package/dist/components/ReportFilterBar.d.ts.map +0 -1
  48. package/dist/components/ReportSummaryRow.d.ts +0 -10
  49. package/dist/components/ReportSummaryRow.d.ts.map +0 -1
  50. package/dist/hooks/useReportData.d.ts +0 -19
  51. package/dist/hooks/useReportData.d.ts.map +0 -1
  52. package/dist/views/ReportHub.d.ts +0 -8
  53. package/dist/views/ReportHub.d.ts.map +0 -1
  54. package/dist/views/ReportViewer.d.ts +0 -9
  55. package/dist/views/ReportViewer.d.ts.map +0 -1
  56. package/src/ReportsContext.tsx +0 -33
  57. package/src/ReportsPage.tsx +0 -62
  58. package/src/components/ExportDropdown.tsx +0 -63
  59. package/src/components/ReportCard.tsx +0 -38
  60. package/src/components/ReportFilterBar.tsx +0 -132
  61. package/src/components/ReportPagination.tsx +0 -59
  62. package/src/components/ReportSummaryRow.tsx +0 -68
  63. package/src/data/index.ts +0 -4
  64. package/src/data/mock.ts +0 -9
  65. package/src/data/supabase.ts +0 -200
  66. package/src/data/types.ts +0 -10
  67. package/src/export/csv.ts +0 -21
  68. package/src/export/excel.ts +0 -42
  69. package/src/export/pdf.ts +0 -47
  70. package/src/hooks/useReportData.ts +0 -123
  71. package/src/index.ts +0 -142
  72. package/src/lib/columns.ts +0 -130
  73. package/src/lib/tenant.ts +0 -3
  74. package/src/locales/en.ts +0 -28
  75. package/src/locales/index.ts +0 -7
  76. package/src/locales/pt-BR.ts +0 -28
  77. package/src/types.ts +0 -171
  78. package/src/views/ReportHub.tsx +0 -97
  79. package/src/views/ReportViewer.tsx +0 -182
package/dist/index.js CHANGED
@@ -1,367 +1,963 @@
1
- import { reportColumnsToTanstack, formatValuePlain } from './chunk-LODXZ2SC.js';
2
- import React7, { createContext, useState, useEffect, useCallback, useMemo, useContext, useRef } from 'react';
3
- import { registerTranslations, createSafeDataProvider, getSupabaseClientOptional, useTranslation, exportCSV } from '@fayz-ai/core';
1
+ import { reportColumnsToTanstack, formatValuePlain } from './chunk-2ZFN6ECM.js';
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';
4
7
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
- import { ArrowLeft, Loader2, Play, Printer, SlidersHorizontal, Construction, Search, Download, ChevronLeft, ChevronRight, FileText } from 'lucide-react';
6
- import { Button, DataTable, Input, DatePicker, Dropdown, DropdownTrigger, DropdownContent, DropdownItem, getLucideIcon, Badge, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@fayz-ai/ui';
7
- import { dedup } from '@fayz-ai/saas';
8
+ import { BarChart3, User, Search, ArrowLeft, Printer, Construction, Download, Radio, ChevronLeft, ChevronRight } from 'lucide-react';
8
9
 
9
- var ReportsContext = createContext(null);
10
- function useReportsContext() {
11
- const ctx = useContext(ReportsContext);
12
- if (!ctx) throw new Error("useReportsContext must be used within ReportsContextProvider");
13
- return ctx;
10
+ // src/data/aggregate.ts
11
+ function num(value) {
12
+ if (value == null) return 0;
13
+ const n = typeof value === "number" ? value : Number(value);
14
+ return Number.isFinite(n) ? n : 0;
14
15
  }
15
- function ReportsContextProvider({
16
- config,
17
- provider,
18
- children
19
- }) {
20
- return /* @__PURE__ */ jsx(ReportsContext.Provider, { value: { config, provider, reports: config.reports }, children });
21
- }
22
- function ReportCard({ report, onClick }) {
23
- const IconComp = getLucideIcon(report.icon) ?? FileText;
24
- return /* @__PURE__ */ jsxs(
25
- "button",
26
- {
27
- type: "button",
28
- onClick: () => onClick(report),
29
- className: "flex flex-col items-start gap-2 rounded-xl border bg-card p-4 text-left transition-all hover:border-primary/30 hover:shadow-md",
30
- children: [
31
- /* @__PURE__ */ jsxs("div", { className: "flex w-full items-start justify-between", children: [
32
- /* @__PURE__ */ jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 text-primary", children: /* @__PURE__ */ jsx(IconComp, { className: "h-5 w-5" }) }),
33
- report.badge && /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "text-xs capitalize", children: report.badge })
34
- ] }),
35
- /* @__PURE__ */ jsxs("div", { children: [
36
- /* @__PURE__ */ jsx("h4", { className: "font-medium text-sm", children: report.name }),
37
- report.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mt-0.5 line-clamp-2", children: report.description })
38
- ] })
39
- ]
16
+ function truncate(value, grain) {
17
+ const d = new Date(String(value));
18
+ if (Number.isNaN(d.getTime())) return null;
19
+ const out = new Date(d);
20
+ switch (grain) {
21
+ case "hour":
22
+ out.setMinutes(0, 0, 0);
23
+ break;
24
+ case "week": {
25
+ const day = (out.getDay() + 6) % 7;
26
+ out.setDate(out.getDate() - day);
27
+ out.setHours(0, 0, 0, 0);
28
+ break;
40
29
  }
41
- );
30
+ case "month":
31
+ out.setDate(1);
32
+ out.setHours(0, 0, 0, 0);
33
+ break;
34
+ case "quarter":
35
+ out.setMonth(Math.floor(out.getMonth() / 3) * 3, 1);
36
+ out.setHours(0, 0, 0, 0);
37
+ break;
38
+ case "year":
39
+ out.setMonth(0, 1);
40
+ out.setHours(0, 0, 0, 0);
41
+ break;
42
+ default:
43
+ out.setHours(0, 0, 0, 0);
44
+ break;
45
+ }
46
+ return out.toISOString();
42
47
  }
43
- function ReportHub({ onSelect }) {
44
- const t2 = useTranslation();
45
- const { config } = useReportsContext();
46
- const [search, setSearch] = useState("");
47
- const categories = useMemo(() => {
48
- const filtered = search ? config.reports.filter(
49
- (r) => r.name.toLowerCase().includes(search.toLowerCase()) || r.description?.toLowerCase().includes(search.toLowerCase())
50
- ) : config.reports;
51
- const map = /* @__PURE__ */ new Map();
52
- for (const report of filtered) {
53
- const list = map.get(report.category) ?? [];
54
- list.push(report);
55
- map.set(report.category, list);
56
- }
57
- const groups = [];
58
- for (const [name, reports] of map) {
59
- groups.push({ name, reports });
48
+ function matchesFilters(row, filters) {
49
+ for (const [key, value] of Object.entries(filters)) {
50
+ if (value == null || value === "") continue;
51
+ const actual = row[key];
52
+ if (Array.isArray(value)) {
53
+ if (value.length > 0 && !value.map(String).includes(String(actual))) return false;
54
+ } else if (String(actual) !== String(value)) {
55
+ return false;
60
56
  }
61
- return groups;
62
- }, [config.reports, search]);
63
- const totalReports = config.reports.length;
64
- return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
65
- config.showHeader && /* @__PURE__ */ jsxs("div", { children: [
66
- /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold", children: t2("reports.pageTitle") }),
67
- /* @__PURE__ */ jsx("p", { className: "text-muted-foreground mt-1", children: t2("reports.pageSubtitle") })
68
- ] }),
69
- totalReports > 6 && /* @__PURE__ */ jsxs("div", { className: "relative max-w-md", children: [
70
- /* @__PURE__ */ jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }),
71
- /* @__PURE__ */ jsx(
72
- Input,
73
- {
74
- value: search,
75
- onChange: (e) => setSearch(e.target.value),
76
- placeholder: t2("reports.search"),
77
- className: "pl-9"
78
- }
79
- )
80
- ] }),
81
- /* @__PURE__ */ jsx("div", { className: "space-y-8", children: categories.map((category) => /* @__PURE__ */ jsxs("div", { className: "rounded-xl border bg-card shadow-sm p-5", children: [
82
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-4", children: [
83
- /* @__PURE__ */ jsx("h2", { className: "font-semibold", children: category.name }),
84
- /* @__PURE__ */ jsxs("span", { className: "text-sm text-muted-foreground", children: [
85
- category.reports.length,
86
- " ",
87
- t2("reports.reports")
88
- ] })
89
- ] }),
90
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3", children: category.reports.map((report) => /* @__PURE__ */ jsx(ReportCard, { report, onClick: onSelect }, report.id)) })
91
- ] }, category.name)) }),
92
- categories.length === 0 && search && /* @__PURE__ */ jsx("div", { className: "text-center py-12 text-muted-foreground", children: t2("reports.noResults") })
93
- ] });
57
+ }
58
+ return true;
94
59
  }
95
- function defaultDateRange() {
96
- const now = /* @__PURE__ */ new Date();
97
- const from = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
98
- const to = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
99
- return { from, to };
60
+ function inRange(row, dateColumn, from, to) {
61
+ const raw = row[dateColumn];
62
+ if (raw == null) return true;
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;
67
+ return true;
68
+ }
69
+ function applyMeasure(rows, agg, column) {
70
+ if (agg === "count") return rows.length;
71
+ const values = rows.map((r) => r[column]).filter((v) => v != null);
72
+ if (agg === "count_distinct") return new Set(values.map(String)).size;
73
+ const numbers = values.map(num);
74
+ if (numbers.length === 0) return agg === "sum" ? 0 : null;
75
+ switch (agg) {
76
+ case "sum":
77
+ return numbers.reduce((a, b) => a + b, 0);
78
+ case "avg":
79
+ return numbers.reduce((a, b) => a + b, 0) / numbers.length;
80
+ case "min":
81
+ return Math.min(...numbers);
82
+ case "max":
83
+ return Math.max(...numbers);
84
+ default:
85
+ return null;
86
+ }
100
87
  }
101
- function buildDefaultFilters(report) {
102
- const filters = {};
103
- for (const f of report.filters ?? []) {
104
- if (f.defaultValue !== void 0) filters[f.key] = f.defaultValue;
88
+ function aggregateRows(source, card, state, options) {
89
+ const snapshot = card.query.dateColumn === null;
90
+ const dateColumn = card.query.dateColumn ?? "created_at";
91
+ const dimensions = card.query.dimensions ?? [];
92
+ const measures = (card.query.measures ?? []).filter((m) => m.agg !== "ratio");
93
+ const filtered = source.filter((row) => snapshot || inRange(row, dateColumn, state.dateRange?.from, state.dateRange?.to)).filter((row) => matchesFilters(row, state.filters ?? {}));
94
+ const summary = {};
95
+ for (const m of measures) {
96
+ const value = applyMeasure(filtered, m.agg, m.column ?? m.key);
97
+ summary[m.key] = value ?? 0;
98
+ }
99
+ if (measures.length === 0 && dimensions.length === 0) {
100
+ const sorted = sortRows(filtered, state.sortBy ?? card.query.defaultSort, state.sortDir ?? card.query.defaultSortDir);
101
+ return { rows: page(sorted, state, options), total: sorted.length, summary };
105
102
  }
106
- return filters;
107
- }
108
- function useReportData(report, provider, defaultPageSize = 50) {
109
- const [query, setQuery] = useState({
110
- dateRange: defaultDateRange(),
111
- filters: buildDefaultFilters(report),
112
- sortBy: report.dataSource.defaultSort,
113
- sortDir: report.dataSource.defaultSortDir ?? "desc",
114
- page: 1,
115
- pageSize: defaultPageSize
103
+ const buckets = /* @__PURE__ */ new Map();
104
+ for (const row of filtered) {
105
+ const keyParts = {};
106
+ for (const d of dimensions) {
107
+ const raw = row[d.key];
108
+ keyParts[d.key] = d.grain && d.grain !== "auto" ? truncate(raw, d.grain) : raw ?? null;
109
+ }
110
+ const id = JSON.stringify(dimensions.map((d) => keyParts[d.key]));
111
+ const existing = buckets.get(id);
112
+ if (existing) existing.rows.push(row);
113
+ else buckets.set(id, { key: keyParts, rows: [row] });
114
+ }
115
+ let grouped = [...buckets.values()].map(({ key, rows }) => {
116
+ const out = { ...key };
117
+ for (const m of measures) out[m.key] = applyMeasure(rows, m.agg, m.column ?? m.key);
118
+ return out;
119
+ });
120
+ grouped = sortRows(grouped, state.sortBy ?? card.query.defaultSort, state.sortDir ?? card.query.defaultSortDir);
121
+ if (card.query.limit) grouped = grouped.slice(0, card.query.limit);
122
+ return { rows: page(grouped, state, options), total: grouped.length, summary };
123
+ }
124
+ function sortRows(rows, sortBy, dir) {
125
+ if (!sortBy) return rows;
126
+ const sign = dir === "asc" ? 1 : -1;
127
+ return [...rows].sort((a, b) => {
128
+ const av = a[sortBy];
129
+ const bv = b[sortBy];
130
+ if (av == null && bv == null) return 0;
131
+ if (av == null) return 1;
132
+ if (bv == null) return -1;
133
+ if (typeof av === "number" || typeof bv === "number") return (num(av) - num(bv)) * sign;
134
+ return String(av).localeCompare(String(bv)) * sign;
116
135
  });
117
- const [result, setResult] = useState({ data: [], total: 0 });
118
- const [loading, setLoading] = useState(false);
119
- const [generated, setGenerated] = useState(false);
120
- const [error, setError] = useState(null);
121
- const queryRef = useRef(query);
122
- queryRef.current = query;
123
- const fetchData = useCallback(async (q) => {
124
- setLoading(true);
125
- setError(null);
126
- try {
127
- const res = await dedup(
128
- `report:${report.id}:${JSON.stringify(q)}`,
129
- () => provider.fetch(report, q)
130
- );
131
- setResult(res);
132
- setGenerated(true);
133
- } catch (err) {
134
- setError(err?.message ?? "Failed to load report");
135
- setResult({ data: [], total: 0 });
136
- } finally {
137
- setLoading(false);
136
+ }
137
+ function page(rows, state, options) {
138
+ if (options?.all) return rows;
139
+ const size = state.pageSize || 50;
140
+ const start = ((state.page || 1) - 1) * size;
141
+ return rows.slice(start, start + size);
142
+ }
143
+
144
+ // src/data/analytics-supabase.ts
145
+ function snakeToCamel(s) {
146
+ return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
147
+ }
148
+ function mapRow(row, reserved, columnMap) {
149
+ const out = {};
150
+ for (const [key, value] of Object.entries(row)) {
151
+ if (reserved.has(key)) {
152
+ out[key] = value;
153
+ continue;
138
154
  }
139
- }, [report, provider]);
140
- const generate = useCallback(() => {
141
- fetchData(queryRef.current);
142
- }, [fetchData]);
143
- const setDateRange = useCallback((dateRange) => {
144
- setQuery((q) => ({ ...q, dateRange, page: 1 }));
145
- }, []);
146
- const setFilter = useCallback((key, value) => {
147
- setQuery((q) => ({ ...q, filters: { ...q.filters, [key]: value }, page: 1 }));
148
- }, []);
149
- const setSort = useCallback((sortBy) => {
150
- setQuery((q) => ({
151
- ...q,
152
- sortBy,
153
- sortDir: q.sortBy === sortBy && q.sortDir === "asc" ? "desc" : "asc",
154
- page: 1
155
- }));
156
- }, []);
157
- const setPage = useCallback((page) => {
158
- setQuery((q) => ({ ...q, page }));
159
- }, []);
160
- const setPageSize = useCallback((pageSize) => {
161
- setQuery((q) => ({ ...q, pageSize, page: 1 }));
162
- }, []);
163
- const setSearch = useCallback((search) => {
164
- setQuery((q) => ({ ...q, search, page: 1 }));
165
- }, []);
155
+ out[columnMap?.[key] ?? snakeToCamel(key)] = value;
156
+ if (!(key in out)) out[key] = value;
157
+ }
158
+ return out;
159
+ }
160
+ function reservedKeys(card) {
161
+ return /* @__PURE__ */ new Set([
162
+ ...(card.query.dimensions ?? []).map((d) => d.key),
163
+ ...(card.query.measures ?? []).map((m) => m.key)
164
+ ]);
165
+ }
166
+ function searchColumns(card) {
167
+ const cols = (card.columns ?? []).filter((c) => c.type === "text" || c.type === "email").map((c) => c.key);
168
+ return cols.length ? cols : void 0;
169
+ }
170
+ function camelToSnake(s) {
171
+ return s.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
172
+ }
173
+ async function selectFromView(card, state, db, tenantId, reserved) {
174
+ const { source } = card.query;
175
+ const size = state.pageSize || 50;
176
+ let q = db.from(source.name).select("*", { count: "exact" });
177
+ if (tenantId) q = q.eq(source.tenantColumn ?? "tenant_id", tenantId);
178
+ const snapshot = card.query.dateColumn === null;
179
+ const dateCol = card.query.dateColumn ?? "created_at";
180
+ if (!snapshot && state.dateRange?.from) q = q.gte(dateCol, state.dateRange.from);
181
+ if (!snapshot && state.dateRange?.to) {
182
+ q = q.lte(dateCol, dateCol.endsWith("_at") ? `${state.dateRange.to}T23:59:59.999` : state.dateRange.to);
183
+ }
184
+ for (const [key, value] of Object.entries(state.filters ?? {})) {
185
+ if (value == null || value === "") continue;
186
+ const col = camelToSnake(key);
187
+ q = Array.isArray(value) ? q.in(col, value) : q.eq(col, value);
188
+ }
189
+ const searchable = searchColumns(card);
190
+ if (state.search?.trim() && searchable?.length) {
191
+ const term = `%${state.search.trim()}%`;
192
+ q = q.or(searchable.map((c) => `${camelToSnake(c)}.ilike.${term}`).join(","));
193
+ }
194
+ const sortBy = state.sortBy ?? card.query.defaultSort;
195
+ if (sortBy) {
196
+ q = q.order(camelToSnake(sortBy), {
197
+ ascending: (state.sortDir ?? card.query.defaultSortDir ?? "desc") === "asc"
198
+ });
199
+ }
200
+ const from = ((state.page || 1) - 1) * size;
201
+ const { data, error, count } = await q.range(from, from + size - 1);
202
+ if (error) throw error;
166
203
  return {
167
- data: result.data,
168
- total: result.total,
169
- loading,
170
- generated,
171
- error,
172
- query,
173
- generate,
174
- setDateRange,
175
- setFilter,
176
- setSort,
177
- setPage,
178
- setPageSize,
179
- setSearch
204
+ rows: (data ?? []).map((r) => mapRow(r, reserved, source.columnMap)),
205
+ total: count ?? 0
180
206
  };
181
207
  }
182
- function ReportFilterBar({
183
- dateRange,
184
- onDateRangeChange,
185
- filters,
186
- filterValues,
187
- onFilterChange
188
- }) {
189
- const t2 = useTranslation();
190
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-end gap-3", children: [
191
- /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-2", children: [
192
- /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
193
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("reports.from") }),
194
- /* @__PURE__ */ jsx(
195
- DatePicker,
196
- {
197
- value: dateRange.from,
198
- onChange: (v) => onDateRangeChange({ ...dateRange, from: v })
199
- }
200
- )
201
- ] }),
202
- /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
203
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("reports.to") }),
204
- /* @__PURE__ */ jsx(
205
- DatePicker,
206
- {
207
- value: dateRange.to,
208
- onChange: (v) => onDateRangeChange({ ...dateRange, to: v })
209
- }
210
- )
211
- ] })
212
- ] }),
213
- filters.map((filter) => /* @__PURE__ */ jsx(
214
- FilterControl,
215
- {
216
- filter,
217
- value: filterValues[filter.key],
218
- onChange: (v) => onFilterChange(filter.key, v)
219
- },
220
- filter.key
221
- ))
222
- ] });
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;
223
212
  }
224
- function FilterControl({
225
- filter,
226
- value,
227
- onChange
228
- }) {
229
- const [options, setOptions] = useState(
230
- Array.isArray(filter.options) ? filter.options : []
231
- );
232
- useEffect(() => {
233
- if (typeof filter.options === "function") {
234
- filter.options().then(setOptions);
235
- }
236
- }, [filter.options]);
237
- if (filter.type === "select") {
238
- return /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
239
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: filter.label }),
240
- /* @__PURE__ */ jsxs(Select, { value: value ?? "", onValueChange: (v) => onChange(v === "__all__" ? "" : v), children: [
241
- /* @__PURE__ */ jsx(SelectTrigger, { className: "w-[180px]", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: filter.placeholder ?? filter.label }) }),
242
- /* @__PURE__ */ jsxs(SelectContent, { children: [
243
- /* @__PURE__ */ jsx(SelectItem, { value: "__all__", children: "All" }),
244
- options.map((opt) => /* @__PURE__ */ jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))
245
- ] })
246
- ] })
247
- ] });
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
+ }
250
+ function createSupabaseAnalyticsProvider(config) {
251
+ const resolveTenantId = () => (typeof config.tenantId === "function" ? config.tenantId() : config.tenantId) || void 0;
252
+ function client() {
253
+ const supabase = getSupabaseClientOptional();
254
+ if (!supabase) throw new Error("Supabase client not available");
255
+ return config.schema && config.schema !== "public" ? supabase.schema(config.schema) : supabase;
248
256
  }
249
- if (filter.type === "text") {
250
- return /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
251
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: filter.label }),
252
- /* @__PURE__ */ jsx(
253
- Input,
254
- {
255
- value: value ?? "",
256
- onChange: (e) => onChange(e.target.value),
257
- placeholder: filter.placeholder ?? filter.label,
258
- className: "w-[180px]"
257
+ let batchSupported = true;
258
+ return {
259
+ async run(card, state) {
260
+ const { source } = card.query;
261
+ const reserved = reservedKeys(card);
262
+ if (source.kind === "fn") {
263
+ if (!source.run) throw new Error(`analytics: card ${card.id} declares kind 'fn' with no run()`);
264
+ const rows = await source.run({
265
+ tenantId: resolveTenantId(),
266
+ dateRange: state.dateRange,
267
+ filters: state.filters
268
+ });
269
+ return aggregateRows(rows, card, state);
270
+ }
271
+ if (!source.name) throw new Error(`analytics: card ${card.id} has no source name`);
272
+ const isDetail = (card.query.dimensions?.length ?? 0) === 0 && (card.query.measures?.length ?? 0) === 0;
273
+ if (source.kind === "view" && isDetail) {
274
+ return selectFromView(card, state, client(), resolveTenantId(), reserved);
275
+ }
276
+ if (source.kind === "rpc") {
277
+ const { data: data2, error: error2 } = await client().rpc(source.name, {
278
+ p_tenant_id: resolveTenantId(),
279
+ p_from: state.dateRange.from,
280
+ p_to: state.dateRange.to,
281
+ ...Object.fromEntries(Object.entries(state.filters ?? {}).map(([k, v]) => [`p_${k}`, v]))
282
+ });
283
+ if (error2) throw error2;
284
+ const rows = Array.isArray(data2) ? data2 : data2?.rows ?? [];
285
+ return {
286
+ rows: rows.map((r) => mapRow(r, reserved, source.columnMap)),
287
+ total: Array.isArray(data2) ? rows.length : data2?.total ?? rows.length,
288
+ summary: Array.isArray(data2) ? void 0 : data2?.summary
289
+ };
290
+ }
291
+ const args = runArgs(card, state, resolveTenantId());
292
+ const { data, error } = await client().rpc("analytics_run", {
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
306
+ });
307
+ if (error) throw error;
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;
259
347
  }
260
- )
261
- ] });
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;
358
+ }
359
+ };
360
+ }
361
+
362
+ // src/data/analytics-mock.ts
363
+ function seeded(seed) {
364
+ let h = 2166136261;
365
+ for (let i = 0; i < seed.length; i++) {
366
+ h ^= seed.charCodeAt(i);
367
+ h = Math.imul(h, 16777619);
262
368
  }
263
- if (filter.type === "number") {
264
- return /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
265
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: filter.label }),
266
- /* @__PURE__ */ jsx(
267
- Input,
268
- {
269
- type: "number",
270
- value: value ?? "",
271
- onChange: (e) => onChange(e.target.value ? Number(e.target.value) : void 0),
272
- placeholder: filter.placeholder ?? filter.label,
273
- className: "w-[120px]"
369
+ return () => {
370
+ h += 1831565813;
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;
375
+ };
376
+ }
377
+ var SAMPLE_LABELS = {
378
+ product_name: ["Camiseta Preta", "Caneca Fayz", "Moletom", "Bon\xE9", "Adesivos"],
379
+ category_name: ["Vestu\xE1rio", "Acess\xF3rios", "Casa"],
380
+ customer_name: ["Ana Souza", "Bruno Lima", "Carla Dias", "Diego Alves"],
381
+ status: ["open", "archived", "cancelled"],
382
+ financial_status: ["paid", "pending", "refunded"],
383
+ fulfillment_status: ["fulfilled", "unfulfilled", "partially_fulfilled"]
384
+ };
385
+ function labelsFor(key, rand) {
386
+ const known = SAMPLE_LABELS[key];
387
+ if (known) return known;
388
+ const n = 3 + Math.floor(rand() * 3);
389
+ return Array.from({ length: n }, (_, i) => `${key.replace(/_/g, " ")} ${i + 1}`);
390
+ }
391
+ function createMockAnalyticsProvider() {
392
+ return {
393
+ async run(card, state) {
394
+ const rand = seeded(card.id);
395
+ const dateColumn = card.query.dateColumn ?? "created_at";
396
+ const dimensions = card.query.dimensions ?? [];
397
+ const measures = card.query.measures ?? [];
398
+ const from = (/* @__PURE__ */ new Date(`${state.dateRange.from}T12:00:00`)).getTime();
399
+ const to = (/* @__PURE__ */ new Date(`${state.dateRange.to}T12:00:00`)).getTime();
400
+ const span = Math.max(to - from, 864e5);
401
+ const rowCount = Math.min(400, Math.max(60, Math.round(span / 864e5) * 4));
402
+ const categoricalDims = dimensions.filter((d) => !d.grain && d.key !== dateColumn);
403
+ const labelPools = new Map(categoricalDims.map((d) => [d.key, labelsFor(d.key, rand)]));
404
+ const rows = Array.from({ length: rowCount }, () => {
405
+ const row = {
406
+ [dateColumn]: new Date(from + rand() * span).toISOString()
407
+ };
408
+ for (const [key, pool] of labelPools) {
409
+ row[key] = pool[Math.floor(rand() * pool.length)];
274
410
  }
275
- )
276
- ] });
411
+ for (const m of measures) {
412
+ if (m.agg === "count" || m.agg === "ratio") continue;
413
+ const column = m.column ?? m.key;
414
+ row[column] = m.format === "currency" ? Math.round(rand() * 4e4) / 100 : m.format === "percent" ? Math.round(rand() * 1e4) / 100 : Math.round(rand() * 50);
415
+ }
416
+ for (const c of card.columns ?? []) {
417
+ if (c.key in row) continue;
418
+ row[c.key] = c.type === "currency" || c.type === "number" ? Math.round(rand() * 1e4) / 100 : c.type === "date" ? new Date(from + rand() * span).toISOString() : `${c.label} ${Math.ceil(rand() * 20)}`;
419
+ }
420
+ return row;
421
+ });
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)))));
432
+ }
433
+ };
434
+ }
435
+ var override;
436
+ function setReportsTenantId(id) {
437
+ override = id;
438
+ }
439
+ function getReportsTenantId() {
440
+ if (override) return override;
441
+ try {
442
+ return useOrganizationStore.getState().currentOrg?.id;
443
+ } catch {
444
+ return void 0;
277
445
  }
278
- return null;
279
446
  }
280
- function ReportPagination({ page, pageSize, total, onPageChange, onPageSizeChange }) {
281
- const totalPages = Math.max(1, Math.ceil(total / pageSize));
282
- const from = (page - 1) * pageSize + 1;
283
- const to = Math.min(page * pageSize, total);
284
- return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-t px-4 py-3", children: [
285
- /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: total > 0 ? `${from}\u2013${to} of ${total}` : "No results" }),
286
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
447
+
448
+ // src/locales/en.ts
449
+ var en = {
450
+ "reports.pageTitle": "Reports",
451
+ "reports.pageSubtitle": "Access complete reports for analysis and decision making",
452
+ "reports.overview": "Overview",
453
+ "reports.allReports": "All reports",
454
+ "reports.dateRange": "Date Range",
455
+ "reports.from": "From",
456
+ "reports.to": "To",
457
+ "reports.export": "Export",
458
+ "reports.exportCsv": "Export CSV",
459
+ "reports.exportExcel": "Export Excel",
460
+ "reports.exportPdf": "Export PDF",
461
+ "reports.noResults": "No results found.",
462
+ "reports.backToReports": "Back",
463
+ "reports.reports": "reports",
464
+ "reports.search": "Search reports...",
465
+ "reports.today": "Today",
466
+ "reports.yesterday": "Yesterday",
467
+ "reports.last7Days": "Last 7 days",
468
+ "reports.thisMonth": "This month",
469
+ "reports.lastMonth": "Last month",
470
+ "reports.thisQuarter": "This quarter",
471
+ "reports.custom": "Custom",
472
+ "reports.generate": "Generate",
473
+ "reports.emptyStateTitle": "Ready to generate",
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?",
486
+ "reports.unavailableTitle": "Coming soon",
487
+ "reports.unavailableDescription": "This report is not available yet. It will be enabled in a future update."
488
+ };
489
+
490
+ // src/locales/pt-BR.ts
491
+ var ptBR = {
492
+ "reports.pageTitle": "Relat\xF3rios",
493
+ "reports.pageSubtitle": "Acesse relat\xF3rios completos para an\xE1lise e tomada de decis\xE3o",
494
+ "reports.overview": "Vis\xE3o geral",
495
+ "reports.allReports": "Todos os relat\xF3rios",
496
+ "reports.dateRange": "Per\xEDodo",
497
+ "reports.from": "De",
498
+ "reports.to": "At\xE9",
499
+ "reports.export": "Exportar",
500
+ "reports.exportCsv": "Exportar CSV",
501
+ "reports.exportExcel": "Exportar Excel",
502
+ "reports.exportPdf": "Exportar PDF",
503
+ "reports.noResults": "Nenhum resultado encontrado.",
504
+ "reports.backToReports": "Voltar",
505
+ "reports.reports": "relat\xF3rios",
506
+ "reports.search": "Buscar relat\xF3rios...",
507
+ "reports.today": "Hoje",
508
+ "reports.yesterday": "Ontem",
509
+ "reports.last7Days": "\xDAltimos 7 dias",
510
+ "reports.thisMonth": "Este m\xEAs",
511
+ "reports.lastMonth": "M\xEAs passado",
512
+ "reports.thisQuarter": "Este trimestre",
513
+ "reports.custom": "Personalizado",
514
+ "reports.generate": "Gerar",
515
+ "reports.emptyStateTitle": "Pronto para gerar",
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?",
525
+ "reports.unavailableTitle": "Em breve",
526
+ "reports.unavailableDescription": "Este relat\xF3rio ainda n\xE3o est\xE1 dispon\xEDvel. Ser\xE1 habilitado em uma atualiza\xE7\xE3o futura."
527
+ };
528
+
529
+ // src/locales/index.ts
530
+ var reportsLocales = {
531
+ en,
532
+ "pt-BR": ptBR
533
+ };
534
+
535
+ // src/lib/legacy.ts
536
+ function reportDefToCard(report, createdBy) {
537
+ return {
538
+ id: report.id,
539
+ name: report.name,
540
+ description: report.description,
541
+ icon: report.icon,
542
+ category: report.category,
543
+ createdBy,
544
+ available: report.available,
545
+ grain: report.grain,
546
+ canonicalOwner: report.canonicalOwner,
547
+ permission: report.permission,
548
+ query: {
549
+ source: {
550
+ kind: report.dataSource.kind,
551
+ name: report.dataSource.name,
552
+ columnMap: report.dataSource.columnMap
553
+ },
554
+ dateColumn: report.dataSource.dateColumn,
555
+ defaultSort: report.dataSource.defaultSort,
556
+ defaultSortDir: report.dataSource.defaultSortDir,
557
+ filters: (report.filters ?? []).map((f) => ({
558
+ key: f.key,
559
+ label: f.label,
560
+ type: f.type,
561
+ options: f.options,
562
+ defaultValue: f.defaultValue,
563
+ placeholder: f.placeholder,
564
+ // Legacy filters declared no category. Grouping them under the report's
565
+ // own category keeps the picker organised instead of dumping every
566
+ // filter from every report into one flat list.
567
+ group: report.category
568
+ }))
569
+ },
570
+ // No dimensions/measures: a legacy report is a detail listing, and inventing
571
+ // aggregations for it would change what the numbers mean.
572
+ viz: { type: "table" },
573
+ columns: report.columns.map((c) => ({
574
+ key: c.key,
575
+ label: c.label,
576
+ type: c.type,
577
+ sortable: c.sortable,
578
+ currency: c.currency,
579
+ idKey: c.idKey,
580
+ renderCell: c.renderCell,
581
+ visible: c.visible,
582
+ align: c.align,
583
+ aggregate: c.aggregate
584
+ }))
585
+ };
586
+ }
587
+ function reportDefsToCards(reports, createdBy) {
588
+ return reports.map((r) => reportDefToCard(r, createdBy));
589
+ }
590
+ var KEY = "fayz.analytics.lastViewed";
591
+ function read() {
592
+ if (typeof window === "undefined") return {};
593
+ try {
594
+ const raw = window.localStorage.getItem(KEY);
595
+ return raw ? JSON.parse(raw) : {};
596
+ } catch {
597
+ return {};
598
+ }
599
+ }
600
+ function write(map) {
601
+ if (typeof window === "undefined") return;
602
+ try {
603
+ window.localStorage.setItem(KEY, JSON.stringify(map));
604
+ } catch {
605
+ }
606
+ }
607
+ var listeners = /* @__PURE__ */ new Set();
608
+ function markViewed(cardId) {
609
+ const next = { ...read(), [cardId]: (/* @__PURE__ */ new Date()).toISOString() };
610
+ write(next);
611
+ for (const listener of listeners) listener(next);
612
+ }
613
+ function useLastViewed() {
614
+ const [map, setMap] = React5.useState(() => read());
615
+ React5.useEffect(() => {
616
+ listeners.add(setMap);
617
+ return () => {
618
+ listeners.delete(setMap);
619
+ };
620
+ }, []);
621
+ return map;
622
+ }
623
+ var ANALYTICS_BASE = "/analytics";
624
+ var REPORTS_BASE = `${ANALYTICS_BASE}/reports`;
625
+ var ConfigContext = React5__default.createContext(null);
626
+ function useAnalyticsConfig() {
627
+ const ctx = React5__default.useContext(ConfigContext);
628
+ if (!ctx) throw new Error("useAnalyticsConfig must be used inside <AnalyticsSurface>");
629
+ return ctx;
630
+ }
631
+ function useAnalyticsCards() {
632
+ const runtime = usePluginRuntime();
633
+ const { cards, createdBy } = useAnalyticsConfig();
634
+ return React5__default.useMemo(() => {
635
+ const byId = /* @__PURE__ */ new Map();
636
+ for (const card of getAnalyticsCards(runtime)) byId.set(card.id, card);
637
+ for (const card of cards) byId.set(card.id, card);
638
+ return [...byId.values()].map((c) => c.createdBy ? c : { ...c, createdBy });
639
+ }, [runtime, cards, createdBy]);
640
+ }
641
+ function useOverviewCards() {
642
+ const all = useAnalyticsCards();
643
+ return React5__default.useMemo(
644
+ () => all.filter((c) => c.dashboard && c.dashboard.defaultVisible !== false).sort((a, b) => (a.dashboard?.order ?? 0) - (b.dashboard?.order ?? 0)),
645
+ [all]
646
+ );
647
+ }
648
+ function cardColumns(card) {
649
+ if (card.columns?.length) return card.columns;
650
+ return [
651
+ ...(card.query.dimensions ?? []).map((d) => ({
652
+ key: d.key,
653
+ label: d.label,
654
+ type: d.format ?? (d.grain ? "date" : "text"),
655
+ align: "left"
656
+ })),
657
+ ...(card.query.measures ?? []).map((m) => ({
658
+ key: m.key,
659
+ label: m.label,
660
+ type: m.format ?? "number",
661
+ align: "right",
662
+ sortable: true
663
+ }))
664
+ ];
665
+ }
666
+ function navigateAnalytics(path) {
667
+ hashRouterAdapter().navigate(path);
668
+ }
669
+ function AnalyticsSurface({ config, provider, children }) {
670
+ const openCard = React5__default.useCallback((card) => {
671
+ markViewed(card.id);
672
+ navigateAnalytics(`${REPORTS_BASE}/${encodeURIComponent(card.id)}`);
673
+ }, []);
674
+ return /* @__PURE__ */ jsx(ConfigContext.Provider, { value: config, children: /* @__PURE__ */ jsx(
675
+ AnalyticsContextProvider,
676
+ {
677
+ provider,
678
+ currency: config.currency,
679
+ defaultPreset: config.defaultRange,
680
+ defaultCompare: config.defaultCompare,
681
+ defaultPageSize: config.defaultPageSize,
682
+ onOpenCard: openCard,
683
+ createdBy: config.createdBy,
684
+ children
685
+ }
686
+ ) });
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
+ }
789
+ function AnalyticsOverview() {
790
+ const { labels, showHeader, assistant } = useAnalyticsConfig();
791
+ const cards = useOverviewCards();
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
+ ] })
823
+ ] });
824
+ }
825
+ function formatViewed(iso, locale, never) {
826
+ if (!iso) return never;
827
+ const d = new Date(iso);
828
+ if (Number.isNaN(d.getTime())) return never;
829
+ return d.toLocaleDateString(locale, { day: "numeric", month: "short", year: "numeric" });
830
+ }
831
+ function ReportsListPage() {
832
+ const { labels, currency, showHeader } = useAnalyticsConfig();
833
+ const cards = useAnalyticsCards();
834
+ const lastViewed = useLastViewed();
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(
839
+ () => [...new Set(cards.map((c) => c.category))].sort((a, b) => a.localeCompare(b)).map((c) => ({ value: c, label: c })),
840
+ [cards]
841
+ );
842
+ const authorOptions = React5__default.useMemo(
843
+ () => [...new Set(cards.map((c) => c.createdBy ?? "\u2014"))].sort((a, b) => a.localeCompare(b)).map((a) => ({ value: a, label: a })),
844
+ [cards]
845
+ );
846
+ const rows = React5__default.useMemo(() => {
847
+ const q = search.trim().toLowerCase();
848
+ return cards.filter((card) => {
849
+ if (category && card.category !== category) return false;
850
+ if (author && (card.createdBy ?? "\u2014") !== author) return false;
851
+ if (!q) return true;
852
+ return card.name.toLowerCase().includes(q) || card.description?.toLowerCase().includes(q) || card.category.toLowerCase().includes(q);
853
+ });
854
+ }, [cards, search, category, author]);
855
+ const columns = React5__default.useMemo(() => [
856
+ {
857
+ id: "name",
858
+ accessorFn: (card) => card.name,
859
+ header: labels.name,
860
+ cell: ({ row }) => /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
861
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: row.original.name }),
862
+ row.original.available === false ? /* @__PURE__ */ jsx("span", { className: "ml-2 rounded-full border px-1.5 py-0.5 text-[10px] text-muted-foreground", children: labels.unavailableTitle }) : null,
863
+ row.original.description ? /* @__PURE__ */ jsx("p", { className: "mt-0.5 line-clamp-1 text-muted-foreground sm:hidden", children: row.original.description }) : null
864
+ ] })
865
+ },
866
+ {
867
+ id: "category",
868
+ accessorFn: (card) => card.category,
869
+ header: labels.category,
870
+ cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "rounded-md bg-muted px-2 py-0.5", children: String(getValue() ?? "") })
871
+ },
872
+ {
873
+ id: "lastViewed",
874
+ // Sorts on the timestamp, renders the date. Never-opened reports carry
875
+ // undefined so they sink to the bottom in BOTH directions rather than
876
+ // topping "most recent" as an epoch-zero date.
877
+ accessorFn: (card) => {
878
+ const iso = lastViewed[card.id];
879
+ return iso ? new Date(iso).getTime() : void 0;
880
+ },
881
+ sortUndefined: 1,
882
+ header: labels.lastViewed,
883
+ cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatViewed(lastViewed[row.original.id], currency.locale, labels.never) })
884
+ },
885
+ {
886
+ id: "createdBy",
887
+ accessorFn: (card) => card.createdBy ?? "\u2014",
888
+ header: labels.createdBy,
889
+ cell: ({ getValue }) => /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5 text-muted-foreground", children: [
890
+ /* @__PURE__ */ jsx(User, { className: "h-3 w-3" }),
891
+ String(getValue() ?? "\u2014")
892
+ ] })
893
+ }
894
+ ], [labels, currency.locale, lastViewed]);
895
+ const open = (card) => {
896
+ markViewed(card.id);
897
+ navigateAnalytics(`${REPORTS_BASE}/${encodeURIComponent(card.id)}`);
898
+ };
899
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
900
+ showHeader ? /* @__PURE__ */ jsxs("div", { children: [
901
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold tracking-tight", children: labels.reports }),
902
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: labels.reportsSubtitle })
903
+ ] }) : null,
904
+ /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
905
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2 border-b border-border px-3 py-2", children: [
906
+ /* @__PURE__ */ jsx("div", { className: "min-w-[10rem] flex-1", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
907
+ /* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
908
+ /* @__PURE__ */ jsx(
909
+ "input",
910
+ {
911
+ type: "text",
912
+ value: search,
913
+ onChange: (e) => setSearch(e.target.value),
914
+ placeholder: labels.searchReports,
915
+ className: "w-full bg-transparent py-1.5 pl-8 pr-3 text-sm placeholder:text-muted-foreground focus:outline-none"
916
+ }
917
+ )
918
+ ] }) }),
919
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-wrap items-center gap-1.5", children: [
920
+ /* @__PURE__ */ jsx(
921
+ FilterChip,
922
+ {
923
+ label: labels.category,
924
+ allLabel: labels.allCategories,
925
+ options: categoryOptions,
926
+ value: category,
927
+ onChange: setCategory
928
+ }
929
+ ),
930
+ /* @__PURE__ */ jsx(
931
+ FilterChip,
932
+ {
933
+ label: labels.createdBy,
934
+ allLabel: labels.allCreatedBy,
935
+ options: authorOptions,
936
+ value: author,
937
+ onChange: setAuthor
938
+ }
939
+ )
940
+ ] })
941
+ ] }),
287
942
  /* @__PURE__ */ jsx(
288
- "select",
943
+ DataTable,
289
944
  {
290
- value: pageSize,
291
- onChange: (e) => onPageSizeChange(Number(e.target.value)),
292
- className: "h-8 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2 text-sm",
293
- children: [25, 50, 100].map((size) => /* @__PURE__ */ jsxs("option", { value: size, children: [
294
- size,
295
- " / page"
296
- ] }, size))
945
+ columns,
946
+ data: rows,
947
+ variant: "bare",
948
+ compact: true,
949
+ onRowClick: open,
950
+ emptyMessage: labels.noResults
297
951
  }
298
952
  ),
299
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
300
- /* @__PURE__ */ jsx(
301
- Button,
302
- {
303
- variant: "outline",
304
- size: "sm",
305
- disabled: page <= 1,
306
- onClick: () => onPageChange(page - 1),
307
- children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" })
308
- }
309
- ),
310
- /* @__PURE__ */ jsxs("span", { className: "text-sm px-2", children: [
311
- page,
312
- " / ",
313
- totalPages
314
- ] }),
315
- /* @__PURE__ */ jsx(
316
- Button,
317
- {
318
- variant: "outline",
319
- size: "sm",
320
- disabled: page >= totalPages,
321
- onClick: () => onPageChange(page + 1),
322
- children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" })
323
- }
324
- )
953
+ /* @__PURE__ */ jsxs("div", { className: "border-t border-border px-3 py-2 text-xs text-muted-foreground", children: [
954
+ rows.length,
955
+ " ",
956
+ labels.results
325
957
  ] })
326
958
  ] })
327
959
  ] });
328
960
  }
329
- function ReportSummaryRow({ columns, data, currency }) {
330
- const visibleCols = columns.filter((c) => c.visible !== false);
331
- const hasAggregates = visibleCols.some((c) => c.aggregate);
332
- if (!hasAggregates || data.length === 0) return null;
333
- return /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-muted/30 overflow-hidden", children: /* @__PURE__ */ jsx("table", { className: "w-full text-xs", children: /* @__PURE__ */ jsx("tbody", { children: /* @__PURE__ */ jsx("tr", { className: "font-semibold", children: visibleCols.map((col) => {
334
- const value = computeAggregate(col, data);
335
- const alignRight = col.align === "right" || col.type === "currency" || col.type === "number";
336
- return /* @__PURE__ */ jsx(
337
- "td",
338
- {
339
- className: `px-3 py-1.5 ${alignRight ? "text-right" : ""}`,
340
- children: value != null ? formatValuePlain(value, col, currency) : col.aggregate ? "\u2014" : ""
341
- },
342
- col.key
343
- );
344
- }) }) }) }) });
345
- }
346
- function computeAggregate(col, data) {
347
- if (!col.aggregate) return null;
348
- const values = data.map((row) => row[col.key]).filter((v) => v != null && !isNaN(Number(v))).map(Number);
349
- if (values.length === 0) return null;
350
- switch (col.aggregate) {
351
- case "sum":
352
- return values.reduce((a, b) => a + b, 0);
353
- case "avg":
354
- return values.reduce((a, b) => a + b, 0) / values.length;
355
- case "count":
356
- return values.length;
357
- case "min":
358
- return Math.min(...values);
359
- case "max":
360
- return Math.max(...values);
361
- default:
362
- return null;
363
- }
364
- }
365
961
  function exportToCsv(columns, data, filename, currency) {
366
962
  const visibleCols = columns.filter((c) => c.visible !== false);
367
963
  const csvColumns = visibleCols.map((col) => ({
@@ -372,235 +968,221 @@ function exportToCsv(columns, data, filename, currency) {
372
968
  exportCSV(csvColumns, data, filename);
373
969
  }
374
970
  function ExportDropdown({ columns, data, filename, currency, title }) {
375
- const t2 = useTranslation();
971
+ const t = useTranslation();
376
972
  const [excelAvailable, setExcelAvailable] = useState(false);
377
973
  const [pdfAvailable, setPdfAvailable] = useState(false);
378
974
  useEffect(() => {
379
- import('./excel-QHUSM6IH.js').then((m) => m.isExcelAvailable()).then(setExcelAvailable).catch(() => {
975
+ import('./excel-WE73YE2V.js').then((m) => m.isExcelAvailable()).then(setExcelAvailable).catch(() => {
380
976
  });
381
- import('./pdf-HETKN26Y.js').then((m) => m.isPdfAvailable()).then(setPdfAvailable).catch(() => {
977
+ import('./pdf-E6DXHFXU.js').then((m) => m.isPdfAvailable()).then(setPdfAvailable).catch(() => {
382
978
  });
383
979
  }, []);
384
980
  const handleCsv = () => exportToCsv(columns, data, filename, currency);
385
981
  const handleExcel = async () => {
386
- const { exportToExcel } = await import('./excel-QHUSM6IH.js');
982
+ const { exportToExcel } = await import('./excel-WE73YE2V.js');
387
983
  await exportToExcel(columns, data, filename, currency);
388
984
  };
389
985
  const handlePdf = async () => {
390
- const { exportToPdf } = await import('./pdf-HETKN26Y.js');
986
+ const { exportToPdf } = await import('./pdf-E6DXHFXU.js');
391
987
  await exportToPdf(columns, data, filename, { title, currency });
392
988
  };
393
989
  return /* @__PURE__ */ jsxs(Dropdown, { children: [
394
990
  /* @__PURE__ */ jsx(DropdownTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", children: [
395
991
  /* @__PURE__ */ jsx(Download, { className: "h-4 w-4 mr-1.5" }),
396
- t2("reports.export")
992
+ t("reports.export")
397
993
  ] }) }),
398
994
  /* @__PURE__ */ jsxs(DropdownContent, { align: "end", children: [
399
- /* @__PURE__ */ jsx(DropdownItem, { onClick: handleCsv, children: t2("reports.exportCsv") }),
400
- excelAvailable && /* @__PURE__ */ jsx(DropdownItem, { onClick: handleExcel, children: t2("reports.exportExcel") }),
401
- 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") })
402
998
  ] })
403
999
  ] });
404
1000
  }
405
- function ReportViewer({ report, onBack }) {
406
- const t2 = useTranslation();
407
- const { config, provider } = useReportsContext();
408
- const {
409
- data,
410
- total,
411
- loading,
412
- generated,
413
- error,
414
- query,
415
- generate,
416
- setDateRange,
417
- setFilter,
418
- setPage,
419
- setPageSize
420
- } = useReportData(report, provider, config.defaultPageSize);
421
- const isAvailable = report.available !== false;
422
- const tanstackColumns = useMemo(
423
- () => reportColumnsToTanstack(report.columns, config.currency.code),
424
- [report.columns, config.currency.code]
425
- );
426
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
427
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-sm no-print", children: [
428
- /* @__PURE__ */ jsxs(
429
- "button",
430
- {
431
- onClick: onBack,
432
- className: "flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors",
433
- children: [
434
- /* @__PURE__ */ jsx(ArrowLeft, { className: "h-3.5 w-3.5" }),
435
- t2("reports.pageTitle")
436
- ]
437
- }
438
- ),
439
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "/" }),
440
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: report.name })
441
- ] }),
442
- /* @__PURE__ */ jsxs("div", { children: [
443
- /* @__PURE__ */ jsx("h2", { className: "text-lg font-bold", children: report.name }),
444
- report.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mt-0.5", children: report.description })
445
- ] }),
446
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-end gap-3 no-print", children: [
1001
+ function ReportPagination({ page: page2, pageSize, total, onPageChange, onPageSizeChange }) {
1002
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
1003
+ const from = (page2 - 1) * pageSize + 1;
1004
+ const to = Math.min(page2 * pageSize, total);
1005
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-t px-4 py-3", children: [
1006
+ /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: total > 0 ? `${from}\u2013${to} of ${total}` : "No results" }),
1007
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
447
1008
  /* @__PURE__ */ jsx(
448
- ReportFilterBar,
1009
+ "select",
449
1010
  {
450
- dateRange: query.dateRange,
451
- onDateRangeChange: setDateRange,
452
- filters: report.filters ?? [],
453
- filterValues: query.filters,
454
- onFilterChange: setFilter
1011
+ value: pageSize,
1012
+ onChange: (e) => onPageSizeChange(Number(e.target.value)),
1013
+ className: "h-8 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2 text-sm",
1014
+ children: [25, 50, 100].map((size) => /* @__PURE__ */ jsxs("option", { value: size, children: [
1015
+ size,
1016
+ " / page"
1017
+ ] }, size))
455
1018
  }
456
1019
  ),
457
- /* @__PURE__ */ jsxs(Button, { onClick: generate, disabled: loading || !isAvailable, size: "sm", className: "h-9", children: [
458
- loading ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 mr-1.5 animate-spin" }) : /* @__PURE__ */ jsx(Play, { className: "h-3.5 w-3.5 mr-1.5" }),
459
- t2("reports.generate")
460
- ] }),
461
- generated && /* @__PURE__ */ jsxs(Fragment, { children: [
1020
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
462
1021
  /* @__PURE__ */ jsx(
463
- ExportDropdown,
1022
+ Button,
464
1023
  {
465
- columns: report.columns,
466
- data,
467
- filename: report.id,
468
- currency: config.currency.code,
469
- title: report.name
1024
+ variant: "outline",
1025
+ size: "sm",
1026
+ disabled: page2 <= 1,
1027
+ onClick: () => onPageChange(page2 - 1),
1028
+ children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" })
470
1029
  }
471
1030
  ),
472
- /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "h-9", onClick: () => window.print(), children: [
473
- /* @__PURE__ */ jsx(Printer, { className: "h-3.5 w-3.5 mr-1.5" }),
474
- t2("reports.print")
475
- ] })
476
- ] })
477
- ] }),
478
- generated && /* @__PURE__ */ jsx("div", { className: "print-only hidden text-[10px] border-b pb-2 mb-2", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-x-4 gap-y-0.5", children: [
479
- query.dateRange.from && /* @__PURE__ */ jsxs("span", { children: [
480
- /* @__PURE__ */ jsxs("strong", { children: [
481
- t2("reports.filterFrom"),
482
- ":"
483
- ] }),
484
- " ",
485
- (/* @__PURE__ */ new Date(query.dateRange.from + "T12:00")).toLocaleDateString()
486
- ] }),
487
- query.dateRange.to && /* @__PURE__ */ jsxs("span", { children: [
488
- /* @__PURE__ */ jsxs("strong", { children: [
489
- t2("reports.filterTo"),
490
- ":"
491
- ] }),
492
- " ",
493
- (/* @__PURE__ */ new Date(query.dateRange.to + "T12:00")).toLocaleDateString()
494
- ] }),
495
- Object.entries(query.filters).map(
496
- ([key, value]) => value ? /* @__PURE__ */ jsxs("span", { children: [
497
- /* @__PURE__ */ jsxs("strong", { children: [
498
- key,
499
- ":"
500
- ] }),
501
- " ",
502
- String(value)
503
- ] }, key) : null
504
- ),
505
- /* @__PURE__ */ jsxs("span", { children: [
506
- /* @__PURE__ */ jsxs("strong", { children: [
507
- t2("reports.filterResults"),
508
- ":"
1031
+ /* @__PURE__ */ jsxs("span", { className: "text-sm px-2", children: [
1032
+ page2,
1033
+ " / ",
1034
+ totalPages
509
1035
  ] }),
510
- " ",
511
- total
1036
+ /* @__PURE__ */ jsx(
1037
+ Button,
1038
+ {
1039
+ variant: "outline",
1040
+ size: "sm",
1041
+ disabled: page2 >= totalPages,
1042
+ onClick: () => onPageChange(page2 + 1),
1043
+ children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" })
1044
+ }
1045
+ )
512
1046
  ] })
513
- ] }) }),
514
- error && /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive", children: error }),
515
- !generated && !loading && /* @__PURE__ */ jsx("div", { className: "flex flex-col items-center justify-center rounded-xl border border-dashed py-16 text-center", children: isAvailable ? /* @__PURE__ */ jsxs(Fragment, { children: [
516
- /* @__PURE__ */ jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-muted mb-4", children: /* @__PURE__ */ jsx(SlidersHorizontal, { className: "h-5 w-5 text-muted-foreground" }) }),
517
- /* @__PURE__ */ jsx("p", { className: "font-medium", children: t2("reports.emptyStateTitle") }),
518
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mt-1 max-w-sm", children: t2("reports.emptyStateDescription") })
519
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
520
- /* @__PURE__ */ jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-muted mb-4", children: /* @__PURE__ */ jsx(Construction, { className: "h-5 w-5 text-muted-foreground" }) }),
521
- /* @__PURE__ */ jsx("p", { className: "font-medium", children: t2("reports.unavailableTitle") }),
522
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mt-1 max-w-sm", children: t2("reports.unavailableDescription") })
523
- ] }) }),
524
- (generated || loading) && /* @__PURE__ */ jsxs(Fragment, { children: [
525
- /* @__PURE__ */ jsx(
526
- DataTable,
527
- {
528
- columns: tanstackColumns,
529
- data,
530
- loading,
531
- skeletonRows: 5,
532
- emptyMessage: t2("reports.noResults"),
533
- variant: "card",
534
- compact: true
535
- }
536
- ),
537
- report.showSummary && !loading && /* @__PURE__ */ jsx(
538
- ReportSummaryRow,
539
- {
540
- columns: report.columns,
541
- data,
542
- currency: config.currency.code
543
- }
544
- ),
545
- !loading && /* @__PURE__ */ jsx("div", { className: "no-print", children: /* @__PURE__ */ jsx(
546
- ReportPagination,
547
- {
548
- page: query.page,
549
- pageSize: query.pageSize,
550
- total,
551
- onPageChange: setPage,
552
- onPageSizeChange: setPageSize
553
- }
554
- ) })
555
1047
  ] })
556
1048
  ] });
557
1049
  }
558
- var HASH_BASE = "/reports";
559
- function getReportIdFromHash() {
560
- const hash = (window.location.hash.slice(1) || "/").split("?")[0];
561
- if (hash.startsWith(HASH_BASE + "/") && hash.length > HASH_BASE.length + 1) {
562
- return hash.slice(HASH_BASE.length + 1);
1050
+ function ViewerBody({ card }) {
1051
+ const { labels, currency } = useAnalyticsConfig();
1052
+ const { state, setPage, setPageSize } = useAnalyticsContext();
1053
+ const { result, loading, error, engineMissing } = useAnalyticsCard(card);
1054
+ const columns = React5__default.useMemo(() => cardColumns(card), [card]);
1055
+ const tanstackColumns = React5__default.useMemo(
1056
+ () => reportColumnsToTanstack(columns, currency.code),
1057
+ [columns, currency.code]
1058
+ );
1059
+ const rows = result?.rows ?? [];
1060
+ if (card.available === false) {
1061
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center rounded-xl border border-dashed py-16 text-center", children: [
1062
+ /* @__PURE__ */ jsx("div", { className: "mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: /* @__PURE__ */ jsx(Construction, { className: "h-5 w-5 text-muted-foreground" }) }),
1063
+ /* @__PURE__ */ jsx("p", { className: "font-medium", children: labels.unavailableTitle }),
1064
+ /* @__PURE__ */ jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: labels.unavailableDescription })
1065
+ ] });
563
1066
  }
564
- return null;
1067
+ const hasViz = card.viz.type !== "table" || (card.query.measures?.length ?? 0) > 0;
1068
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1069
+ hasViz ? /* @__PURE__ */ jsx(
1070
+ AnalyticsCard,
1071
+ {
1072
+ card: { ...card, viz: { ...card.viz, height: card.viz.height ?? 280, limit: void 0 } },
1073
+ hideOpen: true
1074
+ }
1075
+ ) : null,
1076
+ engineMissing ? /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-muted/40 p-3 text-sm", children: [
1077
+ /* @__PURE__ */ jsx("p", { className: "font-medium", children: "Analytics engine not installed" }),
1078
+ /* @__PURE__ */ jsxs("p", { className: "mt-0.5 text-muted-foreground", children: [
1079
+ "This report aggregates server-side and needs migration 0039 (",
1080
+ /* @__PURE__ */ jsx("code", { className: "text-xs", children: "analytics_run" }),
1081
+ " + the read models) applied to this project."
1082
+ ] })
1083
+ ] }) : error ? /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive", children: error }) : null,
1084
+ /* @__PURE__ */ jsx(
1085
+ DataTable,
1086
+ {
1087
+ columns: tanstackColumns,
1088
+ data: rows,
1089
+ loading,
1090
+ skeletonRows: 5,
1091
+ emptyMessage: labels.noResults,
1092
+ variant: "card",
1093
+ compact: true
1094
+ }
1095
+ ),
1096
+ !loading ? /* @__PURE__ */ jsx("div", { className: "no-print", children: /* @__PURE__ */ jsx(
1097
+ ReportPagination,
1098
+ {
1099
+ page: state.page,
1100
+ pageSize: state.pageSize,
1101
+ total: result?.total ?? 0,
1102
+ onPageChange: setPage,
1103
+ onPageSizeChange: setPageSize
1104
+ }
1105
+ ) }) : null
1106
+ ] });
565
1107
  }
566
- function ReportsPage({
567
- config,
568
- provider
569
- }) {
570
- const [activeReportId, setActiveReportId] = useState(getReportIdFromHash);
571
- useEffect(() => {
572
- function handler() {
573
- setActiveReportId(getReportIdFromHash());
574
- }
575
- window.addEventListener("hashchange", handler);
576
- return () => window.removeEventListener("hashchange", handler);
577
- }, []);
578
- const handleSelect = useCallback((report) => {
579
- window.location.hash = `${HASH_BASE}/${report.id}`;
580
- }, []);
581
- const handleBack = useCallback(() => {
582
- window.location.hash = HASH_BASE;
583
- }, []);
584
- const activeReport = activeReportId ? config.reports.find((r) => r.id === activeReportId) ?? null : null;
585
- return /* @__PURE__ */ jsx(ReportsContextProvider, { config, provider, children: /* @__PURE__ */ jsx("div", { className: "p-1", children: activeReport ? /* @__PURE__ */ jsx(
586
- ReportViewer,
587
- {
588
- report: activeReport,
589
- onBack: handleBack
590
- },
591
- activeReport.id
592
- ) : /* @__PURE__ */ jsx(ReportHub, { onSelect: handleSelect }) }) });
1108
+ function AnalyticsReportViewer({ cardId }) {
1109
+ const { labels } = useAnalyticsConfig();
1110
+ const cards = useAnalyticsCards();
1111
+ const card = cards.find((c) => c.id === decodeURIComponent(cardId ?? ""));
1112
+ React5__default.useEffect(() => {
1113
+ if (card) markViewed(card.id);
1114
+ }, [card?.id]);
1115
+ if (!card) {
1116
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1117
+ /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigateAnalytics(REPORTS_BASE), children: [
1118
+ /* @__PURE__ */ jsx(ArrowLeft, { className: "mr-1.5 h-3.5 w-3.5" }),
1119
+ labels.reports
1120
+ ] }),
1121
+ /* @__PURE__ */ jsx("p", { className: "py-12 text-center text-sm text-muted-foreground", children: labels.noResults })
1122
+ ] });
1123
+ }
1124
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1125
+ /* @__PURE__ */ jsx(
1126
+ SubpageHeader,
1127
+ {
1128
+ title: card.name,
1129
+ parentLabel: labels.analytics,
1130
+ onBack: () => navigateAnalytics(ANALYTICS_BASE)
1131
+ }
1132
+ ),
1133
+ card.description ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: card.description }) : null,
1134
+ /* @__PURE__ */ jsx(
1135
+ AnalyticsControls,
1136
+ {
1137
+ filters: card.query.filters ?? [],
1138
+ showCompare: card.compare !== false,
1139
+ className: "no-print",
1140
+ actions: /* @__PURE__ */ jsx(ViewerActions, { card })
1141
+ }
1142
+ ),
1143
+ /* @__PURE__ */ jsx(ViewerBody, { card })
1144
+ ] });
1145
+ }
1146
+ function ViewerActions({ card }) {
1147
+ const { currency } = useAnalyticsConfig();
1148
+ const { result } = useAnalyticsCard(card);
1149
+ const columns = React5__default.useMemo(() => cardColumns(card), [card]);
1150
+ if (!result?.rows?.length) return null;
1151
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1152
+ /* @__PURE__ */ jsx(
1153
+ ExportDropdown,
1154
+ {
1155
+ columns,
1156
+ data: result.rows,
1157
+ filename: card.id,
1158
+ currency: currency.code,
1159
+ title: card.name
1160
+ }
1161
+ ),
1162
+ /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "h-8", onClick: () => window.print(), children: [
1163
+ /* @__PURE__ */ jsx(Printer, { className: "mr-1.5 h-3.5 w-3.5" }),
1164
+ "Print"
1165
+ ] })
1166
+ ] });
593
1167
  }
594
- function camelToSnake(str) {
1168
+ function LiveViewPage() {
1169
+ const { labels } = useAnalyticsConfig();
1170
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center rounded-xl border border-dashed py-20 text-center", children: [
1171
+ /* @__PURE__ */ jsx("div", { className: "mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: /* @__PURE__ */ jsx(Radio, { className: "h-5 w-5 text-muted-foreground" }) }),
1172
+ /* @__PURE__ */ jsx("p", { className: "font-medium", children: labels.liveView }),
1173
+ /* @__PURE__ */ jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: labels.liveViewSoon })
1174
+ ] });
1175
+ }
1176
+ function camelToSnake2(str) {
595
1177
  return str.replace(/[A-Z]/g, (c) => "_" + c.toLowerCase());
596
1178
  }
597
- function snakeToCamel(str) {
1179
+ function snakeToCamel2(str) {
598
1180
  return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
599
1181
  }
600
- function mapRow(row, columnMap) {
1182
+ function mapRow2(row, columnMap) {
601
1183
  const result = {};
602
1184
  for (const [key, value] of Object.entries(row)) {
603
- const camelKey = columnMap?.[key] ?? snakeToCamel(key);
1185
+ const camelKey = columnMap?.[key] ?? snakeToCamel2(key);
604
1186
  result[camelKey] = value;
605
1187
  }
606
1188
  return result;
@@ -644,7 +1226,7 @@ async function fetchViaView(reportDef, query, getClient, resolveTenantId, tenant
644
1226
  }
645
1227
  for (const [key, value] of Object.entries(query.filters)) {
646
1228
  if (value == null || value === "") continue;
647
- const col = camelToSnake(key);
1229
+ const col = camelToSnake2(key);
648
1230
  if (Array.isArray(value)) {
649
1231
  q = q.in(col, value);
650
1232
  } else {
@@ -652,14 +1234,14 @@ async function fetchViaView(reportDef, query, getClient, resolveTenantId, tenant
652
1234
  }
653
1235
  }
654
1236
  if (query.search) {
655
- const searchableCols = reportDef.columns.filter((c) => c.type === "text" || c.type === "email").map((c) => camelToSnake(c.key));
1237
+ const searchableCols = reportDef.columns.filter((c) => c.type === "text" || c.type === "email").map((c) => camelToSnake2(c.key));
656
1238
  if (searchableCols.length > 0) {
657
1239
  const term = `%${query.search}%`;
658
1240
  const orClause = searchableCols.map((col) => `${col}.ilike.${term}`).join(",");
659
1241
  q = q.or(orClause);
660
1242
  }
661
1243
  }
662
- const sortCol = query.sortBy ? camelToSnake(query.sortBy) : dataSource.defaultSort ? camelToSnake(dataSource.defaultSort) : dateCol;
1244
+ const sortCol = query.sortBy ? camelToSnake2(query.sortBy) : dataSource.defaultSort ? camelToSnake2(dataSource.defaultSort) : dateCol;
663
1245
  const ascending = query.sortDir ? query.sortDir === "asc" : dataSource.defaultSortDir ? dataSource.defaultSortDir === "asc" : false;
664
1246
  q = q.order(sortCol, { ascending });
665
1247
  const from = (query.page - 1) * query.pageSize;
@@ -668,7 +1250,7 @@ async function fetchViaView(reportDef, query, getClient, resolveTenantId, tenant
668
1250
  const { data, error, count } = await q;
669
1251
  if (error) throw error;
670
1252
  return {
671
- data: (data ?? []).map((row) => mapRow(row, columnMap)),
1253
+ data: (data ?? []).map((row) => mapRow2(row, columnMap)),
672
1254
  total: count ?? 0
673
1255
  };
674
1256
  }
@@ -684,23 +1266,23 @@ async function fetchViaRpc(reportDef, query, getClient, resolveTenantId, columnM
684
1266
  };
685
1267
  for (const [key, value] of Object.entries(query.filters)) {
686
1268
  if (value == null || value === "") continue;
687
- params[`p_${camelToSnake(key)}`] = value;
1269
+ params[`p_${camelToSnake2(key)}`] = value;
688
1270
  }
689
1271
  if (query.sortBy) {
690
- params.p_sort_by = camelToSnake(query.sortBy);
1272
+ params.p_sort_by = camelToSnake2(query.sortBy);
691
1273
  params.p_sort_dir = query.sortDir ?? "desc";
692
1274
  }
693
1275
  const { data, error } = await getClient().rpc(dataSource.name, params);
694
1276
  if (error) throw error;
695
1277
  if (data && typeof data === "object" && "data" in data) {
696
1278
  return {
697
- data: (data.data ?? []).map((row) => mapRow(row, columnMap)),
1279
+ data: (data.data ?? []).map((row) => mapRow2(row, columnMap)),
698
1280
  total: data.total ?? 0
699
1281
  };
700
1282
  }
701
1283
  const rows = Array.isArray(data) ? data : [];
702
1284
  return {
703
- data: rows.map((row) => mapRow(row, columnMap)),
1285
+ data: rows.map((row) => mapRow2(row, columnMap)),
704
1286
  total: rows.length
705
1287
  };
706
1288
  }
@@ -714,164 +1296,135 @@ function createMockReportProvider() {
714
1296
  };
715
1297
  }
716
1298
 
717
- // src/lib/tenant.ts
718
- var t;
719
- function getReportsTenantId() {
720
- return t;
721
- }
722
-
723
- // src/locales/en.ts
724
- var en = {
725
- "reports.pageTitle": "Reports",
726
- "reports.pageSubtitle": "Access complete reports for analysis and decision making",
727
- "reports.allReports": "All Reports",
728
- "reports.dateRange": "Date Range",
729
- "reports.from": "From",
730
- "reports.to": "To",
731
- "reports.export": "Export",
732
- "reports.exportCsv": "Export CSV",
733
- "reports.exportExcel": "Export Excel",
734
- "reports.exportPdf": "Export PDF",
735
- "reports.noResults": "No results found.",
736
- "reports.backToReports": "Back",
737
- "reports.reports": "reports",
738
- "reports.search": "Search reports...",
739
- "reports.today": "Today",
740
- "reports.yesterday": "Yesterday",
741
- "reports.last7Days": "Last 7 days",
742
- "reports.thisMonth": "This month",
743
- "reports.lastMonth": "Last month",
744
- "reports.thisQuarter": "This quarter",
745
- "reports.custom": "Custom",
746
- "reports.generate": "Generate",
747
- "reports.emptyStateTitle": "Ready to generate",
748
- "reports.emptyStateDescription": "Select a date range and filters, then click Generate to view the report.",
749
- "reports.unavailableTitle": "Coming soon",
750
- "reports.unavailableDescription": "This report is not available yet. It will be enabled in a future update."
751
- };
752
-
753
- // src/locales/pt-BR.ts
754
- var ptBR = {
755
- "reports.pageTitle": "Relat\xF3rios",
756
- "reports.pageSubtitle": "Acesse relat\xF3rios completos para an\xE1lise e tomada de decis\xE3o",
757
- "reports.allReports": "Todos os Relat\xF3rios",
758
- "reports.dateRange": "Per\xEDodo",
759
- "reports.from": "De",
760
- "reports.to": "At\xE9",
761
- "reports.export": "Exportar",
762
- "reports.exportCsv": "Exportar CSV",
763
- "reports.exportExcel": "Exportar Excel",
764
- "reports.exportPdf": "Exportar PDF",
765
- "reports.noResults": "Nenhum resultado encontrado.",
766
- "reports.backToReports": "Voltar",
767
- "reports.reports": "relat\xF3rios",
768
- "reports.search": "Buscar relat\xF3rios...",
769
- "reports.today": "Hoje",
770
- "reports.yesterday": "Ontem",
771
- "reports.last7Days": "\xDAltimos 7 dias",
772
- "reports.thisMonth": "Este m\xEAs",
773
- "reports.lastMonth": "M\xEAs passado",
774
- "reports.thisQuarter": "Este trimestre",
775
- "reports.custom": "Personalizado",
776
- "reports.generate": "Gerar",
777
- "reports.emptyStateTitle": "Pronto para gerar",
778
- "reports.emptyStateDescription": "Selecione o per\xEDodo e os filtros desejados, depois clique em Gerar para visualizar o relat\xF3rio.",
779
- "reports.unavailableTitle": "Em breve",
780
- "reports.unavailableDescription": "Este relat\xF3rio ainda n\xE3o est\xE1 dispon\xEDvel. Ser\xE1 habilitado em uma atualiza\xE7\xE3o futura."
781
- };
782
-
783
- // src/locales/index.ts
784
- var reportsLocales = {
785
- en,
786
- "pt-BR": ptBR
787
- };
788
-
789
1299
  // src/index.ts
790
1300
  var DEFAULT_LABELS = {
791
- pageTitle: "Reports",
792
- pageSubtitle: "Access complete reports for analysis and decision making",
793
- allReports: "All Reports",
794
- dateRange: "Date Range",
795
- from: "From",
796
- to: "To",
797
- export: "Export",
798
- exportCsv: "Export CSV",
799
- exportExcel: "Export Excel",
800
- exportPdf: "Export PDF",
1301
+ analytics: "Analytics",
1302
+ reports: "Reports",
1303
+ liveView: "Live View",
1304
+ overviewSubtitle: "Key metrics for your business",
1305
+ reportsSubtitle: "Every report available to you",
1306
+ searchReports: "Search reports",
1307
+ category: "Category",
1308
+ createdBy: "Created by",
1309
+ allCategories: "All categories",
1310
+ allCreatedBy: "All authors",
1311
+ lastViewed: "Last viewed",
1312
+ name: "Name",
801
1313
  noResults: "No results found.",
802
- backToReports: "Back",
803
- reports: "reports",
804
- search: "Search reports...",
805
- today: "Today",
806
- yesterday: "Yesterday",
807
- last7Days: "Last 7 days",
808
- thisMonth: "This month",
809
- lastMonth: "Last month",
810
- thisQuarter: "This quarter",
811
- custom: "Custom",
812
- generate: "Generate",
1314
+ never: "\u2014",
1315
+ back: "Back",
1316
+ export: "Export",
813
1317
  print: "Print",
814
- filterFrom: "From",
815
- filterTo: "To",
816
- filterResults: "Results",
817
- emptyStateTitle: "Ready to generate",
818
- emptyStateDescription: "Select a date range and filters, then click Generate to view the report.",
819
1318
  unavailableTitle: "Coming soon",
820
- unavailableDescription: "This report is not available yet. It will be enabled in a future update."
1319
+ unavailableDescription: "This report is not connected to data yet. It will be enabled in a future update.",
1320
+ emptyOverviewTitle: "No metrics yet",
1321
+ emptyOverviewDescription: "Enable a plugin that contributes analytics cards, or add them to this app\u2019s pack.",
1322
+ liveViewSoon: "Real-time visitor and checkout activity will appear here once storefront events are being collected.",
1323
+ results: "reports"
821
1324
  };
822
1325
  var DEFAULT_CURRENCY = { code: "BRL", locale: "pt-BR", symbol: "R$" };
823
1326
  function resolveConfig(options) {
1327
+ const labels = { ...DEFAULT_LABELS, ...options.labels };
1328
+ const createdBy = options.createdBy ?? "Fayz";
824
1329
  return {
825
- labels: { ...DEFAULT_LABELS, ...options.labels },
1330
+ labels,
826
1331
  currency: { ...DEFAULT_CURRENCY, ...options.currency },
827
- reports: options.reports,
1332
+ createdBy,
1333
+ cards: [
1334
+ ...options.cards ?? [],
1335
+ ...reportDefsToCards(options.reports ?? [], createdBy)
1336
+ ],
828
1337
  defaultPageSize: options.defaultPageSize ?? 50,
829
- showHeader: options.showHeader !== false
1338
+ showHeader: options.showHeader === true,
1339
+ defaultRange: options.defaultRange ?? "last_30_days",
1340
+ defaultCompare: options.defaultCompare ?? "previous_period",
1341
+ showLiveView: options.showLiveView === true,
1342
+ assistant: options.assistant === false ? null : options.assistant ?? {}
830
1343
  };
831
1344
  }
832
- function createReportsPlugin(options) {
1345
+ function createAnalyticsPlugin(options = {}) {
833
1346
  const config = resolveConfig(options);
834
1347
  registerTranslations(reportsLocales);
835
1348
  const provider = options.dataProvider ?? createSafeDataProvider(
836
- () => createSupabaseReportProvider({ tenantId: () => getReportsTenantId() }),
837
- () => createMockReportProvider()
1349
+ () => createSupabaseAnalyticsProvider({ tenantId: () => getReportsTenantId() }),
1350
+ () => createMockAnalyticsProvider()
838
1351
  );
839
- const PageComponent = () => React7.createElement(ReportsPage, { config, provider });
1352
+ const page2 = (Body, view) => (props) => React5__default.createElement(
1353
+ AnalyticsSurface,
1354
+ { config, provider },
1355
+ React5__default.createElement(
1356
+ AnalyticsModule,
1357
+ { view, cardId: props?.cardId },
1358
+ React5__default.createElement(Body, props)
1359
+ )
1360
+ );
1361
+ const READ = { feature: "reports", action: "read" };
1362
+ const position = options.navPosition ?? 9;
1363
+ const section = options.navSection ?? "main";
1364
+ const nav = [
1365
+ {
1366
+ section,
1367
+ position,
1368
+ label: config.labels.analytics,
1369
+ route: ANALYTICS_BASE,
1370
+ icon: "BarChart3",
1371
+ permission: READ
1372
+ }
1373
+ ];
840
1374
  return {
841
1375
  id: "reports",
842
- name: config.labels.pageTitle,
1376
+ name: config.labels.analytics,
843
1377
  icon: "BarChart3",
844
- version: "1.0.0",
1378
+ version: "2.0.0",
845
1379
  scope: options.scope ?? "universal",
846
1380
  verticalId: options.verticalId,
847
1381
  defaultEnabled: true,
848
1382
  dependencies: [],
849
1383
  declaredFeatures: [
850
- { id: "reports", label: config.labels.pageTitle, group: config.labels.pageTitle }
851
- ],
852
- navigation: [
853
- {
854
- section: options.navSection ?? "main",
855
- position: options.navPosition ?? 9,
856
- label: config.labels.pageTitle,
857
- route: "/reports",
858
- icon: "BarChart3",
859
- permission: { feature: "reports", action: "read" }
860
- }
1384
+ { id: "reports", label: config.labels.analytics, group: config.labels.analytics }
861
1385
  ],
1386
+ navigation: nav,
862
1387
  routes: [
863
- {
864
- path: "/reports",
865
- component: PageComponent,
866
- permission: { feature: "reports", action: "read" }
867
- }
1388
+ { path: ANALYTICS_BASE, component: page2(AnalyticsOverview, "overview"), permission: READ },
1389
+ { path: REPORTS_BASE, component: page2(ReportsListPage, "reports"), permission: READ },
1390
+ // Registered before the list would ever shadow it: matchRoute scores
1391
+ // static segments above params, so /analytics/reports still wins its own
1392
+ // exact path and only /analytics/reports/<id> lands here.
1393
+ // No `parentRoute` here on purpose: the viewer's title depends on DATA
1394
+ // (the card's name), so it claims the shell bar itself via PageHeader.
1395
+ // Declaring both gave the page two stacked back-crumbs saying the same
1396
+ // thing, one of them generic.
1397
+ { path: `${REPORTS_BASE}/:cardId`, component: page2(AnalyticsReportViewer, "report"), permission: READ },
1398
+ { path: `${ANALYTICS_BASE}/live`, component: page2(LiveViewPage, "live"), permission: READ },
1399
+ // The old top-level route kept as a redirect target: bookmarks, saved
1400
+ // links and the five apps still pointing at /reports must not 404.
1401
+ { path: "/reports", component: page2(ReportsListPage, "reports"), permission: READ }
868
1402
  ],
869
1403
  widgets: [],
870
1404
  registries: [],
1405
+ // Measurement, not acquisition — what the Fayz plugin page lists for
1406
+ // Analytics.
1407
+ integrations: ["google-analytics", "hotjar"],
871
1408
  locales: reportsLocales
872
1409
  };
873
1410
  }
1411
+ function createReportsPlugin(options) {
1412
+ return createAnalyticsPlugin({
1413
+ reports: options.reports,
1414
+ labels: {
1415
+ ...options.labels?.pageTitle ? { analytics: options.labels.pageTitle } : {},
1416
+ ...options.labels?.pageSubtitle ? { overviewSubtitle: options.labels.pageSubtitle } : {}
1417
+ },
1418
+ currency: options.currency,
1419
+ navPosition: options.navPosition,
1420
+ navSection: options.navSection,
1421
+ scope: options.scope,
1422
+ verticalId: options.verticalId,
1423
+ defaultPageSize: options.defaultPageSize,
1424
+ showHeader: options.showHeader
1425
+ });
1426
+ }
874
1427
 
875
- export { createMockReportProvider, createReportsPlugin, createSupabaseReportProvider };
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 };
876
1429
  //# sourceMappingURL=index.js.map
877
1430
  //# sourceMappingURL=index.js.map