@pfm-platform/budgets-ui-mui 0.1.1 → 0.2.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 CHANGED
@@ -15,13 +15,13 @@ var DeleteIcon__default = /*#__PURE__*/_interopDefault(DeleteIcon);
15
15
  // src/components/BudgetList.tsx
16
16
  function BudgetList({
17
17
  userId,
18
- start_date,
19
- end_date,
18
+ month,
19
+ year,
20
20
  title = "Budgets",
21
21
  currencySymbol = "$",
22
22
  maxItems
23
23
  }) {
24
- const budgets = budgetsFeature.useBudgetProgress({ userId, start_date, end_date });
24
+ const budgets = budgetsFeature.useBudgetProgress({ userId, month, year });
25
25
  if (!budgets) {
26
26
  return /* @__PURE__ */ jsxRuntime.jsx(material.Card, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.CardContent, { children: [
27
27
  /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", gutterBottom: true, children: title }),
@@ -84,12 +84,12 @@ function BudgetList({
84
84
  }
85
85
  function BudgetSummary({
86
86
  userId,
87
- start_date,
88
- end_date,
87
+ month,
88
+ year,
89
89
  title = "Budget Summary",
90
90
  currencySymbol = "$"
91
91
  }) {
92
- const summary = budgetsFeature.useBudgetSummary({ userId, start_date, end_date });
92
+ const summary = budgetsFeature.useBudgetSummary({ userId, month, year });
93
93
  if (!summary) {
94
94
  return /* @__PURE__ */ jsxRuntime.jsx(material.Card, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.CardContent, { children: [
95
95
  /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", gutterBottom: true, children: title }),
@@ -211,13 +211,16 @@ function BudgetCreateForm({
211
211
  setError("At least one tag is required");
212
212
  return;
213
213
  }
214
+ const now = /* @__PURE__ */ new Date();
214
215
  const data = {
216
+ user_id: userId,
215
217
  name: name.trim(),
218
+ month: now.getMonth() + 1,
219
+ year: now.getFullYear(),
216
220
  budget_amount: amount,
217
- tag_names: tags,
218
221
  show_on_dashboard: true
219
222
  };
220
- createBudget.mutate({ userId, data });
223
+ createBudget.mutate({ userId, data, tagNames: tags });
221
224
  };
222
225
  return /* @__PURE__ */ jsxRuntime.jsxs(material.Paper, { sx: { p: 3 }, children: [
223
226
  /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", gutterBottom: true, children: "Create Budget" }),
@@ -310,7 +313,7 @@ function BudgetEditForm({
310
313
  }) {
311
314
  const [name, setName] = react.useState(budget.name);
312
315
  const [budgetAmount, setBudgetAmount] = react.useState(budget.budget_amount.toString());
313
- const [tagNames, setTagNames] = react.useState(budget.tag_names.join(", "));
316
+ const [tagNames, setTagNames] = react.useState(budget.budget_tags.map((bt) => bt.tag_name).join(", "));
314
317
  const [error, setError] = react.useState(null);
315
318
  const updateBudget = budgetsDataAccess.useUpdateBudget({
316
319
  onSuccess: () => {
@@ -341,10 +344,9 @@ function BudgetEditForm({
341
344
  id: budget.id,
342
345
  name: name.trim(),
343
346
  budget_amount: amount,
344
- tag_names: tags,
345
347
  show_on_dashboard: true
346
348
  };
347
- updateBudget.mutate({ userId, budgetId: budget.id, data });
349
+ updateBudget.mutate({ userId, budgetId: budget.id, data, tagNames: tags });
348
350
  };
349
351
  const handleClose = () => {
350
352
  if (!updateBudget.isPending) {
@@ -525,10 +527,10 @@ function BudgetProgressCard({
525
527
  /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 2 }, children: [
526
528
  /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { children: [
527
529
  /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", gutterBottom: true, children: budget.name }),
528
- budget.tag_names.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
530
+ budget.budget_tags.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
529
531
  material.Chip,
530
532
  {
531
- label: budget.tag_names[0],
533
+ label: budget.budget_tags[0].tag_name,
532
534
  size: "small",
533
535
  variant: "outlined",
534
536
  sx: { mb: 1 }
@@ -630,8 +632,8 @@ function BudgetProgressList({
630
632
  const categories = react.useMemo(() => {
631
633
  const uniqueCategories = /* @__PURE__ */ new Set();
632
634
  budgets.forEach((budget) => {
633
- budget.tag_names.forEach((tag) => {
634
- uniqueCategories.add(tag);
635
+ budget.budget_tags.forEach((bt) => {
636
+ uniqueCategories.add(bt.tag_name);
635
637
  });
636
638
  });
637
639
  return Array.from(uniqueCategories).sort();
@@ -644,7 +646,7 @@ function BudgetProgressList({
644
646
  if (statusFilter === "on-track" && percentage > 70) return false;
645
647
  if (statusFilter === "warning" && (percentage <= 70 || percentage > 90)) return false;
646
648
  if (statusFilter === "over-budget" && percentage <= 90) return false;
647
- if (categoryFilter !== "all" && !budget.tag_names.includes(categoryFilter)) return false;
649
+ if (categoryFilter !== "all" && !budget.budget_tags.some((bt) => bt.tag_name === categoryFilter)) return false;
648
650
  return true;
649
651
  });
650
652
  }, [budgets, spentByBudget, statusFilter, categoryFilter]);
@@ -1090,6 +1092,668 @@ function BudgetInsightDialog({
1090
1092
  }
1091
1093
  );
1092
1094
  }
1095
+ function getStateConfig(state, theme) {
1096
+ switch (state) {
1097
+ case "under":
1098
+ return {
1099
+ color: theme.palette.success.main,
1100
+ bgColor: material.alpha(theme.palette.success.main, 0.1),
1101
+ icon: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.TrendingDown, {}),
1102
+ label: "Under Budget",
1103
+ chipColor: "success"
1104
+ };
1105
+ case "on-track":
1106
+ return {
1107
+ color: theme.palette.info.main,
1108
+ bgColor: material.alpha(theme.palette.info.main, 0.1),
1109
+ icon: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.CheckCircle, {}),
1110
+ label: "On Track",
1111
+ chipColor: "info"
1112
+ };
1113
+ case "over":
1114
+ return {
1115
+ color: theme.palette.warning.main,
1116
+ bgColor: material.alpha(theme.palette.warning.main, 0.1),
1117
+ icon: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.Warning, {}),
1118
+ label: "Over Budget",
1119
+ chipColor: "warning"
1120
+ };
1121
+ case "critical":
1122
+ return {
1123
+ color: theme.palette.error.main,
1124
+ bgColor: material.alpha(theme.palette.error.main, 0.1),
1125
+ icon: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.Error, {}),
1126
+ label: "Critical",
1127
+ chipColor: "error"
1128
+ };
1129
+ }
1130
+ }
1131
+ function getSizeConfig(size) {
1132
+ switch (size) {
1133
+ case "small":
1134
+ return {
1135
+ iconSize: 16,
1136
+ fontSize: "0.75rem",
1137
+ spacing: 0.5
1138
+ };
1139
+ case "medium":
1140
+ return {
1141
+ iconSize: 20,
1142
+ fontSize: "0.875rem",
1143
+ spacing: 1
1144
+ };
1145
+ case "large":
1146
+ return {
1147
+ iconSize: 24,
1148
+ fontSize: "1rem",
1149
+ spacing: 1.5
1150
+ };
1151
+ }
1152
+ }
1153
+ function BudgetStateIndicator({
1154
+ state,
1155
+ percentage,
1156
+ variant = "chip",
1157
+ size = "medium",
1158
+ showPercentage = true,
1159
+ showLabel = true,
1160
+ customLabel
1161
+ }) {
1162
+ const theme = material.useTheme();
1163
+ const config = getStateConfig(state, theme);
1164
+ const sizeConfig = getSizeConfig(size);
1165
+ const label = customLabel || config.label;
1166
+ const displayText = showPercentage ? `${label} (${percentage.toFixed(0)}%)` : label;
1167
+ if (variant === "chip") {
1168
+ return /* @__PURE__ */ jsxRuntime.jsx(
1169
+ material.Chip,
1170
+ {
1171
+ "data-testid": "budget-state-indicator",
1172
+ icon: config.icon,
1173
+ label: displayText,
1174
+ color: config.chipColor,
1175
+ size: size === "large" ? "medium" : "small",
1176
+ sx: {
1177
+ fontSize: sizeConfig.fontSize,
1178
+ fontWeight: "medium"
1179
+ }
1180
+ }
1181
+ );
1182
+ }
1183
+ if (variant === "inline") {
1184
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1185
+ material.Box,
1186
+ {
1187
+ "data-testid": "budget-state-indicator",
1188
+ sx: {
1189
+ display: "inline-flex",
1190
+ alignItems: "center",
1191
+ gap: sizeConfig.spacing,
1192
+ color: config.color
1193
+ },
1194
+ children: [
1195
+ /* @__PURE__ */ jsxRuntime.jsx(
1196
+ material.Box,
1197
+ {
1198
+ sx: {
1199
+ display: "flex",
1200
+ alignItems: "center",
1201
+ fontSize: sizeConfig.iconSize
1202
+ },
1203
+ children: config.icon
1204
+ }
1205
+ ),
1206
+ showLabel && /* @__PURE__ */ jsxRuntime.jsx(
1207
+ material.Typography,
1208
+ {
1209
+ component: "span",
1210
+ sx: {
1211
+ fontSize: sizeConfig.fontSize,
1212
+ fontWeight: "medium"
1213
+ },
1214
+ children: displayText
1215
+ }
1216
+ )
1217
+ ]
1218
+ }
1219
+ );
1220
+ }
1221
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1222
+ material.Box,
1223
+ {
1224
+ "data-testid": "budget-state-indicator",
1225
+ sx: {
1226
+ display: "flex",
1227
+ alignItems: "center",
1228
+ gap: sizeConfig.spacing * 2,
1229
+ p: sizeConfig.spacing * 2,
1230
+ bgcolor: config.bgColor,
1231
+ borderRadius: 1,
1232
+ borderLeft: `4px solid ${config.color}`
1233
+ },
1234
+ children: [
1235
+ /* @__PURE__ */ jsxRuntime.jsx(
1236
+ material.Box,
1237
+ {
1238
+ sx: {
1239
+ color: config.color,
1240
+ display: "flex",
1241
+ alignItems: "center",
1242
+ fontSize: sizeConfig.iconSize * 1.5
1243
+ },
1244
+ children: config.icon
1245
+ }
1246
+ ),
1247
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { flex: 1 }, children: [
1248
+ showLabel && /* @__PURE__ */ jsxRuntime.jsx(
1249
+ material.Typography,
1250
+ {
1251
+ variant: "subtitle2",
1252
+ sx: {
1253
+ color: config.color,
1254
+ fontWeight: "bold",
1255
+ fontSize: sizeConfig.fontSize
1256
+ },
1257
+ children: label
1258
+ }
1259
+ ),
1260
+ showPercentage && /* @__PURE__ */ jsxRuntime.jsxs(
1261
+ material.Typography,
1262
+ {
1263
+ variant: "body2",
1264
+ color: "text.secondary",
1265
+ sx: { fontSize: sizeConfig.fontSize },
1266
+ children: [
1267
+ percentage.toFixed(1),
1268
+ "% of budget used"
1269
+ ]
1270
+ }
1271
+ )
1272
+ ] })
1273
+ ]
1274
+ }
1275
+ );
1276
+ }
1277
+ var defaultCurrencyFormatter3 = (value) => {
1278
+ return new Intl.NumberFormat("en-US", {
1279
+ style: "currency",
1280
+ currency: "USD",
1281
+ minimumFractionDigits: 0,
1282
+ maximumFractionDigits: 0
1283
+ }).format(value);
1284
+ };
1285
+ function getTrendConfig(trend, theme) {
1286
+ switch (trend) {
1287
+ case "increasing":
1288
+ return {
1289
+ color: theme.palette.warning.main,
1290
+ bgColor: material.alpha(theme.palette.warning.main, 0.1),
1291
+ label: "Increasing"
1292
+ };
1293
+ case "decreasing":
1294
+ return {
1295
+ color: theme.palette.success.main,
1296
+ bgColor: material.alpha(theme.palette.success.main, 0.1),
1297
+ label: "Decreasing"
1298
+ };
1299
+ case "stable":
1300
+ return {
1301
+ color: theme.palette.info.main,
1302
+ bgColor: material.alpha(theme.palette.info.main, 0.1),
1303
+ label: "Stable"
1304
+ };
1305
+ }
1306
+ }
1307
+ function BudgetTagInsight({
1308
+ insights,
1309
+ loading = false,
1310
+ onCreateBudget,
1311
+ onViewDetails,
1312
+ onDismiss,
1313
+ currencyFormatter = defaultCurrencyFormatter3,
1314
+ maxVisible = 5
1315
+ }) {
1316
+ const theme = material.useTheme();
1317
+ const visibleInsights = insights.slice(0, maxVisible);
1318
+ const unbudgetedCount = insights.filter((i) => !i.isBudgeted).length;
1319
+ if (loading) {
1320
+ return /* @__PURE__ */ jsxRuntime.jsx(material.Card, { "data-testid": "budget-tag-insight", children: /* @__PURE__ */ jsxRuntime.jsx(material.CardContent, { children: /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", sx: { textAlign: "center", py: 2 }, children: "Loading tag insights..." }) }) });
1321
+ }
1322
+ if (insights.length === 0) {
1323
+ return /* @__PURE__ */ jsxRuntime.jsx(material.Card, { "data-testid": "budget-tag-insight", children: /* @__PURE__ */ jsxRuntime.jsx(material.CardContent, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { textAlign: "center", py: 3 }, children: [
1324
+ /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.Info, { sx: { fontSize: 48, color: "text.disabled", mb: 1 } }),
1325
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "No tag insights available yet. Start tracking your expenses!" })
1326
+ ] }) }) });
1327
+ }
1328
+ return /* @__PURE__ */ jsxRuntime.jsx(material.Card, { "data-testid": "budget-tag-insight", children: /* @__PURE__ */ jsxRuntime.jsxs(material.CardContent, { children: [
1329
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", alignItems: "center", gap: 1, mb: 2 }, children: [
1330
+ /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.LocalOffer, { color: "primary" }),
1331
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", children: "Tag Spending Insights" }),
1332
+ unbudgetedCount > 0 && /* @__PURE__ */ jsxRuntime.jsx(
1333
+ material.Chip,
1334
+ {
1335
+ label: `${unbudgetedCount} unbudgeted`,
1336
+ size: "small",
1337
+ color: "warning",
1338
+ sx: { ml: "auto" }
1339
+ }
1340
+ )
1341
+ ] }),
1342
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", sx: { mb: 2 }, children: "Discover spending patterns and create budgets for frequently used tags." }),
1343
+ /* @__PURE__ */ jsxRuntime.jsx(material.Divider, { sx: { mb: 2 } }),
1344
+ visibleInsights.map((insight) => {
1345
+ const trendConfig = getTrendConfig(insight.trend, theme);
1346
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1347
+ material.Box,
1348
+ {
1349
+ "data-testid": `tag-insight-${insight.id}`,
1350
+ sx: {
1351
+ p: 2,
1352
+ mb: 2,
1353
+ bgcolor: insight.isBudgeted ? material.alpha(theme.palette.success.main, 0.05) : material.alpha(theme.palette.warning.main, 0.05),
1354
+ borderRadius: 1,
1355
+ borderLeft: insight.isBudgeted ? `4px solid ${theme.palette.success.main}` : `4px solid ${theme.palette.warning.main}`
1356
+ },
1357
+ children: [
1358
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", alignItems: "center", gap: 1, mb: 1 }, children: [
1359
+ /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.LocalOffer, { fontSize: "small", color: "action" }),
1360
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", sx: { fontWeight: "bold" }, children: insight.tagName }),
1361
+ insight.isBudgeted && /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { label: "Budgeted", size: "small", color: "success", variant: "outlined" }),
1362
+ !insight.isBudgeted && /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { label: "No Budget", size: "small", color: "warning", variant: "outlined" })
1363
+ ] }),
1364
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { mb: 2 }, children: [
1365
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", sx: { mb: 0.5 }, children: insight.recommendation }),
1366
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", alignItems: "center", gap: 1, mt: 1 }, children: [
1367
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", color: "primary", children: currencyFormatter(insight.averageMonthlySpend) }),
1368
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", children: "avg/month" })
1369
+ ] })
1370
+ ] }),
1371
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", gap: 1, flexWrap: "wrap", mb: 2 }, children: [
1372
+ /* @__PURE__ */ jsxRuntime.jsx(
1373
+ material.Chip,
1374
+ {
1375
+ label: `${insight.transactionCount} transactions`,
1376
+ size: "small",
1377
+ variant: "outlined"
1378
+ }
1379
+ ),
1380
+ /* @__PURE__ */ jsxRuntime.jsx(
1381
+ material.Chip,
1382
+ {
1383
+ label: `${insight.monthsAnalyzed} months`,
1384
+ size: "small",
1385
+ variant: "outlined"
1386
+ }
1387
+ ),
1388
+ /* @__PURE__ */ jsxRuntime.jsx(
1389
+ material.Chip,
1390
+ {
1391
+ icon: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.TrendingUp, {}),
1392
+ label: `${trendConfig.label} ${Math.abs(insight.trendPercentage).toFixed(0)}%`,
1393
+ size: "small",
1394
+ sx: {
1395
+ bgcolor: trendConfig.bgColor,
1396
+ color: trendConfig.color,
1397
+ borderColor: trendConfig.color
1398
+ },
1399
+ variant: "outlined"
1400
+ }
1401
+ )
1402
+ ] }),
1403
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", gap: 1, justifyContent: "flex-end" }, children: [
1404
+ onDismiss && /* @__PURE__ */ jsxRuntime.jsx(material.Button, { size: "small", onClick: () => onDismiss(insight), children: "Dismiss" }),
1405
+ onViewDetails && /* @__PURE__ */ jsxRuntime.jsx(material.Button, { size: "small", onClick: () => onViewDetails(insight), children: "View Details" }),
1406
+ !insight.isBudgeted && onCreateBudget && /* @__PURE__ */ jsxRuntime.jsxs(
1407
+ material.Button,
1408
+ {
1409
+ size: "small",
1410
+ variant: "contained",
1411
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(iconsMaterial.Add, {}),
1412
+ onClick: () => onCreateBudget(insight),
1413
+ children: [
1414
+ "Create Budget",
1415
+ insight.suggestedBudget && ` (${currencyFormatter(insight.suggestedBudget)})`
1416
+ ]
1417
+ }
1418
+ )
1419
+ ] })
1420
+ ]
1421
+ },
1422
+ insight.id
1423
+ );
1424
+ }),
1425
+ insights.length > maxVisible && /* @__PURE__ */ jsxRuntime.jsxs(
1426
+ material.Typography,
1427
+ {
1428
+ variant: "caption",
1429
+ color: "text.secondary",
1430
+ sx: { textAlign: "center", display: "block", mt: 1 },
1431
+ children: [
1432
+ "Showing ",
1433
+ maxVisible,
1434
+ " of ",
1435
+ insights.length,
1436
+ " insights"
1437
+ ]
1438
+ }
1439
+ )
1440
+ ] }) });
1441
+ }
1442
+ var defaultCurrencyFormatter4 = (value) => {
1443
+ return new Intl.NumberFormat("en-US", {
1444
+ style: "currency",
1445
+ currency: "USD",
1446
+ minimumFractionDigits: 0,
1447
+ maximumFractionDigits: 0
1448
+ }).format(Math.abs(value));
1449
+ };
1450
+ function getOverviewMessage(data, currencyFormatter) {
1451
+ const { totalBudgets, budgetsOver, budgetsOnTrack, budgetsUnder, totalOverUnder, unbudgetedTagsCount } = data;
1452
+ if (totalBudgets === 0) {
1453
+ return {
1454
+ type: "info",
1455
+ title: "No Budgets Yet",
1456
+ message: "Create your first budget to start tracking your spending and reach your financial goals.",
1457
+ defaultActionLabel: "Create Budget"
1458
+ };
1459
+ }
1460
+ if (budgetsOver === totalBudgets && totalOverUnder < 0) {
1461
+ return {
1462
+ type: "error",
1463
+ title: "All Budgets Over Limit",
1464
+ message: `You've exceeded all ${totalBudgets} budget${totalBudgets > 1 ? "s" : ""} by ${currencyFormatter(totalOverUnder)} total. Review your spending and adjust budgets as needed.`,
1465
+ defaultActionLabel: "Review Budgets"
1466
+ };
1467
+ }
1468
+ if (budgetsOver > 0) {
1469
+ return {
1470
+ type: "warning",
1471
+ title: `${budgetsOver} Budget${budgetsOver > 1 ? "s" : ""} Over Limit`,
1472
+ message: `You're over budget on ${budgetsOver} of ${totalBudgets} budgets by ${currencyFormatter(totalOverUnder)} total. Consider adjusting these budgets.`,
1473
+ defaultActionLabel: "View Details"
1474
+ };
1475
+ }
1476
+ if (budgetsOnTrack === totalBudgets) {
1477
+ const unbudgetedMsg = unbudgetedTagsCount && unbudgetedTagsCount > 0 ? ` You have ${unbudgetedTagsCount} unbudgeted tag${unbudgetedTagsCount > 1 ? "s" : ""} worth reviewing.` : "";
1478
+ return {
1479
+ type: "success",
1480
+ title: "All Budgets On Track",
1481
+ message: `Great job! All ${totalBudgets} budget${totalBudgets > 1 ? "s are" : " is"} on track.${unbudgetedMsg}`,
1482
+ defaultActionLabel: unbudgetedTagsCount && unbudgetedTagsCount > 0 ? "Review Tags" : "View Budgets"
1483
+ };
1484
+ }
1485
+ if (budgetsUnder > 0 && budgetsOver === 0) {
1486
+ return {
1487
+ type: "success",
1488
+ title: "Budgets Under Control",
1489
+ message: `You're under budget by ${currencyFormatter(totalOverUnder)} total across ${budgetsUnder} budget${budgetsUnder > 1 ? "s" : ""}. Keep up the great work!`,
1490
+ defaultActionLabel: "View Details"
1491
+ };
1492
+ }
1493
+ return {
1494
+ type: "info",
1495
+ title: "Budgets Status",
1496
+ message: `${budgetsOnTrack} on track, ${budgetsUnder} under budget. You're managing your budgets well overall.`,
1497
+ defaultActionLabel: "View All Budgets"
1498
+ };
1499
+ }
1500
+ function BudgetOverviewMessage({
1501
+ data,
1502
+ onAction,
1503
+ actionLabel,
1504
+ currencyFormatter = defaultCurrencyFormatter4,
1505
+ hideAction = false
1506
+ }) {
1507
+ const overview = getOverviewMessage(data, currencyFormatter);
1508
+ const severityMap = {
1509
+ success: "success",
1510
+ warning: "warning",
1511
+ error: "error",
1512
+ info: "info"
1513
+ };
1514
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1515
+ material.Alert,
1516
+ {
1517
+ "data-testid": "budget-overview-message",
1518
+ severity: severityMap[overview.type],
1519
+ sx: {
1520
+ "& .MuiAlert-message": {
1521
+ width: "100%"
1522
+ }
1523
+ },
1524
+ action: !hideAction && onAction ? /* @__PURE__ */ jsxRuntime.jsx(
1525
+ material.Button,
1526
+ {
1527
+ color: "inherit",
1528
+ size: "small",
1529
+ onClick: onAction,
1530
+ sx: { whiteSpace: "nowrap" },
1531
+ children: actionLabel || overview.defaultActionLabel
1532
+ }
1533
+ ) : void 0,
1534
+ children: [
1535
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", sx: { fontWeight: "bold", mb: 0.5 }, children: overview.title }),
1536
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", children: overview.message })
1537
+ ]
1538
+ }
1539
+ );
1540
+ }
1541
+ var steps = ["Basic Information", "Select Tags", "Review"];
1542
+ function validateBasicInfo(name, budgetAmount) {
1543
+ if (!name.trim()) {
1544
+ return "Budget name is required";
1545
+ }
1546
+ if (name.trim().length < 3) {
1547
+ return "Budget name must be at least 3 characters";
1548
+ }
1549
+ const amount = parseFloat(budgetAmount);
1550
+ if (isNaN(amount) || amount <= 0) {
1551
+ return "Budget amount must be a positive number";
1552
+ }
1553
+ if (amount > 1e6) {
1554
+ return "Budget amount cannot exceed $1,000,000";
1555
+ }
1556
+ return null;
1557
+ }
1558
+ function validateTags(tagNames) {
1559
+ if (tagNames.length === 0) {
1560
+ return "At least one tag is required";
1561
+ }
1562
+ if (tagNames.length > 10) {
1563
+ return "Maximum 10 tags allowed";
1564
+ }
1565
+ return null;
1566
+ }
1567
+ function BudgetStepper({
1568
+ initialData,
1569
+ availableTags,
1570
+ onSubmit,
1571
+ onCancel,
1572
+ editMode = false,
1573
+ submitting = false,
1574
+ error
1575
+ }) {
1576
+ const [activeStep, setActiveStep] = react.useState(0);
1577
+ const [name, setName] = react.useState(initialData?.name || "");
1578
+ const [budgetAmount, setBudgetAmount] = react.useState(
1579
+ initialData?.budgetAmount?.toString() || ""
1580
+ );
1581
+ const [tagNames, setTagNames] = react.useState(initialData?.tagNames || []);
1582
+ const [validationError, setValidationError] = react.useState(null);
1583
+ const handleNext = () => {
1584
+ setValidationError(null);
1585
+ if (activeStep === 0) {
1586
+ const error2 = validateBasicInfo(name, budgetAmount);
1587
+ if (error2) {
1588
+ setValidationError(error2);
1589
+ return;
1590
+ }
1591
+ } else if (activeStep === 1) {
1592
+ const error2 = validateTags(tagNames);
1593
+ if (error2) {
1594
+ setValidationError(error2);
1595
+ return;
1596
+ }
1597
+ }
1598
+ setActiveStep((prevStep) => prevStep + 1);
1599
+ };
1600
+ const handleBack = () => {
1601
+ setValidationError(null);
1602
+ setActiveStep((prevStep) => prevStep - 1);
1603
+ };
1604
+ const handleSubmit = async () => {
1605
+ const data = {
1606
+ name: name.trim(),
1607
+ budgetAmount: parseFloat(budgetAmount),
1608
+ tagNames,
1609
+ showOnDashboard: true
1610
+ };
1611
+ await onSubmit(data);
1612
+ };
1613
+ const renderStepContent = (step) => {
1614
+ switch (step) {
1615
+ case 0:
1616
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", flexDirection: "column", gap: 3 }, children: [
1617
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "Enter basic budget information" }),
1618
+ /* @__PURE__ */ jsxRuntime.jsx(
1619
+ material.TextField,
1620
+ {
1621
+ label: "Budget Name",
1622
+ value: name,
1623
+ onChange: (e) => {
1624
+ setName(e.target.value);
1625
+ setValidationError(null);
1626
+ },
1627
+ required: true,
1628
+ fullWidth: true,
1629
+ disabled: submitting,
1630
+ error: !!validationError && !name.trim(),
1631
+ helperText: "e.g., Monthly Groceries, Car Maintenance",
1632
+ inputProps: {
1633
+ maxLength: 255
1634
+ }
1635
+ }
1636
+ ),
1637
+ /* @__PURE__ */ jsxRuntime.jsx(
1638
+ material.TextField,
1639
+ {
1640
+ label: "Budget Amount",
1641
+ type: "number",
1642
+ value: budgetAmount,
1643
+ onChange: (e) => {
1644
+ setBudgetAmount(e.target.value);
1645
+ setValidationError(null);
1646
+ },
1647
+ required: true,
1648
+ fullWidth: true,
1649
+ disabled: submitting,
1650
+ error: !!validationError && !budgetAmount,
1651
+ helperText: "Maximum spending limit for this budget",
1652
+ InputProps: {
1653
+ startAdornment: /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { sx: { mr: 0.5 }, children: "$" })
1654
+ },
1655
+ inputProps: {
1656
+ min: 0,
1657
+ step: 1
1658
+ }
1659
+ }
1660
+ )
1661
+ ] });
1662
+ case 1:
1663
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", flexDirection: "column", gap: 3 }, children: [
1664
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "Select tags to track spending against this budget" }),
1665
+ /* @__PURE__ */ jsxRuntime.jsx(
1666
+ material.Autocomplete,
1667
+ {
1668
+ multiple: true,
1669
+ options: availableTags,
1670
+ value: tagNames,
1671
+ onChange: (_, newValue) => {
1672
+ setTagNames(newValue);
1673
+ setValidationError(null);
1674
+ },
1675
+ disabled: submitting,
1676
+ renderInput: (params) => /* @__PURE__ */ jsxRuntime.jsx(
1677
+ material.TextField,
1678
+ {
1679
+ ...params,
1680
+ label: "Tags",
1681
+ placeholder: "Select tags...",
1682
+ required: true,
1683
+ error: !!validationError && tagNames.length === 0,
1684
+ helperText: "Choose tags related to this budget (e.g., Groceries, Dining Out)"
1685
+ }
1686
+ ),
1687
+ renderTags: (value, getTagProps) => value.map((option, index) => /* @__PURE__ */ react.createElement(
1688
+ material.Chip,
1689
+ {
1690
+ label: option,
1691
+ ...getTagProps({ index }),
1692
+ key: option
1693
+ }
1694
+ ))
1695
+ }
1696
+ ),
1697
+ tagNames.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(material.Box, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "caption", color: "text.secondary", children: [
1698
+ tagNames.length,
1699
+ " tag",
1700
+ tagNames.length > 1 ? "s" : "",
1701
+ " selected"
1702
+ ] }) })
1703
+ ] });
1704
+ case 2:
1705
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", flexDirection: "column", gap: 3 }, children: [
1706
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "body2", color: "text.secondary", children: [
1707
+ "Review your budget before ",
1708
+ editMode ? "saving changes" : "creating"
1709
+ ] }),
1710
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Paper, { sx: { p: 2, bgcolor: "background.default" }, children: [
1711
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", gutterBottom: true, children: "Budget Name" }),
1712
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body1", sx: { mb: 2, fontWeight: "medium" }, children: name }),
1713
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", gutterBottom: true, children: "Budget Amount" }),
1714
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "h6", color: "primary", sx: { mb: 2 }, children: [
1715
+ "$",
1716
+ parseFloat(budgetAmount).toLocaleString("en-US", {
1717
+ minimumFractionDigits: 2,
1718
+ maximumFractionDigits: 2
1719
+ })
1720
+ ] }),
1721
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "subtitle2", gutterBottom: true, children: [
1722
+ "Tags (",
1723
+ tagNames.length,
1724
+ ")"
1725
+ ] }),
1726
+ /* @__PURE__ */ jsxRuntime.jsx(material.Box, { sx: { display: "flex", flexWrap: "wrap", gap: 1 }, children: tagNames.map((tag) => /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { label: tag, size: "small" }, tag)) })
1727
+ ] })
1728
+ ] });
1729
+ default:
1730
+ return null;
1731
+ }
1732
+ };
1733
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.Paper, { "data-testid": "budget-stepper", sx: { p: 3 }, children: [
1734
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", gutterBottom: true, children: editMode ? "Edit Budget" : "Create Budget" }),
1735
+ /* @__PURE__ */ jsxRuntime.jsx(material.Stepper, { activeStep, sx: { my: 3 }, children: steps.map((label) => /* @__PURE__ */ jsxRuntime.jsx(material.Step, { children: /* @__PURE__ */ jsxRuntime.jsx(material.StepLabel, { children: label }) }, label)) }),
1736
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { mt: 3, mb: 2 }, children: [
1737
+ (validationError || error) && /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "error", sx: { mb: 2 }, children: validationError || error }),
1738
+ renderStepContent(activeStep)
1739
+ ] }),
1740
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", justifyContent: "space-between", mt: 3 }, children: [
1741
+ /* @__PURE__ */ jsxRuntime.jsx(material.Box, { children: onCancel && /* @__PURE__ */ jsxRuntime.jsx(material.Button, { onClick: onCancel, disabled: submitting, children: "Cancel" }) }),
1742
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { sx: { display: "flex", gap: 1 }, children: [
1743
+ /* @__PURE__ */ jsxRuntime.jsx(material.Button, { onClick: handleBack, disabled: activeStep === 0 || submitting, children: "Back" }),
1744
+ activeStep < steps.length - 1 ? /* @__PURE__ */ jsxRuntime.jsx(material.Button, { variant: "contained", onClick: handleNext, disabled: submitting, children: "Next" }) : /* @__PURE__ */ jsxRuntime.jsx(
1745
+ material.Button,
1746
+ {
1747
+ variant: "contained",
1748
+ onClick: handleSubmit,
1749
+ disabled: submitting,
1750
+ children: submitting ? editMode ? "Saving..." : "Creating..." : editMode ? "Save Changes" : "Create Budget"
1751
+ }
1752
+ )
1753
+ ] })
1754
+ ] })
1755
+ ] });
1756
+ }
1093
1757
 
1094
1758
  exports.BudgetCreateForm = BudgetCreateForm;
1095
1759
  exports.BudgetDeleteButton = BudgetDeleteButton;
@@ -1097,8 +1761,12 @@ exports.BudgetEditForm = BudgetEditForm;
1097
1761
  exports.BudgetInsightDialog = BudgetInsightDialog;
1098
1762
  exports.BudgetInsightsCard = BudgetInsightsCard;
1099
1763
  exports.BudgetList = BudgetList;
1764
+ exports.BudgetOverviewMessage = BudgetOverviewMessage;
1100
1765
  exports.BudgetProgressCard = BudgetProgressCard;
1101
1766
  exports.BudgetProgressList = BudgetProgressList;
1767
+ exports.BudgetStateIndicator = BudgetStateIndicator;
1768
+ exports.BudgetStepper = BudgetStepper;
1102
1769
  exports.BudgetSummary = BudgetSummary;
1770
+ exports.BudgetTagInsight = BudgetTagInsight;
1103
1771
  //# sourceMappingURL=index.cjs.map
1104
1772
  //# sourceMappingURL=index.cjs.map