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