@burdenoff/microfe-planmagnet 2026.928.4 → 2026.928.5
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/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoot.js +117 -93
- package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +156 -76
- package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
- package/dist/planmagnet/automation/AutomationExecutionsPanel.d.ts +5 -0
- package/dist/planmagnet/automation/AutomationExecutionsPanel.d.ts.map +1 -0
- package/dist/planmagnet/automation/AutomationExecutionsPanel.js +107 -0
- package/dist/planmagnet/automation/AutomationExecutionsPanel.js.map +1 -0
- package/dist/planmagnet/automation/AutomationRuleFields.d.ts +29 -0
- package/dist/planmagnet/automation/AutomationRuleFields.d.ts.map +1 -0
- package/dist/planmagnet/automation/AutomationRuleFields.js +308 -0
- package/dist/planmagnet/automation/AutomationRuleFields.js.map +1 -0
- package/dist/planmagnet/automation/automationRuleTypes.d.ts +60 -0
- package/dist/planmagnet/automation/automationRuleTypes.d.ts.map +1 -0
- package/dist/planmagnet/automation/automationRuleTypes.js +113 -0
- package/dist/planmagnet/automation/automationRuleTypes.js.map +1 -0
- package/dist/planmagnet/board/BulkActions.d.ts +11 -0
- package/dist/planmagnet/board/BulkActions.d.ts.map +1 -0
- package/dist/planmagnet/board/BulkActions.js +182 -0
- package/dist/planmagnet/board/BulkActions.js.map +1 -0
- package/dist/planmagnet/board/ProjectBoard.d.ts.map +1 -1
- package/dist/planmagnet/board/ProjectBoard.js +289 -254
- package/dist/planmagnet/board/ProjectBoard.js.map +1 -1
- package/dist/planmagnet/budgets/BudgetControls.d.ts +30 -0
- package/dist/planmagnet/budgets/BudgetControls.d.ts.map +1 -0
- package/dist/planmagnet/budgets/BudgetControls.js +470 -0
- package/dist/planmagnet/budgets/BudgetControls.js.map +1 -0
- package/dist/planmagnet/budgets/ExpenseControls.d.ts +27 -0
- package/dist/planmagnet/budgets/ExpenseControls.d.ts.map +1 -0
- package/dist/planmagnet/budgets/ExpenseControls.js +366 -0
- package/dist/planmagnet/budgets/ExpenseControls.js.map +1 -0
- package/dist/planmagnet/budgets/ProjectBudgetsSummary.d.ts +5 -0
- package/dist/planmagnet/budgets/ProjectBudgetsSummary.d.ts.map +1 -0
- package/dist/planmagnet/budgets/ProjectBudgetsSummary.js +112 -0
- package/dist/planmagnet/budgets/ProjectBudgetsSummary.js.map +1 -0
- package/dist/planmagnet/budgets/budgetMath.d.ts +26 -0
- package/dist/planmagnet/budgets/budgetMath.d.ts.map +1 -0
- package/dist/planmagnet/budgets/budgetMath.js +49 -0
- package/dist/planmagnet/budgets/budgetMath.js.map +1 -0
- package/dist/planmagnet/controls/AutomationRuleControls.d.ts +23 -0
- package/dist/planmagnet/controls/AutomationRuleControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/AutomationRuleControls.js +172 -0
- package/dist/planmagnet/controls/AutomationRuleControls.js.map +1 -0
- package/dist/planmagnet/controls/CompetitorControls.js +15 -15
- package/dist/planmagnet/controls/CustomFieldControls.d.ts +42 -0
- package/dist/planmagnet/controls/CustomFieldControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/CustomFieldControls.js +84 -0
- package/dist/planmagnet/controls/CustomFieldControls.js.map +1 -0
- package/dist/planmagnet/controls/CustomFieldValuesPanel.d.ts +8 -0
- package/dist/planmagnet/controls/CustomFieldValuesPanel.d.ts.map +1 -0
- package/dist/planmagnet/controls/CustomFieldValuesPanel.js +315 -0
- package/dist/planmagnet/controls/CustomFieldValuesPanel.js.map +1 -0
- package/dist/planmagnet/controls/FeatureControls.js +54 -54
- package/dist/planmagnet/controls/ObjectiveControls.js +15 -15
- package/dist/planmagnet/controls/PortfolioControls.js +7 -7
- package/dist/planmagnet/controls/ProgramControls.js +19 -19
- package/dist/planmagnet/controls/ProjectControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/ProjectControls.js +54 -47
- package/dist/planmagnet/controls/ProjectControls.js.map +1 -1
- package/dist/planmagnet/controls/ReleaseControls.js +7 -7
- package/dist/planmagnet/controls/RiskControls.d.ts +27 -0
- package/dist/planmagnet/controls/RiskControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/RiskControls.js +346 -0
- package/dist/planmagnet/controls/RiskControls.js.map +1 -0
- package/dist/planmagnet/controls/TemplateControls.d.ts +15 -0
- package/dist/planmagnet/controls/TemplateControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/TemplateControls.js +55 -0
- package/dist/planmagnet/controls/TemplateControls.js.map +1 -0
- package/dist/planmagnet/controls/WorkItemControls.js +1 -1
- package/dist/planmagnet/create/CreateDialogs.d.ts +20 -0
- package/dist/planmagnet/create/CreateDialogs.d.ts.map +1 -1
- package/dist/planmagnet/create/CreateDialogs.js +455 -116
- package/dist/planmagnet/create/CreateDialogs.js.map +1 -1
- package/dist/planmagnet/dependency/DependencyPanel.d.ts +6 -0
- package/dist/planmagnet/dependency/DependencyPanel.d.ts.map +1 -0
- package/dist/planmagnet/dependency/DependencyPanel.js +310 -0
- package/dist/planmagnet/dependency/DependencyPanel.js.map +1 -0
- package/dist/planmagnet/pages/AutomationAutomationRuleDetailPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/AutomationAutomationRuleDetailPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/AutomationAutomationRuleDetailPagePage.js +171 -0
- package/dist/planmagnet/pages/AutomationAutomationRuleDetailPagePage.js.map +1 -0
- package/dist/planmagnet/pages/AutomationAutomationRulesPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/AutomationAutomationRulesPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/AutomationAutomationRulesPagePage.js +232 -0
- package/dist/planmagnet/pages/AutomationAutomationRulesPagePage.js.map +1 -0
- package/dist/planmagnet/pages/AutomationCreateAutomationRulePagePage.d.ts +2 -0
- package/dist/planmagnet/pages/AutomationCreateAutomationRulePagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/AutomationCreateAutomationRulePagePage.js +93 -0
- package/dist/planmagnet/pages/AutomationCreateAutomationRulePagePage.js.map +1 -0
- package/dist/planmagnet/pages/PortfoliosEditPortfolioPagePage.js +5 -5
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js +9 -9
- package/dist/planmagnet/pages/PortfoliosPortfoliosPagePage.js +6 -6
- package/dist/planmagnet/pages/ProgramsCreateProgramPagePage.js +4 -4
- package/dist/planmagnet/pages/ProgramsEditProgramPagePage.js +6 -6
- package/dist/planmagnet/pages/ProgramsProgramsPagePage.js +5 -5
- package/dist/planmagnet/pages/ProjectsBudgetDetailPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProjectsBudgetDetailPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProjectsBudgetDetailPagePage.js +315 -0
- package/dist/planmagnet/pages/ProjectsBudgetDetailPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProjectsBudgetsPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProjectsBudgetsPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProjectsBudgetsPagePage.js +186 -0
- package/dist/planmagnet/pages/ProjectsBudgetsPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.js +164 -0
- package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProjectsCustomFieldsPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProjectsCustomFieldsPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProjectsCustomFieldsPagePage.js +348 -0
- package/dist/planmagnet/pages/ProjectsCustomFieldsPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js +10 -10
- package/dist/planmagnet/pages/ProjectsMilestoneDetailPagePage.js +4 -4
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +4 -4
- package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.js +6 -6
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +319 -203
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectsPagePage.js +81 -72
- package/dist/planmagnet/pages/ProjectsProjectsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/RisksRiskDetailPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/RisksRiskDetailPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/RisksRiskDetailPagePage.js +278 -0
- package/dist/planmagnet/pages/RisksRiskDetailPagePage.js.map +1 -0
- package/dist/planmagnet/pages/RisksRiskRegisterPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/RisksRiskRegisterPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/RisksRiskRegisterPagePage.js +191 -0
- package/dist/planmagnet/pages/RisksRiskRegisterPagePage.js.map +1 -0
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.js +5 -5
- package/dist/planmagnet/pages/TasksEditTaskPagePage.js +4 -4
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +161 -137
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksTasksPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksTasksPagePage.js +124 -113
- package/dist/planmagnet/pages/TasksTasksPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksTimeEntriesPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/TasksTimeEntriesPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/TasksTimeEntriesPagePage.js +263 -0
- package/dist/planmagnet/pages/TasksTimeEntriesPagePage.js.map +1 -0
- package/dist/planmagnet/pages/TasksTimesheetDetailPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/TasksTimesheetDetailPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/TasksTimesheetDetailPagePage.js +254 -0
- package/dist/planmagnet/pages/TasksTimesheetDetailPagePage.js.map +1 -0
- package/dist/planmagnet/pages/TasksTimesheetsPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/TasksTimesheetsPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/TasksTimesheetsPagePage.js +165 -0
- package/dist/planmagnet/pages/TasksTimesheetsPagePage.js.map +1 -0
- package/dist/planmagnet/pages/TemplatesEditTemplatePagePage.d.ts +2 -0
- package/dist/planmagnet/pages/TemplatesEditTemplatePagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/TemplatesEditTemplatePagePage.js +316 -0
- package/dist/planmagnet/pages/TemplatesEditTemplatePagePage.js.map +1 -0
- package/dist/planmagnet/pages/TemplatesTemplatesPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/TemplatesTemplatesPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/TemplatesTemplatesPagePage.js +208 -0
- package/dist/planmagnet/pages/TemplatesTemplatesPagePage.js.map +1 -0
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.d.ts +0 -5
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.d.ts.map +1 -1
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js +47 -89
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js.map +1 -1
- package/dist/planmagnet/roadmaps/RoadmapControls.js +24 -24
- package/dist/planmagnet/time/TaskTimePanel.d.ts +7 -0
- package/dist/planmagnet/time/TaskTimePanel.d.ts.map +1 -0
- package/dist/planmagnet/time/TaskTimePanel.js +104 -0
- package/dist/planmagnet/time/TaskTimePanel.js.map +1 -0
- package/dist/planmagnet/time/TimeEntryControls.d.ts +37 -0
- package/dist/planmagnet/time/TimeEntryControls.d.ts.map +1 -0
- package/dist/planmagnet/time/TimeEntryControls.js +338 -0
- package/dist/planmagnet/time/TimeEntryControls.js.map +1 -0
- package/dist/planmagnet/time/TimesheetControls.d.ts +18 -0
- package/dist/planmagnet/time/TimesheetControls.d.ts.map +1 -0
- package/dist/planmagnet/time/TimesheetControls.js +244 -0
- package/dist/planmagnet/time/TimesheetControls.js.map +1 -0
- package/dist/planmagnet/time/timeShared.d.ts +29 -0
- package/dist/planmagnet/time/timeShared.d.ts.map +1 -0
- package/dist/planmagnet/time/timeShared.js +34 -0
- package/dist/planmagnet/time/timeShared.js.map +1 -0
- package/dist/planmagnet/utils/rowNavigation.js +5 -2
- package/dist/planmagnet/utils/rowNavigation.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { BUDGET_STATE_STYLE as n, budgetState as r, budgetUsage as i, money as a } from "../budgets/budgetMath.js";
|
|
3
|
+
import { ProjectSelect as o } from "../controls/Pickers.js";
|
|
4
|
+
import { enumLabel as s } from "../controls/WorkItemControls.js";
|
|
5
|
+
import { detailPath as c, rowLinkProps as l } from "../utils/rowNavigation.js";
|
|
6
|
+
import { BUDGET_STATUSES as u, CreateBudgetDialog as d } from "../budgets/BudgetControls.js";
|
|
7
|
+
import { useDeferredValue as f, useState as p } from "react";
|
|
8
|
+
import { Wallet as m } from "lucide-react";
|
|
9
|
+
import { useSearchParams as h } from "react-router";
|
|
10
|
+
import { gql as g } from "@apollo/client";
|
|
11
|
+
import { useQuery as _ } from "@apollo/client/react";
|
|
12
|
+
import { jsx as v, jsxs as y } from "react/jsx-runtime";
|
|
13
|
+
import { useI18n as b } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
14
|
+
import { Card as x, CardContent as S, CardHeader as C, CardTitle as w, Input as T } from "@burdenoff/fe-libs/ui";
|
|
15
|
+
//#region src/planmagnet/pages/ProjectsBudgetsPagePage.tsx
|
|
16
|
+
var E = g`
|
|
17
|
+
query BudgetsList(
|
|
18
|
+
$workspaceId: ID!
|
|
19
|
+
$filter: PlanmagnetBudgetFilter
|
|
20
|
+
$pagination: PlanmagnetPaginationInput
|
|
21
|
+
) {
|
|
22
|
+
planmagnetBudgets(workspaceId: $workspaceId, filter: $filter, pagination: $pagination) {
|
|
23
|
+
items {
|
|
24
|
+
id
|
|
25
|
+
name
|
|
26
|
+
status
|
|
27
|
+
plannedAmount
|
|
28
|
+
spentAmount
|
|
29
|
+
currency
|
|
30
|
+
projectId
|
|
31
|
+
project {
|
|
32
|
+
id
|
|
33
|
+
name
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
totalCount
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
`, D = ["ALL", ...u];
|
|
40
|
+
function O() {
|
|
41
|
+
let { workspaceId: u } = e(), g = t(), { t: O } = b(), [k] = h(), [A, j] = p(k.get("projectId") ?? ""), [M, N] = p("ALL"), [P, F] = p(""), I = f(P.trim()), L = {
|
|
42
|
+
...A ? { projectId: A } : {},
|
|
43
|
+
...M === "ALL" ? {} : { status: M },
|
|
44
|
+
...I ? { search: I } : {}
|
|
45
|
+
}, { data: R, loading: z, error: B } = _(E, {
|
|
46
|
+
variables: {
|
|
47
|
+
workspaceId: u ?? "",
|
|
48
|
+
filter: L,
|
|
49
|
+
pagination: { first: 100 }
|
|
50
|
+
},
|
|
51
|
+
skip: !u
|
|
52
|
+
}), V = R?.planmagnetBudgets.items ?? [], H = R?.planmagnetBudgets.totalCount ?? 0;
|
|
53
|
+
return /* @__PURE__ */ y("div", {
|
|
54
|
+
className: "space-y-6 p-6",
|
|
55
|
+
children: [
|
|
56
|
+
/* @__PURE__ */ y("div", {
|
|
57
|
+
className: "flex flex-wrap items-center justify-between gap-3",
|
|
58
|
+
children: [/* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("h1", {
|
|
59
|
+
className: "text-2xl font-semibold",
|
|
60
|
+
children: O("planmagnet.budgets.title", "Budgets")
|
|
61
|
+
}), /* @__PURE__ */ v("p", {
|
|
62
|
+
className: "text-sm text-muted-foreground",
|
|
63
|
+
children: R ? O("planmagnet.budgets.subtitle", "{{count}} budget(s) across projects", { count: H }) : O("planmagnet.budgets.subtitleFallback", "Financial plans, tracked against actuals")
|
|
64
|
+
})] }), /* @__PURE__ */ v(d, {
|
|
65
|
+
presetProjectId: A || void 0,
|
|
66
|
+
onCreated: (e) => g(c("/projects/budget-detail-page", e))
|
|
67
|
+
})]
|
|
68
|
+
}),
|
|
69
|
+
/* @__PURE__ */ y("div", {
|
|
70
|
+
className: "flex flex-wrap items-center gap-3",
|
|
71
|
+
children: [/* @__PURE__ */ v("div", {
|
|
72
|
+
className: "w-full max-w-xs",
|
|
73
|
+
children: /* @__PURE__ */ v(o, {
|
|
74
|
+
id: "budgets-project-filter",
|
|
75
|
+
value: A,
|
|
76
|
+
onChange: j,
|
|
77
|
+
noneLabel: O("planmagnet.budgets.allProjects", "All projects")
|
|
78
|
+
})
|
|
79
|
+
}), /* @__PURE__ */ v(T, {
|
|
80
|
+
"aria-label": O("planmagnet.budgets.search", "Search budgets"),
|
|
81
|
+
placeholder: O("planmagnet.budgets.searchPlaceholder", "Search by name or description"),
|
|
82
|
+
className: "w-full max-w-xs",
|
|
83
|
+
value: P,
|
|
84
|
+
onChange: (e) => F(e.target.value)
|
|
85
|
+
})]
|
|
86
|
+
}),
|
|
87
|
+
/* @__PURE__ */ v("div", {
|
|
88
|
+
className: "flex flex-wrap gap-1",
|
|
89
|
+
role: "group",
|
|
90
|
+
"aria-label": O("planmagnet.budgets.statusFilter", "Filter by status"),
|
|
91
|
+
children: D.map((e) => /* @__PURE__ */ v("button", {
|
|
92
|
+
type: "button",
|
|
93
|
+
"aria-pressed": M === e,
|
|
94
|
+
onClick: () => N(e),
|
|
95
|
+
className: `px-3 py-1.5 text-sm rounded-md ${M === e ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted"}`,
|
|
96
|
+
children: e === "ALL" ? O("planmagnet.budgets.allStatuses", "All") : s(e)
|
|
97
|
+
}, e))
|
|
98
|
+
}),
|
|
99
|
+
B && /* @__PURE__ */ y("p", {
|
|
100
|
+
role: "alert",
|
|
101
|
+
className: "text-sm text-destructive",
|
|
102
|
+
children: [
|
|
103
|
+
O("planmagnet.budgets.errorLoading", "Budgets could not be loaded"),
|
|
104
|
+
": ",
|
|
105
|
+
B.message
|
|
106
|
+
]
|
|
107
|
+
}),
|
|
108
|
+
z && !R && /* @__PURE__ */ v("div", {
|
|
109
|
+
className: "flex items-center justify-center p-12",
|
|
110
|
+
children: /* @__PURE__ */ v("div", { className: "animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full" })
|
|
111
|
+
}),
|
|
112
|
+
R && V.length === 0 && !B && /* @__PURE__ */ v("p", {
|
|
113
|
+
className: "text-center text-sm text-muted-foreground py-8",
|
|
114
|
+
children: A || M !== "ALL" || I ? O("planmagnet.budgets.emptyFiltered", "No budgets match these filters.") : O("planmagnet.budgets.empty", "No budgets yet. Create the first one.")
|
|
115
|
+
}),
|
|
116
|
+
/* @__PURE__ */ v("div", {
|
|
117
|
+
className: "grid grid-cols-1 md:grid-cols-2 gap-4",
|
|
118
|
+
children: V.map((e) => {
|
|
119
|
+
let t = i(e.plannedAmount, e.spentAmount), o = n[r(t)], u = t === null ? null : Math.round(t * 100);
|
|
120
|
+
return /* @__PURE__ */ y(x, {
|
|
121
|
+
...l(() => g(c("/projects/budget-detail-page", e.id)), e.name),
|
|
122
|
+
children: [/* @__PURE__ */ v(C, {
|
|
123
|
+
className: "pb-2",
|
|
124
|
+
children: /* @__PURE__ */ y("div", {
|
|
125
|
+
className: "flex items-center justify-between gap-2",
|
|
126
|
+
children: [/* @__PURE__ */ y(w, {
|
|
127
|
+
className: "text-base flex items-center gap-2",
|
|
128
|
+
children: [/* @__PURE__ */ v(m, { className: "h-4 w-4 text-primary" }), e.name]
|
|
129
|
+
}), /* @__PURE__ */ v("span", {
|
|
130
|
+
className: "text-xs px-2 py-0.5 rounded-full bg-muted capitalize",
|
|
131
|
+
children: s(e.status)
|
|
132
|
+
})]
|
|
133
|
+
})
|
|
134
|
+
}), /* @__PURE__ */ y(S, {
|
|
135
|
+
className: "space-y-3",
|
|
136
|
+
children: [
|
|
137
|
+
/* @__PURE__ */ v("p", {
|
|
138
|
+
className: "text-xs text-muted-foreground",
|
|
139
|
+
children: e.project?.name ?? O("planmagnet.budgets.noProject", "No project")
|
|
140
|
+
}),
|
|
141
|
+
/* @__PURE__ */ y("div", {
|
|
142
|
+
className: "flex justify-between text-sm",
|
|
143
|
+
children: [/* @__PURE__ */ v("span", {
|
|
144
|
+
className: "text-muted-foreground",
|
|
145
|
+
children: O("planmagnet.budgets.spent", "Spent")
|
|
146
|
+
}), /* @__PURE__ */ y("span", {
|
|
147
|
+
className: "tabular-nums",
|
|
148
|
+
children: [
|
|
149
|
+
a(e.spentAmount, e.currency),
|
|
150
|
+
" /",
|
|
151
|
+
" ",
|
|
152
|
+
a(e.plannedAmount, e.currency)
|
|
153
|
+
]
|
|
154
|
+
})]
|
|
155
|
+
}),
|
|
156
|
+
/* @__PURE__ */ v("div", {
|
|
157
|
+
role: "meter",
|
|
158
|
+
"aria-valuemin": 0,
|
|
159
|
+
"aria-valuemax": Math.max(100, u ?? 0),
|
|
160
|
+
"aria-valuenow": u ?? 0,
|
|
161
|
+
"aria-label": `${e.name}: ${u === null ? "no plan" : `${u}%`} spent`,
|
|
162
|
+
className: `h-2 w-full overflow-hidden rounded-full ${o.track}`,
|
|
163
|
+
children: /* @__PURE__ */ v("div", {
|
|
164
|
+
className: `h-full rounded-full ${o.fill}`,
|
|
165
|
+
style: { width: `${Math.min(u ?? 0, 100)}%` }
|
|
166
|
+
})
|
|
167
|
+
}),
|
|
168
|
+
/* @__PURE__ */ y("span", {
|
|
169
|
+
className: `flex items-center gap-1 text-xs ${o.text}`,
|
|
170
|
+
children: [/* @__PURE__ */ v(o.icon, {
|
|
171
|
+
className: "h-3 w-3",
|
|
172
|
+
"aria-hidden": !0
|
|
173
|
+
}), u === null ? O("planmagnet.budgets.noPlan", "No plan set") : `${u}%`]
|
|
174
|
+
})
|
|
175
|
+
]
|
|
176
|
+
})]
|
|
177
|
+
}, e.id);
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
]
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
export { O as ProjectsBudgetsPagePage };
|
|
185
|
+
|
|
186
|
+
//# sourceMappingURL=ProjectsBudgetsPagePage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectsBudgetsPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProjectsBudgetsPagePage.tsx"],"sourcesContent":["import { useDeferredValue, useState } from 'react';\nimport { useSearchParams } from 'react-router';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Input } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Wallet } from 'lucide-react';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport { ProjectSelect } from '../controls/Pickers';\nimport { CreateBudgetDialog, BUDGET_STATUSES } from '../budgets/BudgetControls';\nimport { budgetState, budgetUsage, BUDGET_STATE_STYLE, money } from '../budgets/budgetMath';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { rowLinkProps, detailPath } from '../utils/rowNavigation';\n\n/**\n * Budgets (BOFF-8047). The service and the read side (spent-vs-planned meter on Analytics,\n * `analytics/DeliveryHealthCards.tsx`) have existed since 2026-09-24; this page is the first place a budget\n * can be created or edited, closing the `api-only` gap in `planmagnet-specs/CAPABILITY_REGISTER.md`.\n */\n\nconst BUDGETS = gql`\n query BudgetsList(\n $workspaceId: ID!\n $filter: PlanmagnetBudgetFilter\n $pagination: PlanmagnetPaginationInput\n ) {\n planmagnetBudgets(workspaceId: $workspaceId, filter: $filter, pagination: $pagination) {\n items {\n id\n name\n status\n plannedAmount\n spentAmount\n currency\n projectId\n project {\n id\n name\n }\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 projectId: string;\n project: { id: string; name: string } | null;\n}\n\ninterface BudgetsData {\n planmagnetBudgets: { items: BudgetRow[]; totalCount: number };\n}\n\nconst STATUS_TABS = ['ALL', ...BUDGET_STATUSES] as const;\n\nexport function ProjectsBudgetsPagePage() {\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const { t } = useI18n();\n const [params] = useSearchParams();\n // Entry point from a project's own page (\"View budgets\"): preset, but still changeable here.\n const [projectId, setProjectId] = useState(params.get('projectId') ?? '');\n const [status, setStatus] = useState<(typeof STATUS_TABS)[number]>('ALL');\n const [search, setSearch] = useState('');\n const term = useDeferredValue(search.trim());\n\n const filter = {\n ...(projectId ? { projectId } : {}),\n ...(status !== 'ALL' ? { status } : {}),\n ...(term ? { search: term } : {}),\n };\n const { data, loading, error } = useQuery<BudgetsData>(BUDGETS, {\n variables: { workspaceId: workspaceId ?? '', filter, pagination: { first: 100 } },\n skip: !workspaceId,\n });\n\n const budgets = data?.planmagnetBudgets.items ?? [];\n const totalCount = data?.planmagnetBudgets.totalCount ?? 0;\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div>\n <h1 className=\"text-2xl font-semibold\">{t('planmagnet.budgets.title', 'Budgets')}</h1>\n <p className=\"text-sm text-muted-foreground\">\n {data\n ? t('planmagnet.budgets.subtitle', '{{count}} budget(s) across projects', {\n count: totalCount,\n })\n : t(\n 'planmagnet.budgets.subtitleFallback',\n 'Financial plans, tracked against actuals'\n )}\n </p>\n </div>\n <CreateBudgetDialog\n presetProjectId={projectId || undefined}\n onCreated={(id) => navigate(detailPath('/projects/budget-detail-page', id))}\n />\n </div>\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"w-full max-w-xs\">\n <ProjectSelect\n id=\"budgets-project-filter\"\n value={projectId}\n onChange={setProjectId}\n noneLabel={t('planmagnet.budgets.allProjects', 'All projects')}\n />\n </div>\n <Input\n aria-label={t('planmagnet.budgets.search', 'Search budgets')}\n placeholder={t('planmagnet.budgets.searchPlaceholder', 'Search by name or description')}\n className=\"w-full max-w-xs\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n </div>\n <div\n className=\"flex flex-wrap gap-1\"\n role=\"group\"\n aria-label={t('planmagnet.budgets.statusFilter', 'Filter by status')}\n >\n {STATUS_TABS.map((s) => (\n <button\n key={s}\n type=\"button\"\n aria-pressed={status === s}\n onClick={() => setStatus(s)}\n className={`px-3 py-1.5 text-sm rounded-md ${status === s ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted'}`}\n >\n {s === 'ALL' ? t('planmagnet.budgets.allStatuses', 'All') : enumLabel(s)}\n </button>\n ))}\n </div>\n\n {error && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {t('planmagnet.budgets.errorLoading', 'Budgets could not be loaded')}: {error.message}\n </p>\n )}\n {/* Only before the first load: a refetch (after create/edit/delete) must not blank the page. */}\n {loading && !data && (\n <div className=\"flex items-center justify-center p-12\">\n <div className=\"animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n )}\n\n {data && budgets.length === 0 && !error && (\n <p className=\"text-center text-sm text-muted-foreground py-8\">\n {projectId || status !== 'ALL' || term\n ? t('planmagnet.budgets.emptyFiltered', 'No budgets match these filters.')\n : t('planmagnet.budgets.empty', 'No budgets yet. Create the first one.')}\n </p>\n )}\n\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\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 <Card\n key={budget.id}\n {...rowLinkProps(\n () => navigate(detailPath('/projects/budget-detail-page', budget.id)),\n budget.name\n )}\n >\n <CardHeader className=\"pb-2\">\n <div className=\"flex items-center justify-between gap-2\">\n <CardTitle className=\"text-base flex items-center gap-2\">\n <Wallet className=\"h-4 w-4 text-primary\" />\n {budget.name}\n </CardTitle>\n <span className=\"text-xs px-2 py-0.5 rounded-full bg-muted capitalize\">\n {enumLabel(budget.status)}\n </span>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <p className=\"text-xs text-muted-foreground\">\n {budget.project?.name ?? t('planmagnet.budgets.noProject', 'No project')}\n </p>\n <div className=\"flex justify-between text-sm\">\n <span className=\"text-muted-foreground\">\n {t('planmagnet.budgets.spent', 'Spent')}\n </span>\n <span className=\"tabular-nums\">\n {money(budget.spentAmount, budget.currency)} /{' '}\n {money(budget.plannedAmount, budget.currency)}\n </span>\n </div>\n <div\n role=\"meter\"\n aria-valuemin={0}\n aria-valuemax={Math.max(100, percent ?? 0)}\n aria-valuenow={percent ?? 0}\n aria-label={`${budget.name}: ${percent === null ? 'no plan' : `${percent}%`} spent`}\n className={`h-2 w-full overflow-hidden rounded-full ${style.track}`}\n >\n <div\n className={`h-full rounded-full ${style.fill}`}\n style={{ width: `${Math.min(percent ?? 0, 100)}%` }}\n />\n </div>\n <span className={`flex items-center gap-1 text-xs ${style.text}`}>\n <style.icon className=\"h-3 w-3\" aria-hidden={true} />\n {percent === null ? t('planmagnet.budgets.noPlan', 'No plan set') : `${percent}%`}\n </span>\n </CardContent>\n </Card>\n );\n })}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectsCriticalPathPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProjectsCriticalPathPagePage.tsx"],"names":[],"mappings":"AAwDA,wBAAgB,4BAA4B,4CAgL3C"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { useEntityId as n } from "../hooks/useEntityId.js";
|
|
3
|
+
import { ProjectSelect as r } from "../controls/Pickers.js";
|
|
4
|
+
import { useState as i } from "react";
|
|
5
|
+
import { AlertTriangle as a, Route as o } from "lucide-react";
|
|
6
|
+
import { gql as s } from "@apollo/client";
|
|
7
|
+
import { useQuery as c } from "@apollo/client/react";
|
|
8
|
+
import { jsx as l, jsxs as u } from "react/jsx-runtime";
|
|
9
|
+
import { Badge as d, Card as f, CardContent as p, CardHeader as m, CardTitle as h } from "@burdenoff/fe-libs/ui";
|
|
10
|
+
//#region src/planmagnet/pages/ProjectsCriticalPathPagePage.tsx
|
|
11
|
+
var g = s`
|
|
12
|
+
query GetProjectCriticalPath($workspaceId: ID!, $projectId: ID) {
|
|
13
|
+
planmagnetCriticalPath(workspaceId: $workspaceId, projectId: $projectId) {
|
|
14
|
+
totalDurationMinutes
|
|
15
|
+
slack
|
|
16
|
+
path {
|
|
17
|
+
id
|
|
18
|
+
title
|
|
19
|
+
key
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
planmagnetDependencyCycles(workspaceId: $workspaceId, projectId: $projectId) {
|
|
23
|
+
workItemIds
|
|
24
|
+
length
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
`, _ = s`
|
|
28
|
+
query GetCriticalPathProject($id: ID!) {
|
|
29
|
+
planmagnetProject(id: $id) {
|
|
30
|
+
id
|
|
31
|
+
name
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
`;
|
|
35
|
+
function v(e) {
|
|
36
|
+
if (e <= 0) return "0 days";
|
|
37
|
+
let t = Math.round(e);
|
|
38
|
+
if (t < 60) return `${Math.max(1, t)}m`;
|
|
39
|
+
let n = Math.round(t / 60), r = Math.floor(n / 24), i = n % 24;
|
|
40
|
+
return r === 0 ? `${i}h` : i > 0 ? `${r}d ${i}h` : `${r} day${r === 1 ? "" : "s"}`;
|
|
41
|
+
}
|
|
42
|
+
function y() {
|
|
43
|
+
let s = n(), { workspaceId: y } = e(), b = t(), [x, S] = i(""), { data: C, loading: w, error: T } = c(g, {
|
|
44
|
+
variables: {
|
|
45
|
+
workspaceId: y ?? "",
|
|
46
|
+
projectId: s
|
|
47
|
+
},
|
|
48
|
+
skip: !y || !s
|
|
49
|
+
}), { data: E } = c(_, {
|
|
50
|
+
variables: { id: s },
|
|
51
|
+
skip: !s
|
|
52
|
+
});
|
|
53
|
+
if (!s) return /* @__PURE__ */ u("div", {
|
|
54
|
+
className: "space-y-6 p-6",
|
|
55
|
+
children: [/* @__PURE__ */ u("div", { children: [/* @__PURE__ */ u("h1", {
|
|
56
|
+
className: "text-2xl font-semibold flex items-center gap-2",
|
|
57
|
+
children: [/* @__PURE__ */ l(o, { className: "h-5 w-5 text-primary" }), "Critical Path"]
|
|
58
|
+
}), /* @__PURE__ */ l("p", {
|
|
59
|
+
className: "text-sm text-muted-foreground",
|
|
60
|
+
children: "Choose a project to see its critical path."
|
|
61
|
+
})] }), /* @__PURE__ */ l(f, { children: /* @__PURE__ */ u(p, {
|
|
62
|
+
className: "pt-6 space-y-3",
|
|
63
|
+
children: [
|
|
64
|
+
/* @__PURE__ */ l("label", {
|
|
65
|
+
htmlFor: "critical-path-project",
|
|
66
|
+
className: "text-sm font-medium",
|
|
67
|
+
children: "Project"
|
|
68
|
+
}),
|
|
69
|
+
/* @__PURE__ */ l(r, {
|
|
70
|
+
id: "critical-path-project",
|
|
71
|
+
value: x,
|
|
72
|
+
onChange: S,
|
|
73
|
+
noneLabel: "Choose a project…"
|
|
74
|
+
}),
|
|
75
|
+
/* @__PURE__ */ l("button", {
|
|
76
|
+
type: "button",
|
|
77
|
+
className: "text-sm text-primary hover:underline disabled:pointer-events-none disabled:opacity-50",
|
|
78
|
+
disabled: !x,
|
|
79
|
+
onClick: () => b(`/projects/critical-path-page/${encodeURIComponent(x)}`),
|
|
80
|
+
children: "View critical path →"
|
|
81
|
+
})
|
|
82
|
+
]
|
|
83
|
+
}) })]
|
|
84
|
+
});
|
|
85
|
+
if (w && !C) return /* @__PURE__ */ l("div", {
|
|
86
|
+
className: "flex items-center justify-center p-12",
|
|
87
|
+
children: /* @__PURE__ */ l("div", { className: "animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full" })
|
|
88
|
+
});
|
|
89
|
+
if (T) return /* @__PURE__ */ u("div", {
|
|
90
|
+
className: "p-6 text-red-500",
|
|
91
|
+
children: ["Error computing critical path: ", T.message]
|
|
92
|
+
});
|
|
93
|
+
let D = C?.planmagnetCriticalPath.path ?? [], O = C?.planmagnetCriticalPath.slack ?? {}, k = C?.planmagnetCriticalPath.totalDurationMinutes ?? 0, A = C?.planmagnetDependencyCycles ?? [], j = E?.planmagnetProject?.name;
|
|
94
|
+
return /* @__PURE__ */ u("div", {
|
|
95
|
+
className: "space-y-6 p-6",
|
|
96
|
+
children: [
|
|
97
|
+
/* @__PURE__ */ l("div", {
|
|
98
|
+
className: "flex items-center justify-between",
|
|
99
|
+
children: /* @__PURE__ */ u("div", { children: [/* @__PURE__ */ u("h1", {
|
|
100
|
+
className: "text-2xl font-semibold flex items-center gap-2",
|
|
101
|
+
children: [/* @__PURE__ */ l(o, { className: "h-5 w-5 text-primary" }), "Critical Path"]
|
|
102
|
+
}), /* @__PURE__ */ u("p", {
|
|
103
|
+
className: "text-sm text-muted-foreground",
|
|
104
|
+
children: [j ?? "This project", " — the longest chain of dependent work; a delay on any node here delays the whole chain."]
|
|
105
|
+
})] })
|
|
106
|
+
}),
|
|
107
|
+
A.length > 0 && /* @__PURE__ */ u("div", {
|
|
108
|
+
role: "alert",
|
|
109
|
+
className: "flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 p-4 text-sm text-destructive",
|
|
110
|
+
children: [/* @__PURE__ */ l(a, { className: "h-4 w-4 mt-0.5 shrink-0" }), /* @__PURE__ */ u("div", { children: [/* @__PURE__ */ u("p", {
|
|
111
|
+
className: "font-medium",
|
|
112
|
+
children: [
|
|
113
|
+
A.length,
|
|
114
|
+
" dependency cycle",
|
|
115
|
+
A.length === 1 ? "" : "s",
|
|
116
|
+
" detected."
|
|
117
|
+
]
|
|
118
|
+
}), /* @__PURE__ */ l("p", { children: "A cycle has no valid schedule, so the path below excludes the items involved. Break each cycle by removing or re-pointing one of its dependencies from the task detail page." })] })]
|
|
119
|
+
}),
|
|
120
|
+
/* @__PURE__ */ u(f, { children: [/* @__PURE__ */ l(m, { children: /* @__PURE__ */ l(h, { children: "Total duration" }) }), /* @__PURE__ */ u(p, { children: [/* @__PURE__ */ l("p", {
|
|
121
|
+
className: "text-2xl font-bold",
|
|
122
|
+
children: v(k)
|
|
123
|
+
}), /* @__PURE__ */ l("p", {
|
|
124
|
+
className: "text-xs text-muted-foreground mt-1",
|
|
125
|
+
children: "Work item spans plus dependency lag/lead, summed along the longest chain."
|
|
126
|
+
})] })] }),
|
|
127
|
+
/* @__PURE__ */ u(f, { children: [/* @__PURE__ */ l(m, { children: /* @__PURE__ */ u(h, { children: [
|
|
128
|
+
"Path (",
|
|
129
|
+
D.length,
|
|
130
|
+
" item",
|
|
131
|
+
D.length === 1 ? "" : "s",
|
|
132
|
+
")"
|
|
133
|
+
] }) }), /* @__PURE__ */ u(p, {
|
|
134
|
+
className: "space-y-2",
|
|
135
|
+
children: [D.map((e, t) => /* @__PURE__ */ u("div", {
|
|
136
|
+
className: "flex items-center justify-between gap-3 rounded-md border border-input p-3",
|
|
137
|
+
children: [/* @__PURE__ */ u("div", {
|
|
138
|
+
className: "flex items-center gap-3 min-w-0",
|
|
139
|
+
children: [/* @__PURE__ */ l("span", {
|
|
140
|
+
className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary text-xs font-medium text-primary-foreground",
|
|
141
|
+
children: t + 1
|
|
142
|
+
}), /* @__PURE__ */ u("button", {
|
|
143
|
+
type: "button",
|
|
144
|
+
className: "min-w-0 truncate text-left text-sm font-medium hover:underline",
|
|
145
|
+
onClick: () => b(`/tasks/task-detail-page?taskId=${encodeURIComponent(e.id)}`),
|
|
146
|
+
children: [e.key ? `${e.key} ` : "", e.title ?? "Untitled work item"]
|
|
147
|
+
})]
|
|
148
|
+
}), /* @__PURE__ */ l(d, {
|
|
149
|
+
variant: (O[e.id] ?? 0) === 0 ? "default" : "secondary",
|
|
150
|
+
className: "shrink-0",
|
|
151
|
+
children: (O[e.id] ?? 0) === 0 ? "On critical path" : `${v(O[e.id] ?? 0)} slack`
|
|
152
|
+
})]
|
|
153
|
+
}, e.id)), D.length === 0 && /* @__PURE__ */ l("p", {
|
|
154
|
+
className: "text-center text-muted-foreground py-8",
|
|
155
|
+
children: "No schedulable dependencies yet. Add dependencies from a task's detail page to compute a critical path."
|
|
156
|
+
})]
|
|
157
|
+
})] })
|
|
158
|
+
]
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
162
|
+
export { y as ProjectsCriticalPathPagePage };
|
|
163
|
+
|
|
164
|
+
//# sourceMappingURL=ProjectsCriticalPathPagePage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectsCriticalPathPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProjectsCriticalPathPagePage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Badge } from '@burdenoff/fe-libs/ui';\nimport { Route, AlertTriangle } from 'lucide-react';\nimport { useEntityId } from '../hooks/useEntityId';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { ProjectSelect } from '../controls/Pickers';\n\n/**\n * Critical path view for a project (BOFF-8047). planmagnetCriticalPath / planmagnetDependencyCycles were\n * served and prod-verified (duration-weighted CPM over WorkItemDependency, per the P0 decision that this type\n * is CPM-only) but had no UI. Reached from the sidebar (no project preselects one) and from a project's detail\n * page (\"Critical path\" button).\n */\n\nconst GET_CRITICAL_PATH = gql`\n query GetProjectCriticalPath($workspaceId: ID!, $projectId: ID) {\n planmagnetCriticalPath(workspaceId: $workspaceId, projectId: $projectId) {\n totalDurationMinutes\n slack\n path {\n id\n title\n key\n }\n }\n planmagnetDependencyCycles(workspaceId: $workspaceId, projectId: $projectId) {\n workItemIds\n length\n }\n }\n`;\n\nconst GET_CRITICAL_PATH_PROJECT = gql`\n query GetCriticalPathProject($id: ID!) {\n planmagnetProject(id: $id) {\n id\n name\n }\n }\n`;\n\nfunction durationLabel(totalMinutes: number): string {\n if (totalMinutes <= 0) return '0 days';\n // Round once, then split. Rounding the day remainder separately printed \"1d 24h\" for 47.5–47.9 h, and fractional\n // minutes (durations are elapsed ms / 60000) printed decimals.\n const roundedMinutes = Math.round(totalMinutes);\n if (roundedMinutes < 60) return `${Math.max(1, roundedMinutes)}m`;\n const totalHours = Math.round(roundedMinutes / 60);\n const days = Math.floor(totalHours / 24);\n const hours = totalHours % 24;\n if (days === 0) return `${hours}h`;\n return hours > 0 ? `${days}d ${hours}h` : `${days} day${days === 1 ? '' : 's'}`;\n}\n\nexport function ProjectsCriticalPathPagePage() {\n const projectId = useEntityId();\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const [pickerProjectId, setPickerProjectId] = useState('');\n\n const { data, loading, error } = useQuery<{\n planmagnetCriticalPath: {\n totalDurationMinutes: number;\n // The SDL doc comment on this field says \"days\" but the resolver\n // actually returns MINUTES (wspace-planmagnet-svc\n // src/modules/dependency/services/dependency-graph.ts:53, backed by\n // durationMinutes() in dependency.service.ts) — same unit as\n // totalDurationMinutes above. Feed values through durationLabel()\n // below, never render this number bare. SDL doc fix tracked\n // separately; do not \"fix\" this comment to match the SDL.\n slack: Record<string, number>;\n path: { id: string; title: string | null; key: string | null }[];\n };\n planmagnetDependencyCycles: { workItemIds: string[]; length: number }[];\n }>(GET_CRITICAL_PATH, {\n variables: { workspaceId: workspaceId ?? '', projectId },\n skip: !workspaceId || !projectId,\n });\n const { data: projectData } = useQuery<{\n planmagnetProject: { id: string; name: string } | null;\n }>(GET_CRITICAL_PATH_PROJECT, { variables: { id: projectId }, skip: !projectId });\n\n if (!projectId) {\n return (\n <div className=\"space-y-6 p-6\">\n <div>\n <h1 className=\"text-2xl font-semibold flex items-center gap-2\">\n <Route className=\"h-5 w-5 text-primary\" />\n Critical Path\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n Choose a project to see its critical path.\n </p>\n </div>\n <Card>\n <CardContent className=\"pt-6 space-y-3\">\n <label htmlFor=\"critical-path-project\" className=\"text-sm font-medium\">\n Project\n </label>\n <ProjectSelect\n id=\"critical-path-project\"\n value={pickerProjectId}\n onChange={setPickerProjectId}\n noneLabel=\"Choose a project…\"\n />\n <button\n type=\"button\"\n className=\"text-sm text-primary hover:underline disabled:pointer-events-none disabled:opacity-50\"\n disabled={!pickerProjectId}\n onClick={() =>\n navigate(`/projects/critical-path-page/${encodeURIComponent(pickerProjectId)}`)\n }\n >\n View critical path →\n </button>\n </CardContent>\n </Card>\n </div>\n );\n }\n\n // Only before the first load: a refetch must not blank the page.\n if (loading && !data)\n return (\n <div className=\"flex items-center justify-center p-12\">\n <div className=\"animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n );\n if (error)\n return <div className=\"p-6 text-red-500\">Error computing critical path: {error.message}</div>;\n\n const path = data?.planmagnetCriticalPath.path ?? [];\n const slack = data?.planmagnetCriticalPath.slack ?? {};\n const totalMinutes = data?.planmagnetCriticalPath.totalDurationMinutes ?? 0;\n const cycles = data?.planmagnetDependencyCycles ?? [];\n const projectName = projectData?.planmagnetProject?.name;\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold flex items-center gap-2\">\n <Route className=\"h-5 w-5 text-primary\" />\n Critical Path\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {projectName ?? 'This project'} — the longest chain of dependent work; a delay on any\n node here delays the whole chain.\n </p>\n </div>\n </div>\n\n {cycles.length > 0 && (\n <div\n role=\"alert\"\n className=\"flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 p-4 text-sm text-destructive\"\n >\n <AlertTriangle className=\"h-4 w-4 mt-0.5 shrink-0\" />\n <div>\n <p className=\"font-medium\">\n {cycles.length} dependency cycle{cycles.length === 1 ? '' : 's'} detected.\n </p>\n <p>\n A cycle has no valid schedule, so the path below excludes the items involved. Break\n each cycle by removing or re-pointing one of its dependencies from the task detail\n page.\n </p>\n </div>\n </div>\n )}\n\n <Card>\n <CardHeader>\n <CardTitle>Total duration</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-2xl font-bold\">{durationLabel(totalMinutes)}</p>\n <p className=\"text-xs text-muted-foreground mt-1\">\n Work item spans plus dependency lag/lead, summed along the longest chain.\n </p>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>\n Path ({path.length} item{path.length === 1 ? '' : 's'})\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n {path.map((node, index) => (\n <div\n key={node.id}\n className=\"flex items-center justify-between gap-3 rounded-md border border-input p-3\"\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary text-xs font-medium text-primary-foreground\">\n {index + 1}\n </span>\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left text-sm font-medium hover:underline\"\n onClick={() =>\n navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(node.id)}`)\n }\n >\n {node.key ? `${node.key} ` : ''}\n {node.title ?? 'Untitled work item'}\n </button>\n </div>\n <Badge\n variant={(slack[node.id] ?? 0) === 0 ? 'default' : 'secondary'}\n className=\"shrink-0\"\n >\n {(slack[node.id] ?? 0) === 0\n ? 'On critical path'\n : `${durationLabel(slack[node.id] ?? 0)} slack`}\n </Badge>\n </div>\n ))}\n {path.length === 0 && (\n <p className=\"text-center text-muted-foreground py-8\">\n No schedulable dependencies yet. Add dependencies from a task's detail page to\n compute a critical path.\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectsCustomFieldsPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProjectsCustomFieldsPagePage.tsx"],"names":[],"mappings":"AAiFA,wBAAgB,4BAA4B,4CAiH3C"}
|