@burdenoff/microfe-planmagnet 2026.928.4 → 2026.928.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
  2. package/dist/planmagnet/PlanMagnetRoot.js +130 -97
  3. package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
  4. package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
  5. package/dist/planmagnet/PlanMagnetRoutes.js +256 -156
  6. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  7. package/dist/planmagnet/automation/AutomationExecutionsPanel.d.ts +5 -0
  8. package/dist/planmagnet/automation/AutomationExecutionsPanel.d.ts.map +1 -0
  9. package/dist/planmagnet/automation/AutomationExecutionsPanel.js +107 -0
  10. package/dist/planmagnet/automation/AutomationExecutionsPanel.js.map +1 -0
  11. package/dist/planmagnet/automation/AutomationRuleFields.d.ts +29 -0
  12. package/dist/planmagnet/automation/AutomationRuleFields.d.ts.map +1 -0
  13. package/dist/planmagnet/automation/AutomationRuleFields.js +308 -0
  14. package/dist/planmagnet/automation/AutomationRuleFields.js.map +1 -0
  15. package/dist/planmagnet/automation/automationRuleTypes.d.ts +60 -0
  16. package/dist/planmagnet/automation/automationRuleTypes.d.ts.map +1 -0
  17. package/dist/planmagnet/automation/automationRuleTypes.js +113 -0
  18. package/dist/planmagnet/automation/automationRuleTypes.js.map +1 -0
  19. package/dist/planmagnet/board/BulkActions.d.ts +11 -0
  20. package/dist/planmagnet/board/BulkActions.d.ts.map +1 -0
  21. package/dist/planmagnet/board/BulkActions.js +182 -0
  22. package/dist/planmagnet/board/BulkActions.js.map +1 -0
  23. package/dist/planmagnet/board/ProjectBoard.d.ts.map +1 -1
  24. package/dist/planmagnet/board/ProjectBoard.js +289 -254
  25. package/dist/planmagnet/board/ProjectBoard.js.map +1 -1
  26. package/dist/planmagnet/budgets/BudgetControls.d.ts +30 -0
  27. package/dist/planmagnet/budgets/BudgetControls.d.ts.map +1 -0
  28. package/dist/planmagnet/budgets/BudgetControls.js +470 -0
  29. package/dist/planmagnet/budgets/BudgetControls.js.map +1 -0
  30. package/dist/planmagnet/budgets/ExpenseControls.d.ts +27 -0
  31. package/dist/planmagnet/budgets/ExpenseControls.d.ts.map +1 -0
  32. package/dist/planmagnet/budgets/ExpenseControls.js +366 -0
  33. package/dist/planmagnet/budgets/ExpenseControls.js.map +1 -0
  34. package/dist/planmagnet/budgets/ProjectBudgetsSummary.d.ts +5 -0
  35. package/dist/planmagnet/budgets/ProjectBudgetsSummary.d.ts.map +1 -0
  36. package/dist/planmagnet/budgets/ProjectBudgetsSummary.js +112 -0
  37. package/dist/planmagnet/budgets/ProjectBudgetsSummary.js.map +1 -0
  38. package/dist/planmagnet/budgets/budgetMath.d.ts +26 -0
  39. package/dist/planmagnet/budgets/budgetMath.d.ts.map +1 -0
  40. package/dist/planmagnet/budgets/budgetMath.js +49 -0
  41. package/dist/planmagnet/budgets/budgetMath.js.map +1 -0
  42. package/dist/planmagnet/controls/AutomationRuleControls.d.ts +23 -0
  43. package/dist/planmagnet/controls/AutomationRuleControls.d.ts.map +1 -0
  44. package/dist/planmagnet/controls/AutomationRuleControls.js +172 -0
  45. package/dist/planmagnet/controls/AutomationRuleControls.js.map +1 -0
  46. package/dist/planmagnet/controls/CompetitorControls.js +15 -15
  47. package/dist/planmagnet/controls/CustomFieldControls.d.ts +42 -0
  48. package/dist/planmagnet/controls/CustomFieldControls.d.ts.map +1 -0
  49. package/dist/planmagnet/controls/CustomFieldControls.js +84 -0
  50. package/dist/planmagnet/controls/CustomFieldControls.js.map +1 -0
  51. package/dist/planmagnet/controls/CustomFieldValuesPanel.d.ts +8 -0
  52. package/dist/planmagnet/controls/CustomFieldValuesPanel.d.ts.map +1 -0
  53. package/dist/planmagnet/controls/CustomFieldValuesPanel.js +315 -0
  54. package/dist/planmagnet/controls/CustomFieldValuesPanel.js.map +1 -0
  55. package/dist/planmagnet/controls/FeatureControls.js +54 -54
  56. package/dist/planmagnet/controls/ObjectiveControls.js +15 -15
  57. package/dist/planmagnet/controls/Pickers.d.ts +7 -0
  58. package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
  59. package/dist/planmagnet/controls/Pickers.js +58 -20
  60. package/dist/planmagnet/controls/Pickers.js.map +1 -1
  61. package/dist/planmagnet/controls/PortfolioControls.js +7 -7
  62. package/dist/planmagnet/controls/ProgramControls.js +19 -19
  63. package/dist/planmagnet/controls/ProjectControls.d.ts.map +1 -1
  64. package/dist/planmagnet/controls/ProjectControls.js +54 -47
  65. package/dist/planmagnet/controls/ProjectControls.js.map +1 -1
  66. package/dist/planmagnet/controls/ReleaseControls.js +7 -7
  67. package/dist/planmagnet/controls/RiskControls.d.ts +27 -0
  68. package/dist/planmagnet/controls/RiskControls.d.ts.map +1 -0
  69. package/dist/planmagnet/controls/RiskControls.js +346 -0
  70. package/dist/planmagnet/controls/RiskControls.js.map +1 -0
  71. package/dist/planmagnet/controls/TemplateControls.d.ts +15 -0
  72. package/dist/planmagnet/controls/TemplateControls.d.ts.map +1 -0
  73. package/dist/planmagnet/controls/TemplateControls.js +55 -0
  74. package/dist/planmagnet/controls/TemplateControls.js.map +1 -0
  75. package/dist/planmagnet/controls/WorkItemControls.d.ts +6 -2
  76. package/dist/planmagnet/controls/WorkItemControls.d.ts.map +1 -1
  77. package/dist/planmagnet/controls/WorkItemControls.js +9 -9
  78. package/dist/planmagnet/controls/WorkItemControls.js.map +1 -1
  79. package/dist/planmagnet/create/CreateDialogs.d.ts +20 -0
  80. package/dist/planmagnet/create/CreateDialogs.d.ts.map +1 -1
  81. package/dist/planmagnet/create/CreateDialogs.js +455 -116
  82. package/dist/planmagnet/create/CreateDialogs.js.map +1 -1
  83. package/dist/planmagnet/dependency/DependencyPanel.d.ts +6 -0
  84. package/dist/planmagnet/dependency/DependencyPanel.d.ts.map +1 -0
  85. package/dist/planmagnet/dependency/DependencyPanel.js +310 -0
  86. package/dist/planmagnet/dependency/DependencyPanel.js.map +1 -0
  87. package/dist/planmagnet/pages/AutomationAutomationRuleDetailPagePage.d.ts +2 -0
  88. package/dist/planmagnet/pages/AutomationAutomationRuleDetailPagePage.d.ts.map +1 -0
  89. package/dist/planmagnet/pages/AutomationAutomationRuleDetailPagePage.js +171 -0
  90. package/dist/planmagnet/pages/AutomationAutomationRuleDetailPagePage.js.map +1 -0
  91. package/dist/planmagnet/pages/AutomationAutomationRulesPagePage.d.ts +2 -0
  92. package/dist/planmagnet/pages/AutomationAutomationRulesPagePage.d.ts.map +1 -0
  93. package/dist/planmagnet/pages/AutomationAutomationRulesPagePage.js +232 -0
  94. package/dist/planmagnet/pages/AutomationAutomationRulesPagePage.js.map +1 -0
  95. package/dist/planmagnet/pages/AutomationCreateAutomationRulePagePage.d.ts +2 -0
  96. package/dist/planmagnet/pages/AutomationCreateAutomationRulePagePage.d.ts.map +1 -0
  97. package/dist/planmagnet/pages/AutomationCreateAutomationRulePagePage.js +93 -0
  98. package/dist/planmagnet/pages/AutomationCreateAutomationRulePagePage.js.map +1 -0
  99. package/dist/planmagnet/pages/DashboardDashboardPagePage.d.ts.map +1 -1
  100. package/dist/planmagnet/pages/DashboardDashboardPagePage.js +11 -3
  101. package/dist/planmagnet/pages/DashboardDashboardPagePage.js.map +1 -1
  102. package/dist/planmagnet/pages/PortfoliosEditPortfolioPagePage.js +5 -5
  103. package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js +9 -9
  104. package/dist/planmagnet/pages/PortfoliosPortfoliosPagePage.js +6 -6
  105. package/dist/planmagnet/pages/ProgramsCreateProgramPagePage.js +4 -4
  106. package/dist/planmagnet/pages/ProgramsEditProgramPagePage.js +6 -6
  107. package/dist/planmagnet/pages/ProgramsProgramsPagePage.js +5 -5
  108. package/dist/planmagnet/pages/ProjectsBudgetDetailPagePage.d.ts +2 -0
  109. package/dist/planmagnet/pages/ProjectsBudgetDetailPagePage.d.ts.map +1 -0
  110. package/dist/planmagnet/pages/ProjectsBudgetDetailPagePage.js +315 -0
  111. package/dist/planmagnet/pages/ProjectsBudgetDetailPagePage.js.map +1 -0
  112. package/dist/planmagnet/pages/ProjectsBudgetsPagePage.d.ts +2 -0
  113. package/dist/planmagnet/pages/ProjectsBudgetsPagePage.d.ts.map +1 -0
  114. package/dist/planmagnet/pages/ProjectsBudgetsPagePage.js +186 -0
  115. package/dist/planmagnet/pages/ProjectsBudgetsPagePage.js.map +1 -0
  116. package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.d.ts +2 -0
  117. package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.d.ts.map +1 -0
  118. package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.js +164 -0
  119. package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.js.map +1 -0
  120. package/dist/planmagnet/pages/ProjectsCustomFieldsPagePage.d.ts +2 -0
  121. package/dist/planmagnet/pages/ProjectsCustomFieldsPagePage.d.ts.map +1 -0
  122. package/dist/planmagnet/pages/ProjectsCustomFieldsPagePage.js +348 -0
  123. package/dist/planmagnet/pages/ProjectsCustomFieldsPagePage.js.map +1 -0
  124. package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js +10 -10
  125. package/dist/planmagnet/pages/ProjectsMilestoneDetailPagePage.js +4 -4
  126. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
  127. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +20 -10
  128. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
  129. package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.d.ts.map +1 -1
  130. package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.js +13 -8
  131. package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.js.map +1 -1
  132. package/dist/planmagnet/pages/ProjectsPlanningPagePage.d.ts.map +1 -1
  133. package/dist/planmagnet/pages/ProjectsPlanningPagePage.js +76 -56
  134. package/dist/planmagnet/pages/ProjectsPlanningPagePage.js.map +1 -1
  135. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
  136. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +372 -203
  137. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
  138. package/dist/planmagnet/pages/ProjectsProjectsPagePage.d.ts.map +1 -1
  139. package/dist/planmagnet/pages/ProjectsProjectsPagePage.js +81 -72
  140. package/dist/planmagnet/pages/ProjectsProjectsPagePage.js.map +1 -1
  141. package/dist/planmagnet/pages/RisksRiskDetailPagePage.d.ts +2 -0
  142. package/dist/planmagnet/pages/RisksRiskDetailPagePage.d.ts.map +1 -0
  143. package/dist/planmagnet/pages/RisksRiskDetailPagePage.js +278 -0
  144. package/dist/planmagnet/pages/RisksRiskDetailPagePage.js.map +1 -0
  145. package/dist/planmagnet/pages/RisksRiskRegisterPagePage.d.ts +2 -0
  146. package/dist/planmagnet/pages/RisksRiskRegisterPagePage.d.ts.map +1 -0
  147. package/dist/planmagnet/pages/RisksRiskRegisterPagePage.js +191 -0
  148. package/dist/planmagnet/pages/RisksRiskRegisterPagePage.js.map +1 -0
  149. package/dist/planmagnet/pages/SprintsStartSprintPagePage.d.ts.map +1 -1
  150. package/dist/planmagnet/pages/SprintsStartSprintPagePage.js +168 -122
  151. package/dist/planmagnet/pages/SprintsStartSprintPagePage.js.map +1 -1
  152. package/dist/planmagnet/pages/TasksCreateTaskPagePage.js +5 -5
  153. package/dist/planmagnet/pages/TasksEditTaskPagePage.js +4 -4
  154. package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
  155. package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +161 -137
  156. package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
  157. package/dist/planmagnet/pages/TasksTasksPagePage.d.ts.map +1 -1
  158. package/dist/planmagnet/pages/TasksTasksPagePage.js +124 -113
  159. package/dist/planmagnet/pages/TasksTasksPagePage.js.map +1 -1
  160. package/dist/planmagnet/pages/TasksTimeEntriesPagePage.d.ts +2 -0
  161. package/dist/planmagnet/pages/TasksTimeEntriesPagePage.d.ts.map +1 -0
  162. package/dist/planmagnet/pages/TasksTimeEntriesPagePage.js +263 -0
  163. package/dist/planmagnet/pages/TasksTimeEntriesPagePage.js.map +1 -0
  164. package/dist/planmagnet/pages/TasksTimesheetDetailPagePage.d.ts +2 -0
  165. package/dist/planmagnet/pages/TasksTimesheetDetailPagePage.d.ts.map +1 -0
  166. package/dist/planmagnet/pages/TasksTimesheetDetailPagePage.js +254 -0
  167. package/dist/planmagnet/pages/TasksTimesheetDetailPagePage.js.map +1 -0
  168. package/dist/planmagnet/pages/TasksTimesheetsPagePage.d.ts +2 -0
  169. package/dist/planmagnet/pages/TasksTimesheetsPagePage.d.ts.map +1 -0
  170. package/dist/planmagnet/pages/TasksTimesheetsPagePage.js +165 -0
  171. package/dist/planmagnet/pages/TasksTimesheetsPagePage.js.map +1 -0
  172. package/dist/planmagnet/pages/TeamCreateTeamPagePage.d.ts.map +1 -1
  173. package/dist/planmagnet/pages/TeamCreateTeamPagePage.js +288 -91
  174. package/dist/planmagnet/pages/TeamCreateTeamPagePage.js.map +1 -1
  175. package/dist/planmagnet/pages/TeamTeamDetailPagePage.d.ts +2 -0
  176. package/dist/planmagnet/pages/TeamTeamDetailPagePage.d.ts.map +1 -0
  177. package/dist/planmagnet/pages/TeamTeamDetailPagePage.js +217 -0
  178. package/dist/planmagnet/pages/TeamTeamDetailPagePage.js.map +1 -0
  179. package/dist/planmagnet/pages/TeamTeamEditPagePage.d.ts +2 -0
  180. package/dist/planmagnet/pages/TeamTeamEditPagePage.d.ts.map +1 -0
  181. package/dist/planmagnet/pages/TeamTeamEditPagePage.js +211 -0
  182. package/dist/planmagnet/pages/TeamTeamEditPagePage.js.map +1 -0
  183. package/dist/planmagnet/pages/TeamTeamsListPagePage.d.ts +2 -0
  184. package/dist/planmagnet/pages/TeamTeamsListPagePage.d.ts.map +1 -0
  185. package/dist/planmagnet/pages/TeamTeamsListPagePage.js +118 -0
  186. package/dist/planmagnet/pages/TeamTeamsListPagePage.js.map +1 -0
  187. package/dist/planmagnet/pages/TemplatesEditTemplatePagePage.d.ts +2 -0
  188. package/dist/planmagnet/pages/TemplatesEditTemplatePagePage.d.ts.map +1 -0
  189. package/dist/planmagnet/pages/TemplatesEditTemplatePagePage.js +316 -0
  190. package/dist/planmagnet/pages/TemplatesEditTemplatePagePage.js.map +1 -0
  191. package/dist/planmagnet/pages/TemplatesTemplatesPagePage.d.ts +2 -0
  192. package/dist/planmagnet/pages/TemplatesTemplatesPagePage.d.ts.map +1 -0
  193. package/dist/planmagnet/pages/TemplatesTemplatesPagePage.js +208 -0
  194. package/dist/planmagnet/pages/TemplatesTemplatesPagePage.js.map +1 -0
  195. package/dist/planmagnet/pages/analytics/DeliveryHealthCards.d.ts +0 -5
  196. package/dist/planmagnet/pages/analytics/DeliveryHealthCards.d.ts.map +1 -1
  197. package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js +47 -89
  198. package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js.map +1 -1
  199. package/dist/planmagnet/roadmaps/RoadmapControls.js +24 -24
  200. package/dist/planmagnet/sprints/SprintControls.d.ts +15 -3
  201. package/dist/planmagnet/sprints/SprintControls.d.ts.map +1 -1
  202. package/dist/planmagnet/sprints/SprintControls.js +293 -190
  203. package/dist/planmagnet/sprints/SprintControls.js.map +1 -1
  204. package/dist/planmagnet/team/TeamControls.d.ts +31 -0
  205. package/dist/planmagnet/team/TeamControls.d.ts.map +1 -0
  206. package/dist/planmagnet/team/TeamControls.js +407 -0
  207. package/dist/planmagnet/team/TeamControls.js.map +1 -0
  208. package/dist/planmagnet/time/TaskTimePanel.d.ts +7 -0
  209. package/dist/planmagnet/time/TaskTimePanel.d.ts.map +1 -0
  210. package/dist/planmagnet/time/TaskTimePanel.js +104 -0
  211. package/dist/planmagnet/time/TaskTimePanel.js.map +1 -0
  212. package/dist/planmagnet/time/TimeEntryControls.d.ts +37 -0
  213. package/dist/planmagnet/time/TimeEntryControls.d.ts.map +1 -0
  214. package/dist/planmagnet/time/TimeEntryControls.js +338 -0
  215. package/dist/planmagnet/time/TimeEntryControls.js.map +1 -0
  216. package/dist/planmagnet/time/TimesheetControls.d.ts +18 -0
  217. package/dist/planmagnet/time/TimesheetControls.d.ts.map +1 -0
  218. package/dist/planmagnet/time/TimesheetControls.js +244 -0
  219. package/dist/planmagnet/time/TimesheetControls.js.map +1 -0
  220. package/dist/planmagnet/time/timeShared.d.ts +29 -0
  221. package/dist/planmagnet/time/timeShared.d.ts.map +1 -0
  222. package/dist/planmagnet/time/timeShared.js +34 -0
  223. package/dist/planmagnet/time/timeShared.js.map +1 -0
  224. package/dist/planmagnet/utils/rowNavigation.js +5 -2
  225. package/dist/planmagnet/utils/rowNavigation.js.map +1 -1
  226. package/package.json +1 -1
@@ -0,0 +1,366 @@
1
+ import { throwOnError as e } from "../utils/mutationResult.js";
2
+ import { ConfirmButton as t, ErrorLine as n } from "../controls/ConfirmButton.js";
3
+ import { WorkItemSearchSelect as r } from "../controls/Pickers.js";
4
+ import { toDateInputValue as i, todayInputValue as a } from "../time/timeShared.js";
5
+ import { useState as o } from "react";
6
+ import { Pencil as s, Plus as c, Trash2 as l } from "lucide-react";
7
+ import { gql as u } from "@apollo/client";
8
+ import { useMutation as d } from "@apollo/client/react";
9
+ import { jsx as f, jsxs as p } from "react/jsx-runtime";
10
+ import { Button as m, Input as h, ResponsiveDialog as g, ResponsiveDialogContent as _, ResponsiveDialogDescription as v, ResponsiveDialogFooter as y, ResponsiveDialogHeader as b, ResponsiveDialogTitle as x, ResponsiveDialogTrigger as S } from "@burdenoff/fe-libs/ui";
11
+ //#region src/planmagnet/budgets/ExpenseControls.tsx
12
+ var C = u`
13
+ mutation ExpenseControlsLog($input: PlanmagnetLogExpenseInput!) {
14
+ planmagnetLogExpense(input: $input) {
15
+ id
16
+ budgetId
17
+ description
18
+ amount
19
+ category
20
+ date
21
+ workItemId
22
+ referenceUrl
23
+ createdAt
24
+ }
25
+ }
26
+ `, w = u`
27
+ mutation ExpenseControlsUpdate($id: ID!, $input: PlanmagnetUpdateExpenseInput!) {
28
+ planmagnetUpdateExpense(id: $id, input: $input) {
29
+ id
30
+ description
31
+ amount
32
+ category
33
+ date
34
+ workItemId
35
+ referenceUrl
36
+ updatedAt
37
+ }
38
+ }
39
+ `, T = u`
40
+ mutation ExpenseControlsDelete($id: ID!) {
41
+ planmagnetDeleteExpense(id: $id)
42
+ }
43
+ `, E = (e, t) => e instanceof Error ? e.message : t;
44
+ function D({ budgetId: t, projectId: i, refetchQueries: s }) {
45
+ let l = {
46
+ description: "",
47
+ amount: "",
48
+ category: "",
49
+ date: a(),
50
+ workItemId: "",
51
+ referenceUrl: ""
52
+ }, [u, w] = o(!1), [T, D] = o(l), [O, k] = o(null), [A, { loading: j }] = d(C, {
53
+ refetchQueries: s,
54
+ awaitRefetchQueries: !0
55
+ }), M = Number(T.amount), N = !j && T.description.trim() !== "" && Number.isFinite(M) && M >= 0 && !!T.date, P = async () => {
56
+ k(null);
57
+ try {
58
+ await e(A({ variables: { input: {
59
+ budgetId: t,
60
+ description: T.description.trim(),
61
+ amount: M,
62
+ date: T.date,
63
+ ...T.category.trim() ? { category: T.category.trim() } : {},
64
+ ...T.workItemId ? { workItemId: T.workItemId } : {},
65
+ ...T.referenceUrl.trim() ? { referenceUrl: T.referenceUrl.trim() } : {}
66
+ } } })), w(!1), D(l);
67
+ } catch (e) {
68
+ k(E(e, "The expense could not be logged."));
69
+ }
70
+ };
71
+ return /* @__PURE__ */ p(g, {
72
+ open: u,
73
+ onOpenChange: (e) => {
74
+ w(e), e ? D(l) : k(null);
75
+ },
76
+ children: [/* @__PURE__ */ f(S, {
77
+ asChild: !0,
78
+ children: /* @__PURE__ */ p(m, {
79
+ size: "sm",
80
+ "data-testid": "log-expense-open",
81
+ children: [/* @__PURE__ */ f(c, { className: "mr-1 h-4 w-4" }), "Log expense"]
82
+ })
83
+ }), /* @__PURE__ */ p(_, {
84
+ className: "max-w-lg",
85
+ children: [
86
+ /* @__PURE__ */ p(b, { children: [/* @__PURE__ */ f(x, { children: "Log an expense" }), /* @__PURE__ */ f(v, { children: "Logged against this budget; its spent total updates immediately." })] }),
87
+ /* @__PURE__ */ p("div", {
88
+ className: "space-y-3",
89
+ children: [
90
+ /* @__PURE__ */ p("div", { children: [/* @__PURE__ */ f("label", {
91
+ htmlFor: "log-expense-description",
92
+ className: "text-sm font-medium",
93
+ children: "Description"
94
+ }), /* @__PURE__ */ f(h, {
95
+ id: "log-expense-description",
96
+ className: "mt-1",
97
+ value: T.description,
98
+ onChange: (e) => D({
99
+ ...T,
100
+ description: e.target.value
101
+ })
102
+ })] }),
103
+ /* @__PURE__ */ p("div", {
104
+ className: "grid grid-cols-2 gap-3",
105
+ children: [/* @__PURE__ */ p("div", { children: [/* @__PURE__ */ f("label", {
106
+ htmlFor: "log-expense-amount",
107
+ className: "text-sm font-medium",
108
+ children: "Amount"
109
+ }), /* @__PURE__ */ f(h, {
110
+ id: "log-expense-amount",
111
+ className: "mt-1",
112
+ type: "number",
113
+ min: "0",
114
+ step: "0.01",
115
+ inputMode: "decimal",
116
+ value: T.amount,
117
+ onChange: (e) => D({
118
+ ...T,
119
+ amount: e.target.value
120
+ })
121
+ })] }), /* @__PURE__ */ p("div", { children: [/* @__PURE__ */ f("label", {
122
+ htmlFor: "log-expense-date",
123
+ className: "text-sm font-medium",
124
+ children: "Date"
125
+ }), /* @__PURE__ */ f(h, {
126
+ id: "log-expense-date",
127
+ className: "mt-1",
128
+ type: "date",
129
+ value: T.date,
130
+ onChange: (e) => D({
131
+ ...T,
132
+ date: e.target.value
133
+ })
134
+ })] })]
135
+ }),
136
+ /* @__PURE__ */ p("div", { children: [/* @__PURE__ */ f("label", {
137
+ htmlFor: "log-expense-category",
138
+ className: "text-sm font-medium",
139
+ children: "Category (optional)"
140
+ }), /* @__PURE__ */ f(h, {
141
+ id: "log-expense-category",
142
+ className: "mt-1",
143
+ placeholder: "e.g. Travel, Software, Contractors",
144
+ value: T.category,
145
+ onChange: (e) => D({
146
+ ...T,
147
+ category: e.target.value
148
+ })
149
+ })] }),
150
+ /* @__PURE__ */ p("div", { children: [/* @__PURE__ */ f("label", {
151
+ htmlFor: "log-expense-work-item",
152
+ className: "text-sm font-medium",
153
+ children: "Work item (optional)"
154
+ }), /* @__PURE__ */ f("div", {
155
+ className: "mt-1",
156
+ children: /* @__PURE__ */ f(r, {
157
+ id: "log-expense-work-item",
158
+ projectId: i,
159
+ value: T.workItemId,
160
+ onChange: (e) => D({
161
+ ...T,
162
+ workItemId: e
163
+ })
164
+ })
165
+ })] }),
166
+ /* @__PURE__ */ p("div", { children: [/* @__PURE__ */ f("label", {
167
+ htmlFor: "log-expense-reference",
168
+ className: "text-sm font-medium",
169
+ children: "Receipt / reference URL (optional)"
170
+ }), /* @__PURE__ */ f(h, {
171
+ id: "log-expense-reference",
172
+ className: "mt-1",
173
+ type: "url",
174
+ placeholder: "https://…",
175
+ value: T.referenceUrl,
176
+ onChange: (e) => D({
177
+ ...T,
178
+ referenceUrl: e.target.value
179
+ })
180
+ })] }),
181
+ O && /* @__PURE__ */ f(n, { message: O })
182
+ ]
183
+ }),
184
+ /* @__PURE__ */ p(y, { children: [/* @__PURE__ */ f(m, {
185
+ variant: "outline",
186
+ onClick: () => w(!1),
187
+ children: "Cancel"
188
+ }), /* @__PURE__ */ f(m, {
189
+ onClick: () => void P(),
190
+ disabled: !N,
191
+ "data-testid": "log-expense-submit",
192
+ children: j ? "Logging…" : "Log expense"
193
+ })] })
194
+ ]
195
+ })]
196
+ });
197
+ }
198
+ function O({ expense: t, refetchQueries: r }) {
199
+ let a = {
200
+ description: t.description,
201
+ amount: String(t.amount),
202
+ category: t.category ?? "",
203
+ date: i(t.date),
204
+ referenceUrl: t.referenceUrl ?? ""
205
+ }, [c, l] = o(!1), [u, v] = o(a), [C, T] = o(null), [D, { loading: O }] = d(w, {
206
+ refetchQueries: r,
207
+ awaitRefetchQueries: !0
208
+ }), k = Number(u.amount), A = !O && u.description.trim() !== "" && Number.isFinite(k) && k >= 0, j = async () => {
209
+ T(null);
210
+ let n = {};
211
+ if (u.description.trim() !== a.description && (n.description = u.description.trim()), k !== t.amount && (n.amount = k), u.category.trim() !== a.category.trim() && (n.category = u.category.trim() || null), u.date !== a.date && (n.date = u.date), u.referenceUrl.trim() !== a.referenceUrl.trim() && (n.referenceUrl = u.referenceUrl.trim() || null), Object.keys(n).length === 0) {
212
+ l(!1);
213
+ return;
214
+ }
215
+ try {
216
+ await e(D({ variables: {
217
+ id: t.id,
218
+ input: n
219
+ } })), l(!1);
220
+ } catch (e) {
221
+ T(E(e, "The expense could not be saved."));
222
+ }
223
+ };
224
+ return /* @__PURE__ */ p(g, {
225
+ open: c,
226
+ onOpenChange: (e) => {
227
+ l(e), e ? v(a) : T(null);
228
+ },
229
+ children: [/* @__PURE__ */ f(S, {
230
+ asChild: !0,
231
+ children: /* @__PURE__ */ f(m, {
232
+ size: "sm",
233
+ variant: "outline",
234
+ "aria-label": `Edit ${t.description}`,
235
+ "data-testid": "expense-edit",
236
+ children: /* @__PURE__ */ f(s, { className: "h-4 w-4" })
237
+ })
238
+ }), /* @__PURE__ */ p(_, {
239
+ className: "max-w-lg",
240
+ children: [
241
+ /* @__PURE__ */ f(b, { children: /* @__PURE__ */ f(x, { children: "Edit expense" }) }),
242
+ /* @__PURE__ */ p("div", {
243
+ className: "space-y-3",
244
+ children: [
245
+ /* @__PURE__ */ p("div", { children: [/* @__PURE__ */ f("label", {
246
+ htmlFor: "edit-expense-description",
247
+ className: "text-sm font-medium",
248
+ children: "Description"
249
+ }), /* @__PURE__ */ f(h, {
250
+ id: "edit-expense-description",
251
+ className: "mt-1",
252
+ value: u.description,
253
+ onChange: (e) => v({
254
+ ...u,
255
+ description: e.target.value
256
+ })
257
+ })] }),
258
+ /* @__PURE__ */ p("div", {
259
+ className: "grid grid-cols-2 gap-3",
260
+ children: [/* @__PURE__ */ p("div", { children: [/* @__PURE__ */ f("label", {
261
+ htmlFor: "edit-expense-amount",
262
+ className: "text-sm font-medium",
263
+ children: "Amount"
264
+ }), /* @__PURE__ */ f(h, {
265
+ id: "edit-expense-amount",
266
+ className: "mt-1",
267
+ type: "number",
268
+ min: "0",
269
+ step: "0.01",
270
+ inputMode: "decimal",
271
+ value: u.amount,
272
+ onChange: (e) => v({
273
+ ...u,
274
+ amount: e.target.value
275
+ })
276
+ })] }), /* @__PURE__ */ p("div", { children: [/* @__PURE__ */ f("label", {
277
+ htmlFor: "edit-expense-date",
278
+ className: "text-sm font-medium",
279
+ children: "Date"
280
+ }), /* @__PURE__ */ f(h, {
281
+ id: "edit-expense-date",
282
+ className: "mt-1",
283
+ type: "date",
284
+ value: u.date,
285
+ onChange: (e) => v({
286
+ ...u,
287
+ date: e.target.value
288
+ })
289
+ })] })]
290
+ }),
291
+ /* @__PURE__ */ p("div", { children: [/* @__PURE__ */ f("label", {
292
+ htmlFor: "edit-expense-category",
293
+ className: "text-sm font-medium",
294
+ children: "Category"
295
+ }), /* @__PURE__ */ f(h, {
296
+ id: "edit-expense-category",
297
+ className: "mt-1",
298
+ value: u.category,
299
+ onChange: (e) => v({
300
+ ...u,
301
+ category: e.target.value
302
+ })
303
+ })] }),
304
+ /* @__PURE__ */ p("div", { children: [/* @__PURE__ */ f("label", {
305
+ htmlFor: "edit-expense-reference",
306
+ className: "text-sm font-medium",
307
+ children: "Receipt / reference URL"
308
+ }), /* @__PURE__ */ f(h, {
309
+ id: "edit-expense-reference",
310
+ className: "mt-1",
311
+ type: "url",
312
+ value: u.referenceUrl,
313
+ onChange: (e) => v({
314
+ ...u,
315
+ referenceUrl: e.target.value
316
+ })
317
+ })] }),
318
+ C && /* @__PURE__ */ f(n, { message: C })
319
+ ]
320
+ }),
321
+ /* @__PURE__ */ p(y, { children: [/* @__PURE__ */ f(m, {
322
+ variant: "outline",
323
+ onClick: () => l(!1),
324
+ children: "Cancel"
325
+ }), /* @__PURE__ */ f(m, {
326
+ onClick: () => void j(),
327
+ disabled: !A,
328
+ "data-testid": "edit-expense-save",
329
+ children: O ? "Saving…" : "Save"
330
+ })] })
331
+ ]
332
+ })]
333
+ });
334
+ }
335
+ function k({ id: r, description: i, refetchQueries: a }) {
336
+ let [s, { loading: c }] = d(T, {
337
+ refetchQueries: a,
338
+ awaitRefetchQueries: !0
339
+ }), [u, m] = o(null), h = async () => {
340
+ m(null);
341
+ try {
342
+ await e(s({ variables: { id: r } }));
343
+ } catch (e) {
344
+ m(E(e, "The expense could not be deleted."));
345
+ }
346
+ };
347
+ return /* @__PURE__ */ p("div", {
348
+ className: "flex flex-col items-end gap-1",
349
+ children: [/* @__PURE__ */ f(t, {
350
+ label: "",
351
+ ariaLabel: `Delete ${i}`,
352
+ variant: "outline",
353
+ icon: /* @__PURE__ */ f(l, { className: "h-4 w-4" }),
354
+ title: "Delete this expense?",
355
+ description: "The budget's spent total is adjusted back down. This cannot be undone here.",
356
+ confirmLabel: "Delete expense",
357
+ destructive: !0,
358
+ disabled: c,
359
+ onConfirm: () => void h()
360
+ }), u && /* @__PURE__ */ f(n, { message: u })]
361
+ });
362
+ }
363
+ //#endregion
364
+ export { k as DeleteExpenseButton, O as EditExpenseDialog, D as LogExpenseDialog };
365
+
366
+ //# sourceMappingURL=ExpenseControls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpenseControls.js","names":[],"sources":["../../../src/planmagnet/budgets/ExpenseControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport {\n Button,\n Input,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { Pencil, Plus, Trash2 } from 'lucide-react';\nimport { WorkItemSearchSelect } from '../controls/Pickers';\nimport { throwOnError } from '../utils/mutationResult';\nimport { ConfirmButton, ErrorLine } from '../controls/ConfirmButton';\nimport { todayInputValue, toDateInputValue } from '../time/timeShared';\n\n/**\n * Expense controls (BOFF-8047): log an expense against a budget, edit one, delete one.\n *\n * `spentAmount` on the parent budget is maintained by the SERVICE (increment on log, adjust on edit,\n * decrement on delete — `BudgetService.logExpense`/`updateExpense`/`deleteExpense`), inside a transaction\n * for edit. This UI never computes or sends a budget total itself; it only refetches the budget after a\n * write so the meter reflects the server's number.\n */\n\nconst LOG_EXPENSE = gql`\n mutation ExpenseControlsLog($input: PlanmagnetLogExpenseInput!) {\n planmagnetLogExpense(input: $input) {\n id\n budgetId\n description\n amount\n category\n date\n workItemId\n referenceUrl\n createdAt\n }\n }\n`;\n\nconst UPDATE_EXPENSE = gql`\n mutation ExpenseControlsUpdate($id: ID!, $input: PlanmagnetUpdateExpenseInput!) {\n planmagnetUpdateExpense(id: $id, input: $input) {\n id\n description\n amount\n category\n date\n workItemId\n referenceUrl\n updatedAt\n }\n }\n`;\n\nconst DELETE_EXPENSE = gql`\n mutation ExpenseControlsDelete($id: ID!) {\n planmagnetDeleteExpense(id: $id)\n }\n`;\n\nconst messageOf = (e: unknown, fallback: string) => (e instanceof Error ? e.message : fallback);\n\n/** Log an expense against a budget. */\nexport function LogExpenseDialog({\n budgetId,\n projectId,\n refetchQueries,\n}: {\n budgetId: string;\n /** Narrows the optional \"work item\" search to the budget's own project, when known. */\n projectId?: string;\n refetchQueries: string[];\n}) {\n const empty = {\n description: '',\n amount: '',\n category: '',\n date: todayInputValue(),\n workItemId: '',\n referenceUrl: '',\n };\n const [open, setOpen] = useState(false);\n const [form, setForm] = useState(empty);\n const [error, setError] = useState<string | null>(null);\n const [log, { loading }] = useMutation(LOG_EXPENSE, {\n refetchQueries,\n awaitRefetchQueries: true,\n });\n\n const amount = Number(form.amount);\n const canSubmit =\n !loading &&\n form.description.trim() !== '' &&\n Number.isFinite(amount) &&\n amount >= 0 &&\n !!form.date;\n\n const submit = async () => {\n setError(null);\n try {\n await throwOnError(\n log({\n variables: {\n input: {\n budgetId,\n description: form.description.trim(),\n amount,\n date: form.date,\n ...(form.category.trim() ? { category: form.category.trim() } : {}),\n ...(form.workItemId ? { workItemId: form.workItemId } : {}),\n ...(form.referenceUrl.trim() ? { referenceUrl: form.referenceUrl.trim() } : {}),\n },\n },\n })\n );\n setOpen(false);\n setForm(empty);\n } catch (e) {\n setError(messageOf(e, 'The expense could not be logged.'));\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) setForm(empty);\n else setError(null);\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" data-testid=\"log-expense-open\">\n <Plus className=\"mr-1 h-4 w-4\" />\n Log expense\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Log an expense</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Logged against this budget; its spent total updates immediately.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3\">\n <div>\n <label htmlFor=\"log-expense-description\" className=\"text-sm font-medium\">\n Description\n </label>\n <Input\n id=\"log-expense-description\"\n className=\"mt-1\"\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor=\"log-expense-amount\" className=\"text-sm font-medium\">\n Amount\n </label>\n <Input\n id=\"log-expense-amount\"\n className=\"mt-1\"\n type=\"number\"\n min=\"0\"\n step=\"0.01\"\n inputMode=\"decimal\"\n value={form.amount}\n onChange={(e) => setForm({ ...form, amount: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"log-expense-date\" className=\"text-sm font-medium\">\n Date\n </label>\n <Input\n id=\"log-expense-date\"\n className=\"mt-1\"\n type=\"date\"\n value={form.date}\n onChange={(e) => setForm({ ...form, date: e.target.value })}\n />\n </div>\n </div>\n <div>\n <label htmlFor=\"log-expense-category\" className=\"text-sm font-medium\">\n Category (optional)\n </label>\n <Input\n id=\"log-expense-category\"\n className=\"mt-1\"\n placeholder=\"e.g. Travel, Software, Contractors\"\n value={form.category}\n onChange={(e) => setForm({ ...form, category: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"log-expense-work-item\" className=\"text-sm font-medium\">\n Work item (optional)\n </label>\n <div className=\"mt-1\">\n <WorkItemSearchSelect\n id=\"log-expense-work-item\"\n projectId={projectId}\n value={form.workItemId}\n onChange={(workItemId) => setForm({ ...form, workItemId })}\n />\n </div>\n </div>\n <div>\n <label htmlFor=\"log-expense-reference\" className=\"text-sm font-medium\">\n Receipt / reference URL (optional)\n </label>\n <Input\n id=\"log-expense-reference\"\n className=\"mt-1\"\n type=\"url\"\n placeholder=\"https://…\"\n value={form.referenceUrl}\n onChange={(e) => setForm({ ...form, referenceUrl: e.target.value })}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setOpen(false)}>\n Cancel\n </Button>\n <Button\n onClick={() => void submit()}\n disabled={!canSubmit}\n data-testid=\"log-expense-submit\"\n >\n {loading ? 'Logging…' : 'Log expense'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nexport interface ExpenseValues {\n id: string;\n description: string;\n amount: number;\n category: string | null;\n date: string;\n referenceUrl: string | null;\n}\n\n/** Edit an expense's description, amount, category, date or reference URL. Only changed fields are sent. */\nexport function EditExpenseDialog({\n expense,\n refetchQueries,\n}: {\n expense: ExpenseValues;\n refetchQueries: string[];\n}) {\n const initial = {\n description: expense.description,\n amount: String(expense.amount),\n category: expense.category ?? '',\n date: toDateInputValue(expense.date),\n referenceUrl: expense.referenceUrl ?? '',\n };\n const [open, setOpen] = useState(false);\n const [form, setForm] = useState(initial);\n const [error, setError] = useState<string | null>(null);\n const [update, { loading }] = useMutation(UPDATE_EXPENSE, {\n refetchQueries,\n awaitRefetchQueries: true,\n });\n\n const amount = Number(form.amount);\n const canSubmit =\n !loading && form.description.trim() !== '' && Number.isFinite(amount) && amount >= 0;\n\n const save = async () => {\n setError(null);\n const input: Record<string, unknown> = {};\n if (form.description.trim() !== initial.description)\n input.description = form.description.trim();\n if (amount !== expense.amount) input.amount = amount;\n if (form.category.trim() !== initial.category.trim())\n input.category = form.category.trim() || null;\n if (form.date !== initial.date) input.date = form.date;\n if (form.referenceUrl.trim() !== initial.referenceUrl.trim())\n input.referenceUrl = form.referenceUrl.trim() || null;\n if (Object.keys(input).length === 0) {\n setOpen(false);\n return;\n }\n try {\n await throwOnError(update({ variables: { id: expense.id, input } }));\n setOpen(false);\n } catch (e) {\n setError(messageOf(e, 'The expense could not be saved.'));\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) setForm(initial);\n else setError(null);\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button\n size=\"sm\"\n variant=\"outline\"\n aria-label={`Edit ${expense.description}`}\n data-testid=\"expense-edit\"\n >\n <Pencil className=\"h-4 w-4\" />\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Edit expense</ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3\">\n <div>\n <label htmlFor=\"edit-expense-description\" className=\"text-sm font-medium\">\n Description\n </label>\n <Input\n id=\"edit-expense-description\"\n className=\"mt-1\"\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor=\"edit-expense-amount\" className=\"text-sm font-medium\">\n Amount\n </label>\n <Input\n id=\"edit-expense-amount\"\n className=\"mt-1\"\n type=\"number\"\n min=\"0\"\n step=\"0.01\"\n inputMode=\"decimal\"\n value={form.amount}\n onChange={(e) => setForm({ ...form, amount: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"edit-expense-date\" className=\"text-sm font-medium\">\n Date\n </label>\n <Input\n id=\"edit-expense-date\"\n className=\"mt-1\"\n type=\"date\"\n value={form.date}\n onChange={(e) => setForm({ ...form, date: e.target.value })}\n />\n </div>\n </div>\n <div>\n <label htmlFor=\"edit-expense-category\" className=\"text-sm font-medium\">\n Category\n </label>\n <Input\n id=\"edit-expense-category\"\n className=\"mt-1\"\n value={form.category}\n onChange={(e) => setForm({ ...form, category: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"edit-expense-reference\" className=\"text-sm font-medium\">\n Receipt / reference URL\n </label>\n <Input\n id=\"edit-expense-reference\"\n className=\"mt-1\"\n type=\"url\"\n value={form.referenceUrl}\n onChange={(e) => setForm({ ...form, referenceUrl: e.target.value })}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setOpen(false)}>\n Cancel\n </Button>\n <Button onClick={() => void save()} disabled={!canSubmit} data-testid=\"edit-expense-save\">\n {loading ? 'Saving…' : 'Save'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n/** Delete an expense, with confirmation. */\nexport function DeleteExpenseButton({\n id,\n description,\n refetchQueries,\n}: {\n id: string;\n description: string;\n refetchQueries: string[];\n}) {\n const [remove, { loading }] = useMutation(DELETE_EXPENSE, {\n refetchQueries,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id } }));\n } catch (e) {\n setError(messageOf(e, 'The expense could not be deleted.'));\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-1\">\n <ConfirmButton\n label=\"\"\n ariaLabel={`Delete ${description}`}\n variant=\"outline\"\n icon={<Trash2 className=\"h-4 w-4\" />}\n title=\"Delete this expense?\"\n description=\"The budget's spent total is adjusted back down. This cannot be undone here.\"\n confirmLabel=\"Delete expense\"\n destructive\n disabled={loading}\n onConfirm={() => void onDelete()}\n />\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export declare function ProjectBudgetsSummary({ workspaceId, projectId, }: {
2
+ workspaceId?: string | null;
3
+ projectId: string;
4
+ }): import("react/jsx-runtime").JSX.Element;
5
+ //# sourceMappingURL=ProjectBudgetsSummary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProjectBudgetsSummary.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/budgets/ProjectBudgetsSummary.tsx"],"names":[],"mappings":"AA4CA,wBAAgB,qBAAqB,CAAC,EACpC,WAAW,EACX,SAAS,GACV,EAAE;IACD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;CACnB,2CA+EA"}
@@ -0,0 +1,112 @@
1
+ import { usePlanMagnetNavigate as e } from "../providers/PlanMagnetProvider.js";
2
+ import { BUDGET_STATE_STYLE as t, budgetState as n, budgetUsage as r, money as i } from "./budgetMath.js";
3
+ import { detailPath as a } from "../utils/rowNavigation.js";
4
+ import { CreateBudgetDialog as o } from "./BudgetControls.js";
5
+ import { ArrowRight as s, Wallet as c } from "lucide-react";
6
+ import { gql as l } from "@apollo/client";
7
+ import { useQuery as u } from "@apollo/client/react";
8
+ import { jsx as d, jsxs as f } from "react/jsx-runtime";
9
+ import { Button as p, Card as m, CardContent as h, CardHeader as g, CardTitle as _ } from "@burdenoff/fe-libs/ui";
10
+ //#region src/planmagnet/budgets/ProjectBudgetsSummary.tsx
11
+ var v = l`
12
+ query ProjectBudgetsSummary($workspaceId: ID!, $projectId: String!) {
13
+ planmagnetBudgets(
14
+ workspaceId: $workspaceId
15
+ filter: { projectId: $projectId }
16
+ pagination: { first: 3 }
17
+ ) {
18
+ items {
19
+ id
20
+ name
21
+ status
22
+ plannedAmount
23
+ spentAmount
24
+ currency
25
+ }
26
+ totalCount
27
+ }
28
+ }
29
+ `;
30
+ function y({ workspaceId: l, projectId: y }) {
31
+ let b = e(), { data: x, loading: S, error: C } = u(v, {
32
+ variables: {
33
+ workspaceId: l,
34
+ projectId: y
35
+ },
36
+ skip: !l || !y
37
+ }), w = x?.planmagnetBudgets.items ?? [], T = x?.planmagnetBudgets.totalCount ?? 0;
38
+ return /* @__PURE__ */ f(m, { children: [/* @__PURE__ */ f(g, {
39
+ className: "flex flex-row items-center justify-between space-y-0",
40
+ children: [/* @__PURE__ */ f(_, {
41
+ className: "flex items-center gap-2",
42
+ children: [
43
+ /* @__PURE__ */ d(c, { className: "h-4 w-4" }),
44
+ "Budget",
45
+ T === 1 ? "" : "s",
46
+ " ",
47
+ x ? `(${T})` : ""
48
+ ]
49
+ }), /* @__PURE__ */ d(o, {
50
+ presetProjectId: y,
51
+ onCreated: (e) => b(a("/projects/budget-detail-page", e)),
52
+ triggerSize: "sm"
53
+ })]
54
+ }), /* @__PURE__ */ f(h, {
55
+ className: "space-y-2",
56
+ children: [
57
+ S && !x && /* @__PURE__ */ d("p", {
58
+ className: "text-sm text-muted-foreground",
59
+ children: "Loading…"
60
+ }),
61
+ C && /* @__PURE__ */ f("p", {
62
+ role: "alert",
63
+ className: "text-sm text-destructive",
64
+ children: ["Budgets could not be loaded: ", C.message]
65
+ }),
66
+ x && w.length === 0 && /* @__PURE__ */ d("p", {
67
+ className: "text-sm text-muted-foreground",
68
+ children: "No budget for this project yet."
69
+ }),
70
+ w.map((e) => {
71
+ let o = r(e.plannedAmount, e.spentAmount), s = t[n(o)], c = o === null ? null : Math.round(o * 100);
72
+ return /* @__PURE__ */ f("button", {
73
+ type: "button",
74
+ className: "block w-full rounded-md border p-2 text-left hover:border-primary transition-colors",
75
+ onClick: () => b(a("/projects/budget-detail-page", e.id)),
76
+ children: [/* @__PURE__ */ f("div", {
77
+ className: "flex items-center justify-between gap-2 text-sm",
78
+ children: [/* @__PURE__ */ d("span", {
79
+ className: "truncate font-medium",
80
+ children: e.name
81
+ }), /* @__PURE__ */ f("span", {
82
+ className: "shrink-0 tabular-nums text-muted-foreground",
83
+ children: [
84
+ i(e.spentAmount, e.currency),
85
+ " /",
86
+ " ",
87
+ i(e.plannedAmount, e.currency)
88
+ ]
89
+ })]
90
+ }), /* @__PURE__ */ d("div", {
91
+ className: `mt-1.5 h-1.5 w-full overflow-hidden rounded-full ${s.track}`,
92
+ children: /* @__PURE__ */ d("div", {
93
+ className: `h-full rounded-full ${s.fill}`,
94
+ style: { width: `${Math.min(c ?? 0, 100)}%` }
95
+ })
96
+ })]
97
+ }, e.id);
98
+ }),
99
+ T > 0 && /* @__PURE__ */ f(p, {
100
+ size: "sm",
101
+ variant: "ghost",
102
+ className: "mt-1 w-full justify-between",
103
+ onClick: () => b(`/projects/budgets-page?projectId=${encodeURIComponent(y)}`),
104
+ children: ["View all budgets", /* @__PURE__ */ d(s, { className: "h-4 w-4" })]
105
+ })
106
+ ]
107
+ })] });
108
+ }
109
+ //#endregion
110
+ export { y as ProjectBudgetsSummary };
111
+
112
+ //# sourceMappingURL=ProjectBudgetsSummary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProjectBudgetsSummary.js","names":[],"sources":["../../../src/planmagnet/budgets/ProjectBudgetsSummary.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Button, Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { ArrowRight, Wallet } from 'lucide-react';\nimport { budgetState, budgetUsage, BUDGET_STATE_STYLE, money } from './budgetMath';\nimport { CreateBudgetDialog } from './BudgetControls';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\n\n/**\n * The Project Detail entry point into Budgets (BOFF-8047, rule 2): a budget is a per-project record, and a\n * PM opens a project expecting to see (and start) its budget from there, not only from a separate top-level\n * list. Shows up to 3 budgets with their meter; \"View all\" carries the project filter into the full page.\n */\n\nconst PROJECT_BUDGETS = gql`\n query ProjectBudgetsSummary($workspaceId: ID!, $projectId: String!) {\n planmagnetBudgets(\n workspaceId: $workspaceId\n filter: { projectId: $projectId }\n pagination: { first: 3 }\n ) {\n items {\n id\n name\n status\n plannedAmount\n spentAmount\n currency\n }\n totalCount\n }\n }\n`;\n\ninterface BudgetRow {\n id: string;\n name: string;\n status: string;\n plannedAmount: number;\n spentAmount: number;\n currency: string;\n}\n\nexport function ProjectBudgetsSummary({\n workspaceId,\n projectId,\n}: {\n workspaceId?: string | null;\n projectId: string;\n}) {\n const navigate = usePlanMagnetNavigate();\n const { data, loading, error } = useQuery<{\n planmagnetBudgets: { items: BudgetRow[]; totalCount: number };\n }>(PROJECT_BUDGETS, {\n variables: { workspaceId, projectId },\n skip: !workspaceId || !projectId,\n });\n\n const budgets = data?.planmagnetBudgets.items ?? [];\n const total = data?.planmagnetBudgets.totalCount ?? 0;\n const viewAll = () =>\n navigate(`/projects/budgets-page?projectId=${encodeURIComponent(projectId)}`);\n\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0\">\n <CardTitle className=\"flex items-center gap-2\">\n <Wallet className=\"h-4 w-4\" />\n Budget{total === 1 ? '' : 's'} {data ? `(${total})` : ''}\n </CardTitle>\n <CreateBudgetDialog\n presetProjectId={projectId}\n onCreated={(id) => navigate(detailPath('/projects/budget-detail-page', id))}\n triggerSize=\"sm\"\n />\n </CardHeader>\n <CardContent className=\"space-y-2\">\n {loading && !data && <p className=\"text-sm text-muted-foreground\">Loading…</p>}\n {error && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n Budgets could not be loaded: {error.message}\n </p>\n )}\n {data && budgets.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">No budget for this project yet.</p>\n )}\n {budgets.map((budget) => {\n const usage = budgetUsage(budget.plannedAmount, budget.spentAmount);\n const state = budgetState(usage);\n const style = BUDGET_STATE_STYLE[state];\n const percent = usage === null ? null : Math.round(usage * 100);\n return (\n <button\n key={budget.id}\n type=\"button\"\n className=\"block w-full rounded-md border p-2 text-left hover:border-primary transition-colors\"\n onClick={() => navigate(detailPath('/projects/budget-detail-page', budget.id))}\n >\n <div className=\"flex items-center justify-between gap-2 text-sm\">\n <span className=\"truncate font-medium\">{budget.name}</span>\n <span className=\"shrink-0 tabular-nums text-muted-foreground\">\n {money(budget.spentAmount, budget.currency)} /{' '}\n {money(budget.plannedAmount, budget.currency)}\n </span>\n </div>\n <div className={`mt-1.5 h-1.5 w-full overflow-hidden rounded-full ${style.track}`}>\n <div\n className={`h-full rounded-full ${style.fill}`}\n style={{ width: `${Math.min(percent ?? 0, 100)}%` }}\n />\n </div>\n </button>\n );\n })}\n {total > 0 && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"mt-1 w-full justify-between\"\n onClick={viewAll}\n >\n View all budgets\n <ArrowRight className=\"h-4 w-4\" />\n </Button>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":";;;;;;;;;;AAeA,IAAM,IAAkB,CAAG;;;;;;;;;;;;;;;;;;;AA6B3B,SAAgB,EAAsB,EACpC,gBACA,gBAIC;CACD,IAAM,IAAW,GAAuB,EAClC,EAAE,SAAM,YAAS,aAAU,EAE9B,GAAiB;EAClB,WAAW;GAAE;GAAa;GAAW;EACrC,MAAM,CAAC,KAAe,CAAC;EACxB,CAAC,EAEI,IAAU,GAAM,kBAAkB,SAAS,EAAE,EAC7C,IAAQ,GAAM,kBAAkB,cAAc;AAIpD,QACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;EAAY,WAAU;YAAtB,CACE,kBAAC,GAAD;GAAW,WAAU;aAArB;IACE,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;;IACvB,MAAU,IAAI,KAAK;IAAI;IAAE,IAAO,IAAI,EAAM,KAAK;IAC5C;MACZ,kBAAC,GAAD;GACE,iBAAiB;GACjB,YAAY,MAAO,EAAS,EAAW,gCAAgC,EAAG,CAAC;GAC3E,aAAY;GACZ,CAAA,CACS;KACb,kBAAC,GAAD;EAAa,WAAU;YAAvB;GACG,KAAW,CAAC,KAAQ,kBAAC,KAAD;IAAG,WAAU;cAAgC;IAAY,CAAA;GAC7E,KACC,kBAAC,KAAD;IAAG,MAAK;IAAQ,WAAU;cAA1B,CAAqD,iCACrB,EAAM,QAClC;;GAEL,KAAQ,EAAQ,WAAW,KAC1B,kBAAC,KAAD;IAAG,WAAU;cAAgC;IAAmC,CAAA;GAEjF,EAAQ,KAAK,MAAW;IACvB,IAAM,IAAQ,EAAY,EAAO,eAAe,EAAO,YAAY,EAE7D,IAAQ,EADA,EAAY,EAAM,GAE1B,IAAU,MAAU,OAAO,OAAO,KAAK,MAAM,IAAQ,IAAI;AAC/D,WACE,kBAAC,UAAD;KAEE,MAAK;KACL,WAAU;KACV,eAAe,EAAS,EAAW,gCAAgC,EAAO,GAAG,CAAC;eAJhF,CAME,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAwB,EAAO;OAAY,CAAA,EAC3D,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACG,EAAM,EAAO,aAAa,EAAO,SAAS;QAAC;QAAG;QAC9C,EAAM,EAAO,eAAe,EAAO,SAAS;QACxC;SACH;SACN,kBAAC,OAAD;MAAK,WAAW,oDAAoD,EAAM;gBACxE,kBAAC,OAAD;OACE,WAAW,uBAAuB,EAAM;OACxC,OAAO,EAAE,OAAO,GAAG,KAAK,IAAI,KAAW,GAAG,IAAI,CAAC,IAAI;OACnD,CAAA;MACE,CAAA,CACC;OAlBF,EAAO,GAkBL;KAEX;GACD,IAAQ,KACP,kBAAC,GAAD;IACE,MAAK;IACL,SAAQ;IACR,WAAU;IACV,eA1DR,EAAS,oCAAoC,mBAAmB,EAAU,GAAG;cAsDvE,CAKC,oBAEC,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA,CAC3B;;GAEC;IACT,EAAA,CAAA"}
@@ -0,0 +1,26 @@
1
+ import { ComponentType } from 'react';
2
+
3
+ /**
4
+ * Budget math and the meter's visual language, shared by every screen that shows a
5
+ * budget's spend against its plan: the Analytics "Budget performance" card
6
+ * (`analytics/DeliveryHealthCards.tsx`) and the Budgets list/detail pages (BOFF-8047).
7
+ *
8
+ * Kept in one place so the two surfaces can never disagree about what "near limit" or
9
+ * "over budget" means — they read the same thresholds and the same colour/icon pairing.
10
+ */
11
+ export type BudgetState = 'no-plan' | 'on-track' | 'near-limit' | 'over-budget';
12
+ /** Share of the plan spent; `null` when there is no plan to measure against. */
13
+ export declare function budgetUsage(planned: number, spent: number): number | null;
14
+ export declare function budgetState(usage: number | null): BudgetState;
15
+ export declare const BUDGET_STATE_STYLE: Record<BudgetState, {
16
+ track: string;
17
+ fill: string;
18
+ icon: ComponentType<{
19
+ className?: string;
20
+ 'aria-hidden'?: boolean;
21
+ }>;
22
+ text: string;
23
+ }>;
24
+ /** Money, in the budget's own currency. Falls back to a plain number when the currency code is not recognised. */
25
+ export declare function money(amount: number, currency: string): string;
26
+ //# sourceMappingURL=budgetMath.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budgetMath.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/budgets/budgetMath.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAG3C;;;;;;;GAOG;AAEH,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,YAAY,GAAG,aAAa,CAAC;AAEhF,gFAAgF;AAChF,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGzE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,WAAW,CAM7D;AAED,eAAO,MAAM,kBAAkB,EAAE,MAAM,CACrC,WAAW,EACX;IACE,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,aAAa,CAAC;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACrE,IAAI,EAAE,MAAM,CAAC;CACd,CA0BF,CAAC;AAEF,kHAAkH;AAClH,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAW9D"}
@@ -0,0 +1,49 @@
1
+ import { AlertOctagon as e, AlertTriangle as t, CheckCircle2 as n, CircleDashed as r } from "lucide-react";
2
+ //#region src/planmagnet/budgets/budgetMath.ts
3
+ function i(e, t) {
4
+ return !Number.isFinite(e) || e <= 0 ? null : t / e;
5
+ }
6
+ function a(e) {
7
+ return e === null ? "no-plan" : e <= .85 ? "on-track" : e <= 1 ? "near-limit" : "over-budget";
8
+ }
9
+ var o = {
10
+ "no-plan": {
11
+ track: "bg-muted",
12
+ fill: "bg-muted",
13
+ icon: r,
14
+ text: "text-muted-foreground"
15
+ },
16
+ "on-track": {
17
+ track: "bg-primary/15",
18
+ fill: "bg-primary",
19
+ icon: n,
20
+ text: "text-muted-foreground"
21
+ },
22
+ "near-limit": {
23
+ track: "bg-status-warning-subtle",
24
+ fill: "bg-status-warning",
25
+ icon: t,
26
+ text: "text-status-warning-text"
27
+ },
28
+ "over-budget": {
29
+ track: "bg-status-error-subtle",
30
+ fill: "bg-status-error",
31
+ icon: e,
32
+ text: "text-status-error-text"
33
+ }
34
+ };
35
+ function s(e, t) {
36
+ try {
37
+ return new Intl.NumberFormat(void 0, {
38
+ style: "currency",
39
+ currency: t,
40
+ maximumFractionDigits: 0
41
+ }).format(e);
42
+ } catch {
43
+ return `${Math.round(e).toLocaleString()} ${t}`;
44
+ }
45
+ }
46
+ //#endregion
47
+ export { o as BUDGET_STATE_STYLE, a as budgetState, i as budgetUsage, s as money };
48
+
49
+ //# sourceMappingURL=budgetMath.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budgetMath.js","names":[],"sources":["../../../src/planmagnet/budgets/budgetMath.ts"],"sourcesContent":["import type { ComponentType } from 'react';\nimport { AlertOctagon, AlertTriangle, CheckCircle2, CircleDashed } from 'lucide-react';\n\n/**\n * Budget math and the meter's visual language, shared by every screen that shows a\n * budget's spend against its plan: the Analytics \"Budget performance\" card\n * (`analytics/DeliveryHealthCards.tsx`) and the Budgets list/detail pages (BOFF-8047).\n *\n * Kept in one place so the two surfaces can never disagree about what \"near limit\" or\n * \"over budget\" means — they read the same thresholds and the same colour/icon pairing.\n */\n\nexport type BudgetState = 'no-plan' | 'on-track' | 'near-limit' | 'over-budget';\n\n/** Share of the plan spent; `null` when there is no plan to measure against. */\nexport function budgetUsage(planned: number, spent: number): number | null {\n if (!Number.isFinite(planned) || planned <= 0) return null;\n return spent / planned;\n}\n\nexport function budgetState(usage: number | null): BudgetState {\n // Without a plan there is nothing to be on track AGAINST — say so, rather\n // than reporting a comforting \"On track\" the numbers cannot support.\n if (usage === null) return 'no-plan';\n if (usage <= 0.85) return 'on-track';\n return usage <= 1 ? 'near-limit' : 'over-budget';\n}\n\nexport const BUDGET_STATE_STYLE: Record<\n BudgetState,\n {\n track: string;\n fill: string;\n icon: ComponentType<{ className?: string; 'aria-hidden'?: boolean }>;\n text: string;\n }\n> = {\n 'no-plan': {\n track: 'bg-muted',\n fill: 'bg-muted',\n icon: CircleDashed,\n text: 'text-muted-foreground',\n },\n 'on-track': {\n track: 'bg-primary/15',\n fill: 'bg-primary',\n icon: CheckCircle2,\n text: 'text-muted-foreground',\n },\n 'near-limit': {\n track: 'bg-status-warning-subtle',\n fill: 'bg-status-warning',\n icon: AlertTriangle,\n text: 'text-status-warning-text',\n },\n 'over-budget': {\n track: 'bg-status-error-subtle',\n fill: 'bg-status-error',\n icon: AlertOctagon,\n text: 'text-status-error-text',\n },\n};\n\n/** Money, in the budget's own currency. Falls back to a plain number when the currency code is not recognised. */\nexport function money(amount: number, currency: string): string {\n try {\n return new Intl.NumberFormat(undefined, {\n style: 'currency',\n currency,\n maximumFractionDigits: 0,\n }).format(amount);\n } catch {\n // An unrecognised currency code must not blank the whole table.\n return `${Math.round(amount).toLocaleString()} ${currency}`;\n }\n}\n"],"mappings":";;AAeA,SAAgB,EAAY,GAAiB,GAA8B;AAEzE,QADI,CAAC,OAAO,SAAS,EAAQ,IAAI,KAAW,IAAU,OAC/C,IAAQ;;AAGjB,SAAgB,EAAY,GAAmC;AAK7D,QAFI,MAAU,OAAa,YACvB,KAAS,MAAa,aACnB,KAAS,IAAI,eAAe;;AAGrC,IAAa,IAQT;CACF,WAAW;EACT,OAAO;EACP,MAAM;EACN,MAAM;EACN,MAAM;EACP;CACD,YAAY;EACV,OAAO;EACP,MAAM;EACN,MAAM;EACN,MAAM;EACP;CACD,cAAc;EACZ,OAAO;EACP,MAAM;EACN,MAAM;EACN,MAAM;EACP;CACD,eAAe;EACb,OAAO;EACP,MAAM;EACN,MAAM;EACN,MAAM;EACP;CACF;AAGD,SAAgB,EAAM,GAAgB,GAA0B;AAC9D,KAAI;AACF,SAAO,IAAI,KAAK,aAAa,KAAA,GAAW;GACtC,OAAO;GACP;GACA,uBAAuB;GACxB,CAAC,CAAC,OAAO,EAAO;SACX;AAEN,SAAO,GAAG,KAAK,MAAM,EAAO,CAAC,gBAAgB,CAAC,GAAG"}