@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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type AnalyticsCardDef, type AnalyticsProvider as AnalyticsProviderType, type AnalyticsColumnDef, type AnalyticsCompare } from '@fayz-ai/core';
|
|
3
|
+
import { type AnalyticsRangePreset } from '@fayz-ai/ui';
|
|
4
|
+
export declare const ANALYTICS_BASE = "/analytics";
|
|
5
|
+
export declare const REPORTS_BASE = "/analytics/reports";
|
|
6
|
+
export interface AnalyticsLabels {
|
|
7
|
+
analytics: string;
|
|
8
|
+
reports: string;
|
|
9
|
+
liveView: string;
|
|
10
|
+
overviewSubtitle: string;
|
|
11
|
+
reportsSubtitle: string;
|
|
12
|
+
searchReports: string;
|
|
13
|
+
category: string;
|
|
14
|
+
createdBy: string;
|
|
15
|
+
/** Cleared state of each facet chip: "All categories" / "All authors". */
|
|
16
|
+
allCategories: string;
|
|
17
|
+
allCreatedBy: string;
|
|
18
|
+
lastViewed: string;
|
|
19
|
+
name: string;
|
|
20
|
+
noResults: string;
|
|
21
|
+
never: string;
|
|
22
|
+
back: string;
|
|
23
|
+
export: string;
|
|
24
|
+
print: string;
|
|
25
|
+
unavailableTitle: string;
|
|
26
|
+
unavailableDescription: string;
|
|
27
|
+
emptyOverviewTitle: string;
|
|
28
|
+
emptyOverviewDescription: string;
|
|
29
|
+
liveViewSoon: string;
|
|
30
|
+
results: string;
|
|
31
|
+
}
|
|
32
|
+
export interface ResolvedAnalyticsConfig {
|
|
33
|
+
/** Cards declared by the host app (merged with the plugin registry at render). */
|
|
34
|
+
cards: AnalyticsCardDef[];
|
|
35
|
+
labels: AnalyticsLabels;
|
|
36
|
+
currency: {
|
|
37
|
+
code: string;
|
|
38
|
+
locale: string;
|
|
39
|
+
symbol: string;
|
|
40
|
+
};
|
|
41
|
+
/** Attribution for pre-built cards — the product name, e.g. "FayzShop". */
|
|
42
|
+
createdBy: string;
|
|
43
|
+
defaultPageSize: number;
|
|
44
|
+
showHeader: boolean;
|
|
45
|
+
/** Range the surface opens on. A salon reads 30 days; a school reads a term. */
|
|
46
|
+
defaultRange: AnalyticsRangePreset;
|
|
47
|
+
defaultCompare: AnalyticsCompare;
|
|
48
|
+
}
|
|
49
|
+
export declare function useAnalyticsConfig(): ResolvedAnalyticsConfig;
|
|
50
|
+
/**
|
|
51
|
+
* Every card visible on this surface, registry + app, de-duplicated by id.
|
|
52
|
+
*
|
|
53
|
+
* The app's own definition wins a collision so a host can retune a card the
|
|
54
|
+
* plugin shipped (relabel it, move it off the overview) without forking the
|
|
55
|
+
* plugin — the alternative is apps carrying patched copies of plugin cards.
|
|
56
|
+
*/
|
|
57
|
+
export declare function useAnalyticsCards(): AnalyticsCardDef[];
|
|
58
|
+
/** Cards a pack placed on the overview grid, in their declared order. */
|
|
59
|
+
export declare function useOverviewCards(): AnalyticsCardDef[];
|
|
60
|
+
/**
|
|
61
|
+
* Columns for a card's tabular view.
|
|
62
|
+
*
|
|
63
|
+
* An aggregate card has no `columns` of its own — its table IS its dimensions
|
|
64
|
+
* and measures, so deriving them keeps one definition instead of asking every
|
|
65
|
+
* card author to restate the same fields twice and let the two drift.
|
|
66
|
+
*/
|
|
67
|
+
export declare function cardColumns(card: AnalyticsCardDef): AnalyticsColumnDef[];
|
|
68
|
+
export declare function navigateAnalytics(path: string): void;
|
|
69
|
+
export interface AnalyticsSurfaceProps {
|
|
70
|
+
config: ResolvedAnalyticsConfig;
|
|
71
|
+
provider: AnalyticsProviderType;
|
|
72
|
+
children?: React.ReactNode;
|
|
73
|
+
}
|
|
74
|
+
export declare function AnalyticsSurface({ config, provider, children }: AnalyticsSurfaceProps): React.JSX.Element;
|
|
75
|
+
//# sourceMappingURL=AnalyticsSurface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnalyticsSurface.d.ts","sourceRoot":"","sources":["../../src/views/AnalyticsSurface.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAEL,KAAK,gBAAgB,EAAE,KAAK,iBAAiB,IAAI,qBAAqB,EAAE,KAAK,kBAAkB,EAC/F,KAAK,gBAAgB,EACtB,MAAM,eAAe,CAAA;AACtB,OAAO,EAA4B,KAAK,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAYjF,eAAO,MAAM,cAAc,eAAe,CAAA;AAC1C,eAAO,MAAM,YAAY,uBAA8B,CAAA;AAEvD,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,gBAAgB,EAAE,MAAM,CAAA;IACxB,eAAe,EAAE,MAAM,CAAA;IACvB,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,0EAA0E;IAC1E,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,gBAAgB,EAAE,MAAM,CAAA;IACxB,sBAAsB,EAAE,MAAM,CAAA;IAC9B,kBAAkB,EAAE,MAAM,CAAA;IAC1B,wBAAwB,EAAE,MAAM,CAAA;IAChC,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,uBAAuB;IACtC,kFAAkF;IAClF,KAAK,EAAE,gBAAgB,EAAE,CAAA;IACzB,MAAM,EAAE,eAAe,CAAA;IACvB,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1D,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAA;IACjB,eAAe,EAAE,MAAM,CAAA;IACvB,UAAU,EAAE,OAAO,CAAA;IACnB,gFAAgF;IAChF,YAAY,EAAE,oBAAoB,CAAA;IAClC,cAAc,EAAE,gBAAgB,CAAA;CACjC;AAID,wBAAgB,kBAAkB,IAAI,uBAAuB,CAI5D;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,gBAAgB,EAAE,CAStD;AAED,yEAAyE;AACzE,wBAAgB,gBAAgB,IAAI,gBAAgB,EAAE,CAQrD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,gBAAgB,GAAG,kBAAkB,EAAE,CAiBxE;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAEpD;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,uBAAuB,CAAA;IAC/B,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC3B;AAED,wBAAgB,gBAAgB,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,qBAAqB,qBAqBrF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LiveViewPage.d.ts","sourceRoot":"","sources":["../../src/views/LiveViewPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAazB,wBAAgB,YAAY,sBAW3B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReportsListPage.d.ts","sourceRoot":"","sources":["../../src/views/ReportsListPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAiCzB,wBAAgB,eAAe,sBAmK9B"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type { ResolvedReportsConfig } from '
|
|
3
|
-
import type { ReportDataProvider } from '
|
|
2
|
+
import type { ResolvedReportsConfig } from '../types';
|
|
3
|
+
import type { ReportDataProvider } from '../data/types';
|
|
4
4
|
export declare function ReportsPage({ config, provider, }: {
|
|
5
5
|
config: ResolvedReportsConfig;
|
|
6
6
|
provider: ReportDataProvider;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReportsPage.d.ts","sourceRoot":"","sources":["../../src/views/ReportsPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2C,MAAM,OAAO,CAAA;AAI/D,OAAO,KAAK,EAAE,qBAAqB,EAAa,MAAM,UAAU,CAAA;AAChE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAYvD,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,QAAQ,GACT,EAAE;IACD,MAAM,EAAE,qBAAqB,CAAA;IAC7B,QAAQ,EAAE,kBAAkB,CAAA;CAC7B,qBAsCA"}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"fayz": {
|
|
4
4
|
"status": "preview"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.9.
|
|
6
|
+
"version": "0.9.1",
|
|
7
7
|
"description": "[experimental] Fayz SDK — plugin-reports plugin",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"sideEffects": false,
|
|
@@ -12,14 +12,12 @@
|
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
13
13
|
"exports": {
|
|
14
14
|
".": {
|
|
15
|
-
"source": "./src/index.ts",
|
|
16
15
|
"types": "./dist/index.d.ts",
|
|
17
16
|
"import": "./dist/index.js"
|
|
18
17
|
}
|
|
19
18
|
},
|
|
20
19
|
"files": [
|
|
21
|
-
"dist"
|
|
22
|
-
"src"
|
|
20
|
+
"dist"
|
|
23
21
|
],
|
|
24
22
|
"peerDependencies": {
|
|
25
23
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -29,9 +27,9 @@
|
|
|
29
27
|
},
|
|
30
28
|
"dependencies": {
|
|
31
29
|
"@tanstack/react-table": "^8.20.0",
|
|
32
|
-
"@fayz-ai/core": "^0.
|
|
33
|
-
"@fayz-ai/ui": "^0.
|
|
34
|
-
"@fayz-ai/
|
|
30
|
+
"@fayz-ai/core": "^0.10.0",
|
|
31
|
+
"@fayz-ai/ui": "^0.10.0",
|
|
32
|
+
"@fayz-ai/admin": "^0.10.0"
|
|
35
33
|
},
|
|
36
34
|
"devDependencies": {
|
|
37
35
|
"@types/react": "^18.3.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ReportsContext.d.ts","sourceRoot":"","sources":["../src/ReportsContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoC,MAAM,OAAO,CAAA;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAC/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAEtD,UAAU,mBAAmB;IAC3B,MAAM,EAAE,qBAAqB,CAAA;IAC7B,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,OAAO,EAAE,SAAS,EAAE,CAAA;CACrB;AAID,wBAAgB,iBAAiB,IAAI,mBAAmB,CAIvD;AAED,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,QAAQ,EACR,QAAQ,GACT,EAAE;IACD,MAAM,EAAE,qBAAqB,CAAA;IAC7B,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B,qBAMA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ReportsPage.d.ts","sourceRoot":"","sources":["../src/ReportsPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2C,MAAM,OAAO,CAAA;AAI/D,OAAO,KAAK,EAAE,qBAAqB,EAAa,MAAM,SAAS,CAAA;AAC/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAYtD,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,QAAQ,GACT,EAAE;IACD,MAAM,EAAE,qBAAqB,CAAA;IAC7B,QAAQ,EAAE,kBAAkB,CAAA;CAC7B,qBAsCA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/columns.ts"],"names":["formatCurrencyLocale","formatDateLocale","formatDateTimeLocale"],"mappings":"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","file":"chunk-LODXZ2SC.js","sourcesContent":["import React from 'react'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport type { ReportColumnDef } from '../types'\nimport { PersonLink } from '@fayz-ai/saas'\nimport {\n formatCurrency as formatCurrencyLocale,\n formatDate as formatDateLocale,\n formatDateTime as formatDateTimeLocale,\n} from '@fayz-ai/core'\n\n// ---------------------------------------------------------------------------\n// Formatters (mirrors fieldToColumn.tsx logic)\n// ---------------------------------------------------------------------------\n\nfunction formatCurrency(value: any, currency?: string): string {\n const num = typeof value === 'number' ? value : parseFloat(value)\n if (isNaN(num)) return '—'\n return formatCurrencyLocale(num, currency)\n}\n\nfunction formatDate(value: any): string {\n if (!value) return '—'\n return formatDateLocale(value)\n}\n\nfunction formatDateTime(value: any): string {\n if (!value) return '—'\n return formatDateTimeLocale(value)\n}\n\nfunction formatNumber(value: any): string {\n if (value == null) return '—'\n return typeof value === 'number' ? value.toLocaleString() : String(value)\n}\n\n// ---------------------------------------------------------------------------\n// Plain-text formatter for exports\n// ---------------------------------------------------------------------------\n\nexport function formatValuePlain(value: any, col: ReportColumnDef, currency?: string): string {\n if (value == null || value === '') return ''\n switch (col.type) {\n case 'currency':\n return formatCurrency(value, col.currency ?? currency)\n case 'date':\n return formatDate(value)\n case 'datetime':\n return formatDateTime(value)\n case 'number':\n return formatNumber(value)\n case 'boolean':\n return value ? 'Yes' : 'No'\n default:\n return String(value)\n }\n}\n\n// ---------------------------------------------------------------------------\n// ReportColumnDef[] → TanStack ColumnDef[]\n// ---------------------------------------------------------------------------\n\nexport function reportColumnsToTanstack(\n columns: ReportColumnDef[],\n currency?: string,\n): ColumnDef<Record<string, any>>[] {\n return columns\n .filter((c) => c.visible !== false)\n .map((col): ColumnDef<Record<string, any>> => {\n const sortable = col.sortable ?? ['text', 'number', 'currency', 'date', 'datetime', 'email'].includes(col.type)\n const alignRight = col.align === 'right' || (!col.align && (col.type === 'currency' || col.type === 'number'))\n const alignCenter = col.align === 'center'\n\n return {\n accessorKey: col.key,\n header: col.label,\n enableSorting: sortable,\n cell: (ctx) => {\n const value = ctx.getValue()\n const row = ctx.row.original\n\n if (col.renderCell) {\n return col.renderCell(value, row)\n }\n\n if (value == null || value === '') {\n return React.createElement('span', { className: 'text-muted-foreground' }, '—')\n }\n\n let content: React.ReactNode\n switch (col.type) {\n case 'person':\n content = React.createElement(PersonLink, {\n personId: col.idKey ? row[col.idKey] : null,\n name: String(value),\n size: 'sm',\n className: 'text-xs',\n })\n break\n case 'currency':\n content = formatCurrency(value, col.currency ?? currency)\n break\n case 'date':\n content = formatDate(value)\n break\n case 'datetime':\n content = formatDateTime(value)\n break\n case 'boolean':\n content = value\n ? React.createElement('svg', { className: 'h-4 w-4 text-success', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2' },\n React.createElement('polyline', { points: '20 6 9 17 4 12' }),\n )\n : React.createElement('span', { className: 'text-muted-foreground' }, '—')\n break\n case 'number':\n content = formatNumber(value)\n break\n default:\n content = String(value)\n }\n\n const cls = alignRight ? 'text-right' : alignCenter ? 'text-center' : undefined\n return cls\n ? React.createElement('div', { className: cls }, content)\n : React.createElement(React.Fragment, null, content)\n },\n meta: { align: alignRight ? 'right' : alignCenter ? 'center' : 'left' },\n }\n })\n}\n"]}
|
package/src/ReportsContext.tsx
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import React, { createContext, useContext } from 'react'
|
|
2
|
-
import type { ResolvedReportsConfig, ReportDef } from './types'
|
|
3
|
-
import type { ReportDataProvider } from './data/types'
|
|
4
|
-
|
|
5
|
-
interface ReportsContextValue {
|
|
6
|
-
config: ResolvedReportsConfig
|
|
7
|
-
provider: ReportDataProvider
|
|
8
|
-
reports: ReportDef[]
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const ReportsContext = createContext<ReportsContextValue | null>(null)
|
|
12
|
-
|
|
13
|
-
export function useReportsContext(): ReportsContextValue {
|
|
14
|
-
const ctx = useContext(ReportsContext)
|
|
15
|
-
if (!ctx) throw new Error('useReportsContext must be used within ReportsContextProvider')
|
|
16
|
-
return ctx
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function ReportsContextProvider({
|
|
20
|
-
config,
|
|
21
|
-
provider,
|
|
22
|
-
children,
|
|
23
|
-
}: {
|
|
24
|
-
config: ResolvedReportsConfig
|
|
25
|
-
provider: ReportDataProvider
|
|
26
|
-
children: React.ReactNode
|
|
27
|
-
}) {
|
|
28
|
-
return (
|
|
29
|
-
<ReportsContext.Provider value={{ config, provider, reports: config.reports }}>
|
|
30
|
-
{children}
|
|
31
|
-
</ReportsContext.Provider>
|
|
32
|
-
)
|
|
33
|
-
}
|
package/src/ReportsPage.tsx
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useCallback } from 'react'
|
|
2
|
-
import { ReportsContextProvider } from './ReportsContext'
|
|
3
|
-
import { ReportHub } from './views/ReportHub'
|
|
4
|
-
import { ReportViewer } from './views/ReportViewer'
|
|
5
|
-
import type { ResolvedReportsConfig, ReportDef } from './types'
|
|
6
|
-
import type { ReportDataProvider } from './data/types'
|
|
7
|
-
|
|
8
|
-
const HASH_BASE = '/reports'
|
|
9
|
-
|
|
10
|
-
function getReportIdFromHash(): string | null {
|
|
11
|
-
const hash = (window.location.hash.slice(1) || '/').split('?')[0]
|
|
12
|
-
if (hash.startsWith(HASH_BASE + '/') && hash.length > HASH_BASE.length + 1) {
|
|
13
|
-
return hash.slice(HASH_BASE.length + 1)
|
|
14
|
-
}
|
|
15
|
-
return null
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function ReportsPage({
|
|
19
|
-
config,
|
|
20
|
-
provider,
|
|
21
|
-
}: {
|
|
22
|
-
config: ResolvedReportsConfig
|
|
23
|
-
provider: ReportDataProvider
|
|
24
|
-
}) {
|
|
25
|
-
const [activeReportId, setActiveReportId] = useState<string | null>(getReportIdFromHash)
|
|
26
|
-
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
function handler() {
|
|
29
|
-
setActiveReportId(getReportIdFromHash())
|
|
30
|
-
}
|
|
31
|
-
window.addEventListener('hashchange', handler)
|
|
32
|
-
return () => window.removeEventListener('hashchange', handler)
|
|
33
|
-
}, [])
|
|
34
|
-
|
|
35
|
-
const handleSelect = useCallback((report: ReportDef) => {
|
|
36
|
-
window.location.hash = `${HASH_BASE}/${report.id}`
|
|
37
|
-
}, [])
|
|
38
|
-
|
|
39
|
-
const handleBack = useCallback(() => {
|
|
40
|
-
window.location.hash = HASH_BASE
|
|
41
|
-
}, [])
|
|
42
|
-
|
|
43
|
-
const activeReport = activeReportId
|
|
44
|
-
? config.reports.find((r) => r.id === activeReportId) ?? null
|
|
45
|
-
: null
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<ReportsContextProvider config={config} provider={provider}>
|
|
49
|
-
<div className="p-1">
|
|
50
|
-
{activeReport ? (
|
|
51
|
-
<ReportViewer
|
|
52
|
-
key={activeReport.id}
|
|
53
|
-
report={activeReport}
|
|
54
|
-
onBack={handleBack}
|
|
55
|
-
/>
|
|
56
|
-
) : (
|
|
57
|
-
<ReportHub onSelect={handleSelect} />
|
|
58
|
-
)}
|
|
59
|
-
</div>
|
|
60
|
-
</ReportsContextProvider>
|
|
61
|
-
)
|
|
62
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react'
|
|
2
|
-
import { Download } from 'lucide-react'
|
|
3
|
-
import { Button } from '@fayz-ai/ui'
|
|
4
|
-
import {
|
|
5
|
-
Dropdown,
|
|
6
|
-
DropdownTrigger,
|
|
7
|
-
DropdownContent,
|
|
8
|
-
DropdownItem,
|
|
9
|
-
} from '@fayz-ai/ui'
|
|
10
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
11
|
-
import type { ReportColumnDef } from '../types'
|
|
12
|
-
import { exportToCsv } from '../export/csv'
|
|
13
|
-
|
|
14
|
-
interface ExportDropdownProps {
|
|
15
|
-
columns: ReportColumnDef[]
|
|
16
|
-
data: Record<string, any>[]
|
|
17
|
-
filename: string
|
|
18
|
-
currency?: string
|
|
19
|
-
title?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function ExportDropdown({ columns, data, filename, currency, title }: ExportDropdownProps) {
|
|
23
|
-
const t = useTranslation()
|
|
24
|
-
const [excelAvailable, setExcelAvailable] = useState(false)
|
|
25
|
-
const [pdfAvailable, setPdfAvailable] = useState(false)
|
|
26
|
-
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
import('../export/excel').then((m) => m.isExcelAvailable()).then(setExcelAvailable).catch(() => {})
|
|
29
|
-
import('../export/pdf').then((m) => m.isPdfAvailable()).then(setPdfAvailable).catch(() => {})
|
|
30
|
-
}, [])
|
|
31
|
-
|
|
32
|
-
const handleCsv = () => exportToCsv(columns, data, filename, currency)
|
|
33
|
-
|
|
34
|
-
const handleExcel = async () => {
|
|
35
|
-
const { exportToExcel } = await import('../export/excel')
|
|
36
|
-
await exportToExcel(columns, data, filename, currency)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const handlePdf = async () => {
|
|
40
|
-
const { exportToPdf } = await import('../export/pdf')
|
|
41
|
-
await exportToPdf(columns, data, filename, { title, currency })
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<Dropdown>
|
|
46
|
-
<DropdownTrigger asChild>
|
|
47
|
-
<Button variant="outline" size="sm">
|
|
48
|
-
<Download className="h-4 w-4 mr-1.5" />
|
|
49
|
-
{t('reports.export')}
|
|
50
|
-
</Button>
|
|
51
|
-
</DropdownTrigger>
|
|
52
|
-
<DropdownContent align="end">
|
|
53
|
-
<DropdownItem onClick={handleCsv}>{t('reports.exportCsv')}</DropdownItem>
|
|
54
|
-
{excelAvailable && (
|
|
55
|
-
<DropdownItem onClick={handleExcel}>{t('reports.exportExcel')}</DropdownItem>
|
|
56
|
-
)}
|
|
57
|
-
{pdfAvailable && (
|
|
58
|
-
<DropdownItem onClick={handlePdf}>{t('reports.exportPdf')}</DropdownItem>
|
|
59
|
-
)}
|
|
60
|
-
</DropdownContent>
|
|
61
|
-
</Dropdown>
|
|
62
|
-
)
|
|
63
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { FileText } from 'lucide-react'
|
|
3
|
-
import { Badge, getLucideIcon } from '@fayz-ai/ui'
|
|
4
|
-
import type { ReportDef } from '../types'
|
|
5
|
-
|
|
6
|
-
interface ReportCardProps {
|
|
7
|
-
report: ReportDef
|
|
8
|
-
onClick: (report: ReportDef) => void
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function ReportCard({ report, onClick }: ReportCardProps) {
|
|
12
|
-
const IconComp = getLucideIcon(report.icon) ?? FileText
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<button
|
|
16
|
-
type="button"
|
|
17
|
-
onClick={() => onClick(report)}
|
|
18
|
-
className="flex flex-col items-start gap-2 rounded-xl border bg-card p-4 text-left transition-all hover:border-primary/30 hover:shadow-md"
|
|
19
|
-
>
|
|
20
|
-
<div className="flex w-full items-start justify-between">
|
|
21
|
-
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
22
|
-
<IconComp className="h-5 w-5" />
|
|
23
|
-
</div>
|
|
24
|
-
{report.badge && (
|
|
25
|
-
<Badge variant="secondary" className="text-xs capitalize">
|
|
26
|
-
{report.badge}
|
|
27
|
-
</Badge>
|
|
28
|
-
)}
|
|
29
|
-
</div>
|
|
30
|
-
<div>
|
|
31
|
-
<h4 className="font-medium text-sm">{report.name}</h4>
|
|
32
|
-
{report.description && (
|
|
33
|
-
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">{report.description}</p>
|
|
34
|
-
)}
|
|
35
|
-
</div>
|
|
36
|
-
</button>
|
|
37
|
-
)
|
|
38
|
-
}
|
|
@@ -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