@fayz-ai/plugin-reports 0.9.0-next.1 → 0.9.1

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