@object-ui/plugin-dashboard 0.1.1 → 0.5.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/.turbo/turbo-build.log +20 -0
- package/dist/index.css +1 -0
- package/dist/index.js +5797 -266
- package/dist/index.umd.cjs +5 -2
- package/dist/src/DashboardGridLayout.d.ts +11 -0
- package/dist/src/DashboardGridLayout.d.ts.map +1 -0
- package/dist/src/DashboardRenderer.d.ts +1 -1
- package/dist/src/DashboardRenderer.d.ts.map +1 -1
- package/dist/src/MetricCard.d.ts +16 -0
- package/dist/src/MetricCard.d.ts.map +1 -0
- package/dist/src/MetricWidget.d.ts +1 -1
- package/dist/src/MetricWidget.d.ts.map +1 -1
- package/dist/src/ReportBuilder.d.ts +11 -0
- package/dist/src/ReportBuilder.d.ts.map +1 -0
- package/dist/src/ReportRenderer.d.ts +15 -0
- package/dist/src/ReportRenderer.d.ts.map +1 -0
- package/dist/src/ReportViewer.d.ts +11 -0
- package/dist/src/ReportViewer.d.ts.map +1 -0
- package/dist/src/index.d.ts +19 -1
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +10 -8
- package/src/DashboardGridLayout.tsx +210 -0
- package/src/DashboardRenderer.tsx +108 -20
- package/src/MetricCard.tsx +75 -0
- package/src/MetricWidget.tsx +13 -3
- package/src/ReportBuilder.tsx +625 -0
- package/src/ReportRenderer.tsx +89 -0
- package/src/ReportViewer.tsx +232 -0
- package/src/__tests__/DashboardGridLayout.test.tsx +199 -0
- package/src/__tests__/MetricCard.test.tsx +59 -0
- package/src/__tests__/ReportBuilder.test.tsx +115 -0
- package/src/__tests__/ReportViewer.test.tsx +107 -0
- package/src/index.tsx +122 -3
- package/vite.config.ts +19 -0
- package/vitest.config.ts +9 -0
- package/vitest.setup.tsx +18 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useState } from 'react';
|
|
10
|
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription, Button, Input, Label, Tabs, TabsContent, TabsList, TabsTrigger } from '@object-ui/components';
|
|
11
|
+
import type { ReportBuilderSchema, ReportSchema, ReportField, ReportFilter, ReportGroupBy, ReportSection } from '@object-ui/types';
|
|
12
|
+
import { Plus, Trash2, Save, X, Settings, Filter, Layers, Calendar } from 'lucide-react';
|
|
13
|
+
import { ReportViewer } from './ReportViewer';
|
|
14
|
+
|
|
15
|
+
export interface ReportBuilderProps {
|
|
16
|
+
schema: ReportBuilderSchema;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* ReportBuilder - Interactive report builder component
|
|
21
|
+
* Allows users to configure report fields, filters, grouping, sections, and export settings
|
|
22
|
+
*/
|
|
23
|
+
export const ReportBuilder: React.FC<ReportBuilderProps> = ({ schema }) => {
|
|
24
|
+
const {
|
|
25
|
+
report: initialReport,
|
|
26
|
+
dataSources = [],
|
|
27
|
+
availableFields = [],
|
|
28
|
+
showPreview = true,
|
|
29
|
+
onSave,
|
|
30
|
+
onCancel
|
|
31
|
+
} = schema;
|
|
32
|
+
|
|
33
|
+
const [report, setReport] = useState<ReportSchema>(initialReport || {
|
|
34
|
+
type: 'report',
|
|
35
|
+
title: 'New Report',
|
|
36
|
+
fields: [],
|
|
37
|
+
filters: [],
|
|
38
|
+
groupBy: [],
|
|
39
|
+
sections: [],
|
|
40
|
+
showExportButtons: true,
|
|
41
|
+
showPrintButton: true
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const [selectedFields, setSelectedFields] = useState<ReportField[]>(report.fields || []);
|
|
45
|
+
const [filters, setFilters] = useState<ReportFilter[]>(report.filters || []);
|
|
46
|
+
const [groupBy, setGroupBy] = useState<ReportGroupBy[]>(report.groupBy || []);
|
|
47
|
+
const [sections, setSections] = useState<ReportSection[]>(report.sections || []);
|
|
48
|
+
|
|
49
|
+
// Field Management
|
|
50
|
+
const handleAddField = () => {
|
|
51
|
+
if (availableFields.length > 0 && selectedFields.length < availableFields.length) {
|
|
52
|
+
const nextField = availableFields.find(
|
|
53
|
+
f => !selectedFields.some(sf => sf.name === f.name)
|
|
54
|
+
);
|
|
55
|
+
if (nextField) {
|
|
56
|
+
const newFields = [...selectedFields, nextField];
|
|
57
|
+
setSelectedFields(newFields);
|
|
58
|
+
setReport({ ...report, fields: newFields });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleRemoveField = (index: number) => {
|
|
64
|
+
const newFields = selectedFields.filter((_, i) => i !== index);
|
|
65
|
+
setSelectedFields(newFields);
|
|
66
|
+
setReport({ ...report, fields: newFields });
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const handleFieldChange = (index: number, field: ReportField) => {
|
|
70
|
+
const newFields = [...selectedFields];
|
|
71
|
+
newFields[index] = field;
|
|
72
|
+
setSelectedFields(newFields);
|
|
73
|
+
setReport({ ...report, fields: newFields });
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Filter Management
|
|
77
|
+
const handleAddFilter = () => {
|
|
78
|
+
const newFilter: ReportFilter = {
|
|
79
|
+
field: availableFields[0]?.name || '',
|
|
80
|
+
operator: 'equals',
|
|
81
|
+
value: ''
|
|
82
|
+
};
|
|
83
|
+
const newFilters = [...filters, newFilter];
|
|
84
|
+
setFilters(newFilters);
|
|
85
|
+
setReport({ ...report, filters: newFilters });
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const handleRemoveFilter = (index: number) => {
|
|
89
|
+
const newFilters = filters.filter((_, i) => i !== index);
|
|
90
|
+
setFilters(newFilters);
|
|
91
|
+
setReport({ ...report, filters: newFilters });
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleFilterChange = (index: number, filter: ReportFilter) => {
|
|
95
|
+
const newFilters = [...filters];
|
|
96
|
+
newFilters[index] = filter;
|
|
97
|
+
setFilters(newFilters);
|
|
98
|
+
setReport({ ...report, filters: newFilters });
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Group By Management
|
|
102
|
+
const handleAddGroupBy = () => {
|
|
103
|
+
const newGroupBy: ReportGroupBy = {
|
|
104
|
+
field: availableFields[0]?.name || '',
|
|
105
|
+
sort: 'asc'
|
|
106
|
+
};
|
|
107
|
+
const newGroupByList = [...groupBy, newGroupBy];
|
|
108
|
+
setGroupBy(newGroupByList);
|
|
109
|
+
setReport({ ...report, groupBy: newGroupByList });
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const handleRemoveGroupBy = (index: number) => {
|
|
113
|
+
const newGroupByList = groupBy.filter((_, i) => i !== index);
|
|
114
|
+
setGroupBy(newGroupByList);
|
|
115
|
+
setReport({ ...report, groupBy: newGroupByList });
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const handleGroupByChange = (index: number, group: ReportGroupBy) => {
|
|
119
|
+
const newGroupByList = [...groupBy];
|
|
120
|
+
newGroupByList[index] = group;
|
|
121
|
+
setGroupBy(newGroupByList);
|
|
122
|
+
setReport({ ...report, groupBy: newGroupByList });
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Section Management
|
|
126
|
+
const handleAddSection = (type: ReportSection['type']) => {
|
|
127
|
+
const newSection: ReportSection = {
|
|
128
|
+
type,
|
|
129
|
+
title: `New ${type} Section`
|
|
130
|
+
};
|
|
131
|
+
const newSections = [...sections, newSection];
|
|
132
|
+
setSections(newSections);
|
|
133
|
+
setReport({ ...report, sections: newSections });
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const handleRemoveSection = (index: number) => {
|
|
137
|
+
const newSections = sections.filter((_, i) => i !== index);
|
|
138
|
+
setSections(newSections);
|
|
139
|
+
setReport({ ...report, sections: newSections });
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const handleSectionChange = (index: number, section: ReportSection) => {
|
|
143
|
+
const newSections = [...sections];
|
|
144
|
+
newSections[index] = section;
|
|
145
|
+
setSections(newSections);
|
|
146
|
+
setReport({ ...report, sections: newSections });
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const handleSave = () => {
|
|
150
|
+
console.log('Saving report:', report);
|
|
151
|
+
if (onSave) {
|
|
152
|
+
alert('Report saved! (Handler: ' + onSave + ')');
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const handleCancel = () => {
|
|
157
|
+
console.log('Canceling report builder');
|
|
158
|
+
if (onCancel) {
|
|
159
|
+
// TODO: Trigger onCancel handler from schema
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div className="space-y-4">
|
|
165
|
+
{/* Builder Form */}
|
|
166
|
+
<Card>
|
|
167
|
+
<CardHeader>
|
|
168
|
+
<CardTitle>Report Builder</CardTitle>
|
|
169
|
+
<CardDescription>Configure your report settings, fields, filters, and sections</CardDescription>
|
|
170
|
+
</CardHeader>
|
|
171
|
+
<CardContent>
|
|
172
|
+
<Tabs defaultValue="basic" className="w-full">
|
|
173
|
+
<TabsList className="grid w-full grid-cols-5">
|
|
174
|
+
<TabsTrigger value="basic">
|
|
175
|
+
<Settings className="h-4 w-4 mr-2" />
|
|
176
|
+
Basic
|
|
177
|
+
</TabsTrigger>
|
|
178
|
+
<TabsTrigger value="fields">
|
|
179
|
+
<Layers className="h-4 w-4 mr-2" />
|
|
180
|
+
Fields
|
|
181
|
+
</TabsTrigger>
|
|
182
|
+
<TabsTrigger value="filters">
|
|
183
|
+
<Filter className="h-4 w-4 mr-2" />
|
|
184
|
+
Filters
|
|
185
|
+
</TabsTrigger>
|
|
186
|
+
<TabsTrigger value="grouping">
|
|
187
|
+
<Layers className="h-4 w-4 mr-2" />
|
|
188
|
+
Group By
|
|
189
|
+
</TabsTrigger>
|
|
190
|
+
<TabsTrigger value="sections">
|
|
191
|
+
<Layers className="h-4 w-4 mr-2" />
|
|
192
|
+
Sections
|
|
193
|
+
</TabsTrigger>
|
|
194
|
+
</TabsList>
|
|
195
|
+
|
|
196
|
+
{/* Basic Settings Tab */}
|
|
197
|
+
<TabsContent value="basic" className="space-y-4 mt-4">
|
|
198
|
+
<div className="space-y-4">
|
|
199
|
+
<div className="space-y-2">
|
|
200
|
+
<Label htmlFor="report-title">Report Title</Label>
|
|
201
|
+
<Input
|
|
202
|
+
id="report-title"
|
|
203
|
+
value={report.title || ''}
|
|
204
|
+
onChange={(e) => setReport({ ...report, title: e.target.value })}
|
|
205
|
+
placeholder="Enter report title"
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div className="space-y-2">
|
|
210
|
+
<Label htmlFor="report-description">Description</Label>
|
|
211
|
+
<Input
|
|
212
|
+
id="report-description"
|
|
213
|
+
value={report.description || ''}
|
|
214
|
+
onChange={(e) => setReport({ ...report, description: e.target.value })}
|
|
215
|
+
placeholder="Enter report description"
|
|
216
|
+
/>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{dataSources.length > 0 && (
|
|
220
|
+
<div className="space-y-2">
|
|
221
|
+
<Label>Data Source</Label>
|
|
222
|
+
<select className="w-full border rounded-md p-2">
|
|
223
|
+
<option value="">Select a data source</option>
|
|
224
|
+
{dataSources.map((_ds, idx) => (
|
|
225
|
+
<option key={idx} value={idx}>
|
|
226
|
+
Data Source {idx + 1}
|
|
227
|
+
</option>
|
|
228
|
+
))}
|
|
229
|
+
</select>
|
|
230
|
+
</div>
|
|
231
|
+
)}
|
|
232
|
+
|
|
233
|
+
<div className="space-y-2">
|
|
234
|
+
<Label>Export Options</Label>
|
|
235
|
+
<div className="flex items-center gap-4">
|
|
236
|
+
<label className="flex items-center gap-2">
|
|
237
|
+
<input
|
|
238
|
+
type="checkbox"
|
|
239
|
+
checked={report.showExportButtons || false}
|
|
240
|
+
onChange={(e) => setReport({ ...report, showExportButtons: e.target.checked })}
|
|
241
|
+
/>
|
|
242
|
+
<span className="text-sm">Show Export Buttons</span>
|
|
243
|
+
</label>
|
|
244
|
+
<label className="flex items-center gap-2">
|
|
245
|
+
<input
|
|
246
|
+
type="checkbox"
|
|
247
|
+
checked={report.showPrintButton || false}
|
|
248
|
+
onChange={(e) => setReport({ ...report, showPrintButton: e.target.checked })}
|
|
249
|
+
/>
|
|
250
|
+
<span className="text-sm">Show Print Button</span>
|
|
251
|
+
</label>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<div className="space-y-2">
|
|
256
|
+
<Label htmlFor="export-format">Default Export Format</Label>
|
|
257
|
+
<select
|
|
258
|
+
id="export-format"
|
|
259
|
+
className="w-full border rounded-md p-2"
|
|
260
|
+
value={report.defaultExportFormat || 'pdf'}
|
|
261
|
+
onChange={(e) => setReport({ ...report, defaultExportFormat: e.target.value as any })}
|
|
262
|
+
>
|
|
263
|
+
<option value="pdf">PDF</option>
|
|
264
|
+
<option value="excel">Excel</option>
|
|
265
|
+
<option value="csv">CSV</option>
|
|
266
|
+
<option value="json">JSON</option>
|
|
267
|
+
<option value="html">HTML</option>
|
|
268
|
+
</select>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
</TabsContent>
|
|
272
|
+
|
|
273
|
+
{/* Fields Tab */}
|
|
274
|
+
<TabsContent value="fields" className="space-y-4 mt-4">
|
|
275
|
+
<div className="flex items-center justify-between">
|
|
276
|
+
<Label>Report Fields</Label>
|
|
277
|
+
<Button
|
|
278
|
+
size="sm"
|
|
279
|
+
variant="outline"
|
|
280
|
+
onClick={handleAddField}
|
|
281
|
+
disabled={selectedFields.length >= availableFields.length}
|
|
282
|
+
>
|
|
283
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
284
|
+
Add Field
|
|
285
|
+
</Button>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<div className="space-y-2">
|
|
289
|
+
{selectedFields.map((field, index) => (
|
|
290
|
+
<div key={index} className="flex items-center gap-2 p-3 border rounded-lg">
|
|
291
|
+
<div className="flex-1 grid grid-cols-4 gap-2">
|
|
292
|
+
<div>
|
|
293
|
+
<Label className="text-xs">Field Name</Label>
|
|
294
|
+
<div className="text-sm font-medium">{field.name}</div>
|
|
295
|
+
</div>
|
|
296
|
+
<div>
|
|
297
|
+
<Label className="text-xs">Label</Label>
|
|
298
|
+
<Input
|
|
299
|
+
className="h-8 text-sm"
|
|
300
|
+
value={field.label || field.name}
|
|
301
|
+
onChange={(e) =>
|
|
302
|
+
handleFieldChange(index, { ...field, label: e.target.value })
|
|
303
|
+
}
|
|
304
|
+
/>
|
|
305
|
+
</div>
|
|
306
|
+
<div>
|
|
307
|
+
<Label className="text-xs">Aggregation</Label>
|
|
308
|
+
<select
|
|
309
|
+
className="w-full border rounded p-1 text-sm h-8"
|
|
310
|
+
value={field.aggregation || ''}
|
|
311
|
+
onChange={(e) =>
|
|
312
|
+
handleFieldChange(index, {
|
|
313
|
+
...field,
|
|
314
|
+
aggregation: e.target.value as any
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
>
|
|
318
|
+
<option value="">None</option>
|
|
319
|
+
<option value="sum">Sum</option>
|
|
320
|
+
<option value="avg">Average</option>
|
|
321
|
+
<option value="min">Min</option>
|
|
322
|
+
<option value="max">Max</option>
|
|
323
|
+
<option value="count">Count</option>
|
|
324
|
+
<option value="distinct">Distinct</option>
|
|
325
|
+
</select>
|
|
326
|
+
</div>
|
|
327
|
+
<div>
|
|
328
|
+
<Label className="text-xs">Show in Summary</Label>
|
|
329
|
+
<input
|
|
330
|
+
type="checkbox"
|
|
331
|
+
checked={field.showInSummary || false}
|
|
332
|
+
onChange={(e) =>
|
|
333
|
+
handleFieldChange(index, {
|
|
334
|
+
...field,
|
|
335
|
+
showInSummary: e.target.checked
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
className="mt-2"
|
|
339
|
+
/>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
<Button
|
|
343
|
+
size="sm"
|
|
344
|
+
variant="ghost"
|
|
345
|
+
onClick={() => handleRemoveField(index)}
|
|
346
|
+
>
|
|
347
|
+
<Trash2 className="h-4 w-4 text-destructive" />
|
|
348
|
+
</Button>
|
|
349
|
+
</div>
|
|
350
|
+
))}
|
|
351
|
+
|
|
352
|
+
{selectedFields.length === 0 && (
|
|
353
|
+
<div className="text-center py-8 text-muted-foreground border rounded-lg border-dashed">
|
|
354
|
+
No fields selected. Click "Add Field" to get started.
|
|
355
|
+
</div>
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
</TabsContent>
|
|
359
|
+
|
|
360
|
+
{/* Filters Tab */}
|
|
361
|
+
<TabsContent value="filters" className="space-y-4 mt-4">
|
|
362
|
+
<div className="flex items-center justify-between">
|
|
363
|
+
<Label>Report Filters</Label>
|
|
364
|
+
<Button
|
|
365
|
+
size="sm"
|
|
366
|
+
variant="outline"
|
|
367
|
+
onClick={handleAddFilter}
|
|
368
|
+
disabled={availableFields.length === 0}
|
|
369
|
+
>
|
|
370
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
371
|
+
Add Filter
|
|
372
|
+
</Button>
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
<div className="space-y-2">
|
|
376
|
+
{filters.map((filter, index) => (
|
|
377
|
+
<div key={index} className="flex items-center gap-2 p-3 border rounded-lg">
|
|
378
|
+
<div className="flex-1 grid grid-cols-3 gap-2">
|
|
379
|
+
<div>
|
|
380
|
+
<Label className="text-xs">Field</Label>
|
|
381
|
+
<select
|
|
382
|
+
className="w-full border rounded p-1 text-sm h-8"
|
|
383
|
+
value={filter.field}
|
|
384
|
+
onChange={(e) =>
|
|
385
|
+
handleFilterChange(index, { ...filter, field: e.target.value })
|
|
386
|
+
}
|
|
387
|
+
>
|
|
388
|
+
{availableFields.map((f) => (
|
|
389
|
+
<option key={f.name} value={f.name}>
|
|
390
|
+
{f.label || f.name}
|
|
391
|
+
</option>
|
|
392
|
+
))}
|
|
393
|
+
</select>
|
|
394
|
+
</div>
|
|
395
|
+
<div>
|
|
396
|
+
<Label className="text-xs">Operator</Label>
|
|
397
|
+
<select
|
|
398
|
+
className="w-full border rounded p-1 text-sm h-8"
|
|
399
|
+
value={filter.operator}
|
|
400
|
+
onChange={(e) =>
|
|
401
|
+
handleFilterChange(index, { ...filter, operator: e.target.value as any })
|
|
402
|
+
}
|
|
403
|
+
>
|
|
404
|
+
<option value="equals">Equals</option>
|
|
405
|
+
<option value="not_equals">Not Equals</option>
|
|
406
|
+
<option value="contains">Contains</option>
|
|
407
|
+
<option value="greater_than">Greater Than</option>
|
|
408
|
+
<option value="less_than">Less Than</option>
|
|
409
|
+
<option value="between">Between</option>
|
|
410
|
+
<option value="in">In</option>
|
|
411
|
+
<option value="not_in">Not In</option>
|
|
412
|
+
</select>
|
|
413
|
+
</div>
|
|
414
|
+
<div>
|
|
415
|
+
<Label className="text-xs">Value</Label>
|
|
416
|
+
<Input
|
|
417
|
+
className="h-8 text-sm"
|
|
418
|
+
value={filter.value || ''}
|
|
419
|
+
onChange={(e) =>
|
|
420
|
+
handleFilterChange(index, { ...filter, value: e.target.value })
|
|
421
|
+
}
|
|
422
|
+
placeholder="Enter value"
|
|
423
|
+
/>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
<Button
|
|
427
|
+
size="sm"
|
|
428
|
+
variant="ghost"
|
|
429
|
+
onClick={() => handleRemoveFilter(index)}
|
|
430
|
+
>
|
|
431
|
+
<Trash2 className="h-4 w-4 text-destructive" />
|
|
432
|
+
</Button>
|
|
433
|
+
</div>
|
|
434
|
+
))}
|
|
435
|
+
|
|
436
|
+
{filters.length === 0 && (
|
|
437
|
+
<div className="text-center py-8 text-muted-foreground border rounded-lg border-dashed">
|
|
438
|
+
No filters defined. Click "Add Filter" to filter your report data.
|
|
439
|
+
</div>
|
|
440
|
+
)}
|
|
441
|
+
</div>
|
|
442
|
+
</TabsContent>
|
|
443
|
+
|
|
444
|
+
{/* Group By Tab */}
|
|
445
|
+
<TabsContent value="grouping" className="space-y-4 mt-4">
|
|
446
|
+
<div className="flex items-center justify-between">
|
|
447
|
+
<Label>Group By Fields</Label>
|
|
448
|
+
<Button
|
|
449
|
+
size="sm"
|
|
450
|
+
variant="outline"
|
|
451
|
+
onClick={handleAddGroupBy}
|
|
452
|
+
disabled={availableFields.length === 0}
|
|
453
|
+
>
|
|
454
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
455
|
+
Add Group
|
|
456
|
+
</Button>
|
|
457
|
+
</div>
|
|
458
|
+
|
|
459
|
+
<div className="space-y-2">
|
|
460
|
+
{groupBy.map((group, index) => (
|
|
461
|
+
<div key={index} className="flex items-center gap-2 p-3 border rounded-lg">
|
|
462
|
+
<div className="flex-1 grid grid-cols-3 gap-2">
|
|
463
|
+
<div>
|
|
464
|
+
<Label className="text-xs">Field</Label>
|
|
465
|
+
<select
|
|
466
|
+
className="w-full border rounded p-1 text-sm h-8"
|
|
467
|
+
value={group.field}
|
|
468
|
+
onChange={(e) =>
|
|
469
|
+
handleGroupByChange(index, { ...group, field: e.target.value })
|
|
470
|
+
}
|
|
471
|
+
>
|
|
472
|
+
{availableFields.map((f) => (
|
|
473
|
+
<option key={f.name} value={f.name}>
|
|
474
|
+
{f.label || f.name}
|
|
475
|
+
</option>
|
|
476
|
+
))}
|
|
477
|
+
</select>
|
|
478
|
+
</div>
|
|
479
|
+
<div>
|
|
480
|
+
<Label className="text-xs">Label</Label>
|
|
481
|
+
<Input
|
|
482
|
+
className="h-8 text-sm"
|
|
483
|
+
value={group.label || ''}
|
|
484
|
+
onChange={(e) =>
|
|
485
|
+
handleGroupByChange(index, { ...group, label: e.target.value })
|
|
486
|
+
}
|
|
487
|
+
placeholder="Optional label"
|
|
488
|
+
/>
|
|
489
|
+
</div>
|
|
490
|
+
<div>
|
|
491
|
+
<Label className="text-xs">Sort</Label>
|
|
492
|
+
<select
|
|
493
|
+
className="w-full border rounded p-1 text-sm h-8"
|
|
494
|
+
value={group.sort || 'asc'}
|
|
495
|
+
onChange={(e) =>
|
|
496
|
+
handleGroupByChange(index, { ...group, sort: e.target.value as 'asc' | 'desc' })
|
|
497
|
+
}
|
|
498
|
+
>
|
|
499
|
+
<option value="asc">Ascending</option>
|
|
500
|
+
<option value="desc">Descending</option>
|
|
501
|
+
</select>
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
<Button
|
|
505
|
+
size="sm"
|
|
506
|
+
variant="ghost"
|
|
507
|
+
onClick={() => handleRemoveGroupBy(index)}
|
|
508
|
+
>
|
|
509
|
+
<Trash2 className="h-4 w-4 text-destructive" />
|
|
510
|
+
</Button>
|
|
511
|
+
</div>
|
|
512
|
+
))}
|
|
513
|
+
|
|
514
|
+
{groupBy.length === 0 && (
|
|
515
|
+
<div className="text-center py-8 text-muted-foreground border rounded-lg border-dashed">
|
|
516
|
+
No grouping defined. Click "Add Group" to group your report data.
|
|
517
|
+
</div>
|
|
518
|
+
)}
|
|
519
|
+
</div>
|
|
520
|
+
</TabsContent>
|
|
521
|
+
|
|
522
|
+
{/* Sections Tab */}
|
|
523
|
+
<TabsContent value="sections" className="space-y-4 mt-4">
|
|
524
|
+
<div className="flex items-center justify-between">
|
|
525
|
+
<Label>Report Sections</Label>
|
|
526
|
+
<div className="flex gap-2">
|
|
527
|
+
<Button size="sm" variant="outline" onClick={() => handleAddSection('header')}>
|
|
528
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
529
|
+
Header
|
|
530
|
+
</Button>
|
|
531
|
+
<Button size="sm" variant="outline" onClick={() => handleAddSection('summary')}>
|
|
532
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
533
|
+
Summary
|
|
534
|
+
</Button>
|
|
535
|
+
<Button size="sm" variant="outline" onClick={() => handleAddSection('chart')}>
|
|
536
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
537
|
+
Chart
|
|
538
|
+
</Button>
|
|
539
|
+
<Button size="sm" variant="outline" onClick={() => handleAddSection('table')}>
|
|
540
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
541
|
+
Table
|
|
542
|
+
</Button>
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
|
|
546
|
+
<div className="space-y-2">
|
|
547
|
+
{sections.map((section, index) => (
|
|
548
|
+
<div key={index} className="flex items-center gap-2 p-3 border rounded-lg">
|
|
549
|
+
<div className="flex-1 space-y-2">
|
|
550
|
+
<div className="flex items-center gap-2">
|
|
551
|
+
<span className="text-xs font-medium text-muted-foreground uppercase">
|
|
552
|
+
{section.type}
|
|
553
|
+
</span>
|
|
554
|
+
<Input
|
|
555
|
+
className="h-8 text-sm"
|
|
556
|
+
value={section.title || ''}
|
|
557
|
+
onChange={(e) =>
|
|
558
|
+
handleSectionChange(index, { ...section, title: e.target.value })
|
|
559
|
+
}
|
|
560
|
+
placeholder="Section title"
|
|
561
|
+
/>
|
|
562
|
+
</div>
|
|
563
|
+
{section.type === 'text' && (
|
|
564
|
+
<Input
|
|
565
|
+
className="h-8 text-sm"
|
|
566
|
+
value={section.text || ''}
|
|
567
|
+
onChange={(e) =>
|
|
568
|
+
handleSectionChange(index, { ...section, text: e.target.value })
|
|
569
|
+
}
|
|
570
|
+
placeholder="Text content"
|
|
571
|
+
/>
|
|
572
|
+
)}
|
|
573
|
+
</div>
|
|
574
|
+
<Button
|
|
575
|
+
size="sm"
|
|
576
|
+
variant="ghost"
|
|
577
|
+
onClick={() => handleRemoveSection(index)}
|
|
578
|
+
>
|
|
579
|
+
<Trash2 className="h-4 w-4 text-destructive" />
|
|
580
|
+
</Button>
|
|
581
|
+
</div>
|
|
582
|
+
))}
|
|
583
|
+
|
|
584
|
+
{sections.length === 0 && (
|
|
585
|
+
<div className="text-center py-8 text-muted-foreground border rounded-lg border-dashed">
|
|
586
|
+
No sections defined. Click section buttons above to add report sections.
|
|
587
|
+
</div>
|
|
588
|
+
)}
|
|
589
|
+
</div>
|
|
590
|
+
</TabsContent>
|
|
591
|
+
</Tabs>
|
|
592
|
+
|
|
593
|
+
{/* Actions */}
|
|
594
|
+
<div className="flex items-center justify-end gap-2 pt-4 mt-6 border-t">
|
|
595
|
+
<Button variant="outline" onClick={handleCancel}>
|
|
596
|
+
<X className="h-4 w-4 mr-2" />
|
|
597
|
+
Cancel
|
|
598
|
+
</Button>
|
|
599
|
+
<Button onClick={handleSave}>
|
|
600
|
+
<Save className="h-4 w-4 mr-2" />
|
|
601
|
+
Save Report
|
|
602
|
+
</Button>
|
|
603
|
+
</div>
|
|
604
|
+
</CardContent>
|
|
605
|
+
</Card>
|
|
606
|
+
|
|
607
|
+
{/* Preview */}
|
|
608
|
+
{showPreview && (
|
|
609
|
+
<div>
|
|
610
|
+
<h3 className="text-lg font-semibold mb-2">Preview</h3>
|
|
611
|
+
<ReportViewer
|
|
612
|
+
schema={{
|
|
613
|
+
type: 'report-viewer',
|
|
614
|
+
report,
|
|
615
|
+
data: [],
|
|
616
|
+
showToolbar: false,
|
|
617
|
+
allowExport: false,
|
|
618
|
+
allowPrint: false
|
|
619
|
+
}}
|
|
620
|
+
/>
|
|
621
|
+
</div>
|
|
622
|
+
)}
|
|
623
|
+
</div>
|
|
624
|
+
);
|
|
625
|
+
};
|