@pfm-platform/expenses-ui-mui 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,3042 @@
1
+ 'use strict';
2
+
3
+ var material = require('@mui/material');
4
+ var expensesFeature = require('@pfm-platform/expenses-feature');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var React3 = require('react');
7
+ var LocalOfferIcon = require('@mui/icons-material/LocalOffer');
8
+ var PieChart = require('@mui/x-charts/PieChart');
9
+ var framerMotion = require('framer-motion');
10
+ var shared = require('@pfm-platform/shared');
11
+ var LineChart = require('@mui/x-charts/LineChart');
12
+ var BarChart = require('@mui/x-charts/BarChart');
13
+ var expensesDataAccess = require('@pfm-platform/expenses-data-access');
14
+ var DeleteIcon = require('@mui/icons-material/Delete');
15
+ var EditIcon = require('@mui/icons-material/Edit');
16
+ var SettingsIcon = require('@mui/icons-material/Settings');
17
+ var CloseIcon = require('@mui/icons-material/Close');
18
+ var RestartAltIcon = require('@mui/icons-material/RestartAlt');
19
+ var DonutLargeIcon = require('@mui/icons-material/DonutLarge');
20
+ var iconsMaterial = require('@mui/icons-material');
21
+
22
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
23
+
24
+ var React3__default = /*#__PURE__*/_interopDefault(React3);
25
+ var LocalOfferIcon__default = /*#__PURE__*/_interopDefault(LocalOfferIcon);
26
+ var DeleteIcon__default = /*#__PURE__*/_interopDefault(DeleteIcon);
27
+ var EditIcon__default = /*#__PURE__*/_interopDefault(EditIcon);
28
+ var SettingsIcon__default = /*#__PURE__*/_interopDefault(SettingsIcon);
29
+ var CloseIcon__default = /*#__PURE__*/_interopDefault(CloseIcon);
30
+ var RestartAltIcon__default = /*#__PURE__*/_interopDefault(RestartAltIcon);
31
+ var DonutLargeIcon__default = /*#__PURE__*/_interopDefault(DonutLargeIcon);
32
+
33
+ // src/components/ExpenseSummary.tsx
34
+ function ExpenseSummary({
35
+ userId,
36
+ filters,
37
+ title = "Expense Summary",
38
+ currencySymbol = "$"
39
+ }) {
40
+ const summary = expensesFeature.useExpenseSummary({ userId, filters });
41
+ return /* @__PURE__ */ jsxRuntime.jsx(material.Card, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.CardContent, { children: [
42
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", gutterBottom: true, children: title }),
43
+ !summary.hasExpenses ? /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "No expenses found" }) : /* @__PURE__ */ jsxRuntime.jsxs(material.Grid, { container: true, spacing: 2, children: [
44
+ /* @__PURE__ */ jsxRuntime.jsx(material.Grid, { size: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { children: [
45
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "Total" }),
46
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "h5", fontWeight: "medium", children: [
47
+ currencySymbol,
48
+ summary.totalAmount.toFixed(2)
49
+ ] })
50
+ ] }) }),
51
+ /* @__PURE__ */ jsxRuntime.jsx(material.Grid, { size: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { children: [
52
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "Categories" }),
53
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h5", fontWeight: "medium", children: summary.count })
54
+ ] }) }),
55
+ /* @__PURE__ */ jsxRuntime.jsx(material.Grid, { size: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { children: [
56
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "Average" }),
57
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "h5", fontWeight: "medium", children: [
58
+ currencySymbol,
59
+ summary.averageAmount.toFixed(2)
60
+ ] })
61
+ ] }) })
62
+ ] })
63
+ ] }) });
64
+ }
65
+ function ExpenseChart({
66
+ userId,
67
+ filters,
68
+ sortBy = "amount",
69
+ sortOrder = "desc",
70
+ title = "Expenses by Category",
71
+ currencySymbol = "$",
72
+ showPercentage = true
73
+ }) {
74
+ const categories = expensesFeature.useExpensesByCategory({
75
+ userId,
76
+ filters,
77
+ sortBy,
78
+ sortOrder
79
+ });
80
+ return /* @__PURE__ */ jsxRuntime.jsx(material.Card, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.CardContent, { children: [
81
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", gutterBottom: true, children: title }),
82
+ categories.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "No expenses found" }) : /* @__PURE__ */ jsxRuntime.jsx(material.Box, { sx: { display: "flex", flexDirection: "column", gap: 2 }, children: categories.map((category) => /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { children: [
83
+ /* @__PURE__ */ jsxRuntime.jsxs(
84
+ material.Box,
85
+ {
86
+ sx: {
87
+ display: "flex",
88
+ justifyContent: "space-between",
89
+ mb: 0.5
90
+ },
91
+ children: [
92
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", fontWeight: "medium", children: category.tag }),
93
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "body2", color: "text.secondary", children: [
94
+ currencySymbol,
95
+ category.amount.toFixed(2),
96
+ showPercentage && ` (${category.percentage.toFixed(1)}%)`
97
+ ] })
98
+ ]
99
+ }
100
+ ),
101
+ /* @__PURE__ */ jsxRuntime.jsx(
102
+ material.LinearProgress,
103
+ {
104
+ variant: "determinate",
105
+ value: category.percentage,
106
+ sx: { height: 8, borderRadius: 1 }
107
+ }
108
+ )
109
+ ] }, category.tag)) })
110
+ ] }) });
111
+ }
112
+ function ExpenseDonutChart({
113
+ userId,
114
+ filters,
115
+ sortBy = "amount",
116
+ sortOrder = "desc",
117
+ size,
118
+ showCenterText = true,
119
+ title,
120
+ selected,
121
+ onCategoryClick,
122
+ maxCategories = 7,
123
+ animated = true
124
+ }) {
125
+ const theme = material.useTheme();
126
+ const categories = expensesFeature.useExpensesByCategory({
127
+ userId,
128
+ filters,
129
+ sortBy,
130
+ sortOrder
131
+ });
132
+ const processedCategories = React3__default.default.useMemo(() => {
133
+ if (categories.length <= maxCategories) {
134
+ return categories;
135
+ }
136
+ const topCategories = categories.slice(0, maxCategories - 1);
137
+ const otherCategories = categories.slice(maxCategories - 1);
138
+ const otherTotal = otherCategories.reduce((sum, cat) => sum + cat.amount, 0);
139
+ const otherPercentage = otherCategories.reduce((sum, cat) => sum + cat.percentage, 0);
140
+ return [
141
+ ...topCategories,
142
+ {
143
+ tag: "Other",
144
+ amount: otherTotal,
145
+ percentage: otherPercentage
146
+ }
147
+ ];
148
+ }, [categories, maxCategories]);
149
+ const total = processedCategories.reduce((sum, cat) => sum + cat.amount, 0);
150
+ const chartData = processedCategories.map((category) => ({
151
+ id: category.tag,
152
+ label: category.tag,
153
+ value: category.amount
154
+ }));
155
+ const colors = shared.getChartColors(theme);
156
+ const chartSize = size || shared.CHART_DIMENSIONS.donut.width;
157
+ const { innerRadius, outerRadius } = shared.CHART_DIMENSIONS.donut;
158
+ const rotation = React3__default.default.useMemo(() => {
159
+ if (!selected) return 0;
160
+ const selectedIndex = chartData.findIndex((item) => item.id === selected);
161
+ if (selectedIndex === -1) return 0;
162
+ let cumulativeAngle = 0;
163
+ for (let i = 0; i < selectedIndex; i++) {
164
+ cumulativeAngle += chartData[i].value / total * 360;
165
+ }
166
+ const selectedSliceAngle = chartData[selectedIndex].value / total * 360;
167
+ const selectedCenterAngle = cumulativeAngle + selectedSliceAngle / 2;
168
+ return -selectedCenterAngle;
169
+ }, [selected, chartData, total]);
170
+ const handleCategoryClick = React3__default.default.useCallback(
171
+ (_event, itemIdentifier) => {
172
+ const categoryTag = chartData[itemIdentifier.dataIndex]?.id;
173
+ if (categoryTag && onCategoryClick) {
174
+ onCategoryClick(categoryTag);
175
+ }
176
+ },
177
+ [chartData, onCategoryClick]
178
+ );
179
+ if (categories.length === 0) {
180
+ return /* @__PURE__ */ jsxRuntime.jsxs(
181
+ material.Box,
182
+ {
183
+ sx: {
184
+ width: chartSize,
185
+ height: chartSize,
186
+ display: "flex",
187
+ flexDirection: "column",
188
+ alignItems: "center",
189
+ justifyContent: "center",
190
+ gap: 1
191
+ },
192
+ children: [
193
+ title && /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", color: "text.secondary", children: title }),
194
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "No expense data" })
195
+ ]
196
+ }
197
+ );
198
+ }
199
+ return /* @__PURE__ */ jsxRuntime.jsxs(
200
+ material.Box,
201
+ {
202
+ "data-testid": "expense-donut-chart",
203
+ sx: {
204
+ position: "relative",
205
+ width: chartSize,
206
+ display: "flex",
207
+ flexDirection: "column",
208
+ alignItems: "center"
209
+ },
210
+ children: [
211
+ title && /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", gutterBottom: true, sx: { mb: 2 }, children: title }),
212
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { position: "relative", width: chartSize, height: chartSize }, children: [
213
+ /* @__PURE__ */ jsxRuntime.jsx(
214
+ material.Box,
215
+ {
216
+ component: framerMotion.motion.div,
217
+ animate: { rotate: rotation },
218
+ transition: {
219
+ duration: animated ? 1 : 0,
220
+ // 1000ms to match D3 timing
221
+ ease: [0.4, 0, 0.2, 1]
222
+ // D3 easeInOut
223
+ },
224
+ sx: {
225
+ width: "100%",
226
+ height: "100%",
227
+ display: "flex",
228
+ justifyContent: "center",
229
+ alignItems: "center"
230
+ },
231
+ children: /* @__PURE__ */ jsxRuntime.jsx(
232
+ PieChart.PieChart,
233
+ {
234
+ series: [
235
+ {
236
+ data: chartData,
237
+ innerRadius,
238
+ outerRadius,
239
+ paddingAngle: 0,
240
+ cornerRadius: 0,
241
+ // Maintain sort=null behavior from D3 (prevent auto-reordering)
242
+ sortingValues: "none",
243
+ highlightScope: { fade: "global", highlight: "item" },
244
+ // Highlight selected category
245
+ ...selected && {
246
+ highlighted: { additionalRadius: 5 }
247
+ }
248
+ }
249
+ ],
250
+ colors,
251
+ width: chartSize,
252
+ height: chartSize,
253
+ onItemClick: onCategoryClick ? handleCategoryClick : void 0,
254
+ slotProps: {
255
+ legend: {}
256
+ // Legend configuration
257
+ },
258
+ sx: {
259
+ // Ensure proper centering
260
+ "& .MuiChartsLegend-root": {
261
+ display: "none"
262
+ },
263
+ // Make slices clickable if handler provided
264
+ ...onCategoryClick && {
265
+ "& .MuiPieArc-root": {
266
+ cursor: "pointer"
267
+ // Remove transition from here since we're rotating the container
268
+ },
269
+ "& .MuiPieArc-root:hover": {
270
+ filter: "brightness(1.1)"
271
+ }
272
+ }
273
+ }
274
+ }
275
+ )
276
+ }
277
+ ),
278
+ showCenterText && /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { mode: "wait", children: /* @__PURE__ */ jsxRuntime.jsx(
279
+ material.Box,
280
+ {
281
+ component: framerMotion.motion.div,
282
+ variants: animated ? shared.textVariants : void 0,
283
+ initial: animated ? "initial" : void 0,
284
+ animate: animated ? "enter" : void 0,
285
+ exit: animated ? "exit" : void 0,
286
+ sx: {
287
+ position: "absolute",
288
+ top: "50%",
289
+ left: "50%",
290
+ transform: "translate(-50%, -50%)",
291
+ textAlign: "center",
292
+ pointerEvents: "none"
293
+ },
294
+ children: selected ? (
295
+ // Show selected category: icon, name, and amount
296
+ // Using absolute positioning like legacy D3 implementation (Layout2.js:137-179)
297
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
298
+ /* @__PURE__ */ jsxRuntime.jsx(
299
+ LocalOfferIcon__default.default,
300
+ {
301
+ sx: {
302
+ position: "absolute",
303
+ top: "50%",
304
+ left: "50%",
305
+ transform: "translate(-50%, calc(-50% - 30px))",
306
+ fontSize: 20,
307
+ color: theme.palette.text.secondary
308
+ }
309
+ }
310
+ ),
311
+ /* @__PURE__ */ jsxRuntime.jsx(
312
+ material.Typography,
313
+ {
314
+ variant: "body2",
315
+ sx: {
316
+ position: "absolute",
317
+ top: "50%",
318
+ left: "50%",
319
+ transform: "translate(-50%, calc(-50% + 10px))",
320
+ fontWeight: "normal",
321
+ color: theme.palette.text.primary,
322
+ textTransform: "capitalize",
323
+ fontSize: "18px",
324
+ whiteSpace: "nowrap"
325
+ },
326
+ children: selected
327
+ }
328
+ ),
329
+ /* @__PURE__ */ jsxRuntime.jsx(
330
+ material.Typography,
331
+ {
332
+ variant: "h5",
333
+ sx: {
334
+ position: "absolute",
335
+ top: "50%",
336
+ left: "50%",
337
+ transform: "translate(-50%, calc(-50% + 41px))",
338
+ fontWeight: "bold",
339
+ color: theme.palette.text.primary,
340
+ fontSize: "25px",
341
+ whiteSpace: "nowrap"
342
+ },
343
+ children: shared.formatCurrency(
344
+ processedCategories.find((cat) => cat.tag === selected)?.amount || 0
345
+ )
346
+ }
347
+ )
348
+ ] })
349
+ ) : (
350
+ // Show total when nothing selected
351
+ // Also use absolute positioning to ensure proper centering
352
+ /* @__PURE__ */ jsxRuntime.jsx(
353
+ material.Typography,
354
+ {
355
+ variant: "h4",
356
+ sx: {
357
+ position: "absolute",
358
+ top: "50%",
359
+ left: "50%",
360
+ transform: "translate(-50%, -50%)",
361
+ fontWeight: "medium",
362
+ color: theme.palette.text.primary,
363
+ whiteSpace: "nowrap"
364
+ },
365
+ children: shared.formatCurrency(total)
366
+ }
367
+ )
368
+ )
369
+ },
370
+ selected || "total"
371
+ ) })
372
+ ] })
373
+ ]
374
+ }
375
+ );
376
+ }
377
+ function ExpenseThinDonut({
378
+ values,
379
+ selected,
380
+ onSelect,
381
+ valueFormatter = shared.formatCurrency,
382
+ size,
383
+ subtitle = "Total Spending",
384
+ animated = true,
385
+ ariaLabel = "Expense breakdown by category"
386
+ }) {
387
+ const theme = material.useTheme();
388
+ const defaultWidth = 320;
389
+ const defaultHeight = 170;
390
+ const chartWidth = size || defaultWidth;
391
+ const chartHeight = size ? size * (defaultHeight / defaultWidth) : defaultHeight;
392
+ const innerRadius = 40;
393
+ const outerRadius = 48;
394
+ const selectedInnerRadius = 38;
395
+ const selectedOuterRadius = 50;
396
+ const total = React3__default.default.useMemo(() => {
397
+ return values.reduce((sum, item) => sum + item.value, 0);
398
+ }, [values]);
399
+ const selectedValue = React3__default.default.useMemo(() => {
400
+ if (!selected) return total;
401
+ const selectedItem = values.find((item) => item.name === selected);
402
+ return selectedItem ? selectedItem.value : total;
403
+ }, [values, selected, total]);
404
+ const chartData = React3__default.default.useMemo(
405
+ () => values.map((item) => ({
406
+ id: item.name,
407
+ label: item.name,
408
+ value: item.value
409
+ })),
410
+ [values]
411
+ );
412
+ const colors = shared.getChartColors(theme);
413
+ const handleSegmentClick = React3__default.default.useCallback(
414
+ (_event, itemIdentifier) => {
415
+ if (!onSelect) return;
416
+ const segmentName = chartData[itemIdentifier.dataIndex]?.id;
417
+ if (!segmentName) return;
418
+ onSelect(selected === segmentName ? null : segmentName);
419
+ },
420
+ [chartData, onSelect, selected]
421
+ );
422
+ if (values.length === 0) {
423
+ return /* @__PURE__ */ jsxRuntime.jsx(
424
+ material.Box,
425
+ {
426
+ role: "img",
427
+ "aria-label": "No expense data",
428
+ sx: {
429
+ width: chartWidth,
430
+ height: chartHeight,
431
+ display: "flex",
432
+ alignItems: "center",
433
+ justifyContent: "center",
434
+ backgroundColor: theme.palette.primary.main,
435
+ borderRadius: 1
436
+ },
437
+ children: /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", sx: { color: theme.palette.primary.contrastText }, children: "No data available" })
438
+ }
439
+ );
440
+ }
441
+ return /* @__PURE__ */ jsxRuntime.jsx(
442
+ material.Box,
443
+ {
444
+ "data-testid": "expense-thin-donut",
445
+ role: "img",
446
+ "aria-label": ariaLabel,
447
+ sx: {
448
+ position: "relative",
449
+ width: chartWidth,
450
+ height: chartHeight,
451
+ backgroundColor: theme.palette.primary.main,
452
+ // Legacy: theme.palette.primary.main
453
+ borderRadius: 1,
454
+ overflow: "hidden"
455
+ },
456
+ children: /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { position: "relative", width: "100%", height: "100%" }, children: [
457
+ /* @__PURE__ */ jsxRuntime.jsx(
458
+ PieChart.PieChart,
459
+ {
460
+ series: [
461
+ {
462
+ data: chartData,
463
+ innerRadius: selected ? selectedInnerRadius : innerRadius,
464
+ outerRadius: selected ? selectedOuterRadius : outerRadius,
465
+ paddingAngle: 0.02,
466
+ // 0.02 radians matching legacy (line 50)
467
+ cornerRadius: 0,
468
+ // Maintain sort=null behavior from D3 (line 53)
469
+ // Prevent auto-reordering on update
470
+ sortingValues: "none",
471
+ highlightScope: { fade: "global", highlight: "item" }
472
+ }
473
+ ],
474
+ colors,
475
+ width: chartWidth,
476
+ height: chartHeight,
477
+ onItemClick: onSelect ? handleSegmentClick : void 0,
478
+ sx: {
479
+ // Hide legend completely (slotProps.legend.hidden may not work in all cases)
480
+ "& .MuiChartsLegend-root": {
481
+ display: "none"
482
+ },
483
+ // Make slices clickable if handler provided
484
+ ...onSelect && {
485
+ "& .MuiPieArc-root": {
486
+ cursor: "pointer",
487
+ transition: animated ? "all 200ms ease-in-out" : "none"
488
+ // 200ms matching legacy
489
+ },
490
+ "& .MuiPieArc-root:hover": {
491
+ filter: "brightness(1.1)"
492
+ }
493
+ }
494
+ }
495
+ }
496
+ ),
497
+ /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { mode: "wait", children: /* @__PURE__ */ jsxRuntime.jsx(
498
+ material.Box,
499
+ {
500
+ component: framerMotion.motion.div,
501
+ variants: animated ? shared.textVariants : void 0,
502
+ initial: animated ? "initial" : void 0,
503
+ animate: animated ? "enter" : void 0,
504
+ exit: animated ? "exit" : void 0,
505
+ sx: {
506
+ position: "absolute",
507
+ top: "50%",
508
+ left: "50%",
509
+ transform: selected ? "translate(-50%, calc(-50% + 5px))" : "translate(-50%, calc(-50% - 4px))",
510
+ // Legacy y positions
511
+ textAlign: "center",
512
+ pointerEvents: "none",
513
+ transition: animated ? "transform 200ms ease-in-out" : "none",
514
+ zIndex: 10
515
+ // Ensure text is above chart
516
+ },
517
+ children: /* @__PURE__ */ jsxRuntime.jsx(
518
+ material.Typography,
519
+ {
520
+ variant: "h6",
521
+ sx: {
522
+ fontWeight: "bold",
523
+ fontSize: "11px",
524
+ // Legacy font-size
525
+ color: theme.palette.primary.contrastText,
526
+ // Legacy: theme.palette.background.default
527
+ whiteSpace: "nowrap"
528
+ },
529
+ children: valueFormatter(selectedValue)
530
+ }
531
+ )
532
+ },
533
+ selected || "total"
534
+ ) }),
535
+ !selected && subtitle && /* @__PURE__ */ jsxRuntime.jsx(
536
+ material.Box,
537
+ {
538
+ component: framerMotion.motion.div,
539
+ variants: animated ? shared.fadeVariants : void 0,
540
+ initial: animated ? "initial" : void 0,
541
+ animate: animated ? "enter" : void 0,
542
+ exit: animated ? "exit" : void 0,
543
+ sx: {
544
+ position: "absolute",
545
+ top: "50%",
546
+ left: "50%",
547
+ transform: "translate(-50%, calc(-50% + 8px))",
548
+ // Legacy y: 8
549
+ textAlign: "center",
550
+ pointerEvents: "none",
551
+ zIndex: 10
552
+ // Ensure text is above chart
553
+ },
554
+ children: /* @__PURE__ */ jsxRuntime.jsx(
555
+ material.Typography,
556
+ {
557
+ variant: "caption",
558
+ sx: {
559
+ fontSize: "10px",
560
+ // Legacy font-size
561
+ color: theme.palette.primary.contrastText,
562
+ whiteSpace: "nowrap"
563
+ },
564
+ children: subtitle
565
+ }
566
+ )
567
+ }
568
+ )
569
+ ] })
570
+ }
571
+ );
572
+ }
573
+ function ExpenseMeter({
574
+ data,
575
+ total,
576
+ selectedIndex,
577
+ onSelect,
578
+ small = false,
579
+ backgroundColor,
580
+ backgroundOpacity = 1,
581
+ animated = true,
582
+ ariaLabel = "Expense budget progress meter"
583
+ }) {
584
+ const theme = material.useTheme();
585
+ const width = 300;
586
+ const height = small ? 20 : 40;
587
+ const radius = small ? 5 : 10;
588
+ const margin = small ? 5 : 10;
589
+ const plotWidth = width - margin * 2;
590
+ const plotHeight = height - margin * 2;
591
+ const segments = React3__default.default.useMemo(() => {
592
+ let cumulative = 0;
593
+ return data.map((item, index) => {
594
+ const segment = {
595
+ ...item,
596
+ cumulative,
597
+ index
598
+ };
599
+ cumulative += item.value;
600
+ return segment;
601
+ });
602
+ }, [data]);
603
+ const totalValue = React3__default.default.useMemo(
604
+ () => segments.reduce((sum, s) => sum + s.value, 0),
605
+ [segments]
606
+ );
607
+ const isOverBudget = totalValue > total;
608
+ const scale = React3__default.default.useCallback(
609
+ (value) => {
610
+ if (total === 0) return 0;
611
+ return value / total * plotWidth;
612
+ },
613
+ [total, plotWidth]
614
+ );
615
+ const handleSegmentClick = React3__default.default.useCallback(
616
+ (index) => {
617
+ if (!onSelect) return;
618
+ onSelect(selectedIndex === index ? null : index);
619
+ },
620
+ [onSelect, selectedIndex]
621
+ );
622
+ const clipId = React3__default.default.useId();
623
+ const selectedSegment = selectedIndex !== void 0 && selectedIndex >= 0 && selectedIndex < segments.length ? segments[selectedIndex] : null;
624
+ const triangleX = selectedSegment ? margin + scale(selectedSegment.cumulative + selectedSegment.value / 2) : 0;
625
+ return /* @__PURE__ */ jsxRuntime.jsx(
626
+ material.Box,
627
+ {
628
+ "data-testid": "expense-meter",
629
+ role: "img",
630
+ "aria-label": ariaLabel,
631
+ sx: {
632
+ position: "relative",
633
+ width: "100%",
634
+ maxWidth: width
635
+ },
636
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
637
+ "svg",
638
+ {
639
+ width: "100%",
640
+ viewBox: `0 0 ${width} ${height}`,
641
+ preserveAspectRatio: "xMidYMid meet",
642
+ style: { display: "flex" },
643
+ children: [
644
+ /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsxRuntime.jsx(
645
+ "rect",
646
+ {
647
+ x: 0,
648
+ y: 0,
649
+ width: plotWidth,
650
+ height: plotHeight,
651
+ rx: radius,
652
+ ry: radius
653
+ }
654
+ ) }) }),
655
+ /* @__PURE__ */ jsxRuntime.jsx(
656
+ "rect",
657
+ {
658
+ x: margin,
659
+ y: margin,
660
+ width: plotWidth,
661
+ height: plotHeight,
662
+ rx: radius,
663
+ ry: radius,
664
+ fill: backgroundColor || theme.palette.grey[300],
665
+ style: { opacity: backgroundOpacity }
666
+ }
667
+ ),
668
+ /* @__PURE__ */ jsxRuntime.jsx("g", { transform: `translate(${margin}, ${margin})`, clipPath: `url(#${clipId})`, children: segments.map((segment) => {
669
+ const segmentWidth = scale(isOverBudget ? Math.min(segment.value, total) : segment.value);
670
+ const segmentX = scale(segment.cumulative);
671
+ const segmentColor = isOverBudget ? theme.palette.error.main : segment.color;
672
+ return /* @__PURE__ */ jsxRuntime.jsx(
673
+ framerMotion.motion.rect,
674
+ {
675
+ "data-testid": `meter-segment-${segment.index}`,
676
+ x: segmentX,
677
+ y: 0,
678
+ width: segmentWidth,
679
+ height: plotHeight,
680
+ fill: segmentColor,
681
+ onClick: () => handleSegmentClick(segment.index),
682
+ style: {
683
+ cursor: onSelect ? "pointer" : "default"
684
+ },
685
+ initial: animated ? { width: 0 } : void 0,
686
+ animate: animated ? { width: segmentWidth, x: segmentX } : void 0,
687
+ transition: animated ? { duration: 0.6, ease: "easeInOut" } : void 0
688
+ },
689
+ segment.index
690
+ );
691
+ }) }),
692
+ /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: selectedSegment && /* @__PURE__ */ jsxRuntime.jsx(
693
+ framerMotion.motion.path,
694
+ {
695
+ "data-testid": "meter-triangle",
696
+ d: "M -8 -8 L 8 -8 L 0 2 Z",
697
+ transform: `translate(${triangleX}, ${height - 3})`,
698
+ fill: isOverBudget ? theme.palette.error.main : selectedSegment.color,
699
+ initial: animated ? { opacity: 0 } : void 0,
700
+ animate: animated ? { opacity: 1 } : void 0,
701
+ exit: animated ? { opacity: 0 } : void 0,
702
+ transition: animated ? { duration: 0.6 } : void 0
703
+ }
704
+ ) })
705
+ ]
706
+ }
707
+ )
708
+ }
709
+ );
710
+ }
711
+ function ExpenseLineChart({
712
+ data,
713
+ selectedDate,
714
+ onSelectDate,
715
+ width = 120,
716
+ height = 100,
717
+ showGrid = false,
718
+ animated = true
719
+ }) {
720
+ const theme = material.useTheme();
721
+ const allData = data.flat();
722
+ if (allData.length === 0) {
723
+ return /* @__PURE__ */ jsxRuntime.jsx(
724
+ material.Box,
725
+ {
726
+ sx: {
727
+ width,
728
+ height,
729
+ display: "flex",
730
+ alignItems: "center",
731
+ justifyContent: "center",
732
+ color: theme.palette.text.secondary,
733
+ fontSize: "12px"
734
+ },
735
+ children: "No data available"
736
+ }
737
+ );
738
+ }
739
+ const dates = data[0]?.map((point) => point.date.getTime()) || [];
740
+ const sortedDates = [...dates].sort((a, b) => a - b);
741
+ const series = data.map((dataPoints, index) => {
742
+ const sortedPoints = [...dataPoints].sort((a, b) => a.date.getTime() - b.date.getTime());
743
+ return {
744
+ data: sortedPoints.map((point) => point.value),
745
+ label: index === 0 ? "Current" : "Previous",
746
+ color: index === 0 ? theme.palette.primary.dark : theme.palette.primary.light,
747
+ curve: "linear",
748
+ showMark: false
749
+ };
750
+ });
751
+ const selectedIndex = selectedDate ? dates.findIndex((d) => {
752
+ const date = new Date(d);
753
+ return date.getFullYear() === selectedDate.getFullYear() && date.getMonth() === selectedDate.getMonth() && date.getDate() === selectedDate.getDate();
754
+ }) : -1;
755
+ const formatXAxis = (value) => {
756
+ const date = new Date(value);
757
+ return date.toLocaleDateString("en-US", { month: "short" });
758
+ };
759
+ const formatYAxis = (value) => {
760
+ return shared.formatCurrency(value);
761
+ };
762
+ return /* @__PURE__ */ jsxRuntime.jsx(
763
+ material.Box,
764
+ {
765
+ "data-testid": "expense-line-chart",
766
+ sx: {
767
+ width,
768
+ height,
769
+ position: "relative"
770
+ },
771
+ children: /* @__PURE__ */ jsxRuntime.jsx(
772
+ LineChart.LineChart,
773
+ {
774
+ xAxis: [
775
+ {
776
+ data: sortedDates,
777
+ scaleType: "time",
778
+ valueFormatter: formatXAxis,
779
+ tickNumber: data[0]?.length || 0,
780
+ // Hide axis line (legacy hides path)
781
+ disableLine: true,
782
+ // Hide tick marks (legacy sets display: none)
783
+ disableTicks: true
784
+ }
785
+ ],
786
+ yAxis: [
787
+ {
788
+ valueFormatter: formatYAxis,
789
+ // Hide Y-axis labels (legacy sets display: none)
790
+ // Hide axis line (legacy hides path)
791
+ disableLine: true,
792
+ disableTicks: true
793
+ }
794
+ ],
795
+ series,
796
+ width,
797
+ height,
798
+ grid: { vertical: showGrid, horizontal: showGrid },
799
+ margin: { top: 20, right: 0, bottom: 20, left: 10 },
800
+ slotProps: {
801
+ legend: {}
802
+ },
803
+ sx: {
804
+ // X-axis label styling
805
+ "& .MuiChartsAxis-tickLabel": {
806
+ fontSize: "12px",
807
+ fontFamily: theme.typography.fontFamily,
808
+ cursor: onSelectDate ? "pointer" : "default"
809
+ },
810
+ // Selected date background indicator (rounded rect below X-axis)
811
+ ...selectedIndex !== -1 && {
812
+ "&::after": {
813
+ content: '""',
814
+ position: "absolute",
815
+ bottom: "5px",
816
+ left: `calc(${selectedIndex / (dates.length - 1) * 100}% + 10px - 12.5px)`,
817
+ width: "25px",
818
+ height: "15px",
819
+ backgroundColor: theme.palette.grey[200],
820
+ borderRadius: "3px",
821
+ pointerEvents: "none"
822
+ }
823
+ }
824
+ },
825
+ onAxisClick: onSelectDate ? (event, data2) => {
826
+ if (data2?.axisValue !== void 0) {
827
+ const clickedDate = new Date(data2.axisValue);
828
+ onSelectDate(clickedDate);
829
+ }
830
+ } : void 0,
831
+ ...animated ? {} : { skipAnimation: true }
832
+ }
833
+ )
834
+ }
835
+ );
836
+ }
837
+ function ExpenseBarChart({
838
+ data,
839
+ width = 300,
840
+ height = 160,
841
+ animated = true
842
+ }) {
843
+ const theme = material.useTheme();
844
+ if (!data || data.length === 0) {
845
+ return /* @__PURE__ */ jsxRuntime.jsx(
846
+ material.Box,
847
+ {
848
+ sx: {
849
+ width,
850
+ height,
851
+ display: "flex",
852
+ alignItems: "center",
853
+ justifyContent: "center",
854
+ color: theme.palette.text.secondary,
855
+ fontSize: "12px",
856
+ bgcolor: theme.palette.primary.main
857
+ },
858
+ children: "No data available"
859
+ }
860
+ );
861
+ }
862
+ const formatXAxis = (value) => {
863
+ if (typeof value === "number") {
864
+ const date = new Date(data[value]?.date);
865
+ return date.toLocaleDateString("en-US", { month: "short" });
866
+ }
867
+ return String(value);
868
+ };
869
+ const formatYAxis = (value) => {
870
+ if (Math.abs(value) < 1e4) {
871
+ return shared.formatCurrency(value);
872
+ }
873
+ const absValue = Math.abs(value);
874
+ const isNegative = value < 0;
875
+ if (absValue >= 1e6) {
876
+ return `${isNegative ? "-" : ""}$${(absValue / 1e6).toFixed(1)}M`;
877
+ }
878
+ return `${isNegative ? "-" : ""}$${(absValue / 1e3).toFixed(1)}K`;
879
+ };
880
+ const chartData = data.map((point, index) => ({
881
+ month: index,
882
+ value: point.value,
883
+ noData: point.noData
884
+ }));
885
+ const hasNoDataMonths = data.some((d) => d.noData);
886
+ const maxValue = Math.max(...data.map((d) => d.value), 0);
887
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { "data-testid": "expense-bar-chart", sx: { width, height, position: "relative" }, children: [
888
+ /* @__PURE__ */ jsxRuntime.jsx(
889
+ BarChart.BarChart,
890
+ {
891
+ xAxis: [
892
+ {
893
+ data: chartData.map((_, i) => i),
894
+ scaleType: "band",
895
+ valueFormatter: formatXAxis,
896
+ // Hide default axis line and ticks (legacy hides .domain)
897
+ disableLine: true,
898
+ disableTicks: true
899
+ }
900
+ ],
901
+ yAxis: [
902
+ {
903
+ valueFormatter: formatYAxis,
904
+ min: 0,
905
+ max: maxValue || 100,
906
+ // 5 ticks on Y-axis (legacy uses .ticks(5))
907
+ tickNumber: 5,
908
+ disableLine: true,
909
+ disableTicks: true
910
+ }
911
+ ],
912
+ series: [
913
+ {
914
+ data: chartData.map((d) => d.noData ? null : d.value),
915
+ color: theme.palette.background.default
916
+ // Note: MUI X Charts doesn't support borderRadius on BarChart bars directly
917
+ // We'll use custom styling instead
918
+ }
919
+ ],
920
+ width,
921
+ height,
922
+ margin: { top: 20, left: 50, bottom: 40, right: 20 },
923
+ slotProps: {
924
+ legend: {}
925
+ },
926
+ sx: {
927
+ bgcolor: theme.palette.primary.main,
928
+ // X-axis label styling (rotated 270°, positioned like legacy)
929
+ "& .MuiChartsAxis-bottom .MuiChartsAxis-tickLabel": {
930
+ transform: "translate(-15px, 15px) rotate(270deg)",
931
+ fontWeight: "bold",
932
+ fontSize: "90%",
933
+ fill: theme.palette.background.default
934
+ },
935
+ // Y-axis label styling (positioned above tick, bold)
936
+ "& .MuiChartsAxis-left .MuiChartsAxis-tickLabel": {
937
+ transform: "translate(0, -7px)",
938
+ fontWeight: "bold",
939
+ fontSize: "90%",
940
+ fill: theme.palette.background.default
941
+ },
942
+ // Hide default axis domain lines
943
+ "& .MuiChartsAxis-line": {
944
+ display: "none"
945
+ },
946
+ // Hide default tick lines
947
+ "& .MuiChartsAxis-tick": {
948
+ display: "none"
949
+ },
950
+ // Grid lines on Y-axis ticks (dashed, semi-transparent)
951
+ "& .MuiChartsGrid-line": {
952
+ stroke: theme.palette.primary.dark,
953
+ strokeWidth: 0.5,
954
+ strokeDasharray: "2",
955
+ opacity: 0.5
956
+ },
957
+ // Bar styling
958
+ "& .MuiBarElement-root": {
959
+ // MUI X Charts doesn't support rounded bars natively in BarChart
960
+ // This is a known limitation - we'll use rx/ry if available
961
+ rx: 2,
962
+ ry: 2
963
+ }
964
+ },
965
+ grid: { horizontal: true, vertical: false },
966
+ ...animated ? {} : { skipAnimation: true }
967
+ }
968
+ ),
969
+ hasNoDataMonths && /* @__PURE__ */ jsxRuntime.jsxs(
970
+ material.Box,
971
+ {
972
+ sx: {
973
+ position: "absolute",
974
+ bottom: 5,
975
+ right: 20,
976
+ display: "flex",
977
+ alignItems: "center",
978
+ gap: 0.5
979
+ },
980
+ children: [
981
+ /* @__PURE__ */ jsxRuntime.jsx(
982
+ material.Box,
983
+ {
984
+ component: "svg",
985
+ sx: { width: 10, height: 10 },
986
+ viewBox: "0 0 10 10",
987
+ children: /* @__PURE__ */ jsxRuntime.jsx(
988
+ "line",
989
+ {
990
+ x1: "0",
991
+ y1: "5",
992
+ x2: "10",
993
+ y2: "5",
994
+ stroke: theme.palette.background.default,
995
+ strokeWidth: "1.5",
996
+ strokeDasharray: "2,2",
997
+ strokeLinecap: "round"
998
+ }
999
+ )
1000
+ }
1001
+ ),
1002
+ /* @__PURE__ */ jsxRuntime.jsx(
1003
+ material.Box,
1004
+ {
1005
+ component: "span",
1006
+ sx: {
1007
+ fontSize: "8px",
1008
+ color: theme.palette.background.default,
1009
+ fontWeight: "normal"
1010
+ },
1011
+ children: "No data available"
1012
+ }
1013
+ )
1014
+ ]
1015
+ }
1016
+ ),
1017
+ data.map((point, index) => {
1018
+ if (!point.noData) return null;
1019
+ const plotWidth = width - 50 - 20;
1020
+ const barWidth = plotWidth / data.length;
1021
+ const barX = 50 + index * barWidth + barWidth * 0.2;
1022
+ return /* @__PURE__ */ jsxRuntime.jsx(
1023
+ material.Box,
1024
+ {
1025
+ component: "svg",
1026
+ sx: {
1027
+ position: "absolute",
1028
+ left: barX,
1029
+ bottom: 40,
1030
+ // bottom margin
1031
+ width: barWidth * 0.6,
1032
+ height: 2,
1033
+ pointerEvents: "none"
1034
+ },
1035
+ viewBox: `0 0 ${barWidth * 0.6} 2`,
1036
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1037
+ "line",
1038
+ {
1039
+ x1: "0",
1040
+ y1: "1",
1041
+ x2: barWidth * 0.6,
1042
+ y2: "1",
1043
+ stroke: theme.palette.background.default,
1044
+ strokeWidth: "1.5",
1045
+ strokeDasharray: "2,2",
1046
+ strokeLinecap: "round"
1047
+ }
1048
+ )
1049
+ },
1050
+ index
1051
+ );
1052
+ })
1053
+ ] });
1054
+ }
1055
+ function ExpenseCreateForm({ userId, onSuccess }) {
1056
+ const { mode } = React3.useContext(shared.AppModeContext);
1057
+ const [formData, setFormData] = React3.useState({
1058
+ tag: "",
1059
+ amount: "0.00"
1060
+ });
1061
+ const [showSuccess, setShowSuccess] = React3.useState(false);
1062
+ const createMutation = expensesDataAccess.useCreateExpense({
1063
+ onSuccess: () => {
1064
+ setShowSuccess(true);
1065
+ setFormData({
1066
+ tag: "",
1067
+ amount: "0.00"
1068
+ });
1069
+ onSuccess?.();
1070
+ }
1071
+ });
1072
+ const handleSubmit = (e) => {
1073
+ e.preventDefault();
1074
+ createMutation.mutate({
1075
+ userId,
1076
+ data: formData
1077
+ });
1078
+ };
1079
+ if (mode === "user") {
1080
+ return /* @__PURE__ */ jsxRuntime.jsx(material.Paper, { elevation: 2, sx: { p: 3 }, children: /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "info", children: "Expenses are read-only in User mode. They are automatically computed from your transactions." }) });
1081
+ }
1082
+ return /* @__PURE__ */ jsxRuntime.jsx(material.Paper, { elevation: 2, sx: { p: 3 }, children: /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { component: "form", onSubmit: handleSubmit, children: [
1083
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", gutterBottom: true, children: "Create New Expense (Admin Only)" }),
1084
+ /* @__PURE__ */ jsxRuntime.jsx(
1085
+ material.TextField,
1086
+ {
1087
+ label: "Tag",
1088
+ fullWidth: true,
1089
+ margin: "normal",
1090
+ value: formData.tag,
1091
+ onChange: (e) => setFormData({ ...formData, tag: e.target.value }),
1092
+ required: true,
1093
+ helperText: "Expense category tag (e.g., health, diningout, insurance)"
1094
+ }
1095
+ ),
1096
+ /* @__PURE__ */ jsxRuntime.jsx(
1097
+ material.TextField,
1098
+ {
1099
+ label: "Amount",
1100
+ fullWidth: true,
1101
+ margin: "normal",
1102
+ value: formData.amount,
1103
+ onChange: (e) => setFormData({ ...formData, amount: e.target.value }),
1104
+ required: true,
1105
+ type: "text",
1106
+ inputProps: { pattern: "^\\d+\\.\\d{2}$" },
1107
+ helperText: "Amount in decimal format (e.g., 123.45)"
1108
+ }
1109
+ ),
1110
+ createMutation.isError && /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "error", sx: { mt: 2 }, children: createMutation.error instanceof Error ? createMutation.error.message : "Failed to create expense" }),
1111
+ /* @__PURE__ */ jsxRuntime.jsx(material.Box, { sx: { mt: 3 }, children: /* @__PURE__ */ jsxRuntime.jsx(material.Button, { type: "submit", variant: "contained", disabled: createMutation.isPending, children: createMutation.isPending ? "Creating..." : "Create Expense" }) }),
1112
+ /* @__PURE__ */ jsxRuntime.jsx(
1113
+ material.Snackbar,
1114
+ {
1115
+ open: showSuccess,
1116
+ autoHideDuration: 3e3,
1117
+ onClose: () => setShowSuccess(false),
1118
+ message: "Expense created successfully"
1119
+ }
1120
+ )
1121
+ ] }) });
1122
+ }
1123
+ function ExpenseEditForm({ expense, open, onClose, userId }) {
1124
+ const { mode } = React3.useContext(shared.AppModeContext);
1125
+ const [amount, setAmount] = React3.useState(expense.amount);
1126
+ React3.useEffect(() => {
1127
+ setAmount(expense.amount);
1128
+ }, [expense]);
1129
+ const updateMutation = expensesDataAccess.useUpdateExpense({
1130
+ onSuccess: () => {
1131
+ onClose();
1132
+ }
1133
+ });
1134
+ const handleSubmit = (e) => {
1135
+ e.preventDefault();
1136
+ updateMutation.mutate({
1137
+ userId,
1138
+ tag: expense.tag,
1139
+ data: { amount }
1140
+ });
1141
+ };
1142
+ if (mode === "user") {
1143
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.Dialog, { open, onClose, maxWidth: "sm", fullWidth: true, children: [
1144
+ /* @__PURE__ */ jsxRuntime.jsx(material.DialogTitle, { children: "Edit Expense" }),
1145
+ /* @__PURE__ */ jsxRuntime.jsx(material.DialogContent, { children: /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "info", sx: { mt: 2 }, children: "Expenses are read-only in User mode. They are automatically computed from your transactions." }) }),
1146
+ /* @__PURE__ */ jsxRuntime.jsx(material.DialogActions, { children: /* @__PURE__ */ jsxRuntime.jsx(material.Button, { onClick: onClose, children: "Close" }) })
1147
+ ] });
1148
+ }
1149
+ return /* @__PURE__ */ jsxRuntime.jsx(material.Dialog, { open, onClose, maxWidth: "sm", fullWidth: true, children: /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, children: [
1150
+ /* @__PURE__ */ jsxRuntime.jsx(material.DialogTitle, { children: "Edit Expense (Admin Only)" }),
1151
+ /* @__PURE__ */ jsxRuntime.jsxs(material.DialogContent, { children: [
1152
+ /* @__PURE__ */ jsxRuntime.jsx(
1153
+ material.TextField,
1154
+ {
1155
+ label: "Tag",
1156
+ fullWidth: true,
1157
+ margin: "normal",
1158
+ value: expense.tag,
1159
+ disabled: true,
1160
+ helperText: "Tag cannot be changed after creation"
1161
+ }
1162
+ ),
1163
+ /* @__PURE__ */ jsxRuntime.jsx(
1164
+ material.TextField,
1165
+ {
1166
+ label: "Amount",
1167
+ fullWidth: true,
1168
+ margin: "normal",
1169
+ value: amount,
1170
+ onChange: (e) => setAmount(e.target.value),
1171
+ required: true,
1172
+ type: "text",
1173
+ inputProps: { pattern: "^\\d+\\.\\d{2}$" },
1174
+ helperText: "Amount in decimal format (e.g., 123.45)"
1175
+ }
1176
+ ),
1177
+ updateMutation.isError && /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "error", sx: { mt: 2 }, children: updateMutation.error instanceof Error ? updateMutation.error.message : "Failed to update expense" })
1178
+ ] }),
1179
+ /* @__PURE__ */ jsxRuntime.jsxs(material.DialogActions, { children: [
1180
+ /* @__PURE__ */ jsxRuntime.jsx(material.Button, { onClick: onClose, children: "Cancel" }),
1181
+ /* @__PURE__ */ jsxRuntime.jsx(
1182
+ material.Button,
1183
+ {
1184
+ type: "submit",
1185
+ variant: "contained",
1186
+ disabled: updateMutation.isPending,
1187
+ children: updateMutation.isPending ? "Saving..." : "Save Changes"
1188
+ }
1189
+ )
1190
+ ] })
1191
+ ] }) });
1192
+ }
1193
+ function ExpenseDeleteButton({
1194
+ expense,
1195
+ userId,
1196
+ onSuccess,
1197
+ variant = "icon"
1198
+ }) {
1199
+ const { mode } = React3.useContext(shared.AppModeContext);
1200
+ const [open, setOpen] = React3.useState(false);
1201
+ const deleteMutation = expensesDataAccess.useDeleteExpense({
1202
+ onSuccess: () => {
1203
+ setOpen(false);
1204
+ onSuccess?.();
1205
+ }
1206
+ });
1207
+ const handleDelete = () => {
1208
+ deleteMutation.mutate({
1209
+ userId,
1210
+ tag: expense.tag
1211
+ });
1212
+ };
1213
+ const handleClickOpen = () => {
1214
+ setOpen(true);
1215
+ };
1216
+ const handleClose = () => {
1217
+ setOpen(false);
1218
+ };
1219
+ if (mode === "user") {
1220
+ return null;
1221
+ }
1222
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1223
+ variant === "icon" ? /* @__PURE__ */ jsxRuntime.jsx(
1224
+ material.IconButton,
1225
+ {
1226
+ "aria-label": "delete expense",
1227
+ onClick: handleClickOpen,
1228
+ color: "error",
1229
+ size: "small",
1230
+ children: /* @__PURE__ */ jsxRuntime.jsx(DeleteIcon__default.default, {})
1231
+ }
1232
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
1233
+ material.Button,
1234
+ {
1235
+ variant: "outlined",
1236
+ color: "error",
1237
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(DeleteIcon__default.default, {}),
1238
+ onClick: handleClickOpen,
1239
+ children: "Delete"
1240
+ }
1241
+ ),
1242
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Dialog, { open, onClose: handleClose, children: [
1243
+ /* @__PURE__ */ jsxRuntime.jsx(material.DialogTitle, { children: "Delete Expense" }),
1244
+ /* @__PURE__ */ jsxRuntime.jsxs(material.DialogContent, { children: [
1245
+ /* @__PURE__ */ jsxRuntime.jsxs(material.DialogContentText, { children: [
1246
+ 'Are you sure you want to delete the expense for tag "',
1247
+ expense.tag,
1248
+ '"? This action cannot be undone.'
1249
+ ] }),
1250
+ deleteMutation.isError && /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "error", sx: { mt: 2 }, children: deleteMutation.error instanceof Error ? deleteMutation.error.message : "Failed to delete expense" })
1251
+ ] }),
1252
+ /* @__PURE__ */ jsxRuntime.jsxs(material.DialogActions, { children: [
1253
+ /* @__PURE__ */ jsxRuntime.jsx(material.Button, { onClick: handleClose, disabled: deleteMutation.isPending, children: "Cancel" }),
1254
+ /* @__PURE__ */ jsxRuntime.jsx(
1255
+ material.Button,
1256
+ {
1257
+ onClick: handleDelete,
1258
+ color: "error",
1259
+ variant: "contained",
1260
+ disabled: deleteMutation.isPending,
1261
+ children: deleteMutation.isPending ? "Deleting..." : "Delete"
1262
+ }
1263
+ )
1264
+ ] })
1265
+ ] })
1266
+ ] });
1267
+ }
1268
+ function ExpenseList({ userId, startDate, endDate }) {
1269
+ const { mode } = React3.useContext(shared.AppModeContext);
1270
+ const [editingExpense, setEditingExpense] = React3.useState(null);
1271
+ const { data, isLoading, isError, error } = expensesDataAccess.useExpenses({
1272
+ userId,
1273
+ filters: startDate && endDate ? {
1274
+ begin_on: startDate,
1275
+ end_on: endDate
1276
+ } : void 0
1277
+ });
1278
+ if (isLoading) {
1279
+ return /* @__PURE__ */ jsxRuntime.jsx(material.Box, { display: "flex", justifyContent: "center", p: 3, children: /* @__PURE__ */ jsxRuntime.jsx(material.CircularProgress, {}) });
1280
+ }
1281
+ if (isError) {
1282
+ return /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "error", children: error instanceof Error ? error.message : "Failed to load expenses" });
1283
+ }
1284
+ const expenses = data || [];
1285
+ if (expenses.length === 0) {
1286
+ return /* @__PURE__ */ jsxRuntime.jsx(material.Paper, { sx: { p: 3 }, children: /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { color: "text.secondary", align: "center", children: "No expenses found for the selected period." }) });
1287
+ }
1288
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1289
+ /* @__PURE__ */ jsxRuntime.jsx(material.TableContainer, { component: material.Paper, children: /* @__PURE__ */ jsxRuntime.jsxs(material.Table, { children: [
1290
+ /* @__PURE__ */ jsxRuntime.jsx(material.TableHead, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.TableRow, { children: [
1291
+ /* @__PURE__ */ jsxRuntime.jsx(material.TableCell, { children: "Tag" }),
1292
+ /* @__PURE__ */ jsxRuntime.jsx(material.TableCell, { align: "right", children: "Amount" }),
1293
+ mode === "admin" && /* @__PURE__ */ jsxRuntime.jsx(material.TableCell, { align: "right", children: "Actions" })
1294
+ ] }) }),
1295
+ /* @__PURE__ */ jsxRuntime.jsx(material.TableBody, { children: expenses.map((expense) => /* @__PURE__ */ jsxRuntime.jsxs(material.TableRow, { children: [
1296
+ /* @__PURE__ */ jsxRuntime.jsx(material.TableCell, { children: expense.tag }),
1297
+ /* @__PURE__ */ jsxRuntime.jsxs(material.TableCell, { align: "right", children: [
1298
+ "$",
1299
+ expense.amount
1300
+ ] }),
1301
+ mode === "admin" && /* @__PURE__ */ jsxRuntime.jsxs(material.TableCell, { align: "right", children: [
1302
+ /* @__PURE__ */ jsxRuntime.jsx(
1303
+ material.IconButton,
1304
+ {
1305
+ size: "small",
1306
+ onClick: () => setEditingExpense(expense),
1307
+ "aria-label": "edit expense",
1308
+ children: /* @__PURE__ */ jsxRuntime.jsx(EditIcon__default.default, {})
1309
+ }
1310
+ ),
1311
+ /* @__PURE__ */ jsxRuntime.jsx(ExpenseDeleteButton, { expense, userId })
1312
+ ] })
1313
+ ] }, expense.tag)) })
1314
+ ] }) }),
1315
+ editingExpense && /* @__PURE__ */ jsxRuntime.jsx(
1316
+ ExpenseEditForm,
1317
+ {
1318
+ expense: editingExpense,
1319
+ open: Boolean(editingExpense),
1320
+ onClose: () => setEditingExpense(null),
1321
+ userId
1322
+ }
1323
+ )
1324
+ ] });
1325
+ }
1326
+ function ExpenseDateRangeTabs({
1327
+ onChange,
1328
+ useShortLabels = false
1329
+ }) {
1330
+ const theme = material.useTheme();
1331
+ const { selectedDateRangeIndex, allDateRanges, selectDateRange } = expensesFeature.useExpenseWheelContext();
1332
+ const isSmallScreen = material.useMediaQuery(theme.breakpoints.down("sm"));
1333
+ const shouldUseShortLabels = useShortLabels || isSmallScreen;
1334
+ const handleChange = (_event, newIndex) => {
1335
+ selectDateRange(newIndex);
1336
+ onChange?.(newIndex);
1337
+ };
1338
+ return /* @__PURE__ */ jsxRuntime.jsx(
1339
+ material.Tabs,
1340
+ {
1341
+ value: selectedDateRangeIndex !== -1 ? selectedDateRangeIndex : false,
1342
+ onChange: handleChange,
1343
+ textColor: "primary",
1344
+ indicatorColor: "primary",
1345
+ variant: "fullWidth",
1346
+ sx: {
1347
+ borderBottom: 1,
1348
+ borderColor: "divider",
1349
+ minHeight: 48
1350
+ },
1351
+ children: allDateRanges.map((dateRange, index) => /* @__PURE__ */ jsxRuntime.jsx(
1352
+ material.Tab,
1353
+ {
1354
+ label: shouldUseShortLabels ? dateRange.label : dateRange.longLabel,
1355
+ sx: {
1356
+ minWidth: "auto",
1357
+ flex: 1,
1358
+ fontSize: 12,
1359
+ fontWeight: "medium",
1360
+ px: 1,
1361
+ [theme.breakpoints.up("md")]: {
1362
+ minWidth: 0,
1363
+ fontSize: 13
1364
+ }
1365
+ }
1366
+ },
1367
+ dateRange.label
1368
+ ))
1369
+ }
1370
+ );
1371
+ }
1372
+ var DEFAULT_DISPLAY_PREFERENCES = {
1373
+ chartType: "wheel",
1374
+ chartColor: "#2196f3",
1375
+ dateRange: "current-month",
1376
+ categoryGrouping: "none",
1377
+ showLegend: true,
1378
+ showLabels: true
1379
+ };
1380
+ var DEFAULT_EXPENSE_COLUMNS = [
1381
+ "date",
1382
+ "description",
1383
+ "category",
1384
+ "amount",
1385
+ "account",
1386
+ "tags"
1387
+ ];
1388
+ function ExpenseSettingsPanel({
1389
+ accounts = [],
1390
+ availableTags = [],
1391
+ displayPreferences = DEFAULT_DISPLAY_PREFERENCES,
1392
+ onDisplayPreferencesChange,
1393
+ onSave,
1394
+ onCancel,
1395
+ onReset
1396
+ }) {
1397
+ const {
1398
+ settingsVisible,
1399
+ unsavedFilters,
1400
+ toggleAccount,
1401
+ toggleExcludedTag,
1402
+ saveFilters,
1403
+ cancelFilters
1404
+ } = expensesFeature.useExpenseWheelContext();
1405
+ const [selectedTab, setSelectedTab] = React3.useState(0);
1406
+ const [localPrefs, setLocalPrefs] = React3.useState(displayPreferences);
1407
+ const handleSave = () => {
1408
+ saveFilters();
1409
+ onDisplayPreferencesChange?.(localPrefs);
1410
+ onSave?.();
1411
+ };
1412
+ const handleCancel = () => {
1413
+ cancelFilters();
1414
+ setLocalPrefs(displayPreferences);
1415
+ setSelectedTab(0);
1416
+ onCancel?.();
1417
+ };
1418
+ const handleReset = () => {
1419
+ setLocalPrefs(DEFAULT_DISPLAY_PREFERENCES);
1420
+ onDisplayPreferencesChange?.(DEFAULT_DISPLAY_PREFERENCES);
1421
+ onReset?.();
1422
+ };
1423
+ const handleTagChange = (_event, value) => {
1424
+ const currentExcluded = unsavedFilters.excludedTags || [];
1425
+ currentExcluded.forEach((tag) => {
1426
+ if (!value.includes(tag)) {
1427
+ toggleExcludedTag(tag);
1428
+ }
1429
+ });
1430
+ value.forEach((tag) => {
1431
+ if (!currentExcluded.includes(tag)) {
1432
+ toggleExcludedTag(tag);
1433
+ }
1434
+ });
1435
+ };
1436
+ const handlePrefChange = (key, value) => {
1437
+ setLocalPrefs((prev) => ({ ...prev, [key]: value }));
1438
+ };
1439
+ return /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: settingsVisible && /* @__PURE__ */ jsxRuntime.jsxs(
1440
+ material.Box,
1441
+ {
1442
+ "data-testid": "expense-settings-panel",
1443
+ component: framerMotion.motion.div,
1444
+ variants: shared.panelVariants,
1445
+ initial: "initial",
1446
+ animate: "enter",
1447
+ exit: "exit",
1448
+ sx: {
1449
+ position: "relative",
1450
+ p: 3,
1451
+ bgcolor: "background.paper",
1452
+ borderRadius: 1,
1453
+ minWidth: 400
1454
+ },
1455
+ children: [
1456
+ /* @__PURE__ */ jsxRuntime.jsx(
1457
+ material.IconButton,
1458
+ {
1459
+ onClick: handleCancel,
1460
+ sx: {
1461
+ position: "absolute",
1462
+ top: 8,
1463
+ right: 8
1464
+ },
1465
+ "aria-label": "Close settings",
1466
+ children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon__default.default, {})
1467
+ }
1468
+ ),
1469
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", gutterBottom: true, sx: { mb: 2 }, children: "Expense Settings" }),
1470
+ /* @__PURE__ */ jsxRuntime.jsxs(
1471
+ material.Tabs,
1472
+ {
1473
+ value: selectedTab,
1474
+ onChange: (_event, newValue) => setSelectedTab(newValue),
1475
+ sx: { mb: 2, borderBottom: 1, borderColor: "divider" },
1476
+ children: [
1477
+ /* @__PURE__ */ jsxRuntime.jsx(material.Tab, { label: "Filters" }),
1478
+ /* @__PURE__ */ jsxRuntime.jsx(material.Tab, { label: "Display" }),
1479
+ /* @__PURE__ */ jsxRuntime.jsx(material.Tab, { label: "Advanced" })
1480
+ ]
1481
+ }
1482
+ ),
1483
+ selectedTab === 0 && /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { children: [
1484
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { mb: 3 }, children: [
1485
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", gutterBottom: true, sx: { fontWeight: "medium" }, children: "Accounts" }),
1486
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", sx: { mb: 1, display: "block" }, children: "Select accounts to include in analysis" }),
1487
+ /* @__PURE__ */ jsxRuntime.jsx(material.FormGroup, { children: accounts.length > 0 ? accounts.map((account) => /* @__PURE__ */ jsxRuntime.jsx(
1488
+ material.FormControlLabel,
1489
+ {
1490
+ control: /* @__PURE__ */ jsxRuntime.jsx(
1491
+ material.Checkbox,
1492
+ {
1493
+ checked: unsavedFilters.accountIds?.includes(account.id) ?? false,
1494
+ onChange: () => toggleAccount(account.id),
1495
+ size: "small"
1496
+ }
1497
+ ),
1498
+ label: account.name,
1499
+ sx: { ml: 0 }
1500
+ },
1501
+ account.id
1502
+ )) : /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "No accounts available" }) })
1503
+ ] }),
1504
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { mb: 3 }, children: [
1505
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", gutterBottom: true, sx: { fontWeight: "medium" }, children: "Excluded Tags" }),
1506
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", sx: { mb: 1, display: "block" }, children: "Tags to exclude from analysis" }),
1507
+ /* @__PURE__ */ jsxRuntime.jsx(
1508
+ material.Autocomplete,
1509
+ {
1510
+ multiple: true,
1511
+ options: availableTags,
1512
+ value: unsavedFilters.excludedTags || [],
1513
+ onChange: handleTagChange,
1514
+ renderTags: (value, getTagProps) => value.map((option, index) => /* @__PURE__ */ React3.createElement(
1515
+ material.Chip,
1516
+ {
1517
+ ...getTagProps({ index }),
1518
+ key: option,
1519
+ label: option,
1520
+ size: "small"
1521
+ }
1522
+ )),
1523
+ renderInput: (params) => /* @__PURE__ */ jsxRuntime.jsx(
1524
+ material.TextField,
1525
+ {
1526
+ ...params,
1527
+ placeholder: unsavedFilters.excludedTags?.length ? "" : "Select tags to exclude",
1528
+ size: "small"
1529
+ }
1530
+ ),
1531
+ sx: { mt: 1 }
1532
+ }
1533
+ )
1534
+ ] })
1535
+ ] }),
1536
+ selectedTab === 1 && /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { children: [
1537
+ /* @__PURE__ */ jsxRuntime.jsxs(material.FormControl, { fullWidth: true, sx: { mb: 2 }, children: [
1538
+ /* @__PURE__ */ jsxRuntime.jsx(material.InputLabel, { size: "small", children: "Chart Type" }),
1539
+ /* @__PURE__ */ jsxRuntime.jsxs(
1540
+ material.Select,
1541
+ {
1542
+ size: "small",
1543
+ value: localPrefs.chartType,
1544
+ label: "Chart Type",
1545
+ onChange: (e) => handlePrefChange("chartType", e.target.value),
1546
+ children: [
1547
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "wheel", children: "Wheel Chart" }),
1548
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "donut", children: "Donut Chart" }),
1549
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "bar", children: "Bar Chart" }),
1550
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "meter", children: "Meter Chart" })
1551
+ ]
1552
+ }
1553
+ )
1554
+ ] }),
1555
+ /* @__PURE__ */ jsxRuntime.jsx(
1556
+ material.TextField,
1557
+ {
1558
+ fullWidth: true,
1559
+ size: "small",
1560
+ label: "Chart Color",
1561
+ type: "color",
1562
+ value: localPrefs.chartColor,
1563
+ onChange: (e) => handlePrefChange("chartColor", e.target.value),
1564
+ sx: { mb: 2 }
1565
+ }
1566
+ ),
1567
+ /* @__PURE__ */ jsxRuntime.jsxs(material.FormControl, { fullWidth: true, sx: { mb: 2 }, children: [
1568
+ /* @__PURE__ */ jsxRuntime.jsx(material.InputLabel, { size: "small", children: "Date Range" }),
1569
+ /* @__PURE__ */ jsxRuntime.jsxs(
1570
+ material.Select,
1571
+ {
1572
+ size: "small",
1573
+ value: localPrefs.dateRange,
1574
+ label: "Date Range",
1575
+ onChange: (e) => handlePrefChange("dateRange", e.target.value),
1576
+ children: [
1577
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "current-month", children: "Current Month" }),
1578
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "last-30-days", children: "Last 30 Days" }),
1579
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "last-3-months", children: "Last 3 Months" }),
1580
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "last-6-months", children: "Last 6 Months" }),
1581
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "last-year", children: "Last Year" })
1582
+ ]
1583
+ }
1584
+ )
1585
+ ] }),
1586
+ /* @__PURE__ */ jsxRuntime.jsx(
1587
+ material.FormControlLabel,
1588
+ {
1589
+ control: /* @__PURE__ */ jsxRuntime.jsx(
1590
+ material.Checkbox,
1591
+ {
1592
+ checked: localPrefs.showLegend,
1593
+ onChange: (e) => handlePrefChange("showLegend", e.target.checked),
1594
+ size: "small"
1595
+ }
1596
+ ),
1597
+ label: "Show Legend",
1598
+ sx: { mb: 1 }
1599
+ }
1600
+ ),
1601
+ /* @__PURE__ */ jsxRuntime.jsx(
1602
+ material.FormControlLabel,
1603
+ {
1604
+ control: /* @__PURE__ */ jsxRuntime.jsx(
1605
+ material.Checkbox,
1606
+ {
1607
+ checked: localPrefs.showLabels,
1608
+ onChange: (e) => handlePrefChange("showLabels", e.target.checked),
1609
+ size: "small"
1610
+ }
1611
+ ),
1612
+ label: "Show Labels"
1613
+ }
1614
+ )
1615
+ ] }),
1616
+ selectedTab === 2 && /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { children: [
1617
+ /* @__PURE__ */ jsxRuntime.jsxs(material.FormControl, { fullWidth: true, sx: { mb: 2 }, children: [
1618
+ /* @__PURE__ */ jsxRuntime.jsx(material.InputLabel, { size: "small", children: "Category Grouping" }),
1619
+ /* @__PURE__ */ jsxRuntime.jsxs(
1620
+ material.Select,
1621
+ {
1622
+ size: "small",
1623
+ value: localPrefs.categoryGrouping,
1624
+ label: "Category Grouping",
1625
+ onChange: (e) => handlePrefChange("categoryGrouping", e.target.value),
1626
+ children: [
1627
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "none", children: "No Grouping" }),
1628
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "parent", children: "Group by Parent Category" }),
1629
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "custom", children: "Custom Grouping" })
1630
+ ]
1631
+ }
1632
+ )
1633
+ ] }),
1634
+ /* @__PURE__ */ jsxRuntime.jsx(material.Divider, { sx: { my: 2 } }),
1635
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { mb: 2 }, children: [
1636
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", gutterBottom: true, sx: { fontWeight: "medium" }, children: "Default Table Columns" }),
1637
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "caption", color: "text.secondary", sx: { mb: 1, display: "block" }, children: [
1638
+ "Default columns: ",
1639
+ DEFAULT_EXPENSE_COLUMNS.join(", ")
1640
+ ] })
1641
+ ] }),
1642
+ /* @__PURE__ */ jsxRuntime.jsx(material.Divider, { sx: { my: 2 } }),
1643
+ /* @__PURE__ */ jsxRuntime.jsx(
1644
+ material.Button,
1645
+ {
1646
+ onClick: handleReset,
1647
+ variant: "outlined",
1648
+ size: "small",
1649
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(RestartAltIcon__default.default, {}),
1650
+ fullWidth: true,
1651
+ children: "Reset All to Defaults"
1652
+ }
1653
+ )
1654
+ ] }),
1655
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", gap: 1, justifyContent: "flex-end", mt: 3 }, children: [
1656
+ /* @__PURE__ */ jsxRuntime.jsx(material.Button, { onClick: handleCancel, variant: "outlined", size: "small", children: "Cancel" }),
1657
+ /* @__PURE__ */ jsxRuntime.jsx(material.Button, { onClick: handleSave, variant: "contained", size: "small", children: "Apply Settings" })
1658
+ ] })
1659
+ ]
1660
+ }
1661
+ ) });
1662
+ }
1663
+ function ExpenseLoadingState({
1664
+ message = "Loading expenses...",
1665
+ minHeight = 300,
1666
+ size = 40
1667
+ }) {
1668
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1669
+ material.Box,
1670
+ {
1671
+ "data-testid": "loading-state",
1672
+ component: framerMotion.motion.div,
1673
+ variants: shared.fadeVariants,
1674
+ initial: "initial",
1675
+ animate: "enter",
1676
+ exit: "exit",
1677
+ sx: {
1678
+ minHeight,
1679
+ height: "100%",
1680
+ width: "100%",
1681
+ display: "flex",
1682
+ flexDirection: "column",
1683
+ alignItems: "center",
1684
+ justifyContent: "center",
1685
+ gap: 2,
1686
+ py: 4
1687
+ },
1688
+ children: [
1689
+ /* @__PURE__ */ jsxRuntime.jsx(material.CircularProgress, { size }),
1690
+ message && /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: message })
1691
+ ]
1692
+ }
1693
+ );
1694
+ }
1695
+ function ExpenseEmptyState({
1696
+ title = "No expense data",
1697
+ message = "Try selecting a different date range or check your accounts.",
1698
+ minHeight = 300
1699
+ }) {
1700
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1701
+ material.Box,
1702
+ {
1703
+ "data-testid": "empty-state",
1704
+ component: framerMotion.motion.div,
1705
+ variants: shared.fadeVariants,
1706
+ initial: "initial",
1707
+ animate: "enter",
1708
+ exit: "exit",
1709
+ sx: {
1710
+ minHeight,
1711
+ height: "100%",
1712
+ width: "100%",
1713
+ display: "flex",
1714
+ flexDirection: "column",
1715
+ alignItems: "center",
1716
+ justifyContent: "center",
1717
+ gap: 2,
1718
+ py: 4
1719
+ },
1720
+ children: [
1721
+ /* @__PURE__ */ jsxRuntime.jsx(
1722
+ DonutLargeIcon__default.default,
1723
+ {
1724
+ sx: {
1725
+ fontSize: 64,
1726
+ color: "action.disabled",
1727
+ opacity: 0.5
1728
+ }
1729
+ }
1730
+ ),
1731
+ /* @__PURE__ */ jsxRuntime.jsx(
1732
+ material.Typography,
1733
+ {
1734
+ variant: "h6",
1735
+ color: "text.secondary",
1736
+ sx: { fontWeight: "medium" },
1737
+ children: title
1738
+ }
1739
+ ),
1740
+ /* @__PURE__ */ jsxRuntime.jsx(
1741
+ material.Typography,
1742
+ {
1743
+ variant: "body2",
1744
+ color: "text.secondary",
1745
+ align: "center",
1746
+ sx: { maxWidth: 400, px: 2 },
1747
+ children: message
1748
+ }
1749
+ )
1750
+ ]
1751
+ }
1752
+ );
1753
+ }
1754
+ function ExpenseWheelContent({
1755
+ userId,
1756
+ accounts = [],
1757
+ availableTags = [],
1758
+ enableSettings = true,
1759
+ onCategoryClick,
1760
+ chartSize
1761
+ }) {
1762
+ const {
1763
+ filters,
1764
+ selectedTag,
1765
+ selectTag,
1766
+ settingsVisible,
1767
+ showSettings,
1768
+ selectedDateRange
1769
+ } = expensesFeature.useExpenseWheelContext();
1770
+ const { data: expenses, isLoading } = expensesDataAccess.useExpenses({
1771
+ userId,
1772
+ filters: {
1773
+ begin_on: filters.begin_on,
1774
+ end_on: filters.end_on
1775
+ }
1776
+ });
1777
+ const hasData = expenses && expenses.length > 0;
1778
+ const handleCategoryClick = (tag) => {
1779
+ selectTag(tag);
1780
+ if (onCategoryClick) {
1781
+ onCategoryClick({
1782
+ tag,
1783
+ accountIds: filters.accountIds || [],
1784
+ dateRange: selectedDateRange
1785
+ });
1786
+ }
1787
+ };
1788
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1789
+ material.Box,
1790
+ {
1791
+ "data-testid": "expense-wheel",
1792
+ sx: {
1793
+ position: "relative",
1794
+ width: "100%"
1795
+ },
1796
+ children: [
1797
+ isLoading && /* @__PURE__ */ jsxRuntime.jsx(
1798
+ material.LinearProgress,
1799
+ {
1800
+ sx: {
1801
+ position: "absolute",
1802
+ top: 0,
1803
+ left: 0,
1804
+ right: 0,
1805
+ height: 1
1806
+ }
1807
+ }
1808
+ ),
1809
+ settingsVisible ? /* @__PURE__ */ jsxRuntime.jsx(
1810
+ ExpenseSettingsPanel,
1811
+ {
1812
+ accounts,
1813
+ availableTags
1814
+ }
1815
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1816
+ /* @__PURE__ */ jsxRuntime.jsx(ExpenseDateRangeTabs, {}),
1817
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { position: "relative", mt: 2 }, children: [
1818
+ enableSettings && /* @__PURE__ */ jsxRuntime.jsx(
1819
+ material.IconButton,
1820
+ {
1821
+ onClick: showSettings,
1822
+ sx: {
1823
+ position: "absolute",
1824
+ top: 8,
1825
+ right: 16,
1826
+ zIndex: 1
1827
+ },
1828
+ "aria-label": "Open settings",
1829
+ children: /* @__PURE__ */ jsxRuntime.jsx(SettingsIcon__default.default, {})
1830
+ }
1831
+ ),
1832
+ isLoading && /* @__PURE__ */ jsxRuntime.jsx(ExpenseLoadingState, {}),
1833
+ !isLoading && !hasData && /* @__PURE__ */ jsxRuntime.jsx(
1834
+ ExpenseEmptyState,
1835
+ {
1836
+ title: "No expenses found",
1837
+ message: "Try selecting a different date range or adjusting your filters."
1838
+ }
1839
+ ),
1840
+ !isLoading && hasData && /* @__PURE__ */ jsxRuntime.jsx(
1841
+ material.Box,
1842
+ {
1843
+ sx: {
1844
+ display: "flex",
1845
+ justifyContent: "center",
1846
+ py: 3
1847
+ },
1848
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1849
+ ExpenseDonutChart,
1850
+ {
1851
+ userId,
1852
+ filters,
1853
+ selected: selectedTag,
1854
+ onCategoryClick: handleCategoryClick,
1855
+ size: chartSize,
1856
+ maxCategories: 7,
1857
+ animated: true
1858
+ }
1859
+ )
1860
+ }
1861
+ )
1862
+ ] })
1863
+ ] })
1864
+ ]
1865
+ }
1866
+ );
1867
+ }
1868
+ function ExpenseWheelContainer({
1869
+ userId,
1870
+ initialDateRangeIndex = 0,
1871
+ ...props
1872
+ }) {
1873
+ return /* @__PURE__ */ jsxRuntime.jsx(expensesFeature.ExpenseWheelProvider, { userId, initialDateRangeIndex, children: /* @__PURE__ */ jsxRuntime.jsx(ExpenseWheelContent, { userId, ...props }) });
1874
+ }
1875
+ function ExpenseFilterForm({
1876
+ filters,
1877
+ onFiltersChange,
1878
+ compact = false
1879
+ }) {
1880
+ const [localFilters, setLocalFilters] = React3.useState(filters);
1881
+ const [errors, setErrors] = React3.useState([]);
1882
+ const filterState = expensesFeature.useExpenseFilters(localFilters);
1883
+ const handleChange = (field, value) => {
1884
+ setLocalFilters((prev) => ({
1885
+ ...prev,
1886
+ [field]: value
1887
+ }));
1888
+ setErrors([]);
1889
+ };
1890
+ const handleApply = () => {
1891
+ const validation = expensesFeature.validateExpenseFilters(localFilters);
1892
+ if (!validation.isValid) {
1893
+ setErrors(validation.errors);
1894
+ return;
1895
+ }
1896
+ onFiltersChange(localFilters);
1897
+ };
1898
+ const handleClear = () => {
1899
+ const clearedFilters = {
1900
+ begin_on: void 0,
1901
+ end_on: void 0,
1902
+ threshold: void 0
1903
+ };
1904
+ setLocalFilters(clearedFilters);
1905
+ setErrors([]);
1906
+ onFiltersChange(clearedFilters);
1907
+ };
1908
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.Paper, { sx: { p: compact ? 2 : 3 }, children: [
1909
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", alignItems: "center", justifyContent: "space-between", mb: 2 }, children: [
1910
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: compact ? "subtitle1" : "h6", children: "Filter Expenses" }),
1911
+ filterState.hasActiveFilters && /* @__PURE__ */ jsxRuntime.jsx(
1912
+ material.Chip,
1913
+ {
1914
+ label: `${filterState.hasDateFilter ? "1" : "0"} + ${filterState.hasThresholdFilter ? "1" : "0"} filters`,
1915
+ size: "small",
1916
+ color: "primary"
1917
+ }
1918
+ )
1919
+ ] }),
1920
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", gap: 2, mb: 2, flexDirection: compact ? "column" : "row" }, children: [
1921
+ /* @__PURE__ */ jsxRuntime.jsx(
1922
+ material.TextField,
1923
+ {
1924
+ fullWidth: true,
1925
+ type: "date",
1926
+ label: "Start Date",
1927
+ value: localFilters.begin_on || "",
1928
+ onChange: (e) => handleChange("begin_on", e.target.value),
1929
+ InputLabelProps: { shrink: true },
1930
+ helperText: compact ? void 0 : "YYYY-MM-DD format"
1931
+ }
1932
+ ),
1933
+ /* @__PURE__ */ jsxRuntime.jsx(
1934
+ material.TextField,
1935
+ {
1936
+ fullWidth: true,
1937
+ type: "date",
1938
+ label: "End Date",
1939
+ value: localFilters.end_on || "",
1940
+ onChange: (e) => handleChange("end_on", e.target.value),
1941
+ InputLabelProps: { shrink: true },
1942
+ helperText: compact ? void 0 : "YYYY-MM-DD format"
1943
+ }
1944
+ )
1945
+ ] }),
1946
+ /* @__PURE__ */ jsxRuntime.jsx(
1947
+ material.TextField,
1948
+ {
1949
+ fullWidth: true,
1950
+ type: "number",
1951
+ label: "Minimum Amount",
1952
+ inputProps: { step: "0.01", min: "0" },
1953
+ value: localFilters.threshold || "",
1954
+ onChange: (e) => handleChange("threshold", parseFloat(e.target.value)),
1955
+ helperText: compact ? void 0 : "Show only expenses above this amount",
1956
+ sx: { mb: 2 }
1957
+ }
1958
+ ),
1959
+ filterState.dateRangeLabel && /* @__PURE__ */ jsxRuntime.jsxs(material.Alert, { severity: "info", sx: { mb: 2 }, children: [
1960
+ filterState.dateRangeLabel,
1961
+ filterState.hasThresholdFilter && ` \u2022 Min: $${localFilters.threshold}`
1962
+ ] }),
1963
+ errors.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "error", sx: { mb: 2 }, children: errors.map((error, index) => /* @__PURE__ */ jsxRuntime.jsx("div", { children: error }, index)) }),
1964
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", gap: 2 }, children: [
1965
+ /* @__PURE__ */ jsxRuntime.jsx(
1966
+ material.Button,
1967
+ {
1968
+ variant: "contained",
1969
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.FilterList, {}),
1970
+ onClick: handleApply,
1971
+ fullWidth: !compact,
1972
+ children: "Apply Filters"
1973
+ }
1974
+ ),
1975
+ /* @__PURE__ */ jsxRuntime.jsx(
1976
+ material.Button,
1977
+ {
1978
+ variant: "outlined",
1979
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.Clear, {}),
1980
+ onClick: handleClear,
1981
+ disabled: !filterState.hasActiveFilters,
1982
+ children: "Clear"
1983
+ }
1984
+ )
1985
+ ] })
1986
+ ] });
1987
+ }
1988
+ function ExpenseAdvancedFilterForm({
1989
+ filters,
1990
+ onFiltersChange,
1991
+ availableCategories = [],
1992
+ availableMerchants = [],
1993
+ compact = false,
1994
+ showPresets = true
1995
+ }) {
1996
+ const [localFilters, setLocalFilters] = React3.useState(filters);
1997
+ const [amountRange, setAmountRange] = React3.useState([0, 1e3]);
1998
+ React3.useEffect(() => {
1999
+ if (localFilters.minAmount !== void 0 || localFilters.maxAmount !== void 0) {
2000
+ setAmountRange([
2001
+ localFilters.minAmount || 0,
2002
+ localFilters.maxAmount || 1e3
2003
+ ]);
2004
+ }
2005
+ }, [localFilters.minAmount, localFilters.maxAmount]);
2006
+ const handleChange = (field, value) => {
2007
+ setLocalFilters((prev) => ({
2008
+ ...prev,
2009
+ [field]: value
2010
+ }));
2011
+ };
2012
+ const handleAmountRangeChange = (_event, newValue) => {
2013
+ if (Array.isArray(newValue)) {
2014
+ setAmountRange(newValue);
2015
+ setLocalFilters((prev) => ({
2016
+ ...prev,
2017
+ minAmount: newValue[0],
2018
+ maxAmount: newValue[1]
2019
+ }));
2020
+ }
2021
+ };
2022
+ const handleApply = () => {
2023
+ onFiltersChange(localFilters);
2024
+ };
2025
+ const handleClear = () => {
2026
+ const clearedFilters = {
2027
+ begin_on: void 0,
2028
+ end_on: void 0,
2029
+ threshold: void 0,
2030
+ categories: void 0,
2031
+ merchants: void 0,
2032
+ minAmount: void 0,
2033
+ maxAmount: void 0,
2034
+ amountComparison: "between",
2035
+ type: "all",
2036
+ sortBy: "date",
2037
+ sortDirection: "desc"
2038
+ };
2039
+ setLocalFilters(clearedFilters);
2040
+ setAmountRange([0, 1e3]);
2041
+ onFiltersChange(clearedFilters);
2042
+ };
2043
+ const handlePreset = (preset) => {
2044
+ const now = /* @__PURE__ */ new Date();
2045
+ let begin_on;
2046
+ let end_on;
2047
+ switch (preset) {
2048
+ case "this-month":
2049
+ begin_on = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().split("T")[0];
2050
+ end_on = new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString().split("T")[0];
2051
+ break;
2052
+ case "last-month":
2053
+ begin_on = new Date(now.getFullYear(), now.getMonth() - 1, 1).toISOString().split("T")[0];
2054
+ end_on = new Date(now.getFullYear(), now.getMonth(), 0).toISOString().split("T")[0];
2055
+ break;
2056
+ case "this-year":
2057
+ begin_on = new Date(now.getFullYear(), 0, 1).toISOString().split("T")[0];
2058
+ end_on = new Date(now.getFullYear(), 11, 31).toISOString().split("T")[0];
2059
+ break;
2060
+ case "last-30-days":
2061
+ begin_on = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3).toISOString().split("T")[0];
2062
+ end_on = now.toISOString().split("T")[0];
2063
+ break;
2064
+ }
2065
+ setLocalFilters((prev) => ({ ...prev, begin_on, end_on }));
2066
+ };
2067
+ const activeFilterCount = [
2068
+ localFilters.begin_on || localFilters.end_on,
2069
+ localFilters.categories && localFilters.categories.length > 0,
2070
+ localFilters.merchants && localFilters.merchants.length > 0,
2071
+ localFilters.minAmount !== void 0 || localFilters.maxAmount !== void 0,
2072
+ localFilters.type && localFilters.type !== "all"
2073
+ ].filter(Boolean).length;
2074
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.Paper, { sx: { p: compact ? 2 : 3 }, children: [
2075
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", alignItems: "center", justifyContent: "space-between", mb: 2 }, children: [
2076
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: compact ? "subtitle1" : "h6", children: "Advanced Filters" }),
2077
+ activeFilterCount > 0 && /* @__PURE__ */ jsxRuntime.jsx(
2078
+ material.Chip,
2079
+ {
2080
+ label: `${activeFilterCount} filter${activeFilterCount !== 1 ? "s" : ""}`,
2081
+ size: "small",
2082
+ color: "primary",
2083
+ icon: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.FilterList, {})
2084
+ }
2085
+ )
2086
+ ] }),
2087
+ showPresets && /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { mb: 2 }, children: [
2088
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", gutterBottom: true, display: "block", children: "Quick Date Presets:" }),
2089
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", gap: 1, flexWrap: "wrap" }, children: [
2090
+ /* @__PURE__ */ jsxRuntime.jsx(
2091
+ material.Chip,
2092
+ {
2093
+ label: "This Month",
2094
+ size: "small",
2095
+ onClick: () => handlePreset("this-month"),
2096
+ variant: "outlined"
2097
+ }
2098
+ ),
2099
+ /* @__PURE__ */ jsxRuntime.jsx(
2100
+ material.Chip,
2101
+ {
2102
+ label: "Last Month",
2103
+ size: "small",
2104
+ onClick: () => handlePreset("last-month"),
2105
+ variant: "outlined"
2106
+ }
2107
+ ),
2108
+ /* @__PURE__ */ jsxRuntime.jsx(
2109
+ material.Chip,
2110
+ {
2111
+ label: "This Year",
2112
+ size: "small",
2113
+ onClick: () => handlePreset("this-year"),
2114
+ variant: "outlined"
2115
+ }
2116
+ ),
2117
+ /* @__PURE__ */ jsxRuntime.jsx(
2118
+ material.Chip,
2119
+ {
2120
+ label: "Last 30 Days",
2121
+ size: "small",
2122
+ onClick: () => handlePreset("last-30-days"),
2123
+ variant: "outlined"
2124
+ }
2125
+ )
2126
+ ] })
2127
+ ] }),
2128
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Accordion, { defaultExpanded: !compact, children: [
2129
+ /* @__PURE__ */ jsxRuntime.jsx(material.AccordionSummary, { expandIcon: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.ExpandMore, {}), children: /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", children: "Date Range" }) }),
2130
+ /* @__PURE__ */ jsxRuntime.jsx(material.AccordionDetails, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", gap: 2, flexDirection: compact ? "column" : "row" }, children: [
2131
+ /* @__PURE__ */ jsxRuntime.jsx(
2132
+ material.TextField,
2133
+ {
2134
+ fullWidth: true,
2135
+ type: "date",
2136
+ label: "Start Date",
2137
+ value: localFilters.begin_on || "",
2138
+ onChange: (e) => handleChange("begin_on", e.target.value || void 0),
2139
+ InputLabelProps: { shrink: true }
2140
+ }
2141
+ ),
2142
+ /* @__PURE__ */ jsxRuntime.jsx(
2143
+ material.TextField,
2144
+ {
2145
+ fullWidth: true,
2146
+ type: "date",
2147
+ label: "End Date",
2148
+ value: localFilters.end_on || "",
2149
+ onChange: (e) => handleChange("end_on", e.target.value || void 0),
2150
+ InputLabelProps: { shrink: true }
2151
+ }
2152
+ )
2153
+ ] }) })
2154
+ ] }),
2155
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Accordion, { children: [
2156
+ /* @__PURE__ */ jsxRuntime.jsx(material.AccordionSummary, { expandIcon: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.ExpandMore, {}), children: /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "subtitle2", children: [
2157
+ "Categories",
2158
+ localFilters.categories && localFilters.categories.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
2159
+ material.Chip,
2160
+ {
2161
+ label: localFilters.categories.length,
2162
+ size: "small",
2163
+ sx: { ml: 1 }
2164
+ }
2165
+ )
2166
+ ] }) }),
2167
+ /* @__PURE__ */ jsxRuntime.jsx(material.AccordionDetails, { children: /* @__PURE__ */ jsxRuntime.jsx(
2168
+ material.Autocomplete,
2169
+ {
2170
+ multiple: true,
2171
+ freeSolo: true,
2172
+ options: availableCategories,
2173
+ value: localFilters.categories || [],
2174
+ onChange: (_e, value) => handleChange("categories", value.length > 0 ? value : void 0),
2175
+ renderTags: (value, getTagProps) => value.map((option, index) => /* @__PURE__ */ React3.createElement(material.Chip, { label: option, ...getTagProps({ index }), key: option, size: "small" })),
2176
+ renderInput: (params) => /* @__PURE__ */ jsxRuntime.jsx(material.TextField, { ...params, label: "Filter by Categories", placeholder: "Add categories..." })
2177
+ }
2178
+ ) })
2179
+ ] }),
2180
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Accordion, { children: [
2181
+ /* @__PURE__ */ jsxRuntime.jsx(material.AccordionSummary, { expandIcon: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.ExpandMore, {}), children: /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "subtitle2", children: [
2182
+ "Merchants",
2183
+ localFilters.merchants && localFilters.merchants.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
2184
+ material.Chip,
2185
+ {
2186
+ label: localFilters.merchants.length,
2187
+ size: "small",
2188
+ sx: { ml: 1 }
2189
+ }
2190
+ )
2191
+ ] }) }),
2192
+ /* @__PURE__ */ jsxRuntime.jsx(material.AccordionDetails, { children: /* @__PURE__ */ jsxRuntime.jsx(
2193
+ material.Autocomplete,
2194
+ {
2195
+ multiple: true,
2196
+ freeSolo: true,
2197
+ options: availableMerchants,
2198
+ value: localFilters.merchants || [],
2199
+ onChange: (_e, value) => handleChange("merchants", value.length > 0 ? value : void 0),
2200
+ renderTags: (value, getTagProps) => value.map((option, index) => /* @__PURE__ */ React3.createElement(material.Chip, { label: option, ...getTagProps({ index }), key: option, size: "small" })),
2201
+ renderInput: (params) => /* @__PURE__ */ jsxRuntime.jsx(material.TextField, { ...params, label: "Filter by Merchants", placeholder: "Add merchants..." })
2202
+ }
2203
+ ) })
2204
+ ] }),
2205
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Accordion, { children: [
2206
+ /* @__PURE__ */ jsxRuntime.jsx(material.AccordionSummary, { expandIcon: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.ExpandMore, {}), children: /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", children: "Amount Range" }) }),
2207
+ /* @__PURE__ */ jsxRuntime.jsxs(material.AccordionDetails, { children: [
2208
+ /* @__PURE__ */ jsxRuntime.jsxs(material.FormControl, { fullWidth: true, sx: { mb: 2 }, children: [
2209
+ /* @__PURE__ */ jsxRuntime.jsx(material.InputLabel, { children: "Comparison Type" }),
2210
+ /* @__PURE__ */ jsxRuntime.jsxs(
2211
+ material.Select,
2212
+ {
2213
+ value: localFilters.amountComparison || "between",
2214
+ label: "Comparison Type",
2215
+ onChange: (e) => handleChange("amountComparison", e.target.value),
2216
+ children: [
2217
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "between", children: "Between" }),
2218
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "above", children: "Above" }),
2219
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "below", children: "Below" }),
2220
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "exact", children: "Exact" })
2221
+ ]
2222
+ }
2223
+ )
2224
+ ] }),
2225
+ localFilters.amountComparison === "between" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2226
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "caption", gutterBottom: true, children: [
2227
+ "Amount: $",
2228
+ amountRange[0],
2229
+ " - $",
2230
+ amountRange[1]
2231
+ ] }),
2232
+ /* @__PURE__ */ jsxRuntime.jsx(
2233
+ material.Slider,
2234
+ {
2235
+ value: amountRange,
2236
+ onChange: handleAmountRangeChange,
2237
+ valueLabelDisplay: "auto",
2238
+ min: 0,
2239
+ max: 1e3,
2240
+ step: 10,
2241
+ marks: [
2242
+ { value: 0, label: "$0" },
2243
+ { value: 500, label: "$500" },
2244
+ { value: 1e3, label: "$1000" }
2245
+ ]
2246
+ }
2247
+ )
2248
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(
2249
+ material.TextField,
2250
+ {
2251
+ fullWidth: true,
2252
+ type: "number",
2253
+ label: "Amount",
2254
+ inputProps: { step: "0.01", min: "0" },
2255
+ value: localFilters.minAmount || "",
2256
+ onChange: (e) => handleChange("minAmount", parseFloat(e.target.value) || void 0)
2257
+ }
2258
+ )
2259
+ ] })
2260
+ ] }),
2261
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Accordion, { children: [
2262
+ /* @__PURE__ */ jsxRuntime.jsx(material.AccordionSummary, { expandIcon: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.ExpandMore, {}), children: /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", children: "Type & Sorting" }) }),
2263
+ /* @__PURE__ */ jsxRuntime.jsxs(material.AccordionDetails, { children: [
2264
+ /* @__PURE__ */ jsxRuntime.jsxs(material.FormControl, { fullWidth: true, sx: { mb: 2 }, children: [
2265
+ /* @__PURE__ */ jsxRuntime.jsx(material.InputLabel, { children: "Expense Type" }),
2266
+ /* @__PURE__ */ jsxRuntime.jsxs(
2267
+ material.Select,
2268
+ {
2269
+ value: localFilters.type || "all",
2270
+ label: "Expense Type",
2271
+ onChange: (e) => handleChange("type", e.target.value),
2272
+ children: [
2273
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "all", children: "All Expenses" }),
2274
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "recurring", children: "Recurring Only" }),
2275
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "one-time", children: "One-Time Only" })
2276
+ ]
2277
+ }
2278
+ )
2279
+ ] }),
2280
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", gap: 2 }, children: [
2281
+ /* @__PURE__ */ jsxRuntime.jsxs(material.FormControl, { fullWidth: true, children: [
2282
+ /* @__PURE__ */ jsxRuntime.jsx(material.InputLabel, { children: "Sort By" }),
2283
+ /* @__PURE__ */ jsxRuntime.jsxs(
2284
+ material.Select,
2285
+ {
2286
+ value: localFilters.sortBy || "date",
2287
+ label: "Sort By",
2288
+ onChange: (e) => handleChange("sortBy", e.target.value),
2289
+ children: [
2290
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "date", children: "Date" }),
2291
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "amount", children: "Amount" }),
2292
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "merchant", children: "Merchant" })
2293
+ ]
2294
+ }
2295
+ )
2296
+ ] }),
2297
+ /* @__PURE__ */ jsxRuntime.jsxs(material.FormControl, { fullWidth: true, children: [
2298
+ /* @__PURE__ */ jsxRuntime.jsx(material.InputLabel, { children: "Direction" }),
2299
+ /* @__PURE__ */ jsxRuntime.jsxs(
2300
+ material.Select,
2301
+ {
2302
+ value: localFilters.sortDirection || "desc",
2303
+ label: "Direction",
2304
+ onChange: (e) => handleChange("sortDirection", e.target.value),
2305
+ children: [
2306
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "asc", children: /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", alignItems: "center", gap: 1 }, children: [
2307
+ /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.TrendingUp, { fontSize: "small" }),
2308
+ " Ascending"
2309
+ ] }) }),
2310
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "desc", children: /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", alignItems: "center", gap: 1 }, children: [
2311
+ /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.TrendingDown, { fontSize: "small" }),
2312
+ " Descending"
2313
+ ] }) })
2314
+ ]
2315
+ }
2316
+ )
2317
+ ] })
2318
+ ] })
2319
+ ] })
2320
+ ] }),
2321
+ activeFilterCount > 0 && /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "info", sx: { mt: 2 }, children: /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "body2", children: [
2322
+ localFilters.begin_on && localFilters.end_on && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2323
+ "Dates: ",
2324
+ localFilters.begin_on,
2325
+ " to ",
2326
+ localFilters.end_on
2327
+ ] }),
2328
+ localFilters.categories && localFilters.categories.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2329
+ " \u2022 Categories: ",
2330
+ localFilters.categories.length
2331
+ ] }),
2332
+ localFilters.merchants && localFilters.merchants.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2333
+ " \u2022 Merchants: ",
2334
+ localFilters.merchants.length
2335
+ ] }),
2336
+ (localFilters.minAmount !== void 0 || localFilters.maxAmount !== void 0) && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2337
+ " \u2022 Amount: $",
2338
+ localFilters.minAmount || 0,
2339
+ " - $",
2340
+ localFilters.maxAmount || 1e3
2341
+ ] })
2342
+ ] }) }),
2343
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { mt: 3, display: "flex", gap: 2 }, children: [
2344
+ /* @__PURE__ */ jsxRuntime.jsx(
2345
+ material.Button,
2346
+ {
2347
+ variant: "contained",
2348
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.FilterList, {}),
2349
+ onClick: handleApply,
2350
+ fullWidth: !compact,
2351
+ children: "Apply Filters"
2352
+ }
2353
+ ),
2354
+ /* @__PURE__ */ jsxRuntime.jsx(
2355
+ material.Button,
2356
+ {
2357
+ variant: "outlined",
2358
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.Clear, {}),
2359
+ onClick: handleClear,
2360
+ disabled: activeFilterCount === 0,
2361
+ children: "Clear All"
2362
+ }
2363
+ )
2364
+ ] })
2365
+ ] });
2366
+ }
2367
+ function ExpenseExportButton({
2368
+ expenses,
2369
+ dateRangeLabel = "expenses",
2370
+ currencySymbol = "$",
2371
+ variant = "outlined",
2372
+ size = "medium",
2373
+ disabled = false
2374
+ }) {
2375
+ const [anchorEl, setAnchorEl] = React3.useState(null);
2376
+ const [showSuccess, setShowSuccess] = React3.useState(false);
2377
+ const [showError, setShowError] = React3.useState(false);
2378
+ const [errorMessage, setErrorMessage] = React3.useState("");
2379
+ const open = Boolean(anchorEl);
2380
+ const handleClick = (event) => {
2381
+ setAnchorEl(event.currentTarget);
2382
+ };
2383
+ const handleClose = () => {
2384
+ setAnchorEl(null);
2385
+ };
2386
+ const generateFilename = (extension) => {
2387
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2388
+ const sanitizedLabel = dateRangeLabel.replace(/[^a-z0-9]/gi, "_").toLowerCase();
2389
+ return `expenses_${sanitizedLabel}_${timestamp}.${extension}`;
2390
+ };
2391
+ const formatAmount = (amount) => {
2392
+ return `${currencySymbol}${amount}`;
2393
+ };
2394
+ const calculateTotal = () => {
2395
+ return expenses.reduce((sum, expense) => sum + parseFloat(expense.amount), 0);
2396
+ };
2397
+ const exportToCSV = () => {
2398
+ try {
2399
+ const headers = ["Tag", "Amount", "Percentage"];
2400
+ const total = calculateTotal();
2401
+ const rows = expenses.map((expense) => {
2402
+ const amount = parseFloat(expense.amount);
2403
+ const percentage = total > 0 ? (amount / total * 100).toFixed(2) : "0.00";
2404
+ return [
2405
+ expense.tag,
2406
+ formatAmount(expense.amount),
2407
+ `${percentage}%`
2408
+ ];
2409
+ });
2410
+ rows.push(["Total", formatAmount(total.toFixed(2)), "100.00%"]);
2411
+ const csvContent = [
2412
+ headers.join(","),
2413
+ ...rows.map((row) => row.join(","))
2414
+ ].join("\n");
2415
+ const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
2416
+ const link = document.createElement("a");
2417
+ const url = URL.createObjectURL(blob);
2418
+ link.setAttribute("href", url);
2419
+ link.setAttribute("download", generateFilename("csv"));
2420
+ link.style.visibility = "hidden";
2421
+ document.body.appendChild(link);
2422
+ link.click();
2423
+ document.body.removeChild(link);
2424
+ setShowSuccess(true);
2425
+ handleClose();
2426
+ } catch (error) {
2427
+ setErrorMessage(error instanceof Error ? error.message : "Failed to export CSV");
2428
+ setShowError(true);
2429
+ }
2430
+ };
2431
+ const exportToExcel = () => {
2432
+ try {
2433
+ const headers = ["Tag", "Amount", "Percentage"];
2434
+ const total = calculateTotal();
2435
+ const rows = expenses.map((expense) => {
2436
+ const amount = parseFloat(expense.amount);
2437
+ const percentage = total > 0 ? (amount / total * 100).toFixed(2) : "0.00";
2438
+ return [
2439
+ expense.tag,
2440
+ formatAmount(expense.amount),
2441
+ `${percentage}%`
2442
+ ];
2443
+ });
2444
+ rows.push(["Total", formatAmount(total.toFixed(2)), "100.00%"]);
2445
+ const tsvContent = [
2446
+ headers.join(" "),
2447
+ ...rows.map((row) => row.join(" "))
2448
+ ].join("\n");
2449
+ const blob = new Blob([tsvContent], { type: "application/vnd.ms-excel" });
2450
+ const link = document.createElement("a");
2451
+ const url = URL.createObjectURL(blob);
2452
+ link.setAttribute("href", url);
2453
+ link.setAttribute("download", generateFilename("xls"));
2454
+ link.style.visibility = "hidden";
2455
+ document.body.appendChild(link);
2456
+ link.click();
2457
+ document.body.removeChild(link);
2458
+ setShowSuccess(true);
2459
+ handleClose();
2460
+ } catch (error) {
2461
+ setErrorMessage(error instanceof Error ? error.message : "Failed to export Excel");
2462
+ setShowError(true);
2463
+ }
2464
+ };
2465
+ const exportToPDF = () => {
2466
+ try {
2467
+ const total = calculateTotal();
2468
+ const htmlContent = `
2469
+ <!DOCTYPE html>
2470
+ <html>
2471
+ <head>
2472
+ <title>Expense Report - ${dateRangeLabel}</title>
2473
+ <style>
2474
+ body { font-family: Arial, sans-serif; margin: 40px; }
2475
+ h1 { color: #333; }
2476
+ table { width: 100%; border-collapse: collapse; margin-top: 20px; }
2477
+ th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
2478
+ th { background-color: #f5f5f5; font-weight: bold; }
2479
+ .total { font-weight: bold; background-color: #f9f9f9; }
2480
+ .right { text-align: right; }
2481
+ </style>
2482
+ </head>
2483
+ <body>
2484
+ <h1>Expense Report</h1>
2485
+ <p><strong>Period:</strong> ${dateRangeLabel}</p>
2486
+ <p><strong>Generated:</strong> ${(/* @__PURE__ */ new Date()).toLocaleDateString()}</p>
2487
+ <table>
2488
+ <thead>
2489
+ <tr>
2490
+ <th>Tag</th>
2491
+ <th class="right">Amount</th>
2492
+ <th class="right">Percentage</th>
2493
+ </tr>
2494
+ </thead>
2495
+ <tbody>
2496
+ ${expenses.map((expense) => {
2497
+ const amount = parseFloat(expense.amount);
2498
+ const percentage = total > 0 ? (amount / total * 100).toFixed(2) : "0.00";
2499
+ return `
2500
+ <tr>
2501
+ <td>${expense.tag}</td>
2502
+ <td class="right">${formatAmount(expense.amount)}</td>
2503
+ <td class="right">${percentage}%</td>
2504
+ </tr>
2505
+ `;
2506
+ }).join("")}
2507
+ <tr class="total">
2508
+ <td>Total</td>
2509
+ <td class="right">${formatAmount(total.toFixed(2))}</td>
2510
+ <td class="right">100.00%</td>
2511
+ </tr>
2512
+ </tbody>
2513
+ </table>
2514
+ </body>
2515
+ </html>
2516
+ `;
2517
+ const printWindow = window.open("", "_blank");
2518
+ if (printWindow) {
2519
+ printWindow.document.write(htmlContent);
2520
+ printWindow.document.close();
2521
+ printWindow.focus();
2522
+ setTimeout(() => {
2523
+ printWindow.print();
2524
+ }, 250);
2525
+ setShowSuccess(true);
2526
+ handleClose();
2527
+ } else {
2528
+ throw new Error("Could not open print window. Please allow popups.");
2529
+ }
2530
+ } catch (error) {
2531
+ setErrorMessage(error instanceof Error ? error.message : "Failed to export PDF");
2532
+ setShowError(true);
2533
+ }
2534
+ };
2535
+ const isEmpty = expenses.length === 0;
2536
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2537
+ /* @__PURE__ */ jsxRuntime.jsx(
2538
+ material.Button,
2539
+ {
2540
+ variant,
2541
+ size,
2542
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.Download, {}),
2543
+ onClick: handleClick,
2544
+ disabled: disabled || isEmpty,
2545
+ "aria-label": "Export expenses",
2546
+ "aria-controls": open ? "export-menu" : void 0,
2547
+ "aria-haspopup": "true",
2548
+ "aria-expanded": open ? "true" : void 0,
2549
+ children: "Export"
2550
+ }
2551
+ ),
2552
+ /* @__PURE__ */ jsxRuntime.jsxs(
2553
+ material.Menu,
2554
+ {
2555
+ id: "export-menu",
2556
+ anchorEl,
2557
+ open,
2558
+ onClose: handleClose,
2559
+ MenuListProps: {
2560
+ "aria-labelledby": "export-button"
2561
+ },
2562
+ children: [
2563
+ /* @__PURE__ */ jsxRuntime.jsxs(material.MenuItem, { onClick: exportToCSV, children: [
2564
+ /* @__PURE__ */ jsxRuntime.jsx(material.ListItemIcon, { children: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.FileDownload, { fontSize: "small" }) }),
2565
+ /* @__PURE__ */ jsxRuntime.jsx(material.ListItemText, { children: "Export as CSV" })
2566
+ ] }),
2567
+ /* @__PURE__ */ jsxRuntime.jsxs(material.MenuItem, { onClick: exportToExcel, children: [
2568
+ /* @__PURE__ */ jsxRuntime.jsx(material.ListItemIcon, { children: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.TableChart, { fontSize: "small" }) }),
2569
+ /* @__PURE__ */ jsxRuntime.jsx(material.ListItemText, { children: "Export as Excel" })
2570
+ ] }),
2571
+ /* @__PURE__ */ jsxRuntime.jsxs(material.MenuItem, { onClick: exportToPDF, children: [
2572
+ /* @__PURE__ */ jsxRuntime.jsx(material.ListItemIcon, { children: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.PictureAsPdf, { fontSize: "small" }) }),
2573
+ /* @__PURE__ */ jsxRuntime.jsx(material.ListItemText, { children: "Export as PDF" })
2574
+ ] })
2575
+ ]
2576
+ }
2577
+ ),
2578
+ /* @__PURE__ */ jsxRuntime.jsx(
2579
+ material.Snackbar,
2580
+ {
2581
+ open: showSuccess,
2582
+ autoHideDuration: 3e3,
2583
+ onClose: () => setShowSuccess(false),
2584
+ anchorOrigin: { vertical: "bottom", horizontal: "center" },
2585
+ children: /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "success", onClose: () => setShowSuccess(false), children: "Export completed successfully!" })
2586
+ }
2587
+ ),
2588
+ /* @__PURE__ */ jsxRuntime.jsx(
2589
+ material.Snackbar,
2590
+ {
2591
+ open: showError,
2592
+ autoHideDuration: 5e3,
2593
+ onClose: () => setShowError(false),
2594
+ anchorOrigin: { vertical: "bottom", horizontal: "center" },
2595
+ children: /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "error", onClose: () => setShowError(false), children: errorMessage || "Export failed" })
2596
+ }
2597
+ )
2598
+ ] });
2599
+ }
2600
+ function ExpenseBreakdownItem({
2601
+ name,
2602
+ value,
2603
+ percent,
2604
+ icon,
2605
+ color,
2606
+ onClick,
2607
+ clickable = !!onClick
2608
+ }) {
2609
+ const theme = material.useTheme();
2610
+ const displayName = name || "Untagged";
2611
+ const iconColor = color || theme.palette.text.primary;
2612
+ const content = /* @__PURE__ */ jsxRuntime.jsxs(
2613
+ material.Box,
2614
+ {
2615
+ sx: {
2616
+ display: "flex",
2617
+ alignItems: "center",
2618
+ width: "100%",
2619
+ padding: theme.spacing(1),
2620
+ gap: theme.spacing(1.5)
2621
+ },
2622
+ children: [
2623
+ /* @__PURE__ */ jsxRuntime.jsx(
2624
+ material.Box,
2625
+ {
2626
+ sx: {
2627
+ display: "flex",
2628
+ alignItems: "center",
2629
+ justifyContent: "center",
2630
+ minWidth: 32
2631
+ },
2632
+ children: icon || /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.LocalOffer, { sx: { color: iconColor, fontSize: 20 } })
2633
+ }
2634
+ ),
2635
+ /* @__PURE__ */ jsxRuntime.jsxs(
2636
+ material.Box,
2637
+ {
2638
+ sx: {
2639
+ display: "flex",
2640
+ flexDirection: "column",
2641
+ flex: 1,
2642
+ minWidth: 0
2643
+ // Allow text truncation
2644
+ },
2645
+ children: [
2646
+ /* @__PURE__ */ jsxRuntime.jsx(
2647
+ material.Typography,
2648
+ {
2649
+ variant: "body2",
2650
+ sx: {
2651
+ fontWeight: 500,
2652
+ overflow: "hidden",
2653
+ textOverflow: "ellipsis",
2654
+ whiteSpace: "nowrap"
2655
+ },
2656
+ children: displayName
2657
+ }
2658
+ ),
2659
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", children: value })
2660
+ ]
2661
+ }
2662
+ ),
2663
+ /* @__PURE__ */ jsxRuntime.jsx(
2664
+ material.Box,
2665
+ {
2666
+ sx: {
2667
+ display: "flex",
2668
+ alignItems: "center",
2669
+ minWidth: 50,
2670
+ justifyContent: "flex-end"
2671
+ },
2672
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
2673
+ material.Typography,
2674
+ {
2675
+ variant: "body2",
2676
+ sx: {
2677
+ fontWeight: 600,
2678
+ color: theme.palette.primary.main
2679
+ },
2680
+ children: [
2681
+ percent.toFixed(1),
2682
+ "%"
2683
+ ]
2684
+ }
2685
+ )
2686
+ }
2687
+ )
2688
+ ]
2689
+ }
2690
+ );
2691
+ if (clickable && onClick) {
2692
+ return /* @__PURE__ */ jsxRuntime.jsx(
2693
+ material.ButtonBase,
2694
+ {
2695
+ "data-testid": "expense-breakdown-item",
2696
+ onClick,
2697
+ sx: {
2698
+ width: "100%",
2699
+ borderRadius: 1,
2700
+ transition: "background-color 0.2s",
2701
+ "&:hover": {
2702
+ backgroundColor: theme.palette.action.hover
2703
+ },
2704
+ "&:active": {
2705
+ backgroundColor: theme.palette.action.selected
2706
+ }
2707
+ },
2708
+ children: content
2709
+ }
2710
+ );
2711
+ }
2712
+ return /* @__PURE__ */ jsxRuntime.jsx(material.Box, { "data-testid": "expense-breakdown-item", sx: { borderRadius: 1 }, children: content });
2713
+ }
2714
+ function ExpenseAnalyzer({
2715
+ loading = false,
2716
+ dateRangeSelector,
2717
+ trendData,
2718
+ detailData,
2719
+ transactionCount,
2720
+ transactionSum,
2721
+ transactionType = "Debit",
2722
+ onTransactionTypeToggle,
2723
+ tagBreakdown,
2724
+ onViewAllTransactions,
2725
+ onTagClick,
2726
+ currencyFormatter = (value) => `$${value.toFixed(2)}`,
2727
+ emptyMessage = "No expense data available for the selected period"
2728
+ }) {
2729
+ const theme = material.useTheme();
2730
+ if (loading) {
2731
+ return /* @__PURE__ */ jsxRuntime.jsx(ExpenseLoadingState, {});
2732
+ }
2733
+ const isEmpty = trendData.length === 0 && detailData.length === 0 && tagBreakdown.length === 0;
2734
+ if (isEmpty) {
2735
+ return /* @__PURE__ */ jsxRuntime.jsx(ExpenseEmptyState, { message: emptyMessage });
2736
+ }
2737
+ return /* @__PURE__ */ jsxRuntime.jsx(
2738
+ material.Box,
2739
+ {
2740
+ "data-testid": "expense-analyzer",
2741
+ sx: {
2742
+ padding: { xs: 0, md: theme.spacing(2) }
2743
+ },
2744
+ children: /* @__PURE__ */ jsxRuntime.jsxs(material.Grid, { container: true, spacing: 2, children: [
2745
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Grid, { size: { xs: 12, sm: 8 }, children: [
2746
+ dateRangeSelector && /* @__PURE__ */ jsxRuntime.jsx(material.Box, { sx: { mb: 2 }, children: dateRangeSelector }),
2747
+ trendData.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(material.Paper, { elevation: 1, sx: { p: 2, mb: 2 }, children: /* @__PURE__ */ jsxRuntime.jsx(ExpenseLineChart, { data: [trendData], height: 200 }) }),
2748
+ /* @__PURE__ */ jsxRuntime.jsxs(
2749
+ material.Box,
2750
+ {
2751
+ sx: {
2752
+ display: "flex",
2753
+ justifyContent: "space-between",
2754
+ alignItems: "center",
2755
+ mb: 2,
2756
+ px: 1
2757
+ },
2758
+ children: [
2759
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { children: [
2760
+ /* @__PURE__ */ jsxRuntime.jsxs(
2761
+ material.Typography,
2762
+ {
2763
+ variant: "body2",
2764
+ component: "span",
2765
+ sx: {
2766
+ mr: 1,
2767
+ cursor: onTransactionTypeToggle ? "pointer" : "default",
2768
+ "&:hover": onTransactionTypeToggle ? { textDecoration: "underline" } : void 0
2769
+ },
2770
+ onClick: onTransactionTypeToggle,
2771
+ children: [
2772
+ transactionCount,
2773
+ " ",
2774
+ transactionType,
2775
+ transactionCount !== 1 ? "s" : ""
2776
+ ]
2777
+ }
2778
+ ),
2779
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", component: "span", sx: { fontWeight: 600 }, children: currencyFormatter(transactionSum) })
2780
+ ] }),
2781
+ onViewAllTransactions && /* @__PURE__ */ jsxRuntime.jsx(
2782
+ material.Button,
2783
+ {
2784
+ variant: "text",
2785
+ size: "small",
2786
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.Visibility, {}),
2787
+ onClick: onViewAllTransactions,
2788
+ children: "View All"
2789
+ }
2790
+ )
2791
+ ]
2792
+ }
2793
+ ),
2794
+ detailData.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(material.Paper, { elevation: 1, sx: { p: 2 }, children: /* @__PURE__ */ jsxRuntime.jsx(ExpenseLineChart, { data: [detailData], height: 300 }) })
2795
+ ] }),
2796
+ /* @__PURE__ */ jsxRuntime.jsx(material.Grid, { size: { xs: 12, sm: 4 }, children: /* @__PURE__ */ jsxRuntime.jsx(
2797
+ material.Paper,
2798
+ {
2799
+ elevation: 1,
2800
+ sx: {
2801
+ p: 1,
2802
+ maxHeight: { sm: 600 },
2803
+ overflowY: "auto"
2804
+ },
2805
+ children: tagBreakdown.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(material.Box, { children: tagBreakdown.map((tag, index) => /* @__PURE__ */ jsxRuntime.jsx(
2806
+ ExpenseBreakdownItem,
2807
+ {
2808
+ name: tag.name,
2809
+ value: currencyFormatter(tag.value),
2810
+ percent: tag.percent,
2811
+ color: tag.color,
2812
+ icon: tag.icon,
2813
+ onClick: onTagClick ? () => onTagClick(tag) : void 0
2814
+ },
2815
+ tag.name || `untagged-${index}`
2816
+ )) }) : /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", sx: { p: 2, textAlign: "center" }, children: "No tag data available" })
2817
+ }
2818
+ ) })
2819
+ ] })
2820
+ }
2821
+ );
2822
+ }
2823
+ var defaultCurrencyFormatter = (value) => {
2824
+ return new Intl.NumberFormat("en-US", {
2825
+ style: "currency",
2826
+ currency: "USD",
2827
+ minimumFractionDigits: 2,
2828
+ maximumFractionDigits: 2
2829
+ }).format(value);
2830
+ };
2831
+ function ExpenseCompareCard({
2832
+ title = "Compare Categories",
2833
+ firstTag,
2834
+ secondTag,
2835
+ dateRangeLabel,
2836
+ onViewDetails,
2837
+ currencyFormatter = defaultCurrencyFormatter,
2838
+ showTrends = true
2839
+ }) {
2840
+ const theme = material.useTheme();
2841
+ const renderTrendIcon = (trend) => {
2842
+ switch (trend) {
2843
+ case "up":
2844
+ return /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.TrendingUp, { fontSize: "small", color: "error" });
2845
+ case "down":
2846
+ return /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.TrendingDown, { fontSize: "small", color: "success" });
2847
+ case "flat":
2848
+ default:
2849
+ return /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.TrendingFlat, { fontSize: "small", color: "action" });
2850
+ }
2851
+ };
2852
+ const renderTagColumn = (tag, isPrimary) => {
2853
+ const color = tag.color || (isPrimary ? theme.palette.primary.dark : theme.palette.primary.light);
2854
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2855
+ material.Box,
2856
+ {
2857
+ sx: {
2858
+ flex: 1,
2859
+ display: "flex",
2860
+ flexDirection: "column",
2861
+ alignItems: "center",
2862
+ gap: 1,
2863
+ p: 2,
2864
+ borderRadius: 1,
2865
+ bgcolor: isPrimary ? "primary.50" : "secondary.50"
2866
+ },
2867
+ children: [
2868
+ /* @__PURE__ */ jsxRuntime.jsx(
2869
+ material.Chip,
2870
+ {
2871
+ label: tag.name || "Untagged",
2872
+ sx: {
2873
+ bgcolor: color,
2874
+ color: "white",
2875
+ fontWeight: "medium"
2876
+ }
2877
+ }
2878
+ ),
2879
+ /* @__PURE__ */ jsxRuntime.jsx(
2880
+ material.Typography,
2881
+ {
2882
+ variant: "h4",
2883
+ sx: {
2884
+ fontWeight: 600,
2885
+ color
2886
+ },
2887
+ children: currencyFormatter(tag.amount)
2888
+ }
2889
+ ),
2890
+ /* @__PURE__ */ jsxRuntime.jsxs(
2891
+ material.Typography,
2892
+ {
2893
+ variant: "body2",
2894
+ color: "text.secondary",
2895
+ children: [
2896
+ tag.percent.toFixed(1),
2897
+ "% of total"
2898
+ ]
2899
+ }
2900
+ ),
2901
+ showTrends && tag.trend && /* @__PURE__ */ jsxRuntime.jsxs(
2902
+ material.Box,
2903
+ {
2904
+ sx: {
2905
+ display: "flex",
2906
+ alignItems: "center",
2907
+ gap: 0.5
2908
+ },
2909
+ children: [
2910
+ renderTrendIcon(tag.trend),
2911
+ tag.trendPercent !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs(
2912
+ material.Typography,
2913
+ {
2914
+ variant: "caption",
2915
+ sx: {
2916
+ color: tag.trend === "up" ? "error.main" : tag.trend === "down" ? "success.main" : "text.secondary"
2917
+ },
2918
+ children: [
2919
+ tag.trendPercent > 0 ? "+" : "",
2920
+ tag.trendPercent.toFixed(1),
2921
+ "%"
2922
+ ]
2923
+ }
2924
+ )
2925
+ ]
2926
+ }
2927
+ )
2928
+ ]
2929
+ }
2930
+ );
2931
+ };
2932
+ const difference = firstTag.amount - secondTag.amount;
2933
+ const percentDifference = difference / secondTag.amount * 100;
2934
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.Card, { "data-testid": "expense-compare-card", children: [
2935
+ /* @__PURE__ */ jsxRuntime.jsx(
2936
+ material.CardHeader,
2937
+ {
2938
+ avatar: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.CompareArrows, { color: "primary" }),
2939
+ title,
2940
+ subheader: dateRangeLabel,
2941
+ titleTypographyProps: {
2942
+ variant: "h6",
2943
+ color: "primary"
2944
+ }
2945
+ }
2946
+ ),
2947
+ /* @__PURE__ */ jsxRuntime.jsxs(material.CardContent, { children: [
2948
+ /* @__PURE__ */ jsxRuntime.jsxs(
2949
+ material.Box,
2950
+ {
2951
+ sx: {
2952
+ display: "flex",
2953
+ gap: 2,
2954
+ mb: 2
2955
+ },
2956
+ children: [
2957
+ renderTagColumn(firstTag, true),
2958
+ renderTagColumn(secondTag, false)
2959
+ ]
2960
+ }
2961
+ ),
2962
+ /* @__PURE__ */ jsxRuntime.jsxs(
2963
+ material.Box,
2964
+ {
2965
+ sx: {
2966
+ p: 2,
2967
+ borderRadius: 1,
2968
+ bgcolor: "background.default",
2969
+ textAlign: "center"
2970
+ },
2971
+ children: [
2972
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", gutterBottom: true, children: "Difference" }),
2973
+ /* @__PURE__ */ jsxRuntime.jsxs(
2974
+ material.Typography,
2975
+ {
2976
+ variant: "h6",
2977
+ sx: {
2978
+ fontWeight: 600,
2979
+ color: difference > 0 ? "error.main" : difference < 0 ? "success.main" : "text.secondary"
2980
+ },
2981
+ children: [
2982
+ difference > 0 ? "+" : "",
2983
+ currencyFormatter(Math.abs(difference))
2984
+ ]
2985
+ }
2986
+ ),
2987
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "caption", color: "text.secondary", children: [
2988
+ "(",
2989
+ percentDifference > 0 ? "+" : "",
2990
+ percentDifference.toFixed(1),
2991
+ "%)"
2992
+ ] })
2993
+ ]
2994
+ }
2995
+ )
2996
+ ] }),
2997
+ onViewDetails && /* @__PURE__ */ jsxRuntime.jsx(
2998
+ material.CardActions,
2999
+ {
3000
+ sx: {
3001
+ justifyContent: "flex-end",
3002
+ borderTop: `1px solid ${theme.palette.divider}`
3003
+ },
3004
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3005
+ material.Button,
3006
+ {
3007
+ size: "small",
3008
+ color: "primary",
3009
+ endIcon: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.ChevronRight, {}),
3010
+ onClick: onViewDetails,
3011
+ children: "View Details"
3012
+ }
3013
+ )
3014
+ }
3015
+ )
3016
+ ] });
3017
+ }
3018
+
3019
+ exports.ExpenseAdvancedFilterForm = ExpenseAdvancedFilterForm;
3020
+ exports.ExpenseAnalyzer = ExpenseAnalyzer;
3021
+ exports.ExpenseBarChart = ExpenseBarChart;
3022
+ exports.ExpenseBreakdownItem = ExpenseBreakdownItem;
3023
+ exports.ExpenseChart = ExpenseChart;
3024
+ exports.ExpenseCompareCard = ExpenseCompareCard;
3025
+ exports.ExpenseCreateForm = ExpenseCreateForm;
3026
+ exports.ExpenseDateRangeTabs = ExpenseDateRangeTabs;
3027
+ exports.ExpenseDeleteButton = ExpenseDeleteButton;
3028
+ exports.ExpenseDonutChart = ExpenseDonutChart;
3029
+ exports.ExpenseEditForm = ExpenseEditForm;
3030
+ exports.ExpenseEmptyState = ExpenseEmptyState;
3031
+ exports.ExpenseExportButton = ExpenseExportButton;
3032
+ exports.ExpenseFilterForm = ExpenseFilterForm;
3033
+ exports.ExpenseLineChart = ExpenseLineChart;
3034
+ exports.ExpenseList = ExpenseList;
3035
+ exports.ExpenseLoadingState = ExpenseLoadingState;
3036
+ exports.ExpenseMeter = ExpenseMeter;
3037
+ exports.ExpenseSettingsPanel = ExpenseSettingsPanel;
3038
+ exports.ExpenseSummary = ExpenseSummary;
3039
+ exports.ExpenseThinDonut = ExpenseThinDonut;
3040
+ exports.ExpenseWheelContainer = ExpenseWheelContainer;
3041
+ //# sourceMappingURL=index.cjs.map
3042
+ //# sourceMappingURL=index.cjs.map