@fayz-ai/plugin-reports 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +4 -4
  2. package/dist/{chunk-LODXZ2SC.js → chunk-2ZFN6ECM.js} +3 -3
  3. package/dist/chunk-2ZFN6ECM.js.map +1 -0
  4. package/dist/data/aggregate.d.ts +14 -0
  5. package/dist/data/aggregate.d.ts.map +1 -0
  6. package/dist/data/analytics-mock.d.ts +3 -0
  7. package/dist/data/analytics-mock.d.ts.map +1 -0
  8. package/dist/data/analytics-supabase.d.ts +7 -0
  9. package/dist/data/analytics-supabase.d.ts.map +1 -0
  10. package/dist/{excel-QHUSM6IH.js → excel-WE73YE2V.js} +3 -3
  11. package/dist/{excel-QHUSM6IH.js.map → excel-WE73YE2V.js.map} +1 -1
  12. package/dist/index.d.ts +73 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1207 -654
  15. package/dist/index.js.map +1 -1
  16. package/dist/lib/lastViewed.d.ts +5 -0
  17. package/dist/lib/lastViewed.d.ts.map +1 -0
  18. package/dist/lib/legacy.d.ts +5 -0
  19. package/dist/lib/legacy.d.ts.map +1 -0
  20. package/dist/lib/tenant.d.ts +1 -0
  21. package/dist/lib/tenant.d.ts.map +1 -1
  22. package/dist/locales/en.d.ts.map +1 -1
  23. package/dist/locales/pt-BR.d.ts.map +1 -1
  24. package/dist/{pdf-HETKN26Y.js → pdf-E6DXHFXU.js} +3 -3
  25. package/dist/{pdf-HETKN26Y.js.map → pdf-E6DXHFXU.js.map} +1 -1
  26. package/dist/views/AnalyticsModule.d.ts +10 -0
  27. package/dist/views/AnalyticsModule.d.ts.map +1 -0
  28. package/dist/views/AnalyticsOverview.d.ts +3 -0
  29. package/dist/views/AnalyticsOverview.d.ts.map +1 -0
  30. package/dist/views/AnalyticsReportViewer.d.ts +5 -0
  31. package/dist/views/AnalyticsReportViewer.d.ts.map +1 -0
  32. package/dist/views/AnalyticsSurface.d.ts +97 -0
  33. package/dist/views/AnalyticsSurface.d.ts.map +1 -0
  34. package/dist/views/LiveViewPage.d.ts +3 -0
  35. package/dist/views/LiveViewPage.d.ts.map +1 -0
  36. package/dist/views/ReportsListPage.d.ts +3 -0
  37. package/dist/views/ReportsListPage.d.ts.map +1 -0
  38. package/package.json +5 -7
  39. package/dist/ReportsContext.d.ts +0 -16
  40. package/dist/ReportsContext.d.ts.map +0 -1
  41. package/dist/ReportsPage.d.ts +0 -8
  42. package/dist/ReportsPage.d.ts.map +0 -1
  43. package/dist/chunk-LODXZ2SC.js.map +0 -1
  44. package/dist/components/ReportCard.d.ts +0 -9
  45. package/dist/components/ReportCard.d.ts.map +0 -1
  46. package/dist/components/ReportFilterBar.d.ts +0 -12
  47. package/dist/components/ReportFilterBar.d.ts.map +0 -1
  48. package/dist/components/ReportSummaryRow.d.ts +0 -10
  49. package/dist/components/ReportSummaryRow.d.ts.map +0 -1
  50. package/dist/hooks/useReportData.d.ts +0 -19
  51. package/dist/hooks/useReportData.d.ts.map +0 -1
  52. package/dist/views/ReportHub.d.ts +0 -8
  53. package/dist/views/ReportHub.d.ts.map +0 -1
  54. package/dist/views/ReportViewer.d.ts +0 -9
  55. package/dist/views/ReportViewer.d.ts.map +0 -1
  56. package/src/ReportsContext.tsx +0 -33
  57. package/src/ReportsPage.tsx +0 -62
  58. package/src/components/ExportDropdown.tsx +0 -63
  59. package/src/components/ReportCard.tsx +0 -38
  60. package/src/components/ReportFilterBar.tsx +0 -132
  61. package/src/components/ReportPagination.tsx +0 -59
  62. package/src/components/ReportSummaryRow.tsx +0 -68
  63. package/src/data/index.ts +0 -4
  64. package/src/data/mock.ts +0 -9
  65. package/src/data/supabase.ts +0 -200
  66. package/src/data/types.ts +0 -10
  67. package/src/export/csv.ts +0 -21
  68. package/src/export/excel.ts +0 -42
  69. package/src/export/pdf.ts +0 -47
  70. package/src/hooks/useReportData.ts +0 -123
  71. package/src/index.ts +0 -142
  72. package/src/lib/columns.ts +0 -130
  73. package/src/lib/tenant.ts +0 -3
  74. package/src/locales/en.ts +0 -28
  75. package/src/locales/index.ts +0 -7
  76. package/src/locales/pt-BR.ts +0 -28
  77. package/src/types.ts +0 -171
  78. package/src/views/ReportHub.tsx +0 -97
  79. package/src/views/ReportViewer.tsx +0 -182
@@ -1,132 +0,0 @@
1
- import React, { useEffect, useState } from 'react'
2
- import { DatePicker } from '@fayz-ai/ui'
3
- import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@fayz-ai/ui'
4
- import { Input } from '@fayz-ai/ui'
5
- import { useTranslation } from '@fayz-ai/core'
6
- import type { ReportFilterDef, ReportDateRange, ReportFilterOption } from '../types'
7
-
8
- interface ReportFilterBarProps {
9
- dateRange: ReportDateRange
10
- onDateRangeChange: (range: ReportDateRange) => void
11
- filters: ReportFilterDef[]
12
- filterValues: Record<string, any>
13
- onFilterChange: (key: string, value: any) => void
14
- }
15
-
16
- export function ReportFilterBar({
17
- dateRange,
18
- onDateRangeChange,
19
- filters,
20
- filterValues,
21
- onFilterChange,
22
- }: ReportFilterBarProps) {
23
- const t = useTranslation()
24
-
25
- return (
26
- <div className="flex flex-wrap items-end gap-3">
27
- {/* Date range — always present */}
28
- <div className="flex items-end gap-2">
29
- <div className="space-y-1">
30
- <label className="text-xs font-medium text-muted-foreground">{t('reports.from')}</label>
31
- <DatePicker
32
- value={dateRange.from}
33
- onChange={(v) => onDateRangeChange({ ...dateRange, from: v })}
34
- />
35
- </div>
36
- <div className="space-y-1">
37
- <label className="text-xs font-medium text-muted-foreground">{t('reports.to')}</label>
38
- <DatePicker
39
- value={dateRange.to}
40
- onChange={(v) => onDateRangeChange({ ...dateRange, to: v })}
41
- />
42
- </div>
43
- </div>
44
-
45
- {/* Custom filters */}
46
- {filters.map((filter) => (
47
- <FilterControl
48
- key={filter.key}
49
- filter={filter}
50
- value={filterValues[filter.key]}
51
- onChange={(v) => onFilterChange(filter.key, v)}
52
- />
53
- ))}
54
- </div>
55
- )
56
- }
57
-
58
- // ---------------------------------------------------------------------------
59
- // Individual filter control
60
- // ---------------------------------------------------------------------------
61
-
62
- function FilterControl({
63
- filter,
64
- value,
65
- onChange,
66
- }: {
67
- filter: ReportFilterDef
68
- value: any
69
- onChange: (value: any) => void
70
- }) {
71
- const [options, setOptions] = useState<ReportFilterOption[]>(
72
- Array.isArray(filter.options) ? filter.options : [],
73
- )
74
-
75
- useEffect(() => {
76
- if (typeof filter.options === 'function') {
77
- filter.options().then(setOptions)
78
- }
79
- }, [filter.options])
80
-
81
- if (filter.type === 'select') {
82
- return (
83
- <div className="space-y-1">
84
- <label className="text-xs font-medium text-muted-foreground">{filter.label}</label>
85
- <Select value={value ?? ''} onValueChange={(v) => onChange(v === '__all__' ? '' : v)}>
86
- <SelectTrigger className="w-[180px]">
87
- <SelectValue placeholder={filter.placeholder ?? filter.label} />
88
- </SelectTrigger>
89
- <SelectContent>
90
- <SelectItem value="__all__">All</SelectItem>
91
- {options.map((opt) => (
92
- <SelectItem key={opt.value} value={opt.value}>
93
- {opt.label}
94
- </SelectItem>
95
- ))}
96
- </SelectContent>
97
- </Select>
98
- </div>
99
- )
100
- }
101
-
102
- if (filter.type === 'text') {
103
- return (
104
- <div className="space-y-1">
105
- <label className="text-xs font-medium text-muted-foreground">{filter.label}</label>
106
- <Input
107
- value={value ?? ''}
108
- onChange={(e) => onChange(e.target.value)}
109
- placeholder={filter.placeholder ?? filter.label}
110
- className="w-[180px]"
111
- />
112
- </div>
113
- )
114
- }
115
-
116
- if (filter.type === 'number') {
117
- return (
118
- <div className="space-y-1">
119
- <label className="text-xs font-medium text-muted-foreground">{filter.label}</label>
120
- <Input
121
- type="number"
122
- value={value ?? ''}
123
- onChange={(e) => onChange(e.target.value ? Number(e.target.value) : undefined)}
124
- placeholder={filter.placeholder ?? filter.label}
125
- className="w-[120px]"
126
- />
127
- </div>
128
- )
129
- }
130
-
131
- return null
132
- }
@@ -1,59 +0,0 @@
1
- import React from 'react'
2
- import { ChevronLeft, ChevronRight } from 'lucide-react'
3
- import { Button } from '@fayz-ai/ui'
4
-
5
- interface ReportPaginationProps {
6
- page: number
7
- pageSize: number
8
- total: number
9
- onPageChange: (page: number) => void
10
- onPageSizeChange: (size: number) => void
11
- }
12
-
13
- export function ReportPagination({ page, pageSize, total, onPageChange, onPageSizeChange }: ReportPaginationProps) {
14
- const totalPages = Math.max(1, Math.ceil(total / pageSize))
15
- const from = (page - 1) * pageSize + 1
16
- const to = Math.min(page * pageSize, total)
17
-
18
- return (
19
- <div className="flex items-center justify-between border-t px-4 py-3">
20
- <div className="text-sm text-muted-foreground">
21
- {total > 0 ? `${from}–${to} of ${total}` : 'No results'}
22
- </div>
23
-
24
- <div className="flex items-center gap-3">
25
- <select
26
- value={pageSize}
27
- onChange={(e) => onPageSizeChange(Number(e.target.value))}
28
- 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"
29
- >
30
- {[25, 50, 100].map((size) => (
31
- <option key={size} value={size}>{size} / page</option>
32
- ))}
33
- </select>
34
-
35
- <div className="flex items-center gap-1">
36
- <Button
37
- variant="outline"
38
- size="sm"
39
- disabled={page <= 1}
40
- onClick={() => onPageChange(page - 1)}
41
- >
42
- <ChevronLeft className="h-4 w-4" />
43
- </Button>
44
- <span className="text-sm px-2">
45
- {page} / {totalPages}
46
- </span>
47
- <Button
48
- variant="outline"
49
- size="sm"
50
- disabled={page >= totalPages}
51
- onClick={() => onPageChange(page + 1)}
52
- >
53
- <ChevronRight className="h-4 w-4" />
54
- </Button>
55
- </div>
56
- </div>
57
- </div>
58
- )
59
- }
@@ -1,68 +0,0 @@
1
- import React from 'react'
2
- import type { ReportColumnDef } from '../types'
3
- import { formatValuePlain } from '../lib/columns'
4
-
5
- interface ReportSummaryRowProps {
6
- columns: ReportColumnDef[]
7
- data: Record<string, any>[]
8
- currency?: string
9
- }
10
-
11
- export function ReportSummaryRow({ columns, data, currency }: ReportSummaryRowProps) {
12
- const visibleCols = columns.filter((c) => c.visible !== false)
13
- const hasAggregates = visibleCols.some((c) => c.aggregate)
14
- if (!hasAggregates || data.length === 0) return null
15
-
16
- return (
17
- <div className="rounded-lg border bg-muted/30 overflow-hidden">
18
- <table className="w-full text-xs">
19
- <tbody>
20
- <tr className="font-semibold">
21
- {visibleCols.map((col) => {
22
- const value = computeAggregate(col, data)
23
- const alignRight = col.align === 'right' || col.type === 'currency' || col.type === 'number'
24
- return (
25
- <td
26
- key={col.key}
27
- className={`px-3 py-1.5 ${alignRight ? 'text-right' : ''}`}
28
- >
29
- {value != null
30
- ? formatValuePlain(value, col, currency)
31
- : col.aggregate
32
- ? '—'
33
- : ''}
34
- </td>
35
- )
36
- })}
37
- </tr>
38
- </tbody>
39
- </table>
40
- </div>
41
- )
42
- }
43
-
44
- function computeAggregate(col: ReportColumnDef, data: Record<string, any>[]): number | null {
45
- if (!col.aggregate) return null
46
-
47
- const values = data
48
- .map((row) => row[col.key])
49
- .filter((v) => v != null && !isNaN(Number(v)))
50
- .map(Number)
51
-
52
- if (values.length === 0) return null
53
-
54
- switch (col.aggregate) {
55
- case 'sum':
56
- return values.reduce((a, b) => a + b, 0)
57
- case 'avg':
58
- return values.reduce((a, b) => a + b, 0) / values.length
59
- case 'count':
60
- return values.length
61
- case 'min':
62
- return Math.min(...values)
63
- case 'max':
64
- return Math.max(...values)
65
- default:
66
- return null
67
- }
68
- }
package/src/data/index.ts DELETED
@@ -1,4 +0,0 @@
1
- export type { ReportDataProvider, ReportResult } from './types'
2
- export { createSupabaseReportProvider } from './supabase'
3
- export type { SupabaseReportProviderConfig } from './supabase'
4
- export { createMockReportProvider } from './mock'
package/src/data/mock.ts DELETED
@@ -1,9 +0,0 @@
1
- import type { ReportDataProvider } from './types'
2
-
3
- export function createMockReportProvider(): ReportDataProvider {
4
- return {
5
- async fetch() {
6
- return { data: [], total: 0 }
7
- },
8
- }
9
- }
@@ -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
- }
@@ -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
- }