@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.
Files changed (89) hide show
  1. package/dist/ReportsContext.d.ts +16 -0
  2. package/dist/ReportsContext.d.ts.map +1 -0
  3. package/dist/ReportsPage.d.ts +8 -0
  4. package/dist/ReportsPage.d.ts.map +1 -0
  5. package/dist/chunk-LODXZ2SC.js +100 -0
  6. package/dist/chunk-LODXZ2SC.js.map +1 -0
  7. package/dist/chunk-ZSELW4DA.cjs +107 -0
  8. package/dist/chunk-ZSELW4DA.cjs.map +1 -0
  9. package/dist/components/ExportDropdown.d.ts +12 -0
  10. package/dist/components/ExportDropdown.d.ts.map +1 -0
  11. package/dist/components/ReportCard.d.ts +9 -0
  12. package/dist/components/ReportCard.d.ts.map +1 -0
  13. package/dist/components/ReportFilterBar.d.ts +12 -0
  14. package/dist/components/ReportFilterBar.d.ts.map +1 -0
  15. package/dist/components/ReportPagination.d.ts +11 -0
  16. package/dist/components/ReportPagination.d.ts.map +1 -0
  17. package/dist/components/ReportSummaryRow.d.ts +10 -0
  18. package/dist/components/ReportSummaryRow.d.ts.map +1 -0
  19. package/dist/data/index.d.ts +5 -0
  20. package/dist/data/index.d.ts.map +1 -0
  21. package/dist/data/mock.d.ts +3 -0
  22. package/dist/data/mock.d.ts.map +1 -0
  23. package/dist/data/supabase.d.ts +8 -0
  24. package/dist/data/supabase.d.ts.map +1 -0
  25. package/dist/data/types.d.ts +9 -0
  26. package/dist/data/types.d.ts.map +1 -0
  27. package/dist/excel-2PDUZ55J.cjs +38 -0
  28. package/dist/excel-2PDUZ55J.cjs.map +1 -0
  29. package/dist/excel-QHUSM6IH.js +35 -0
  30. package/dist/excel-QHUSM6IH.js.map +1 -0
  31. package/dist/export/csv.d.ts +3 -0
  32. package/dist/export/csv.d.ts.map +1 -0
  33. package/dist/export/excel.d.ts +4 -0
  34. package/dist/export/excel.d.ts.map +1 -0
  35. package/dist/export/pdf.d.ts +7 -0
  36. package/dist/export/pdf.d.ts.map +1 -0
  37. package/dist/hooks/useReportData.d.ts +19 -0
  38. package/dist/hooks/useReportData.d.ts.map +1 -0
  39. package/dist/index.cjs +904 -0
  40. package/dist/index.cjs.map +1 -0
  41. package/dist/index.d.ts +9 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +878 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/lib/columns.d.ts +5 -0
  46. package/dist/lib/columns.d.ts.map +1 -0
  47. package/dist/lib/tenant.d.ts +3 -0
  48. package/dist/lib/tenant.d.ts.map +1 -0
  49. package/dist/locales/en.d.ts +2 -0
  50. package/dist/locales/en.d.ts.map +1 -0
  51. package/dist/locales/index.d.ts +2 -0
  52. package/dist/locales/index.d.ts.map +1 -0
  53. package/dist/locales/pt-BR.d.ts +2 -0
  54. package/dist/locales/pt-BR.d.ts.map +1 -0
  55. package/dist/pdf-GB4JYUYH.cjs +40 -0
  56. package/dist/pdf-GB4JYUYH.cjs.map +1 -0
  57. package/dist/pdf-HETKN26Y.js +37 -0
  58. package/dist/pdf-HETKN26Y.js.map +1 -0
  59. package/dist/types.d.ts +139 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/views/ReportHub.d.ts +8 -0
  62. package/dist/views/ReportHub.d.ts.map +1 -0
  63. package/dist/views/ReportViewer.d.ts +9 -0
  64. package/dist/views/ReportViewer.d.ts.map +1 -0
  65. package/package.json +55 -0
  66. package/src/ReportsContext.tsx +33 -0
  67. package/src/ReportsPage.tsx +62 -0
  68. package/src/components/ExportDropdown.tsx +63 -0
  69. package/src/components/ReportCard.tsx +38 -0
  70. package/src/components/ReportFilterBar.tsx +132 -0
  71. package/src/components/ReportPagination.tsx +59 -0
  72. package/src/components/ReportSummaryRow.tsx +68 -0
  73. package/src/data/index.ts +4 -0
  74. package/src/data/mock.ts +9 -0
  75. package/src/data/supabase.ts +200 -0
  76. package/src/data/types.ts +10 -0
  77. package/src/export/csv.ts +21 -0
  78. package/src/export/excel.ts +42 -0
  79. package/src/export/pdf.ts +47 -0
  80. package/src/hooks/useReportData.ts +123 -0
  81. package/src/index.ts +140 -0
  82. package/src/lib/columns.ts +130 -0
  83. package/src/lib/tenant.ts +3 -0
  84. package/src/locales/en.ts +28 -0
  85. package/src/locales/index.ts +7 -0
  86. package/src/locales/pt-BR.ts +28 -0
  87. package/src/types.ts +157 -0
  88. package/src/views/ReportHub.tsx +97 -0
  89. 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
+ }