@burdenoff/microfe-planmagnet 2026.928.3 → 2026.928.4

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 (84) hide show
  1. package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
  2. package/dist/planmagnet/PlanMagnetRoot.js +102 -84
  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 +103 -55
  6. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  7. package/dist/planmagnet/controls/CompetitorControls.d.ts +20 -0
  8. package/dist/planmagnet/controls/CompetitorControls.d.ts.map +1 -0
  9. package/dist/planmagnet/controls/CompetitorControls.js +172 -0
  10. package/dist/planmagnet/controls/CompetitorControls.js.map +1 -0
  11. package/dist/planmagnet/controls/FeatureControls.js +29 -29
  12. package/dist/planmagnet/controls/FeatureTriage.js +11 -11
  13. package/dist/planmagnet/controls/ObjectiveControls.js +24 -24
  14. package/dist/planmagnet/controls/Pickers.d.ts +17 -2
  15. package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
  16. package/dist/planmagnet/controls/Pickers.js +100 -9
  17. package/dist/planmagnet/controls/Pickers.js.map +1 -1
  18. package/dist/planmagnet/controls/PortfolioControls.d.ts +13 -0
  19. package/dist/planmagnet/controls/PortfolioControls.d.ts.map +1 -0
  20. package/dist/planmagnet/controls/PortfolioControls.js +114 -0
  21. package/dist/planmagnet/controls/PortfolioControls.js.map +1 -0
  22. package/dist/planmagnet/controls/ProgramControls.d.ts +25 -0
  23. package/dist/planmagnet/controls/ProgramControls.d.ts.map +1 -0
  24. package/dist/planmagnet/controls/ProgramControls.js +187 -0
  25. package/dist/planmagnet/controls/ProgramControls.js.map +1 -0
  26. package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.d.ts +2 -0
  27. package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.d.ts.map +1 -0
  28. package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.js +127 -0
  29. package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.js.map +1 -0
  30. package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.d.ts +2 -0
  31. package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.d.ts.map +1 -0
  32. package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.js +129 -0
  33. package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.js.map +1 -0
  34. package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.d.ts +2 -0
  35. package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.d.ts.map +1 -0
  36. package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.js +132 -0
  37. package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.js.map +1 -0
  38. package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.d.ts +2 -0
  39. package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.d.ts.map +1 -0
  40. package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.js +181 -0
  41. package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.js.map +1 -0
  42. package/dist/planmagnet/pages/PortfoliosCreatePortfolioPagePage.d.ts +2 -0
  43. package/dist/planmagnet/pages/PortfoliosCreatePortfolioPagePage.d.ts.map +1 -0
  44. package/dist/planmagnet/pages/PortfoliosCreatePortfolioPagePage.js +242 -0
  45. package/dist/planmagnet/pages/PortfoliosCreatePortfolioPagePage.js.map +1 -0
  46. package/dist/planmagnet/pages/PortfoliosEditPortfolioPagePage.d.ts +2 -0
  47. package/dist/planmagnet/pages/PortfoliosEditPortfolioPagePage.d.ts.map +1 -0
  48. package/dist/planmagnet/pages/PortfoliosEditPortfolioPagePage.js +302 -0
  49. package/dist/planmagnet/pages/PortfoliosEditPortfolioPagePage.js.map +1 -0
  50. package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.d.ts +2 -0
  51. package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.d.ts.map +1 -0
  52. package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js +335 -0
  53. package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js.map +1 -0
  54. package/dist/planmagnet/pages/PortfoliosPortfoliosPagePage.d.ts +2 -0
  55. package/dist/planmagnet/pages/PortfoliosPortfoliosPagePage.d.ts.map +1 -0
  56. package/dist/planmagnet/pages/PortfoliosPortfoliosPagePage.js +154 -0
  57. package/dist/planmagnet/pages/PortfoliosPortfoliosPagePage.js.map +1 -0
  58. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
  59. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +124 -138
  60. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
  61. package/dist/planmagnet/pages/ProgramsCreateProgramPagePage.d.ts +2 -0
  62. package/dist/planmagnet/pages/ProgramsCreateProgramPagePage.d.ts.map +1 -0
  63. package/dist/planmagnet/pages/ProgramsCreateProgramPagePage.js +221 -0
  64. package/dist/planmagnet/pages/ProgramsCreateProgramPagePage.js.map +1 -0
  65. package/dist/planmagnet/pages/ProgramsEditProgramPagePage.d.ts +2 -0
  66. package/dist/planmagnet/pages/ProgramsEditProgramPagePage.d.ts.map +1 -0
  67. package/dist/planmagnet/pages/ProgramsEditProgramPagePage.js +279 -0
  68. package/dist/planmagnet/pages/ProgramsEditProgramPagePage.js.map +1 -0
  69. package/dist/planmagnet/pages/ProgramsProgramDetailPagePage.d.ts +2 -0
  70. package/dist/planmagnet/pages/ProgramsProgramDetailPagePage.d.ts.map +1 -0
  71. package/dist/planmagnet/pages/ProgramsProgramDetailPagePage.js +178 -0
  72. package/dist/planmagnet/pages/ProgramsProgramDetailPagePage.js.map +1 -0
  73. package/dist/planmagnet/pages/ProgramsProgramsPagePage.d.ts +2 -0
  74. package/dist/planmagnet/pages/ProgramsProgramsPagePage.d.ts.map +1 -0
  75. package/dist/planmagnet/pages/ProgramsProgramsPagePage.js +157 -0
  76. package/dist/planmagnet/pages/ProgramsProgramsPagePage.js.map +1 -0
  77. package/dist/planmagnet/pages/ProjectsEditProjectPagePage.d.ts.map +1 -1
  78. package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js +130 -103
  79. package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js.map +1 -1
  80. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
  81. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +156 -136
  82. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
  83. package/dist/planmagnet/pages/TasksImportPagePage.js +5 -5
  84. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PortfoliosEditPortfolioPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/PortfoliosEditPortfolioPagePage.tsx"],"sourcesContent":["import { useState, useMemo } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery, useMutation } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Button, Input } from '@burdenoff/fe-libs/ui';\nimport { Save, ArrowLeft, Loader2 } from 'lucide-react';\nimport { useParams } from 'react-router';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { throwOnError } from '../utils/mutationResult';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport { PORTFOLIO_STATUSES, STRATEGIC_ALIGNMENTS } from '../controls/PortfolioControls';\n\nconst GET_PORTFOLIO = gql`\n query GetPortfolioForEdit($id: ID!) {\n planmagnetPortfolio(id: $id) {\n id\n name\n description\n status\n alignment\n alignmentLabel\n budgetTotal\n currency\n fiscalPeriod\n tags\n }\n }\n`;\n\nconst UPDATE_PORTFOLIO = gql`\n mutation UpdatePortfolio($id: ID!, $input: PlanmagnetUpdatePortfolioInput!) {\n planmagnetUpdatePortfolio(id: $id, input: $input) {\n id\n name\n description\n status\n alignment\n alignmentLabel\n budgetTotal\n currency\n fiscalPeriod\n tags\n updatedAt\n }\n }\n`;\n\nexport function PortfoliosEditPortfolioPagePage() {\n const navigate = usePlanMagnetNavigate();\n const params = useParams<{ id: string }>();\n const portfolioId = params.id ?? '';\n\n const { data, loading, error } = useQuery<{\n planmagnetPortfolio: {\n id: string;\n name: string;\n description: string | null;\n status: string;\n alignment: string | null;\n alignmentLabel: string | null;\n budgetTotal: number | null;\n currency: string | null;\n fiscalPeriod: string | null;\n tags: string[];\n } | null;\n }>(GET_PORTFOLIO, { variables: { id: portfolioId }, skip: !portfolioId });\n\n const [updatePortfolio, { loading: saving }] = useMutation(UPDATE_PORTFOLIO, {\n refetchQueries: [\n 'ListPortfolios',\n 'GetPortfolioForEdit',\n 'GetPortfolio',\n 'GetPortfolioOverview',\n ],\n awaitRefetchQueries: true,\n });\n const [saveError, setSaveError] = useState<string | null>(null);\n const [saved, setSaved] = useState(false);\n\n const portfolio = data?.planmagnetPortfolio;\n\n const defaultForm = useMemo(\n () => ({\n name: portfolio?.name ?? '',\n description: portfolio?.description ?? '',\n status: portfolio?.status ?? 'ACTIVE',\n alignment: portfolio?.alignment ?? '',\n alignmentLabel: portfolio?.alignmentLabel ?? '',\n budgetTotal: portfolio?.budgetTotal != null ? String(portfolio.budgetTotal) : '',\n currency: portfolio?.currency ?? '',\n fiscalPeriod: portfolio?.fiscalPeriod ?? '',\n tags: portfolio?.tags?.join(', ') ?? '',\n }),\n [portfolio]\n );\n\n const [formOverrides, setFormOverrides] = useState<Record<string, string>>({});\n const form = { ...defaultForm, ...formOverrides };\n const setForm = (val: Record<string, string>) => setFormOverrides(val);\n\n const statuses = (PORTFOLIO_STATUSES as readonly string[]).includes(form.status)\n ? PORTFOLIO_STATUSES\n : [form.status, ...PORTFOLIO_STATUSES];\n const needsLabel = form.alignment === 'CUSTOM' && !form.alignmentLabel.trim();\n const invalidBudget = form.budgetTotal !== '' && Number(form.budgetTotal) < 0;\n\n const handleSave = async () => {\n if (!portfolio || !form.name.trim() || needsLabel || invalidBudget) return;\n const tagList = (v: string) =>\n v\n .split(',')\n .map((t) => t.trim())\n .filter(Boolean);\n const next: Record<string, unknown> = {\n name: form.name.trim(),\n description: form.description.trim() || null,\n status: form.status,\n alignment: form.alignment || null,\n alignmentLabel: form.alignment === 'CUSTOM' ? form.alignmentLabel.trim() : null,\n budgetTotal: form.budgetTotal !== '' ? Number(form.budgetTotal) : null,\n currency: form.currency.trim() || null,\n fiscalPeriod: form.fiscalPeriod.trim() || null,\n tags: tagList(form.tags),\n };\n const before: Record<string, unknown> = {\n name: defaultForm.name,\n description: defaultForm.description.trim() || null,\n status: defaultForm.status,\n alignment: defaultForm.alignment || null,\n alignmentLabel: defaultForm.alignment === 'CUSTOM' ? defaultForm.alignmentLabel.trim() : null,\n budgetTotal: defaultForm.budgetTotal !== '' ? Number(defaultForm.budgetTotal) : null,\n currency: defaultForm.currency.trim() || null,\n fiscalPeriod: defaultForm.fiscalPeriod.trim() || null,\n tags: tagList(defaultForm.tags),\n };\n const input = Object.fromEntries(\n Object.entries(next).filter(([k, v]) => JSON.stringify(v) !== JSON.stringify(before[k]))\n );\n setSaveError(null);\n setSaved(false);\n if (Object.keys(input).length === 0) {\n setSaved(true);\n return;\n }\n try {\n await throwOnError(updatePortfolio({ variables: { id: portfolioId, input } }));\n setSaved(true);\n } catch (e) {\n setSaveError(e instanceof Error ? e.message : 'The portfolio could not be saved.');\n }\n };\n\n // Only before the first load: a refetch must not blank the page (it unmounts every component on it and\n // loses their state).\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-destructive\">Failed to load portfolio: {error.message}</div>;\n }\n\n if (!portfolio) return <div className=\"p-6 text-muted-foreground\">Portfolio not found.</div>;\n\n return (\n <div className=\"space-y-6 p-6 max-w-4xl\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n aria-label=\"Back to the portfolio\"\n onClick={() => navigate(detailPath('/products/portfolio-detail-page', portfolioId))}\n >\n <ArrowLeft className=\"h-4 w-4\" />\n </Button>\n <div>\n <h1 className=\"text-2xl font-semibold\">Edit Portfolio</h1>\n <p className=\"text-sm text-muted-foreground\">{portfolio.name}</p>\n </div>\n </div>\n <Button size=\"sm\" onClick={handleSave} disabled={saving || needsLabel || invalidBudget}>\n {saving ? (\n <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"mr-1 h-4 w-4\" />\n )}\n Save Changes\n </Button>\n </div>\n\n {saved && (\n <div role=\"status\" className=\"p-3 bg-muted text-status-success-text rounded-md text-sm\">\n Changes saved.\n </div>\n )}\n {saveError && (\n <div role=\"alert\" className=\"p-3 rounded-md bg-destructive/10 text-destructive text-sm\">\n {saveError}\n </div>\n )}\n\n <Card>\n <CardHeader>\n <CardTitle>Portfolio Details</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div>\n <label className=\"text-sm font-medium\">Name</label>\n <Input\n value={form.name}\n onChange={(e) => setForm({ ...form, name: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n <div>\n <label className=\"text-sm font-medium\">Description</label>\n <textarea\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[80px]\"\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n />\n </div>\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label htmlFor=\"portfolio-edit-status\" className=\"text-sm font-medium\">\n Status\n </label>\n <select\n id=\"portfolio-edit-status\"\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={form.status}\n onChange={(e) => setForm({ ...form, status: e.target.value })}\n >\n {statuses.map((s) => (\n <option key={s} value={s}>\n {enumLabel(s)}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"portfolio-edit-alignment\" className=\"text-sm font-medium\">\n Strategic Alignment\n </label>\n <select\n id=\"portfolio-edit-alignment\"\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={form.alignment}\n onChange={(e) => setForm({ ...form, alignment: e.target.value })}\n >\n <option value=\"\">Not set</option>\n {STRATEGIC_ALIGNMENTS.map((a) => (\n <option key={a} value={a}>\n {a === 'CUSTOM' ? 'Custom…' : enumLabel(a)}\n </option>\n ))}\n </select>\n </div>\n </div>\n {form.alignment === 'CUSTOM' && (\n <div>\n <label className=\"text-sm font-medium\">Custom Alignment Label</label>\n <Input\n value={form.alignmentLabel}\n onChange={(e) => setForm({ ...form, alignmentLabel: e.target.value })}\n className=\"mt-1\"\n aria-invalid={needsLabel}\n />\n {needsLabel && (\n <p role=\"alert\" className=\"text-sm text-destructive mt-1\">\n A custom alignment needs a label.\n </p>\n )}\n </div>\n )}\n <div className=\"grid grid-cols-3 gap-4\">\n <div>\n <label className=\"text-sm font-medium\">Total Budget</label>\n <Input\n type=\"number\"\n min=\"0\"\n value={form.budgetTotal}\n onChange={(e) => setForm({ ...form, budgetTotal: e.target.value })}\n className=\"mt-1\"\n aria-invalid={invalidBudget}\n />\n </div>\n <div>\n <label className=\"text-sm font-medium\">Currency</label>\n <Input\n value={form.currency}\n onChange={(e) => setForm({ ...form, currency: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n <div>\n <label className=\"text-sm font-medium\">Fiscal Period</label>\n <Input\n value={form.fiscalPeriod}\n onChange={(e) => setForm({ ...form, fiscalPeriod: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n </div>\n {invalidBudget && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n The budget cannot be negative.\n </p>\n )}\n <div>\n <label className=\"text-sm font-medium\">Tags (comma-separated)</label>\n <Input\n value={form.tags}\n onChange={(e) => setForm({ ...form, tags: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function PortfoliosPortfolioDetailPagePage(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=PortfoliosPortfolioDetailPagePage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PortfoliosPortfolioDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/PortfoliosPortfolioDetailPagePage.tsx"],"names":[],"mappings":"AA6EA,wBAAgB,iCAAiC,4CAuQhD"}
@@ -0,0 +1,335 @@
1
+ import { usePlanMagnetNavigate as e } from "../providers/PlanMagnetProvider.js";
2
+ import { PersonName as t } from "../people/People.js";
3
+ import { detailPath as n } from "../utils/rowNavigation.js";
4
+ import { enumLabel as r } from "../controls/WorkItemControls.js";
5
+ import { PortfolioHeaderActions as i } from "../controls/PortfolioControls.js";
6
+ import { AlertTriangle as a, Briefcase as o, Clock as s, Loader2 as c, Target as l } from "lucide-react";
7
+ import { useParams as u } from "react-router";
8
+ import { gql as d } from "@apollo/client";
9
+ import { useQuery as f } from "@apollo/client/react";
10
+ import { jsx as p, jsxs as m } from "react/jsx-runtime";
11
+ import { Badge as h, Card as g, CardContent as _, CardHeader as v, CardTitle as y } from "@burdenoff/fe-libs/ui";
12
+ //#region src/planmagnet/pages/PortfoliosPortfolioDetailPagePage.tsx
13
+ var b = d`
14
+ query GetPortfolio($id: ID!) {
15
+ planmagnetPortfolio(id: $id) {
16
+ id
17
+ workspaceId
18
+ name
19
+ description
20
+ status
21
+ alignment
22
+ alignmentLabel
23
+ ownerId
24
+ budgetTotal
25
+ currency
26
+ fiscalPeriod
27
+ tags
28
+ createdAt
29
+ updatedAt
30
+ programs {
31
+ id
32
+ name
33
+ status
34
+ }
35
+ projects {
36
+ id
37
+ name
38
+ status
39
+ }
40
+ }
41
+ }
42
+ `, x = d`
43
+ query GetPortfolioOverview($portfolioId: ID!) {
44
+ planmagnetPortfolioOverview(portfolioId: $portfolioId) {
45
+ portfolioId
46
+ totalPrograms
47
+ totalProjects
48
+ projectsByStatus
49
+ totalBudget
50
+ totalSpent
51
+ budgetUtilization
52
+ healthScore
53
+ atRiskCount
54
+ overdueMilestones
55
+ }
56
+ }
57
+ `;
58
+ function S() {
59
+ let d = e(), S = u().id ?? "", { data: C, loading: w, error: T } = f(b, {
60
+ variables: { id: S },
61
+ skip: !S
62
+ }), { data: E } = f(x, {
63
+ variables: { portfolioId: S },
64
+ skip: !S
65
+ });
66
+ if (w && !C) return /* @__PURE__ */ p("div", {
67
+ className: "flex items-center justify-center p-12",
68
+ children: /* @__PURE__ */ p(c, { className: "h-8 w-8 animate-spin text-muted-foreground" })
69
+ });
70
+ if (T) return /* @__PURE__ */ m("div", {
71
+ className: "p-6 text-destructive",
72
+ children: ["Failed to load portfolio: ", T.message]
73
+ });
74
+ let D = C?.planmagnetPortfolio;
75
+ if (!D) return /* @__PURE__ */ p("div", {
76
+ className: "p-6 text-muted-foreground",
77
+ children: "Portfolio not found."
78
+ });
79
+ let O = E?.planmagnetPortfolioOverview ?? null;
80
+ return /* @__PURE__ */ m("div", {
81
+ className: "space-y-6 p-6",
82
+ children: [
83
+ /* @__PURE__ */ m("div", {
84
+ className: "flex items-center justify-between",
85
+ children: [/* @__PURE__ */ m("div", { children: [/* @__PURE__ */ m("div", {
86
+ className: "flex items-center gap-2",
87
+ children: [
88
+ /* @__PURE__ */ p(o, { className: "h-5 w-5 text-primary" }),
89
+ /* @__PURE__ */ p("h1", {
90
+ className: "text-2xl font-semibold",
91
+ children: D.name
92
+ }),
93
+ /* @__PURE__ */ p(h, {
94
+ variant: "outline",
95
+ className: "text-xs",
96
+ children: r(D.status)
97
+ })
98
+ ]
99
+ }), D.alignment && /* @__PURE__ */ p("p", {
100
+ className: "text-sm text-muted-foreground mt-1",
101
+ children: D.alignment === "CUSTOM" ? D.alignmentLabel ?? "Custom alignment" : r(D.alignment)
102
+ })] }), /* @__PURE__ */ p(i, {
103
+ portfolioId: D.id,
104
+ name: D.name,
105
+ status: D.status
106
+ })]
107
+ }),
108
+ /* @__PURE__ */ m("div", {
109
+ className: "grid grid-cols-2 md:grid-cols-4 gap-4",
110
+ children: [
111
+ /* @__PURE__ */ p(g, { children: /* @__PURE__ */ m(_, {
112
+ className: "pt-4 text-center",
113
+ children: [
114
+ /* @__PURE__ */ p(o, { className: "h-5 w-5 mx-auto text-primary" }),
115
+ /* @__PURE__ */ p("p", {
116
+ className: "text-2xl font-bold mt-1",
117
+ children: O?.totalPrograms ?? D.programs.length
118
+ }),
119
+ /* @__PURE__ */ p("p", {
120
+ className: "text-xs text-muted-foreground",
121
+ children: "Programs"
122
+ })
123
+ ]
124
+ }) }),
125
+ /* @__PURE__ */ p(g, { children: /* @__PURE__ */ m(_, {
126
+ className: "pt-4 text-center",
127
+ children: [
128
+ /* @__PURE__ */ p(l, { className: "h-5 w-5 mx-auto text-primary" }),
129
+ /* @__PURE__ */ p("p", {
130
+ className: "text-2xl font-bold mt-1",
131
+ children: O?.totalProjects ?? D.projects.length
132
+ }),
133
+ /* @__PURE__ */ p("p", {
134
+ className: "text-xs text-muted-foreground",
135
+ children: "Projects"
136
+ })
137
+ ]
138
+ }) }),
139
+ /* @__PURE__ */ p(g, { children: /* @__PURE__ */ m(_, {
140
+ className: "pt-4 text-center",
141
+ children: [
142
+ /* @__PURE__ */ p(a, { className: "h-5 w-5 mx-auto text-destructive" }),
143
+ /* @__PURE__ */ p("p", {
144
+ className: "text-2xl font-bold mt-1",
145
+ children: O?.atRiskCount ?? 0
146
+ }),
147
+ /* @__PURE__ */ p("p", {
148
+ className: "text-xs text-muted-foreground",
149
+ children: "At Risk"
150
+ })
151
+ ]
152
+ }) }),
153
+ /* @__PURE__ */ p(g, { children: /* @__PURE__ */ m(_, {
154
+ className: "pt-4 text-center",
155
+ children: [
156
+ /* @__PURE__ */ p(s, { className: "h-5 w-5 mx-auto text-status-warning-text" }),
157
+ /* @__PURE__ */ p("p", {
158
+ className: "text-2xl font-bold mt-1",
159
+ children: O?.overdueMilestones ?? 0
160
+ }),
161
+ /* @__PURE__ */ p("p", {
162
+ className: "text-xs text-muted-foreground",
163
+ children: "Overdue Milestones"
164
+ })
165
+ ]
166
+ }) })
167
+ ]
168
+ }),
169
+ /* @__PURE__ */ m("div", {
170
+ className: "grid grid-cols-1 lg:grid-cols-3 gap-6",
171
+ children: [/* @__PURE__ */ m("div", {
172
+ className: "lg:col-span-2 space-y-6",
173
+ children: [
174
+ /* @__PURE__ */ m(g, { children: [/* @__PURE__ */ p(v, { children: /* @__PURE__ */ p(y, { children: "Overview" }) }), /* @__PURE__ */ m(_, {
175
+ className: "space-y-4",
176
+ children: [/* @__PURE__ */ p("p", {
177
+ className: "text-sm",
178
+ children: D.description ?? "No description provided."
179
+ }), O ? /* @__PURE__ */ m("div", {
180
+ className: "grid grid-cols-2 gap-4",
181
+ children: [
182
+ /* @__PURE__ */ m("div", {
183
+ className: "p-3 bg-muted rounded-md",
184
+ children: [/* @__PURE__ */ p("p", {
185
+ className: "text-xs text-muted-foreground",
186
+ children: "Budget Utilization"
187
+ }), /* @__PURE__ */ p("p", {
188
+ className: "text-lg font-semibold mt-1",
189
+ children: O.budgetUtilization == null ? "No budget data" : `${O.budgetUtilization.toFixed(0)}%`
190
+ })]
191
+ }),
192
+ /* @__PURE__ */ m("div", {
193
+ className: "p-3 bg-muted rounded-md",
194
+ children: [/* @__PURE__ */ p("p", {
195
+ className: "text-xs text-muted-foreground",
196
+ children: "Health Score"
197
+ }), /* @__PURE__ */ p("p", {
198
+ className: "text-lg font-semibold mt-1",
199
+ children: O.healthScore == null ? "No projects yet" : O.healthScore.toFixed(0)
200
+ })]
201
+ }),
202
+ /* @__PURE__ */ m("div", {
203
+ className: "p-3 bg-muted rounded-md",
204
+ children: [/* @__PURE__ */ p("p", {
205
+ className: "text-xs text-muted-foreground",
206
+ children: "Total Spent"
207
+ }), /* @__PURE__ */ p("p", {
208
+ className: "text-lg font-semibold mt-1",
209
+ children: O.totalSpent == null ? "N/A" : O.totalSpent.toLocaleString()
210
+ })]
211
+ }),
212
+ /* @__PURE__ */ m("div", {
213
+ className: "p-3 bg-muted rounded-md",
214
+ children: [/* @__PURE__ */ p("p", {
215
+ className: "text-xs text-muted-foreground",
216
+ children: "Projects by Status"
217
+ }), /* @__PURE__ */ p("p", {
218
+ className: "text-sm mt-1",
219
+ children: Object.keys(O.projectsByStatus).length > 0 ? Object.entries(O.projectsByStatus).map(([e, t]) => `${r(e)}: ${t}`).join(", ") : "No projects yet"
220
+ })]
221
+ })
222
+ ]
223
+ }) : /* @__PURE__ */ p("p", {
224
+ className: "text-sm text-muted-foreground",
225
+ children: "Loading overview…"
226
+ })]
227
+ })] }),
228
+ /* @__PURE__ */ m(g, { children: [/* @__PURE__ */ p(v, { children: /* @__PURE__ */ m(y, { children: [
229
+ "Programs (",
230
+ D.programs.length,
231
+ ")"
232
+ ] }) }), /* @__PURE__ */ m(_, {
233
+ className: "space-y-2",
234
+ children: [D.programs.length === 0 && /* @__PURE__ */ p("p", {
235
+ className: "text-sm text-muted-foreground",
236
+ children: "No programs are linked to this portfolio yet."
237
+ }), D.programs.map((e) => /* @__PURE__ */ m("button", {
238
+ type: "button",
239
+ className: "flex w-full items-center justify-between rounded-md border p-3 text-left hover:bg-muted",
240
+ onClick: () => d(n("/products/program-detail-page", e.id)),
241
+ children: [/* @__PURE__ */ p("span", {
242
+ className: "text-sm font-medium",
243
+ children: e.name
244
+ }), /* @__PURE__ */ p(h, {
245
+ variant: "outline",
246
+ className: "text-xs",
247
+ children: r(e.status)
248
+ })]
249
+ }, e.id))]
250
+ })] }),
251
+ /* @__PURE__ */ m(g, { children: [/* @__PURE__ */ p(v, { children: /* @__PURE__ */ m(y, { children: [
252
+ "Projects (",
253
+ D.projects.length,
254
+ ")"
255
+ ] }) }), /* @__PURE__ */ m(_, {
256
+ className: "space-y-2",
257
+ children: [D.projects.length === 0 && /* @__PURE__ */ p("p", {
258
+ className: "text-sm text-muted-foreground",
259
+ children: "No projects are linked to this portfolio yet."
260
+ }), D.projects.map((e) => /* @__PURE__ */ m("button", {
261
+ type: "button",
262
+ className: "flex w-full items-center justify-between rounded-md border p-3 text-left hover:bg-muted",
263
+ onClick: () => d(n("/projects/project-detail-page", e.id)),
264
+ children: [/* @__PURE__ */ p("span", {
265
+ className: "text-sm font-medium",
266
+ children: e.name
267
+ }), /* @__PURE__ */ p(h, {
268
+ variant: "outline",
269
+ className: "text-xs",
270
+ children: r(e.status)
271
+ })]
272
+ }, e.id))]
273
+ })] })
274
+ ]
275
+ }), /* @__PURE__ */ m("div", {
276
+ className: "space-y-6",
277
+ children: [/* @__PURE__ */ m(g, { children: [/* @__PURE__ */ p(v, { children: /* @__PURE__ */ p(y, { children: "Details" }) }), /* @__PURE__ */ m(_, {
278
+ className: "space-y-2 text-sm",
279
+ children: [
280
+ /* @__PURE__ */ m("div", {
281
+ className: "flex justify-between",
282
+ children: [/* @__PURE__ */ p("span", {
283
+ className: "text-muted-foreground",
284
+ children: "Owner"
285
+ }), /* @__PURE__ */ p(t, { id: D.ownerId })]
286
+ }),
287
+ /* @__PURE__ */ m("div", {
288
+ className: "flex justify-between",
289
+ children: [/* @__PURE__ */ p("span", {
290
+ className: "text-muted-foreground",
291
+ children: "Budget"
292
+ }), /* @__PURE__ */ p("span", {
293
+ className: "font-medium",
294
+ children: D.budgetTotal == null ? "Not set" : `${D.currency ?? ""} ${D.budgetTotal.toLocaleString()}`.trim()
295
+ })]
296
+ }),
297
+ /* @__PURE__ */ m("div", {
298
+ className: "flex justify-between",
299
+ children: [/* @__PURE__ */ p("span", {
300
+ className: "text-muted-foreground",
301
+ children: "Fiscal Period"
302
+ }), /* @__PURE__ */ p("span", { children: D.fiscalPeriod ?? "Not set" })]
303
+ }),
304
+ /* @__PURE__ */ m("div", {
305
+ className: "flex justify-between",
306
+ children: [/* @__PURE__ */ p("span", {
307
+ className: "text-muted-foreground",
308
+ children: "Created"
309
+ }), /* @__PURE__ */ p("span", { children: new Date(D.createdAt).toLocaleDateString() })]
310
+ }),
311
+ /* @__PURE__ */ m("div", {
312
+ className: "flex justify-between",
313
+ children: [/* @__PURE__ */ p("span", {
314
+ className: "text-muted-foreground",
315
+ children: "Updated"
316
+ }), /* @__PURE__ */ p("span", { children: new Date(D.updatedAt).toLocaleDateString() })]
317
+ })
318
+ ]
319
+ })] }), D.tags.length > 0 && /* @__PURE__ */ m(g, { children: [/* @__PURE__ */ p(v, { children: /* @__PURE__ */ p(y, { children: "Tags" }) }), /* @__PURE__ */ p(_, {
320
+ className: "flex flex-wrap gap-1",
321
+ children: D.tags.map((e) => /* @__PURE__ */ p(h, {
322
+ variant: "outline",
323
+ className: "text-xs",
324
+ children: e
325
+ }, e))
326
+ })] })]
327
+ })]
328
+ })
329
+ ]
330
+ });
331
+ }
332
+ //#endregion
333
+ export { S as PortfoliosPortfolioDetailPagePage };
334
+
335
+ //# sourceMappingURL=PortfoliosPortfolioDetailPagePage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PortfoliosPortfolioDetailPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/PortfoliosPortfolioDetailPagePage.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Badge } from '@burdenoff/fe-libs/ui';\nimport { Briefcase, Target, AlertTriangle, Clock, Loader2 } from 'lucide-react';\nimport { useParams } from 'react-router';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { PersonName } from '../people/People';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport { PortfolioHeaderActions } from '../controls/PortfolioControls';\n\nconst GET_PORTFOLIO = gql`\n query GetPortfolio($id: ID!) {\n planmagnetPortfolio(id: $id) {\n id\n workspaceId\n name\n description\n status\n alignment\n alignmentLabel\n ownerId\n budgetTotal\n currency\n fiscalPeriod\n tags\n createdAt\n updatedAt\n programs {\n id\n name\n status\n }\n projects {\n id\n name\n status\n }\n }\n }\n`;\n\nconst GET_PORTFOLIO_OVERVIEW = gql`\n query GetPortfolioOverview($portfolioId: ID!) {\n planmagnetPortfolioOverview(portfolioId: $portfolioId) {\n portfolioId\n totalPrograms\n totalProjects\n projectsByStatus\n totalBudget\n totalSpent\n budgetUtilization\n healthScore\n atRiskCount\n overdueMilestones\n }\n }\n`;\n\ninterface RelatedNode {\n id: string;\n name: string;\n status: string;\n}\n\ninterface OverviewNode {\n totalPrograms: number;\n totalProjects: number;\n projectsByStatus: Record<string, number>;\n totalBudget: number | null;\n totalSpent: number | null;\n budgetUtilization: number | null;\n healthScore: number | null;\n atRiskCount: number;\n overdueMilestones: number;\n}\n\nexport function PortfoliosPortfolioDetailPagePage() {\n const navigate = usePlanMagnetNavigate();\n const params = useParams<{ id: string }>();\n const portfolioId = params.id ?? '';\n\n const { data, loading, error } = useQuery<{\n planmagnetPortfolio: {\n id: string;\n name: string;\n description: string | null;\n status: string;\n alignment: string | null;\n alignmentLabel: string | null;\n ownerId: string;\n budgetTotal: number | null;\n currency: string | null;\n fiscalPeriod: string | null;\n tags: string[];\n createdAt: string;\n updatedAt: string;\n programs: RelatedNode[];\n projects: RelatedNode[];\n } | null;\n }>(GET_PORTFOLIO, { variables: { id: portfolioId }, skip: !portfolioId });\n\n const { data: overviewData } = useQuery<{ planmagnetPortfolioOverview: OverviewNode }>(\n GET_PORTFOLIO_OVERVIEW,\n { variables: { portfolioId }, skip: !portfolioId }\n );\n\n // Only before the first load: a refetch must not blank the page (it unmounts every component on it and\n // loses their state).\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-destructive\">Failed to load portfolio: {error.message}</div>;\n }\n\n const portfolio = data?.planmagnetPortfolio;\n if (!portfolio) return <div className=\"p-6 text-muted-foreground\">Portfolio not found.</div>;\n\n const overview = overviewData?.planmagnetPortfolioOverview ?? null;\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <div className=\"flex items-center gap-2\">\n <Briefcase className=\"h-5 w-5 text-primary\" />\n <h1 className=\"text-2xl font-semibold\">{portfolio.name}</h1>\n <Badge variant=\"outline\" className=\"text-xs\">\n {enumLabel(portfolio.status)}\n </Badge>\n </div>\n {portfolio.alignment && (\n <p className=\"text-sm text-muted-foreground mt-1\">\n {portfolio.alignment === 'CUSTOM'\n ? (portfolio.alignmentLabel ?? 'Custom alignment')\n : enumLabel(portfolio.alignment)}\n </p>\n )}\n </div>\n <PortfolioHeaderActions\n portfolioId={portfolio.id}\n name={portfolio.name}\n status={portfolio.status}\n />\n </div>\n\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n <Card>\n <CardContent className=\"pt-4 text-center\">\n <Briefcase className=\"h-5 w-5 mx-auto text-primary\" />\n <p className=\"text-2xl font-bold mt-1\">\n {overview?.totalPrograms ?? portfolio.programs.length}\n </p>\n <p className=\"text-xs text-muted-foreground\">Programs</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4 text-center\">\n <Target className=\"h-5 w-5 mx-auto text-primary\" />\n <p className=\"text-2xl font-bold mt-1\">\n {overview?.totalProjects ?? portfolio.projects.length}\n </p>\n <p className=\"text-xs text-muted-foreground\">Projects</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4 text-center\">\n <AlertTriangle className=\"h-5 w-5 mx-auto text-destructive\" />\n <p className=\"text-2xl font-bold mt-1\">{overview?.atRiskCount ?? 0}</p>\n <p className=\"text-xs text-muted-foreground\">At Risk</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4 text-center\">\n <Clock className=\"h-5 w-5 mx-auto text-status-warning-text\" />\n <p className=\"text-2xl font-bold mt-1\">{overview?.overdueMilestones ?? 0}</p>\n <p className=\"text-xs text-muted-foreground\">Overdue Milestones</p>\n </CardContent>\n </Card>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Overview</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <p className=\"text-sm\">{portfolio.description ?? 'No description provided.'}</p>\n {overview ? (\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"p-3 bg-muted rounded-md\">\n <p className=\"text-xs text-muted-foreground\">Budget Utilization</p>\n <p className=\"text-lg font-semibold mt-1\">\n {overview.budgetUtilization != null\n ? `${overview.budgetUtilization.toFixed(0)}%`\n : 'No budget data'}\n </p>\n </div>\n <div className=\"p-3 bg-muted rounded-md\">\n <p className=\"text-xs text-muted-foreground\">Health Score</p>\n <p className=\"text-lg font-semibold mt-1\">\n {overview.healthScore != null\n ? overview.healthScore.toFixed(0)\n : 'No projects yet'}\n </p>\n </div>\n <div className=\"p-3 bg-muted rounded-md\">\n <p className=\"text-xs text-muted-foreground\">Total Spent</p>\n <p className=\"text-lg font-semibold mt-1\">\n {overview.totalSpent != null ? overview.totalSpent.toLocaleString() : 'N/A'}\n </p>\n </div>\n <div className=\"p-3 bg-muted rounded-md\">\n <p className=\"text-xs text-muted-foreground\">Projects by Status</p>\n <p className=\"text-sm mt-1\">\n {Object.keys(overview.projectsByStatus).length > 0\n ? Object.entries(overview.projectsByStatus)\n .map(([status, count]) => `${enumLabel(status)}: ${count}`)\n .join(', ')\n : 'No projects yet'}\n </p>\n </div>\n </div>\n ) : (\n <p className=\"text-sm text-muted-foreground\">Loading overview…</p>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Programs ({portfolio.programs.length})</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n {portfolio.programs.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">\n No programs are linked to this portfolio yet.\n </p>\n )}\n {portfolio.programs.map((program) => (\n <button\n key={program.id}\n type=\"button\"\n className=\"flex w-full items-center justify-between rounded-md border p-3 text-left hover:bg-muted\"\n onClick={() => navigate(detailPath('/products/program-detail-page', program.id))}\n >\n <span className=\"text-sm font-medium\">{program.name}</span>\n <Badge variant=\"outline\" className=\"text-xs\">\n {enumLabel(program.status)}\n </Badge>\n </button>\n ))}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Projects ({portfolio.projects.length})</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n {portfolio.projects.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">\n No projects are linked to this portfolio yet.\n </p>\n )}\n {portfolio.projects.map((project) => (\n <button\n key={project.id}\n type=\"button\"\n className=\"flex w-full items-center justify-between rounded-md border p-3 text-left hover:bg-muted\"\n onClick={() => navigate(detailPath('/projects/project-detail-page', project.id))}\n >\n <span className=\"text-sm font-medium\">{project.name}</span>\n <Badge variant=\"outline\" className=\"text-xs\">\n {enumLabel(project.status)}\n </Badge>\n </button>\n ))}\n </CardContent>\n </Card>\n </div>\n\n <div className=\"space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Details</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2 text-sm\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Owner</span>\n <PersonName id={portfolio.ownerId} />\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Budget</span>\n <span className=\"font-medium\">\n {portfolio.budgetTotal != null\n ? `${portfolio.currency ?? ''} ${portfolio.budgetTotal.toLocaleString()}`.trim()\n : 'Not set'}\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Fiscal Period</span>\n <span>{portfolio.fiscalPeriod ?? 'Not set'}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Created</span>\n <span>{new Date(portfolio.createdAt).toLocaleDateString()}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Updated</span>\n <span>{new Date(portfolio.updatedAt).toLocaleDateString()}</span>\n </div>\n </CardContent>\n </Card>\n\n {portfolio.tags.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle>Tags</CardTitle>\n </CardHeader>\n <CardContent className=\"flex flex-wrap gap-1\">\n {portfolio.tags.map((tag) => (\n <Badge key={tag} variant=\"outline\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </CardContent>\n </Card>\n )}\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function PortfoliosPortfoliosPagePage(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=PortfoliosPortfoliosPagePage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PortfoliosPortfoliosPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/PortfoliosPortfoliosPagePage.tsx"],"names":[],"mappings":"AA4DA,wBAAgB,4BAA4B,4CAuI3C"}
@@ -0,0 +1,154 @@
1
+ import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/PlanMagnetProvider.js";
2
+ import { PersonName as n } from "../people/People.js";
3
+ import { detailPath as r, rowLinkProps as i } from "../utils/rowNavigation.js";
4
+ import { enumLabel as a } from "../controls/WorkItemControls.js";
5
+ import { PORTFOLIO_STATUSES as o } from "../controls/PortfolioControls.js";
6
+ import { useState as s } from "react";
7
+ import { Briefcase as c, Loader2 as l, Plus as u } from "lucide-react";
8
+ import { gql as d } from "@apollo/client";
9
+ import { useQuery as f } from "@apollo/client/react";
10
+ import { jsx as p, jsxs as m } from "react/jsx-runtime";
11
+ import { Badge as h, Button as g, Card as _, CardContent as v, CardHeader as y, CardTitle as b, Input as x } from "@burdenoff/fe-libs/ui";
12
+ //#region src/planmagnet/pages/PortfoliosPortfoliosPagePage.tsx
13
+ var S = d`
14
+ query ListPortfolios(
15
+ $workspaceId: ID!
16
+ $filter: PlanmagnetPortfolioFilter
17
+ $pagination: PlanmagnetPaginationInput
18
+ ) {
19
+ planmagnetPortfolios(workspaceId: $workspaceId, filter: $filter, pagination: $pagination) {
20
+ items {
21
+ id
22
+ name
23
+ description
24
+ status
25
+ alignment
26
+ alignmentLabel
27
+ ownerId
28
+ budgetTotal
29
+ currency
30
+ fiscalPeriod
31
+ tags
32
+ updatedAt
33
+ }
34
+ totalCount
35
+ }
36
+ }
37
+ `;
38
+ function C() {
39
+ let d = t(), { workspaceId: C } = e(), [w, T] = s("all"), [E, D] = s(""), { data: O, loading: k, error: A } = f(S, {
40
+ variables: {
41
+ workspaceId: C,
42
+ pagination: { first: 50 },
43
+ filter: {
44
+ ...w === "all" ? {} : { status: w },
45
+ ...E.trim() ? { search: E.trim() } : {}
46
+ }
47
+ },
48
+ skip: !C
49
+ });
50
+ if (k && !O) return /* @__PURE__ */ p("div", {
51
+ className: "flex items-center justify-center p-12",
52
+ children: /* @__PURE__ */ p(l, { className: "h-8 w-8 animate-spin text-muted-foreground" })
53
+ });
54
+ if (A) return /* @__PURE__ */ m("div", {
55
+ className: "p-6 text-destructive",
56
+ children: ["Failed to load portfolios: ", A.message]
57
+ });
58
+ let j = O?.planmagnetPortfolios.items ?? [], M = O?.planmagnetPortfolios.totalCount ?? 0, N = ["all", ...o];
59
+ return /* @__PURE__ */ m("div", {
60
+ className: "space-y-6 p-6",
61
+ children: [
62
+ /* @__PURE__ */ m("div", {
63
+ className: "flex items-center justify-between",
64
+ children: [/* @__PURE__ */ m("div", { children: [/* @__PURE__ */ p("h1", {
65
+ className: "text-2xl font-semibold",
66
+ children: "Portfolios"
67
+ }), /* @__PURE__ */ m("p", {
68
+ className: "text-sm text-muted-foreground",
69
+ children: [
70
+ M,
71
+ " portfolio",
72
+ M === 1 ? "" : "s",
73
+ " across the workspace"
74
+ ]
75
+ })] }), /* @__PURE__ */ m(g, {
76
+ size: "sm",
77
+ onClick: () => d("/products/create-portfolio-page"),
78
+ children: [/* @__PURE__ */ p(u, { className: "mr-1 h-4 w-4" }), "New Portfolio"]
79
+ })]
80
+ }),
81
+ /* @__PURE__ */ m("div", {
82
+ className: "flex flex-wrap items-center gap-4",
83
+ children: [/* @__PURE__ */ p(x, {
84
+ "aria-label": "Search portfolios",
85
+ placeholder: "Search by name…",
86
+ value: E,
87
+ onChange: (e) => D(e.target.value),
88
+ className: "max-w-xs"
89
+ }), /* @__PURE__ */ p("div", {
90
+ className: "flex items-center gap-2 flex-wrap",
91
+ children: N.map((e) => /* @__PURE__ */ p("button", {
92
+ type: "button",
93
+ "aria-pressed": w === e,
94
+ onClick: () => T(e),
95
+ className: `px-2 py-1 text-xs rounded-md capitalize ${w === e ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`,
96
+ children: e === "all" ? "All" : a(e)
97
+ }, e))
98
+ })]
99
+ }),
100
+ /* @__PURE__ */ p("div", {
101
+ className: "grid grid-cols-1 md:grid-cols-2 gap-4",
102
+ children: j.map((e) => /* @__PURE__ */ m(_, {
103
+ ...i(() => d(r("/products/portfolio-detail-page", e.id)), e.name),
104
+ children: [/* @__PURE__ */ p(y, {
105
+ className: "pb-2",
106
+ children: /* @__PURE__ */ m("div", {
107
+ className: "flex items-center justify-between gap-2",
108
+ children: [/* @__PURE__ */ m(b, {
109
+ className: "text-base flex items-center gap-2",
110
+ children: [/* @__PURE__ */ p(c, { className: "h-4 w-4 text-primary" }), e.name]
111
+ }), /* @__PURE__ */ p(h, {
112
+ variant: "outline",
113
+ className: "text-xs",
114
+ children: a(e.status)
115
+ })]
116
+ })
117
+ }), /* @__PURE__ */ m(v, {
118
+ className: "space-y-3",
119
+ children: [
120
+ /* @__PURE__ */ p("p", {
121
+ className: "text-sm text-muted-foreground",
122
+ children: e.description ?? "No description"
123
+ }),
124
+ /* @__PURE__ */ m("div", {
125
+ className: "flex items-center gap-2 flex-wrap",
126
+ children: [e.alignment && /* @__PURE__ */ p(h, {
127
+ variant: "secondary",
128
+ className: "text-xs",
129
+ children: e.alignment === "CUSTOM" ? e.alignmentLabel ?? "Custom" : a(e.alignment)
130
+ }), e.fiscalPeriod && /* @__PURE__ */ p(h, {
131
+ variant: "outline",
132
+ className: "text-xs",
133
+ children: e.fiscalPeriod
134
+ })]
135
+ }),
136
+ /* @__PURE__ */ m("div", {
137
+ className: "flex items-center justify-between text-xs text-muted-foreground border-t pt-2",
138
+ children: [/* @__PURE__ */ m("span", { children: ["Owner: ", /* @__PURE__ */ p(n, { id: e.ownerId })] }), /* @__PURE__ */ p("span", { children: e.budgetTotal == null ? "No budget set" : `${e.currency ?? ""} ${e.budgetTotal.toLocaleString()}`.trim() })]
139
+ })
140
+ ]
141
+ })]
142
+ }, e.id))
143
+ }),
144
+ j.length === 0 && /* @__PURE__ */ p("p", {
145
+ className: "text-center text-muted-foreground py-8",
146
+ children: w !== "all" || E ? "No portfolios match the selected filters." : "No portfolios yet. Create one to group programs and projects for executive oversight."
147
+ })
148
+ ]
149
+ });
150
+ }
151
+ //#endregion
152
+ export { C as PortfoliosPortfoliosPagePage };
153
+
154
+ //# sourceMappingURL=PortfoliosPortfoliosPagePage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PortfoliosPortfoliosPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/PortfoliosPortfoliosPagePage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Badge,\n Button,\n Input,\n} from '@burdenoff/fe-libs/ui';\nimport { Briefcase, Plus, Loader2 } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { rowLinkProps, detailPath } from '../utils/rowNavigation';\nimport { PersonName } from '../people/People';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport { PORTFOLIO_STATUSES } from '../controls/PortfolioControls';\n\nconst LIST_PORTFOLIOS = gql`\n query ListPortfolios(\n $workspaceId: ID!\n $filter: PlanmagnetPortfolioFilter\n $pagination: PlanmagnetPaginationInput\n ) {\n planmagnetPortfolios(workspaceId: $workspaceId, filter: $filter, pagination: $pagination) {\n items {\n id\n name\n description\n status\n alignment\n alignmentLabel\n ownerId\n budgetTotal\n currency\n fiscalPeriod\n tags\n updatedAt\n }\n totalCount\n }\n }\n`;\n\ninterface PortfolioNode {\n id: string;\n name: string;\n description: string | null;\n status: string;\n alignment: string | null;\n alignmentLabel: string | null;\n ownerId: string;\n budgetTotal: number | null;\n currency: string | null;\n fiscalPeriod: string | null;\n tags: string[];\n updatedAt: string;\n}\n\nexport function PortfoliosPortfoliosPagePage() {\n const navigate = usePlanMagnetNavigate();\n const { workspaceId } = usePlanMagnet();\n const [statusFilter, setStatusFilter] = useState<string>('all');\n const [search, setSearch] = useState('');\n\n const { data, loading, error } = useQuery<{\n planmagnetPortfolios: { items: PortfolioNode[]; totalCount: number };\n }>(LIST_PORTFOLIOS, {\n variables: {\n workspaceId,\n pagination: { first: 50 },\n filter: {\n ...(statusFilter !== 'all' ? { status: statusFilter } : {}),\n ...(search.trim() ? { search: search.trim() } : {}),\n },\n },\n skip: !workspaceId,\n });\n\n // Only before the first load: a refetch must not blank the page (it unmounts every component on it and\n // loses their state).\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-destructive\">Failed to load portfolios: {error.message}</div>;\n }\n\n const portfolios = data?.planmagnetPortfolios.items ?? [];\n const totalCount = data?.planmagnetPortfolios.totalCount ?? 0;\n const statuses = ['all', ...PORTFOLIO_STATUSES];\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\">Portfolios</h1>\n <p className=\"text-sm text-muted-foreground\">\n {totalCount} portfolio{totalCount === 1 ? '' : 's'} across the workspace\n </p>\n </div>\n <Button size=\"sm\" onClick={() => navigate('/products/create-portfolio-page')}>\n <Plus className=\"mr-1 h-4 w-4\" />\n New Portfolio\n </Button>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-4\">\n <Input\n aria-label=\"Search portfolios\"\n placeholder=\"Search by name…\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"max-w-xs\"\n />\n <div className=\"flex items-center gap-2 flex-wrap\">\n {statuses.map((s) => (\n <button\n key={s}\n type=\"button\"\n aria-pressed={statusFilter === s}\n onClick={() => setStatusFilter(s)}\n className={`px-2 py-1 text-xs rounded-md capitalize ${statusFilter === s ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}\n >\n {s === 'all' ? 'All' : enumLabel(s)}\n </button>\n ))}\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n {portfolios.map((pf) => (\n <Card\n key={pf.id}\n {...rowLinkProps(\n () => navigate(detailPath('/products/portfolio-detail-page', pf.id)),\n pf.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 <Briefcase className=\"h-4 w-4 text-primary\" />\n {pf.name}\n </CardTitle>\n <Badge variant=\"outline\" className=\"text-xs\">\n {enumLabel(pf.status)}\n </Badge>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <p className=\"text-sm text-muted-foreground\">{pf.description ?? 'No description'}</p>\n <div className=\"flex items-center gap-2 flex-wrap\">\n {pf.alignment && (\n <Badge variant=\"secondary\" className=\"text-xs\">\n {pf.alignment === 'CUSTOM'\n ? (pf.alignmentLabel ?? 'Custom')\n : enumLabel(pf.alignment)}\n </Badge>\n )}\n {pf.fiscalPeriod && (\n <Badge variant=\"outline\" className=\"text-xs\">\n {pf.fiscalPeriod}\n </Badge>\n )}\n </div>\n <div className=\"flex items-center justify-between text-xs text-muted-foreground border-t pt-2\">\n <span>\n Owner: <PersonName id={pf.ownerId} />\n </span>\n <span>\n {pf.budgetTotal != null\n ? `${pf.currency ?? ''} ${pf.budgetTotal.toLocaleString()}`.trim()\n : 'No budget set'}\n </span>\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n {portfolios.length === 0 && (\n <p className=\"text-center text-muted-foreground py-8\">\n {statusFilter !== 'all' || search\n ? 'No portfolios match the selected filters.'\n : 'No portfolios yet. Create one to group programs and projects for executive oversight.'}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ProductsFeatureDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsFeatureDetailPagePage.tsx"],"names":[],"mappings":"AAyEA,wBAAgB,6BAA6B,4CA8Q5C"}
1
+ {"version":3,"file":"ProductsFeatureDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsFeatureDetailPagePage.tsx"],"names":[],"mappings":"AA0EA,wBAAgB,6BAA6B,4CAmQ5C"}