@burdenoff/microfe-planmagnet 2026.928.5 → 2026.928.7
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/generated/wspace-operations.d.ts +5 -0
- package/dist/generated/wspace-operations.d.ts.map +1 -1
- package/dist/generated/wspace-types.d.ts +15 -0
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoot.js +20 -11
- package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +216 -196
- package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
- package/dist/planmagnet/controls/Pickers.d.ts +14 -0
- package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
- package/dist/planmagnet/controls/Pickers.js +97 -22
- package/dist/planmagnet/controls/Pickers.js.map +1 -1
- package/dist/planmagnet/controls/WorkItemControls.d.ts +6 -2
- package/dist/planmagnet/controls/WorkItemControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/WorkItemControls.js +8 -8
- package/dist/planmagnet/controls/WorkItemControls.js.map +1 -1
- package/dist/planmagnet/pages/DashboardDashboardPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DashboardDashboardPagePage.js +11 -3
- package/dist/planmagnet/pages/DashboardDashboardPagePage.js.map +1 -1
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js +74 -59
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsEditProjectPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js +138 -112
- package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +16 -6
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.js +7 -2
- package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningPagePage.js +76 -56
- package/dist/planmagnet/pages/ProjectsPlanningPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +241 -188
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/SprintsStartSprintPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/SprintsStartSprintPagePage.js +168 -122
- package/dist/planmagnet/pages/SprintsStartSprintPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TeamCreateTeamPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TeamCreateTeamPagePage.js +288 -91
- package/dist/planmagnet/pages/TeamCreateTeamPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TeamTeamDetailPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/TeamTeamDetailPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/TeamTeamDetailPagePage.js +217 -0
- package/dist/planmagnet/pages/TeamTeamDetailPagePage.js.map +1 -0
- package/dist/planmagnet/pages/TeamTeamEditPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/TeamTeamEditPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/TeamTeamEditPagePage.js +211 -0
- package/dist/planmagnet/pages/TeamTeamEditPagePage.js.map +1 -0
- package/dist/planmagnet/pages/TeamTeamsListPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/TeamTeamsListPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/TeamTeamsListPagePage.js +118 -0
- package/dist/planmagnet/pages/TeamTeamsListPagePage.js.map +1 -0
- package/dist/planmagnet/sprints/SprintControls.d.ts +15 -3
- package/dist/planmagnet/sprints/SprintControls.d.ts.map +1 -1
- package/dist/planmagnet/sprints/SprintControls.js +293 -190
- package/dist/planmagnet/sprints/SprintControls.js.map +1 -1
- package/dist/planmagnet/team/TeamControls.d.ts +31 -0
- package/dist/planmagnet/team/TeamControls.d.ts.map +1 -0
- package/dist/planmagnet/team/TeamControls.js +407 -0
- package/dist/planmagnet/team/TeamControls.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkItemControls.js","names":[],"sources":["../../../src/planmagnet/controls/WorkItemControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport { useWorkspaceDirectory } from '@burdenoff/fe-libs/sharing';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Pencil, Trash2 } from 'lucide-react';\nimport { allowedTargets, useProjectWorkflow, workflowLabel } from '../hooks/useProjectWorkflow';\nimport { usePersonProfile } from '../people/People';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { throwOnError, LEAVES_PAGE_CONTEXT } from '../utils/mutationResult';\nimport { ConfirmButton, ErrorLine } from './ConfirmButton';\n\n/**\n * Task controls (BOFF-7938, BOFF-7939). Until 2026-09-27 the task detail page was read-only: no status, priority or\n * assignee control, no Edit link (the Edit Task page existed but nothing led to it) and no Delete. A task's status\n * could only change by dragging its card on a project board, and nobody could be assigned at all — so every Team\n * page, sprint \"Team Members\" list and board card read \"Unassigned\".\n *\n * Every control saves through planmagnetUpdateWorkItem, the same path the board uses, so a status change records\n * its history (activity feed, cycle time) exactly like a drag. A refusal keeps the stored value and shows the reason.\n */\n\nexport const WORK_ITEM_STATUSES = [\n 'BACKLOG',\n 'TODO',\n 'IN_PROGRESS',\n 'IN_REVIEW',\n 'TESTING',\n 'DONE',\n 'CANCELLED',\n] as const;\nexport const WORK_ITEM_PRIORITIES = ['NONE', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] as const;\n\nexport function enumLabel(value: string): string {\n const words = value.toLowerCase().replace(/_/g, ' ');\n return words.charAt(0).toUpperCase() + words.slice(1);\n}\n\nconst UPDATE_WORK_ITEM = gql`\n mutation WorkItemControlsUpdate($id: ID!, $input: PlanmagnetUpdateWorkItemInput!) {\n planmagnetUpdateWorkItem(id: $id, input: $input) {\n id\n status\n priority\n assigneeId\n }\n }\n`;\nconst DELETE_WORK_ITEM = gql`\n mutation WorkItemControlsDelete($id: ID!) {\n planmagnetDeleteWorkItem(id: $id)\n }\n`;\n\n/** Every query that shows a task's status, priority or assignee. */\nconst LIST_QUERIES = ['GetWorkItems', 'GetWorkItemCounts', 'ProjectBoard', 'GetDashboardData'];\nconst DETAIL_QUERIES = ['GetWorkItemDetail', 'WorkItemActivity', ...LIST_QUERIES];\n\nconst selectClass =\n 'rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60 max-w-[12rem]';\n\nfunction useWorkItemUpdate(workItemId: string) {\n const [update, state] = useMutation(UPDATE_WORK_ITEM, {\n refetchQueries: DETAIL_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const save = async (input: Record<string, unknown>) => {\n setError(null);\n try {\n await throwOnError(update({ variables: { id: workItemId, input } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The task could not be updated.');\n }\n };\n return { save, saving: state.loading, error };\n}\n\n/** A status or priority select that saves on change. */\nexport function WorkItemEnumSelect({\n workItemId,\n field,\n value,\n options,\n label,\n}: {\n workItemId: string;\n field: 'status' | 'priority';\n value: string;\n options: readonly string[];\n label: string;\n}) {\n const { save, saving, error } = useWorkItemUpdate(workItemId);\n // A stored value the list does not know (older data) stays selectable rather than silently showing another one.\n const all = options.includes(value) ? options : [value, ...options];\n return (\n <div className=\"flex flex-col items-end gap-1\">\n <select\n aria-label={label}\n data-testid={`task-${field}`}\n className={selectClass}\n value={value}\n disabled={saving}\n onChange={(e) => {\n if (e.target.value !== value) void save({ [field]: e.target.value });\n }}\n >\n {all.map((option) => (\n <option key={option} value={option}>\n {enumLabel(option)}\n </option>\n ))}\n </select>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/**\n * The status select (BOFF-8043): the project's names for its statuses, and only the moves its workflow allows — the\n * server refuses anything else anyway, with the reason shown below the select.\n */\nexport function WorkItemStatusSelect({\n workItemId,\n projectId,\n value,\n}: {\n workItemId: string;\n projectId: string;\n value: string;\n}) {\n const workflow = useProjectWorkflow(projectId);\n const { save, saving, error } = useWorkItemUpdate(workItemId);\n const options = allowedTargets(workflow, value, WORK_ITEM_STATUSES);\n return (\n <div className=\"flex flex-col items-end gap-1\">\n <select\n aria-label=\"Status\"\n data-testid=\"task-status\"\n className={selectClass}\n value={value}\n disabled={saving}\n onChange={(e) => {\n if (e.target.value !== value) void save({ status: e.target.value });\n }}\n >\n {options.map((option) => (\n <option key={option} value={option}>\n {workflowLabel(workflow, option)}\n </option>\n ))}\n </select>\n {workflow?.enforceTransitions && (\n <span className=\"text-[11px] text-muted-foreground\">\n Only the moves this project allows\n </span>\n )}\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/**\n * Workspace members for an assignee picker, from the fe-libs workspace directory (what the sharing picker uses; any\n * member can read it). Option values are USER ids. `planmagnetWorkspaceEditors` is not used: it needs PlanMagnet\n * admin rights, so an ordinary member would get an empty picker.\n */\nexport function useAssigneeOptions(currentId: string | null | undefined) {\n const { workspaceId, authToken, apiGatewayUrl } = usePlanMagnet();\n const directory = useWorkspaceDirectory({\n workspaceId: workspaceId ?? '',\n authToken: authToken ?? undefined,\n apiGatewayUrl: apiGatewayUrl ?? undefined,\n });\n const current = usePersonProfile(currentId);\n const options = directory.members.map((m) => ({ id: m.id, name: m.name }));\n // The current assignee may be outside the loaded page (or no longer a member): keep them selectable by name.\n if (currentId && current && !options.some((o) => o.id === currentId)) {\n options.unshift({ id: currentId, name: current.displayName });\n }\n return {\n options,\n loading: directory.loading,\n hasMore: directory.hasMore,\n fetchMore: directory.fetchMore,\n };\n}\n\n/** A plain assignee select (for forms). '' means unassigned. */\nexport function AssigneeSelect({\n id,\n value,\n onChange,\n disabled = false,\n}: {\n id?: string;\n value: string;\n onChange: (userId: string) => void;\n disabled?: boolean;\n}) {\n const { options, loading, hasMore, fetchMore } = useAssigneeOptions(value || null);\n return (\n <div className=\"flex flex-col gap-1\">\n <select\n id={id}\n aria-label={id ? undefined : 'Assignee'}\n data-testid=\"assignee-select\"\n className={selectClass}\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">Unassigned</option>\n {options.map((o) => (\n <option key={o.id} value={o.id}>\n {o.name}\n </option>\n ))}\n </select>\n {loading && options.length === 0 && (\n <span className=\"text-xs text-muted-foreground\">Loading members…</span>\n )}\n {hasMore && (\n <button\n type=\"button\"\n className=\"self-start text-xs text-primary hover:underline\"\n onClick={fetchMore}\n >\n Show more members\n </button>\n )}\n </div>\n );\n}\n\n/** The task detail's assignee control: saves on change. */\nexport function WorkItemAssigneeSelect({\n workItemId,\n assigneeId,\n}: {\n workItemId: string;\n assigneeId: string | null;\n}) {\n const { save, saving, error } = useWorkItemUpdate(workItemId);\n return (\n <div className=\"flex flex-col items-end gap-1\">\n <AssigneeSelect\n value={assigneeId ?? ''}\n disabled={saving}\n onChange={(next) => {\n if (next !== (assigneeId ?? '')) void save({ assigneeId: next || null });\n }}\n />\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/** Edit and Delete for the task detail header. */\nexport function WorkItemHeaderActions({\n workItemId,\n title,\n}: {\n workItemId: string;\n title: string;\n}) {\n const navigate = usePlanMagnetNavigate();\n const [remove, removeState] = useMutation(DELETE_WORK_ITEM, {\n refetchQueries: LIST_QUERIES,\n context: LEAVES_PAGE_CONTEXT,\n });\n const [error, setError] = useState<string | null>(null);\n\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: workItemId } }));\n navigate('/tasks/tasks-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The task could not be deleted.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"task-edit\"\n onClick={() => navigate(`/tasks/edit-task-page?taskId=${encodeURIComponent(workItemId)}`)}\n >\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n <ConfirmButton\n label=\"Delete\"\n variant=\"outline\"\n icon={<Trash2 className=\"mr-1 h-4 w-4\" />}\n title={`Delete \"${title}\"?`}\n description=\"The task is deleted and disappears from boards, sprints and releases. This cannot be undone here.\"\n confirmLabel=\"Delete task\"\n destructive\n disabled={removeState.loading}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WorkItemControls.js","names":[],"sources":["../../../src/planmagnet/controls/WorkItemControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport { useWorkspaceDirectory } from '@burdenoff/fe-libs/sharing';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Pencil, Trash2 } from 'lucide-react';\nimport { allowedTargets, useProjectWorkflow, workflowLabel } from '../hooks/useProjectWorkflow';\nimport { usePersonProfile } from '../people/People';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { throwOnError, LEAVES_PAGE_CONTEXT } from '../utils/mutationResult';\nimport { ConfirmButton, ErrorLine } from './ConfirmButton';\n\n/**\n * Task controls (BOFF-7938, BOFF-7939). Until 2026-09-27 the task detail page was read-only: no status, priority or\n * assignee control, no Edit link (the Edit Task page existed but nothing led to it) and no Delete. A task's status\n * could only change by dragging its card on a project board, and nobody could be assigned at all — so every Team\n * page, sprint \"Team Members\" list and board card read \"Unassigned\".\n *\n * Every control saves through planmagnetUpdateWorkItem, the same path the board uses, so a status change records\n * its history (activity feed, cycle time) exactly like a drag. A refusal keeps the stored value and shows the reason.\n */\n\nexport const WORK_ITEM_STATUSES = [\n 'BACKLOG',\n 'TODO',\n 'IN_PROGRESS',\n 'IN_REVIEW',\n 'TESTING',\n 'DONE',\n 'CANCELLED',\n] as const;\nexport const WORK_ITEM_PRIORITIES = ['NONE', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] as const;\n\nexport function enumLabel(value: string): string {\n const words = value.toLowerCase().replace(/_/g, ' ');\n return words.charAt(0).toUpperCase() + words.slice(1);\n}\n\nconst UPDATE_WORK_ITEM = gql`\n mutation WorkItemControlsUpdate($id: ID!, $input: PlanmagnetUpdateWorkItemInput!) {\n planmagnetUpdateWorkItem(id: $id, input: $input) {\n id\n status\n priority\n assigneeId\n }\n }\n`;\nconst DELETE_WORK_ITEM = gql`\n mutation WorkItemControlsDelete($id: ID!) {\n planmagnetDeleteWorkItem(id: $id)\n }\n`;\n\n/** Every query that shows a task's status, priority or assignee. */\nconst LIST_QUERIES = ['GetWorkItems', 'GetWorkItemCounts', 'ProjectBoard', 'GetDashboardData'];\nconst DETAIL_QUERIES = ['GetWorkItemDetail', 'WorkItemActivity', ...LIST_QUERIES];\n\nconst selectClass =\n 'rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60 max-w-[12rem]';\n\nfunction useWorkItemUpdate(workItemId: string) {\n const [update, state] = useMutation(UPDATE_WORK_ITEM, {\n refetchQueries: DETAIL_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const save = async (input: Record<string, unknown>) => {\n setError(null);\n try {\n await throwOnError(update({ variables: { id: workItemId, input } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The task could not be updated.');\n }\n };\n return { save, saving: state.loading, error };\n}\n\n/** A status or priority select that saves on change. */\nexport function WorkItemEnumSelect({\n workItemId,\n field,\n value,\n options,\n label,\n}: {\n workItemId: string;\n field: 'status' | 'priority';\n value: string;\n options: readonly string[];\n label: string;\n}) {\n const { save, saving, error } = useWorkItemUpdate(workItemId);\n // A stored value the list does not know (older data) stays selectable rather than silently showing another one.\n const all = options.includes(value) ? options : [value, ...options];\n return (\n <div className=\"flex flex-col items-end gap-1\">\n <select\n aria-label={label}\n data-testid={`task-${field}`}\n className={selectClass}\n value={value}\n disabled={saving}\n onChange={(e) => {\n if (e.target.value !== value) void save({ [field]: e.target.value });\n }}\n >\n {all.map((option) => (\n <option key={option} value={option}>\n {enumLabel(option)}\n </option>\n ))}\n </select>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/**\n * The status select (BOFF-8043): the project's names for its statuses, and only the moves its workflow allows — the\n * server refuses anything else anyway, with the reason shown below the select.\n */\nexport function WorkItemStatusSelect({\n workItemId,\n projectId,\n value,\n}: {\n workItemId: string;\n projectId: string;\n value: string;\n}) {\n const workflow = useProjectWorkflow(projectId);\n const { save, saving, error } = useWorkItemUpdate(workItemId);\n const options = allowedTargets(workflow, value, WORK_ITEM_STATUSES);\n return (\n <div className=\"flex flex-col items-end gap-1\">\n <select\n aria-label=\"Status\"\n data-testid=\"task-status\"\n className={selectClass}\n value={value}\n disabled={saving}\n onChange={(e) => {\n if (e.target.value !== value) void save({ status: e.target.value });\n }}\n >\n {options.map((option) => (\n <option key={option} value={option}>\n {workflowLabel(workflow, option)}\n </option>\n ))}\n </select>\n {workflow?.enforceTransitions && (\n <span className=\"text-[11px] text-muted-foreground\">\n Only the moves this project allows\n </span>\n )}\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/**\n * Workspace members for an assignee picker, from the fe-libs workspace directory (what the sharing picker uses; any\n * member can read it). Option values are USER ids. `planmagnetWorkspaceEditors` is not used: it needs PlanMagnet\n * admin rights, so an ordinary member would get an empty picker.\n */\nexport function useAssigneeOptions(currentId: string | null | undefined) {\n const { workspaceId, authToken, apiGatewayUrl } = usePlanMagnet();\n const directory = useWorkspaceDirectory({\n workspaceId: workspaceId ?? '',\n authToken: authToken ?? undefined,\n apiGatewayUrl: apiGatewayUrl ?? undefined,\n });\n const current = usePersonProfile(currentId);\n const options = directory.members.map((m) => ({ id: m.id, name: m.name }));\n // The current assignee may be outside the loaded page (or no longer a member): keep them selectable by name.\n if (currentId && current && !options.some((o) => o.id === currentId)) {\n options.unshift({ id: currentId, name: current.displayName });\n }\n return {\n options,\n loading: directory.loading,\n hasMore: directory.hasMore,\n fetchMore: directory.fetchMore,\n };\n}\n\n/** A plain workspace-member select (for forms). '' means `noneLabel` (\"Unassigned\" by default). */\nexport function AssigneeSelect({\n id,\n value,\n onChange,\n disabled = false,\n noneLabel = 'Unassigned',\n ariaLabel = 'Assignee',\n}: {\n id?: string;\n value: string;\n onChange: (userId: string) => void;\n disabled?: boolean;\n /** BOFF-8044: e.g. \"No lead\" when reused for a Team's leadId, which isn't an \"assignee\". */\n noneLabel?: string;\n /** The control's accessible name when it has no visible `<label>` (i.e. no `id`). */\n ariaLabel?: string;\n}) {\n const { options, loading, hasMore, fetchMore } = useAssigneeOptions(value || null);\n return (\n <div className=\"flex flex-col gap-1\">\n <select\n id={id}\n aria-label={id ? undefined : ariaLabel}\n data-testid=\"assignee-select\"\n className={selectClass}\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">{noneLabel}</option>\n {options.map((o) => (\n <option key={o.id} value={o.id}>\n {o.name}\n </option>\n ))}\n </select>\n {loading && options.length === 0 && (\n <span className=\"text-xs text-muted-foreground\">Loading members…</span>\n )}\n {hasMore && (\n <button\n type=\"button\"\n className=\"self-start text-xs text-primary hover:underline\"\n onClick={fetchMore}\n >\n Show more members\n </button>\n )}\n </div>\n );\n}\n\n/** The task detail's assignee control: saves on change. */\nexport function WorkItemAssigneeSelect({\n workItemId,\n assigneeId,\n}: {\n workItemId: string;\n assigneeId: string | null;\n}) {\n const { save, saving, error } = useWorkItemUpdate(workItemId);\n return (\n <div className=\"flex flex-col items-end gap-1\">\n <AssigneeSelect\n value={assigneeId ?? ''}\n disabled={saving}\n onChange={(next) => {\n if (next !== (assigneeId ?? '')) void save({ assigneeId: next || null });\n }}\n />\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/** Edit and Delete for the task detail header. */\nexport function WorkItemHeaderActions({\n workItemId,\n title,\n}: {\n workItemId: string;\n title: string;\n}) {\n const navigate = usePlanMagnetNavigate();\n const [remove, removeState] = useMutation(DELETE_WORK_ITEM, {\n refetchQueries: LIST_QUERIES,\n context: LEAVES_PAGE_CONTEXT,\n });\n const [error, setError] = useState<string | null>(null);\n\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: workItemId } }));\n navigate('/tasks/tasks-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The task could not be deleted.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"task-edit\"\n onClick={() => navigate(`/tasks/edit-task-page?taskId=${encodeURIComponent(workItemId)}`)}\n >\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n <ConfirmButton\n label=\"Delete\"\n variant=\"outline\"\n icon={<Trash2 className=\"mr-1 h-4 w-4\" />}\n title={`Delete \"${title}\"?`}\n description=\"The task is deleted and disappears from boards, sprints and releases. This cannot be undone here.\"\n confirmLabel=\"Delete task\"\n destructive\n disabled={removeState.loading}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardDashboardPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/DashboardDashboardPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"DashboardDashboardPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/DashboardDashboardPagePage.tsx"],"names":[],"mappings":"AAkIA,wBAAgB,0BAA0B,4CAiYzC"}
|
|
@@ -102,6 +102,10 @@ var A = y`
|
|
|
102
102
|
id
|
|
103
103
|
name
|
|
104
104
|
}
|
|
105
|
+
team {
|
|
106
|
+
id
|
|
107
|
+
name
|
|
108
|
+
}
|
|
105
109
|
workItems {
|
|
106
110
|
id
|
|
107
111
|
status
|
|
@@ -197,14 +201,18 @@ function j() {
|
|
|
197
201
|
children: [/* @__PURE__ */ C("div", { children: [/* @__PURE__ */ S("p", {
|
|
198
202
|
className: "font-medium",
|
|
199
203
|
children: e.name
|
|
200
|
-
}), /* @__PURE__ */
|
|
204
|
+
}), /* @__PURE__ */ S("p", {
|
|
201
205
|
className: "text-xs text-muted-foreground",
|
|
202
|
-
children: [
|
|
206
|
+
children: e.team ? /* @__PURE__ */ C(x, { children: [
|
|
207
|
+
j("planmagnet.dashboard.shared.team", "Team"),
|
|
208
|
+
": ",
|
|
209
|
+
e.team.name
|
|
210
|
+
] }) : /* @__PURE__ */ C(x, { children: [
|
|
203
211
|
j("planmagnet.dashboard.shared.project", "Project"),
|
|
204
212
|
":",
|
|
205
213
|
" ",
|
|
206
214
|
e.project?.name ?? j("planmagnet.dashboard.shared.projectUnavailable", "not available")
|
|
207
|
-
]
|
|
215
|
+
] })
|
|
208
216
|
})] }), /* @__PURE__ */ C(T, {
|
|
209
217
|
variant: "secondary",
|
|
210
218
|
children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardDashboardPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/DashboardDashboardPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Badge } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n LayoutDashboard,\n FolderKanban,\n CheckSquare,\n Zap,\n Users,\n Calendar,\n TrendingUp,\n AlertTriangle,\n ArrowUpRight,\n} from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { countLabel, useProjectCounts, useSprintCounts, workProgress } from '../hooks/useCounts';\nimport { ROW_LINK_CLASS, detailPath, rowLinkProps } from '../utils/rowNavigation';\nimport { PersonName } from '../people/People';\n\n// What the dashboard shows is what it queries (BOFF-7969/7970/7975): active and in-review sprints, the nearest\n// upcoming milestones, active and on-hold projects. It used to load the 50 newest of everything and filter in the\n// browser, so a long-running sprint or an older project dropped off, and \"upcoming\" meant \"newest-created\".\nconst GET_DASHBOARD_DATA = gql`\n query GetDashboardData($workspaceId: ID!, $today: DateTime) {\n activeProjects: planmagnetProjects(\n workspaceId: $workspaceId\n filter: { status: ACTIVE }\n pagination: { first: 20 }\n ) {\n items {\n id\n name\n status\n ownerId\n }\n totalCount\n }\n onHoldProjects: planmagnetProjects(\n workspaceId: $workspaceId\n filter: { status: ON_HOLD }\n pagination: { first: 20 }\n ) {\n items {\n id\n name\n status\n }\n totalCount\n }\n activeSprints: planmagnetSprints(\n workspaceId: $workspaceId\n filter: { status: ACTIVE }\n pagination: { first: 20 }\n ) {\n items {\n ...DashboardSprint\n }\n }\n reviewSprints: planmagnetSprints(\n workspaceId: $workspaceId\n filter: { status: REVIEW }\n pagination: { first: 20 }\n ) {\n items {\n ...DashboardSprint\n }\n }\n upcomingMilestones: planmagnetMilestones(\n workspaceId: $workspaceId\n filter: { open: true, dueFrom: $today }\n orderBy: DUE_SOONEST\n pagination: { first: 4 }\n ) {\n items {\n id\n name\n status\n dueDate\n project {\n id\n name\n }\n }\n totalCount\n }\n allMilestones: planmagnetMilestones(workspaceId: $workspaceId, pagination: { first: 1 }) {\n totalCount\n }\n planmagnetWorkItems(workspaceId: $workspaceId, pagination: { first: 5 }) {\n items {\n id\n key\n title\n status\n storyPoints\n assigneeId\n project {\n id\n name\n }\n }\n totalCount\n }\n }\n\n fragment DashboardSprint on Sprint {\n id\n name\n status\n startDate\n endDate\n project {\n id\n name\n }\n workItems {\n id\n status\n storyPoints\n }\n }\n`;\n\nexport function DashboardDashboardPagePage() {\n const { workspaceId } = usePlanMagnet();\n const { t } = useI18n();\n\n const {\n data,\n loading,\n error,\n } = // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n useQuery<Record<string, any>>(GET_DASHBOARD_DATA, {\n // \"Upcoming\" starts today (UTC day); overdue milestones are Analytics' \"at risk\", not upcoming.\n variables: {\n workspaceId: workspaceId ?? 'default',\n today: new Date().toISOString().slice(0, 10),\n },\n });\n const projectCounts = useProjectCounts();\n const sprintCounts = useSprintCounts();\n const navigate = usePlanMagnetNavigate();\n\n // Only before the first load: a refetch must not blank the page (it\n\n // unmounts every component on it and loses their state).\n\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 (\n <div className=\"p-6 text-red-500\">\n {t('planmagnet.dashboard.errorLoading', 'Error loading dashboard')}: {error.message}\n </div>\n );\n\n const activeProjects = data?.activeProjects?.items ?? [];\n const onHoldProjects = data?.onHoldProjects?.items ?? [];\n const activeSprints = [\n ...(data?.activeSprints?.items ?? []),\n ...(data?.reviewSprints?.items ?? []),\n ];\n const upcomingMilestones = data?.upcomingMilestones?.items ?? [];\n const recentWorkItems = data?.planmagnetWorkItems?.items ?? [];\n\n const metrics = [\n {\n label: t('planmagnet.dashboard.metrics.totalProjects.label', 'Total Projects'),\n value: countLabel(projectCounts?.all),\n trend: `${countLabel(projectCounts?.active)} ${t('planmagnet.dashboard.metrics.totalProjects.trend', 'active')}`,\n },\n {\n label: t('planmagnet.dashboard.metrics.activeSprints.label', 'Active Sprints'),\n value: countLabel(sprintCounts?.running),\n trend: `${countLabel(sprintCounts?.all)} ${t('planmagnet.dashboard.metrics.activeSprints.trend', 'total')}`,\n },\n {\n label: t('planmagnet.dashboard.metrics.workItems.label', 'Work Items'),\n value: countLabel(data?.planmagnetWorkItems?.totalCount),\n trend: t('planmagnet.dashboard.metrics.workItems.trend', 'across all projects'),\n },\n {\n label: t('planmagnet.dashboard.metrics.milestones.label', 'Milestones'),\n value: countLabel(data?.allMilestones?.totalCount),\n trend: `${countLabel(data?.upcomingMilestones?.totalCount)} ${t('planmagnet.dashboard.metrics.milestones.trend', 'upcoming')}`,\n },\n ];\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 <LayoutDashboard className=\"h-6 w-6\" />\n {t('planmagnet.dashboard.title', 'Dashboard')}\n </h1>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {t('planmagnet.dashboard.subtitle', 'Welcome back! Here is your project overview.')}\n </p>\n </div>\n </div>\n\n {/* KPI Metrics */}\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\n {metrics.map((m) => (\n <Card key={m.label}>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">{m.label}</CardTitle>\n <TrendingUp className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold\">{m.value}</div>\n <p className=\"text-xs text-muted-foreground\">{m.trend}</p>\n </CardContent>\n </Card>\n ))}\n </div>\n\n <div className=\"grid gap-6 lg:grid-cols-12\">\n {/* Active Sprints */}\n <Card className=\"lg:col-span-8\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <Zap className=\"h-5 w-5 text-blue-500\" />\n {t('planmagnet.dashboard.activeSprints.title', 'Active Sprints')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {activeSprints.map(\n (sprint: {\n id: string;\n name: string;\n projectId: string;\n capacity: number | null;\n velocity: number | null;\n startDate: string | null;\n endDate: string | null;\n workItems: { id: string; status: string; storyPoints: number | null }[];\n }) => {\n const totalPoints = sprint.workItems.reduce(\n (sum: number, wi: { storyPoints: number | null }) => sum + (wi.storyPoints ?? 0),\n 0\n );\n const completedPoints = sprint.workItems\n .filter((wi: { status: string }) => wi.status === 'DONE')\n .reduce(\n (sum: number, wi: { storyPoints: number | null }) =>\n sum + (wi.storyPoints ?? 0),\n 0\n );\n const totalTasks = sprint.workItems.length;\n const completedTasks = sprint.workItems.filter(\n (wi: { status: string }) => wi.status === 'DONE'\n ).length;\n // Done over everything not cancelled; by points, or by tasks when nothing is estimated (BOFF-7989).\n const pct = workProgress(sprint.workItems) ?? 0;\n return (\n <div\n key={sprint.id}\n {...rowLinkProps(\n () => navigate(detailPath('/projects/planning-detail-page', sprint.id)),\n sprint.name\n )}\n className={`rounded-lg border p-4 ${ROW_LINK_CLASS}`}\n >\n <div className=\"flex items-center justify-between mb-2\">\n <div>\n <p className=\"font-medium\">{sprint.name}</p>\n <p className=\"text-xs text-muted-foreground\">\n {t('planmagnet.dashboard.shared.project', 'Project')}:{' '}\n {sprint.project?.name ??\n t('planmagnet.dashboard.shared.projectUnavailable', 'not available')}\n </p>\n </div>\n <Badge variant=\"secondary\">\n {completedPoints}/{totalPoints}{' '}\n {t('planmagnet.dashboard.shared.pointsShort', 'pts')}\n </Badge>\n </div>\n <div className=\"w-full h-2 bg-muted rounded-full overflow-hidden\">\n <div\n className=\"h-full bg-primary rounded-full transition-all\"\n style={{ width: `${pct}%` }}\n />\n </div>\n <div className=\"flex items-center justify-between mt-2 text-xs text-muted-foreground\">\n <span>\n {completedTasks}/{totalTasks}{' '}\n {t('planmagnet.dashboard.shared.tasks', 'tasks')}\n </span>\n <span>\n {sprint.startDate ? new Date(sprint.startDate).toLocaleDateString() : '—'} -{' '}\n {sprint.endDate ? new Date(sprint.endDate).toLocaleDateString() : '—'}\n </span>\n </div>\n </div>\n );\n }\n )}\n {activeSprints.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n {t('planmagnet.dashboard.activeSprints.empty', 'No active sprints.')}\n </p>\n )}\n </CardContent>\n </Card>\n\n {/* Sidebar */}\n <div className=\"lg:col-span-4 space-y-6\">\n {/* On-Hold Projects */}\n {onHoldProjects.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <AlertTriangle className=\"h-5 w-5 text-orange-500\" />\n {t('planmagnet.dashboard.onHoldProjects.title', 'On-Hold Projects')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {onHoldProjects.map((p: { id: string; name: string; status: string }) => (\n <div key={p.id} className=\"flex items-center justify-between rounded border p-2\">\n <div>\n <p className=\"text-sm font-medium\">{p.name}</p>\n </div>\n <Badge variant=\"destructive\">{p.status.toLowerCase().replace('_', ' ')}</Badge>\n </div>\n ))}\n </CardContent>\n </Card>\n )}\n\n {/* Upcoming Milestones */}\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <Calendar className=\"h-5 w-5 text-green-500\" />\n {t('planmagnet.dashboard.upcomingMilestones.title', 'Upcoming Milestones')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {upcomingMilestones.map(\n (ms: {\n id: string;\n name: string;\n projectId: string;\n dueDate: string | null;\n status: string;\n }) => (\n <div\n key={ms.id}\n {...rowLinkProps(\n () => navigate(detailPath('/projects/milestone-detail-page', ms.id)),\n ms.name\n )}\n className={`flex items-start gap-3 rounded border p-2 ${ROW_LINK_CLASS}`}\n >\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium truncate\">{ms.name}</p>\n <p className=\"text-xs text-muted-foreground\">\n {t('planmagnet.dashboard.shared.project', 'Project')}:{' '}\n {ms.project?.name ??\n t('planmagnet.dashboard.shared.projectUnavailable', 'not available')}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {t('planmagnet.dashboard.shared.due', 'Due')}{' '}\n {ms.dueDate ? new Date(ms.dueDate).toLocaleDateString() : '—'}\n </p>\n </div>\n <Badge variant=\"secondary\" className=\"text-xs flex-shrink-0 capitalize\">\n {ms.status.toLowerCase().replace('_', ' ')}\n </Badge>\n </div>\n )\n )}\n {upcomingMilestones.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-2\">\n {t('planmagnet.dashboard.upcomingMilestones.empty', 'No upcoming milestones.')}\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n\n {/* Recent Work Items & Project Status */}\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <CheckSquare className=\"h-5 w-5 text-purple-500\" />\n {t('planmagnet.dashboard.recentWorkItems.title', 'Recent Work Items')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {recentWorkItems.map(\n (item: {\n id: string;\n key: string;\n description: string | null;\n status: string;\n storyPoints: number | null;\n assigneeId: string | null;\n projectId: string;\n }) => (\n <div\n key={item.id}\n {...rowLinkProps(\n () => navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(item.id)}`),\n `${item.key} ${item.title}`\n )}\n className={`flex items-center justify-between rounded border p-2 ${ROW_LINK_CLASS}`}\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium truncate\">\n {item.key} <span className=\"font-normal\">{item.title}</span>\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {item.project?.name ??\n t('planmagnet.dashboard.shared.projectUnavailable', 'not available')}\n {item.assigneeId ? (\n <>\n {' \\u00b7 '}\n <PersonName id={item.assigneeId} />\n </>\n ) : null}\n </p>\n </div>\n <div className=\"flex items-center gap-2 flex-shrink-0\">\n <Badge variant=\"outline\" className=\"text-xs\">\n {item.storyPoints ?? 0} {t('planmagnet.dashboard.shared.pointsShort', 'pts')}\n </Badge>\n <Badge variant=\"secondary\" className=\"text-xs capitalize\">\n {item.status.toLowerCase().replace('_', ' ')}\n </Badge>\n </div>\n </div>\n )\n )}\n {recentWorkItems.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n {t('planmagnet.dashboard.recentWorkItems.empty', 'No work items yet.')}\n </p>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <FolderKanban className=\"h-5 w-5 text-cyan-500\" />\n {t('planmagnet.dashboard.projectStatus.title', 'Project Status')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {activeProjects.map(\n (p: { id: string; name: string; status: string; ownerId: string }) => (\n // The row opens the project: it drew an \"open\" arrow but did nothing (BOFF-7981).\n <div\n key={p.id}\n {...rowLinkProps(\n () => navigate(detailPath('/projects/project-detail-page', p.id)),\n p.name\n )}\n className={`flex items-center justify-between rounded border p-2 ${ROW_LINK_CLASS}`}\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium truncate\">{p.name}</p>\n <p className=\"text-xs text-muted-foreground capitalize\">\n {p.status.toLowerCase().replace('_', ' ')}\n </p>\n </div>\n <div className=\"flex items-center gap-2 ml-3 flex-shrink-0\">\n <Users className=\"h-3 w-3 text-muted-foreground\" />\n <span className=\"text-xs text-muted-foreground\">\n <PersonName id={p.ownerId} />\n </span>\n <ArrowUpRight className=\"h-3 w-3 text-muted-foreground\" />\n </div>\n </div>\n )\n )}\n {activeProjects.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n {t('planmagnet.dashboard.projectStatus.empty', 'No active projects.')}\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DashboardDashboardPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/DashboardDashboardPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Badge } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n LayoutDashboard,\n FolderKanban,\n CheckSquare,\n Zap,\n Users,\n Calendar,\n TrendingUp,\n AlertTriangle,\n ArrowUpRight,\n} from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { countLabel, useProjectCounts, useSprintCounts, workProgress } from '../hooks/useCounts';\nimport { ROW_LINK_CLASS, detailPath, rowLinkProps } from '../utils/rowNavigation';\nimport { PersonName } from '../people/People';\n\n// What the dashboard shows is what it queries (BOFF-7969/7970/7975): active and in-review sprints, the nearest\n// upcoming milestones, active and on-hold projects. It used to load the 50 newest of everything and filter in the\n// browser, so a long-running sprint or an older project dropped off, and \"upcoming\" meant \"newest-created\".\nconst GET_DASHBOARD_DATA = gql`\n query GetDashboardData($workspaceId: ID!, $today: DateTime) {\n activeProjects: planmagnetProjects(\n workspaceId: $workspaceId\n filter: { status: ACTIVE }\n pagination: { first: 20 }\n ) {\n items {\n id\n name\n status\n ownerId\n }\n totalCount\n }\n onHoldProjects: planmagnetProjects(\n workspaceId: $workspaceId\n filter: { status: ON_HOLD }\n pagination: { first: 20 }\n ) {\n items {\n id\n name\n status\n }\n totalCount\n }\n activeSprints: planmagnetSprints(\n workspaceId: $workspaceId\n filter: { status: ACTIVE }\n pagination: { first: 20 }\n ) {\n items {\n ...DashboardSprint\n }\n }\n reviewSprints: planmagnetSprints(\n workspaceId: $workspaceId\n filter: { status: REVIEW }\n pagination: { first: 20 }\n ) {\n items {\n ...DashboardSprint\n }\n }\n upcomingMilestones: planmagnetMilestones(\n workspaceId: $workspaceId\n filter: { open: true, dueFrom: $today }\n orderBy: DUE_SOONEST\n pagination: { first: 4 }\n ) {\n items {\n id\n name\n status\n dueDate\n project {\n id\n name\n }\n }\n totalCount\n }\n allMilestones: planmagnetMilestones(workspaceId: $workspaceId, pagination: { first: 1 }) {\n totalCount\n }\n planmagnetWorkItems(workspaceId: $workspaceId, pagination: { first: 5 }) {\n items {\n id\n key\n title\n status\n storyPoints\n assigneeId\n project {\n id\n name\n }\n }\n totalCount\n }\n }\n\n fragment DashboardSprint on Sprint {\n id\n name\n status\n startDate\n endDate\n project {\n id\n name\n }\n team {\n id\n name\n }\n workItems {\n id\n status\n storyPoints\n }\n }\n`;\n\nexport function DashboardDashboardPagePage() {\n const { workspaceId } = usePlanMagnet();\n const { t } = useI18n();\n\n const {\n data,\n loading,\n error,\n } = // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n useQuery<Record<string, any>>(GET_DASHBOARD_DATA, {\n // \"Upcoming\" starts today (UTC day); overdue milestones are Analytics' \"at risk\", not upcoming.\n variables: {\n workspaceId: workspaceId ?? 'default',\n today: new Date().toISOString().slice(0, 10),\n },\n });\n const projectCounts = useProjectCounts();\n const sprintCounts = useSprintCounts();\n const navigate = usePlanMagnetNavigate();\n\n // Only before the first load: a refetch must not blank the page (it\n\n // unmounts every component on it and loses their state).\n\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 (\n <div className=\"p-6 text-red-500\">\n {t('planmagnet.dashboard.errorLoading', 'Error loading dashboard')}: {error.message}\n </div>\n );\n\n const activeProjects = data?.activeProjects?.items ?? [];\n const onHoldProjects = data?.onHoldProjects?.items ?? [];\n const activeSprints = [\n ...(data?.activeSprints?.items ?? []),\n ...(data?.reviewSprints?.items ?? []),\n ];\n const upcomingMilestones = data?.upcomingMilestones?.items ?? [];\n const recentWorkItems = data?.planmagnetWorkItems?.items ?? [];\n\n const metrics = [\n {\n label: t('planmagnet.dashboard.metrics.totalProjects.label', 'Total Projects'),\n value: countLabel(projectCounts?.all),\n trend: `${countLabel(projectCounts?.active)} ${t('planmagnet.dashboard.metrics.totalProjects.trend', 'active')}`,\n },\n {\n label: t('planmagnet.dashboard.metrics.activeSprints.label', 'Active Sprints'),\n value: countLabel(sprintCounts?.running),\n trend: `${countLabel(sprintCounts?.all)} ${t('planmagnet.dashboard.metrics.activeSprints.trend', 'total')}`,\n },\n {\n label: t('planmagnet.dashboard.metrics.workItems.label', 'Work Items'),\n value: countLabel(data?.planmagnetWorkItems?.totalCount),\n trend: t('planmagnet.dashboard.metrics.workItems.trend', 'across all projects'),\n },\n {\n label: t('planmagnet.dashboard.metrics.milestones.label', 'Milestones'),\n value: countLabel(data?.allMilestones?.totalCount),\n trend: `${countLabel(data?.upcomingMilestones?.totalCount)} ${t('planmagnet.dashboard.metrics.milestones.trend', 'upcoming')}`,\n },\n ];\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 <LayoutDashboard className=\"h-6 w-6\" />\n {t('planmagnet.dashboard.title', 'Dashboard')}\n </h1>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {t('planmagnet.dashboard.subtitle', 'Welcome back! Here is your project overview.')}\n </p>\n </div>\n </div>\n\n {/* KPI Metrics */}\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\n {metrics.map((m) => (\n <Card key={m.label}>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">{m.label}</CardTitle>\n <TrendingUp className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold\">{m.value}</div>\n <p className=\"text-xs text-muted-foreground\">{m.trend}</p>\n </CardContent>\n </Card>\n ))}\n </div>\n\n <div className=\"grid gap-6 lg:grid-cols-12\">\n {/* Active Sprints */}\n <Card className=\"lg:col-span-8\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <Zap className=\"h-5 w-5 text-blue-500\" />\n {t('planmagnet.dashboard.activeSprints.title', 'Active Sprints')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {activeSprints.map(\n (sprint: {\n id: string;\n name: string;\n project: { id: string; name: string } | null;\n team: { id: string; name: string } | null;\n capacity: number | null;\n velocity: number | null;\n startDate: string | null;\n endDate: string | null;\n workItems: { id: string; status: string; storyPoints: number | null }[];\n }) => {\n const totalPoints = sprint.workItems.reduce(\n (sum: number, wi: { storyPoints: number | null }) => sum + (wi.storyPoints ?? 0),\n 0\n );\n const completedPoints = sprint.workItems\n .filter((wi: { status: string }) => wi.status === 'DONE')\n .reduce(\n (sum: number, wi: { storyPoints: number | null }) =>\n sum + (wi.storyPoints ?? 0),\n 0\n );\n const totalTasks = sprint.workItems.length;\n const completedTasks = sprint.workItems.filter(\n (wi: { status: string }) => wi.status === 'DONE'\n ).length;\n // Done over everything not cancelled; by points, or by tasks when nothing is estimated (BOFF-7989).\n const pct = workProgress(sprint.workItems) ?? 0;\n return (\n <div\n key={sprint.id}\n {...rowLinkProps(\n () => navigate(detailPath('/projects/planning-detail-page', sprint.id)),\n sprint.name\n )}\n className={`rounded-lg border p-4 ${ROW_LINK_CLASS}`}\n >\n <div className=\"flex items-center justify-between mb-2\">\n <div>\n <p className=\"font-medium\">{sprint.name}</p>\n <p className=\"text-xs text-muted-foreground\">\n {sprint.team ? (\n <>\n {t('planmagnet.dashboard.shared.team', 'Team')}: {sprint.team.name}\n </>\n ) : (\n <>\n {t('planmagnet.dashboard.shared.project', 'Project')}:{' '}\n {sprint.project?.name ??\n t(\n 'planmagnet.dashboard.shared.projectUnavailable',\n 'not available'\n )}\n </>\n )}\n </p>\n </div>\n <Badge variant=\"secondary\">\n {completedPoints}/{totalPoints}{' '}\n {t('planmagnet.dashboard.shared.pointsShort', 'pts')}\n </Badge>\n </div>\n <div className=\"w-full h-2 bg-muted rounded-full overflow-hidden\">\n <div\n className=\"h-full bg-primary rounded-full transition-all\"\n style={{ width: `${pct}%` }}\n />\n </div>\n <div className=\"flex items-center justify-between mt-2 text-xs text-muted-foreground\">\n <span>\n {completedTasks}/{totalTasks}{' '}\n {t('planmagnet.dashboard.shared.tasks', 'tasks')}\n </span>\n <span>\n {sprint.startDate ? new Date(sprint.startDate).toLocaleDateString() : '—'} -{' '}\n {sprint.endDate ? new Date(sprint.endDate).toLocaleDateString() : '—'}\n </span>\n </div>\n </div>\n );\n }\n )}\n {activeSprints.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n {t('planmagnet.dashboard.activeSprints.empty', 'No active sprints.')}\n </p>\n )}\n </CardContent>\n </Card>\n\n {/* Sidebar */}\n <div className=\"lg:col-span-4 space-y-6\">\n {/* On-Hold Projects */}\n {onHoldProjects.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <AlertTriangle className=\"h-5 w-5 text-orange-500\" />\n {t('planmagnet.dashboard.onHoldProjects.title', 'On-Hold Projects')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {onHoldProjects.map((p: { id: string; name: string; status: string }) => (\n <div key={p.id} className=\"flex items-center justify-between rounded border p-2\">\n <div>\n <p className=\"text-sm font-medium\">{p.name}</p>\n </div>\n <Badge variant=\"destructive\">{p.status.toLowerCase().replace('_', ' ')}</Badge>\n </div>\n ))}\n </CardContent>\n </Card>\n )}\n\n {/* Upcoming Milestones */}\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <Calendar className=\"h-5 w-5 text-green-500\" />\n {t('planmagnet.dashboard.upcomingMilestones.title', 'Upcoming Milestones')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {upcomingMilestones.map(\n (ms: {\n id: string;\n name: string;\n projectId: string;\n dueDate: string | null;\n status: string;\n }) => (\n <div\n key={ms.id}\n {...rowLinkProps(\n () => navigate(detailPath('/projects/milestone-detail-page', ms.id)),\n ms.name\n )}\n className={`flex items-start gap-3 rounded border p-2 ${ROW_LINK_CLASS}`}\n >\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium truncate\">{ms.name}</p>\n <p className=\"text-xs text-muted-foreground\">\n {t('planmagnet.dashboard.shared.project', 'Project')}:{' '}\n {ms.project?.name ??\n t('planmagnet.dashboard.shared.projectUnavailable', 'not available')}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {t('planmagnet.dashboard.shared.due', 'Due')}{' '}\n {ms.dueDate ? new Date(ms.dueDate).toLocaleDateString() : '—'}\n </p>\n </div>\n <Badge variant=\"secondary\" className=\"text-xs flex-shrink-0 capitalize\">\n {ms.status.toLowerCase().replace('_', ' ')}\n </Badge>\n </div>\n )\n )}\n {upcomingMilestones.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-2\">\n {t('planmagnet.dashboard.upcomingMilestones.empty', 'No upcoming milestones.')}\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n\n {/* Recent Work Items & Project Status */}\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <CheckSquare className=\"h-5 w-5 text-purple-500\" />\n {t('planmagnet.dashboard.recentWorkItems.title', 'Recent Work Items')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {recentWorkItems.map(\n (item: {\n id: string;\n key: string;\n description: string | null;\n status: string;\n storyPoints: number | null;\n assigneeId: string | null;\n projectId: string;\n }) => (\n <div\n key={item.id}\n {...rowLinkProps(\n () => navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(item.id)}`),\n `${item.key} ${item.title}`\n )}\n className={`flex items-center justify-between rounded border p-2 ${ROW_LINK_CLASS}`}\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium truncate\">\n {item.key} <span className=\"font-normal\">{item.title}</span>\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {item.project?.name ??\n t('planmagnet.dashboard.shared.projectUnavailable', 'not available')}\n {item.assigneeId ? (\n <>\n {' \\u00b7 '}\n <PersonName id={item.assigneeId} />\n </>\n ) : null}\n </p>\n </div>\n <div className=\"flex items-center gap-2 flex-shrink-0\">\n <Badge variant=\"outline\" className=\"text-xs\">\n {item.storyPoints ?? 0} {t('planmagnet.dashboard.shared.pointsShort', 'pts')}\n </Badge>\n <Badge variant=\"secondary\" className=\"text-xs capitalize\">\n {item.status.toLowerCase().replace('_', ' ')}\n </Badge>\n </div>\n </div>\n )\n )}\n {recentWorkItems.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n {t('planmagnet.dashboard.recentWorkItems.empty', 'No work items yet.')}\n </p>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <FolderKanban className=\"h-5 w-5 text-cyan-500\" />\n {t('planmagnet.dashboard.projectStatus.title', 'Project Status')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {activeProjects.map(\n (p: { id: string; name: string; status: string; ownerId: string }) => (\n // The row opens the project: it drew an \"open\" arrow but did nothing (BOFF-7981).\n <div\n key={p.id}\n {...rowLinkProps(\n () => navigate(detailPath('/projects/project-detail-page', p.id)),\n p.name\n )}\n className={`flex items-center justify-between rounded border p-2 ${ROW_LINK_CLASS}`}\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium truncate\">{p.name}</p>\n <p className=\"text-xs text-muted-foreground capitalize\">\n {p.status.toLowerCase().replace('_', ' ')}\n </p>\n </div>\n <div className=\"flex items-center gap-2 ml-3 flex-shrink-0\">\n <Users className=\"h-3 w-3 text-muted-foreground\" />\n <span className=\"text-xs text-muted-foreground\">\n <PersonName id={p.ownerId} />\n </span>\n <ArrowUpRight className=\"h-3 w-3 text-muted-foreground\" />\n </div>\n </div>\n )\n )}\n {activeProjects.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n {t('planmagnet.dashboard.projectStatus.empty', 'No active projects.')}\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PortfoliosPortfolioDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/PortfoliosPortfolioDetailPagePage.tsx"],"names":[],"mappings":"AA6EA,wBAAgB,iCAAiC,
|
|
1
|
+
{"version":3,"file":"PortfoliosPortfolioDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/PortfoliosPortfolioDetailPagePage.tsx"],"names":[],"mappings":"AA6EA,wBAAgB,iCAAiC,4CAmRhD"}
|
|
@@ -8,9 +8,9 @@ import { useParams as u } from "react-router";
|
|
|
8
8
|
import { gql as d } from "@apollo/client";
|
|
9
9
|
import { useQuery as f } from "@apollo/client/react";
|
|
10
10
|
import { jsx as p, jsxs as m } from "react/jsx-runtime";
|
|
11
|
-
import { Badge as h,
|
|
11
|
+
import { Badge as h, Button as g, Card as _, CardContent as v, CardHeader as y, CardTitle as b } from "@burdenoff/fe-libs/ui";
|
|
12
12
|
//#region src/planmagnet/pages/PortfoliosPortfolioDetailPagePage.tsx
|
|
13
|
-
var
|
|
13
|
+
var x = d`
|
|
14
14
|
query GetPortfolio($id: ID!) {
|
|
15
15
|
planmagnetPortfolio(id: $id) {
|
|
16
16
|
id
|
|
@@ -39,7 +39,7 @@ var b = d`
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
`,
|
|
42
|
+
`, S = d`
|
|
43
43
|
query GetPortfolioOverview($portfolioId: ID!) {
|
|
44
44
|
planmagnetPortfolioOverview(portfolioId: $portfolioId) {
|
|
45
45
|
portfolioId
|
|
@@ -55,28 +55,28 @@ var b = d`
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
`;
|
|
58
|
-
function
|
|
59
|
-
let d = e(),
|
|
60
|
-
variables: { id:
|
|
61
|
-
skip: !
|
|
62
|
-
}), { data:
|
|
63
|
-
variables: { portfolioId:
|
|
64
|
-
skip: !
|
|
58
|
+
function C() {
|
|
59
|
+
let d = e(), C = u().id ?? "", { data: w, loading: T, error: E } = f(x, {
|
|
60
|
+
variables: { id: C },
|
|
61
|
+
skip: !C
|
|
62
|
+
}), { data: D } = f(S, {
|
|
63
|
+
variables: { portfolioId: C },
|
|
64
|
+
skip: !C
|
|
65
65
|
});
|
|
66
|
-
if (
|
|
66
|
+
if (T && !w) return /* @__PURE__ */ p("div", {
|
|
67
67
|
className: "flex items-center justify-center p-12",
|
|
68
68
|
children: /* @__PURE__ */ p(c, { className: "h-8 w-8 animate-spin text-muted-foreground" })
|
|
69
69
|
});
|
|
70
|
-
if (
|
|
70
|
+
if (E) return /* @__PURE__ */ m("div", {
|
|
71
71
|
className: "p-6 text-destructive",
|
|
72
|
-
children: ["Failed to load portfolio: ",
|
|
72
|
+
children: ["Failed to load portfolio: ", E.message]
|
|
73
73
|
});
|
|
74
|
-
let
|
|
75
|
-
if (!
|
|
74
|
+
let O = w?.planmagnetPortfolio;
|
|
75
|
+
if (!O) return /* @__PURE__ */ p("div", {
|
|
76
76
|
className: "p-6 text-muted-foreground",
|
|
77
77
|
children: "Portfolio not found."
|
|
78
78
|
});
|
|
79
|
-
let
|
|
79
|
+
let k = D?.planmagnetPortfolioOverview ?? null;
|
|
80
80
|
return /* @__PURE__ */ m("div", {
|
|
81
81
|
className: "space-y-6 p-6",
|
|
82
82
|
children: [
|
|
@@ -88,33 +88,33 @@ function S() {
|
|
|
88
88
|
/* @__PURE__ */ p(o, { className: "h-5 w-5 text-primary" }),
|
|
89
89
|
/* @__PURE__ */ p("h1", {
|
|
90
90
|
className: "text-2xl font-semibold",
|
|
91
|
-
children:
|
|
91
|
+
children: O.name
|
|
92
92
|
}),
|
|
93
93
|
/* @__PURE__ */ p(h, {
|
|
94
94
|
variant: "outline",
|
|
95
95
|
className: "text-xs",
|
|
96
|
-
children: n(
|
|
96
|
+
children: n(O.status)
|
|
97
97
|
})
|
|
98
98
|
]
|
|
99
|
-
}),
|
|
99
|
+
}), O.alignment && /* @__PURE__ */ p("p", {
|
|
100
100
|
className: "text-sm text-muted-foreground mt-1",
|
|
101
|
-
children:
|
|
101
|
+
children: O.alignment === "CUSTOM" ? O.alignmentLabel ?? "Custom alignment" : n(O.alignment)
|
|
102
102
|
})] }), /* @__PURE__ */ p(i, {
|
|
103
|
-
portfolioId:
|
|
104
|
-
name:
|
|
105
|
-
status:
|
|
103
|
+
portfolioId: O.id,
|
|
104
|
+
name: O.name,
|
|
105
|
+
status: O.status
|
|
106
106
|
})]
|
|
107
107
|
}),
|
|
108
108
|
/* @__PURE__ */ m("div", {
|
|
109
109
|
className: "grid grid-cols-2 md:grid-cols-4 gap-4",
|
|
110
110
|
children: [
|
|
111
|
-
/* @__PURE__ */ p(
|
|
111
|
+
/* @__PURE__ */ p(_, { children: /* @__PURE__ */ m(v, {
|
|
112
112
|
className: "pt-4 text-center",
|
|
113
113
|
children: [
|
|
114
114
|
/* @__PURE__ */ p(o, { className: "h-5 w-5 mx-auto text-primary" }),
|
|
115
115
|
/* @__PURE__ */ p("p", {
|
|
116
116
|
className: "text-2xl font-bold mt-1",
|
|
117
|
-
children:
|
|
117
|
+
children: k?.totalPrograms ?? O.programs.length
|
|
118
118
|
}),
|
|
119
119
|
/* @__PURE__ */ p("p", {
|
|
120
120
|
className: "text-xs text-muted-foreground",
|
|
@@ -122,13 +122,13 @@ function S() {
|
|
|
122
122
|
})
|
|
123
123
|
]
|
|
124
124
|
}) }),
|
|
125
|
-
/* @__PURE__ */ p(
|
|
125
|
+
/* @__PURE__ */ p(_, { children: /* @__PURE__ */ m(v, {
|
|
126
126
|
className: "pt-4 text-center",
|
|
127
127
|
children: [
|
|
128
128
|
/* @__PURE__ */ p(l, { className: "h-5 w-5 mx-auto text-primary" }),
|
|
129
129
|
/* @__PURE__ */ p("p", {
|
|
130
130
|
className: "text-2xl font-bold mt-1",
|
|
131
|
-
children:
|
|
131
|
+
children: k?.totalProjects ?? O.projects.length
|
|
132
132
|
}),
|
|
133
133
|
/* @__PURE__ */ p("p", {
|
|
134
134
|
className: "text-xs text-muted-foreground",
|
|
@@ -136,13 +136,13 @@ function S() {
|
|
|
136
136
|
})
|
|
137
137
|
]
|
|
138
138
|
}) }),
|
|
139
|
-
/* @__PURE__ */ p(
|
|
139
|
+
/* @__PURE__ */ p(_, { children: /* @__PURE__ */ m(v, {
|
|
140
140
|
className: "pt-4 text-center",
|
|
141
141
|
children: [
|
|
142
142
|
/* @__PURE__ */ p(a, { className: "h-5 w-5 mx-auto text-destructive" }),
|
|
143
143
|
/* @__PURE__ */ p("p", {
|
|
144
144
|
className: "text-2xl font-bold mt-1",
|
|
145
|
-
children:
|
|
145
|
+
children: k?.atRiskCount ?? 0
|
|
146
146
|
}),
|
|
147
147
|
/* @__PURE__ */ p("p", {
|
|
148
148
|
className: "text-xs text-muted-foreground",
|
|
@@ -150,13 +150,13 @@ function S() {
|
|
|
150
150
|
})
|
|
151
151
|
]
|
|
152
152
|
}) }),
|
|
153
|
-
/* @__PURE__ */ p(
|
|
153
|
+
/* @__PURE__ */ p(_, { children: /* @__PURE__ */ m(v, {
|
|
154
154
|
className: "pt-4 text-center",
|
|
155
155
|
children: [
|
|
156
156
|
/* @__PURE__ */ p(s, { className: "h-5 w-5 mx-auto text-status-warning-text" }),
|
|
157
157
|
/* @__PURE__ */ p("p", {
|
|
158
158
|
className: "text-2xl font-bold mt-1",
|
|
159
|
-
children:
|
|
159
|
+
children: k?.overdueMilestones ?? 0
|
|
160
160
|
}),
|
|
161
161
|
/* @__PURE__ */ p("p", {
|
|
162
162
|
className: "text-xs text-muted-foreground",
|
|
@@ -171,12 +171,12 @@ function S() {
|
|
|
171
171
|
children: [/* @__PURE__ */ m("div", {
|
|
172
172
|
className: "lg:col-span-2 space-y-6",
|
|
173
173
|
children: [
|
|
174
|
-
/* @__PURE__ */ m(
|
|
174
|
+
/* @__PURE__ */ m(_, { children: [/* @__PURE__ */ p(y, { children: /* @__PURE__ */ p(b, { children: "Overview" }) }), /* @__PURE__ */ m(v, {
|
|
175
175
|
className: "space-y-4",
|
|
176
176
|
children: [/* @__PURE__ */ p("p", {
|
|
177
177
|
className: "text-sm",
|
|
178
|
-
children:
|
|
179
|
-
}),
|
|
178
|
+
children: O.description ?? "No description provided."
|
|
179
|
+
}), k ? /* @__PURE__ */ m("div", {
|
|
180
180
|
className: "grid grid-cols-2 gap-4",
|
|
181
181
|
children: [
|
|
182
182
|
/* @__PURE__ */ m("div", {
|
|
@@ -186,7 +186,7 @@ function S() {
|
|
|
186
186
|
children: "Budget Utilization"
|
|
187
187
|
}), /* @__PURE__ */ p("p", {
|
|
188
188
|
className: "text-lg font-semibold mt-1",
|
|
189
|
-
children:
|
|
189
|
+
children: k.budgetUtilization == null ? "No budget data" : `${k.budgetUtilization.toFixed(0)}%`
|
|
190
190
|
})]
|
|
191
191
|
}),
|
|
192
192
|
/* @__PURE__ */ m("div", {
|
|
@@ -196,7 +196,7 @@ function S() {
|
|
|
196
196
|
children: "Health Score"
|
|
197
197
|
}), /* @__PURE__ */ p("p", {
|
|
198
198
|
className: "text-lg font-semibold mt-1",
|
|
199
|
-
children:
|
|
199
|
+
children: k.healthScore == null ? "No projects yet" : k.healthScore.toFixed(0)
|
|
200
200
|
})]
|
|
201
201
|
}),
|
|
202
202
|
/* @__PURE__ */ m("div", {
|
|
@@ -206,7 +206,7 @@ function S() {
|
|
|
206
206
|
children: "Total Spent"
|
|
207
207
|
}), /* @__PURE__ */ p("p", {
|
|
208
208
|
className: "text-lg font-semibold mt-1",
|
|
209
|
-
children:
|
|
209
|
+
children: k.totalSpent == null ? "N/A" : k.totalSpent.toLocaleString()
|
|
210
210
|
})]
|
|
211
211
|
}),
|
|
212
212
|
/* @__PURE__ */ m("div", {
|
|
@@ -216,7 +216,7 @@ function S() {
|
|
|
216
216
|
children: "Projects by Status"
|
|
217
217
|
}), /* @__PURE__ */ p("p", {
|
|
218
218
|
className: "text-sm mt-1",
|
|
219
|
-
children: Object.keys(
|
|
219
|
+
children: Object.keys(k.projectsByStatus).length > 0 ? Object.entries(k.projectsByStatus).map(([e, t]) => `${n(e)}: ${t}`).join(", ") : "No projects yet"
|
|
220
220
|
})]
|
|
221
221
|
})
|
|
222
222
|
]
|
|
@@ -225,16 +225,16 @@ function S() {
|
|
|
225
225
|
children: "Loading overview…"
|
|
226
226
|
})]
|
|
227
227
|
})] }),
|
|
228
|
-
/* @__PURE__ */ m(
|
|
228
|
+
/* @__PURE__ */ m(_, { children: [/* @__PURE__ */ p(y, { children: /* @__PURE__ */ m(b, { children: [
|
|
229
229
|
"Programs (",
|
|
230
|
-
|
|
230
|
+
O.programs.length,
|
|
231
231
|
")"
|
|
232
|
-
] }) }), /* @__PURE__ */ m(
|
|
232
|
+
] }) }), /* @__PURE__ */ m(v, {
|
|
233
233
|
className: "space-y-2",
|
|
234
|
-
children: [
|
|
234
|
+
children: [O.programs.length === 0 && /* @__PURE__ */ p("p", {
|
|
235
235
|
className: "text-sm text-muted-foreground",
|
|
236
236
|
children: "No programs are linked to this portfolio yet."
|
|
237
|
-
}),
|
|
237
|
+
}), O.programs.map((e) => /* @__PURE__ */ m("button", {
|
|
238
238
|
type: "button",
|
|
239
239
|
className: "flex w-full items-center justify-between rounded-md border p-3 text-left hover:bg-muted",
|
|
240
240
|
onClick: () => d(r("/products/program-detail-page", e.id)),
|
|
@@ -248,16 +248,31 @@ function S() {
|
|
|
248
248
|
})]
|
|
249
249
|
}, e.id))]
|
|
250
250
|
})] }),
|
|
251
|
-
/* @__PURE__ */ m(
|
|
251
|
+
/* @__PURE__ */ m(_, { children: [/* @__PURE__ */ p(y, { children: /* @__PURE__ */ m(b, { children: [
|
|
252
252
|
"Projects (",
|
|
253
|
-
|
|
253
|
+
O.projects.length,
|
|
254
254
|
")"
|
|
255
|
-
] }) }), /* @__PURE__ */ m(
|
|
255
|
+
] }) }), /* @__PURE__ */ m(v, {
|
|
256
256
|
className: "space-y-2",
|
|
257
|
-
children: [
|
|
258
|
-
className: "
|
|
259
|
-
children:
|
|
260
|
-
|
|
257
|
+
children: [O.projects.length === 0 && /* @__PURE__ */ m("div", {
|
|
258
|
+
className: "space-y-1",
|
|
259
|
+
children: [/* @__PURE__ */ p("p", {
|
|
260
|
+
className: "text-sm text-muted-foreground",
|
|
261
|
+
children: "No projects are linked to this portfolio yet."
|
|
262
|
+
}), /* @__PURE__ */ m("p", {
|
|
263
|
+
className: "text-sm text-muted-foreground",
|
|
264
|
+
children: [
|
|
265
|
+
"Open a project and set its Portfolio field to add it here.",
|
|
266
|
+
" ",
|
|
267
|
+
/* @__PURE__ */ p(g, {
|
|
268
|
+
variant: "link",
|
|
269
|
+
className: "h-auto p-0 text-sm",
|
|
270
|
+
onClick: () => d("/projects/projects-page"),
|
|
271
|
+
children: "Go to Projects"
|
|
272
|
+
})
|
|
273
|
+
]
|
|
274
|
+
})]
|
|
275
|
+
}), O.projects.map((e) => /* @__PURE__ */ m("button", {
|
|
261
276
|
type: "button",
|
|
262
277
|
className: "flex w-full items-center justify-between rounded-md border p-3 text-left hover:bg-muted",
|
|
263
278
|
onClick: () => d(r("/projects/project-detail-page", e.id)),
|
|
@@ -274,7 +289,7 @@ function S() {
|
|
|
274
289
|
]
|
|
275
290
|
}), /* @__PURE__ */ m("div", {
|
|
276
291
|
className: "space-y-6",
|
|
277
|
-
children: [/* @__PURE__ */ m(
|
|
292
|
+
children: [/* @__PURE__ */ m(_, { children: [/* @__PURE__ */ p(y, { children: /* @__PURE__ */ p(b, { children: "Details" }) }), /* @__PURE__ */ m(v, {
|
|
278
293
|
className: "space-y-2 text-sm",
|
|
279
294
|
children: [
|
|
280
295
|
/* @__PURE__ */ m("div", {
|
|
@@ -282,7 +297,7 @@ function S() {
|
|
|
282
297
|
children: [/* @__PURE__ */ p("span", {
|
|
283
298
|
className: "text-muted-foreground",
|
|
284
299
|
children: "Owner"
|
|
285
|
-
}), /* @__PURE__ */ p(t, { id:
|
|
300
|
+
}), /* @__PURE__ */ p(t, { id: O.ownerId })]
|
|
286
301
|
}),
|
|
287
302
|
/* @__PURE__ */ m("div", {
|
|
288
303
|
className: "flex justify-between",
|
|
@@ -291,7 +306,7 @@ function S() {
|
|
|
291
306
|
children: "Budget"
|
|
292
307
|
}), /* @__PURE__ */ p("span", {
|
|
293
308
|
className: "font-medium",
|
|
294
|
-
children:
|
|
309
|
+
children: O.budgetTotal == null ? "Not set" : `${O.currency ?? ""} ${O.budgetTotal.toLocaleString()}`.trim()
|
|
295
310
|
})]
|
|
296
311
|
}),
|
|
297
312
|
/* @__PURE__ */ m("div", {
|
|
@@ -299,26 +314,26 @@ function S() {
|
|
|
299
314
|
children: [/* @__PURE__ */ p("span", {
|
|
300
315
|
className: "text-muted-foreground",
|
|
301
316
|
children: "Fiscal Period"
|
|
302
|
-
}), /* @__PURE__ */ p("span", { children:
|
|
317
|
+
}), /* @__PURE__ */ p("span", { children: O.fiscalPeriod ?? "Not set" })]
|
|
303
318
|
}),
|
|
304
319
|
/* @__PURE__ */ m("div", {
|
|
305
320
|
className: "flex justify-between",
|
|
306
321
|
children: [/* @__PURE__ */ p("span", {
|
|
307
322
|
className: "text-muted-foreground",
|
|
308
323
|
children: "Created"
|
|
309
|
-
}), /* @__PURE__ */ p("span", { children: new Date(
|
|
324
|
+
}), /* @__PURE__ */ p("span", { children: new Date(O.createdAt).toLocaleDateString() })]
|
|
310
325
|
}),
|
|
311
326
|
/* @__PURE__ */ m("div", {
|
|
312
327
|
className: "flex justify-between",
|
|
313
328
|
children: [/* @__PURE__ */ p("span", {
|
|
314
329
|
className: "text-muted-foreground",
|
|
315
330
|
children: "Updated"
|
|
316
|
-
}), /* @__PURE__ */ p("span", { children: new Date(
|
|
331
|
+
}), /* @__PURE__ */ p("span", { children: new Date(O.updatedAt).toLocaleDateString() })]
|
|
317
332
|
})
|
|
318
333
|
]
|
|
319
|
-
})] }),
|
|
334
|
+
})] }), O.tags.length > 0 && /* @__PURE__ */ m(_, { children: [/* @__PURE__ */ p(y, { children: /* @__PURE__ */ p(b, { children: "Tags" }) }), /* @__PURE__ */ p(v, {
|
|
320
335
|
className: "flex flex-wrap gap-1",
|
|
321
|
-
children:
|
|
336
|
+
children: O.tags.map((e) => /* @__PURE__ */ p(h, {
|
|
322
337
|
variant: "outline",
|
|
323
338
|
className: "text-xs",
|
|
324
339
|
children: e
|
|
@@ -330,6 +345,6 @@ function S() {
|
|
|
330
345
|
});
|
|
331
346
|
}
|
|
332
347
|
//#endregion
|
|
333
|
-
export {
|
|
348
|
+
export { C as PortfoliosPortfolioDetailPagePage };
|
|
334
349
|
|
|
335
350
|
//# sourceMappingURL=PortfoliosPortfolioDetailPagePage.js.map
|