@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.
- package/README.md +4 -4
- package/dist/{chunk-LODXZ2SC.js → chunk-2ZFN6ECM.js} +3 -3
- package/dist/chunk-2ZFN6ECM.js.map +1 -0
- package/dist/data/aggregate.d.ts +14 -0
- package/dist/data/aggregate.d.ts.map +1 -0
- package/dist/data/analytics-mock.d.ts +3 -0
- package/dist/data/analytics-mock.d.ts.map +1 -0
- package/dist/data/analytics-supabase.d.ts +7 -0
- package/dist/data/analytics-supabase.d.ts.map +1 -0
- package/dist/{excel-QHUSM6IH.js → excel-WE73YE2V.js} +3 -3
- package/dist/{excel-QHUSM6IH.js.map → excel-WE73YE2V.js.map} +1 -1
- package/dist/index.d.ts +73 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1207 -654
- package/dist/index.js.map +1 -1
- package/dist/lib/lastViewed.d.ts +5 -0
- package/dist/lib/lastViewed.d.ts.map +1 -0
- package/dist/lib/legacy.d.ts +5 -0
- package/dist/lib/legacy.d.ts.map +1 -0
- package/dist/lib/tenant.d.ts +1 -0
- package/dist/lib/tenant.d.ts.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/{pdf-HETKN26Y.js → pdf-E6DXHFXU.js} +3 -3
- package/dist/{pdf-HETKN26Y.js.map → pdf-E6DXHFXU.js.map} +1 -1
- package/dist/views/AnalyticsModule.d.ts +10 -0
- package/dist/views/AnalyticsModule.d.ts.map +1 -0
- package/dist/views/AnalyticsOverview.d.ts +3 -0
- package/dist/views/AnalyticsOverview.d.ts.map +1 -0
- package/dist/views/AnalyticsReportViewer.d.ts +5 -0
- package/dist/views/AnalyticsReportViewer.d.ts.map +1 -0
- package/dist/views/AnalyticsSurface.d.ts +97 -0
- package/dist/views/AnalyticsSurface.d.ts.map +1 -0
- package/dist/views/LiveViewPage.d.ts +3 -0
- package/dist/views/LiveViewPage.d.ts.map +1 -0
- package/dist/views/ReportsListPage.d.ts +3 -0
- package/dist/views/ReportsListPage.d.ts.map +1 -0
- package/package.json +5 -7
- package/dist/ReportsContext.d.ts +0 -16
- package/dist/ReportsContext.d.ts.map +0 -1
- package/dist/ReportsPage.d.ts +0 -8
- package/dist/ReportsPage.d.ts.map +0 -1
- package/dist/chunk-LODXZ2SC.js.map +0 -1
- package/dist/components/ReportCard.d.ts +0 -9
- package/dist/components/ReportCard.d.ts.map +0 -1
- package/dist/components/ReportFilterBar.d.ts +0 -12
- package/dist/components/ReportFilterBar.d.ts.map +0 -1
- package/dist/components/ReportSummaryRow.d.ts +0 -10
- package/dist/components/ReportSummaryRow.d.ts.map +0 -1
- package/dist/hooks/useReportData.d.ts +0 -19
- package/dist/hooks/useReportData.d.ts.map +0 -1
- package/dist/views/ReportHub.d.ts +0 -8
- package/dist/views/ReportHub.d.ts.map +0 -1
- package/dist/views/ReportViewer.d.ts +0 -9
- package/dist/views/ReportViewer.d.ts.map +0 -1
- package/src/ReportsContext.tsx +0 -33
- package/src/ReportsPage.tsx +0 -62
- package/src/components/ExportDropdown.tsx +0 -63
- package/src/components/ReportCard.tsx +0 -38
- package/src/components/ReportFilterBar.tsx +0 -132
- package/src/components/ReportPagination.tsx +0 -59
- package/src/components/ReportSummaryRow.tsx +0 -68
- package/src/data/index.ts +0 -4
- package/src/data/mock.ts +0 -9
- package/src/data/supabase.ts +0 -200
- package/src/data/types.ts +0 -10
- package/src/export/csv.ts +0 -21
- package/src/export/excel.ts +0 -42
- package/src/export/pdf.ts +0 -47
- package/src/hooks/useReportData.ts +0 -123
- package/src/index.ts +0 -142
- package/src/lib/columns.ts +0 -130
- package/src/lib/tenant.ts +0 -3
- package/src/locales/en.ts +0 -28
- package/src/locales/index.ts +0 -7
- package/src/locales/pt-BR.ts +0 -28
- package/src/types.ts +0 -171
- package/src/views/ReportHub.tsx +0 -97
- package/src/views/ReportViewer.tsx +0 -182
package/dist/index.js
CHANGED
|
@@ -1,367 +1,963 @@
|
|
|
1
|
-
import { reportColumnsToTanstack, formatValuePlain } from './chunk-
|
|
2
|
-
import
|
|
3
|
-
import
|
|
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 {
|
|
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
|
-
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
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
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
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
|
-
|
|
168
|
-
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
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
943
|
+
DataTable,
|
|
289
944
|
{
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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: "
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
971
|
+
const t = useTranslation();
|
|
376
972
|
const [excelAvailable, setExcelAvailable] = useState(false);
|
|
377
973
|
const [pdfAvailable, setPdfAvailable] = useState(false);
|
|
378
974
|
useEffect(() => {
|
|
379
|
-
import('./excel-
|
|
975
|
+
import('./excel-WE73YE2V.js').then((m) => m.isExcelAvailable()).then(setExcelAvailable).catch(() => {
|
|
380
976
|
});
|
|
381
|
-
import('./pdf-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
992
|
+
t("reports.export")
|
|
397
993
|
] }) }),
|
|
398
994
|
/* @__PURE__ */ jsxs(DropdownContent, { align: "end", children: [
|
|
399
|
-
/* @__PURE__ */ jsx(DropdownItem, { onClick: handleCsv, children:
|
|
400
|
-
excelAvailable && /* @__PURE__ */ jsx(DropdownItem, { onClick: handleExcel, children:
|
|
401
|
-
pdfAvailable && /* @__PURE__ */ jsx(DropdownItem, { onClick: handlePdf, children:
|
|
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
|
|
406
|
-
const
|
|
407
|
-
const
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
total,
|
|
411
|
-
|
|
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
|
-
|
|
1009
|
+
"select",
|
|
449
1010
|
{
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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(
|
|
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
|
-
|
|
1022
|
+
Button,
|
|
464
1023
|
{
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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(
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
|
|
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
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
|
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
|
|
1179
|
+
function snakeToCamel2(str) {
|
|
598
1180
|
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
599
1181
|
}
|
|
600
|
-
function
|
|
1182
|
+
function mapRow2(row, columnMap) {
|
|
601
1183
|
const result = {};
|
|
602
1184
|
for (const [key, value] of Object.entries(row)) {
|
|
603
|
-
const camelKey = columnMap?.[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 =
|
|
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) =>
|
|
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 ?
|
|
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) =>
|
|
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_${
|
|
1269
|
+
params[`p_${camelToSnake2(key)}`] = value;
|
|
688
1270
|
}
|
|
689
1271
|
if (query.sortBy) {
|
|
690
|
-
params.p_sort_by =
|
|
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) =>
|
|
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) =>
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
|
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
|
|
1330
|
+
labels,
|
|
826
1331
|
currency: { ...DEFAULT_CURRENCY, ...options.currency },
|
|
827
|
-
|
|
1332
|
+
createdBy,
|
|
1333
|
+
cards: [
|
|
1334
|
+
...options.cards ?? [],
|
|
1335
|
+
...reportDefsToCards(options.reports ?? [], createdBy)
|
|
1336
|
+
],
|
|
828
1337
|
defaultPageSize: options.defaultPageSize ?? 50,
|
|
829
|
-
showHeader: options.showHeader
|
|
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
|
|
1345
|
+
function createAnalyticsPlugin(options = {}) {
|
|
833
1346
|
const config = resolveConfig(options);
|
|
834
1347
|
registerTranslations(reportsLocales);
|
|
835
1348
|
const provider = options.dataProvider ?? createSafeDataProvider(
|
|
836
|
-
() =>
|
|
837
|
-
() =>
|
|
1349
|
+
() => createSupabaseAnalyticsProvider({ tenantId: () => getReportsTenantId() }),
|
|
1350
|
+
() => createMockAnalyticsProvider()
|
|
838
1351
|
);
|
|
839
|
-
const
|
|
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.
|
|
1376
|
+
name: config.labels.analytics,
|
|
843
1377
|
icon: "BarChart3",
|
|
844
|
-
version: "
|
|
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.
|
|
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
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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
|