@pagamio/frontend-commons-lib 0.8.208 → 0.8.210

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/lib/api/client.js CHANGED
@@ -322,7 +322,8 @@ export class ApiClient {
322
322
  data = response;
323
323
  }
324
324
  if (this.config.onResponse) {
325
- data = await this.config.onResponse(response, data);
325
+ const transformedData = await this.config.onResponse(response, data);
326
+ data = transformedData;
326
327
  }
327
328
  return data;
328
329
  }
@@ -48,6 +48,9 @@ export const useChartData = (url, query, transform) => {
48
48
  lastDataRef.current = data;
49
49
  lastFetchTimeRef.current = Date.now();
50
50
  },
51
+ onError: (error) => {
52
+ console.error('[useChartData] Error fetching data', { url, query, error });
53
+ },
51
54
  });
52
55
  // Effect to update last fetch time when data changes
53
56
  useEffect(() => {
@@ -55,7 +58,7 @@ export const useChartData = (url, query, transform) => {
55
58
  lastDataRef.current = data;
56
59
  lastFetchTimeRef.current = Date.now();
57
60
  }
58
- }, [data]);
61
+ }, [data, url]);
59
62
  const transformedData = transform ? transform(data) : data;
60
63
  const isEmpty = !error && !loading && Array.isArray(transformedData) ? !transformedData.length : transformedData === undefined;
61
64
  // Enhanced refresh function that uses intelligent caching without custom headers
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import type { FilterConfig } from './components/types';
3
+ import { type DashboardApiPaths } from './utils';
3
4
  type FilterValue = string | string[] | Date | null | undefined;
4
5
  interface Config {
5
6
  title: string;
@@ -33,6 +34,8 @@ interface DashboardWrapperProps {
33
34
  resetFilters?: () => void;
34
35
  handleApplyFilters?: () => void;
35
36
  handleApplyTourFilters?: () => void;
37
+ /** Custom API paths for different domains*/
38
+ apiPaths?: DashboardApiPaths;
36
39
  }
37
40
  declare const DashboardWrapper: React.FC<DashboardWrapperProps>;
38
41
  export default DashboardWrapper;
@@ -7,7 +7,7 @@ import { FilterComponent, Tab } from '../components';
7
7
  import { useMediaQueries } from '../shared';
8
8
  import ErrorState from './components/ErrorState';
9
9
  import FilterComponentSkeleton from './components/FilterComponentSkeleton';
10
- import { DashboardPaths, getFilterOptionsData, getGridColProps } from './utils';
10
+ import { defaultDashboardApiPaths, getFilterOptionsData, getGridColProps } from './utils';
11
11
  import componentRegistry from './visualRegistry';
12
12
  const OverviewContent = ({ visualData, renderContent, renderVisual }) => {
13
13
  return (_jsxs(_Fragment, { children: [renderContent(), _jsx("main", { children: visualData.map((section) => (_jsxs("div", { className: "mb-2", children: [section.sectionTitle && _jsx("h2", { className: "mb-2 text-1xl font-semibold", children: section.sectionTitle }), _jsx(Grid, { gutter: "md", align: "stretch", className: "mb-[6px]", children: section.data.map((visual) => {
@@ -51,7 +51,7 @@ const renderFilterComponent = ({ isLoading, error, filters, filterOptions, selec
51
51
  rangeKeys: filter.rangeKeys,
52
52
  })), showClearFilters: true, selectedFilters: selectedFilters, handleFilterChange: handleFilterChange, handleApplyFilters: handleApplyFilters, resetFilters: resetFilters, isNarrow: isNarrow }));
53
53
  };
54
- const DashboardWrapper = ({ data, showVisualHeader = true, showEventsTabbedLayout = false, handleFilterChange = () => { }, selectedFilters = {}, tourSelectedFilters = {}, resetFilters = () => { }, handleApplyFilters = () => { }, handleApplyTourFilters = () => { }, handleTourFilterChange = () => { }, }) => {
54
+ const DashboardWrapper = ({ data, showVisualHeader = true, showEventsTabbedLayout = false, handleFilterChange = () => { }, selectedFilters = {}, tourSelectedFilters = {}, resetFilters = () => { }, handleApplyFilters = () => { }, handleApplyTourFilters = () => { }, handleTourFilterChange = () => { }, apiPaths = defaultDashboardApiPaths, }) => {
55
55
  const [loading, setLoading] = useState(true);
56
56
  const [error, setError] = useState(null);
57
57
  const [tourError, setTourError] = useState(null);
@@ -99,7 +99,9 @@ const DashboardWrapper = ({ data, showVisualHeader = true, showEventsTabbedLayou
99
99
  const fetchFilters = (filters, isTour = false) => {
100
100
  filters.forEach((filter) => {
101
101
  if (filter.query) {
102
- const queryUrl = isTour ? (filter.tourUrl ?? DashboardPaths.QUERY) : (filter.url ?? DashboardPaths.QUERY);
102
+ // Use custom apiPaths.query if filter doesn't have its own url
103
+ const defaultQueryUrl = apiPaths.query;
104
+ const queryUrl = isTour ? (filter.tourUrl ?? defaultQueryUrl) : (filter.url ?? defaultQueryUrl);
103
105
  fetchFilterData(filter.name, filter.valueKey, queryUrl, filter.query, isTour);
104
106
  }
105
107
  });
@@ -108,7 +110,7 @@ const DashboardWrapper = ({ data, showVisualHeader = true, showEventsTabbedLayou
108
110
  if (config.tourFilters) {
109
111
  fetchFilters(config.tourFilters, true);
110
112
  }
111
- }, [config.filters, isRefreshOpttonsQuery]);
113
+ }, [config.filters, isRefreshOpttonsQuery, apiPaths]);
112
114
  const handleRetry = () => {
113
115
  setIsRefreshOpttonsQuery((prev) => prev + 1);
114
116
  };
@@ -167,6 +169,11 @@ const DashboardWrapper = ({ data, showVisualHeader = true, showEventsTabbedLayou
167
169
  console.error(`Visual type "${metricData.type}" not recognized in section "${sectionTitle}" at visual index ${index}.`);
168
170
  return null;
169
171
  }
172
+ // Determine URL based on component type and custom apiPaths
173
+ // If metricData already has a custom url, use it; otherwise use apiPaths
174
+ const isTileType = metricData.type === 'Tile' || metricData.type === 'MetricSummaryCard';
175
+ const defaultUrl = isTileType ? apiPaths.metrics : apiPaths.query;
176
+ const url = metricData.url || defaultUrl;
170
177
  const props = {
171
178
  data: metricData.data,
172
179
  options: metricData.options,
@@ -179,9 +186,10 @@ const DashboardWrapper = ({ data, showVisualHeader = true, showEventsTabbedLayou
179
186
  themeColor: config.themeColor,
180
187
  currencyDisplaySymbol: metricData.currencyDisplaySymbol,
181
188
  ...metricData,
189
+ url, // Override url with apiPaths or metricData.url
182
190
  };
183
191
  return (_jsx(Grid.Col, { span: matchesSmall ? 12 : span, offset: offset, children: _jsx(VisualComponent, { ...props }) }, `visual-${sectionTitle}-${visual.id}`));
184
- }, [config.themeColor, isXs, isSm]);
192
+ }, [config.themeColor, isXs, isSm, apiPaths]);
185
193
  const tabs = [
186
194
  {
187
195
  id: 1,
@@ -3,3 +3,9 @@ export declare enum DashboardPaths {
3
3
  QUERY = "/dashboard/query",
4
4
  METRICS = "/dashboard/metrics"
5
5
  }
6
+ export interface DashboardApiPaths {
7
+ query: string;
8
+ metrics: string;
9
+ paged?: string;
10
+ }
11
+ export declare const defaultDashboardApiPaths: DashboardApiPaths;
@@ -4,3 +4,9 @@ export var DashboardPaths;
4
4
  DashboardPaths["QUERY"] = "/dashboard/query";
5
5
  DashboardPaths["METRICS"] = "/dashboard/metrics";
6
6
  })(DashboardPaths || (DashboardPaths = {}));
7
+ // Default paths matching the enum
8
+ export const defaultDashboardApiPaths = {
9
+ query: DashboardPaths.QUERY,
10
+ metrics: DashboardPaths.METRICS,
11
+ paged: DashboardPaths.PAGED,
12
+ };
@@ -6,9 +6,23 @@ import { useChartData } from '../hooks/useChartData';
6
6
  import { DashboardPaths, formatValue } from '../utils';
7
7
  const Tile = ({ query, title, format, url = DashboardPaths.METRICS, details, valueKey, previousValueKey, changeKey, currency: propCurrency, currencyDisplaySymbol, ...props }) => {
8
8
  const { data, error, loading, isEmpty, refresh } = useChartData(url, query);
9
- // Safe default values
10
- const value = data?.[valueKey] ?? 0;
11
- const currency = propCurrency || data?.additionalData?.currency || undefined;
9
+ // Safe default values - handle both flat and nested data structures
10
+ let value = 0;
11
+ if (data) {
12
+ // Check if data is an array (events analytics format)
13
+ if (Array.isArray(data)) {
14
+ // Aggregate values across all items
15
+ value = data.reduce((sum, item) => {
16
+ const itemValue = Number(item[valueKey]) || 0;
17
+ return sum + itemValue;
18
+ }, 0);
19
+ }
20
+ else {
21
+ // Direct access for flat structure
22
+ value = data[valueKey] ?? 0;
23
+ }
24
+ }
25
+ const currency = propCurrency || (!Array.isArray(data) ? data?.additionalData?.currency : undefined) || undefined;
12
26
  return (_jsx(Card, { title: title, children: _jsx(ChartWrapper, { loading: loading && !data, error: error, isEmpty: isEmpty, onRetry: refresh, children: _jsx("div", { className: "flex justify-between", style: { height: '50px' }, children: _jsx("div", { className: "text-xl font-400", children: currency
13
27
  ? formatPrice(Number(value.toFixed(2)), currency, 2, undefined, currencyDisplaySymbol)
14
28
  : formatValue(Number(value.toFixed(2)), 'number') }) }) }) }));
@@ -4,7 +4,7 @@ import { Checkbox, Label, TextInput } from 'flowbite-react';
4
4
  import { HiMail } from 'react-icons/hi';
5
5
  import { useEffect, useRef, useState } from 'react';
6
6
  import { Modal, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components';
7
- import { exportToCsv, exportToPdf, exportToXlsx, generateExportAsBase64, sendExportEmail } from './exportUtils';
7
+ import { exportToCsv, exportToPdf, exportToXlsx, generateExportAsFile, sendExportEmail } from './exportUtils';
8
8
  const ExportDropdown = ({ data, columns, containerClassName, buttonClassName, extraOptions = [], pdfOptions, xlsxOptions, csvOptions, exportAll = false, fetchData, enableEmailExport = false, emailExportApiUrl, onEmailExportSuccess, onEmailExportError, }) => {
9
9
  const [exportType, setExportType] = useState(null);
10
10
  const [excludedColumns, setExcludedColumns] = useState(['action']);
@@ -78,27 +78,18 @@ const ExportDropdown = ({ data, columns, containerClassName, buttonClassName, ex
78
78
  }
79
79
  };
80
80
  const handleEmailModalSubmit = async () => {
81
- console.log('handleEmailModalSubmit called');
82
- console.log('emailAddress:', emailAddress);
83
- console.log('pendingExportData:', pendingExportData);
84
- console.log('pendingEmailFormat:', pendingEmailFormat);
85
81
  if (!emailAddress.trim() || !validateEmail(emailAddress)) {
86
82
  setEmailError(emailAddress.trim() ? 'Please enter a valid email address' : 'Email address is required');
87
83
  return;
88
84
  }
89
85
  if (!pendingExportData || !pendingEmailFormat) {
90
- console.log('Missing pendingExportData or pendingEmailFormat');
91
86
  return;
92
87
  }
93
88
  setEmailError('');
94
89
  setIsExporting(true);
95
90
  try {
96
- console.log('Generating export as base64...');
97
- const base64File = await generateExportAsBase64(pendingExportData.data, pendingExportData.columns, pendingEmailFormat, { pdfOptions, xlsxOptions, csvOptions });
98
- console.log('Base64 file generated, length:', base64File.length);
99
- console.log('Sending email to:', emailAddress);
100
- const result = await sendExportEmail(emailAddress, base64File, emailExportApiUrl);
101
- console.log('Email sent successfully:', result);
91
+ const exportFile = await generateExportAsFile(pendingExportData.data, pendingExportData.columns, pendingEmailFormat, { pdfOptions, xlsxOptions, csvOptions });
92
+ const result = await sendExportEmail(emailAddress, exportFile, emailExportApiUrl);
102
93
  onEmailExportSuccess?.(result || 'Export sent successfully to your email');
103
94
  setEmailAddress('');
104
95
  setShowEmailModal(false);
@@ -40,18 +40,20 @@ export declare const exportToCsv: <T extends Record<string, any>>(data: T[], col
40
40
  export declare const exportToXlsx: <T extends Record<string, any>>(data: T[], columns: MRT_ColumnDef<T>[], options?: XlsxExportOptions) => void;
41
41
  export declare const exportToPdf: <T extends Record<string, any>>(data: T[], columns: MRT_ColumnDef<T>[], options?: PdfExportOptions) => Promise<void>;
42
42
  /**
43
- * Sends an export file to the user via email
43
+ * Sends an export file to the user via email using multipart/form-data
44
+ * Automatically includes Authorization header from cookies if available
44
45
  * @param email - The recipient email address
45
- * @param file - Base64 encoded file string
46
+ * @param file - The File object to send
46
47
  * @param baseUrl - The API base URL (optional, defaults to /api/v1)
48
+ * @param additionalHeaders - Optional additional headers to merge with auth headers
47
49
  */
48
- export declare const sendExportEmail: (email: string, file: string, baseUrl?: string) => Promise<string>;
50
+ export declare const sendExportEmail: (email: string, file: File, baseUrl?: string, additionalHeaders?: HeadersInit) => Promise<string>;
49
51
  /**
50
- * Generates export file as base64 string for email sending
52
+ * Generates export as a File object for email sending
51
53
  */
52
- export declare const generateExportAsBase64: <T extends Record<string, any>>(data: T[], columns: MRT_ColumnDef<T>[], format: "csv" | "xlsx" | "pdf", options?: {
54
+ export declare const generateExportAsFile: <T extends Record<string, any>>(data: T[], columns: MRT_ColumnDef<T>[], format: "csv" | "xlsx" | "pdf", options?: {
53
55
  pdfOptions?: PdfExportOptions;
54
56
  xlsxOptions?: XlsxExportOptions;
55
57
  csvOptions?: CsvExportOptions;
56
- }) => Promise<string>;
58
+ }) => Promise<File>;
57
59
  export {};
@@ -781,28 +781,55 @@ export const exportToPdf = async (data, columns, options = {}) => {
781
781
  doc.save(filename);
782
782
  };
783
783
  /**
784
- * Sends an export file to the user via email
784
+ * Helper to get authentication headers from browser cookies
785
+ * This reads the accessToken cookie that's set by TokenManager
786
+ */
787
+ const getAuthHeaders = () => {
788
+ if (typeof document === 'undefined')
789
+ return {};
790
+ // Read accessToken from cookies (matches TokenManager cookie name)
791
+ const cookies = document.cookie.split(';');
792
+ const accessTokenCookie = cookies.find((cookie) => cookie.trim().startsWith('accessToken='));
793
+ if (!accessTokenCookie)
794
+ return {};
795
+ const token = accessTokenCookie.split('=')[1];
796
+ return {
797
+ Authorization: `Bearer ${token}`,
798
+ };
799
+ };
800
+ /**
801
+ * Sends an export file to the user via email using multipart/form-data
802
+ * Automatically includes Authorization header from cookies if available
785
803
  * @param email - The recipient email address
786
- * @param file - Base64 encoded file string
804
+ * @param file - The File object to send
787
805
  * @param baseUrl - The API base URL (optional, defaults to /api/v1)
806
+ * @param additionalHeaders - Optional additional headers to merge with auth headers
788
807
  */
789
- export const sendExportEmail = async (email, file, baseUrl = '/api/v1') => {
808
+ export const sendExportEmail = async (email, file, baseUrl = '/api/v1', additionalHeaders) => {
809
+ const formData = new FormData();
810
+ formData.append('file', file);
811
+ // Build headers: start with auth, then merge additional headers if provided
812
+ const authHeaders = getAuthHeaders();
813
+ const headers = additionalHeaders ? { ...authHeaders, ...additionalHeaders } : authHeaders;
814
+ // Note: Do NOT set Content-Type here - browser will set it automatically with boundary for multipart/form-data
790
815
  const response = await fetch(`${baseUrl}/exports/send-email?email=${encodeURIComponent(email)}`, {
791
816
  method: 'POST',
792
- headers: {
793
- 'Content-Type': 'application/json',
794
- },
795
- body: JSON.stringify({ file }),
817
+ headers,
818
+ body: formData,
796
819
  });
797
820
  if (!response.ok) {
798
- throw new Error(`Failed to send email: ${response.statusText}`);
821
+ const errorText = await response.text().catch(() => response.statusText);
822
+ throw new Error(`Failed to send email: ${errorText}`);
799
823
  }
800
824
  return response.text();
801
825
  };
802
826
  /**
803
- * Generates export file as base64 string for email sending
827
+ * Generates export as a File object for email sending
804
828
  */
805
- export const generateExportAsBase64 = async (data, columns, format, options) => {
829
+ export const generateExportAsFile = async (data, columns, format, options) => {
830
+ const timestamp = new Date().toISOString().split('T')[0];
831
+ const title = options?.pdfOptions?.title || options?.xlsxOptions?.title || 'export';
832
+ const baseFilename = `${title.toLowerCase().replaceAll(/\s+/g, '_')}_${timestamp}`;
806
833
  if (format === 'csv') {
807
834
  const visibleColumns = columns.filter((col) => col.accessorKey !== 'action');
808
835
  const headers = visibleColumns.map((col) => String(col.header ?? col.accessorKey));
@@ -812,7 +839,8 @@ export const generateExportAsBase64 = async (data, columns, format, options) =>
812
839
  return value !== null && value !== undefined ? String(value) : '';
813
840
  }));
814
841
  const csvContent = Papa.unparse({ fields: headers, data: rows });
815
- return btoa(csvContent);
842
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
843
+ return new File([blob], `${baseFilename}.csv`, { type: 'text/csv' });
816
844
  }
817
845
  if (format === 'xlsx') {
818
846
  const visibleColumns = columns.filter((col) => col.accessorKey !== 'action');
@@ -824,8 +852,13 @@ export const generateExportAsBase64 = async (data, columns, format, options) =>
824
852
  const worksheet = XLSX.utils.aoa_to_sheet([headers, ...rows]);
825
853
  const workbook = XLSX.utils.book_new();
826
854
  XLSX.utils.book_append_sheet(workbook, worksheet, options?.xlsxOptions?.sheetName || 'Data');
827
- const xlsxData = XLSX.write(workbook, { bookType: 'xlsx', type: 'base64' });
828
- return xlsxData;
855
+ const xlsxBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
856
+ const blob = new Blob([xlsxBuffer], {
857
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
858
+ });
859
+ return new File([blob], `${baseFilename}.xlsx`, {
860
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
861
+ });
829
862
  }
830
863
  if (format === 'pdf') {
831
864
  const doc = new jsPDF();
@@ -841,7 +874,8 @@ export const generateExportAsBase64 = async (data, columns, format, options) =>
841
874
  body: rows,
842
875
  startY: 20,
843
876
  });
844
- return doc.output('datauristring').split(',')[1];
877
+ const pdfBlob = doc.output('blob');
878
+ return new File([pdfBlob], `${baseFilename}.pdf`, { type: 'application/pdf' });
845
879
  }
846
880
  throw new Error(`Unsupported format: ${format}`);
847
881
  };
@@ -1,5 +1,5 @@
1
1
  export { default } from './data-table/index';
2
2
  export * from './data-table/types';
3
3
  export * from './data-table/pdfExportUtils';
4
- export { sendExportEmail, generateExportAsBase64 } from './data-table/exportUtils';
4
+ export { sendExportEmail, generateExportAsFile } from './data-table/exportUtils';
5
5
  export * from './utils';
@@ -1,5 +1,5 @@
1
1
  export { default } from './data-table/index';
2
2
  export * from './data-table/types';
3
3
  export * from './data-table/pdfExportUtils';
4
- export { sendExportEmail, generateExportAsBase64 } from './data-table/exportUtils';
4
+ export { sendExportEmail, generateExportAsFile } from './data-table/exportUtils';
5
5
  export * from './utils';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pagamio/frontend-commons-lib",
3
3
  "description": "Pagamio library for Frontend reusable components like the form engine and table container",
4
- "version": "0.8.208",
4
+ "version": "0.8.210",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "provenance": false