@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.
Files changed (63) hide show
  1. package/README.md +4 -4
  2. package/dist/{chunk-LODXZ2SC.js → chunk-2ZFN6ECM.js} +3 -3
  3. package/dist/chunk-2ZFN6ECM.js.map +1 -0
  4. package/dist/{ReportsContext.d.ts → context.d.ts} +1 -1
  5. package/dist/context.d.ts.map +1 -0
  6. package/dist/data/aggregate.d.ts +14 -0
  7. package/dist/data/aggregate.d.ts.map +1 -0
  8. package/dist/data/analytics-mock.d.ts +3 -0
  9. package/dist/data/analytics-mock.d.ts.map +1 -0
  10. package/dist/data/analytics-supabase.d.ts +7 -0
  11. package/dist/data/analytics-supabase.d.ts.map +1 -0
  12. package/dist/{excel-QHUSM6IH.js → excel-WE73YE2V.js} +3 -3
  13. package/dist/{excel-QHUSM6IH.js.map → excel-WE73YE2V.js.map} +1 -1
  14. package/dist/index.d.ts +63 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +972 -650
  17. package/dist/index.js.map +1 -1
  18. package/dist/lib/lastViewed.d.ts +5 -0
  19. package/dist/lib/lastViewed.d.ts.map +1 -0
  20. package/dist/lib/legacy.d.ts +5 -0
  21. package/dist/lib/legacy.d.ts.map +1 -0
  22. package/dist/{pdf-HETKN26Y.js → pdf-E6DXHFXU.js} +3 -3
  23. package/dist/{pdf-HETKN26Y.js.map → pdf-E6DXHFXU.js.map} +1 -1
  24. package/dist/views/AnalyticsOverview.d.ts +3 -0
  25. package/dist/views/AnalyticsOverview.d.ts.map +1 -0
  26. package/dist/views/AnalyticsReportViewer.d.ts +5 -0
  27. package/dist/views/AnalyticsReportViewer.d.ts.map +1 -0
  28. package/dist/views/AnalyticsSurface.d.ts +75 -0
  29. package/dist/views/AnalyticsSurface.d.ts.map +1 -0
  30. package/dist/views/LiveViewPage.d.ts +3 -0
  31. package/dist/views/LiveViewPage.d.ts.map +1 -0
  32. package/dist/views/ReportsListPage.d.ts +3 -0
  33. package/dist/views/ReportsListPage.d.ts.map +1 -0
  34. package/dist/{ReportsPage.d.ts → views/ReportsPage.d.ts} +2 -2
  35. package/dist/views/ReportsPage.d.ts.map +1 -0
  36. package/package.json +5 -7
  37. package/dist/ReportsContext.d.ts.map +0 -1
  38. package/dist/ReportsPage.d.ts.map +0 -1
  39. package/dist/chunk-LODXZ2SC.js.map +0 -1
  40. package/src/ReportsContext.tsx +0 -33
  41. package/src/ReportsPage.tsx +0 -62
  42. package/src/components/ExportDropdown.tsx +0 -63
  43. package/src/components/ReportCard.tsx +0 -38
  44. package/src/components/ReportFilterBar.tsx +0 -132
  45. package/src/components/ReportPagination.tsx +0 -59
  46. package/src/components/ReportSummaryRow.tsx +0 -68
  47. package/src/data/index.ts +0 -4
  48. package/src/data/mock.ts +0 -9
  49. package/src/data/supabase.ts +0 -200
  50. package/src/data/types.ts +0 -10
  51. package/src/export/csv.ts +0 -21
  52. package/src/export/excel.ts +0 -42
  53. package/src/export/pdf.ts +0 -47
  54. package/src/hooks/useReportData.ts +0 -123
  55. package/src/index.ts +0 -142
  56. package/src/lib/columns.ts +0 -130
  57. package/src/lib/tenant.ts +0 -3
  58. package/src/locales/en.ts +0 -28
  59. package/src/locales/index.ts +0 -7
  60. package/src/locales/pt-BR.ts +0 -28
  61. package/src/types.ts +0 -171
  62. package/src/views/ReportHub.tsx +0 -97
  63. package/src/views/ReportViewer.tsx +0 -182
@@ -1,130 +0,0 @@
1
- import React from 'react'
2
- import type { ColumnDef } from '@tanstack/react-table'
3
- import type { ReportColumnDef } from '../types'
4
- import { PersonLink } from '@fayz-ai/saas'
5
- import {
6
- formatCurrency as formatCurrencyLocale,
7
- formatDate as formatDateLocale,
8
- formatDateTime as formatDateTimeLocale,
9
- } from '@fayz-ai/core'
10
-
11
- // ---------------------------------------------------------------------------
12
- // Formatters (mirrors fieldToColumn.tsx logic)
13
- // ---------------------------------------------------------------------------
14
-
15
- function formatCurrency(value: any, currency?: string): string {
16
- const num = typeof value === 'number' ? value : parseFloat(value)
17
- if (isNaN(num)) return '—'
18
- return formatCurrencyLocale(num, currency)
19
- }
20
-
21
- function formatDate(value: any): string {
22
- if (!value) return '—'
23
- return formatDateLocale(value)
24
- }
25
-
26
- function formatDateTime(value: any): string {
27
- if (!value) return '—'
28
- return formatDateTimeLocale(value)
29
- }
30
-
31
- function formatNumber(value: any): string {
32
- if (value == null) return '—'
33
- return typeof value === 'number' ? value.toLocaleString() : String(value)
34
- }
35
-
36
- // ---------------------------------------------------------------------------
37
- // Plain-text formatter for exports
38
- // ---------------------------------------------------------------------------
39
-
40
- export function formatValuePlain(value: any, col: ReportColumnDef, currency?: string): string {
41
- if (value == null || value === '') return ''
42
- switch (col.type) {
43
- case 'currency':
44
- return formatCurrency(value, col.currency ?? currency)
45
- case 'date':
46
- return formatDate(value)
47
- case 'datetime':
48
- return formatDateTime(value)
49
- case 'number':
50
- return formatNumber(value)
51
- case 'boolean':
52
- return value ? 'Yes' : 'No'
53
- default:
54
- return String(value)
55
- }
56
- }
57
-
58
- // ---------------------------------------------------------------------------
59
- // ReportColumnDef[] → TanStack ColumnDef[]
60
- // ---------------------------------------------------------------------------
61
-
62
- export function reportColumnsToTanstack(
63
- columns: ReportColumnDef[],
64
- currency?: string,
65
- ): ColumnDef<Record<string, any>>[] {
66
- return columns
67
- .filter((c) => c.visible !== false)
68
- .map((col): ColumnDef<Record<string, any>> => {
69
- const sortable = col.sortable ?? ['text', 'number', 'currency', 'date', 'datetime', 'email'].includes(col.type)
70
- const alignRight = col.align === 'right' || (!col.align && (col.type === 'currency' || col.type === 'number'))
71
- const alignCenter = col.align === 'center'
72
-
73
- return {
74
- accessorKey: col.key,
75
- header: col.label,
76
- enableSorting: sortable,
77
- cell: (ctx) => {
78
- const value = ctx.getValue()
79
- const row = ctx.row.original
80
-
81
- if (col.renderCell) {
82
- return col.renderCell(value, row)
83
- }
84
-
85
- if (value == null || value === '') {
86
- return React.createElement('span', { className: 'text-muted-foreground' }, '—')
87
- }
88
-
89
- let content: React.ReactNode
90
- switch (col.type) {
91
- case 'person':
92
- content = React.createElement(PersonLink, {
93
- personId: col.idKey ? row[col.idKey] : null,
94
- name: String(value),
95
- size: 'sm',
96
- className: 'text-xs',
97
- })
98
- break
99
- case 'currency':
100
- content = formatCurrency(value, col.currency ?? currency)
101
- break
102
- case 'date':
103
- content = formatDate(value)
104
- break
105
- case 'datetime':
106
- content = formatDateTime(value)
107
- break
108
- case 'boolean':
109
- content = value
110
- ? React.createElement('svg', { className: 'h-4 w-4 text-success', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2' },
111
- React.createElement('polyline', { points: '20 6 9 17 4 12' }),
112
- )
113
- : React.createElement('span', { className: 'text-muted-foreground' }, '—')
114
- break
115
- case 'number':
116
- content = formatNumber(value)
117
- break
118
- default:
119
- content = String(value)
120
- }
121
-
122
- const cls = alignRight ? 'text-right' : alignCenter ? 'text-center' : undefined
123
- return cls
124
- ? React.createElement('div', { className: cls }, content)
125
- : React.createElement(React.Fragment, null, content)
126
- },
127
- meta: { align: alignRight ? 'right' : alignCenter ? 'center' : 'left' },
128
- }
129
- })
130
- }
package/src/lib/tenant.ts DELETED
@@ -1,3 +0,0 @@
1
- let t: string | undefined
2
- export function setReportsTenantId(id: string | undefined){ t = id }
3
- export function getReportsTenantId(): string | undefined { return t }
package/src/locales/en.ts DELETED
@@ -1,28 +0,0 @@
1
- export const en: Record<string, string> = {
2
- 'reports.pageTitle': 'Reports',
3
- 'reports.pageSubtitle': 'Access complete reports for analysis and decision making',
4
- 'reports.allReports': 'All Reports',
5
- 'reports.dateRange': 'Date Range',
6
- 'reports.from': 'From',
7
- 'reports.to': 'To',
8
- 'reports.export': 'Export',
9
- 'reports.exportCsv': 'Export CSV',
10
- 'reports.exportExcel': 'Export Excel',
11
- 'reports.exportPdf': 'Export PDF',
12
- 'reports.noResults': 'No results found.',
13
- 'reports.backToReports': 'Back',
14
- 'reports.reports': 'reports',
15
- 'reports.search': 'Search reports...',
16
- 'reports.today': 'Today',
17
- 'reports.yesterday': 'Yesterday',
18
- 'reports.last7Days': 'Last 7 days',
19
- 'reports.thisMonth': 'This month',
20
- 'reports.lastMonth': 'Last month',
21
- 'reports.thisQuarter': 'This quarter',
22
- 'reports.custom': 'Custom',
23
- 'reports.generate': 'Generate',
24
- 'reports.emptyStateTitle': 'Ready to generate',
25
- 'reports.emptyStateDescription': 'Select a date range and filters, then click Generate to view the report.',
26
- 'reports.unavailableTitle': 'Coming soon',
27
- 'reports.unavailableDescription': 'This report is not available yet. It will be enabled in a future update.',
28
- }
@@ -1,7 +0,0 @@
1
- import { en } from './en'
2
- import { ptBR } from './pt-BR'
3
-
4
- export const reportsLocales: Record<string, Record<string, string>> = {
5
- en,
6
- 'pt-BR': ptBR,
7
- }
@@ -1,28 +0,0 @@
1
- export const ptBR: Record<string, string> = {
2
- 'reports.pageTitle': 'Relatórios',
3
- 'reports.pageSubtitle': 'Acesse relatórios completos para análise e tomada de decisão',
4
- 'reports.allReports': 'Todos os Relatórios',
5
- 'reports.dateRange': 'Período',
6
- 'reports.from': 'De',
7
- 'reports.to': 'Até',
8
- 'reports.export': 'Exportar',
9
- 'reports.exportCsv': 'Exportar CSV',
10
- 'reports.exportExcel': 'Exportar Excel',
11
- 'reports.exportPdf': 'Exportar PDF',
12
- 'reports.noResults': 'Nenhum resultado encontrado.',
13
- 'reports.backToReports': 'Voltar',
14
- 'reports.reports': 'relatórios',
15
- 'reports.search': 'Buscar relatórios...',
16
- 'reports.today': 'Hoje',
17
- 'reports.yesterday': 'Ontem',
18
- 'reports.last7Days': 'Últimos 7 dias',
19
- 'reports.thisMonth': 'Este mês',
20
- 'reports.lastMonth': 'Mês passado',
21
- 'reports.thisQuarter': 'Este trimestre',
22
- 'reports.custom': 'Personalizado',
23
- 'reports.generate': 'Gerar',
24
- 'reports.emptyStateTitle': 'Pronto para gerar',
25
- 'reports.emptyStateDescription': 'Selecione o período e os filtros desejados, depois clique em Gerar para visualizar o relatório.',
26
- 'reports.unavailableTitle': 'Em breve',
27
- 'reports.unavailableDescription': 'Este relatório ainda não está disponível. Será habilitado em uma atualização futura.',
28
- }
package/src/types.ts DELETED
@@ -1,171 +0,0 @@
1
- import type React from 'react'
2
- import type { FieldType } from '@fayz-ai/core'
3
-
4
- // ---------------------------------------------------------------------------
5
- // Report column
6
- // ---------------------------------------------------------------------------
7
-
8
- export type ReportFieldType = FieldType | 'person'
9
-
10
- export interface ReportColumnDef {
11
- key: string
12
- label: string
13
- type: ReportFieldType
14
- sortable?: boolean
15
- currency?: string
16
- /** For type 'person': the row key containing the person UUID */
17
- idKey?: string
18
- renderCell?: (value: any, row: any) => React.ReactNode
19
- /** Include in export but hide in table. Default: true */
20
- visible?: boolean
21
- /** Text alignment. Currency/number default to 'right' */
22
- align?: 'left' | 'center' | 'right'
23
- /** Aggregate for optional summary row */
24
- aggregate?: 'sum' | 'avg' | 'count' | 'min' | 'max'
25
- }
26
-
27
- // ---------------------------------------------------------------------------
28
- // Report filter
29
- // ---------------------------------------------------------------------------
30
-
31
- export type ReportFilterOption = { label: string; value: string }
32
-
33
- export interface ReportFilterDef {
34
- key: string
35
- label: string
36
- type: 'select' | 'multiselect' | 'text' | 'boolean' | 'number'
37
- options?: ReportFilterOption[] | (() => Promise<ReportFilterOption[]>)
38
- defaultValue?: any
39
- placeholder?: string
40
- }
41
-
42
- // ---------------------------------------------------------------------------
43
- // Report definition
44
- // ---------------------------------------------------------------------------
45
-
46
- export type ReportBadge = 'popular' | 'essential' | 'new' | (string & {})
47
-
48
- export type ReportDataSourceKind = 'view' | 'rpc'
49
-
50
- export interface ReportDataSource {
51
- kind: ReportDataSourceKind
52
- /** View name (e.g. 'rep_appointments_by_period') or RPC function name */
53
- name: string
54
- /** Column for date range filtering. Default: 'created_at' */
55
- dateColumn?: string
56
- defaultSort?: string
57
- defaultSortDir?: 'asc' | 'desc'
58
- /** snake_case DB → camelCase JS column mapping */
59
- columnMap?: Record<string, string>
60
- }
61
-
62
- export type ReportGrain = 'order' | 'order_item' | 'booking' | 'ledger_movement' | 'event'
63
-
64
- export type ReportAllowedAction = 'read'
65
-
66
- export interface ReportDef {
67
- id: string
68
- name: string
69
- description?: string
70
- icon: string
71
- category: string
72
- badge?: ReportBadge
73
- columns: ReportColumnDef[]
74
- filters?: ReportFilterDef[]
75
- dataSource: ReportDataSource
76
- /** Canonical read model/view used by the report. Defaults to dataSource.name. */
77
- sourceView?: string
78
- /** Business grain of each report row, used to prevent double counting. */
79
- grain?: ReportGrain
80
- /** Plugin that owns the report UI/definition. */
81
- ownerPlugin?: string
82
- /** Canonical data owner/spine for this report's facts. */
83
- canonicalOwner?: string
84
- /** Reports are normally read-only; explicit actions make that contract visible. */
85
- allowedActions?: ReportAllowedAction[]
86
- permission?: { feature: string; action: 'read' }
87
- /** Show aggregate summary row */
88
- showSummary?: boolean
89
- /** Whether the backing view/RPC exists. Default: true */
90
- available?: boolean
91
- }
92
-
93
- // ---------------------------------------------------------------------------
94
- // Query state
95
- // ---------------------------------------------------------------------------
96
-
97
- export interface ReportDateRange {
98
- from: string // YYYY-MM-DD
99
- to: string // YYYY-MM-DD
100
- }
101
-
102
- export interface ReportQueryState {
103
- dateRange: ReportDateRange
104
- filters: Record<string, any>
105
- sortBy?: string
106
- sortDir?: 'asc' | 'desc'
107
- page: number
108
- pageSize: number
109
- search?: string
110
- }
111
-
112
- // ---------------------------------------------------------------------------
113
- // Plugin options
114
- // ---------------------------------------------------------------------------
115
-
116
- export interface ReportsPluginLabels {
117
- pageTitle: string
118
- pageSubtitle: string
119
- allReports: string
120
- dateRange: string
121
- from: string
122
- to: string
123
- export: string
124
- exportCsv: string
125
- exportExcel: string
126
- exportPdf: string
127
- noResults: string
128
- backToReports: string
129
- reports: string
130
- search: string
131
- today: string
132
- yesterday: string
133
- last7Days: string
134
- thisMonth: string
135
- lastMonth: string
136
- thisQuarter: string
137
- custom: string
138
- generate: string
139
- print: string
140
- filterFrom: string
141
- filterTo: string
142
- filterResults: string
143
- emptyStateTitle: string
144
- emptyStateDescription: string
145
- unavailableTitle: string
146
- unavailableDescription: string
147
- }
148
-
149
- export interface ReportsPluginOptions {
150
- reports: ReportDef[]
151
- labels?: Partial<ReportsPluginLabels>
152
- currency?: { code?: string; locale?: string; symbol?: string }
153
- dataProvider?: import('./data/types').ReportDataProvider
154
- navPosition?: number
155
- navSection?: 'main' | 'secondary'
156
- scope?: import('@fayz-ai/core').PluginScope
157
- verticalId?: import('@fayz-ai/core').VerticalId
158
- /** Default page size. Default: 50 */
159
- defaultPageSize?: number
160
- /** Render the in-content title/subtitle. Set false when the app shell owns the
161
- * page title (sidebar/GHL-style layouts). Default: true. */
162
- showHeader?: boolean
163
- }
164
-
165
- export interface ResolvedReportsConfig {
166
- labels: ReportsPluginLabels
167
- currency: { code: string; locale: string; symbol: string }
168
- reports: ReportDef[]
169
- defaultPageSize: number
170
- showHeader: boolean
171
- }
@@ -1,97 +0,0 @@
1
- import React, { useMemo, useState } from 'react'
2
- import { Search } from 'lucide-react'
3
- import { Input } from '@fayz-ai/ui'
4
- import { useTranslation } from '@fayz-ai/core'
5
- import { ReportCard } from '../components/ReportCard'
6
- import { useReportsContext } from '../ReportsContext'
7
- import type { ReportDef } from '../types'
8
-
9
- interface ReportHubProps {
10
- onSelect: (report: ReportDef) => void
11
- }
12
-
13
- interface CategoryGroup {
14
- name: string
15
- reports: ReportDef[]
16
- }
17
-
18
- export function ReportHub({ onSelect }: ReportHubProps) {
19
- const t = useTranslation()
20
- const { config } = useReportsContext()
21
- const [search, setSearch] = useState('')
22
-
23
- const categories = useMemo(() => {
24
- const filtered = search
25
- ? config.reports.filter(
26
- (r) =>
27
- r.name.toLowerCase().includes(search.toLowerCase()) ||
28
- r.description?.toLowerCase().includes(search.toLowerCase()),
29
- )
30
- : config.reports
31
-
32
- const map = new Map<string, ReportDef[]>()
33
- for (const report of filtered) {
34
- const list = map.get(report.category) ?? []
35
- list.push(report)
36
- map.set(report.category, list)
37
- }
38
-
39
- const groups: CategoryGroup[] = []
40
- for (const [name, reports] of map) {
41
- groups.push({ name, reports })
42
- }
43
- return groups
44
- }, [config.reports, search])
45
-
46
- const totalReports = config.reports.length
47
-
48
- return (
49
- <div className="space-y-6">
50
- {/* Header (omitted when the app shell owns the page title) */}
51
- {config.showHeader && (
52
- <div>
53
- <h1 className="text-2xl font-bold">{t('reports.pageTitle')}</h1>
54
- <p className="text-muted-foreground mt-1">{t('reports.pageSubtitle')}</p>
55
- </div>
56
- )}
57
-
58
- {/* Search */}
59
- {totalReports > 6 && (
60
- <div className="relative max-w-md">
61
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
62
- <Input
63
- value={search}
64
- onChange={(e) => setSearch(e.target.value)}
65
- placeholder={t('reports.search')}
66
- className="pl-9"
67
- />
68
- </div>
69
- )}
70
-
71
- {/* Categories */}
72
- <div className="space-y-8">
73
- {categories.map((category) => (
74
- <div key={category.name} className="rounded-xl border bg-card shadow-sm p-5">
75
- <div className="flex items-center justify-between mb-4">
76
- <h2 className="font-semibold">{category.name}</h2>
77
- <span className="text-sm text-muted-foreground">
78
- {category.reports.length} {t('reports.reports')}
79
- </span>
80
- </div>
81
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
82
- {category.reports.map((report) => (
83
- <ReportCard key={report.id} report={report} onClick={onSelect} />
84
- ))}
85
- </div>
86
- </div>
87
- ))}
88
- </div>
89
-
90
- {categories.length === 0 && search && (
91
- <div className="text-center py-12 text-muted-foreground">
92
- {t('reports.noResults')}
93
- </div>
94
- )}
95
- </div>
96
- )
97
- }
@@ -1,182 +0,0 @@
1
- import React, { useMemo } from 'react'
2
- import { ArrowLeft, Play, SlidersHorizontal, Construction, Loader2, Printer } from 'lucide-react'
3
- import { Button } from '@fayz-ai/ui'
4
- import { DataTable } from '@fayz-ai/ui'
5
- import { useTranslation } from '@fayz-ai/core'
6
- import { useReportsContext } from '../ReportsContext'
7
- import { useReportData } from '../hooks/useReportData'
8
- import { reportColumnsToTanstack } from '../lib/columns'
9
- import { ReportFilterBar } from '../components/ReportFilterBar'
10
- import { ReportPagination } from '../components/ReportPagination'
11
- import { ReportSummaryRow } from '../components/ReportSummaryRow'
12
- import { ExportDropdown } from '../components/ExportDropdown'
13
- import type { ReportDef } from '../types'
14
-
15
- interface ReportViewerProps {
16
- report: ReportDef
17
- onBack: () => void
18
- }
19
-
20
- export function ReportViewer({ report, onBack }: ReportViewerProps) {
21
- const t = useTranslation()
22
- const { config, provider } = useReportsContext()
23
- const {
24
- data,
25
- total,
26
- loading,
27
- generated,
28
- error,
29
- query,
30
- generate,
31
- setDateRange,
32
- setFilter,
33
- setPage,
34
- setPageSize,
35
- } = useReportData(report, provider, config.defaultPageSize)
36
-
37
- const isAvailable = report.available !== false
38
- const tanstackColumns = useMemo(
39
- () => reportColumnsToTanstack(report.columns, config.currency.code),
40
- [report.columns, config.currency.code],
41
- )
42
-
43
- return (
44
- <div className="space-y-4">
45
- {/* Breadcrumb — hidden in print */}
46
- <div className="flex items-center gap-1.5 text-sm no-print">
47
- <button
48
- onClick={onBack}
49
- className="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors"
50
- >
51
- <ArrowLeft className="h-3.5 w-3.5" />
52
- {t('reports.pageTitle')}
53
- </button>
54
- <span className="text-muted-foreground">/</span>
55
- <span className="font-medium">{report.name}</span>
56
- </div>
57
-
58
- <div>
59
- <h2 className="text-lg font-bold">{report.name}</h2>
60
- {report.description && (
61
- <p className="text-xs text-muted-foreground mt-0.5">{report.description}</p>
62
- )}
63
- </div>
64
-
65
- {/* Filters + Generate — hidden in print */}
66
- <div className="flex flex-wrap items-end gap-3 no-print">
67
- <ReportFilterBar
68
- dateRange={query.dateRange}
69
- onDateRangeChange={setDateRange}
70
- filters={report.filters ?? []}
71
- filterValues={query.filters}
72
- onFilterChange={setFilter}
73
- />
74
- <Button onClick={generate} disabled={loading || !isAvailable} size="sm" className="h-9">
75
- {loading
76
- ? <Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
77
- : <Play className="h-3.5 w-3.5 mr-1.5" />
78
- }
79
- {t('reports.generate')}
80
- </Button>
81
- {generated && (
82
- <>
83
- <ExportDropdown
84
- columns={report.columns}
85
- data={data}
86
- filename={report.id}
87
- currency={config.currency.code}
88
- title={report.name}
89
- />
90
- <Button variant="outline" size="sm" className="h-9" onClick={() => window.print()}>
91
- <Printer className="h-3.5 w-3.5 mr-1.5" />
92
- {t('reports.print')}
93
- </Button>
94
- </>
95
- )}
96
- </div>
97
-
98
- {/* Print-only: compact filter summary as document header */}
99
- {generated && (
100
- <div className="print-only hidden text-[10px] border-b pb-2 mb-2">
101
- <div className="flex flex-wrap gap-x-4 gap-y-0.5">
102
- {query.dateRange.from && (
103
- <span><strong>{t('reports.filterFrom')}:</strong> {new Date(query.dateRange.from + 'T12:00').toLocaleDateString()}</span>
104
- )}
105
- {query.dateRange.to && (
106
- <span><strong>{t('reports.filterTo')}:</strong> {new Date(query.dateRange.to + 'T12:00').toLocaleDateString()}</span>
107
- )}
108
- {Object.entries(query.filters).map(([key, value]) =>
109
- value ? <span key={key}><strong>{key}:</strong> {String(value)}</span> : null
110
- )}
111
- <span><strong>{t('reports.filterResults')}:</strong> {total}</span>
112
- </div>
113
- </div>
114
- )}
115
-
116
- {/* Error */}
117
- {error && (
118
- <div className="rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive">
119
- {error}
120
- </div>
121
- )}
122
-
123
- {/* Empty state — before generate */}
124
- {!generated && !loading && (
125
- <div className="flex flex-col items-center justify-center rounded-xl border border-dashed py-16 text-center">
126
- {isAvailable ? (
127
- <>
128
- <div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted mb-4">
129
- <SlidersHorizontal className="h-5 w-5 text-muted-foreground" />
130
- </div>
131
- <p className="font-medium">{t('reports.emptyStateTitle')}</p>
132
- <p className="text-sm text-muted-foreground mt-1 max-w-sm">{t('reports.emptyStateDescription')}</p>
133
- </>
134
- ) : (
135
- <>
136
- <div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted mb-4">
137
- <Construction className="h-5 w-5 text-muted-foreground" />
138
- </div>
139
- <p className="font-medium">{t('reports.unavailableTitle')}</p>
140
- <p className="text-sm text-muted-foreground mt-1 max-w-sm">{t('reports.unavailableDescription')}</p>
141
- </>
142
- )}
143
- </div>
144
- )}
145
-
146
- {/* Results — only shown after generate */}
147
- {(generated || loading) && (
148
- <>
149
- <DataTable
150
- columns={tanstackColumns}
151
- data={data}
152
- loading={loading}
153
- skeletonRows={5}
154
- emptyMessage={t('reports.noResults')}
155
- variant="card"
156
- compact
157
- />
158
-
159
- {report.showSummary && !loading && (
160
- <ReportSummaryRow
161
- columns={report.columns}
162
- data={data}
163
- currency={config.currency.code}
164
- />
165
- )}
166
-
167
- {!loading && (
168
- <div className="no-print">
169
- <ReportPagination
170
- page={query.page}
171
- pageSize={query.pageSize}
172
- total={total}
173
- onPageChange={setPage}
174
- onPageSizeChange={setPageSize}
175
- />
176
- </div>
177
- )}
178
- </>
179
- )}
180
- </div>
181
- )
182
- }