@fayz-ai/plugin-reports 0.1.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/dist/ReportsContext.d.ts +16 -0
- package/dist/ReportsContext.d.ts.map +1 -0
- package/dist/ReportsPage.d.ts +8 -0
- package/dist/ReportsPage.d.ts.map +1 -0
- package/dist/chunk-LODXZ2SC.js +100 -0
- package/dist/chunk-LODXZ2SC.js.map +1 -0
- package/dist/chunk-ZSELW4DA.cjs +107 -0
- package/dist/chunk-ZSELW4DA.cjs.map +1 -0
- package/dist/components/ExportDropdown.d.ts +12 -0
- package/dist/components/ExportDropdown.d.ts.map +1 -0
- package/dist/components/ReportCard.d.ts +9 -0
- package/dist/components/ReportCard.d.ts.map +1 -0
- package/dist/components/ReportFilterBar.d.ts +12 -0
- package/dist/components/ReportFilterBar.d.ts.map +1 -0
- package/dist/components/ReportPagination.d.ts +11 -0
- package/dist/components/ReportPagination.d.ts.map +1 -0
- package/dist/components/ReportSummaryRow.d.ts +10 -0
- package/dist/components/ReportSummaryRow.d.ts.map +1 -0
- package/dist/data/index.d.ts +5 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/mock.d.ts +3 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/supabase.d.ts +8 -0
- package/dist/data/supabase.d.ts.map +1 -0
- package/dist/data/types.d.ts +9 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/excel-2PDUZ55J.cjs +38 -0
- package/dist/excel-2PDUZ55J.cjs.map +1 -0
- package/dist/excel-QHUSM6IH.js +35 -0
- package/dist/excel-QHUSM6IH.js.map +1 -0
- package/dist/export/csv.d.ts +3 -0
- package/dist/export/csv.d.ts.map +1 -0
- package/dist/export/excel.d.ts +4 -0
- package/dist/export/excel.d.ts.map +1 -0
- package/dist/export/pdf.d.ts +7 -0
- package/dist/export/pdf.d.ts.map +1 -0
- package/dist/hooks/useReportData.d.ts +19 -0
- package/dist/hooks/useReportData.d.ts.map +1 -0
- package/dist/index.cjs +904 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +878 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/columns.d.ts +5 -0
- package/dist/lib/columns.d.ts.map +1 -0
- package/dist/lib/tenant.d.ts +3 -0
- package/dist/lib/tenant.d.ts.map +1 -0
- package/dist/locales/en.d.ts +2 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/index.d.ts +2 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/pt-BR.d.ts +2 -0
- package/dist/locales/pt-BR.d.ts.map +1 -0
- package/dist/pdf-GB4JYUYH.cjs +40 -0
- package/dist/pdf-GB4JYUYH.cjs.map +1 -0
- package/dist/pdf-HETKN26Y.js +37 -0
- package/dist/pdf-HETKN26Y.js.map +1 -0
- package/dist/types.d.ts +139 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/views/ReportHub.d.ts +8 -0
- package/dist/views/ReportHub.d.ts.map +1 -0
- package/dist/views/ReportViewer.d.ts +9 -0
- package/dist/views/ReportViewer.d.ts.map +1 -0
- package/package.json +55 -0
- package/src/ReportsContext.tsx +33 -0
- package/src/ReportsPage.tsx +62 -0
- package/src/components/ExportDropdown.tsx +63 -0
- package/src/components/ReportCard.tsx +38 -0
- package/src/components/ReportFilterBar.tsx +132 -0
- package/src/components/ReportPagination.tsx +59 -0
- package/src/components/ReportSummaryRow.tsx +68 -0
- package/src/data/index.ts +4 -0
- package/src/data/mock.ts +9 -0
- package/src/data/supabase.ts +200 -0
- package/src/data/types.ts +10 -0
- package/src/export/csv.ts +21 -0
- package/src/export/excel.ts +42 -0
- package/src/export/pdf.ts +47 -0
- package/src/hooks/useReportData.ts +123 -0
- package/src/index.ts +140 -0
- package/src/lib/columns.ts +130 -0
- package/src/lib/tenant.ts +3 -0
- package/src/locales/en.ts +28 -0
- package/src/locales/index.ts +7 -0
- package/src/locales/pt-BR.ts +28 -0
- package/src/types.ts +157 -0
- package/src/views/ReportHub.tsx +97 -0
- package/src/views/ReportViewer.tsx +183 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
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 interface ReportDef {
|
|
63
|
+
id: string
|
|
64
|
+
name: string
|
|
65
|
+
description?: string
|
|
66
|
+
icon: string
|
|
67
|
+
category: string
|
|
68
|
+
badge?: ReportBadge
|
|
69
|
+
columns: ReportColumnDef[]
|
|
70
|
+
filters?: ReportFilterDef[]
|
|
71
|
+
dataSource: ReportDataSource
|
|
72
|
+
permission?: { feature: string; action: 'read' }
|
|
73
|
+
/** Show aggregate summary row */
|
|
74
|
+
showSummary?: boolean
|
|
75
|
+
/** Whether the backing view/RPC exists. Default: true */
|
|
76
|
+
available?: boolean
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Query state
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
export interface ReportDateRange {
|
|
84
|
+
from: string // YYYY-MM-DD
|
|
85
|
+
to: string // YYYY-MM-DD
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ReportQueryState {
|
|
89
|
+
dateRange: ReportDateRange
|
|
90
|
+
filters: Record<string, any>
|
|
91
|
+
sortBy?: string
|
|
92
|
+
sortDir?: 'asc' | 'desc'
|
|
93
|
+
page: number
|
|
94
|
+
pageSize: number
|
|
95
|
+
search?: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Plugin options
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
export interface ReportsPluginLabels {
|
|
103
|
+
pageTitle: string
|
|
104
|
+
pageSubtitle: string
|
|
105
|
+
allReports: string
|
|
106
|
+
dateRange: string
|
|
107
|
+
from: string
|
|
108
|
+
to: string
|
|
109
|
+
export: string
|
|
110
|
+
exportCsv: string
|
|
111
|
+
exportExcel: string
|
|
112
|
+
exportPdf: string
|
|
113
|
+
noResults: string
|
|
114
|
+
backToReports: string
|
|
115
|
+
reports: string
|
|
116
|
+
search: string
|
|
117
|
+
today: string
|
|
118
|
+
yesterday: string
|
|
119
|
+
last7Days: string
|
|
120
|
+
thisMonth: string
|
|
121
|
+
lastMonth: string
|
|
122
|
+
thisQuarter: string
|
|
123
|
+
custom: string
|
|
124
|
+
generate: string
|
|
125
|
+
print: string
|
|
126
|
+
filterFrom: string
|
|
127
|
+
filterTo: string
|
|
128
|
+
filterResults: string
|
|
129
|
+
emptyStateTitle: string
|
|
130
|
+
emptyStateDescription: string
|
|
131
|
+
unavailableTitle: string
|
|
132
|
+
unavailableDescription: string
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ReportsPluginOptions {
|
|
136
|
+
reports: ReportDef[]
|
|
137
|
+
labels?: Partial<ReportsPluginLabels>
|
|
138
|
+
currency?: { code?: string; locale?: string; symbol?: string }
|
|
139
|
+
dataProvider?: import('./data/types').ReportDataProvider
|
|
140
|
+
navPosition?: number
|
|
141
|
+
navSection?: 'main' | 'secondary'
|
|
142
|
+
scope?: import('@fayz-ai/core').PluginScope
|
|
143
|
+
verticalId?: import('@fayz-ai/core').VerticalId
|
|
144
|
+
/** Default page size. Default: 50 */
|
|
145
|
+
defaultPageSize?: number
|
|
146
|
+
/** Render the in-content title/subtitle. Set false when the app shell owns the
|
|
147
|
+
* page title (sidebar/GHL-style layouts). Default: true. */
|
|
148
|
+
showHeader?: boolean
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface ResolvedReportsConfig {
|
|
152
|
+
labels: ReportsPluginLabels
|
|
153
|
+
currency: { code: string; locale: string; symbol: string }
|
|
154
|
+
reports: ReportDef[]
|
|
155
|
+
defaultPageSize: number
|
|
156
|
+
showHeader: boolean
|
|
157
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
|
|
39
|
+
const tanstackColumns = useMemo(
|
|
40
|
+
() => reportColumnsToTanstack(report.columns, config.currency.code),
|
|
41
|
+
[report.columns, config.currency.code],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="space-y-4">
|
|
46
|
+
{/* Breadcrumb — hidden in print */}
|
|
47
|
+
<div className="flex items-center gap-1.5 text-sm no-print">
|
|
48
|
+
<button
|
|
49
|
+
onClick={onBack}
|
|
50
|
+
className="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors"
|
|
51
|
+
>
|
|
52
|
+
<ArrowLeft className="h-3.5 w-3.5" />
|
|
53
|
+
{t('reports.pageTitle')}
|
|
54
|
+
</button>
|
|
55
|
+
<span className="text-muted-foreground">/</span>
|
|
56
|
+
<span className="font-medium">{report.name}</span>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div>
|
|
60
|
+
<h2 className="text-lg font-bold">{report.name}</h2>
|
|
61
|
+
{report.description && (
|
|
62
|
+
<p className="text-xs text-muted-foreground mt-0.5">{report.description}</p>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{/* Filters + Generate — hidden in print */}
|
|
67
|
+
<div className="flex flex-wrap items-end gap-3 no-print">
|
|
68
|
+
<ReportFilterBar
|
|
69
|
+
dateRange={query.dateRange}
|
|
70
|
+
onDateRangeChange={setDateRange}
|
|
71
|
+
filters={report.filters ?? []}
|
|
72
|
+
filterValues={query.filters}
|
|
73
|
+
onFilterChange={setFilter}
|
|
74
|
+
/>
|
|
75
|
+
<Button onClick={generate} disabled={loading || !isAvailable} size="sm" className="h-9">
|
|
76
|
+
{loading
|
|
77
|
+
? <Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
|
|
78
|
+
: <Play className="h-3.5 w-3.5 mr-1.5" />
|
|
79
|
+
}
|
|
80
|
+
{t('reports.generate')}
|
|
81
|
+
</Button>
|
|
82
|
+
{generated && (
|
|
83
|
+
<>
|
|
84
|
+
<ExportDropdown
|
|
85
|
+
columns={report.columns}
|
|
86
|
+
data={data}
|
|
87
|
+
filename={report.id}
|
|
88
|
+
currency={config.currency.code}
|
|
89
|
+
title={report.name}
|
|
90
|
+
/>
|
|
91
|
+
<Button variant="outline" size="sm" className="h-9" onClick={() => window.print()}>
|
|
92
|
+
<Printer className="h-3.5 w-3.5 mr-1.5" />
|
|
93
|
+
{t('reports.print')}
|
|
94
|
+
</Button>
|
|
95
|
+
</>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{/* Print-only: compact filter summary as document header */}
|
|
100
|
+
{generated && (
|
|
101
|
+
<div className="print-only hidden text-[10px] border-b pb-2 mb-2">
|
|
102
|
+
<div className="flex flex-wrap gap-x-4 gap-y-0.5">
|
|
103
|
+
{query.dateRange.from && (
|
|
104
|
+
<span><strong>{t('reports.filterFrom')}:</strong> {new Date(query.dateRange.from + 'T12:00').toLocaleDateString()}</span>
|
|
105
|
+
)}
|
|
106
|
+
{query.dateRange.to && (
|
|
107
|
+
<span><strong>{t('reports.filterTo')}:</strong> {new Date(query.dateRange.to + 'T12:00').toLocaleDateString()}</span>
|
|
108
|
+
)}
|
|
109
|
+
{Object.entries(query.filters).map(([key, value]) =>
|
|
110
|
+
value ? <span key={key}><strong>{key}:</strong> {String(value)}</span> : null
|
|
111
|
+
)}
|
|
112
|
+
<span><strong>{t('reports.filterResults')}:</strong> {total}</span>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
{/* Error */}
|
|
118
|
+
{error && (
|
|
119
|
+
<div className="rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive">
|
|
120
|
+
{error}
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
{/* Empty state — before generate */}
|
|
125
|
+
{!generated && !loading && (
|
|
126
|
+
<div className="flex flex-col items-center justify-center rounded-xl border border-dashed py-16 text-center">
|
|
127
|
+
{isAvailable ? (
|
|
128
|
+
<>
|
|
129
|
+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted mb-4">
|
|
130
|
+
<SlidersHorizontal className="h-5 w-5 text-muted-foreground" />
|
|
131
|
+
</div>
|
|
132
|
+
<p className="font-medium">{t('reports.emptyStateTitle')}</p>
|
|
133
|
+
<p className="text-sm text-muted-foreground mt-1 max-w-sm">{t('reports.emptyStateDescription')}</p>
|
|
134
|
+
</>
|
|
135
|
+
) : (
|
|
136
|
+
<>
|
|
137
|
+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted mb-4">
|
|
138
|
+
<Construction className="h-5 w-5 text-muted-foreground" />
|
|
139
|
+
</div>
|
|
140
|
+
<p className="font-medium">{t('reports.unavailableTitle')}</p>
|
|
141
|
+
<p className="text-sm text-muted-foreground mt-1 max-w-sm">{t('reports.unavailableDescription')}</p>
|
|
142
|
+
</>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{/* Results — only shown after generate */}
|
|
148
|
+
{(generated || loading) && (
|
|
149
|
+
<>
|
|
150
|
+
<DataTable
|
|
151
|
+
columns={tanstackColumns}
|
|
152
|
+
data={data}
|
|
153
|
+
loading={loading}
|
|
154
|
+
skeletonRows={5}
|
|
155
|
+
emptyMessage={t('reports.noResults')}
|
|
156
|
+
variant="card"
|
|
157
|
+
compact
|
|
158
|
+
/>
|
|
159
|
+
|
|
160
|
+
{report.showSummary && !loading && (
|
|
161
|
+
<ReportSummaryRow
|
|
162
|
+
columns={report.columns}
|
|
163
|
+
data={data}
|
|
164
|
+
currency={config.currency.code}
|
|
165
|
+
/>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{!loading && (
|
|
169
|
+
<div className="no-print">
|
|
170
|
+
<ReportPagination
|
|
171
|
+
page={query.page}
|
|
172
|
+
pageSize={query.pageSize}
|
|
173
|
+
total={total}
|
|
174
|
+
onPageChange={setPage}
|
|
175
|
+
onPageSizeChange={setPageSize}
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
</>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
)
|
|
183
|
+
}
|