@pfm-platform/budgets-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.js ADDED
@@ -0,0 +1,1090 @@
1
+ import { Card, CardContent, Typography, List, ListItem, Box, Chip, LinearProgress, Grid, Paper, TextField, Alert, Button, CircularProgress, Dialog, DialogTitle, DialogContent, DialogActions, Tooltip, IconButton, DialogContentText, FormControl, InputLabel, Select, MenuItem, useTheme, alpha, Divider } from '@mui/material';
2
+ import { useBudgetProgress, useBudgetSummary } from '@pfm-platform/budgets-feature';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+ import { useState, useMemo, useEffect } from 'react';
5
+ import { useCreateBudget, useUpdateBudget, useDeleteBudget } from '@pfm-platform/budgets-data-access';
6
+ import DeleteIcon from '@mui/icons-material/Delete';
7
+ import { Warning, TrendingUp, TrendingDown, ExpandMore, CheckCircle } from '@mui/icons-material';
8
+
9
+ // src/components/BudgetList.tsx
10
+ function BudgetList({
11
+ userId,
12
+ start_date,
13
+ end_date,
14
+ title = "Budgets",
15
+ currencySymbol = "$",
16
+ maxItems
17
+ }) {
18
+ const budgets = useBudgetProgress({ userId, start_date, end_date });
19
+ if (!budgets) {
20
+ return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { children: [
21
+ /* @__PURE__ */ jsx(Typography, { variant: "h6", gutterBottom: true, children: title }),
22
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.secondary", children: "Loading budgets..." })
23
+ ] }) });
24
+ }
25
+ const displayBudgets = maxItems ? budgets.slice(0, maxItems) : budgets;
26
+ return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { children: [
27
+ /* @__PURE__ */ jsx(Typography, { variant: "h6", gutterBottom: true, children: title }),
28
+ displayBudgets.length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.secondary", children: "No budgets found" }) : /* @__PURE__ */ jsx(List, { disablePadding: true, children: displayBudgets.map((budget, index) => /* @__PURE__ */ jsxs(
29
+ ListItem,
30
+ {
31
+ divider: index < displayBudgets.length - 1,
32
+ sx: { px: 0, display: "flex", flexDirection: "column", alignItems: "flex-start" },
33
+ children: [
34
+ /* @__PURE__ */ jsxs(Box, { sx: { width: "100%", display: "flex", alignItems: "center", justifyContent: "space-between", mb: 0.5 }, children: [
35
+ /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 1 }, children: [
36
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", fontWeight: "medium", children: budget.name }),
37
+ /* @__PURE__ */ jsx(
38
+ Chip,
39
+ {
40
+ label: budget.state,
41
+ size: "small",
42
+ color: budget.state === "over" ? "error" : budget.state === "risk" ? "warning" : "success",
43
+ variant: "outlined"
44
+ }
45
+ )
46
+ ] }),
47
+ /* @__PURE__ */ jsxs(Typography, { variant: "body2", fontWeight: "medium", children: [
48
+ currencySymbol,
49
+ budget.spent.toFixed(2),
50
+ " / ",
51
+ currencySymbol,
52
+ budget.budgetAmount.toFixed(2)
53
+ ] })
54
+ ] }),
55
+ /* @__PURE__ */ jsxs(Box, { sx: { width: "100%" }, children: [
56
+ /* @__PURE__ */ jsx(
57
+ LinearProgress,
58
+ {
59
+ variant: "determinate",
60
+ value: Math.min(budget.percentSpent, 100),
61
+ color: budget.state === "over" ? "error" : budget.state === "risk" ? "warning" : "primary",
62
+ sx: { height: 8, borderRadius: 1 }
63
+ }
64
+ ),
65
+ /* @__PURE__ */ jsxs(Typography, { variant: "caption", color: "text.secondary", children: [
66
+ budget.percentSpent.toFixed(1),
67
+ "% \u2022 ",
68
+ currencySymbol,
69
+ budget.remaining.toFixed(2),
70
+ " remaining"
71
+ ] })
72
+ ] })
73
+ ]
74
+ },
75
+ budget.id
76
+ )) })
77
+ ] }) });
78
+ }
79
+ function BudgetSummary({
80
+ userId,
81
+ start_date,
82
+ end_date,
83
+ title = "Budget Summary",
84
+ currencySymbol = "$"
85
+ }) {
86
+ const summary = useBudgetSummary({ userId, start_date, end_date });
87
+ if (!summary) {
88
+ return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { children: [
89
+ /* @__PURE__ */ jsx(Typography, { variant: "h6", gutterBottom: true, children: title }),
90
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.secondary", children: "Loading budget summary..." })
91
+ ] }) });
92
+ }
93
+ if (!summary.hasBudgets) {
94
+ return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { children: [
95
+ /* @__PURE__ */ jsx(Typography, { variant: "h6", gutterBottom: true, children: title }),
96
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.secondary", children: "No budgets found" })
97
+ ] }) });
98
+ }
99
+ return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { children: [
100
+ /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", alignItems: "center", justifyContent: "space-between", mb: 2 }, children: [
101
+ /* @__PURE__ */ jsx(Typography, { variant: "h6", children: title }),
102
+ /* @__PURE__ */ jsx(
103
+ Chip,
104
+ {
105
+ label: summary.state,
106
+ size: "small",
107
+ color: summary.state === "over" ? "error" : summary.state === "risk" ? "warning" : "success"
108
+ }
109
+ )
110
+ ] }),
111
+ /* @__PURE__ */ jsxs(Grid, { container: true, spacing: 2, children: [
112
+ /* @__PURE__ */ jsx(Grid, { size: 6, children: /* @__PURE__ */ jsxs(Box, { children: [
113
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.secondary", gutterBottom: true, children: "Total Budgets" }),
114
+ /* @__PURE__ */ jsx(Typography, { variant: "h5", children: summary.totalBudgets })
115
+ ] }) }),
116
+ /* @__PURE__ */ jsx(Grid, { size: 6, children: /* @__PURE__ */ jsxs(Box, { children: [
117
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.secondary", gutterBottom: true, children: "Total Budget" }),
118
+ /* @__PURE__ */ jsxs(Typography, { variant: "h5", children: [
119
+ currencySymbol,
120
+ summary.totalBudget.toFixed(2)
121
+ ] })
122
+ ] }) }),
123
+ /* @__PURE__ */ jsx(Grid, { size: 6, children: /* @__PURE__ */ jsxs(Box, { children: [
124
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.secondary", gutterBottom: true, children: "Total Spent" }),
125
+ /* @__PURE__ */ jsxs(
126
+ Typography,
127
+ {
128
+ variant: "h6",
129
+ color: summary.state === "over" ? "error.main" : "text.primary",
130
+ children: [
131
+ currencySymbol,
132
+ summary.totalSpent.toFixed(2)
133
+ ]
134
+ }
135
+ )
136
+ ] }) }),
137
+ /* @__PURE__ */ jsx(Grid, { size: 6, children: /* @__PURE__ */ jsxs(Box, { children: [
138
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.secondary", gutterBottom: true, children: "Remaining" }),
139
+ /* @__PURE__ */ jsxs(
140
+ Typography,
141
+ {
142
+ variant: "h6",
143
+ color: summary.totalRemaining >= 0 ? "success.main" : "error.main",
144
+ children: [
145
+ currencySymbol,
146
+ summary.totalRemaining.toFixed(2)
147
+ ]
148
+ }
149
+ )
150
+ ] }) }),
151
+ /* @__PURE__ */ jsx(Grid, { size: 12, children: /* @__PURE__ */ jsxs(Box, { children: [
152
+ /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", justifyContent: "space-between", mb: 0.5 }, children: [
153
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.secondary", children: "Overall Progress" }),
154
+ /* @__PURE__ */ jsxs(Typography, { variant: "body2", fontWeight: "medium", children: [
155
+ summary.percentSpent.toFixed(1),
156
+ "%"
157
+ ] })
158
+ ] }),
159
+ /* @__PURE__ */ jsx(
160
+ LinearProgress,
161
+ {
162
+ variant: "determinate",
163
+ value: Math.min(summary.percentSpent, 100),
164
+ color: summary.state === "over" ? "error" : summary.state === "risk" ? "warning" : "primary",
165
+ sx: { height: 10, borderRadius: 1 }
166
+ }
167
+ )
168
+ ] }) })
169
+ ] })
170
+ ] }) });
171
+ }
172
+ function BudgetCreateForm({
173
+ userId,
174
+ onSuccess
175
+ }) {
176
+ const [name, setName] = useState("");
177
+ const [budgetAmount, setBudgetAmount] = useState("");
178
+ const [tagNames, setTagNames] = useState("");
179
+ const [error, setError] = useState(null);
180
+ const createBudget = useCreateBudget({
181
+ onSuccess: () => {
182
+ setName("");
183
+ setBudgetAmount("");
184
+ setTagNames("");
185
+ setError(null);
186
+ onSuccess?.();
187
+ },
188
+ onError: (err) => {
189
+ setError(err instanceof Error ? err.message : "Failed to create budget");
190
+ }
191
+ });
192
+ const handleSubmit = (e) => {
193
+ e.preventDefault();
194
+ if (!name.trim()) {
195
+ setError("Budget name is required");
196
+ return;
197
+ }
198
+ const amount = parseFloat(budgetAmount);
199
+ if (isNaN(amount) || amount <= 0) {
200
+ setError("Budget amount must be a positive number");
201
+ return;
202
+ }
203
+ const tags = tagNames.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0);
204
+ if (tags.length === 0) {
205
+ setError("At least one tag is required");
206
+ return;
207
+ }
208
+ const data = {
209
+ name: name.trim(),
210
+ budget_amount: amount,
211
+ tag_names: tags,
212
+ show_on_dashboard: true
213
+ };
214
+ createBudget.mutate({ userId, data });
215
+ };
216
+ return /* @__PURE__ */ jsxs(Paper, { sx: { p: 3 }, children: [
217
+ /* @__PURE__ */ jsx(Typography, { variant: "h6", gutterBottom: true, children: "Create Budget" }),
218
+ /* @__PURE__ */ jsxs(
219
+ Box,
220
+ {
221
+ component: "form",
222
+ onSubmit: handleSubmit,
223
+ sx: { display: "flex", flexDirection: "column", gap: 2 },
224
+ children: [
225
+ /* @__PURE__ */ jsx(
226
+ TextField,
227
+ {
228
+ label: "Budget Name",
229
+ value: name,
230
+ onChange: (e) => {
231
+ setName(e.target.value);
232
+ setError(null);
233
+ },
234
+ required: true,
235
+ fullWidth: true,
236
+ disabled: createBudget.isPending,
237
+ error: !!error && !name.trim(),
238
+ helperText: "Enter a name for your budget (e.g., 'Monthly Groceries')",
239
+ inputProps: {
240
+ maxLength: 255
241
+ }
242
+ }
243
+ ),
244
+ /* @__PURE__ */ jsx(
245
+ TextField,
246
+ {
247
+ label: "Budget Amount",
248
+ type: "number",
249
+ value: budgetAmount,
250
+ onChange: (e) => {
251
+ setBudgetAmount(e.target.value);
252
+ setError(null);
253
+ },
254
+ required: true,
255
+ fullWidth: true,
256
+ disabled: createBudget.isPending,
257
+ error: !!error && (isNaN(parseFloat(budgetAmount)) || parseFloat(budgetAmount) <= 0),
258
+ helperText: "Enter the total budget amount (e.g., 500.00)",
259
+ inputProps: {
260
+ min: 0.01,
261
+ step: 0.01
262
+ }
263
+ }
264
+ ),
265
+ /* @__PURE__ */ jsx(
266
+ TextField,
267
+ {
268
+ label: "Tags (comma-separated)",
269
+ value: tagNames,
270
+ onChange: (e) => {
271
+ setTagNames(e.target.value);
272
+ setError(null);
273
+ },
274
+ required: true,
275
+ fullWidth: true,
276
+ disabled: createBudget.isPending,
277
+ error: !!error && tagNames.split(",").filter((t) => t.trim()).length === 0,
278
+ helperText: "Enter tags separated by commas (e.g., 'groceries, food, essentials')",
279
+ multiline: true,
280
+ rows: 2
281
+ }
282
+ ),
283
+ error && /* @__PURE__ */ jsx(Alert, { severity: "error", onClose: () => setError(null), children: error }),
284
+ /* @__PURE__ */ jsx(Box, { sx: { display: "flex", gap: 1 }, children: /* @__PURE__ */ jsx(
285
+ Button,
286
+ {
287
+ type: "submit",
288
+ variant: "contained",
289
+ disabled: createBudget.isPending || !name.trim() || !budgetAmount || !tagNames.trim(),
290
+ startIcon: createBudget.isPending ? /* @__PURE__ */ jsx(CircularProgress, { size: 20 }) : null,
291
+ children: createBudget.isPending ? "Creating..." : "Create Budget"
292
+ }
293
+ ) })
294
+ ]
295
+ }
296
+ )
297
+ ] });
298
+ }
299
+ function BudgetEditForm({
300
+ userId,
301
+ budget,
302
+ open,
303
+ onClose
304
+ }) {
305
+ const [name, setName] = useState(budget.name);
306
+ const [budgetAmount, setBudgetAmount] = useState(budget.budget_amount.toString());
307
+ const [tagNames, setTagNames] = useState(budget.tag_names.join(", "));
308
+ const [error, setError] = useState(null);
309
+ const updateBudget = useUpdateBudget({
310
+ onSuccess: () => {
311
+ setError(null);
312
+ onClose();
313
+ },
314
+ onError: (err) => {
315
+ setError(err instanceof Error ? err.message : "Failed to update budget");
316
+ }
317
+ });
318
+ const handleSubmit = (e) => {
319
+ e.preventDefault();
320
+ if (!name.trim()) {
321
+ setError("Budget name is required");
322
+ return;
323
+ }
324
+ const amount = parseFloat(budgetAmount);
325
+ if (isNaN(amount) || amount <= 0) {
326
+ setError("Budget amount must be a positive number");
327
+ return;
328
+ }
329
+ const tags = tagNames.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0);
330
+ if (tags.length === 0) {
331
+ setError("At least one tag is required");
332
+ return;
333
+ }
334
+ const data = {
335
+ id: budget.id,
336
+ name: name.trim(),
337
+ budget_amount: amount,
338
+ tag_names: tags,
339
+ show_on_dashboard: true
340
+ };
341
+ updateBudget.mutate({ userId, budgetId: budget.id, data });
342
+ };
343
+ const handleClose = () => {
344
+ if (!updateBudget.isPending) {
345
+ onClose();
346
+ }
347
+ };
348
+ return /* @__PURE__ */ jsx(
349
+ Dialog,
350
+ {
351
+ open,
352
+ onClose: handleClose,
353
+ maxWidth: "sm",
354
+ fullWidth: true,
355
+ "aria-labelledby": "edit-budget-dialog-title",
356
+ children: /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, children: [
357
+ /* @__PURE__ */ jsx(DialogTitle, { id: "edit-budget-dialog-title", children: "Edit Budget" }),
358
+ /* @__PURE__ */ jsx(DialogContent, { children: /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", flexDirection: "column", gap: 2, pt: 1 }, children: [
359
+ /* @__PURE__ */ jsx(
360
+ TextField,
361
+ {
362
+ label: "Budget Name",
363
+ value: name,
364
+ onChange: (e) => {
365
+ setName(e.target.value);
366
+ setError(null);
367
+ },
368
+ required: true,
369
+ fullWidth: true,
370
+ disabled: updateBudget.isPending,
371
+ error: !!error && !name.trim(),
372
+ helperText: "Enter a name for your budget",
373
+ inputProps: {
374
+ maxLength: 255
375
+ }
376
+ }
377
+ ),
378
+ /* @__PURE__ */ jsx(
379
+ TextField,
380
+ {
381
+ label: "Budget Amount",
382
+ type: "number",
383
+ value: budgetAmount,
384
+ onChange: (e) => {
385
+ setBudgetAmount(e.target.value);
386
+ setError(null);
387
+ },
388
+ required: true,
389
+ fullWidth: true,
390
+ disabled: updateBudget.isPending,
391
+ error: !!error && (isNaN(parseFloat(budgetAmount)) || parseFloat(budgetAmount) <= 0),
392
+ helperText: "Enter the total budget amount",
393
+ inputProps: {
394
+ min: 0.01,
395
+ step: 0.01
396
+ }
397
+ }
398
+ ),
399
+ /* @__PURE__ */ jsx(
400
+ TextField,
401
+ {
402
+ label: "Tags (comma-separated)",
403
+ value: tagNames,
404
+ onChange: (e) => {
405
+ setTagNames(e.target.value);
406
+ setError(null);
407
+ },
408
+ required: true,
409
+ fullWidth: true,
410
+ disabled: updateBudget.isPending,
411
+ error: !!error && tagNames.split(",").filter((t) => t.trim()).length === 0,
412
+ helperText: "Enter tags separated by commas",
413
+ multiline: true,
414
+ rows: 2
415
+ }
416
+ ),
417
+ error && /* @__PURE__ */ jsx(Alert, { severity: "error", onClose: () => setError(null), children: error })
418
+ ] }) }),
419
+ /* @__PURE__ */ jsxs(DialogActions, { children: [
420
+ /* @__PURE__ */ jsx(Button, { onClick: handleClose, disabled: updateBudget.isPending, children: "Cancel" }),
421
+ /* @__PURE__ */ jsx(
422
+ Button,
423
+ {
424
+ type: "submit",
425
+ variant: "contained",
426
+ disabled: updateBudget.isPending || !name.trim() || !budgetAmount || !tagNames.trim(),
427
+ startIcon: updateBudget.isPending ? /* @__PURE__ */ jsx(CircularProgress, { size: 20 }) : null,
428
+ children: updateBudget.isPending ? "Saving..." : "Save Changes"
429
+ }
430
+ )
431
+ ] })
432
+ ] })
433
+ }
434
+ );
435
+ }
436
+ function BudgetDeleteButton({
437
+ userId,
438
+ budgetId,
439
+ budgetName
440
+ }) {
441
+ const [open, setOpen] = useState(false);
442
+ const deleteBudget = useDeleteBudget({
443
+ onSuccess: () => {
444
+ setOpen(false);
445
+ }
446
+ });
447
+ const handleDelete = () => {
448
+ deleteBudget.mutate({ userId, budgetId });
449
+ };
450
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
451
+ /* @__PURE__ */ jsx(Tooltip, { title: "Delete budget", children: /* @__PURE__ */ jsx(
452
+ IconButton,
453
+ {
454
+ onClick: () => setOpen(true),
455
+ color: "error",
456
+ size: "small",
457
+ "aria-label": `delete budget ${budgetName}`,
458
+ children: /* @__PURE__ */ jsx(DeleteIcon, {})
459
+ }
460
+ ) }),
461
+ /* @__PURE__ */ jsxs(
462
+ Dialog,
463
+ {
464
+ open,
465
+ onClose: () => !deleteBudget.isPending && setOpen(false),
466
+ "aria-labelledby": "delete-budget-dialog-title",
467
+ children: [
468
+ /* @__PURE__ */ jsx(DialogTitle, { id: "delete-budget-dialog-title", children: "Delete Budget?" }),
469
+ /* @__PURE__ */ jsx(DialogContent, { children: /* @__PURE__ */ jsxs(DialogContentText, { children: [
470
+ 'Are you sure you want to delete the budget "',
471
+ budgetName,
472
+ '"? This action cannot be undone.'
473
+ ] }) }),
474
+ /* @__PURE__ */ jsxs(DialogActions, { children: [
475
+ /* @__PURE__ */ jsx(
476
+ Button,
477
+ {
478
+ onClick: () => setOpen(false),
479
+ disabled: deleteBudget.isPending,
480
+ children: "Cancel"
481
+ }
482
+ ),
483
+ /* @__PURE__ */ jsx(
484
+ Button,
485
+ {
486
+ onClick: handleDelete,
487
+ color: "error",
488
+ variant: "contained",
489
+ disabled: deleteBudget.isPending,
490
+ autoFocus: true,
491
+ children: deleteBudget.isPending ? "Deleting..." : "Delete"
492
+ }
493
+ )
494
+ ] })
495
+ ]
496
+ }
497
+ )
498
+ ] });
499
+ }
500
+ function BudgetProgressCard({
501
+ budget,
502
+ currentSpent,
503
+ currencySymbol = "$",
504
+ showTrend = false,
505
+ lastPeriodSpent
506
+ }) {
507
+ const limit = budget.budget_amount;
508
+ const remaining = limit - currentSpent;
509
+ const percentage = currentSpent / limit * 100;
510
+ const getColor = () => {
511
+ if (percentage <= 70) return "success";
512
+ if (percentage <= 90) return "warning";
513
+ return "error";
514
+ };
515
+ const color = getColor();
516
+ const trend = lastPeriodSpent !== void 0 ? currentSpent - lastPeriodSpent : null;
517
+ const trendPercentage = trend !== null && lastPeriodSpent !== void 0 && lastPeriodSpent > 0 ? trend / lastPeriodSpent * 100 : null;
518
+ return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { children: [
519
+ /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 2 }, children: [
520
+ /* @__PURE__ */ jsxs(Box, { children: [
521
+ /* @__PURE__ */ jsx(Typography, { variant: "h6", gutterBottom: true, children: budget.name }),
522
+ budget.tag_names.length > 0 && /* @__PURE__ */ jsx(
523
+ Chip,
524
+ {
525
+ label: budget.tag_names[0],
526
+ size: "small",
527
+ variant: "outlined",
528
+ sx: { mb: 1 }
529
+ }
530
+ )
531
+ ] }),
532
+ percentage > 90 && /* @__PURE__ */ jsx(
533
+ Chip,
534
+ {
535
+ icon: /* @__PURE__ */ jsx(Warning, {}),
536
+ label: "Over Budget",
537
+ color: "error",
538
+ size: "small"
539
+ }
540
+ )
541
+ ] }),
542
+ /* @__PURE__ */ jsxs(Box, { sx: { mb: 2 }, children: [
543
+ /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", justifyContent: "space-between", mb: 1 }, children: [
544
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.secondary", children: "Progress" }),
545
+ /* @__PURE__ */ jsxs(Typography, { variant: "body2", fontWeight: "bold", color: `${color}.main`, children: [
546
+ percentage.toFixed(1),
547
+ "%"
548
+ ] })
549
+ ] }),
550
+ /* @__PURE__ */ jsx(
551
+ LinearProgress,
552
+ {
553
+ variant: "determinate",
554
+ value: Math.min(percentage, 100),
555
+ color,
556
+ sx: { height: 8, borderRadius: 4 }
557
+ }
558
+ )
559
+ ] }),
560
+ /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", justifyContent: "space-between", mb: 2 }, children: [
561
+ /* @__PURE__ */ jsxs(Box, { children: [
562
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "text.secondary", display: "block", children: "Spent" }),
563
+ /* @__PURE__ */ jsxs(Typography, { variant: "h6", color: `${color}.main`, children: [
564
+ currencySymbol,
565
+ currentSpent.toFixed(2)
566
+ ] })
567
+ ] }),
568
+ /* @__PURE__ */ jsxs(Box, { sx: { textAlign: "center" }, children: [
569
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "text.secondary", display: "block", children: "Limit" }),
570
+ /* @__PURE__ */ jsxs(Typography, { variant: "h6", children: [
571
+ currencySymbol,
572
+ limit.toFixed(2)
573
+ ] })
574
+ ] }),
575
+ /* @__PURE__ */ jsxs(Box, { sx: { textAlign: "right" }, children: [
576
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "text.secondary", display: "block", children: "Remaining" }),
577
+ /* @__PURE__ */ jsxs(
578
+ Typography,
579
+ {
580
+ variant: "h6",
581
+ color: remaining >= 0 ? "success.main" : "error.main",
582
+ children: [
583
+ currencySymbol,
584
+ Math.abs(remaining).toFixed(2)
585
+ ]
586
+ }
587
+ )
588
+ ] })
589
+ ] }),
590
+ showTrend && trend !== null && trendPercentage !== null && /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 1 }, children: [
591
+ trend > 0 ? /* @__PURE__ */ jsx(
592
+ Chip,
593
+ {
594
+ icon: /* @__PURE__ */ jsx(TrendingUp, {}),
595
+ label: `+${currencySymbol}${Math.abs(trend).toFixed(2)} (+${trendPercentage.toFixed(1)}%)`,
596
+ color: "error",
597
+ size: "small",
598
+ variant: "outlined"
599
+ }
600
+ ) : /* @__PURE__ */ jsx(
601
+ Chip,
602
+ {
603
+ icon: /* @__PURE__ */ jsx(TrendingDown, {}),
604
+ label: `-${currencySymbol}${Math.abs(trend).toFixed(2)} (${trendPercentage.toFixed(1)}%)`,
605
+ color: "success",
606
+ size: "small",
607
+ variant: "outlined"
608
+ }
609
+ ),
610
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "text.secondary", children: "vs last period" })
611
+ ] })
612
+ ] }) });
613
+ }
614
+ function BudgetProgressList({
615
+ budgets,
616
+ spentByBudget,
617
+ lastPeriodSpentByBudget,
618
+ currencySymbol = "$",
619
+ showTrends = false,
620
+ enableCategoryFilter = true
621
+ }) {
622
+ const [statusFilter, setStatusFilter] = useState("all");
623
+ const [categoryFilter, setCategory] = useState("all");
624
+ const categories = useMemo(() => {
625
+ const uniqueCategories = /* @__PURE__ */ new Set();
626
+ budgets.forEach((budget) => {
627
+ budget.tag_names.forEach((tag) => {
628
+ uniqueCategories.add(tag);
629
+ });
630
+ });
631
+ return Array.from(uniqueCategories).sort();
632
+ }, [budgets]);
633
+ const filteredBudgets = useMemo(() => {
634
+ return budgets.filter((budget) => {
635
+ const spent = spentByBudget.get(budget.id) || 0;
636
+ const limit = budget.budget_amount;
637
+ const percentage = spent / limit * 100;
638
+ if (statusFilter === "on-track" && percentage > 70) return false;
639
+ if (statusFilter === "warning" && (percentage <= 70 || percentage > 90)) return false;
640
+ if (statusFilter === "over-budget" && percentage <= 90) return false;
641
+ if (categoryFilter !== "all" && !budget.tag_names.includes(categoryFilter)) return false;
642
+ return true;
643
+ });
644
+ }, [budgets, spentByBudget, statusFilter, categoryFilter]);
645
+ const stats = useMemo(() => {
646
+ const onTrack = budgets.filter((b) => {
647
+ const spent = spentByBudget.get(b.id) || 0;
648
+ const percentage = spent / b.budget_amount * 100;
649
+ return percentage <= 70;
650
+ }).length;
651
+ const warning = budgets.filter((b) => {
652
+ const spent = spentByBudget.get(b.id) || 0;
653
+ const percentage = spent / b.budget_amount * 100;
654
+ return percentage > 70 && percentage <= 90;
655
+ }).length;
656
+ const overBudget = budgets.filter((b) => {
657
+ const spent = spentByBudget.get(b.id) || 0;
658
+ const percentage = spent / b.budget_amount * 100;
659
+ return percentage > 90;
660
+ }).length;
661
+ return { onTrack, warning, overBudget };
662
+ }, [budgets, spentByBudget]);
663
+ if (budgets.length === 0) {
664
+ return /* @__PURE__ */ jsx(Alert, { severity: "info", children: "No budgets found. Create a budget to start tracking your spending." });
665
+ }
666
+ return /* @__PURE__ */ jsxs(Box, { children: [
667
+ /* @__PURE__ */ jsxs(Box, { sx: { mb: 3 }, children: [
668
+ /* @__PURE__ */ jsx(Typography, { variant: "h5", gutterBottom: true, children: "Budget Progress" }),
669
+ /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", gap: 2, mb: 2, flexWrap: "wrap" }, children: [
670
+ /* @__PURE__ */ jsxs(Typography, { variant: "body2", color: "text.secondary", children: [
671
+ /* @__PURE__ */ jsx("strong", { children: stats.onTrack }),
672
+ " on track"
673
+ ] }),
674
+ /* @__PURE__ */ jsxs(Typography, { variant: "body2", color: "warning.main", children: [
675
+ /* @__PURE__ */ jsx("strong", { children: stats.warning }),
676
+ " warning"
677
+ ] }),
678
+ /* @__PURE__ */ jsxs(Typography, { variant: "body2", color: "error.main", children: [
679
+ /* @__PURE__ */ jsx("strong", { children: stats.overBudget }),
680
+ " over budget"
681
+ ] })
682
+ ] }),
683
+ /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", gap: 2, flexWrap: "wrap" }, children: [
684
+ /* @__PURE__ */ jsxs(FormControl, { size: "small", sx: { minWidth: 150 }, children: [
685
+ /* @__PURE__ */ jsx(InputLabel, { children: "Status" }),
686
+ /* @__PURE__ */ jsxs(
687
+ Select,
688
+ {
689
+ value: statusFilter,
690
+ label: "Status",
691
+ onChange: (e) => setStatusFilter(e.target.value),
692
+ children: [
693
+ /* @__PURE__ */ jsx(MenuItem, { value: "all", children: "All Budgets" }),
694
+ /* @__PURE__ */ jsx(MenuItem, { value: "on-track", children: "On Track" }),
695
+ /* @__PURE__ */ jsx(MenuItem, { value: "warning", children: "Warning" }),
696
+ /* @__PURE__ */ jsx(MenuItem, { value: "over-budget", children: "Over Budget" })
697
+ ]
698
+ }
699
+ )
700
+ ] }),
701
+ enableCategoryFilter && categories.length > 0 && /* @__PURE__ */ jsxs(FormControl, { size: "small", sx: { minWidth: 150 }, children: [
702
+ /* @__PURE__ */ jsx(InputLabel, { children: "Category" }),
703
+ /* @__PURE__ */ jsxs(
704
+ Select,
705
+ {
706
+ value: categoryFilter,
707
+ label: "Category",
708
+ onChange: (e) => setCategory(e.target.value),
709
+ children: [
710
+ /* @__PURE__ */ jsx(MenuItem, { value: "all", children: "All Categories" }),
711
+ categories.map((cat) => /* @__PURE__ */ jsx(MenuItem, { value: cat, children: cat }, cat))
712
+ ]
713
+ }
714
+ )
715
+ ] })
716
+ ] })
717
+ ] }),
718
+ filteredBudgets.length === 0 ? /* @__PURE__ */ jsx(Alert, { severity: "info", children: "No budgets match the selected filters." }) : /* @__PURE__ */ jsx(Grid, { container: true, spacing: 3, children: filteredBudgets.map((budget) => {
719
+ const spent = spentByBudget.get(budget.id) || 0;
720
+ const lastPeriodSpent = lastPeriodSpentByBudget?.get(budget.id);
721
+ return /* @__PURE__ */ jsx(Grid, { size: { xs: 12, sm: 6, lg: 4 }, children: /* @__PURE__ */ jsx(
722
+ BudgetProgressCard,
723
+ {
724
+ budget,
725
+ currentSpent: spent,
726
+ currencySymbol,
727
+ showTrend: showTrends,
728
+ lastPeriodSpent
729
+ }
730
+ ) }, budget.id);
731
+ }) })
732
+ ] });
733
+ }
734
+ var defaultCurrencyFormatter = (value) => {
735
+ return new Intl.NumberFormat("en-US", {
736
+ style: "currency",
737
+ currency: "USD",
738
+ minimumFractionDigits: 0,
739
+ maximumFractionDigits: 0
740
+ }).format(value);
741
+ };
742
+ function getInsightStyle(type, theme) {
743
+ switch (type) {
744
+ case "overspending":
745
+ return {
746
+ icon: /* @__PURE__ */ jsx(Warning, {}),
747
+ color: theme.palette.error.main,
748
+ bgColor: alpha(theme.palette.error.main, 0.1)
749
+ };
750
+ case "underspending":
751
+ return {
752
+ icon: /* @__PURE__ */ jsx(TrendingDown, {}),
753
+ color: theme.palette.info.main,
754
+ bgColor: alpha(theme.palette.info.main, 0.1)
755
+ };
756
+ case "on-track":
757
+ return {
758
+ icon: /* @__PURE__ */ jsx(CheckCircle, {}),
759
+ color: theme.palette.success.main,
760
+ bgColor: alpha(theme.palette.success.main, 0.1)
761
+ };
762
+ case "suggest-tag":
763
+ return {
764
+ icon: /* @__PURE__ */ jsx(TrendingUp, {}),
765
+ color: theme.palette.warning.main,
766
+ bgColor: alpha(theme.palette.warning.main, 0.1)
767
+ };
768
+ default:
769
+ return {
770
+ icon: /* @__PURE__ */ jsx(CheckCircle, {}),
771
+ color: theme.palette.text.secondary,
772
+ bgColor: alpha(theme.palette.text.secondary, 0.1)
773
+ };
774
+ }
775
+ }
776
+ function BudgetInsightsCard({
777
+ insights,
778
+ overallSummary,
779
+ initialVisibleCount = 2,
780
+ onInsightAction,
781
+ onInsightDismiss,
782
+ currencyFormatter = defaultCurrencyFormatter
783
+ }) {
784
+ const theme = useTheme();
785
+ const [showAll, setShowAll] = useState(false);
786
+ const visibleInsights = showAll ? insights : insights.slice(0, initialVisibleCount);
787
+ const hasMore = insights.length > initialVisibleCount;
788
+ const overallStyle = overallSummary.state === "over" ? { color: theme.palette.error.main, icon: /* @__PURE__ */ jsx(Warning, {}) } : overallSummary.state === "under" ? { color: theme.palette.success.main, icon: /* @__PURE__ */ jsx(CheckCircle, {}) } : { color: theme.palette.info.main, icon: /* @__PURE__ */ jsx(CheckCircle, {}) };
789
+ return /* @__PURE__ */ jsx(Card, { "data-testid": "budget-insights-card", children: /* @__PURE__ */ jsxs(CardContent, { children: [
790
+ /* @__PURE__ */ jsx(Typography, { variant: "h6", gutterBottom: true, children: "Budget Insights" }),
791
+ /* @__PURE__ */ jsxs(
792
+ Box,
793
+ {
794
+ sx: {
795
+ display: "flex",
796
+ alignItems: "center",
797
+ gap: 2,
798
+ p: 2,
799
+ mb: 2,
800
+ bgcolor: alpha(overallStyle.color, 0.1),
801
+ borderRadius: 1
802
+ },
803
+ children: [
804
+ /* @__PURE__ */ jsx(Box, { sx: { color: overallStyle.color }, children: overallStyle.icon }),
805
+ /* @__PURE__ */ jsxs(Box, { sx: { flex: 1 }, children: [
806
+ overallSummary.totalAmount > 0 && /* @__PURE__ */ jsxs(Typography, { variant: "body2", children: [
807
+ "You've been ",
808
+ /* @__PURE__ */ jsx("strong", { children: "under budget" }),
809
+ " by",
810
+ " ",
811
+ /* @__PURE__ */ jsx("strong", { children: currencyFormatter(overallSummary.totalAmount) }),
812
+ " ",
813
+ "on all budgets over the last",
814
+ " ",
815
+ /* @__PURE__ */ jsxs("strong", { children: [
816
+ overallSummary.months,
817
+ " months"
818
+ ] }),
819
+ "."
820
+ ] }),
821
+ overallSummary.totalAmount < 0 && /* @__PURE__ */ jsxs(Typography, { variant: "body2", children: [
822
+ "You have ",
823
+ /* @__PURE__ */ jsx("strong", { children: "overspent" }),
824
+ " by",
825
+ " ",
826
+ /* @__PURE__ */ jsx("strong", { children: currencyFormatter(Math.abs(overallSummary.totalAmount)) }),
827
+ " ",
828
+ "on all budgets over the last",
829
+ " ",
830
+ /* @__PURE__ */ jsxs("strong", { children: [
831
+ overallSummary.months,
832
+ " months"
833
+ ] }),
834
+ "."
835
+ ] }),
836
+ overallSummary.totalAmount === 0 && /* @__PURE__ */ jsxs(Typography, { variant: "body2", children: [
837
+ "You are ",
838
+ /* @__PURE__ */ jsx("strong", { children: "on track" }),
839
+ " with all budgets over the last",
840
+ " ",
841
+ /* @__PURE__ */ jsxs("strong", { children: [
842
+ overallSummary.months,
843
+ " months"
844
+ ] }),
845
+ "."
846
+ ] })
847
+ ] })
848
+ ]
849
+ }
850
+ ),
851
+ /* @__PURE__ */ jsx(Divider, { sx: { my: 2 } }),
852
+ insights.length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.secondary", sx: { textAlign: "center", py: 2 }, children: "No insights available. Keep tracking your budgets!" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
853
+ visibleInsights.map((insight) => {
854
+ const style = getInsightStyle(insight.type, theme);
855
+ return /* @__PURE__ */ jsxs(
856
+ Box,
857
+ {
858
+ "data-testid": `budget-insight-${insight.id}`,
859
+ sx: {
860
+ p: 2,
861
+ mb: 1,
862
+ bgcolor: style.bgColor,
863
+ borderRadius: 1,
864
+ borderLeft: `4px solid ${style.color}`
865
+ },
866
+ children: [
867
+ /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", alignItems: "flex-start", gap: 1.5 }, children: [
868
+ /* @__PURE__ */ jsx(Box, { sx: { color: style.color, mt: 0.5 }, children: style.icon }),
869
+ /* @__PURE__ */ jsxs(Box, { sx: { flex: 1 }, children: [
870
+ /* @__PURE__ */ jsx(Typography, { variant: "subtitle2", gutterBottom: true, children: insight.budgetName }),
871
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mb: 1 }, children: insight.recommendation }),
872
+ /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", gap: 1, flexWrap: "wrap" }, children: [
873
+ /* @__PURE__ */ jsx(
874
+ Chip,
875
+ {
876
+ label: `${insight.occurrences} times`,
877
+ size: "small",
878
+ variant: "outlined"
879
+ }
880
+ ),
881
+ /* @__PURE__ */ jsx(
882
+ Chip,
883
+ {
884
+ label: `${insight.monthsAnalyzed} months`,
885
+ size: "small",
886
+ variant: "outlined"
887
+ }
888
+ ),
889
+ /* @__PURE__ */ jsx(
890
+ Chip,
891
+ {
892
+ label: `Avg: ${currencyFormatter(Math.abs(insight.averageAmount))}`,
893
+ size: "small",
894
+ variant: "outlined"
895
+ }
896
+ )
897
+ ] })
898
+ ] })
899
+ ] }),
900
+ /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", gap: 1, mt: 2, justifyContent: "flex-end" }, children: [
901
+ onInsightDismiss && /* @__PURE__ */ jsx(
902
+ Button,
903
+ {
904
+ size: "small",
905
+ onClick: () => onInsightDismiss(insight),
906
+ children: "Dismiss"
907
+ }
908
+ ),
909
+ onInsightAction && insight.type !== "suggest-tag" && /* @__PURE__ */ jsx(
910
+ Button,
911
+ {
912
+ size: "small",
913
+ variant: "contained",
914
+ onClick: () => onInsightAction(insight),
915
+ children: "Adjust Budget"
916
+ }
917
+ ),
918
+ onInsightAction && insight.type === "suggest-tag" && /* @__PURE__ */ jsx(
919
+ Button,
920
+ {
921
+ size: "small",
922
+ variant: "contained",
923
+ onClick: () => onInsightAction(insight),
924
+ children: "Add Tag"
925
+ }
926
+ )
927
+ ] })
928
+ ]
929
+ },
930
+ insight.id
931
+ );
932
+ }),
933
+ hasMore && /* @__PURE__ */ jsx(Box, { sx: { textAlign: "center", mt: 2 }, children: /* @__PURE__ */ jsx(
934
+ Button,
935
+ {
936
+ size: "small",
937
+ endIcon: /* @__PURE__ */ jsx(
938
+ ExpandMore,
939
+ {
940
+ sx: {
941
+ transform: showAll ? "rotate(180deg)" : "rotate(0)",
942
+ transition: "transform 0.3s"
943
+ }
944
+ }
945
+ ),
946
+ onClick: () => setShowAll(!showAll),
947
+ children: showAll ? "Show Less" : `Show ${insights.length - initialVisibleCount} More`
948
+ }
949
+ ) })
950
+ ] })
951
+ ] }) });
952
+ }
953
+ var defaultCurrencyFormatter2 = (value) => {
954
+ return new Intl.NumberFormat("en-US", {
955
+ style: "currency",
956
+ currency: "USD",
957
+ minimumFractionDigits: 2,
958
+ maximumFractionDigits: 2
959
+ }).format(value);
960
+ };
961
+ function BudgetInsightDialog({
962
+ insight,
963
+ open,
964
+ onClose,
965
+ onSave,
966
+ saving = false,
967
+ currencyFormatter = defaultCurrencyFormatter2
968
+ }) {
969
+ const [amount, setAmount] = useState("");
970
+ const [error, setError] = useState(null);
971
+ useEffect(() => {
972
+ if (open && insight) {
973
+ const suggestedAmount = Math.abs(insight.averageAmount);
974
+ setAmount(suggestedAmount.toFixed(2));
975
+ setError(null);
976
+ }
977
+ }, [open, insight]);
978
+ const handleAmountChange = (value) => {
979
+ setAmount(value);
980
+ const numValue = parseFloat(value);
981
+ if (isNaN(numValue)) {
982
+ setError("Please enter a valid number");
983
+ } else if (numValue <= 0) {
984
+ setError("Amount must be greater than zero");
985
+ } else if (numValue > 1e6) {
986
+ setError("Amount is too large");
987
+ } else {
988
+ setError(null);
989
+ }
990
+ };
991
+ const handleSave = async () => {
992
+ if (!insight || error || !amount) return;
993
+ const numValue = parseFloat(amount);
994
+ if (isNaN(numValue) || numValue <= 0) return;
995
+ await onSave(insight, numValue);
996
+ onClose();
997
+ };
998
+ const handleCancel = () => {
999
+ setAmount("");
1000
+ setError(null);
1001
+ onClose();
1002
+ };
1003
+ if (!insight) {
1004
+ return null;
1005
+ }
1006
+ const isOverspending = insight.type === "overspending";
1007
+ const actionLabel = isOverspending ? "Increase Budget By" : "Decrease Budget By";
1008
+ return /* @__PURE__ */ jsxs(
1009
+ Dialog,
1010
+ {
1011
+ open,
1012
+ onClose: handleCancel,
1013
+ maxWidth: "sm",
1014
+ fullWidth: true,
1015
+ "data-testid": "budget-insight-dialog",
1016
+ children: [
1017
+ /* @__PURE__ */ jsx(DialogTitle, { children: insight.budgetName }),
1018
+ /* @__PURE__ */ jsxs(DialogContent, { children: [
1019
+ isOverspending && /* @__PURE__ */ jsxs(DialogContentText, { sx: { mb: 3 }, children: [
1020
+ "You've ",
1021
+ /* @__PURE__ */ jsx("strong", { children: "exceeded" }),
1022
+ " this budget",
1023
+ " ",
1024
+ /* @__PURE__ */ jsx("strong", { children: insight.occurrences }),
1025
+ " times in the last",
1026
+ " ",
1027
+ /* @__PURE__ */ jsx("strong", { children: insight.monthsAnalyzed }),
1028
+ " months by an average of",
1029
+ " ",
1030
+ /* @__PURE__ */ jsx("strong", { children: currencyFormatter(Math.abs(insight.averageAmount)) }),
1031
+ " a month."
1032
+ ] }),
1033
+ !isOverspending && /* @__PURE__ */ jsxs(DialogContentText, { sx: { mb: 3 }, children: [
1034
+ "You've been ",
1035
+ /* @__PURE__ */ jsx("strong", { children: "under" }),
1036
+ " this budget",
1037
+ " ",
1038
+ /* @__PURE__ */ jsx("strong", { children: insight.occurrences }),
1039
+ " times in the last",
1040
+ " ",
1041
+ /* @__PURE__ */ jsx("strong", { children: insight.monthsAnalyzed }),
1042
+ " months by an average of",
1043
+ " ",
1044
+ /* @__PURE__ */ jsx("strong", { children: currencyFormatter(Math.abs(insight.averageAmount)) }),
1045
+ " a month."
1046
+ ] }),
1047
+ /* @__PURE__ */ jsx(
1048
+ TextField,
1049
+ {
1050
+ autoFocus: true,
1051
+ fullWidth: true,
1052
+ required: true,
1053
+ type: "number",
1054
+ label: actionLabel,
1055
+ value: amount,
1056
+ onChange: (e) => handleAmountChange(e.target.value),
1057
+ error: !!error,
1058
+ helperText: error || "Suggested amount based on your spending pattern",
1059
+ disabled: saving,
1060
+ inputProps: {
1061
+ min: 0,
1062
+ step: 0.01,
1063
+ "aria-label": actionLabel
1064
+ },
1065
+ InputLabelProps: {
1066
+ shrink: true
1067
+ }
1068
+ }
1069
+ )
1070
+ ] }),
1071
+ /* @__PURE__ */ jsxs(DialogActions, { children: [
1072
+ /* @__PURE__ */ jsx(Button, { onClick: handleCancel, disabled: saving, children: "Cancel" }),
1073
+ /* @__PURE__ */ jsx(
1074
+ Button,
1075
+ {
1076
+ onClick: handleSave,
1077
+ variant: "contained",
1078
+ disabled: saving || !!error || !amount,
1079
+ children: saving ? "Saving..." : "Save"
1080
+ }
1081
+ )
1082
+ ] })
1083
+ ]
1084
+ }
1085
+ );
1086
+ }
1087
+
1088
+ export { BudgetCreateForm, BudgetDeleteButton, BudgetEditForm, BudgetInsightDialog, BudgetInsightsCard, BudgetList, BudgetProgressCard, BudgetProgressList, BudgetSummary };
1089
+ //# sourceMappingURL=index.js.map
1090
+ //# sourceMappingURL=index.js.map