@fayz-ai/plugin-reports 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/{ReportsContext.d.ts → context.d.ts} +1 -1
- package/dist/context.d.ts.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 +63 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +972 -650
- 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/{pdf-HETKN26Y.js → pdf-E6DXHFXU.js} +3 -3
- package/dist/{pdf-HETKN26Y.js.map → pdf-E6DXHFXU.js.map} +1 -1
- 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 +75 -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/dist/{ReportsPage.d.ts → views/ReportsPage.d.ts} +2 -2
- package/dist/views/ReportsPage.d.ts.map +1 -0
- package/package.json +5 -7
- package/dist/ReportsContext.d.ts.map +0 -1
- package/dist/ReportsPage.d.ts.map +0 -1
- package/dist/chunk-LODXZ2SC.js.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/src/data/supabase.ts
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import { getSupabaseClientOptional } from '@fayz-ai/core'
|
|
2
|
-
import type { ReportDef, ReportQueryState } from '../types'
|
|
3
|
-
import type { ReportDataProvider, ReportResult } from './types'
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// Case conversion
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
function camelToSnake(str: string): string {
|
|
10
|
-
return str.replace(/[A-Z]/g, (c) => '_' + c.toLowerCase())
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function snakeToCamel(str: string): string {
|
|
14
|
-
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function mapRow(row: Record<string, unknown>, columnMap?: Record<string, string>): Record<string, any> {
|
|
18
|
-
const result: Record<string, unknown> = {}
|
|
19
|
-
for (const [key, value] of Object.entries(row)) {
|
|
20
|
-
const camelKey = columnMap?.[key] ?? snakeToCamel(key)
|
|
21
|
-
result[camelKey] = value
|
|
22
|
-
}
|
|
23
|
-
return result
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
// Provider factory
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
|
|
30
|
-
export interface SupabaseReportProviderConfig {
|
|
31
|
-
tenantId: string | (() => string | undefined)
|
|
32
|
-
tenantIdColumn?: string
|
|
33
|
-
schema?: string
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function createSupabaseReportProvider(config: SupabaseReportProviderConfig): ReportDataProvider {
|
|
37
|
-
const schema = config.schema ?? 'public'
|
|
38
|
-
const tenantIdCol = config.tenantIdColumn ?? 'tenant_id'
|
|
39
|
-
|
|
40
|
-
function resolveTenantId(): string | undefined {
|
|
41
|
-
return typeof config.tenantId === 'function' ? config.tenantId() : config.tenantId
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function getClient() {
|
|
45
|
-
const supabase = getSupabaseClientOptional() as any
|
|
46
|
-
if (!supabase) throw new Error('Supabase client not available')
|
|
47
|
-
return schema === 'public' ? supabase : supabase.schema(schema)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
async fetch(reportDef: ReportDef, query: ReportQueryState): Promise<ReportResult> {
|
|
52
|
-
const { dataSource } = reportDef
|
|
53
|
-
const columnMap = dataSource.columnMap
|
|
54
|
-
|
|
55
|
-
if (dataSource.kind === 'rpc') {
|
|
56
|
-
return fetchViaRpc(reportDef, query, getClient, resolveTenantId, columnMap)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return fetchViaView(reportDef, query, getClient, resolveTenantId, tenantIdCol, columnMap)
|
|
60
|
-
},
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
// View-based fetch
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
|
|
68
|
-
async function fetchViaView(
|
|
69
|
-
reportDef: ReportDef,
|
|
70
|
-
query: ReportQueryState,
|
|
71
|
-
getClient: () => any,
|
|
72
|
-
resolveTenantId: () => string | undefined,
|
|
73
|
-
tenantIdCol: string,
|
|
74
|
-
columnMap?: Record<string, string>,
|
|
75
|
-
): Promise<ReportResult> {
|
|
76
|
-
const { dataSource } = reportDef
|
|
77
|
-
let q = getClient().from(dataSource.name).select('*', { count: 'exact' })
|
|
78
|
-
|
|
79
|
-
// Tenant scoping
|
|
80
|
-
const tenantId = resolveTenantId()
|
|
81
|
-
if (tenantId) {
|
|
82
|
-
q = q.eq(tenantIdCol, tenantId)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Date range
|
|
86
|
-
const dateCol = dataSource.dateColumn ?? 'created_at'
|
|
87
|
-
if (query.dateRange.from) {
|
|
88
|
-
q = q.gte(dateCol, query.dateRange.from)
|
|
89
|
-
}
|
|
90
|
-
if (query.dateRange.to) {
|
|
91
|
-
// For date-type columns, plain date string is sufficient.
|
|
92
|
-
// For timestamptz columns, extend to end of day.
|
|
93
|
-
const toValue = dateCol === 'created_at' || dateCol.endsWith('_at')
|
|
94
|
-
? query.dateRange.to + 'T23:59:59'
|
|
95
|
-
: query.dateRange.to
|
|
96
|
-
q = q.lte(dateCol, toValue)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Custom filters
|
|
100
|
-
for (const [key, value] of Object.entries(query.filters)) {
|
|
101
|
-
if (value == null || value === '') continue
|
|
102
|
-
const col = camelToSnake(key)
|
|
103
|
-
if (Array.isArray(value)) {
|
|
104
|
-
q = q.in(col, value)
|
|
105
|
-
} else {
|
|
106
|
-
q = q.eq(col, value)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Search
|
|
111
|
-
if (query.search) {
|
|
112
|
-
const searchableCols = reportDef.columns
|
|
113
|
-
.filter((c) => c.type === 'text' || c.type === 'email')
|
|
114
|
-
.map((c: any) => camelToSnake(c.key))
|
|
115
|
-
if (searchableCols.length > 0) {
|
|
116
|
-
const term = `%${query.search}%`
|
|
117
|
-
const orClause = searchableCols.map((col) => `${col}.ilike.${term}`).join(',')
|
|
118
|
-
q = q.or(orClause)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Sort
|
|
123
|
-
const sortCol = query.sortBy
|
|
124
|
-
? camelToSnake(query.sortBy)
|
|
125
|
-
: dataSource.defaultSort
|
|
126
|
-
? camelToSnake(dataSource.defaultSort)
|
|
127
|
-
: dateCol
|
|
128
|
-
const ascending = query.sortDir
|
|
129
|
-
? query.sortDir === 'asc'
|
|
130
|
-
: dataSource.defaultSortDir
|
|
131
|
-
? dataSource.defaultSortDir === 'asc'
|
|
132
|
-
: false
|
|
133
|
-
q = q.order(sortCol, { ascending })
|
|
134
|
-
|
|
135
|
-
// Pagination
|
|
136
|
-
const from = (query.page - 1) * query.pageSize
|
|
137
|
-
const to = from + query.pageSize - 1
|
|
138
|
-
q = q.range(from, to)
|
|
139
|
-
|
|
140
|
-
const { data, error, count } = await q
|
|
141
|
-
|
|
142
|
-
if (error) throw error
|
|
143
|
-
|
|
144
|
-
return {
|
|
145
|
-
data: (data ?? []).map((row: any) => mapRow(row, columnMap)),
|
|
146
|
-
total: count ?? 0,
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ---------------------------------------------------------------------------
|
|
151
|
-
// RPC-based fetch
|
|
152
|
-
// ---------------------------------------------------------------------------
|
|
153
|
-
|
|
154
|
-
async function fetchViaRpc(
|
|
155
|
-
reportDef: ReportDef,
|
|
156
|
-
query: ReportQueryState,
|
|
157
|
-
getClient: () => any,
|
|
158
|
-
resolveTenantId: () => string | undefined,
|
|
159
|
-
columnMap?: Record<string, string>,
|
|
160
|
-
): Promise<ReportResult> {
|
|
161
|
-
const { dataSource } = reportDef
|
|
162
|
-
const tenantId = resolveTenantId()
|
|
163
|
-
|
|
164
|
-
const params: Record<string, any> = {
|
|
165
|
-
p_tenant_id: tenantId,
|
|
166
|
-
p_from: query.dateRange.from,
|
|
167
|
-
p_to: query.dateRange.to,
|
|
168
|
-
p_page: query.page,
|
|
169
|
-
p_page_size: query.pageSize,
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Custom filters
|
|
173
|
-
for (const [key, value] of Object.entries(query.filters)) {
|
|
174
|
-
if (value == null || value === '') continue
|
|
175
|
-
params[`p_${camelToSnake(key)}`] = value
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (query.sortBy) {
|
|
179
|
-
params.p_sort_by = camelToSnake(query.sortBy)
|
|
180
|
-
params.p_sort_dir = query.sortDir ?? 'desc'
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const { data, error } = await getClient().rpc(dataSource.name, params)
|
|
184
|
-
if (error) throw error
|
|
185
|
-
|
|
186
|
-
// RPC expected to return { data: [...], total: number }
|
|
187
|
-
if (data && typeof data === 'object' && 'data' in data) {
|
|
188
|
-
return {
|
|
189
|
-
data: (data.data ?? []).map((row: any) => mapRow(row, columnMap)),
|
|
190
|
-
total: data.total ?? 0,
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Fallback: RPC returns a plain array
|
|
195
|
-
const rows = Array.isArray(data) ? data : []
|
|
196
|
-
return {
|
|
197
|
-
data: rows.map((row: any) => mapRow(row, columnMap)),
|
|
198
|
-
total: rows.length,
|
|
199
|
-
}
|
|
200
|
-
}
|
package/src/data/types.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { ReportDef, ReportQueryState } from '../types'
|
|
2
|
-
|
|
3
|
-
export interface ReportResult {
|
|
4
|
-
data: Record<string, any>[]
|
|
5
|
-
total: number
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface ReportDataProvider {
|
|
9
|
-
fetch(reportDef: ReportDef, query: ReportQueryState): Promise<ReportResult>
|
|
10
|
-
}
|
package/src/export/csv.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { exportCSV } from '@fayz-ai/core'
|
|
2
|
-
import type { CSVColumn } from '@fayz-ai/core'
|
|
3
|
-
import type { ReportColumnDef } from '../types'
|
|
4
|
-
import { formatValuePlain } from '../lib/columns'
|
|
5
|
-
|
|
6
|
-
export function exportToCsv(
|
|
7
|
-
columns: ReportColumnDef[],
|
|
8
|
-
data: Record<string, any>[],
|
|
9
|
-
filename: string,
|
|
10
|
-
currency?: string,
|
|
11
|
-
): void {
|
|
12
|
-
const visibleCols = columns.filter((c) => c.visible !== false)
|
|
13
|
-
|
|
14
|
-
const csvColumns: CSVColumn[] = visibleCols.map((col) => ({
|
|
15
|
-
key: col.key,
|
|
16
|
-
label: col.label,
|
|
17
|
-
format: (value: any) => formatValuePlain(value, col, currency),
|
|
18
|
-
}))
|
|
19
|
-
|
|
20
|
-
exportCSV(csvColumns, data, filename)
|
|
21
|
-
}
|
package/src/export/excel.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { ReportColumnDef } from '../types'
|
|
2
|
-
import { formatValuePlain } from '../lib/columns'
|
|
3
|
-
|
|
4
|
-
// Opaque dynamic import — hidden from Vite's static analysis so it won't
|
|
5
|
-
// fail when xlsx is not installed (it's an optional peer dependency).
|
|
6
|
-
const load = (m: string) => new Function('m', 'return import(m)')(m) as Promise<any>
|
|
7
|
-
|
|
8
|
-
export async function exportToExcel(
|
|
9
|
-
columns: ReportColumnDef[],
|
|
10
|
-
data: Record<string, any>[],
|
|
11
|
-
filename: string,
|
|
12
|
-
currency?: string,
|
|
13
|
-
): Promise<void> {
|
|
14
|
-
const XLSX = await load('xlsx')
|
|
15
|
-
const visibleCols = columns.filter((c) => c.visible !== false)
|
|
16
|
-
|
|
17
|
-
const header = visibleCols.map((c) => c.label)
|
|
18
|
-
const rows = data.map((row) =>
|
|
19
|
-
visibleCols.map((col) => {
|
|
20
|
-
const value = row[col.key]
|
|
21
|
-
if (col.type === 'number' || col.type === 'currency') {
|
|
22
|
-
const num = typeof value === 'number' ? value : parseFloat(value)
|
|
23
|
-
return isNaN(num) ? '' : num
|
|
24
|
-
}
|
|
25
|
-
return formatValuePlain(value, col, currency)
|
|
26
|
-
}),
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
const ws = XLSX.utils.aoa_to_sheet([header, ...rows])
|
|
30
|
-
const wb = XLSX.utils.book_new()
|
|
31
|
-
XLSX.utils.book_append_sheet(wb, ws, 'Report')
|
|
32
|
-
XLSX.writeFile(wb, `${filename}.xlsx`)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function isExcelAvailable(): Promise<boolean> {
|
|
36
|
-
try {
|
|
37
|
-
await load('xlsx')
|
|
38
|
-
return true
|
|
39
|
-
} catch {
|
|
40
|
-
return false
|
|
41
|
-
}
|
|
42
|
-
}
|
package/src/export/pdf.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type { ReportColumnDef } from '../types'
|
|
2
|
-
import { formatValuePlain } from '../lib/columns'
|
|
3
|
-
|
|
4
|
-
// Opaque dynamic import — hidden from Vite's static analysis so it won't
|
|
5
|
-
// fail when jspdf is not installed (it's an optional peer dependency).
|
|
6
|
-
const load = (m: string) => new Function('m', 'return import(m)')(m) as Promise<any>
|
|
7
|
-
|
|
8
|
-
export async function exportToPdf(
|
|
9
|
-
columns: ReportColumnDef[],
|
|
10
|
-
data: Record<string, any>[],
|
|
11
|
-
filename: string,
|
|
12
|
-
options?: { title?: string; currency?: string },
|
|
13
|
-
): Promise<void> {
|
|
14
|
-
const { default: jsPDF } = await load('jspdf')
|
|
15
|
-
await load('jspdf-autotable')
|
|
16
|
-
|
|
17
|
-
const visibleCols = columns.filter((c) => c.visible !== false)
|
|
18
|
-
|
|
19
|
-
const doc = new jsPDF({ orientation: visibleCols.length > 6 ? 'landscape' : 'portrait' })
|
|
20
|
-
|
|
21
|
-
if (options?.title) {
|
|
22
|
-
doc.setFontSize(14)
|
|
23
|
-
doc.text(options.title, 14, 20)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
;(doc as any).autoTable({
|
|
27
|
-
startY: options?.title ? 30 : 14,
|
|
28
|
-
head: [visibleCols.map((c) => c.label)],
|
|
29
|
-
body: data.map((row) =>
|
|
30
|
-
visibleCols.map((col) => formatValuePlain(row[col.key], col, options?.currency)),
|
|
31
|
-
),
|
|
32
|
-
styles: { fontSize: 8 },
|
|
33
|
-
headStyles: { fillColor: [100, 100, 100] },
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
doc.save(`${filename}.pdf`)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function isPdfAvailable(): Promise<boolean> {
|
|
40
|
-
try {
|
|
41
|
-
await load('jspdf')
|
|
42
|
-
await load('jspdf-autotable')
|
|
43
|
-
return true
|
|
44
|
-
} catch {
|
|
45
|
-
return false
|
|
46
|
-
}
|
|
47
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { useState, useCallback, useRef } from 'react'
|
|
2
|
-
import type { ReportDef, ReportQueryState, ReportDateRange } from '../types'
|
|
3
|
-
import type { ReportDataProvider, ReportResult } from '../data/types'
|
|
4
|
-
import { dedup } from '@fayz-ai/saas'
|
|
5
|
-
|
|
6
|
-
function defaultDateRange(): ReportDateRange {
|
|
7
|
-
const now = new Date()
|
|
8
|
-
const from = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01`
|
|
9
|
-
const to = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
|
10
|
-
return { from, to }
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function buildDefaultFilters(report: ReportDef): Record<string, any> {
|
|
14
|
-
const filters: Record<string, any> = {}
|
|
15
|
-
for (const f of report.filters ?? []) {
|
|
16
|
-
if (f.defaultValue !== undefined) filters[f.key] = f.defaultValue
|
|
17
|
-
}
|
|
18
|
-
return filters
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface UseReportDataReturn {
|
|
22
|
-
data: Record<string, any>[]
|
|
23
|
-
total: number
|
|
24
|
-
loading: boolean
|
|
25
|
-
generated: boolean
|
|
26
|
-
error: string | null
|
|
27
|
-
query: ReportQueryState
|
|
28
|
-
generate: () => void
|
|
29
|
-
setDateRange: (range: ReportDateRange) => void
|
|
30
|
-
setFilter: (key: string, value: any) => void
|
|
31
|
-
setSort: (sortBy: string) => void
|
|
32
|
-
setPage: (page: number) => void
|
|
33
|
-
setPageSize: (size: number) => void
|
|
34
|
-
setSearch: (search: string) => void
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function useReportData(
|
|
38
|
-
report: ReportDef,
|
|
39
|
-
provider: ReportDataProvider,
|
|
40
|
-
defaultPageSize: number = 50,
|
|
41
|
-
): UseReportDataReturn {
|
|
42
|
-
const [query, setQuery] = useState<ReportQueryState>({
|
|
43
|
-
dateRange: defaultDateRange(),
|
|
44
|
-
filters: buildDefaultFilters(report),
|
|
45
|
-
sortBy: report.dataSource.defaultSort,
|
|
46
|
-
sortDir: report.dataSource.defaultSortDir ?? 'desc',
|
|
47
|
-
page: 1,
|
|
48
|
-
pageSize: defaultPageSize,
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
const [result, setResult] = useState<ReportResult>({ data: [], total: 0 })
|
|
52
|
-
const [loading, setLoading] = useState(false)
|
|
53
|
-
const [generated, setGenerated] = useState(false)
|
|
54
|
-
const [error, setError] = useState<string | null>(null)
|
|
55
|
-
const queryRef = useRef(query)
|
|
56
|
-
queryRef.current = query
|
|
57
|
-
|
|
58
|
-
const fetchData = useCallback(async (q: ReportQueryState) => {
|
|
59
|
-
setLoading(true)
|
|
60
|
-
setError(null)
|
|
61
|
-
try {
|
|
62
|
-
const res = await dedup(`report:${report.id}:${JSON.stringify(q)}`, () =>
|
|
63
|
-
provider.fetch(report, q),
|
|
64
|
-
)
|
|
65
|
-
setResult(res)
|
|
66
|
-
setGenerated(true)
|
|
67
|
-
} catch (err: any) {
|
|
68
|
-
setError(err?.message ?? 'Failed to load report')
|
|
69
|
-
setResult({ data: [], total: 0 })
|
|
70
|
-
} finally {
|
|
71
|
-
setLoading(false)
|
|
72
|
-
}
|
|
73
|
-
}, [report, provider])
|
|
74
|
-
|
|
75
|
-
const generate = useCallback(() => {
|
|
76
|
-
fetchData(queryRef.current)
|
|
77
|
-
}, [fetchData])
|
|
78
|
-
|
|
79
|
-
const setDateRange = useCallback((dateRange: ReportDateRange) => {
|
|
80
|
-
setQuery((q) => ({ ...q, dateRange, page: 1 }))
|
|
81
|
-
}, [])
|
|
82
|
-
|
|
83
|
-
const setFilter = useCallback((key: string, value: any) => {
|
|
84
|
-
setQuery((q) => ({ ...q, filters: { ...q.filters, [key]: value }, page: 1 }))
|
|
85
|
-
}, [])
|
|
86
|
-
|
|
87
|
-
const setSort = useCallback((sortBy: string) => {
|
|
88
|
-
setQuery((q) => ({
|
|
89
|
-
...q,
|
|
90
|
-
sortBy,
|
|
91
|
-
sortDir: q.sortBy === sortBy && q.sortDir === 'asc' ? 'desc' : 'asc',
|
|
92
|
-
page: 1,
|
|
93
|
-
}))
|
|
94
|
-
}, [])
|
|
95
|
-
|
|
96
|
-
const setPage = useCallback((page: number) => {
|
|
97
|
-
setQuery((q) => ({ ...q, page }))
|
|
98
|
-
}, [])
|
|
99
|
-
|
|
100
|
-
const setPageSize = useCallback((pageSize: number) => {
|
|
101
|
-
setQuery((q) => ({ ...q, pageSize, page: 1 }))
|
|
102
|
-
}, [])
|
|
103
|
-
|
|
104
|
-
const setSearch = useCallback((search: string) => {
|
|
105
|
-
setQuery((q) => ({ ...q, search, page: 1 }))
|
|
106
|
-
}, [])
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
data: result.data,
|
|
110
|
-
total: result.total,
|
|
111
|
-
loading,
|
|
112
|
-
generated,
|
|
113
|
-
error,
|
|
114
|
-
query,
|
|
115
|
-
generate,
|
|
116
|
-
setDateRange,
|
|
117
|
-
setFilter,
|
|
118
|
-
setSort,
|
|
119
|
-
setPage,
|
|
120
|
-
setPageSize,
|
|
121
|
-
setSearch,
|
|
122
|
-
}
|
|
123
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core'
|
|
3
|
-
import { createSafeDataProvider, registerTranslations } from '@fayz-ai/core'
|
|
4
|
-
import { ReportsPage } from './ReportsPage'
|
|
5
|
-
import type { ReportDataProvider } from './data/types'
|
|
6
|
-
import { createSupabaseReportProvider } from './data/supabase'
|
|
7
|
-
import { createMockReportProvider } from './data/mock'
|
|
8
|
-
import { getReportsTenantId } from './lib/tenant'
|
|
9
|
-
import { reportsLocales } from './locales'
|
|
10
|
-
import type {
|
|
11
|
-
ReportsPluginOptions,
|
|
12
|
-
ReportsPluginLabels,
|
|
13
|
-
ResolvedReportsConfig,
|
|
14
|
-
} from './types'
|
|
15
|
-
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// Defaults
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
|
|
20
|
-
const DEFAULT_LABELS: ReportsPluginLabels = {
|
|
21
|
-
pageTitle: 'Reports',
|
|
22
|
-
pageSubtitle: 'Access complete reports for analysis and decision making',
|
|
23
|
-
allReports: 'All Reports',
|
|
24
|
-
dateRange: 'Date Range',
|
|
25
|
-
from: 'From',
|
|
26
|
-
to: 'To',
|
|
27
|
-
export: 'Export',
|
|
28
|
-
exportCsv: 'Export CSV',
|
|
29
|
-
exportExcel: 'Export Excel',
|
|
30
|
-
exportPdf: 'Export PDF',
|
|
31
|
-
noResults: 'No results found.',
|
|
32
|
-
backToReports: 'Back',
|
|
33
|
-
reports: 'reports',
|
|
34
|
-
search: 'Search reports...',
|
|
35
|
-
today: 'Today',
|
|
36
|
-
yesterday: 'Yesterday',
|
|
37
|
-
last7Days: 'Last 7 days',
|
|
38
|
-
thisMonth: 'This month',
|
|
39
|
-
lastMonth: 'Last month',
|
|
40
|
-
thisQuarter: 'This quarter',
|
|
41
|
-
custom: 'Custom',
|
|
42
|
-
generate: 'Generate',
|
|
43
|
-
print: 'Print',
|
|
44
|
-
filterFrom: 'From',
|
|
45
|
-
filterTo: 'To',
|
|
46
|
-
filterResults: 'Results',
|
|
47
|
-
emptyStateTitle: 'Ready to generate',
|
|
48
|
-
emptyStateDescription: 'Select a date range and filters, then click Generate to view the report.',
|
|
49
|
-
unavailableTitle: 'Coming soon',
|
|
50
|
-
unavailableDescription: 'This report is not available yet. It will be enabled in a future update.',
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const DEFAULT_CURRENCY = { code: 'BRL', locale: 'pt-BR', symbol: 'R$' }
|
|
54
|
-
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
// Config resolver
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
|
|
59
|
-
function resolveConfig(options: ReportsPluginOptions): ResolvedReportsConfig {
|
|
60
|
-
return {
|
|
61
|
-
labels: { ...DEFAULT_LABELS, ...options.labels },
|
|
62
|
-
currency: { ...DEFAULT_CURRENCY, ...options.currency },
|
|
63
|
-
reports: options.reports,
|
|
64
|
-
defaultPageSize: options.defaultPageSize ?? 50,
|
|
65
|
-
showHeader: options.showHeader !== false,
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
// Factory
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
|
|
73
|
-
export function createReportsPlugin(options: ReportsPluginOptions): PluginManifest {
|
|
74
|
-
const config = resolveConfig(options)
|
|
75
|
-
registerTranslations(reportsLocales)
|
|
76
|
-
const provider: ReportDataProvider = options.dataProvider ?? createSafeDataProvider(
|
|
77
|
-
() => createSupabaseReportProvider({ tenantId: () => getReportsTenantId() }),
|
|
78
|
-
() => createMockReportProvider(),
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
const PageComponent: React.FC<any> = () =>
|
|
82
|
-
React.createElement(ReportsPage, { config, provider })
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
id: 'reports',
|
|
86
|
-
name: config.labels.pageTitle,
|
|
87
|
-
icon: 'BarChart3',
|
|
88
|
-
version: '1.0.0',
|
|
89
|
-
scope: options.scope ?? 'universal',
|
|
90
|
-
verticalId: options.verticalId,
|
|
91
|
-
defaultEnabled: true,
|
|
92
|
-
dependencies: [],
|
|
93
|
-
declaredFeatures: [
|
|
94
|
-
{ id: 'reports', label: config.labels.pageTitle, group: config.labels.pageTitle },
|
|
95
|
-
],
|
|
96
|
-
navigation: [
|
|
97
|
-
{
|
|
98
|
-
section: options.navSection ?? 'main',
|
|
99
|
-
position: options.navPosition ?? 9,
|
|
100
|
-
label: config.labels.pageTitle,
|
|
101
|
-
route: '/reports',
|
|
102
|
-
icon: 'BarChart3',
|
|
103
|
-
permission: { feature: 'reports', action: 'read' as const },
|
|
104
|
-
},
|
|
105
|
-
],
|
|
106
|
-
routes: [
|
|
107
|
-
{
|
|
108
|
-
path: '/reports',
|
|
109
|
-
component: PageComponent,
|
|
110
|
-
permission: { feature: 'reports', action: 'read' as const },
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
widgets: [],
|
|
114
|
-
registries: [],
|
|
115
|
-
locales: reportsLocales,
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ---------------------------------------------------------------------------
|
|
120
|
-
// Re-exports
|
|
121
|
-
// ---------------------------------------------------------------------------
|
|
122
|
-
|
|
123
|
-
export type {
|
|
124
|
-
ReportDef,
|
|
125
|
-
ReportColumnDef,
|
|
126
|
-
ReportFilterDef,
|
|
127
|
-
ReportDateRange,
|
|
128
|
-
ReportQueryState,
|
|
129
|
-
ReportBadge,
|
|
130
|
-
ReportDataSource,
|
|
131
|
-
ReportGrain,
|
|
132
|
-
ReportAllowedAction,
|
|
133
|
-
ReportFilterOption,
|
|
134
|
-
ReportsPluginOptions,
|
|
135
|
-
ReportsPluginLabels,
|
|
136
|
-
ResolvedReportsConfig,
|
|
137
|
-
} from './types'
|
|
138
|
-
|
|
139
|
-
export type { ReportDataProvider, ReportResult } from './data/types'
|
|
140
|
-
export { createSupabaseReportProvider } from './data/supabase'
|
|
141
|
-
export type { SupabaseReportProviderConfig } from './data/supabase'
|
|
142
|
-
export { createMockReportProvider } from './data/mock'
|