@papernote/ui 1.1.0 → 1.2.0
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/LICENSE +21 -21
- package/README.md +455 -455
- package/dist/components/CurrencyInput.d.ts +52 -0
- package/dist/components/CurrencyInput.d.ts.map +1 -0
- package/dist/components/DataTable.d.ts +3 -1
- package/dist/components/DataTable.d.ts.map +1 -1
- package/dist/components/Modal.d.ts.map +1 -1
- package/dist/components/Page.d.ts +2 -0
- package/dist/components/Page.d.ts.map +1 -1
- package/dist/components/PageLayout.d.ts +5 -1
- package/dist/components/PageLayout.d.ts.map +1 -1
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/index.d.ts +204 -4
- package/dist/index.esm.js +415 -88
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +413 -82
- package/dist/index.js.map +1 -1
- package/dist/styles.css +2877 -2675
- package/dist/utils/excelExport.d.ts +143 -0
- package/dist/utils/excelExport.d.ts.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/AdminModal.css +49 -49
- package/src/components/CurrencyInput.stories.tsx +290 -0
- package/src/components/CurrencyInput.tsx +193 -0
- package/src/components/DataTable.tsx +78 -14
- package/src/components/Modal.stories.tsx +64 -0
- package/src/components/Modal.tsx +15 -2
- package/src/components/Page.stories.tsx +76 -0
- package/src/components/Page.tsx +35 -3
- package/src/components/PageLayout.stories.tsx +75 -0
- package/src/components/PageLayout.tsx +28 -9
- package/src/components/RoleManager.css +10 -10
- package/src/components/Spreadsheet.css +216 -216
- package/src/components/Spreadsheet.stories.tsx +362 -362
- package/src/components/Spreadsheet.tsx +351 -351
- package/src/components/SpreadsheetSimple.stories.tsx +27 -27
- package/src/components/Tabs.tsx +152 -152
- package/src/components/index.ts +5 -0
- package/src/styles/index.css +41 -4
- package/src/utils/excelExport.stories.tsx +535 -0
- package/src/utils/excelExport.ts +225 -0
- package/src/utils/index.ts +3 -0
- package/tailwind.config.js +253 -253
- package/dist/components/Button.stories.d.ts +0 -51
- package/dist/components/Button.stories.d.ts.map +0 -1
- package/dist/components/ChartVisualizationUI.d.ts +0 -21
- package/dist/components/ChartVisualizationUI.d.ts.map +0 -1
- package/dist/components/ChatUI.d.ts +0 -23
- package/dist/components/ChatUI.d.ts.map +0 -1
- package/dist/components/CommissionDashboardUI.d.ts +0 -25
- package/dist/components/CommissionDashboardUI.d.ts.map +0 -1
- package/dist/components/DataTable.stories.d.ts +0 -23
- package/dist/components/DataTable.stories.d.ts.map +0 -1
- package/dist/components/FormField.d.ts +0 -35
- package/dist/components/FormField.d.ts.map +0 -1
- package/dist/components/Input.stories.d.ts +0 -366
- package/dist/components/Input.stories.d.ts.map +0 -1
- package/dist/components/InsightsPanelUI.d.ts +0 -21
- package/dist/components/InsightsPanelUI.d.ts.map +0 -1
- package/dist/components/PaymentHistoryTimeline.d.ts +0 -34
- package/dist/components/PaymentHistoryTimeline.d.ts.map +0 -1
- package/dist/components/RelationshipManagerUI.d.ts +0 -60
- package/dist/components/RelationshipManagerUI.d.ts.map +0 -1
- package/dist/components/RoleManager.d.ts +0 -19
- package/dist/components/RoleManager.d.ts.map +0 -1
- package/dist/components/SplitCommissionBadge.d.ts +0 -18
- package/dist/components/SplitCommissionBadge.d.ts.map +0 -1
- package/dist/components/Spreadsheet.css +0 -216
- package/dist/components/__tests__/Button.test.d.ts +0 -2
- package/dist/components/__tests__/Button.test.d.ts.map +0 -1
- package/dist/components/__tests__/Input.test.d.ts +0 -2
- package/dist/components/__tests__/Input.test.d.ts.map +0 -1
|
@@ -1,351 +1,351 @@
|
|
|
1
|
-
import React, { useState, useCallback } from 'react';
|
|
2
|
-
import BaseSpreadsheet, { CellBase, Matrix } from 'react-spreadsheet';
|
|
3
|
-
import { read, utils, writeFile, WorkBook } from 'xlsx';
|
|
4
|
-
import Button from './Button';
|
|
5
|
-
import Card, { CardHeader, CardTitle, CardContent } from './Card';
|
|
6
|
-
import Stack from './Stack';
|
|
7
|
-
import { Download, Upload, Save } from 'lucide-react';
|
|
8
|
-
import { addSuccessMessage, addErrorMessage } from './StatusBar';
|
|
9
|
-
import './Spreadsheet.css';
|
|
10
|
-
|
|
11
|
-
// Re-export types for external use
|
|
12
|
-
export type { CellBase, Matrix } from 'react-spreadsheet';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Enhanced cell type with formula support
|
|
16
|
-
*/
|
|
17
|
-
export interface SpreadsheetCell extends CellBase {
|
|
18
|
-
value: string | number | boolean;
|
|
19
|
-
formula?: string;
|
|
20
|
-
readOnly?: boolean;
|
|
21
|
-
className?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Spreadsheet component props
|
|
26
|
-
*/
|
|
27
|
-
export interface SpreadsheetProps {
|
|
28
|
-
/** Initial data matrix */
|
|
29
|
-
data?: Matrix<SpreadsheetCell>;
|
|
30
|
-
/** Callback when data changes */
|
|
31
|
-
onChange?: (data: Matrix<SpreadsheetCell>) => void;
|
|
32
|
-
/** Number of rows to display */
|
|
33
|
-
rows?: number;
|
|
34
|
-
/** Number of columns to display */
|
|
35
|
-
columns?: number;
|
|
36
|
-
/** Column labels (A, B, C... if not provided) */
|
|
37
|
-
columnLabels?: string[];
|
|
38
|
-
/** Row labels (1, 2, 3... if not provided) */
|
|
39
|
-
rowLabels?: string[];
|
|
40
|
-
/** Show toolbar with actions */
|
|
41
|
-
showToolbar?: boolean;
|
|
42
|
-
/** Enable Excel import */
|
|
43
|
-
enableImport?: boolean;
|
|
44
|
-
/** Enable Excel export */
|
|
45
|
-
enableExport?: boolean;
|
|
46
|
-
/** Enable save button */
|
|
47
|
-
enableSave?: boolean;
|
|
48
|
-
/** Save handler */
|
|
49
|
-
onSave?: (data: Matrix<SpreadsheetCell>) => Promise<void> | void;
|
|
50
|
-
/** Title to display in toolbar */
|
|
51
|
-
title?: string;
|
|
52
|
-
/** Additional toolbar actions */
|
|
53
|
-
actions?: React.ReactNode;
|
|
54
|
-
/** Wrap in Card component */
|
|
55
|
-
wrapInCard?: boolean;
|
|
56
|
-
/** Custom className for the spreadsheet container */
|
|
57
|
-
className?: string;
|
|
58
|
-
/** Make entire spreadsheet read-only */
|
|
59
|
-
readOnly?: boolean;
|
|
60
|
-
/** Default export filename */
|
|
61
|
-
exportFileName?: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Spreadsheet - Interactive spreadsheet component with formula support
|
|
66
|
-
*
|
|
67
|
-
* A full-featured spreadsheet component for report designers and data editing.
|
|
68
|
-
* Built on react-spreadsheet with Fast Formula Parser (280+ Excel formulas).
|
|
69
|
-
*
|
|
70
|
-
* **Features:**
|
|
71
|
-
* - Excel-like formula support (SUM, AVERAGE, VLOOKUP, IF, etc.)
|
|
72
|
-
* - Excel import/export with SheetJS
|
|
73
|
-
* - Save/load functionality
|
|
74
|
-
* - Keyboard navigation
|
|
75
|
-
* - Copy/paste support
|
|
76
|
-
* - Customizable styling to match notebook-ui aesthetic
|
|
77
|
-
*
|
|
78
|
-
* @example
|
|
79
|
-
* ```tsx
|
|
80
|
-
* // Basic spreadsheet
|
|
81
|
-
* <Spreadsheet
|
|
82
|
-
* rows={10}
|
|
83
|
-
* columns={5}
|
|
84
|
-
* showToolbar
|
|
85
|
-
* />
|
|
86
|
-
*
|
|
87
|
-
* // Report designer with formulas
|
|
88
|
-
* const [reportData, setReportData] = useState<Matrix<SpreadsheetCell>>([
|
|
89
|
-
* [{ value: 'Q1' }, { value: 100 }],
|
|
90
|
-
* [{ value: 'Q2' }, { value: 200 }],
|
|
91
|
-
* [{ value: 'Total' }, { formula: '=SUM(B1:B2)' }],
|
|
92
|
-
* ]);
|
|
93
|
-
*
|
|
94
|
-
* <Spreadsheet
|
|
95
|
-
* data={reportData}
|
|
96
|
-
* onChange={setReportData}
|
|
97
|
-
* title="Sales Report"
|
|
98
|
-
* showToolbar
|
|
99
|
-
* enableImport
|
|
100
|
-
* enableExport
|
|
101
|
-
* enableSave
|
|
102
|
-
* onSave={async (data) => {
|
|
103
|
-
* await saveReport(data);
|
|
104
|
-
* }}
|
|
105
|
-
* />
|
|
106
|
-
*
|
|
107
|
-
* // With custom actions
|
|
108
|
-
* <Spreadsheet
|
|
109
|
-
* data={data}
|
|
110
|
-
* onChange={setData}
|
|
111
|
-
* showToolbar
|
|
112
|
-
* actions={
|
|
113
|
-
* <Button onClick={handleCustomAction}>Custom Action</Button>
|
|
114
|
-
* }
|
|
115
|
-
* />
|
|
116
|
-
* ```
|
|
117
|
-
*/
|
|
118
|
-
export const Spreadsheet: React.FC<SpreadsheetProps> = ({
|
|
119
|
-
data: initialData,
|
|
120
|
-
onChange,
|
|
121
|
-
rows = 20,
|
|
122
|
-
columns = 10,
|
|
123
|
-
columnLabels,
|
|
124
|
-
rowLabels,
|
|
125
|
-
showToolbar = false,
|
|
126
|
-
enableImport = false,
|
|
127
|
-
enableExport = false,
|
|
128
|
-
enableSave = false,
|
|
129
|
-
onSave,
|
|
130
|
-
title,
|
|
131
|
-
actions,
|
|
132
|
-
wrapInCard = false,
|
|
133
|
-
className = '',
|
|
134
|
-
readOnly = false,
|
|
135
|
-
exportFileName = 'spreadsheet.xlsx',
|
|
136
|
-
}) => {
|
|
137
|
-
// Initialize data if not provided
|
|
138
|
-
const [data, setData] = useState<Matrix<SpreadsheetCell>>(() => {
|
|
139
|
-
if (initialData) return initialData;
|
|
140
|
-
return Array(rows)
|
|
141
|
-
.fill(null)
|
|
142
|
-
.map(() => Array(columns).fill(null).map(() => ({ value: '' })));
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const [isSaving, setIsSaving] = useState(false);
|
|
146
|
-
|
|
147
|
-
// Handle data changes
|
|
148
|
-
const handleChange = useCallback(
|
|
149
|
-
(newData: Matrix<SpreadsheetCell>) => {
|
|
150
|
-
setData(newData);
|
|
151
|
-
onChange?.(newData);
|
|
152
|
-
},
|
|
153
|
-
[onChange]
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
// Handle Excel import
|
|
157
|
-
const handleImport = useCallback(
|
|
158
|
-
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
159
|
-
const file = event.target.files?.[0];
|
|
160
|
-
if (!file) return;
|
|
161
|
-
|
|
162
|
-
const reader = new FileReader();
|
|
163
|
-
reader.onload = (e) => {
|
|
164
|
-
try {
|
|
165
|
-
const workbook: WorkBook = read(e.target?.result, { type: 'binary' });
|
|
166
|
-
const sheetName = workbook.SheetNames[0];
|
|
167
|
-
const worksheet = workbook.Sheets[sheetName];
|
|
168
|
-
|
|
169
|
-
// Convert to array of arrays
|
|
170
|
-
const jsonData: any[][] = utils.sheet_to_json(worksheet, { header: 1 });
|
|
171
|
-
|
|
172
|
-
// Convert to spreadsheet format
|
|
173
|
-
const spreadsheetData: Matrix<SpreadsheetCell> = jsonData.map(row =>
|
|
174
|
-
row.map(cell => ({
|
|
175
|
-
value: cell,
|
|
176
|
-
}))
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
handleChange(spreadsheetData);
|
|
180
|
-
addSuccessMessage('Excel file imported successfully');
|
|
181
|
-
} catch (error) {
|
|
182
|
-
console.error('Error importing Excel file:', error);
|
|
183
|
-
addErrorMessage('Failed to import Excel file');
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
reader.readAsBinaryString(file);
|
|
187
|
-
|
|
188
|
-
// Reset input
|
|
189
|
-
event.target.value = '';
|
|
190
|
-
},
|
|
191
|
-
[handleChange]
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
// Handle Excel export
|
|
195
|
-
const handleExport = useCallback(() => {
|
|
196
|
-
try {
|
|
197
|
-
// Convert spreadsheet data to worksheet format
|
|
198
|
-
const worksheetData = data.map(row =>
|
|
199
|
-
row.map(cell => {
|
|
200
|
-
// If cell has a formula, export the calculated value
|
|
201
|
-
if (cell?.formula) {
|
|
202
|
-
return cell.value ?? cell.formula;
|
|
203
|
-
}
|
|
204
|
-
return cell?.value ?? '';
|
|
205
|
-
})
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
const worksheet = utils.aoa_to_sheet(worksheetData);
|
|
209
|
-
const workbook = utils.book_new();
|
|
210
|
-
utils.book_append_sheet(workbook, worksheet, 'Sheet1');
|
|
211
|
-
|
|
212
|
-
writeFile(workbook, exportFileName);
|
|
213
|
-
addSuccessMessage('Excel file exported successfully');
|
|
214
|
-
} catch (error) {
|
|
215
|
-
console.error('Error exporting Excel file:', error);
|
|
216
|
-
addErrorMessage('Failed to export Excel file');
|
|
217
|
-
}
|
|
218
|
-
}, [data, exportFileName]);
|
|
219
|
-
|
|
220
|
-
// Handle save
|
|
221
|
-
const handleSave = useCallback(async () => {
|
|
222
|
-
if (!onSave) return;
|
|
223
|
-
|
|
224
|
-
setIsSaving(true);
|
|
225
|
-
try {
|
|
226
|
-
await onSave(data);
|
|
227
|
-
addSuccessMessage('Spreadsheet saved successfully');
|
|
228
|
-
} catch (error) {
|
|
229
|
-
console.error('Error saving spreadsheet:', error);
|
|
230
|
-
addErrorMessage('Failed to save spreadsheet');
|
|
231
|
-
} finally {
|
|
232
|
-
setIsSaving(false);
|
|
233
|
-
}
|
|
234
|
-
}, [onSave, data]);
|
|
235
|
-
|
|
236
|
-
// Build toolbar
|
|
237
|
-
const toolbar = showToolbar && (
|
|
238
|
-
<Stack direction="horizontal" spacing="md" align="center" className="mb-4">
|
|
239
|
-
{title && <div className="text-lg font-medium text-ink-900 flex-1">{title}</div>}
|
|
240
|
-
|
|
241
|
-
{enableImport && (
|
|
242
|
-
<label>
|
|
243
|
-
<input
|
|
244
|
-
type="file"
|
|
245
|
-
accept=".xlsx,.xls,.csv"
|
|
246
|
-
onChange={handleImport}
|
|
247
|
-
className="hidden"
|
|
248
|
-
/>
|
|
249
|
-
<Button variant="ghost" size="sm" icon={<Upload className="h-4 w-4" />}>
|
|
250
|
-
Import
|
|
251
|
-
</Button>
|
|
252
|
-
</label>
|
|
253
|
-
)}
|
|
254
|
-
|
|
255
|
-
{enableExport && (
|
|
256
|
-
<Button
|
|
257
|
-
variant="ghost"
|
|
258
|
-
size="sm"
|
|
259
|
-
icon={<Download className="h-4 w-4" />}
|
|
260
|
-
onClick={handleExport}
|
|
261
|
-
>
|
|
262
|
-
Export
|
|
263
|
-
</Button>
|
|
264
|
-
)}
|
|
265
|
-
|
|
266
|
-
{enableSave && onSave && (
|
|
267
|
-
<Button
|
|
268
|
-
variant="primary"
|
|
269
|
-
size="sm"
|
|
270
|
-
icon={<Save className="h-4 w-4" />}
|
|
271
|
-
onClick={handleSave}
|
|
272
|
-
loading={isSaving}
|
|
273
|
-
>
|
|
274
|
-
Save
|
|
275
|
-
</Button>
|
|
276
|
-
)}
|
|
277
|
-
|
|
278
|
-
{actions}
|
|
279
|
-
</Stack>
|
|
280
|
-
);
|
|
281
|
-
|
|
282
|
-
// Spreadsheet component
|
|
283
|
-
const spreadsheet = (
|
|
284
|
-
<div className={`spreadsheet-container ${className}`}>
|
|
285
|
-
<BaseSpreadsheet
|
|
286
|
-
data={data}
|
|
287
|
-
onChange={readOnly ? undefined : handleChange}
|
|
288
|
-
columnLabels={columnLabels}
|
|
289
|
-
rowLabels={rowLabels}
|
|
290
|
-
className="notebook-spreadsheet"
|
|
291
|
-
/>
|
|
292
|
-
</div>
|
|
293
|
-
);
|
|
294
|
-
|
|
295
|
-
// Wrap in card if requested
|
|
296
|
-
if (wrapInCard) {
|
|
297
|
-
return (
|
|
298
|
-
<Card>
|
|
299
|
-
{(showToolbar || title) && (
|
|
300
|
-
<CardHeader>
|
|
301
|
-
{title && !showToolbar && <CardTitle>{title}</CardTitle>}
|
|
302
|
-
{toolbar}
|
|
303
|
-
</CardHeader>
|
|
304
|
-
)}
|
|
305
|
-
<CardContent>{spreadsheet}</CardContent>
|
|
306
|
-
</Card>
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return (
|
|
311
|
-
<>
|
|
312
|
-
{toolbar}
|
|
313
|
-
{spreadsheet}
|
|
314
|
-
</>
|
|
315
|
-
);
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* SpreadsheetReport - Pre-configured spreadsheet for report designer
|
|
320
|
-
*
|
|
321
|
-
* A ready-to-use spreadsheet component specifically designed for building
|
|
322
|
-
* and editing reports with formulas, import/export, and save functionality.
|
|
323
|
-
*
|
|
324
|
-
* @example
|
|
325
|
-
* ```tsx
|
|
326
|
-
* const [reportData, setReportData] = useState<Matrix<SpreadsheetCell>>();
|
|
327
|
-
*
|
|
328
|
-
* <SpreadsheetReport
|
|
329
|
-
* data={reportData}
|
|
330
|
-
* onChange={setReportData}
|
|
331
|
-
* title="Monthly Sales Report"
|
|
332
|
-
* onSave={async (data) => {
|
|
333
|
-
* await api.saveReport(reportId, data);
|
|
334
|
-
* }}
|
|
335
|
-
* />
|
|
336
|
-
* ```
|
|
337
|
-
*/
|
|
338
|
-
export const SpreadsheetReport: React.FC<
|
|
339
|
-
Omit<SpreadsheetProps, 'showToolbar' | 'enableImport' | 'enableExport' | 'enableSave' | 'wrapInCard'>
|
|
340
|
-
> = (props) => {
|
|
341
|
-
return (
|
|
342
|
-
<Spreadsheet
|
|
343
|
-
{...props}
|
|
344
|
-
showToolbar
|
|
345
|
-
enableImport
|
|
346
|
-
enableExport
|
|
347
|
-
enableSave
|
|
348
|
-
wrapInCard
|
|
349
|
-
/>
|
|
350
|
-
);
|
|
351
|
-
};
|
|
1
|
+
import React, { useState, useCallback } from 'react';
|
|
2
|
+
import BaseSpreadsheet, { CellBase, Matrix } from 'react-spreadsheet';
|
|
3
|
+
import { read, utils, writeFile, WorkBook } from 'xlsx';
|
|
4
|
+
import Button from './Button';
|
|
5
|
+
import Card, { CardHeader, CardTitle, CardContent } from './Card';
|
|
6
|
+
import Stack from './Stack';
|
|
7
|
+
import { Download, Upload, Save } from 'lucide-react';
|
|
8
|
+
import { addSuccessMessage, addErrorMessage } from './StatusBar';
|
|
9
|
+
import './Spreadsheet.css';
|
|
10
|
+
|
|
11
|
+
// Re-export types for external use
|
|
12
|
+
export type { CellBase, Matrix } from 'react-spreadsheet';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Enhanced cell type with formula support
|
|
16
|
+
*/
|
|
17
|
+
export interface SpreadsheetCell extends CellBase {
|
|
18
|
+
value: string | number | boolean;
|
|
19
|
+
formula?: string;
|
|
20
|
+
readOnly?: boolean;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Spreadsheet component props
|
|
26
|
+
*/
|
|
27
|
+
export interface SpreadsheetProps {
|
|
28
|
+
/** Initial data matrix */
|
|
29
|
+
data?: Matrix<SpreadsheetCell>;
|
|
30
|
+
/** Callback when data changes */
|
|
31
|
+
onChange?: (data: Matrix<SpreadsheetCell>) => void;
|
|
32
|
+
/** Number of rows to display */
|
|
33
|
+
rows?: number;
|
|
34
|
+
/** Number of columns to display */
|
|
35
|
+
columns?: number;
|
|
36
|
+
/** Column labels (A, B, C... if not provided) */
|
|
37
|
+
columnLabels?: string[];
|
|
38
|
+
/** Row labels (1, 2, 3... if not provided) */
|
|
39
|
+
rowLabels?: string[];
|
|
40
|
+
/** Show toolbar with actions */
|
|
41
|
+
showToolbar?: boolean;
|
|
42
|
+
/** Enable Excel import */
|
|
43
|
+
enableImport?: boolean;
|
|
44
|
+
/** Enable Excel export */
|
|
45
|
+
enableExport?: boolean;
|
|
46
|
+
/** Enable save button */
|
|
47
|
+
enableSave?: boolean;
|
|
48
|
+
/** Save handler */
|
|
49
|
+
onSave?: (data: Matrix<SpreadsheetCell>) => Promise<void> | void;
|
|
50
|
+
/** Title to display in toolbar */
|
|
51
|
+
title?: string;
|
|
52
|
+
/** Additional toolbar actions */
|
|
53
|
+
actions?: React.ReactNode;
|
|
54
|
+
/** Wrap in Card component */
|
|
55
|
+
wrapInCard?: boolean;
|
|
56
|
+
/** Custom className for the spreadsheet container */
|
|
57
|
+
className?: string;
|
|
58
|
+
/** Make entire spreadsheet read-only */
|
|
59
|
+
readOnly?: boolean;
|
|
60
|
+
/** Default export filename */
|
|
61
|
+
exportFileName?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Spreadsheet - Interactive spreadsheet component with formula support
|
|
66
|
+
*
|
|
67
|
+
* A full-featured spreadsheet component for report designers and data editing.
|
|
68
|
+
* Built on react-spreadsheet with Fast Formula Parser (280+ Excel formulas).
|
|
69
|
+
*
|
|
70
|
+
* **Features:**
|
|
71
|
+
* - Excel-like formula support (SUM, AVERAGE, VLOOKUP, IF, etc.)
|
|
72
|
+
* - Excel import/export with SheetJS
|
|
73
|
+
* - Save/load functionality
|
|
74
|
+
* - Keyboard navigation
|
|
75
|
+
* - Copy/paste support
|
|
76
|
+
* - Customizable styling to match notebook-ui aesthetic
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```tsx
|
|
80
|
+
* // Basic spreadsheet
|
|
81
|
+
* <Spreadsheet
|
|
82
|
+
* rows={10}
|
|
83
|
+
* columns={5}
|
|
84
|
+
* showToolbar
|
|
85
|
+
* />
|
|
86
|
+
*
|
|
87
|
+
* // Report designer with formulas
|
|
88
|
+
* const [reportData, setReportData] = useState<Matrix<SpreadsheetCell>>([
|
|
89
|
+
* [{ value: 'Q1' }, { value: 100 }],
|
|
90
|
+
* [{ value: 'Q2' }, { value: 200 }],
|
|
91
|
+
* [{ value: 'Total' }, { formula: '=SUM(B1:B2)' }],
|
|
92
|
+
* ]);
|
|
93
|
+
*
|
|
94
|
+
* <Spreadsheet
|
|
95
|
+
* data={reportData}
|
|
96
|
+
* onChange={setReportData}
|
|
97
|
+
* title="Sales Report"
|
|
98
|
+
* showToolbar
|
|
99
|
+
* enableImport
|
|
100
|
+
* enableExport
|
|
101
|
+
* enableSave
|
|
102
|
+
* onSave={async (data) => {
|
|
103
|
+
* await saveReport(data);
|
|
104
|
+
* }}
|
|
105
|
+
* />
|
|
106
|
+
*
|
|
107
|
+
* // With custom actions
|
|
108
|
+
* <Spreadsheet
|
|
109
|
+
* data={data}
|
|
110
|
+
* onChange={setData}
|
|
111
|
+
* showToolbar
|
|
112
|
+
* actions={
|
|
113
|
+
* <Button onClick={handleCustomAction}>Custom Action</Button>
|
|
114
|
+
* }
|
|
115
|
+
* />
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export const Spreadsheet: React.FC<SpreadsheetProps> = ({
|
|
119
|
+
data: initialData,
|
|
120
|
+
onChange,
|
|
121
|
+
rows = 20,
|
|
122
|
+
columns = 10,
|
|
123
|
+
columnLabels,
|
|
124
|
+
rowLabels,
|
|
125
|
+
showToolbar = false,
|
|
126
|
+
enableImport = false,
|
|
127
|
+
enableExport = false,
|
|
128
|
+
enableSave = false,
|
|
129
|
+
onSave,
|
|
130
|
+
title,
|
|
131
|
+
actions,
|
|
132
|
+
wrapInCard = false,
|
|
133
|
+
className = '',
|
|
134
|
+
readOnly = false,
|
|
135
|
+
exportFileName = 'spreadsheet.xlsx',
|
|
136
|
+
}) => {
|
|
137
|
+
// Initialize data if not provided
|
|
138
|
+
const [data, setData] = useState<Matrix<SpreadsheetCell>>(() => {
|
|
139
|
+
if (initialData) return initialData;
|
|
140
|
+
return Array(rows)
|
|
141
|
+
.fill(null)
|
|
142
|
+
.map(() => Array(columns).fill(null).map(() => ({ value: '' })));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
146
|
+
|
|
147
|
+
// Handle data changes
|
|
148
|
+
const handleChange = useCallback(
|
|
149
|
+
(newData: Matrix<SpreadsheetCell>) => {
|
|
150
|
+
setData(newData);
|
|
151
|
+
onChange?.(newData);
|
|
152
|
+
},
|
|
153
|
+
[onChange]
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Handle Excel import
|
|
157
|
+
const handleImport = useCallback(
|
|
158
|
+
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
159
|
+
const file = event.target.files?.[0];
|
|
160
|
+
if (!file) return;
|
|
161
|
+
|
|
162
|
+
const reader = new FileReader();
|
|
163
|
+
reader.onload = (e) => {
|
|
164
|
+
try {
|
|
165
|
+
const workbook: WorkBook = read(e.target?.result, { type: 'binary' });
|
|
166
|
+
const sheetName = workbook.SheetNames[0];
|
|
167
|
+
const worksheet = workbook.Sheets[sheetName];
|
|
168
|
+
|
|
169
|
+
// Convert to array of arrays
|
|
170
|
+
const jsonData: any[][] = utils.sheet_to_json(worksheet, { header: 1 });
|
|
171
|
+
|
|
172
|
+
// Convert to spreadsheet format
|
|
173
|
+
const spreadsheetData: Matrix<SpreadsheetCell> = jsonData.map(row =>
|
|
174
|
+
row.map(cell => ({
|
|
175
|
+
value: cell,
|
|
176
|
+
}))
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
handleChange(spreadsheetData);
|
|
180
|
+
addSuccessMessage('Excel file imported successfully');
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error('Error importing Excel file:', error);
|
|
183
|
+
addErrorMessage('Failed to import Excel file');
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
reader.readAsBinaryString(file);
|
|
187
|
+
|
|
188
|
+
// Reset input
|
|
189
|
+
event.target.value = '';
|
|
190
|
+
},
|
|
191
|
+
[handleChange]
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// Handle Excel export
|
|
195
|
+
const handleExport = useCallback(() => {
|
|
196
|
+
try {
|
|
197
|
+
// Convert spreadsheet data to worksheet format
|
|
198
|
+
const worksheetData = data.map(row =>
|
|
199
|
+
row.map(cell => {
|
|
200
|
+
// If cell has a formula, export the calculated value
|
|
201
|
+
if (cell?.formula) {
|
|
202
|
+
return cell.value ?? cell.formula;
|
|
203
|
+
}
|
|
204
|
+
return cell?.value ?? '';
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const worksheet = utils.aoa_to_sheet(worksheetData);
|
|
209
|
+
const workbook = utils.book_new();
|
|
210
|
+
utils.book_append_sheet(workbook, worksheet, 'Sheet1');
|
|
211
|
+
|
|
212
|
+
writeFile(workbook, exportFileName);
|
|
213
|
+
addSuccessMessage('Excel file exported successfully');
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.error('Error exporting Excel file:', error);
|
|
216
|
+
addErrorMessage('Failed to export Excel file');
|
|
217
|
+
}
|
|
218
|
+
}, [data, exportFileName]);
|
|
219
|
+
|
|
220
|
+
// Handle save
|
|
221
|
+
const handleSave = useCallback(async () => {
|
|
222
|
+
if (!onSave) return;
|
|
223
|
+
|
|
224
|
+
setIsSaving(true);
|
|
225
|
+
try {
|
|
226
|
+
await onSave(data);
|
|
227
|
+
addSuccessMessage('Spreadsheet saved successfully');
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error('Error saving spreadsheet:', error);
|
|
230
|
+
addErrorMessage('Failed to save spreadsheet');
|
|
231
|
+
} finally {
|
|
232
|
+
setIsSaving(false);
|
|
233
|
+
}
|
|
234
|
+
}, [onSave, data]);
|
|
235
|
+
|
|
236
|
+
// Build toolbar
|
|
237
|
+
const toolbar = showToolbar && (
|
|
238
|
+
<Stack direction="horizontal" spacing="md" align="center" className="mb-4">
|
|
239
|
+
{title && <div className="text-lg font-medium text-ink-900 flex-1">{title}</div>}
|
|
240
|
+
|
|
241
|
+
{enableImport && (
|
|
242
|
+
<label>
|
|
243
|
+
<input
|
|
244
|
+
type="file"
|
|
245
|
+
accept=".xlsx,.xls,.csv"
|
|
246
|
+
onChange={handleImport}
|
|
247
|
+
className="hidden"
|
|
248
|
+
/>
|
|
249
|
+
<Button variant="ghost" size="sm" icon={<Upload className="h-4 w-4" />}>
|
|
250
|
+
Import
|
|
251
|
+
</Button>
|
|
252
|
+
</label>
|
|
253
|
+
)}
|
|
254
|
+
|
|
255
|
+
{enableExport && (
|
|
256
|
+
<Button
|
|
257
|
+
variant="ghost"
|
|
258
|
+
size="sm"
|
|
259
|
+
icon={<Download className="h-4 w-4" />}
|
|
260
|
+
onClick={handleExport}
|
|
261
|
+
>
|
|
262
|
+
Export
|
|
263
|
+
</Button>
|
|
264
|
+
)}
|
|
265
|
+
|
|
266
|
+
{enableSave && onSave && (
|
|
267
|
+
<Button
|
|
268
|
+
variant="primary"
|
|
269
|
+
size="sm"
|
|
270
|
+
icon={<Save className="h-4 w-4" />}
|
|
271
|
+
onClick={handleSave}
|
|
272
|
+
loading={isSaving}
|
|
273
|
+
>
|
|
274
|
+
Save
|
|
275
|
+
</Button>
|
|
276
|
+
)}
|
|
277
|
+
|
|
278
|
+
{actions}
|
|
279
|
+
</Stack>
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// Spreadsheet component
|
|
283
|
+
const spreadsheet = (
|
|
284
|
+
<div className={`spreadsheet-container ${className}`}>
|
|
285
|
+
<BaseSpreadsheet
|
|
286
|
+
data={data}
|
|
287
|
+
onChange={readOnly ? undefined : handleChange}
|
|
288
|
+
columnLabels={columnLabels}
|
|
289
|
+
rowLabels={rowLabels}
|
|
290
|
+
className="notebook-spreadsheet"
|
|
291
|
+
/>
|
|
292
|
+
</div>
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// Wrap in card if requested
|
|
296
|
+
if (wrapInCard) {
|
|
297
|
+
return (
|
|
298
|
+
<Card>
|
|
299
|
+
{(showToolbar || title) && (
|
|
300
|
+
<CardHeader>
|
|
301
|
+
{title && !showToolbar && <CardTitle>{title}</CardTitle>}
|
|
302
|
+
{toolbar}
|
|
303
|
+
</CardHeader>
|
|
304
|
+
)}
|
|
305
|
+
<CardContent>{spreadsheet}</CardContent>
|
|
306
|
+
</Card>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<>
|
|
312
|
+
{toolbar}
|
|
313
|
+
{spreadsheet}
|
|
314
|
+
</>
|
|
315
|
+
);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* SpreadsheetReport - Pre-configured spreadsheet for report designer
|
|
320
|
+
*
|
|
321
|
+
* A ready-to-use spreadsheet component specifically designed for building
|
|
322
|
+
* and editing reports with formulas, import/export, and save functionality.
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* ```tsx
|
|
326
|
+
* const [reportData, setReportData] = useState<Matrix<SpreadsheetCell>>();
|
|
327
|
+
*
|
|
328
|
+
* <SpreadsheetReport
|
|
329
|
+
* data={reportData}
|
|
330
|
+
* onChange={setReportData}
|
|
331
|
+
* title="Monthly Sales Report"
|
|
332
|
+
* onSave={async (data) => {
|
|
333
|
+
* await api.saveReport(reportId, data);
|
|
334
|
+
* }}
|
|
335
|
+
* />
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
export const SpreadsheetReport: React.FC<
|
|
339
|
+
Omit<SpreadsheetProps, 'showToolbar' | 'enableImport' | 'enableExport' | 'enableSave' | 'wrapInCard'>
|
|
340
|
+
> = (props) => {
|
|
341
|
+
return (
|
|
342
|
+
<Spreadsheet
|
|
343
|
+
{...props}
|
|
344
|
+
showToolbar
|
|
345
|
+
enableImport
|
|
346
|
+
enableExport
|
|
347
|
+
enableSave
|
|
348
|
+
wrapInCard
|
|
349
|
+
/>
|
|
350
|
+
);
|
|
351
|
+
};
|