@burdenoff/microfe-planmagnet 2026.925.18 → 2026.926.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/wspace-types.d.ts +61 -2
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js +1 -1
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsCreateProductPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsCreateProductPagePage.js +93 -98
- package/dist/planmagnet/pages/ProductsCreateProductPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsCreateRoadmapPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsCreateRoadmapPagePage.js +72 -79
- package/dist/planmagnet/pages/ProductsCreateRoadmapPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsEditProductPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsEditProductPagePage.js +112 -123
- package/dist/planmagnet/pages/ProductsEditProductPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +92 -98
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsProductsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsProductsPagePage.js +88 -96
- package/dist/planmagnet/pages/ProductsProductsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js +116 -114
- package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js +82 -88
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapsPagePage.js +79 -79
- package/dist/planmagnet/pages/ProductsRoadmapsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +150 -154
- 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 +120 -109
- package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.js.map +1 -1
- package/dist/planmagnet/pages/SprintsStartSprintPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/SprintsStartSprintPagePage.js +75 -31
- package/dist/planmagnet/pages/SprintsStartSprintPagePage.js.map +1 -1
- package/dist/planmagnet/roadmaps/planningFocus.d.ts +23 -0
- package/dist/planmagnet/roadmaps/planningFocus.d.ts.map +1 -0
- package/dist/planmagnet/roadmaps/planningFocus.js +76 -0
- package/dist/planmagnet/roadmaps/planningFocus.js.map +1 -0
- package/dist/planmagnet/sprints/SprintDates.d.ts +37 -0
- package/dist/planmagnet/sprints/SprintDates.d.ts.map +1 -0
- package/dist/planmagnet/sprints/SprintDates.js +114 -0
- package/dist/planmagnet/sprints/SprintDates.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SprintsStartSprintPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/SprintsStartSprintPagePage.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 { Zap, Calendar, Users } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { NewSprintDialog, SprintLifecycleActions } from '../sprints/SprintControls';\n\nconst GET_SPRINTS = gql`\n query GetSprints(\n $workspaceId: ID!\n $filter: PlanmagnetSprintFilter\n $pagination: PlanmagnetPaginationInput\n ) {\n planmagnetSprints(workspaceId: $workspaceId, filter: $filter, pagination: $pagination) {\n items {\n id\n name\n goal\n status\n startDate\n endDate\n velocity\n workItems {\n id\n title\n status\n storyPoints\n assigneeId\n }\n }\n totalCount\n }\n }\n`;\n\ninterface SprintWorkItem {\n id: string;\n title: string;\n status: string;\n storyPoints: number | null;\n assigneeId: string | null;\n}\n\ninterface SprintNode {\n id: string;\n name: string;\n goal: string | null;\n status: string;\n startDate: string | null;\n endDate: string | null;\n velocity: number | null;\n workItems: SprintWorkItem[];\n}\n\ninterface SprintMetrics {\n totalPoints: number;\n completedPoints: number;\n tasksTotal: number;\n tasksCompleted: number;\n teamMemberIds: string[];\n velocity: number;\n}\n\nfunction deriveSprintMetrics(sprint: SprintNode): SprintMetrics {\n const items = sprint.workItems ?? [];\n const totalPoints = items.reduce((sum, w) => sum + (w.storyPoints ?? 0), 0);\n const completedPoints = items\n .filter((w) => w.status === 'DONE')\n .reduce((sum, w) => sum + (w.storyPoints ?? 0), 0);\n const tasksTotal = items.length;\n const tasksCompleted = items.filter((w) => w.status === 'DONE').length;\n const teamMemberIds = Array.from(\n new Set(items.map((w) => w.assigneeId).filter((id): id is string => Boolean(id)))\n );\n const velocity = sprint.velocity ?? completedPoints;\n return { totalPoints, completedPoints, tasksTotal, tasksCompleted, teamMemberIds, velocity };\n}\n\nexport function SprintsStartSprintPagePage() {\n const { workspaceId } = usePlanMagnet();\n\n const {\n data: sprintData,\n loading: sprintsLoading,\n error: sprintsError,\n } = useQuery<{\n planmagnetSprints: { items: SprintNode[]; totalCount: number };\n }>(GET_SPRINTS, {\n variables: { workspaceId, pagination: { first: 50 } },\n skip: !workspaceId,\n });\n\n const sprints = sprintData?.planmagnetSprints?.items ?? [];\n // REVIEW is still in flight (it can be completed or cancelled), so it\n // belongs with the active sprints rather than vanishing from this page.\n const activeSprints = sprints.filter((s) => s.status === 'ACTIVE' || s.status === 'REVIEW');\n const planningSprints = sprints.filter((s) => s.status === 'PLANNING');\n\n const navigate = usePlanMagnetNavigate();\n const openSprint = (id: string) => navigate(detailPath('/projects/planning-detail-page', id));\n\n // Only before the first load: a refetch after a sprint action must not blank\n // the page (it unmounted the controls mid-action).\n if (sprintsLoading && !sprintData) {\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 }\n\n if (sprintsError) {\n return <div className=\"p-6 text-red-500\">Error: {sprintsError.message}</div>;\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\">Sprint Management</h1>\n <p className=\"text-sm text-muted-foreground\">\n {sprints.length} sprints total · {activeSprints.length} active\n </p>\n </div>\n <NewSprintDialog onCreated={openSprint} />\n </div>\n\n {activeSprints.length > 0 && (\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-medium\">Active Sprints</h2>\n {activeSprints.map((sprint) => {\n const metrics = deriveSprintMetrics(sprint);\n const tasksForSprint = sprint.workItems ?? [];\n const pctDone =\n metrics.totalPoints > 0\n ? Math.round((metrics.completedPoints / metrics.totalPoints) * 100)\n : 0;\n return (\n <Card key={sprint.id}>\n <CardHeader className=\"pb-2\">\n <div className=\"flex items-center justify-between\">\n <CardTitle className=\"text-base flex items-center gap-2\">\n <Zap className=\"h-4 w-4 text-primary\" />\n <button\n type=\"button\"\n className=\"hover:underline text-left\"\n onClick={() => openSprint(sprint.id)}\n >\n {sprint.name}\n </button>\n </CardTitle>\n <div className=\"flex items-start gap-2\">\n <Badge>{sprint.status === 'REVIEW' ? 'Review' : 'Active'}</Badge>\n <SprintLifecycleActions\n sprintId={sprint.id}\n sprintName={sprint.name}\n status={sprint.status}\n />\n </div>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <p className=\"text-sm text-muted-foreground\">{sprint.goal ?? ''}</p>\n <div className=\"flex gap-4 text-xs text-muted-foreground\">\n <span className=\"flex items-center gap-1\">\n <Calendar className=\"h-3 w-3\" />\n {sprint.startDate ?? '?'} - {sprint.endDate ?? '?'}\n </span>\n <span className=\"flex items-center gap-1\">\n <Users className=\"h-3 w-3\" />\n {metrics.teamMemberIds.length} members\n </span>\n </div>\n <div className=\"grid grid-cols-3 gap-4 text-center\">\n <div className=\"p-3 bg-muted rounded-md\">\n <p className=\"text-lg font-bold\">\n {metrics.completedPoints}/{metrics.totalPoints}\n </p>\n <p className=\"text-xs text-muted-foreground\">Points</p>\n </div>\n <div className=\"p-3 bg-muted rounded-md\">\n <p className=\"text-lg font-bold\">\n {metrics.tasksCompleted}/{metrics.tasksTotal}\n </p>\n <p className=\"text-xs text-muted-foreground\">Tasks</p>\n </div>\n <div className=\"p-3 bg-muted rounded-md\">\n <p className=\"text-lg font-bold\">{metrics.velocity}</p>\n <p className=\"text-xs text-muted-foreground\">Velocity</p>\n </div>\n </div>\n <div>\n <div className=\"flex justify-between text-xs mb-1\">\n <span className=\"text-muted-foreground\">Sprint Progress</span>\n <span className=\"font-medium\">{pctDone}%</span>\n </div>\n <div className=\"w-full h-2 rounded-full bg-muted overflow-hidden\">\n <div\n className=\"h-full bg-primary rounded-full\"\n style={{ width: `${pctDone}%` }}\n />\n </div>\n </div>\n {tasksForSprint.length > 0 && (\n <div>\n <p className=\"text-xs font-medium text-muted-foreground mb-2\">\n Sprint Backlog\n </p>\n <div className=\"space-y-1\">\n {tasksForSprint.map((t) => (\n <div\n key={t.id}\n className=\"flex items-center justify-between border rounded p-2 text-sm\"\n >\n <span>{t.title}</span>\n <div className=\"flex gap-1\">\n <Badge variant=\"secondary\" className=\"text-xs capitalize\">\n {t.status.toLowerCase().replace('_', ' ')}\n </Badge>\n <span className=\"text-xs text-muted-foreground\">\n {t.storyPoints ?? 0}pts\n </span>\n </div>\n </div>\n ))}\n </div>\n </div>\n )}\n <div>\n <p className=\"text-xs font-medium text-muted-foreground mb-1\">Team</p>\n <div className=\"flex gap-2\">\n {metrics.teamMemberIds.map((memberId) => (\n <div\n key={memberId}\n className=\"h-7 w-7 rounded-full bg-primary/10 flex items-center justify-center text-[10px] font-medium text-primary\"\n title={memberId}\n >\n {memberId.slice(0, 2).toUpperCase()}\n </div>\n ))}\n </div>\n </div>\n </CardContent>\n </Card>\n );\n })}\n </div>\n )}\n\n {planningSprints.length > 0 && (\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-medium\">Planning</h2>\n {planningSprints.map((sprint) => {\n const metrics = deriveSprintMetrics(sprint);\n return (\n <Card key={sprint.id}>\n <CardHeader className=\"pb-2\">\n <div className=\"flex items-center justify-between\">\n <CardTitle className=\"text-base\">\n <button\n type=\"button\"\n className=\"hover:underline text-left\"\n onClick={() => openSprint(sprint.id)}\n >\n {sprint.name}\n </button>\n </CardTitle>\n <div className=\"flex items-start gap-2\">\n <Badge variant=\"secondary\">Planning</Badge>\n <SprintLifecycleActions\n sprintId={sprint.id}\n sprintName={sprint.name}\n status={sprint.status}\n />\n </div>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n <p className=\"text-sm text-muted-foreground\">{sprint.goal ?? ''}</p>\n <div className=\"flex gap-4 text-xs text-muted-foreground\">\n <span>\n {sprint.startDate ?? '?'} - {sprint.endDate ?? '?'}\n </span>\n <span>{metrics.totalPoints} points planned</span>\n <span>{metrics.teamMemberIds.length} members</span>\n </div>\n </CardContent>\n </Card>\n );\n })}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;AAUA,IAAM,IAAc,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDvB,SAAS,EAAoB,GAAmC;CAC9D,IAAM,IAAQ,EAAO,aAAa,EAAE,EAC9B,IAAc,EAAM,QAAQ,GAAK,MAAM,KAAO,EAAE,eAAe,IAAI,EAAE,EACrE,IAAkB,EACrB,QAAQ,MAAM,EAAE,WAAW,OAAO,CAClC,QAAQ,GAAK,MAAM,KAAO,EAAE,eAAe,IAAI,EAAE;AAOpD,QAAO;EAAE;EAAa;EAAiB,YANpB,EAAM;EAM0B,gBAL5B,EAAM,QAAQ,MAAM,EAAE,WAAW,OAAO,CAAC;EAKG,eAJ7C,MAAM,KAC1B,IAAI,IAAI,EAAM,KAAK,MAAM,EAAE,WAAW,CAAC,QAAQ,MAAqB,EAAQ,EAAI,CAAC,CAClF;EAEiF,UADjE,EAAO,YAAY;EACwD;;AAG9F,SAAgB,IAA6B;CAC3C,IAAM,EAAE,mBAAgB,GAAe,EAEjC,EACJ,MAAM,GACN,SAAS,GACT,OAAO,MACL,EAED,GAAa;EACd,WAAW;GAAE;GAAa,YAAY,EAAE,OAAO,IAAI;GAAE;EACrD,MAAM,CAAC;EACR,CAAC,EAEI,IAAU,GAAY,mBAAmB,SAAS,EAAE,EAGpD,IAAgB,EAAQ,QAAQ,MAAM,EAAE,WAAW,YAAY,EAAE,WAAW,SAAS,EACrF,IAAkB,EAAQ,QAAQ,MAAM,EAAE,WAAW,WAAW,EAEhE,IAAW,GAAuB,EAClC,KAAc,MAAe,EAAS,EAAW,kCAAkC,EAAG,CAAC;AAgB7F,QAZI,KAAkB,CAAC,IAEnB,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD,EAAK,WAAU,kFAAmF,CAAA;EAC9F,CAAA,GAIN,IACK,kBAAC,OAAD;EAAK,WAAU;YAAf,CAAkC,WAAQ,EAAa,QAAc;MAI5E,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;KAAI,WAAU;eAAyB;KAAsB,CAAA,EAC7D,kBAAC,KAAD;KAAG,WAAU;eAAb;MACG,EAAQ;MAAO;MAAyB,EAAc;MAAO;MAC5D;OACA,EAAA,CAAA,EACN,kBAAC,GAAD,EAAiB,WAAW,GAAc,CAAA,CACtC;;GAEL,EAAc,SAAS,KACtB,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eAAsB;KAAmB,CAAA,EACtD,EAAc,KAAK,MAAW;KAC7B,IAAM,IAAU,EAAoB,EAAO,EACrC,IAAiB,EAAO,aAAa,EAAE,EACvC,IACJ,EAAQ,cAAc,IAClB,KAAK,MAAO,EAAQ,kBAAkB,EAAQ,cAAe,IAAI,GACjE;AACN,YACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;MAAY,WAAU;gBACpB,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAW,WAAU;kBAArB,CACE,kBAAC,GAAD,EAAK,WAAU,wBAAyB,CAAA,EACxC,kBAAC,UAAD;SACE,MAAK;SACL,WAAU;SACV,eAAe,EAAW,EAAO,GAAG;mBAEnC,EAAO;SACD,CAAA,CACC;WACZ,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD,EAAA,UAAQ,EAAO,WAAW,WAAW,WAAW,UAAiB,CAAA,EACjE,kBAAC,GAAD;SACE,UAAU,EAAO;SACjB,YAAY,EAAO;SACnB,QAAQ,EAAO;SACf,CAAA,CACE;UACF;;MACK,CAAA,EACb,kBAAC,GAAD;MAAa,WAAU;gBAAvB;OACE,kBAAC,KAAD;QAAG,WAAU;kBAAiC,EAAO,QAAQ;QAAO,CAAA;OACpE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,QAAD;SAAM,WAAU;mBAAhB;UACE,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;UAC/B,EAAO,aAAa;UAAI;UAAI,EAAO,WAAW;UAC1C;YACP,kBAAC,QAAD;SAAM,WAAU;mBAAhB;UACE,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;UAC5B,EAAQ,cAAc;UAAO;UACzB;WACH;;OACN,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBAAb;YACG,EAAQ;YAAgB;YAAE,EAAQ;YACjC;cACJ,kBAAC,KAAD;WAAG,WAAU;qBAAgC;WAAU,CAAA,CACnD;;SACN,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBAAb;YACG,EAAQ;YAAe;YAAE,EAAQ;YAChC;cACJ,kBAAC,KAAD;WAAG,WAAU;qBAAgC;WAAS,CAAA,CAClD;;SACN,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBAAqB,EAAQ;WAAa,CAAA,EACvD,kBAAC,KAAD;WAAG,WAAU;qBAAgC;WAAY,CAAA,CACrD;;SACF;;OACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,QAAD;SAAM,WAAU;mBAAwB;SAAsB,CAAA,EAC9D,kBAAC,QAAD;SAAM,WAAU;mBAAhB,CAA+B,GAAQ,IAAQ;WAC3C;WACN,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,OAAD;SACE,WAAU;SACV,OAAO,EAAE,OAAO,GAAG,EAAQ,IAAI;SAC/B,CAAA;QACE,CAAA,CACF,EAAA,CAAA;OACL,EAAe,SAAS,KACvB,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;QAAG,WAAU;kBAAiD;QAE1D,CAAA,EACJ,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAe,KAAK,MACnB,kBAAC,OAAD;SAEE,WAAU;mBAFZ,CAIE,kBAAC,QAAD,EAAA,UAAO,EAAE,OAAa,CAAA,EACtB,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,GAAD;WAAO,SAAQ;WAAY,WAAU;qBAClC,EAAE,OAAO,aAAa,CAAC,QAAQ,KAAK,IAAI;WACnC,CAAA,EACR,kBAAC,QAAD;WAAM,WAAU;qBAAhB,CACG,EAAE,eAAe,GAAE,MACf;aACH;YACF;WAZC,EAAE,GAYH,CACN;QACE,CAAA,CACF,EAAA,CAAA;OAER,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;QAAG,WAAU;kBAAiD;QAAQ,CAAA,EACtE,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAQ,cAAc,KAAK,MAC1B,kBAAC,OAAD;SAEE,WAAU;SACV,OAAO;mBAEN,EAAS,MAAM,GAAG,EAAE,CAAC,aAAa;SAC/B,EALC,EAKD,CACN;QACE,CAAA,CACF,EAAA,CAAA;OACM;QACT,EAAA,EAzGI,EAAO,GAyGX;MAET,CACE;;GAGP,EAAgB,SAAS,KACxB,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eAAsB;KAAa,CAAA,EAChD,EAAgB,KAAK,MAAW;KAC/B,IAAM,IAAU,EAAoB,EAAO;AAC3C,YACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;MAAY,WAAU;gBACpB,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAW,WAAU;kBACnB,kBAAC,UAAD;SACE,MAAK;SACL,WAAU;SACV,eAAe,EAAW,EAAO,GAAG;mBAEnC,EAAO;SACD,CAAA;QACC,CAAA,EACZ,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAQ;mBAAY;SAAgB,CAAA,EAC3C,kBAAC,GAAD;SACE,UAAU,EAAO;SACjB,YAAY,EAAO;SACnB,QAAQ,EAAO;SACf,CAAA,CACE;UACF;;MACK,CAAA,EACb,kBAAC,GAAD;MAAa,WAAU;gBAAvB,CACE,kBAAC,KAAD;OAAG,WAAU;iBAAiC,EAAO,QAAQ;OAAO,CAAA,EACpE,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,QAAD,EAAA,UAAA;SACG,EAAO,aAAa;SAAI;SAAI,EAAO,WAAW;SAC1C,EAAA,CAAA;QACP,kBAAC,QAAD,EAAA,UAAA,CAAO,EAAQ,aAAY,kBAAsB,EAAA,CAAA;QACjD,kBAAC,QAAD,EAAA,UAAA,CAAO,EAAQ,cAAc,QAAO,WAAe,EAAA,CAAA;QAC/C;SACM;QACT,EAAA,EAhCI,EAAO,GAgCX;MAET,CACE;;GAEJ"}
|
|
1
|
+
{"version":3,"file":"SprintsStartSprintPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/SprintsStartSprintPagePage.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 { Zap, Users } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { NewSprintDialog, SprintLifecycleActions } from '../sprints/SprintControls';\nimport { SprintDates, type SprintPlanChange } from '../sprints/SprintDates';\n\nconst GET_SPRINTS = gql`\n query GetSprints(\n $workspaceId: ID!\n $filter: PlanmagnetSprintFilter\n $pagination: PlanmagnetPaginationInput\n ) {\n planmagnetSprints(workspaceId: $workspaceId, filter: $filter, pagination: $pagination) {\n items {\n id\n name\n goal\n status\n startDate\n endDate\n startedAt\n completedAt\n baselineStartDate\n baselineEndDate\n planChanges {\n field\n fromValue\n toValue\n sprintStatus\n changedBy\n changedAt\n }\n velocity\n workItems {\n id\n title\n status\n storyPoints\n assigneeId\n }\n }\n totalCount\n }\n }\n`;\n\ninterface SprintWorkItem {\n id: string;\n title: string;\n status: string;\n storyPoints: number | null;\n assigneeId: string | null;\n}\n\ninterface SprintNode {\n id: string;\n name: string;\n goal: string | null;\n status: string;\n startDate: string | null;\n endDate: string | null;\n startedAt: string | null;\n completedAt: string | null;\n baselineStartDate: string | null;\n baselineEndDate: string | null;\n planChanges: SprintPlanChange[];\n velocity: number | null;\n workItems: SprintWorkItem[];\n}\n\ninterface SprintMetrics {\n totalPoints: number;\n completedPoints: number;\n tasksTotal: number;\n tasksCompleted: number;\n teamMemberIds: string[];\n velocity: number;\n}\n\nfunction deriveSprintMetrics(sprint: SprintNode): SprintMetrics {\n const items = sprint.workItems ?? [];\n const totalPoints = items.reduce((sum, w) => sum + (w.storyPoints ?? 0), 0);\n const completedPoints = items\n .filter((w) => w.status === 'DONE')\n .reduce((sum, w) => sum + (w.storyPoints ?? 0), 0);\n const tasksTotal = items.length;\n const tasksCompleted = items.filter((w) => w.status === 'DONE').length;\n const teamMemberIds = Array.from(\n new Set(items.map((w) => w.assigneeId).filter((id): id is string => Boolean(id)))\n );\n const velocity = sprint.velocity ?? completedPoints;\n return { totalPoints, completedPoints, tasksTotal, tasksCompleted, teamMemberIds, velocity };\n}\n\nexport function SprintsStartSprintPagePage() {\n const { workspaceId } = usePlanMagnet();\n\n const {\n data: sprintData,\n loading: sprintsLoading,\n error: sprintsError,\n } = useQuery<{\n planmagnetSprints: { items: SprintNode[]; totalCount: number };\n }>(GET_SPRINTS, {\n variables: { workspaceId, pagination: { first: 50 } },\n skip: !workspaceId,\n });\n\n const sprints = sprintData?.planmagnetSprints?.items ?? [];\n // REVIEW is still in flight (it can be completed or cancelled), so it\n // belongs with the active sprints rather than vanishing from this page.\n const activeSprints = sprints.filter((s) => s.status === 'ACTIVE' || s.status === 'REVIEW');\n const planningSprints = sprints.filter((s) => s.status === 'PLANNING');\n // Finished sprints used to vanish from this page, so \"did we finish on\n // time?\" had nowhere to be answered (BOFF-7918).\n const finishedSprints = sprints.filter(\n (s) => s.status === 'COMPLETED' || s.status === 'CANCELLED'\n );\n\n const navigate = usePlanMagnetNavigate();\n const openSprint = (id: string) => navigate(detailPath('/projects/planning-detail-page', id));\n\n // Only before the first load: a refetch after a sprint action must not blank\n // the page (it unmounted the controls mid-action).\n if (sprintsLoading && !sprintData) {\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 }\n\n if (sprintsError) {\n return <div className=\"p-6 text-red-500\">Error: {sprintsError.message}</div>;\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\">Sprint Management</h1>\n <p className=\"text-sm text-muted-foreground\">\n {sprints.length} sprints total · {activeSprints.length} active\n </p>\n </div>\n <NewSprintDialog onCreated={openSprint} />\n </div>\n\n {activeSprints.length > 0 && (\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-medium\">Active Sprints</h2>\n {activeSprints.map((sprint) => {\n const metrics = deriveSprintMetrics(sprint);\n const tasksForSprint = sprint.workItems ?? [];\n const pctDone =\n metrics.totalPoints > 0\n ? Math.round((metrics.completedPoints / metrics.totalPoints) * 100)\n : 0;\n return (\n <Card key={sprint.id}>\n <CardHeader className=\"pb-2\">\n <div className=\"flex items-center justify-between\">\n <CardTitle className=\"text-base flex items-center gap-2\">\n <Zap className=\"h-4 w-4 text-primary\" />\n <button\n type=\"button\"\n className=\"hover:underline text-left\"\n onClick={() => openSprint(sprint.id)}\n >\n {sprint.name}\n </button>\n </CardTitle>\n <div className=\"flex items-start gap-2\">\n <Badge>{sprint.status === 'REVIEW' ? 'Review' : 'Active'}</Badge>\n <SprintLifecycleActions\n sprintId={sprint.id}\n sprintName={sprint.name}\n status={sprint.status}\n />\n </div>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <p className=\"text-sm text-muted-foreground\">{sprint.goal ?? ''}</p>\n <SprintDates sprint={sprint} />\n <div className=\"flex gap-4 text-xs text-muted-foreground\">\n <span className=\"flex items-center gap-1\">\n <Users className=\"h-3 w-3\" />\n {metrics.teamMemberIds.length} members\n </span>\n </div>\n <div className=\"grid grid-cols-3 gap-4 text-center\">\n <div className=\"p-3 bg-muted rounded-md\">\n <p className=\"text-lg font-bold\">\n {metrics.completedPoints}/{metrics.totalPoints}\n </p>\n <p className=\"text-xs text-muted-foreground\">Points</p>\n </div>\n <div className=\"p-3 bg-muted rounded-md\">\n <p className=\"text-lg font-bold\">\n {metrics.tasksCompleted}/{metrics.tasksTotal}\n </p>\n <p className=\"text-xs text-muted-foreground\">Tasks</p>\n </div>\n <div className=\"p-3 bg-muted rounded-md\">\n <p className=\"text-lg font-bold\">{metrics.velocity}</p>\n <p className=\"text-xs text-muted-foreground\">Velocity</p>\n </div>\n </div>\n <div>\n <div className=\"flex justify-between text-xs mb-1\">\n <span className=\"text-muted-foreground\">Sprint Progress</span>\n <span className=\"font-medium\">{pctDone}%</span>\n </div>\n <div className=\"w-full h-2 rounded-full bg-muted overflow-hidden\">\n <div\n className=\"h-full bg-primary rounded-full\"\n style={{ width: `${pctDone}%` }}\n />\n </div>\n </div>\n {tasksForSprint.length > 0 && (\n <div>\n <p className=\"text-xs font-medium text-muted-foreground mb-2\">\n Sprint Backlog\n </p>\n <div className=\"space-y-1\">\n {tasksForSprint.map((t) => (\n <div\n key={t.id}\n className=\"flex items-center justify-between border rounded p-2 text-sm\"\n >\n <span>{t.title}</span>\n <div className=\"flex gap-1\">\n <Badge variant=\"secondary\" className=\"text-xs capitalize\">\n {t.status.toLowerCase().replace('_', ' ')}\n </Badge>\n <span className=\"text-xs text-muted-foreground\">\n {t.storyPoints ?? 0}pts\n </span>\n </div>\n </div>\n ))}\n </div>\n </div>\n )}\n <div>\n <p className=\"text-xs font-medium text-muted-foreground mb-1\">Team</p>\n <div className=\"flex gap-2\">\n {metrics.teamMemberIds.map((memberId) => (\n <div\n key={memberId}\n className=\"h-7 w-7 rounded-full bg-primary/10 flex items-center justify-center text-[10px] font-medium text-primary\"\n title={memberId}\n >\n {memberId.slice(0, 2).toUpperCase()}\n </div>\n ))}\n </div>\n </div>\n </CardContent>\n </Card>\n );\n })}\n </div>\n )}\n\n {planningSprints.length > 0 && (\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-medium\">Planning</h2>\n {planningSprints.map((sprint) => {\n const metrics = deriveSprintMetrics(sprint);\n return (\n <Card key={sprint.id}>\n <CardHeader className=\"pb-2\">\n <div className=\"flex items-center justify-between\">\n <CardTitle className=\"text-base\">\n <button\n type=\"button\"\n className=\"hover:underline text-left\"\n onClick={() => openSprint(sprint.id)}\n >\n {sprint.name}\n </button>\n </CardTitle>\n <div className=\"flex items-start gap-2\">\n <Badge variant=\"secondary\">Planning</Badge>\n <SprintLifecycleActions\n sprintId={sprint.id}\n sprintName={sprint.name}\n status={sprint.status}\n />\n </div>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n <p className=\"text-sm text-muted-foreground\">{sprint.goal ?? ''}</p>\n <SprintDates sprint={sprint} />\n <div className=\"flex gap-4 text-xs text-muted-foreground\">\n <span>{metrics.totalPoints} points planned</span>\n <span>{metrics.teamMemberIds.length} members</span>\n </div>\n </CardContent>\n </Card>\n );\n })}\n </div>\n )}\n\n {finishedSprints.length > 0 && (\n <div className=\"space-y-4\" data-testid=\"finished-sprints\">\n <h2 className=\"text-lg font-medium\">Finished</h2>\n {finishedSprints.map((sprint) => {\n const metrics = deriveSprintMetrics(sprint);\n return (\n <Card key={sprint.id} data-testid=\"finished-sprint\">\n <CardHeader className=\"pb-2\">\n <div className=\"flex items-center justify-between\">\n <CardTitle className=\"text-base\">\n <button\n type=\"button\"\n className=\"hover:underline text-left\"\n onClick={() => openSprint(sprint.id)}\n >\n {sprint.name}\n </button>\n </CardTitle>\n <Badge variant=\"outline\">\n {sprint.status === 'COMPLETED' ? 'Completed' : 'Cancelled'}\n </Badge>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n <SprintDates sprint={sprint} />\n <p className=\"text-xs text-muted-foreground\">\n {metrics.completedPoints}/{metrics.totalPoints} points done\n </p>\n </CardContent>\n </Card>\n );\n })}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;AAWA,IAAM,IAAc,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyEvB,SAAS,EAAoB,GAAmC;CAC9D,IAAM,IAAQ,EAAO,aAAa,EAAE,EAC9B,IAAc,EAAM,QAAQ,GAAK,MAAM,KAAO,EAAE,eAAe,IAAI,EAAE,EACrE,IAAkB,EACrB,QAAQ,MAAM,EAAE,WAAW,OAAO,CAClC,QAAQ,GAAK,MAAM,KAAO,EAAE,eAAe,IAAI,EAAE;AAOpD,QAAO;EAAE;EAAa;EAAiB,YANpB,EAAM;EAM0B,gBAL5B,EAAM,QAAQ,MAAM,EAAE,WAAW,OAAO,CAAC;EAKG,eAJ7C,MAAM,KAC1B,IAAI,IAAI,EAAM,KAAK,MAAM,EAAE,WAAW,CAAC,QAAQ,MAAqB,EAAQ,EAAI,CAAC,CAClF;EAEiF,UADjE,EAAO,YAAY;EACwD;;AAG9F,SAAgB,IAA6B;CAC3C,IAAM,EAAE,mBAAgB,GAAe,EAEjC,EACJ,MAAM,GACN,SAAS,GACT,OAAO,MACL,EAED,GAAa;EACd,WAAW;GAAE;GAAa,YAAY,EAAE,OAAO,IAAI;GAAE;EACrD,MAAM,CAAC;EACR,CAAC,EAEI,IAAU,GAAY,mBAAmB,SAAS,EAAE,EAGpD,IAAgB,EAAQ,QAAQ,MAAM,EAAE,WAAW,YAAY,EAAE,WAAW,SAAS,EACrF,IAAkB,EAAQ,QAAQ,MAAM,EAAE,WAAW,WAAW,EAGhE,IAAkB,EAAQ,QAC7B,MAAM,EAAE,WAAW,eAAe,EAAE,WAAW,YACjD,EAEK,IAAW,GAAuB,EAClC,KAAc,MAAe,EAAS,EAAW,kCAAkC,EAAG,CAAC;AAgB7F,QAZI,KAAkB,CAAC,IAEnB,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD,EAAK,WAAU,kFAAmF,CAAA;EAC9F,CAAA,GAIN,IACK,kBAAC,OAAD;EAAK,WAAU;YAAf,CAAkC,WAAQ,EAAa,QAAc;MAI5E,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;KAAI,WAAU;eAAyB;KAAsB,CAAA,EAC7D,kBAAC,KAAD;KAAG,WAAU;eAAb;MACG,EAAQ;MAAO;MAAyB,EAAc;MAAO;MAC5D;OACA,EAAA,CAAA,EACN,kBAAC,GAAD,EAAiB,WAAW,GAAc,CAAA,CACtC;;GAEL,EAAc,SAAS,KACtB,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eAAsB;KAAmB,CAAA,EACtD,EAAc,KAAK,MAAW;KAC7B,IAAM,IAAU,EAAoB,EAAO,EACrC,IAAiB,EAAO,aAAa,EAAE,EACvC,IACJ,EAAQ,cAAc,IAClB,KAAK,MAAO,EAAQ,kBAAkB,EAAQ,cAAe,IAAI,GACjE;AACN,YACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;MAAY,WAAU;gBACpB,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAW,WAAU;kBAArB,CACE,kBAAC,GAAD,EAAK,WAAU,wBAAyB,CAAA,EACxC,kBAAC,UAAD;SACE,MAAK;SACL,WAAU;SACV,eAAe,EAAW,EAAO,GAAG;mBAEnC,EAAO;SACD,CAAA,CACC;WACZ,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD,EAAA,UAAQ,EAAO,WAAW,WAAW,WAAW,UAAiB,CAAA,EACjE,kBAAC,GAAD;SACE,UAAU,EAAO;SACjB,YAAY,EAAO;SACnB,QAAQ,EAAO;SACf,CAAA,CACE;UACF;;MACK,CAAA,EACb,kBAAC,GAAD;MAAa,WAAU;gBAAvB;OACE,kBAAC,KAAD;QAAG,WAAU;kBAAiC,EAAO,QAAQ;QAAO,CAAA;OACpE,kBAAC,GAAD,EAAqB,WAAU,CAAA;OAC/B,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,QAAD;SAAM,WAAU;mBAAhB;UACE,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;UAC5B,EAAQ,cAAc;UAAO;UACzB;;QACH,CAAA;OACN,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBAAb;YACG,EAAQ;YAAgB;YAAE,EAAQ;YACjC;cACJ,kBAAC,KAAD;WAAG,WAAU;qBAAgC;WAAU,CAAA,CACnD;;SACN,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBAAb;YACG,EAAQ;YAAe;YAAE,EAAQ;YAChC;cACJ,kBAAC,KAAD;WAAG,WAAU;qBAAgC;WAAS,CAAA,CAClD;;SACN,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBAAqB,EAAQ;WAAa,CAAA,EACvD,kBAAC,KAAD;WAAG,WAAU;qBAAgC;WAAY,CAAA,CACrD;;SACF;;OACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,QAAD;SAAM,WAAU;mBAAwB;SAAsB,CAAA,EAC9D,kBAAC,QAAD;SAAM,WAAU;mBAAhB,CAA+B,GAAQ,IAAQ;WAC3C;WACN,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,OAAD;SACE,WAAU;SACV,OAAO,EAAE,OAAO,GAAG,EAAQ,IAAI;SAC/B,CAAA;QACE,CAAA,CACF,EAAA,CAAA;OACL,EAAe,SAAS,KACvB,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;QAAG,WAAU;kBAAiD;QAE1D,CAAA,EACJ,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAe,KAAK,MACnB,kBAAC,OAAD;SAEE,WAAU;mBAFZ,CAIE,kBAAC,QAAD,EAAA,UAAO,EAAE,OAAa,CAAA,EACtB,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,GAAD;WAAO,SAAQ;WAAY,WAAU;qBAClC,EAAE,OAAO,aAAa,CAAC,QAAQ,KAAK,IAAI;WACnC,CAAA,EACR,kBAAC,QAAD;WAAM,WAAU;qBAAhB,CACG,EAAE,eAAe,GAAE,MACf;aACH;YACF;WAZC,EAAE,GAYH,CACN;QACE,CAAA,CACF,EAAA,CAAA;OAER,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;QAAG,WAAU;kBAAiD;QAAQ,CAAA,EACtE,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAQ,cAAc,KAAK,MAC1B,kBAAC,OAAD;SAEE,WAAU;SACV,OAAO;mBAEN,EAAS,MAAM,GAAG,EAAE,CAAC,aAAa;SAC/B,EALC,EAKD,CACN;QACE,CAAA,CACF,EAAA,CAAA;OACM;QACT,EAAA,EAtGI,EAAO,GAsGX;MAET,CACE;;GAGP,EAAgB,SAAS,KACxB,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eAAsB;KAAa,CAAA,EAChD,EAAgB,KAAK,MAAW;KAC/B,IAAM,IAAU,EAAoB,EAAO;AAC3C,YACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;MAAY,WAAU;gBACpB,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAW,WAAU;kBACnB,kBAAC,UAAD;SACE,MAAK;SACL,WAAU;SACV,eAAe,EAAW,EAAO,GAAG;mBAEnC,EAAO;SACD,CAAA;QACC,CAAA,EACZ,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAQ;mBAAY;SAAgB,CAAA,EAC3C,kBAAC,GAAD;SACE,UAAU,EAAO;SACjB,YAAY,EAAO;SACnB,QAAQ,EAAO;SACf,CAAA,CACE;UACF;;MACK,CAAA,EACb,kBAAC,GAAD;MAAa,WAAU;gBAAvB;OACE,kBAAC,KAAD;QAAG,WAAU;kBAAiC,EAAO,QAAQ;QAAO,CAAA;OACpE,kBAAC,GAAD,EAAqB,WAAU,CAAA;OAC/B,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,QAAD,EAAA,UAAA,CAAO,EAAQ,aAAY,kBAAsB,EAAA,CAAA,EACjD,kBAAC,QAAD,EAAA,UAAA,CAAO,EAAQ,cAAc,QAAO,WAAe,EAAA,CAAA,CAC/C;;OACM;QACT,EAAA,EA9BI,EAAO,GA8BX;MAET,CACE;;GAGP,EAAgB,SAAS,KACxB,kBAAC,OAAD;IAAK,WAAU;IAAY,eAAY;cAAvC,CACE,kBAAC,MAAD;KAAI,WAAU;eAAsB;KAAa,CAAA,EAChD,EAAgB,KAAK,MAAW;KAC/B,IAAM,IAAU,EAAoB,EAAO;AAC3C,YACE,kBAAC,GAAD;MAAsB,eAAY;gBAAlC,CACE,kBAAC,GAAD;OAAY,WAAU;iBACpB,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAW,WAAU;mBACnB,kBAAC,UAAD;UACE,MAAK;UACL,WAAU;UACV,eAAe,EAAW,EAAO,GAAG;oBAEnC,EAAO;UACD,CAAA;SACC,CAAA,EACZ,kBAAC,GAAD;SAAO,SAAQ;mBACZ,EAAO,WAAW,cAAc,cAAc;SACzC,CAAA,CACJ;;OACK,CAAA,EACb,kBAAC,GAAD;OAAa,WAAU;iBAAvB,CACE,kBAAC,GAAD,EAAqB,WAAU,CAAA,EAC/B,kBAAC,KAAD;QAAG,WAAU;kBAAb;SACG,EAAQ;SAAgB;SAAE,EAAQ;SAAY;SAC7C;UACQ;SACT;QAvBI,EAAO,GAuBX;MAET,CACE;;GAEJ"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a roadmap is organised around (BOFF-7934). Separate from how it is
|
|
3
|
+
* displayed: the roadmap page has ONE layout today (items grouped by lane with
|
|
4
|
+
* their dates), so the service accepts only the TIMELINE display and these
|
|
5
|
+
* pages no longer offer Gantt / Swimlane / Custom — a stored label is not a
|
|
6
|
+
* view. Legacy roadmaps were never classified, so a missing focus is shown as
|
|
7
|
+
* "not set", never guessed.
|
|
8
|
+
*/
|
|
9
|
+
export type PlanningFocus = 'THEME' | 'FEATURE' | 'TECHNOLOGY' | 'PORTFOLIO';
|
|
10
|
+
export declare const PLANNING_FOCUSES: readonly PlanningFocus[];
|
|
11
|
+
/** The only display the roadmap page has; the create/update inputs require one. */
|
|
12
|
+
export declare const ROADMAP_DISPLAY = "TIMELINE";
|
|
13
|
+
export declare function planningFocusLabel(focus: string | null | undefined): string;
|
|
14
|
+
/** Icon AND label, never colour alone. */
|
|
15
|
+
export declare function PlanningFocusBadge({ focus }: {
|
|
16
|
+
focus: string | null | undefined;
|
|
17
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export declare function PlanningFocusSelect({ value, onChange, id, }: {
|
|
19
|
+
value: PlanningFocus | null;
|
|
20
|
+
onChange: (value: PlanningFocus | null) => void;
|
|
21
|
+
id?: string;
|
|
22
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
//# sourceMappingURL=planningFocus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planningFocus.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/roadmaps/planningFocus.tsx"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,SAAS,GAAG,YAAY,GAAG,WAAW,CAAC;AAE7E,eAAO,MAAM,gBAAgB,EAAE,SAAS,aAAa,EAKpD,CAAC;AASF,mFAAmF;AACnF,eAAO,MAAM,eAAe,aAAa,CAAC;AAE1C,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAE3E;AAED,0CAA0C;AAC1C,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAAE,2CAoBjF;AAED,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,EACR,EAAqB,GACtB,EAAE;IACD,KAAK,EAAE,aAAa,GAAG,IAAI,CAAC;IAC5B,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,KAAK,IAAI,CAAC;IAChD,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,2CAwBA"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Boxes as e, Cpu as t, Layers as n, Lightbulb as r } from "lucide-react";
|
|
2
|
+
import { jsx as i, jsxs as a } from "react/jsx-runtime";
|
|
3
|
+
//#region src/planmagnet/roadmaps/planningFocus.tsx
|
|
4
|
+
var o = [
|
|
5
|
+
"THEME",
|
|
6
|
+
"FEATURE",
|
|
7
|
+
"TECHNOLOGY",
|
|
8
|
+
"PORTFOLIO"
|
|
9
|
+
], s = {
|
|
10
|
+
THEME: {
|
|
11
|
+
label: "Theme",
|
|
12
|
+
icon: r
|
|
13
|
+
},
|
|
14
|
+
FEATURE: {
|
|
15
|
+
label: "Feature",
|
|
16
|
+
icon: n
|
|
17
|
+
},
|
|
18
|
+
TECHNOLOGY: {
|
|
19
|
+
label: "Technology",
|
|
20
|
+
icon: t
|
|
21
|
+
},
|
|
22
|
+
PORTFOLIO: {
|
|
23
|
+
label: "Portfolio",
|
|
24
|
+
icon: e
|
|
25
|
+
}
|
|
26
|
+
}, c = "TIMELINE";
|
|
27
|
+
function l(e) {
|
|
28
|
+
return e && e in s ? s[e].label : "Focus not set";
|
|
29
|
+
}
|
|
30
|
+
function u({ focus: e }) {
|
|
31
|
+
if (!e || !(e in s)) return /* @__PURE__ */ i("span", {
|
|
32
|
+
className: "inline-flex items-center gap-1 text-xs text-muted-foreground",
|
|
33
|
+
"data-testid": "planning-focus",
|
|
34
|
+
children: "Focus not set"
|
|
35
|
+
});
|
|
36
|
+
let { label: t, icon: n } = s[e];
|
|
37
|
+
return /* @__PURE__ */ a("span", {
|
|
38
|
+
className: "inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-xs text-foreground",
|
|
39
|
+
"data-testid": "planning-focus",
|
|
40
|
+
children: [
|
|
41
|
+
/* @__PURE__ */ i(n, { className: "h-3 w-3" }),
|
|
42
|
+
" ",
|
|
43
|
+
t
|
|
44
|
+
]
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function d({ value: e, onChange: t, id: n = "planning-focus" }) {
|
|
48
|
+
return /* @__PURE__ */ a("div", { children: [
|
|
49
|
+
/* @__PURE__ */ i("label", {
|
|
50
|
+
htmlFor: n,
|
|
51
|
+
className: "text-sm font-medium",
|
|
52
|
+
children: "Planning focus"
|
|
53
|
+
}),
|
|
54
|
+
/* @__PURE__ */ a("select", {
|
|
55
|
+
id: n,
|
|
56
|
+
className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
|
|
57
|
+
value: e ?? "",
|
|
58
|
+
onChange: (e) => t(e.target.value ? e.target.value : null),
|
|
59
|
+
children: [/* @__PURE__ */ i("option", {
|
|
60
|
+
value: "",
|
|
61
|
+
children: "Not set"
|
|
62
|
+
}), o.map((e) => /* @__PURE__ */ i("option", {
|
|
63
|
+
value: e,
|
|
64
|
+
children: s[e].label
|
|
65
|
+
}, e))]
|
|
66
|
+
}),
|
|
67
|
+
/* @__PURE__ */ i("p", {
|
|
68
|
+
className: "mt-1 text-xs text-muted-foreground",
|
|
69
|
+
children: "What the roadmap is organised around. Roadmaps are shown in one layout for now."
|
|
70
|
+
})
|
|
71
|
+
] });
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { o as PLANNING_FOCUSES, u as PlanningFocusBadge, d as PlanningFocusSelect, c as ROADMAP_DISPLAY, l as planningFocusLabel };
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=planningFocus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planningFocus.js","names":[],"sources":["../../../src/planmagnet/roadmaps/planningFocus.tsx"],"sourcesContent":["import { Boxes, Cpu, Layers, Lightbulb, type LucideIcon } from 'lucide-react';\n\n/**\n * What a roadmap is organised around (BOFF-7934). Separate from how it is\n * displayed: the roadmap page has ONE layout today (items grouped by lane with\n * their dates), so the service accepts only the TIMELINE display and these\n * pages no longer offer Gantt / Swimlane / Custom — a stored label is not a\n * view. Legacy roadmaps were never classified, so a missing focus is shown as\n * \"not set\", never guessed.\n */\nexport type PlanningFocus = 'THEME' | 'FEATURE' | 'TECHNOLOGY' | 'PORTFOLIO';\n\nexport const PLANNING_FOCUSES: readonly PlanningFocus[] = [\n 'THEME',\n 'FEATURE',\n 'TECHNOLOGY',\n 'PORTFOLIO',\n];\n\nconst FOCUS: Record<PlanningFocus, { label: string; icon: LucideIcon }> = {\n THEME: { label: 'Theme', icon: Lightbulb },\n FEATURE: { label: 'Feature', icon: Layers },\n TECHNOLOGY: { label: 'Technology', icon: Cpu },\n PORTFOLIO: { label: 'Portfolio', icon: Boxes },\n};\n\n/** The only display the roadmap page has; the create/update inputs require one. */\nexport const ROADMAP_DISPLAY = 'TIMELINE';\n\nexport function planningFocusLabel(focus: string | null | undefined): string {\n return focus && focus in FOCUS ? FOCUS[focus as PlanningFocus].label : 'Focus not set';\n}\n\n/** Icon AND label, never colour alone. */\nexport function PlanningFocusBadge({ focus }: { focus: string | null | undefined }) {\n if (!focus || !(focus in FOCUS)) {\n return (\n <span\n className=\"inline-flex items-center gap-1 text-xs text-muted-foreground\"\n data-testid=\"planning-focus\"\n >\n Focus not set\n </span>\n );\n }\n const { label, icon: Icon } = FOCUS[focus as PlanningFocus];\n return (\n <span\n className=\"inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-xs text-foreground\"\n data-testid=\"planning-focus\"\n >\n <Icon className=\"h-3 w-3\" /> {label}\n </span>\n );\n}\n\nexport function PlanningFocusSelect({\n value,\n onChange,\n id = 'planning-focus',\n}: {\n value: PlanningFocus | null;\n onChange: (value: PlanningFocus | null) => void;\n id?: string;\n}) {\n return (\n <div>\n <label htmlFor={id} className=\"text-sm font-medium\">\n Planning focus\n </label>\n <select\n id={id}\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={value ?? ''}\n onChange={(e) => onChange(e.target.value ? (e.target.value as PlanningFocus) : null)}\n >\n <option value=\"\">Not set</option>\n {PLANNING_FOCUSES.map((f) => (\n <option key={f} value={f}>\n {FOCUS[f].label}\n </option>\n ))}\n </select>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n What the roadmap is organised around. Roadmaps are shown in one layout for now.\n </p>\n </div>\n );\n}\n"],"mappings":";;;AAYA,IAAa,IAA6C;CACxD;CACA;CACA;CACA;CACD,EAEK,IAAoE;CACxE,OAAO;EAAE,OAAO;EAAS,MAAM;EAAW;CAC1C,SAAS;EAAE,OAAO;EAAW,MAAM;EAAQ;CAC3C,YAAY;EAAE,OAAO;EAAc,MAAM;EAAK;CAC9C,WAAW;EAAE,OAAO;EAAa,MAAM;EAAO;CAC/C,EAGY,IAAkB;AAE/B,SAAgB,EAAmB,GAA0C;AAC3E,QAAO,KAAS,KAAS,IAAQ,EAAM,GAAwB,QAAQ;;AAIzE,SAAgB,EAAmB,EAAE,YAA+C;AAClF,KAAI,CAAC,KAAS,EAAE,KAAS,GACvB,QACE,kBAAC,QAAD;EACE,WAAU;EACV,eAAY;YACb;EAEM,CAAA;CAGX,IAAM,EAAE,UAAO,MAAM,MAAS,EAAM;AACpC,QACE,kBAAC,QAAD;EACE,WAAU;EACV,eAAY;YAFd;GAIE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;;GAAE;GACzB;;;AAIX,SAAgB,EAAoB,EAClC,UACA,aACA,QAAK,oBAKJ;AACD,QACE,kBAAC,OAAD,EAAA,UAAA;EACE,kBAAC,SAAD;GAAO,SAAS;GAAI,WAAU;aAAsB;GAE5C,CAAA;EACR,kBAAC,UAAD;GACM;GACJ,WAAU;GACV,OAAO,KAAS;GAChB,WAAW,MAAM,EAAS,EAAE,OAAO,QAAS,EAAE,OAAO,QAA0B,KAAK;aAJtF,CAME,kBAAC,UAAD;IAAQ,OAAM;cAAG;IAAgB,CAAA,EAChC,EAAiB,KAAK,MACrB,kBAAC,UAAD;IAAgB,OAAO;cACpB,EAAM,GAAG;IACH,EAFI,EAEJ,CACT,CACK;;EACT,kBAAC,KAAD;GAAG,WAAU;aAAqC;GAE9C,CAAA;EACA,EAAA,CAAA"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A sprint's PLANNED dates next to what actually happened (BOFF-7918).
|
|
3
|
+
*
|
|
4
|
+
* Until 2026-09-26 Start and Complete overwrote the planned start/end with the
|
|
5
|
+
* moment they were clicked, so the plan was lost. The service now keeps the
|
|
6
|
+
* plan, records `startedAt`/`completedAt`, snapshots the plan in force at Start
|
|
7
|
+
* as the baseline (the commitment), and appends every later plan change to a
|
|
8
|
+
* history. Sprints started before the fix have no recorded actual times: they
|
|
9
|
+
* read "not recorded", never a guessed date.
|
|
10
|
+
*/
|
|
11
|
+
export interface SprintPlanChange {
|
|
12
|
+
field: string;
|
|
13
|
+
fromValue: string | null;
|
|
14
|
+
toValue: string | null;
|
|
15
|
+
sprintStatus: string;
|
|
16
|
+
changedBy: string;
|
|
17
|
+
changedAt: string;
|
|
18
|
+
}
|
|
19
|
+
export interface SprintDateFields {
|
|
20
|
+
status: string;
|
|
21
|
+
startDate: string | null;
|
|
22
|
+
endDate: string | null;
|
|
23
|
+
startedAt?: string | null;
|
|
24
|
+
completedAt?: string | null;
|
|
25
|
+
baselineStartDate?: string | null;
|
|
26
|
+
baselineEndDate?: string | null;
|
|
27
|
+
planChanges?: SprintPlanChange[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Whole calendar days between the committed end and the actual completion.
|
|
31
|
+
* Positive = late, negative = early, 0 = on the day.
|
|
32
|
+
*/
|
|
33
|
+
export declare function daysAgainstCommitment(completedAt: string, committedEnd: string): number;
|
|
34
|
+
export declare function SprintDates({ sprint }: {
|
|
35
|
+
sprint: SprintDateFields;
|
|
36
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
37
|
+
//# sourceMappingURL=SprintDates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SprintDates.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/sprints/SprintDates.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAClC;AAkBD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAMvF;AAqCD,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,gBAAgB,CAAA;CAAE,2CA8EnE"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { PersonName as e } from "../people/People.js";
|
|
2
|
+
import { useState as t } from "react";
|
|
3
|
+
import { AlertTriangle as n, CalendarClock as r, CheckCircle2 as i, History as a } from "lucide-react";
|
|
4
|
+
import { jsx as o, jsxs as s } from "react/jsx-runtime";
|
|
5
|
+
//#region src/planmagnet/sprints/SprintDates.tsx
|
|
6
|
+
var c = (e) => e ? new Date(e).toLocaleDateString(void 0, {
|
|
7
|
+
timeZone: "UTC",
|
|
8
|
+
day: "numeric",
|
|
9
|
+
month: "short",
|
|
10
|
+
year: "numeric"
|
|
11
|
+
}) : "—", l = (e) => new Date(e).toLocaleString(void 0, {
|
|
12
|
+
dateStyle: "medium",
|
|
13
|
+
timeStyle: "short"
|
|
14
|
+
}), u = 1440 * 60 * 1e3;
|
|
15
|
+
function d(e, t) {
|
|
16
|
+
let n = new Date(e), r = Date.UTC(n.getUTCFullYear(), n.getUTCMonth(), n.getUTCDate()), i = new Date(t), a = Date.UTC(i.getUTCFullYear(), i.getUTCMonth(), i.getUTCDate());
|
|
17
|
+
return Math.round((r - a) / u);
|
|
18
|
+
}
|
|
19
|
+
function f({ completedAt: e, committedEnd: t }) {
|
|
20
|
+
if (!t) return /* @__PURE__ */ o("span", {
|
|
21
|
+
className: "text-xs text-muted-foreground",
|
|
22
|
+
children: "No committed end to compare with."
|
|
23
|
+
});
|
|
24
|
+
let r = d(e, t);
|
|
25
|
+
return r <= 0 ? /* @__PURE__ */ s("span", {
|
|
26
|
+
className: "inline-flex items-center gap-1 text-xs text-status-success-text",
|
|
27
|
+
"data-testid": "sprint-outcome",
|
|
28
|
+
children: [/* @__PURE__ */ o(i, { className: "h-3.5 w-3.5" }), r === 0 ? "Finished on the committed day" : `Finished ${-r} day${r === -1 ? "" : "s"} early`]
|
|
29
|
+
}) : /* @__PURE__ */ s("span", {
|
|
30
|
+
className: "inline-flex items-center gap-1 text-xs text-status-warning-text",
|
|
31
|
+
"data-testid": "sprint-outcome",
|
|
32
|
+
children: [
|
|
33
|
+
/* @__PURE__ */ o(n, { className: "h-3.5 w-3.5" }),
|
|
34
|
+
"Finished ",
|
|
35
|
+
r,
|
|
36
|
+
" day",
|
|
37
|
+
r === 1 ? "" : "s",
|
|
38
|
+
" after the committed end"
|
|
39
|
+
]
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function p({ sprint: n }) {
|
|
43
|
+
let [i, u] = t(!1), d = n.status !== "PLANNING", p = n.status === "COMPLETED", m = n.planChanges ?? [], h = n.baselineEndDate ?? null, g = m.filter((e) => e.sprintStatus !== "PLANNING").length, _ = d && !n.startedAt && !!n.startDate, v = p && !n.completedAt && !!n.endDate;
|
|
44
|
+
return /* @__PURE__ */ s("div", {
|
|
45
|
+
className: "space-y-1 text-xs text-muted-foreground",
|
|
46
|
+
"data-testid": "sprint-dates",
|
|
47
|
+
children: [
|
|
48
|
+
/* @__PURE__ */ s("p", {
|
|
49
|
+
className: "flex items-center gap-1",
|
|
50
|
+
children: [
|
|
51
|
+
/* @__PURE__ */ o(r, { className: "h-3 w-3" }),
|
|
52
|
+
/* @__PURE__ */ s("span", { children: [
|
|
53
|
+
"Planned ",
|
|
54
|
+
_ ? "start not recorded" : c(n.startDate),
|
|
55
|
+
" –",
|
|
56
|
+
" ",
|
|
57
|
+
v ? "end not recorded" : c(n.endDate)
|
|
58
|
+
] }),
|
|
59
|
+
h && n.endDate && h !== n.endDate && /* @__PURE__ */ s("span", { children: [
|
|
60
|
+
"(committed end ",
|
|
61
|
+
c(h),
|
|
62
|
+
")"
|
|
63
|
+
] })
|
|
64
|
+
]
|
|
65
|
+
}),
|
|
66
|
+
d && /* @__PURE__ */ s("p", {
|
|
67
|
+
"data-testid": "sprint-actuals",
|
|
68
|
+
children: [
|
|
69
|
+
n.startedAt ? `Started ${l(n.startedAt)}` : _ && n.startDate ? `Started ${l(n.startDate)}` : "Actual start not recorded",
|
|
70
|
+
p && (n.completedAt ? ` · Completed ${l(n.completedAt)}` : v && n.endDate ? ` · Completed ${l(n.endDate)}` : " · Actual completion not recorded"),
|
|
71
|
+
(_ || v) && " (recorded before planned dates were kept)"
|
|
72
|
+
]
|
|
73
|
+
}),
|
|
74
|
+
p && n.completedAt && /* @__PURE__ */ o(f, {
|
|
75
|
+
completedAt: n.completedAt,
|
|
76
|
+
committedEnd: h
|
|
77
|
+
}),
|
|
78
|
+
m.length > 0 && /* @__PURE__ */ s("div", { children: [/* @__PURE__ */ s("button", {
|
|
79
|
+
type: "button",
|
|
80
|
+
className: "inline-flex items-center gap-1 hover:underline",
|
|
81
|
+
"aria-expanded": i,
|
|
82
|
+
onClick: () => u(!i),
|
|
83
|
+
"data-testid": "sprint-plan-history-toggle",
|
|
84
|
+
children: [
|
|
85
|
+
/* @__PURE__ */ o(a, { className: "h-3 w-3" }),
|
|
86
|
+
"Plan changed ",
|
|
87
|
+
m.length,
|
|
88
|
+
"×",
|
|
89
|
+
g > 0 ? ` (${g} after start)` : ""
|
|
90
|
+
]
|
|
91
|
+
}), i && /* @__PURE__ */ o("ol", {
|
|
92
|
+
className: "mt-1 space-y-0.5 pl-4",
|
|
93
|
+
"data-testid": "sprint-plan-history",
|
|
94
|
+
children: m.map((t, n) => /* @__PURE__ */ s("li", { children: [
|
|
95
|
+
t.field === "startDate" ? "Start" : "End",
|
|
96
|
+
": ",
|
|
97
|
+
c(t.fromValue),
|
|
98
|
+
" → ",
|
|
99
|
+
c(t.toValue),
|
|
100
|
+
t.sprintStatus === "PLANNING" ? "" : " (after start)",
|
|
101
|
+
" ·",
|
|
102
|
+
" ",
|
|
103
|
+
/* @__PURE__ */ o(e, { id: t.changedBy }),
|
|
104
|
+
" · ",
|
|
105
|
+
l(t.changedAt)
|
|
106
|
+
] }, `${t.changedAt}-${t.field}-${n}`))
|
|
107
|
+
})] })
|
|
108
|
+
]
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
export { p as SprintDates };
|
|
113
|
+
|
|
114
|
+
//# sourceMappingURL=SprintDates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SprintDates.js","names":[],"sources":["../../../src/planmagnet/sprints/SprintDates.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { CalendarClock, CheckCircle2, AlertTriangle, History } from 'lucide-react';\nimport { PersonName } from '../people/People';\n\n/**\n * A sprint's PLANNED dates next to what actually happened (BOFF-7918).\n *\n * Until 2026-09-26 Start and Complete overwrote the planned start/end with the\n * moment they were clicked, so the plan was lost. The service now keeps the\n * plan, records `startedAt`/`completedAt`, snapshots the plan in force at Start\n * as the baseline (the commitment), and appends every later plan change to a\n * history. Sprints started before the fix have no recorded actual times: they\n * read \"not recorded\", never a guessed date.\n */\n\nexport interface SprintPlanChange {\n field: string;\n fromValue: string | null;\n toValue: string | null;\n sprintStatus: string;\n changedBy: string;\n changedAt: string;\n}\n\nexport interface SprintDateFields {\n status: string;\n startDate: string | null;\n endDate: string | null;\n startedAt?: string | null;\n completedAt?: string | null;\n baselineStartDate?: string | null;\n baselineEndDate?: string | null;\n planChanges?: SprintPlanChange[];\n}\n\n/** Planned dates are calendar days (UTC midnight); show them without a time-zone shift. */\nconst day = (iso: string | null | undefined) =>\n iso\n ? new Date(iso).toLocaleDateString(undefined, {\n timeZone: 'UTC',\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n })\n : '—';\n/** Actual times are moments; show them in the viewer's zone. */\nconst moment = (iso: string) =>\n new Date(iso).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' });\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Whole calendar days between the committed end and the actual completion.\n * Positive = late, negative = early, 0 = on the day.\n */\nexport function daysAgainstCommitment(completedAt: string, committedEnd: string): number {\n const done = new Date(completedAt);\n const doneDay = Date.UTC(done.getUTCFullYear(), done.getUTCMonth(), done.getUTCDate());\n const end = new Date(committedEnd);\n const endDay = Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate());\n return Math.round((doneDay - endDay) / DAY_MS);\n}\n\nfunction Outcome({\n completedAt,\n committedEnd,\n}: {\n completedAt: string;\n committedEnd: string | null;\n}) {\n if (!committedEnd) {\n return <span className=\"text-xs text-muted-foreground\">No committed end to compare with.</span>;\n }\n const delta = daysAgainstCommitment(completedAt, committedEnd);\n if (delta <= 0) {\n return (\n <span\n className=\"inline-flex items-center gap-1 text-xs text-status-success-text\"\n data-testid=\"sprint-outcome\"\n >\n <CheckCircle2 className=\"h-3.5 w-3.5\" />\n {delta === 0\n ? 'Finished on the committed day'\n : `Finished ${-delta} day${delta === -1 ? '' : 's'} early`}\n </span>\n );\n }\n return (\n <span\n className=\"inline-flex items-center gap-1 text-xs text-status-warning-text\"\n data-testid=\"sprint-outcome\"\n >\n <AlertTriangle className=\"h-3.5 w-3.5\" />\n Finished {delta} day{delta === 1 ? '' : 's'} after the committed end\n </span>\n );\n}\n\nexport function SprintDates({ sprint }: { sprint: SprintDateFields }) {\n const [showHistory, setShowHistory] = useState(false);\n const started = sprint.status !== 'PLANNING';\n const finished = sprint.status === 'COMPLETED';\n const changes = sprint.planChanges ?? [];\n // The commitment is the plan in force at Start; without one (never started,\n // or started before it was recorded) fall back to the current plan only for\n // display, never for the on-time verdict.\n const committedEnd = sprint.baselineEndDate ?? null;\n const replannedAfterStart = changes.filter((c) => c.sprintStatus !== 'PLANNING').length;\n // Sprints started / completed before 2026-09-26: the old Start wrote the\n // moment of Start into startDate, and the old Complete wrote the moment of\n // Complete into endDate (measured in prod: all 30 such sprints carry a time\n // of day). Their planned dates were overwritten and cannot be recovered\n // (outside the backup window), so they read \"not recorded\", and the stored\n // values are shown for what they are — the recorded start/completion.\n const legacyStart = started && !sprint.startedAt && Boolean(sprint.startDate);\n const legacyEnd = finished && !sprint.completedAt && Boolean(sprint.endDate);\n\n return (\n <div className=\"space-y-1 text-xs text-muted-foreground\" data-testid=\"sprint-dates\">\n <p className=\"flex items-center gap-1\">\n <CalendarClock className=\"h-3 w-3\" />\n <span>\n Planned {legacyStart ? 'start not recorded' : day(sprint.startDate)} –{' '}\n {legacyEnd ? 'end not recorded' : day(sprint.endDate)}\n </span>\n {committedEnd && sprint.endDate && committedEnd !== sprint.endDate && (\n <span>(committed end {day(committedEnd)})</span>\n )}\n </p>\n {started && (\n <p data-testid=\"sprint-actuals\">\n {sprint.startedAt\n ? `Started ${moment(sprint.startedAt)}`\n : legacyStart && sprint.startDate\n ? `Started ${moment(sprint.startDate)}`\n : 'Actual start not recorded'}\n {finished &&\n (sprint.completedAt\n ? ` · Completed ${moment(sprint.completedAt)}`\n : legacyEnd && sprint.endDate\n ? ` · Completed ${moment(sprint.endDate)}`\n : ' · Actual completion not recorded')}\n {(legacyStart || legacyEnd) && ' (recorded before planned dates were kept)'}\n </p>\n )}\n {finished && sprint.completedAt && (\n <Outcome completedAt={sprint.completedAt} committedEnd={committedEnd} />\n )}\n {changes.length > 0 && (\n <div>\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1 hover:underline\"\n aria-expanded={showHistory}\n onClick={() => setShowHistory(!showHistory)}\n data-testid=\"sprint-plan-history-toggle\"\n >\n <History className=\"h-3 w-3\" />\n Plan changed {changes.length}×\n {replannedAfterStart > 0 ? ` (${replannedAfterStart} after start)` : ''}\n </button>\n {showHistory && (\n <ol className=\"mt-1 space-y-0.5 pl-4\" data-testid=\"sprint-plan-history\">\n {changes.map((c, i) => (\n <li key={`${c.changedAt}-${c.field}-${i}`}>\n {c.field === 'startDate' ? 'Start' : 'End'}: {day(c.fromValue)} → {day(c.toValue)}\n {c.sprintStatus !== 'PLANNING' ? ' (after start)' : ''} ·{' '}\n <PersonName id={c.changedBy} /> · {moment(c.changedAt)}\n </li>\n ))}\n </ol>\n )}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|