@pattern-stack/frontend-patterns 0.0.4 → 0.0.6
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/dist/atoms/composed/SalesPanel/SalesPanel.d.ts +19 -0
- package/dist/atoms/composed/SalesPanel/SalesPanel.d.ts.map +1 -0
- package/dist/atoms/composed/SalesPanel/index.d.ts +2 -0
- package/dist/atoms/composed/SalesPanel/index.d.ts.map +1 -0
- package/dist/atoms/composed/SalesPanel/mockSalesData.d.ts +63 -0
- package/dist/atoms/composed/SalesPanel/mockSalesData.d.ts.map +1 -0
- package/dist/atoms/composed/index.d.ts +1 -0
- package/dist/atoms/composed/index.d.ts.map +1 -1
- package/dist/atoms/types/entity-config.d.ts +117 -0
- package/dist/atoms/types/entity-config.d.ts.map +1 -0
- package/dist/atoms/types/index.d.ts +2 -0
- package/dist/atoms/types/index.d.ts.map +1 -1
- package/dist/atoms/types/navigation.d.ts +30 -0
- package/dist/atoms/types/navigation.d.ts.map +1 -0
- package/dist/atoms/ui/ErrorBoundary.d.ts +1 -1
- package/dist/atoms/ui/button.d.ts +1 -1
- package/dist/atoms/utils/icon-resolver.d.ts +72 -0
- package/dist/atoms/utils/icon-resolver.d.ts.map +1 -0
- package/dist/atoms/utils/metric-engine.d.ts +30 -0
- package/dist/atoms/utils/metric-engine.d.ts.map +1 -0
- package/dist/atoms/utils/utils.d.ts +2 -0
- package/dist/atoms/utils/utils.d.ts.map +1 -1
- package/dist/features/auth/components/ProtectedRoute.d.ts +1 -1
- package/dist/frontend-patterns.css +1 -1
- package/dist/index.es.js +402 -14
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +402 -14
- package/dist/index.js.map +1 -1
- package/dist/molecules/layout/DashboardWithSidePanel/DashboardWithSidePanel.d.ts +16 -0
- package/dist/molecules/layout/DashboardWithSidePanel/DashboardWithSidePanel.d.ts.map +1 -0
- package/dist/molecules/layout/DashboardWithSidePanel/index.d.ts +2 -0
- package/dist/molecules/layout/DashboardWithSidePanel/index.d.ts.map +1 -0
- package/dist/molecules/layout/NavigationContext.d.ts +15 -0
- package/dist/molecules/layout/NavigationContext.d.ts.map +1 -0
- package/dist/molecules/layout/Sidebar.d.ts.map +1 -1
- package/dist/molecules/layout/SidebarButton/SidebarButton.d.ts +2 -0
- package/dist/molecules/layout/SidebarButton/SidebarButton.d.ts.map +1 -1
- package/dist/molecules/layout/index.d.ts +3 -0
- package/dist/molecules/layout/index.d.ts.map +1 -1
- package/dist/templates/factory.d.ts +2 -1
- package/dist/templates/factory.d.ts.map +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/package.json +7 -3
- package/src/App.tsx +11 -1
- package/src/__tests__/atoms/composed/databadge.test.tsx +106 -0
- package/src/__tests__/atoms/composed/statcard.test.tsx +133 -0
- package/src/__tests__/atoms/utils/icon-resolver.test.tsx +140 -0
- package/src/atoms/composed/SalesPanel/SalesPanel.tsx +116 -0
- package/src/atoms/composed/SalesPanel/index.ts +1 -0
- package/src/atoms/composed/SalesPanel/mockSalesData.ts +151 -0
- package/src/atoms/composed/index.ts +1 -0
- package/src/atoms/types/entity-config.ts +127 -0
- package/src/atoms/types/index.ts +3 -1
- package/src/atoms/types/navigation.ts +43 -0
- package/src/atoms/utils/icon-resolver.tsx +54 -0
- package/src/atoms/utils/metric-engine.ts +236 -0
- package/src/atoms/utils/utils.ts +4 -2
- package/src/molecules/layout/DashboardWithSidePanel/DashboardWithSidePanel.tsx +42 -0
- package/src/molecules/layout/DashboardWithSidePanel/index.ts +1 -0
- package/src/molecules/layout/NavigationContext.tsx +63 -0
- package/src/molecules/layout/Sidebar.tsx +10 -23
- package/src/molecules/layout/SidebarButton/SidebarButton.tsx +32 -10
- package/src/molecules/layout/index.ts +4 -1
- package/src/organisms/entity/CategoryBreakdownPanel.tsx +427 -0
- package/src/organisms/entity/EntityListPanel.tsx +339 -0
- package/src/organisms/entity/MetricsOverviewPanel.tsx +236 -0
- package/src/organisms/entity/TrendAnalysisPanel.tsx +337 -0
- package/src/organisms/entity/index.ts +4 -0
- package/src/organisms/index.ts +5 -1
- package/src/pages/AdminShowcase/AdminDashboardShowcase.tsx +77 -75
- package/src/pages/AdminShowcase/SalesPerformanceDashboard.tsx +158 -0
- package/src/pages/AdminShowcase/index.tsx +2 -1
- package/src/pages/EntityShowcase/EntityManagementShowcase.tsx +137 -0
- package/src/pages/EntityShowcase/EntityPerformanceShowcase.tsx +117 -0
- package/src/pages/EntityShowcase/index.ts +2 -0
- package/src/pages/EntityTemplateExample.tsx +229 -0
- package/src/pages/TestEntityTemplate.tsx +40 -0
- package/src/pages/index.ts +2 -1
- package/src/templates/entity/EntityManagementTemplate.tsx +430 -0
- package/src/templates/entity/EntityPerformanceDashboardTemplate.tsx +277 -0
- package/src/templates/entity/configs/financial-config.ts +141 -0
- package/src/templates/entity/configs/index.ts +1 -0
- package/src/templates/entity/index.ts +3 -0
- package/src/templates/factory.tsx +14 -7
- package/src/templates/financial/FinancialDashboardTemplate.tsx +326 -0
- package/src/templates/index.ts +4 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
|
+
import { Chart } from '../../atoms/composed/Chart';
|
|
3
|
+
import type { ChartDataPoint } from '../../atoms/composed/Chart';
|
|
4
|
+
import { Card } from '../../atoms/ui/card';
|
|
5
|
+
import { Button } from '../../atoms/ui/button';
|
|
6
|
+
import { Badge } from '../../atoms/ui/Badge';
|
|
7
|
+
import { DataBadge } from '../../atoms/composed/DataBadge';
|
|
8
|
+
import { MetricCalculationEngine } from '../../atoms/utils/metric-engine';
|
|
9
|
+
import type { CategoryConfig, EntityData, CategoryBreakdown } from '../../atoms/types';
|
|
10
|
+
import { cn } from '../../atoms/utils/utils';
|
|
11
|
+
import {
|
|
12
|
+
ChevronRight,
|
|
13
|
+
ChevronDown,
|
|
14
|
+
PieChart,
|
|
15
|
+
BarChart3,
|
|
16
|
+
List,
|
|
17
|
+
Download,
|
|
18
|
+
Filter
|
|
19
|
+
} from 'lucide-react';
|
|
20
|
+
|
|
21
|
+
export interface CategoryBreakdownPanelProps {
|
|
22
|
+
data: EntityData[];
|
|
23
|
+
categoryConfig: CategoryConfig;
|
|
24
|
+
valueField: string;
|
|
25
|
+
isLoading?: boolean;
|
|
26
|
+
className?: string;
|
|
27
|
+
category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
|
28
|
+
|
|
29
|
+
// Display configuration
|
|
30
|
+
title?: string;
|
|
31
|
+
subtitle?: string;
|
|
32
|
+
defaultView?: 'chart' | 'list' | 'both';
|
|
33
|
+
defaultChartType?: 'pie' | 'bar';
|
|
34
|
+
maxCategories?: number;
|
|
35
|
+
showPercentages?: boolean;
|
|
36
|
+
enableDrillDown?: boolean;
|
|
37
|
+
|
|
38
|
+
// Extension points
|
|
39
|
+
renderHeader?: () => React.ReactNode;
|
|
40
|
+
renderFooter?: () => React.ReactNode;
|
|
41
|
+
renderCustomCategory?: (category: CategoryBreakdown, index: number) => React.ReactNode;
|
|
42
|
+
renderDrillDownContent?: (category: string, subcategories: CategoryBreakdown[]) => React.ReactNode;
|
|
43
|
+
headerSlot?: React.ReactNode;
|
|
44
|
+
footerSlot?: React.ReactNode;
|
|
45
|
+
|
|
46
|
+
// Event handlers
|
|
47
|
+
onCategoryClick?: (category: CategoryBreakdown) => void;
|
|
48
|
+
onDrillDown?: (category: string, level: number) => void;
|
|
49
|
+
onExport?: (data: CategoryBreakdown[]) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const CategoryBreakdownPanel: React.FC<CategoryBreakdownPanelProps> = ({
|
|
53
|
+
data,
|
|
54
|
+
categoryConfig,
|
|
55
|
+
valueField,
|
|
56
|
+
isLoading = false,
|
|
57
|
+
className,
|
|
58
|
+
category,
|
|
59
|
+
title = 'Category Breakdown',
|
|
60
|
+
subtitle,
|
|
61
|
+
defaultView = 'both',
|
|
62
|
+
defaultChartType = 'pie',
|
|
63
|
+
maxCategories = 8,
|
|
64
|
+
showPercentages = true,
|
|
65
|
+
enableDrillDown = true,
|
|
66
|
+
renderHeader,
|
|
67
|
+
renderFooter,
|
|
68
|
+
renderCustomCategory,
|
|
69
|
+
renderDrillDownContent,
|
|
70
|
+
headerSlot,
|
|
71
|
+
footerSlot,
|
|
72
|
+
onCategoryClick,
|
|
73
|
+
onDrillDown,
|
|
74
|
+
onExport
|
|
75
|
+
}) => {
|
|
76
|
+
const [currentView, setCurrentView] = useState<'chart' | 'list' | 'both'>(defaultView);
|
|
77
|
+
const [chartType, setChartType] = useState<'pie' | 'bar'>(defaultChartType);
|
|
78
|
+
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set());
|
|
79
|
+
const [drillDownLevel, setDrillDownLevel] = useState(0);
|
|
80
|
+
const [drillDownPath, setDrillDownPath] = useState<string[]>([]);
|
|
81
|
+
|
|
82
|
+
// Calculate category breakdown
|
|
83
|
+
const categoryBreakdown = useMemo(() => {
|
|
84
|
+
if (!data.length) return [];
|
|
85
|
+
|
|
86
|
+
const currentCategoryField = categoryConfig?.hierarchy?.[drillDownLevel] || categoryConfig?.defaultGroupBy || 'category';
|
|
87
|
+
|
|
88
|
+
return MetricCalculationEngine.calculateCategoryBreakdown(
|
|
89
|
+
data,
|
|
90
|
+
currentCategoryField,
|
|
91
|
+
valueField,
|
|
92
|
+
maxCategories
|
|
93
|
+
);
|
|
94
|
+
}, [data, categoryConfig, valueField, maxCategories, drillDownLevel]);
|
|
95
|
+
|
|
96
|
+
// Convert to chart data
|
|
97
|
+
const chartData = useMemo((): ChartDataPoint[] => {
|
|
98
|
+
return categoryBreakdown.map((item, index) => ({
|
|
99
|
+
label: item.category,
|
|
100
|
+
value: item.value,
|
|
101
|
+
category: ((index % 8) + 1) as 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
|
|
102
|
+
}));
|
|
103
|
+
}, [categoryBreakdown]);
|
|
104
|
+
|
|
105
|
+
// Calculate total value
|
|
106
|
+
const totalValue = useMemo(() => {
|
|
107
|
+
return categoryBreakdown.reduce((sum, item) => sum + item.value, 0);
|
|
108
|
+
}, [categoryBreakdown]);
|
|
109
|
+
|
|
110
|
+
const handleCategoryClick = (categoryItem: CategoryBreakdown) => {
|
|
111
|
+
onCategoryClick?.(categoryItem);
|
|
112
|
+
|
|
113
|
+
if (enableDrillDown && categoryConfig?.enableDrillDown && drillDownLevel < (categoryConfig?.hierarchy?.length || 0) - 1) {
|
|
114
|
+
setDrillDownLevel(prev => prev + 1);
|
|
115
|
+
setDrillDownPath(prev => [...prev, categoryItem.category]);
|
|
116
|
+
onDrillDown?.(categoryItem.category, drillDownLevel + 1);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const handleDrillUp = (level: number) => {
|
|
121
|
+
if (level < drillDownLevel) {
|
|
122
|
+
setDrillDownLevel(level);
|
|
123
|
+
setDrillDownPath(prev => prev.slice(0, level));
|
|
124
|
+
onDrillDown?.(level > 0 ? drillDownPath[level - 1] : '', level);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const toggleCategoryExpansion = (categoryName: string) => {
|
|
129
|
+
setExpandedCategories(prev => {
|
|
130
|
+
const newSet = new Set(prev);
|
|
131
|
+
if (newSet.has(categoryName)) {
|
|
132
|
+
newSet.delete(categoryName);
|
|
133
|
+
} else {
|
|
134
|
+
newSet.add(categoryName);
|
|
135
|
+
}
|
|
136
|
+
return newSet;
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const handleExport = () => {
|
|
141
|
+
if (onExport) {
|
|
142
|
+
onExport(categoryBreakdown);
|
|
143
|
+
} else {
|
|
144
|
+
// Default CSV export
|
|
145
|
+
const csvContent = [
|
|
146
|
+
'Category,Value,Percentage',
|
|
147
|
+
...categoryBreakdown.map(item =>
|
|
148
|
+
`${item.category},${item.value},${item.percentage.toFixed(2)}%`
|
|
149
|
+
)
|
|
150
|
+
].join('\n');
|
|
151
|
+
|
|
152
|
+
const blob = new Blob([csvContent], { type: 'text/csv' });
|
|
153
|
+
const url = URL.createObjectURL(blob);
|
|
154
|
+
const a = document.createElement('a');
|
|
155
|
+
a.href = url;
|
|
156
|
+
a.download = `category-breakdown-${Date.now()}.csv`;
|
|
157
|
+
a.click();
|
|
158
|
+
URL.revokeObjectURL(url);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const renderControls = () => {
|
|
163
|
+
return (
|
|
164
|
+
<div className="flex items-center justify-between mb-4">
|
|
165
|
+
<div className="flex items-center gap-4">
|
|
166
|
+
<div className="flex items-center gap-2">
|
|
167
|
+
<PieChart className="w-4 h-4 text-muted-foreground" />
|
|
168
|
+
<h3 className="text-lg font-semibold text-foreground">{title}</h3>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
{subtitle && (
|
|
172
|
+
<Badge variant="outline" className="text-xs">
|
|
173
|
+
{subtitle}
|
|
174
|
+
</Badge>
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
{totalValue > 0 && (
|
|
178
|
+
<Badge variant="outline">
|
|
179
|
+
Total: {new Intl.NumberFormat().format(totalValue)}
|
|
180
|
+
</Badge>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div className="flex items-center gap-2">
|
|
185
|
+
{/* View toggle */}
|
|
186
|
+
<div className="flex items-center border rounded">
|
|
187
|
+
<Button
|
|
188
|
+
variant={currentView === 'chart' ? 'default' : 'ghost'}
|
|
189
|
+
size="sm"
|
|
190
|
+
onClick={() => setCurrentView('chart')}
|
|
191
|
+
>
|
|
192
|
+
<PieChart className="w-4 h-4" />
|
|
193
|
+
</Button>
|
|
194
|
+
<Button
|
|
195
|
+
variant={currentView === 'list' ? 'default' : 'ghost'}
|
|
196
|
+
size="sm"
|
|
197
|
+
onClick={() => setCurrentView('list')}
|
|
198
|
+
>
|
|
199
|
+
<List className="w-4 h-4" />
|
|
200
|
+
</Button>
|
|
201
|
+
<Button
|
|
202
|
+
variant={currentView === 'both' ? 'default' : 'ghost'}
|
|
203
|
+
size="sm"
|
|
204
|
+
onClick={() => setCurrentView('both')}
|
|
205
|
+
>
|
|
206
|
+
Both
|
|
207
|
+
</Button>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
{/* Chart type toggle */}
|
|
211
|
+
{(currentView === 'chart' || currentView === 'both') && (
|
|
212
|
+
<div className="flex items-center border rounded">
|
|
213
|
+
<Button
|
|
214
|
+
variant={chartType === 'pie' ? 'default' : 'ghost'}
|
|
215
|
+
size="sm"
|
|
216
|
+
onClick={() => setChartType('pie')}
|
|
217
|
+
>
|
|
218
|
+
<PieChart className="w-4 h-4" />
|
|
219
|
+
</Button>
|
|
220
|
+
<Button
|
|
221
|
+
variant={chartType === 'bar' ? 'default' : 'ghost'}
|
|
222
|
+
size="sm"
|
|
223
|
+
onClick={() => setChartType('bar')}
|
|
224
|
+
>
|
|
225
|
+
<BarChart3 className="w-4 h-4" />
|
|
226
|
+
</Button>
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
<Button
|
|
231
|
+
variant="outline"
|
|
232
|
+
size="sm"
|
|
233
|
+
onClick={handleExport}
|
|
234
|
+
disabled={categoryBreakdown.length === 0}
|
|
235
|
+
>
|
|
236
|
+
<Download className="w-4 h-4" />
|
|
237
|
+
</Button>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const renderBreadcrumb = () => {
|
|
244
|
+
if (drillDownLevel === 0) return null;
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<div className="flex items-center gap-2 mb-4 text-sm text-muted-foreground">
|
|
248
|
+
<Button
|
|
249
|
+
variant="ghost"
|
|
250
|
+
size="sm"
|
|
251
|
+
onClick={() => handleDrillUp(0)}
|
|
252
|
+
className="p-0 h-auto font-normal"
|
|
253
|
+
>
|
|
254
|
+
All
|
|
255
|
+
</Button>
|
|
256
|
+
{drillDownPath.map((segment, index) => (
|
|
257
|
+
<React.Fragment key={index}>
|
|
258
|
+
<ChevronRight className="w-3 h-3" />
|
|
259
|
+
<Button
|
|
260
|
+
variant="ghost"
|
|
261
|
+
size="sm"
|
|
262
|
+
onClick={() => handleDrillUp(index + 1)}
|
|
263
|
+
className="p-0 h-auto font-normal"
|
|
264
|
+
>
|
|
265
|
+
{segment}
|
|
266
|
+
</Button>
|
|
267
|
+
</React.Fragment>
|
|
268
|
+
))}
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const renderChart = () => {
|
|
274
|
+
if (currentView === 'list') return null;
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<div className={cn(currentView === 'both' && 'mb-6')}>
|
|
278
|
+
<Chart
|
|
279
|
+
title=""
|
|
280
|
+
data={chartData}
|
|
281
|
+
type={chartType}
|
|
282
|
+
category={category}
|
|
283
|
+
showLegend={true}
|
|
284
|
+
height="medium"
|
|
285
|
+
isLoading={isLoading}
|
|
286
|
+
noWrapper={true}
|
|
287
|
+
onClick={() => {}}
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const renderCategoryList = () => {
|
|
294
|
+
if (currentView === 'chart') return null;
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<div className="space-y-2">
|
|
298
|
+
{categoryBreakdown.map((item, index) => {
|
|
299
|
+
if (renderCustomCategory) {
|
|
300
|
+
const customContent = renderCustomCategory(item, index);
|
|
301
|
+
if (customContent) return customContent;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const canExpand = enableDrillDown && categoryConfig.enableDrillDown && 'subcategories' in item && item.subcategories && Array.isArray(item.subcategories) && item.subcategories.length > 0;
|
|
305
|
+
const isExpanded = expandedCategories.has(item.category);
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<div key={item.category} className="border rounded-lg">
|
|
309
|
+
<div
|
|
310
|
+
className={cn(
|
|
311
|
+
"flex items-center justify-between p-3 cursor-pointer hover:bg-muted/50",
|
|
312
|
+
canExpand && "cursor-pointer"
|
|
313
|
+
)}
|
|
314
|
+
onClick={() => handleCategoryClick(item)}
|
|
315
|
+
>
|
|
316
|
+
<div className="flex items-center gap-3">
|
|
317
|
+
{canExpand && (
|
|
318
|
+
<Button
|
|
319
|
+
variant="ghost"
|
|
320
|
+
size="sm"
|
|
321
|
+
className="p-0 w-4 h-4"
|
|
322
|
+
onClick={(e) => {
|
|
323
|
+
e.stopPropagation();
|
|
324
|
+
toggleCategoryExpansion(item.category);
|
|
325
|
+
}}
|
|
326
|
+
>
|
|
327
|
+
{isExpanded ? (
|
|
328
|
+
<ChevronDown className="w-3 h-3" />
|
|
329
|
+
) : (
|
|
330
|
+
<ChevronRight className="w-3 h-3" />
|
|
331
|
+
)}
|
|
332
|
+
</Button>
|
|
333
|
+
)}
|
|
334
|
+
|
|
335
|
+
<DataBadge
|
|
336
|
+
variant="category"
|
|
337
|
+
category={((index % 8) + 1) as 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8}
|
|
338
|
+
size="sm"
|
|
339
|
+
>
|
|
340
|
+
{item.category}
|
|
341
|
+
</DataBadge>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
<div className="flex items-center gap-3">
|
|
345
|
+
<div className="text-right">
|
|
346
|
+
<div className="font-semibold">
|
|
347
|
+
{new Intl.NumberFormat().format(item.value)}
|
|
348
|
+
</div>
|
|
349
|
+
{showPercentages && (
|
|
350
|
+
<div className="text-xs text-muted-foreground">
|
|
351
|
+
{item.percentage.toFixed(1)}%
|
|
352
|
+
</div>
|
|
353
|
+
)}
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
{/* Progress bar */}
|
|
357
|
+
<div className="w-20 h-2 bg-muted rounded-full overflow-hidden">
|
|
358
|
+
<div
|
|
359
|
+
className={cn(
|
|
360
|
+
"h-full transition-all duration-300",
|
|
361
|
+
`bg-category-${((index % 8) + 1)}`
|
|
362
|
+
)}
|
|
363
|
+
style={{ width: `${item.percentage}%` }}
|
|
364
|
+
/>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
{/* Subcategories */}
|
|
370
|
+
{canExpand && isExpanded && 'subcategories' in item && item.subcategories && (
|
|
371
|
+
<div className="border-t bg-muted/20">
|
|
372
|
+
{renderDrillDownContent ? (
|
|
373
|
+
renderDrillDownContent(item.category, item.subcategories)
|
|
374
|
+
) : (
|
|
375
|
+
<div className="p-3 space-y-1">
|
|
376
|
+
{Array.isArray(item.subcategories) && item.subcategories.map((subItem: CategoryBreakdown) => (
|
|
377
|
+
<div key={subItem.category} className="flex items-center justify-between text-sm pl-4">
|
|
378
|
+
<span className="text-muted-foreground">{subItem.category}</span>
|
|
379
|
+
<span className="font-medium">{new Intl.NumberFormat().format(subItem.value)}</span>
|
|
380
|
+
</div>
|
|
381
|
+
))}
|
|
382
|
+
</div>
|
|
383
|
+
)}
|
|
384
|
+
</div>
|
|
385
|
+
)}
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
})}
|
|
389
|
+
</div>
|
|
390
|
+
);
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const renderEmptyState = () => {
|
|
394
|
+
if (categoryBreakdown.length > 0) return null;
|
|
395
|
+
|
|
396
|
+
return (
|
|
397
|
+
<div className="text-center py-12">
|
|
398
|
+
<Filter className="w-8 h-8 text-muted-foreground mx-auto mb-4" />
|
|
399
|
+
<div className="text-muted-foreground">
|
|
400
|
+
No category data available
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
);
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
return (
|
|
407
|
+
<Card className={cn('p-6', className)} category={category} data-component-name="CategoryBreakdownPanel">
|
|
408
|
+
{headerSlot}
|
|
409
|
+
{renderHeader && renderHeader()}
|
|
410
|
+
|
|
411
|
+
{renderControls()}
|
|
412
|
+
{renderBreadcrumb()}
|
|
413
|
+
|
|
414
|
+
{categoryBreakdown.length === 0 ? (
|
|
415
|
+
renderEmptyState()
|
|
416
|
+
) : (
|
|
417
|
+
<>
|
|
418
|
+
{renderChart()}
|
|
419
|
+
{renderCategoryList()}
|
|
420
|
+
</>
|
|
421
|
+
)}
|
|
422
|
+
|
|
423
|
+
{renderFooter && renderFooter()}
|
|
424
|
+
{footerSlot}
|
|
425
|
+
</Card>
|
|
426
|
+
);
|
|
427
|
+
};
|