@burdenoff/microfe-planmagnet 2026.926.1 → 2026.926.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
  2. package/dist/planmagnet/PlanMagnetRoot.js +90 -79
  3. package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
  4. package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
  5. package/dist/planmagnet/PlanMagnetRoutes.js +124 -127
  6. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  7. package/dist/planmagnet/create/CreateDialogs.d.ts.map +1 -1
  8. package/dist/planmagnet/create/CreateDialogs.js +149 -124
  9. package/dist/planmagnet/create/CreateDialogs.js.map +1 -1
  10. package/dist/planmagnet/pages/NotFoundPage.d.ts +6 -0
  11. package/dist/planmagnet/pages/NotFoundPage.d.ts.map +1 -0
  12. package/dist/planmagnet/pages/NotFoundPage.js +18 -0
  13. package/dist/planmagnet/pages/NotFoundPage.js.map +1 -0
  14. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.d.ts.map +1 -1
  15. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +103 -87
  16. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
  17. package/dist/planmagnet/pages/ProjectsCreateProjectPagePage.d.ts.map +1 -1
  18. package/dist/planmagnet/pages/ProjectsCreateProjectPagePage.js +111 -91
  19. package/dist/planmagnet/pages/ProjectsCreateProjectPagePage.js.map +1 -1
  20. package/dist/planmagnet/pages/ProjectsEditProjectPagePage.d.ts.map +1 -1
  21. package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js +117 -97
  22. package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js.map +1 -1
  23. package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.d.ts.map +1 -1
  24. package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.js +133 -107
  25. package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.js.map +1 -1
  26. package/dist/planmagnet/pages/TasksCreateTaskPagePage.d.ts.map +1 -1
  27. package/dist/planmagnet/pages/TasksCreateTaskPagePage.js +161 -115
  28. package/dist/planmagnet/pages/TasksCreateTaskPagePage.js.map +1 -1
  29. package/dist/planmagnet/pages/TasksEditTaskPagePage.d.ts.map +1 -1
  30. package/dist/planmagnet/pages/TasksEditTaskPagePage.js +117 -102
  31. package/dist/planmagnet/pages/TasksEditTaskPagePage.js.map +1 -1
  32. package/dist/planmagnet/pages/TeamInviteMemberPagePage.d.ts.map +1 -1
  33. package/dist/planmagnet/pages/TeamInviteMemberPagePage.js +147 -147
  34. package/dist/planmagnet/pages/TeamInviteMemberPagePage.js.map +1 -1
  35. package/dist/planmagnet/pages/TeamTeamPagePage.d.ts.map +1 -1
  36. package/dist/planmagnet/pages/TeamTeamPagePage.js +77 -67
  37. package/dist/planmagnet/pages/TeamTeamPagePage.js.map +1 -1
  38. package/dist/planmagnet/sprints/SprintControls.d.ts.map +1 -1
  39. package/dist/planmagnet/sprints/SprintControls.js +48 -29
  40. package/dist/planmagnet/sprints/SprintControls.js.map +1 -1
  41. package/dist/planmagnet/utils/email.d.ts +9 -0
  42. package/dist/planmagnet/utils/email.d.ts.map +1 -0
  43. package/dist/planmagnet/utils/email.js +15 -0
  44. package/dist/planmagnet/utils/email.js.map +1 -0
  45. package/dist/planmagnet/utils/formValidation.d.ts +18 -0
  46. package/dist/planmagnet/utils/formValidation.d.ts.map +1 -0
  47. package/dist/planmagnet/utils/formValidation.js +17 -0
  48. package/dist/planmagnet/utils/formValidation.js.map +1 -0
  49. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"SprintControls.js","names":[],"sources":["../../../src/planmagnet/sprints/SprintControls.tsx"],"sourcesContent":["import { useDeferredValue, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogTrigger,\n Badge,\n Button,\n Checkbox,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n Input,\n} from '@burdenoff/fe-libs/ui';\nimport { CheckCircle2, Play, Plus, X, XCircle } from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { throwOnError } from '../utils/mutationResult';\n\n/**\n * Sprint lifecycle + membership controls.\n *\n * The buttons mirror the service's transition table exactly\n * (wspace-planmagnet-svc sprint.service.ts `TRANSITIONS`):\n * start PLANNING -> ACTIVE\n * complete ACTIVE | REVIEW -> COMPLETED\n * cancel PLANNING|ACTIVE|REVIEW -> CANCELLED\n * A COMPLETED or CANCELLED sprint is finished: no transitions, and its work\n * items can no longer be added or removed. The server enforces all of this;\n * the UI only hides what would be refused, and still surfaces a refusal (a\n * concurrent change can make a shown button stale).\n */\n\nexport type SprintStatus = 'PLANNING' | 'ACTIVE' | 'REVIEW' | 'COMPLETED' | 'CANCELLED';\nexport type SprintAction = 'start' | 'complete' | 'cancel';\n\nconst ALLOWED: Record<SprintAction, readonly SprintStatus[]> = {\n start: ['PLANNING'],\n complete: ['ACTIVE', 'REVIEW'],\n cancel: ['PLANNING', 'ACTIVE', 'REVIEW'],\n};\n\nexport function sprintActionsFor(status: string): SprintAction[] {\n return (Object.keys(ALLOWED) as SprintAction[]).filter((action) =>\n (ALLOWED[action] as readonly string[]).includes(status)\n );\n}\n\nexport function isSprintFinished(status: string): boolean {\n return status === 'COMPLETED' || status === 'CANCELLED';\n}\n\n/** Every query that renders sprint status or membership. */\nconst SPRINT_QUERIES = ['GetSprints', 'GetSprintDetail', 'GetSprintForEdit', 'GetDashboardData'];\n\nconst START_SPRINT = gql`\n mutation SprintControlsStart($id: ID!) {\n planmagnetStartSprint(id: $id) {\n id\n status\n }\n }\n`;\nconst COMPLETE_SPRINT = gql`\n mutation SprintControlsComplete($id: ID!) {\n planmagnetCompleteSprint(id: $id) {\n id\n status\n }\n }\n`;\nconst CANCEL_SPRINT = gql`\n mutation SprintControlsCancel($id: ID!) {\n planmagnetCancelSprint(id: $id) {\n id\n status\n }\n }\n`;\n\nfunction ErrorLine({ message }: { message: string }) {\n return (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {message}\n </p>\n );\n}\n\nexport function SprintLifecycleActions({\n sprintId,\n sprintName,\n status,\n}: {\n sprintId: string;\n sprintName: string;\n status: string;\n}) {\n const options = { refetchQueries: SPRINT_QUERIES, awaitRefetchQueries: true };\n const [start, startState] = useMutation(START_SPRINT, options);\n const [complete, completeState] = useMutation(COMPLETE_SPRINT, options);\n const [cancel, cancelState] = useMutation(CANCEL_SPRINT, options);\n const [error, setError] = useState<string | null>(null);\n\n const busy = startState.loading || completeState.loading || cancelState.loading;\n const actions = sprintActionsFor(status);\n // A refused transition still refetches; if the sprint turned out to be\n // finished there are no buttons left, but the reason must stay visible.\n if (actions.length === 0) return error ? <ErrorLine message={error} /> : null;\n\n const run = async (mutate: typeof start) => {\n setError(null);\n try {\n await throwOnError(mutate({ variables: { id: sprintId } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The sprint could not be updated.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\" data-testid=\"sprint-lifecycle-actions\">\n <div className=\"flex gap-2\">\n {actions.includes('start') && (\n <Button size=\"sm\" onClick={() => run(start)} disabled={busy}>\n <Play className=\"mr-1 h-4 w-4\" />\n Start sprint\n </Button>\n )}\n {actions.includes('complete') && (\n <ConfirmButton\n label=\"Complete sprint\"\n icon={<CheckCircle2 className=\"mr-1 h-4 w-4\" />}\n title={`Complete ${sprintName}?`}\n description=\"The sprint is closed and its work items can no longer be changed from it. This cannot be undone.\"\n confirmLabel=\"Complete sprint\"\n disabled={busy}\n onConfirm={() => run(complete)}\n />\n )}\n {actions.includes('cancel') && (\n <ConfirmButton\n label=\"Cancel sprint\"\n variant=\"outline\"\n icon={<XCircle className=\"mr-1 h-4 w-4\" />}\n title={`Cancel ${sprintName}?`}\n description=\"The sprint is abandoned and cannot be restarted. Its work items stay in it for the record. This cannot be undone.\"\n confirmLabel=\"Cancel sprint\"\n destructive\n disabled={busy}\n onConfirm={() => run(cancel)}\n />\n )}\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\nfunction ConfirmButton({\n label,\n icon,\n title,\n description,\n confirmLabel,\n onConfirm,\n disabled,\n destructive = false,\n variant = 'default',\n}: {\n label: string;\n icon: React.ReactNode;\n title: string;\n description: string;\n confirmLabel: string;\n onConfirm: () => void;\n disabled: boolean;\n destructive?: boolean;\n variant?: 'default' | 'outline';\n}) {\n return (\n <AlertDialog>\n <AlertDialogTrigger asChild>\n <Button size=\"sm\" variant={variant} disabled={disabled}>\n {icon}\n {label}\n </Button>\n </AlertDialogTrigger>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>{title}</AlertDialogTitle>\n <AlertDialogDescription>{description}</AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Keep it</AlertDialogCancel>\n <AlertDialogAction\n onClick={onConfirm}\n className={\n destructive\n ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'\n : ''\n }\n >\n {confirmLabel}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n\n// ─── Work items in the sprint ──────────────────────────────────────────────\n\nconst ADD_ITEMS = gql`\n mutation SprintControlsAddItems($id: ID!, $input: PlanmagnetSprintWorkItemsInput!) {\n planmagnetAddWorkItemsToSprint(id: $id, input: $input) {\n id\n }\n }\n`;\nconst REMOVE_ITEMS = gql`\n mutation SprintControlsRemoveItems($id: ID!, $input: PlanmagnetSprintWorkItemsInput!) {\n planmagnetRemoveWorkItemsFromSprint(id: $id, input: $input) {\n id\n }\n }\n`;\nconst PROJECT_WORK_ITEMS = gql`\n query SprintControlsCandidates($workspaceId: ID!, $filter: PlanmagnetWorkItemFilter) {\n planmagnetWorkItems(workspaceId: $workspaceId, filter: $filter, pagination: { first: 100 }) {\n totalCount\n items {\n id\n key\n title\n status\n storyPoints\n sprintId\n }\n }\n }\n`;\n\nexport interface SprintWorkItem {\n id: string;\n key: string;\n title?: string | null;\n status: string;\n storyPoints: number | null;\n}\n\ninterface CandidateItem extends SprintWorkItem {\n sprintId: string | null;\n}\n\nconst statusLabel = (status: string) => status.toLowerCase().replace(/_/g, ' ');\n\nexport function SprintWorkItemsPanel({\n sprintId,\n projectId,\n status,\n items,\n}: {\n sprintId: string;\n projectId: string;\n status: string;\n items: SprintWorkItem[];\n}) {\n const finished = isSprintFinished(status);\n const [remove, removeState] = useMutation(REMOVE_ITEMS, {\n refetchQueries: SPRINT_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const [removingId, setRemovingId] = useState<string | null>(null);\n\n const handleRemove = async (workItemId: string) => {\n setError(null);\n setRemovingId(workItemId);\n try {\n await throwOnError(\n remove({ variables: { id: sprintId, input: { workItemIds: [workItemId] } } })\n );\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The work item could not be removed.');\n } finally {\n setRemovingId(null);\n }\n };\n\n return (\n <div className=\"space-y-3\" data-testid=\"sprint-work-items\">\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {items.length} work item{items.length === 1 ? '' : 's'}\n </p>\n {!finished && <AddWorkItemsDialog sprintId={sprintId} projectId={projectId} />}\n </div>\n {finished && (\n <p className=\"text-xs text-muted-foreground\">\n This sprint is {statusLabel(status)}; its work items can no longer be changed.\n </p>\n )}\n {error && <ErrorLine message={error} />}\n {items.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n No work items in this sprint yet.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {items.map((item) => (\n <li\n key={item.id}\n className=\"flex items-center justify-between gap-2 border rounded-md p-2 text-sm\"\n >\n <span className=\"min-w-0 truncate\">\n <span className=\"text-muted-foreground mr-2\">{item.key}</span>\n {item.title}\n </span>\n <span className=\"flex items-center gap-2 shrink-0\">\n <Badge variant=\"secondary\" className=\"text-xs capitalize\">\n {statusLabel(item.status)}\n </Badge>\n <span className=\"text-xs text-muted-foreground\">{item.storyPoints ?? 0} pts</span>\n {!finished && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Remove ${item.key} from sprint`}\n disabled={removeState.loading}\n onClick={() => handleRemove(item.id)}\n >\n {removingId === item.id ? '…' : <X className=\"h-4 w-4\" />}\n </Button>\n )}\n </span>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nfunction AddWorkItemsDialog({ sprintId, projectId }: { sprintId: string; projectId: string }) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState('');\n const deferredSearch = useDeferredValue(search.trim());\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [error, setError] = useState<string | null>(null);\n\n const { data, loading } = useQuery<{\n planmagnetWorkItems: { totalCount: number; items: CandidateItem[] };\n }>(PROJECT_WORK_ITEMS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: { projectId, ...(deferredSearch ? { search: deferredSearch } : {}) },\n },\n skip: !open || !workspaceId,\n fetchPolicy: 'network-only',\n });\n const [add, addState] = useMutation(ADD_ITEMS, {\n refetchQueries: SPRINT_QUERIES,\n awaitRefetchQueries: true,\n });\n\n const candidates = (data?.planmagnetWorkItems.items ?? []).filter(\n (item) => item.sprintId !== sprintId\n );\n const totalCount = data?.planmagnetWorkItems.totalCount ?? 0;\n\n const toggle = (id: string, checked: boolean) =>\n setSelected((prev) => {\n const next = new Set(prev);\n if (checked) next.add(id);\n else next.delete(id);\n return next;\n });\n\n const reset = () => {\n setSelected(new Set());\n setSearch('');\n setError(null);\n };\n\n const handleAdd = async () => {\n setError(null);\n try {\n await throwOnError(\n add({ variables: { id: sprintId, input: { workItemIds: [...selected] } } })\n );\n reset();\n setOpen(false);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The work items could not be added.');\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) reset();\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\">\n <Plus className=\"mr-1 h-4 w-4\" />\n Add work items\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add work items</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Work items from this sprint&apos;s project. An item already in another sprint moves\n here.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <Input\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n placeholder=\"Search by title\"\n aria-label=\"Search work items\"\n />\n <div className=\"max-h-80 overflow-y-auto space-y-1\" data-testid=\"sprint-add-candidates\">\n {loading && <p className=\"text-sm text-muted-foreground py-4 text-center\">Loading…</p>}\n {!loading && candidates.length === 0 && (\n <p className=\"text-sm text-muted-foreground py-4 text-center\">\n {deferredSearch\n ? 'No matching work items.'\n : 'Every work item in this project is already in this sprint.'}\n </p>\n )}\n {candidates.map((item) => {\n const inputId = `sprint-add-${item.id}`;\n return (\n <label\n key={item.id}\n htmlFor={inputId}\n className=\"flex items-center gap-3 border rounded-md p-2 text-sm cursor-pointer hover:bg-muted\"\n >\n <Checkbox\n id={inputId}\n checked={selected.has(item.id)}\n onCheckedChange={(checked) => toggle(item.id, checked === true)}\n />\n <span className=\"min-w-0 flex-1 truncate\">\n <span className=\"text-muted-foreground mr-2\">{item.key}</span>\n {item.title}\n </span>\n {item.sprintId && (\n <span className=\"text-xs text-muted-foreground shrink-0\">in another sprint</span>\n )}\n <Badge variant=\"secondary\" className=\"text-xs capitalize shrink-0\">\n {statusLabel(item.status)}\n </Badge>\n </label>\n );\n })}\n {totalCount > 100 && (\n <p className=\"text-xs text-muted-foreground pt-2\">\n Showing the first 100 of {totalCount}. Search to narrow the list.\n </p>\n )}\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter>\n <Button\n onClick={handleAdd}\n disabled={selected.size === 0 || addState.loading}\n data-testid=\"sprint-add-confirm\"\n >\n {addState.loading\n ? 'Adding…'\n : `Add ${selected.size} work item${selected.size === 1 ? '' : 's'}`}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ─── New sprint ─────────────────────────────────────────────────────────────\n\nconst CREATE_SPRINT = gql`\n mutation SprintControlsCreate($input: PlanmagnetCreateSprintInput!) {\n planmagnetCreateSprint(input: $input) {\n id\n }\n }\n`;\nconst SPRINT_PROJECTS = gql`\n query SprintControlsProjects($workspaceId: ID!) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n }\n }\n`;\n\nconst EMPTY_FORM = { name: '', projectId: '', startDate: '', endDate: '', goal: '', capacity: '' };\n\nexport function NewSprintDialog({ onCreated }: { onCreated: (sprintId: string) => void }) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [form, setForm] = useState(EMPTY_FORM);\n const [error, setError] = useState<string | null>(null);\n\n const { data: projectsData } = useQuery<{\n planmagnetProjects: { items: Array<{ id: string; name: string }> };\n }>(SPRINT_PROJECTS, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !open || !workspaceId,\n });\n const projects = projectsData?.planmagnetProjects.items ?? [];\n const projectId = form.projectId || projects[0]?.id || '';\n\n const [create, { loading }] = useMutation<{ planmagnetCreateSprint: { id: string } }>(\n CREATE_SPRINT,\n { refetchQueries: SPRINT_QUERIES, awaitRefetchQueries: true }\n );\n\n const capacity = form.capacity.trim() === '' ? undefined : Number(form.capacity);\n const invalidCapacity = capacity !== undefined && (!Number.isInteger(capacity) || capacity < 0);\n const invalidDates = Boolean(form.startDate && form.endDate && form.endDate < form.startDate);\n const canSubmit =\n form.name.trim() !== '' && projectId !== '' && !invalidCapacity && !invalidDates && !loading;\n\n const handleCreate = async () => {\n setError(null);\n try {\n const result = await throwOnError(\n create({\n variables: {\n input: {\n name: form.name.trim(),\n projectId,\n startDate: form.startDate || undefined,\n endDate: form.endDate || undefined,\n goal: form.goal.trim() || undefined,\n capacity,\n },\n },\n })\n );\n const id = result.data?.planmagnetCreateSprint.id;\n setForm(EMPTY_FORM);\n setOpen(false);\n if (id) onCreated(id);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The sprint could not be created.');\n }\n };\n\n const field = 'mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm';\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) setError(null);\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\">\n <Plus className=\"mr-1 h-4 w-4\" />\n New sprint\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New sprint</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n The sprint starts in Planning. Start it when it is ready.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3\">\n <div>\n <label htmlFor=\"new-sprint-name\" className=\"text-sm font-medium\">\n Name\n </label>\n <Input\n id=\"new-sprint-name\"\n className=\"mt-1\"\n value={form.name}\n onChange={(e) => setForm({ ...form, name: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"new-sprint-project\" className=\"text-sm font-medium\">\n Project\n </label>\n <select\n id=\"new-sprint-project\"\n className={field}\n value={projectId}\n onChange={(e) => setForm({ ...form, projectId: e.target.value })}\n >\n {projects.length === 0 && <option value=\"\">No projects</option>}\n {projects.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor=\"new-sprint-start\" className=\"text-sm font-medium\">\n Start date\n </label>\n <Input\n id=\"new-sprint-start\"\n type=\"date\"\n className=\"mt-1\"\n value={form.startDate}\n onChange={(e) => setForm({ ...form, startDate: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"new-sprint-end\" className=\"text-sm font-medium\">\n End date\n </label>\n <Input\n id=\"new-sprint-end\"\n type=\"date\"\n className=\"mt-1\"\n value={form.endDate}\n onChange={(e) => setForm({ ...form, endDate: e.target.value })}\n />\n </div>\n </div>\n {invalidDates && <ErrorLine message=\"The end date is before the start date.\" />}\n <div>\n <label htmlFor=\"new-sprint-capacity\" className=\"text-sm font-medium\">\n Capacity (story points)\n </label>\n <Input\n id=\"new-sprint-capacity\"\n type=\"number\"\n min={0}\n className=\"mt-1\"\n value={form.capacity}\n onChange={(e) => setForm({ ...form, capacity: e.target.value })}\n />\n </div>\n {invalidCapacity && <ErrorLine message=\"Capacity must be a whole number, 0 or more.\" />}\n <div>\n <label htmlFor=\"new-sprint-goal\" className=\"text-sm font-medium\">\n Goal\n </label>\n <Input\n id=\"new-sprint-goal\"\n className=\"mt-1\"\n value={form.goal}\n onChange={(e) => setForm({ ...form, goal: e.target.value })}\n />\n </div>\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter>\n <Button onClick={handleCreate} disabled={!canSubmit} data-testid=\"new-sprint-submit\">\n {loading ? 'Creating…' : 'Create sprint'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"SprintControls.js","names":[],"sources":["../../../src/planmagnet/sprints/SprintControls.tsx"],"sourcesContent":["import { useDeferredValue, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogTrigger,\n Badge,\n Button,\n Checkbox,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n Input,\n} from '@burdenoff/fe-libs/ui';\nimport { CheckCircle2, Play, Plus, X, XCircle } from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { throwOnError } from '../utils/mutationResult';\n\n/**\n * Sprint lifecycle + membership controls.\n *\n * The buttons mirror the service's transition table exactly\n * (wspace-planmagnet-svc sprint.service.ts `TRANSITIONS`):\n * start PLANNING -> ACTIVE\n * complete ACTIVE | REVIEW -> COMPLETED\n * cancel PLANNING|ACTIVE|REVIEW -> CANCELLED\n * A COMPLETED or CANCELLED sprint is finished: no transitions, and its work\n * items can no longer be added or removed. The server enforces all of this;\n * the UI only hides what would be refused, and still surfaces a refusal (a\n * concurrent change can make a shown button stale).\n */\n\nexport type SprintStatus = 'PLANNING' | 'ACTIVE' | 'REVIEW' | 'COMPLETED' | 'CANCELLED';\nexport type SprintAction = 'start' | 'complete' | 'cancel';\n\nconst ALLOWED: Record<SprintAction, readonly SprintStatus[]> = {\n start: ['PLANNING'],\n complete: ['ACTIVE', 'REVIEW'],\n cancel: ['PLANNING', 'ACTIVE', 'REVIEW'],\n};\n\nexport function sprintActionsFor(status: string): SprintAction[] {\n return (Object.keys(ALLOWED) as SprintAction[]).filter((action) =>\n (ALLOWED[action] as readonly string[]).includes(status)\n );\n}\n\nexport function isSprintFinished(status: string): boolean {\n return status === 'COMPLETED' || status === 'CANCELLED';\n}\n\n/** Every query that renders sprint status or membership. */\nconst SPRINT_QUERIES = ['GetSprints', 'GetSprintDetail', 'GetSprintForEdit', 'GetDashboardData'];\n\nconst START_SPRINT = gql`\n mutation SprintControlsStart($id: ID!) {\n planmagnetStartSprint(id: $id) {\n id\n status\n }\n }\n`;\nconst COMPLETE_SPRINT = gql`\n mutation SprintControlsComplete($id: ID!) {\n planmagnetCompleteSprint(id: $id) {\n id\n status\n }\n }\n`;\nconst CANCEL_SPRINT = gql`\n mutation SprintControlsCancel($id: ID!) {\n planmagnetCancelSprint(id: $id) {\n id\n status\n }\n }\n`;\n\nfunction ErrorLine({ message }: { message: string }) {\n return (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {message}\n </p>\n );\n}\n\nexport function SprintLifecycleActions({\n sprintId,\n sprintName,\n status,\n}: {\n sprintId: string;\n sprintName: string;\n status: string;\n}) {\n const options = { refetchQueries: SPRINT_QUERIES, awaitRefetchQueries: true };\n const [start, startState] = useMutation(START_SPRINT, options);\n const [complete, completeState] = useMutation(COMPLETE_SPRINT, options);\n const [cancel, cancelState] = useMutation(CANCEL_SPRINT, options);\n const [error, setError] = useState<string | null>(null);\n\n const busy = startState.loading || completeState.loading || cancelState.loading;\n const actions = sprintActionsFor(status);\n // A refused transition still refetches; if the sprint turned out to be\n // finished there are no buttons left, but the reason must stay visible.\n if (actions.length === 0) return error ? <ErrorLine message={error} /> : null;\n\n const run = async (mutate: typeof start) => {\n setError(null);\n try {\n await throwOnError(mutate({ variables: { id: sprintId } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The sprint could not be updated.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\" data-testid=\"sprint-lifecycle-actions\">\n <div className=\"flex gap-2\">\n {actions.includes('start') && (\n <Button size=\"sm\" onClick={() => run(start)} disabled={busy}>\n <Play className=\"mr-1 h-4 w-4\" />\n Start sprint\n </Button>\n )}\n {actions.includes('complete') && (\n <ConfirmButton\n label=\"Complete sprint\"\n icon={<CheckCircle2 className=\"mr-1 h-4 w-4\" />}\n title={`Complete ${sprintName}?`}\n description=\"The sprint is closed and its work items can no longer be changed from it. This cannot be undone.\"\n confirmLabel=\"Complete sprint\"\n disabled={busy}\n onConfirm={() => run(complete)}\n />\n )}\n {actions.includes('cancel') && (\n <ConfirmButton\n label=\"Cancel sprint\"\n variant=\"outline\"\n icon={<XCircle className=\"mr-1 h-4 w-4\" />}\n title={`Cancel ${sprintName}?`}\n description=\"The sprint is abandoned and cannot be restarted. Its work items stay in it for the record. This cannot be undone.\"\n confirmLabel=\"Cancel sprint\"\n destructive\n disabled={busy}\n onConfirm={() => run(cancel)}\n />\n )}\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\nfunction ConfirmButton({\n label,\n icon,\n title,\n description,\n confirmLabel,\n onConfirm,\n disabled,\n destructive = false,\n variant = 'default',\n}: {\n label: string;\n icon: React.ReactNode;\n title: string;\n description: string;\n confirmLabel: string;\n onConfirm: () => void;\n disabled: boolean;\n destructive?: boolean;\n variant?: 'default' | 'outline';\n}) {\n return (\n <AlertDialog>\n <AlertDialogTrigger asChild>\n <Button size=\"sm\" variant={variant} disabled={disabled}>\n {icon}\n {label}\n </Button>\n </AlertDialogTrigger>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>{title}</AlertDialogTitle>\n <AlertDialogDescription>{description}</AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Keep it</AlertDialogCancel>\n <AlertDialogAction\n onClick={onConfirm}\n className={\n destructive\n ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'\n : ''\n }\n >\n {confirmLabel}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n\n// ─── Work items in the sprint ──────────────────────────────────────────────\n\nconst ADD_ITEMS = gql`\n mutation SprintControlsAddItems($id: ID!, $input: PlanmagnetSprintWorkItemsInput!) {\n planmagnetAddWorkItemsToSprint(id: $id, input: $input) {\n id\n }\n }\n`;\nconst REMOVE_ITEMS = gql`\n mutation SprintControlsRemoveItems($id: ID!, $input: PlanmagnetSprintWorkItemsInput!) {\n planmagnetRemoveWorkItemsFromSprint(id: $id, input: $input) {\n id\n }\n }\n`;\nconst PROJECT_WORK_ITEMS = gql`\n query SprintControlsCandidates($workspaceId: ID!, $filter: PlanmagnetWorkItemFilter) {\n planmagnetWorkItems(workspaceId: $workspaceId, filter: $filter, pagination: { first: 100 }) {\n totalCount\n items {\n id\n key\n title\n status\n storyPoints\n sprintId\n }\n }\n }\n`;\n\nexport interface SprintWorkItem {\n id: string;\n key: string;\n title?: string | null;\n status: string;\n storyPoints: number | null;\n}\n\ninterface CandidateItem extends SprintWorkItem {\n sprintId: string | null;\n}\n\nconst statusLabel = (status: string) => status.toLowerCase().replace(/_/g, ' ');\n\nexport function SprintWorkItemsPanel({\n sprintId,\n projectId,\n status,\n items,\n}: {\n sprintId: string;\n projectId: string;\n status: string;\n items: SprintWorkItem[];\n}) {\n const finished = isSprintFinished(status);\n const [remove, removeState] = useMutation(REMOVE_ITEMS, {\n refetchQueries: SPRINT_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const [removingId, setRemovingId] = useState<string | null>(null);\n\n const handleRemove = async (workItemId: string) => {\n setError(null);\n setRemovingId(workItemId);\n try {\n await throwOnError(\n remove({ variables: { id: sprintId, input: { workItemIds: [workItemId] } } })\n );\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The work item could not be removed.');\n } finally {\n setRemovingId(null);\n }\n };\n\n return (\n <div className=\"space-y-3\" data-testid=\"sprint-work-items\">\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {items.length} work item{items.length === 1 ? '' : 's'}\n </p>\n {!finished && <AddWorkItemsDialog sprintId={sprintId} projectId={projectId} />}\n </div>\n {finished && (\n <p className=\"text-xs text-muted-foreground\">\n This sprint is {statusLabel(status)}; its work items can no longer be changed.\n </p>\n )}\n {error && <ErrorLine message={error} />}\n {items.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n No work items in this sprint yet.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {items.map((item) => (\n <li\n key={item.id}\n className=\"flex items-center justify-between gap-2 border rounded-md p-2 text-sm\"\n >\n <span className=\"min-w-0 truncate\">\n <span className=\"text-muted-foreground mr-2\">{item.key}</span>\n {item.title}\n </span>\n <span className=\"flex items-center gap-2 shrink-0\">\n <Badge variant=\"secondary\" className=\"text-xs capitalize\">\n {statusLabel(item.status)}\n </Badge>\n <span className=\"text-xs text-muted-foreground\">{item.storyPoints ?? 0} pts</span>\n {!finished && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Remove ${item.key} from sprint`}\n disabled={removeState.loading}\n onClick={() => handleRemove(item.id)}\n >\n {removingId === item.id ? '…' : <X className=\"h-4 w-4\" />}\n </Button>\n )}\n </span>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nfunction AddWorkItemsDialog({ sprintId, projectId }: { sprintId: string; projectId: string }) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState('');\n const deferredSearch = useDeferredValue(search.trim());\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [error, setError] = useState<string | null>(null);\n\n const { data, loading } = useQuery<{\n planmagnetWorkItems: { totalCount: number; items: CandidateItem[] };\n }>(PROJECT_WORK_ITEMS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: { projectId, ...(deferredSearch ? { search: deferredSearch } : {}) },\n },\n skip: !open || !workspaceId,\n fetchPolicy: 'network-only',\n });\n const [add, addState] = useMutation(ADD_ITEMS, {\n refetchQueries: SPRINT_QUERIES,\n awaitRefetchQueries: true,\n });\n\n const candidates = (data?.planmagnetWorkItems.items ?? []).filter(\n (item) => item.sprintId !== sprintId\n );\n const totalCount = data?.planmagnetWorkItems.totalCount ?? 0;\n\n const toggle = (id: string, checked: boolean) =>\n setSelected((prev) => {\n const next = new Set(prev);\n if (checked) next.add(id);\n else next.delete(id);\n return next;\n });\n\n const reset = () => {\n setSelected(new Set());\n setSearch('');\n setError(null);\n };\n\n const handleAdd = async () => {\n setError(null);\n try {\n await throwOnError(\n add({ variables: { id: sprintId, input: { workItemIds: [...selected] } } })\n );\n reset();\n setOpen(false);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The work items could not be added.');\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) reset();\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\">\n <Plus className=\"mr-1 h-4 w-4\" />\n Add work items\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add work items</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Work items from this sprint&apos;s project. An item already in another sprint moves\n here.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <Input\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n placeholder=\"Search by title\"\n aria-label=\"Search work items\"\n />\n <div className=\"max-h-80 overflow-y-auto space-y-1\" data-testid=\"sprint-add-candidates\">\n {loading && <p className=\"text-sm text-muted-foreground py-4 text-center\">Loading…</p>}\n {!loading && candidates.length === 0 && (\n <p className=\"text-sm text-muted-foreground py-4 text-center\">\n {deferredSearch\n ? 'No matching work items.'\n : 'Every work item in this project is already in this sprint.'}\n </p>\n )}\n {candidates.map((item) => {\n const inputId = `sprint-add-${item.id}`;\n return (\n <label\n key={item.id}\n htmlFor={inputId}\n className=\"flex items-center gap-3 border rounded-md p-2 text-sm cursor-pointer hover:bg-muted\"\n >\n <Checkbox\n id={inputId}\n checked={selected.has(item.id)}\n onCheckedChange={(checked) => toggle(item.id, checked === true)}\n />\n <span className=\"min-w-0 flex-1 truncate\">\n <span className=\"text-muted-foreground mr-2\">{item.key}</span>\n {item.title}\n </span>\n {item.sprintId && (\n <span className=\"text-xs text-muted-foreground shrink-0\">in another sprint</span>\n )}\n <Badge variant=\"secondary\" className=\"text-xs capitalize shrink-0\">\n {statusLabel(item.status)}\n </Badge>\n </label>\n );\n })}\n {totalCount > 100 && (\n <p className=\"text-xs text-muted-foreground pt-2\">\n Showing the first 100 of {totalCount}. Search to narrow the list.\n </p>\n )}\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter>\n <Button\n onClick={handleAdd}\n disabled={selected.size === 0 || addState.loading}\n data-testid=\"sprint-add-confirm\"\n >\n {addState.loading\n ? 'Adding…'\n : `Add ${selected.size} work item${selected.size === 1 ? '' : 's'}`}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ─── New sprint ─────────────────────────────────────────────────────────────\n\nconst CREATE_SPRINT = gql`\n mutation SprintControlsCreate($input: PlanmagnetCreateSprintInput!) {\n planmagnetCreateSprint(input: $input) {\n id\n }\n }\n`;\nconst SPRINT_PROJECTS = gql`\n query SprintControlsProjects($workspaceId: ID!) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n }\n }\n`;\n\nconst EMPTY_FORM = { name: '', projectId: '', startDate: '', endDate: '', goal: '', capacity: '' };\n\nexport function NewSprintDialog({ onCreated }: { onCreated: (sprintId: string) => void }) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [form, setForm] = useState(EMPTY_FORM);\n const [error, setError] = useState<string | null>(null);\n\n const {\n data: projectsData,\n loading: projectsLoading,\n error: projectsError,\n refetch: refetchProjects,\n } = useQuery<{\n planmagnetProjects: { items: Array<{ id: string; name: string }> };\n }>(SPRINT_PROJECTS, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !open || !workspaceId,\n });\n const projects = projectsData?.planmagnetProjects.items ?? [];\n const projectId = form.projectId || projects[0]?.id || '';\n\n const [create, { loading }] = useMutation<{ planmagnetCreateSprint: { id: string } }>(\n CREATE_SPRINT,\n { refetchQueries: SPRINT_QUERIES, awaitRefetchQueries: true }\n );\n\n const capacity = form.capacity.trim() === '' ? undefined : Number(form.capacity);\n const invalidCapacity = capacity !== undefined && (!Number.isInteger(capacity) || capacity < 0);\n const invalidDates = Boolean(form.startDate && form.endDate && form.endDate < form.startDate);\n const canSubmit =\n form.name.trim() !== '' && projectId !== '' && !invalidCapacity && !invalidDates && !loading;\n\n const handleCreate = async () => {\n setError(null);\n try {\n const result = await throwOnError(\n create({\n variables: {\n input: {\n name: form.name.trim(),\n projectId,\n startDate: form.startDate || undefined,\n endDate: form.endDate || undefined,\n goal: form.goal.trim() || undefined,\n capacity,\n },\n },\n })\n );\n const id = result.data?.planmagnetCreateSprint.id;\n setForm(EMPTY_FORM);\n setOpen(false);\n if (id) onCreated(id);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The sprint could not be created.');\n }\n };\n\n const field = 'mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm';\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) setError(null);\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\">\n <Plus className=\"mr-1 h-4 w-4\" />\n New sprint\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New sprint</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n The sprint starts in Planning. Start it when it is ready.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3\">\n <div>\n <label htmlFor=\"new-sprint-name\" className=\"text-sm font-medium\">\n Name\n </label>\n <Input\n id=\"new-sprint-name\"\n className=\"mt-1\"\n value={form.name}\n onChange={(e) => setForm({ ...form, name: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"new-sprint-project\" className=\"text-sm font-medium\">\n Project\n </label>\n <select\n id=\"new-sprint-project\"\n className={field}\n value={projectId}\n onChange={(e) => setForm({ ...form, projectId: e.target.value })}\n >\n {projects.length === 0 && (\n <option value=\"\">\n {projectsLoading && !projectsData\n ? 'Loading projects…'\n : projectsError\n ? 'Projects could not be loaded'\n : 'No projects yet'}\n </option>\n )}\n {projects.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n {/* C2 (BOFF-7833): loading, failure and \"no projects\" are told apart. */}\n {projectsError && (\n <p role=\"alert\" className=\"mt-1 flex items-center gap-2 text-xs text-destructive\">\n {projectsError.message}\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n onClick={() => void refetchProjects()}\n >\n Retry\n </Button>\n </p>\n )}\n {!projectsLoading && !projectsError && projectsData && projects.length === 0 && (\n <p className=\"mt-1 text-xs text-muted-foreground\">\n Create a project first (Projects → New project).\n </p>\n )}\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor=\"new-sprint-start\" className=\"text-sm font-medium\">\n Start date\n </label>\n <Input\n id=\"new-sprint-start\"\n type=\"date\"\n className=\"mt-1\"\n value={form.startDate}\n onChange={(e) => setForm({ ...form, startDate: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"new-sprint-end\" className=\"text-sm font-medium\">\n End date\n </label>\n <Input\n id=\"new-sprint-end\"\n type=\"date\"\n className=\"mt-1\"\n value={form.endDate}\n onChange={(e) => setForm({ ...form, endDate: e.target.value })}\n />\n </div>\n </div>\n {invalidDates && <ErrorLine message=\"The end date is before the start date.\" />}\n <div>\n <label htmlFor=\"new-sprint-capacity\" className=\"text-sm font-medium\">\n Capacity (story points)\n </label>\n <Input\n id=\"new-sprint-capacity\"\n type=\"number\"\n min={0}\n className=\"mt-1\"\n value={form.capacity}\n onChange={(e) => setForm({ ...form, capacity: e.target.value })}\n />\n </div>\n {invalidCapacity && <ErrorLine message=\"Capacity must be a whole number, 0 or more.\" />}\n <div>\n <label htmlFor=\"new-sprint-goal\" className=\"text-sm font-medium\">\n Goal\n </label>\n <Input\n id=\"new-sprint-goal\"\n className=\"mt-1\"\n value={form.goal}\n onChange={(e) => setForm({ ...form, goal: e.target.value })}\n />\n </div>\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter>\n <Button onClick={handleCreate} disabled={!canSubmit} data-testid=\"new-sprint-submit\">\n {loading ? 'Creating…' : 'Create sprint'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The invite form's email check (H12, BOFF-7833). It mirrors wspace-workspace-svc
3
+ * InvitationManagementService.isValidEmailAddress exactly — that backend check is the
4
+ * real gate (createInvitation refuses a malformed address with "Invalid email format");
5
+ * this copy only lets the form say so before sending, and must never refuse an address
6
+ * the backend would accept.
7
+ */
8
+ export declare function isValidInviteEmail(raw: string): boolean;
9
+ //# sourceMappingURL=email.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/utils/email.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CASvD"}
@@ -0,0 +1,15 @@
1
+ //#region src/planmagnet/utils/email.ts
2
+ function e(e) {
3
+ let t = e.trim();
4
+ if (!t || t.length > 320 || t.includes(" ")) return !1;
5
+ let n = t.indexOf("@");
6
+ if (n <= 0 || n !== t.lastIndexOf("@")) return !1;
7
+ let r = t.slice(n + 1);
8
+ if (!r || r.startsWith(".") || r.endsWith(".")) return !1;
9
+ let i = r.split(".");
10
+ return i.length >= 2 && i.every((e) => e.length > 0);
11
+ }
12
+ //#endregion
13
+ export { e as isValidInviteEmail };
14
+
15
+ //# sourceMappingURL=email.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.js","names":[],"sources":["../../../src/planmagnet/utils/email.ts"],"sourcesContent":["/**\n * The invite form's email check (H12, BOFF-7833). It mirrors wspace-workspace-svc\n * InvitationManagementService.isValidEmailAddress exactly — that backend check is the\n * real gate (createInvitation refuses a malformed address with \"Invalid email format\");\n * this copy only lets the form say so before sending, and must never refuse an address\n * the backend would accept.\n */\nexport function isValidInviteEmail(raw: string): boolean {\n const value = raw.trim();\n if (!value || value.length > 320 || value.includes(' ')) return false;\n const at = value.indexOf('@');\n if (at <= 0 || at !== value.lastIndexOf('@')) return false;\n const domain = value.slice(at + 1);\n if (!domain || domain.startsWith('.') || domain.endsWith('.')) return false;\n const labels = domain.split('.');\n return labels.length >= 2 && labels.every((label) => label.length > 0);\n}\n"],"mappings":";AAOA,SAAgB,EAAmB,GAAsB;CACvD,IAAM,IAAQ,EAAI,MAAM;AACxB,KAAI,CAAC,KAAS,EAAM,SAAS,OAAO,EAAM,SAAS,IAAI,CAAE,QAAO;CAChE,IAAM,IAAK,EAAM,QAAQ,IAAI;AAC7B,KAAI,KAAM,KAAK,MAAO,EAAM,YAAY,IAAI,CAAE,QAAO;CACrD,IAAM,IAAS,EAAM,MAAM,IAAK,EAAE;AAClC,KAAI,CAAC,KAAU,EAAO,WAAW,IAAI,IAAI,EAAO,SAAS,IAAI,CAAE,QAAO;CACtE,IAAM,IAAS,EAAO,MAAM,IAAI;AAChC,QAAO,EAAO,UAAU,KAAK,EAAO,OAAO,MAAU,EAAM,SAAS,EAAE"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Form-side checks (BOFF-7833). The service is the real gate — every rule here is also enforced there, and a
3
+ * refusal comes back with its reason — these only let a form say so before sending.
4
+ */
5
+ /** True when both dates are set and the end (YYYY-MM-DD) is before the start. Equal days are allowed. */
6
+ export declare function endBeforeStart(start: string | null | undefined, end: string | null | undefined): boolean;
7
+ /**
8
+ * Parse a whole-number field. Empty is "not set" (valid). Anything that is not a whole number inside
9
+ * [min, max] is invalid.
10
+ */
11
+ export declare function parseIntField(raw: string, opts: {
12
+ min: number;
13
+ max?: number;
14
+ }): {
15
+ value?: number;
16
+ invalid: boolean;
17
+ };
18
+ //# sourceMappingURL=formValidation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formValidation.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/utils/formValidation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,yGAAyG;AACzG,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC7B,OAAO,CAGT;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAOtC"}
@@ -0,0 +1,17 @@
1
+ //#region src/planmagnet/utils/formValidation.ts
2
+ function e(e, t) {
3
+ return !e || !t ? !1 : t.slice(0, 10) < e.slice(0, 10);
4
+ }
5
+ function t(e, t) {
6
+ let n = e.trim();
7
+ if (n === "") return { invalid: !1 };
8
+ let r = Number(n);
9
+ return Number.isInteger(r) && r >= t.min && (t.max === void 0 || r <= t.max) ? {
10
+ value: r,
11
+ invalid: !1
12
+ } : { invalid: !0 };
13
+ }
14
+ //#endregion
15
+ export { e as endBeforeStart, t as parseIntField };
16
+
17
+ //# sourceMappingURL=formValidation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formValidation.js","names":[],"sources":["../../../src/planmagnet/utils/formValidation.ts"],"sourcesContent":["/**\n * Form-side checks (BOFF-7833). The service is the real gate — every rule here is also enforced there, and a\n * refusal comes back with its reason — these only let a form say so before sending.\n */\n\n/** True when both dates are set and the end (YYYY-MM-DD) is before the start. Equal days are allowed. */\nexport function endBeforeStart(\n start: string | null | undefined,\n end: string | null | undefined\n): boolean {\n if (!start || !end) return false;\n return end.slice(0, 10) < start.slice(0, 10);\n}\n\n/**\n * Parse a whole-number field. Empty is \"not set\" (valid). Anything that is not a whole number inside\n * [min, max] is invalid.\n */\nexport function parseIntField(\n raw: string,\n opts: { min: number; max?: number }\n): { value?: number; invalid: boolean } {\n const trimmed = raw.trim();\n if (trimmed === '') return { invalid: false };\n const value = Number(trimmed);\n const ok =\n Number.isInteger(value) && value >= opts.min && (opts.max === undefined || value <= opts.max);\n return ok ? { value, invalid: false } : { invalid: true };\n}\n"],"mappings":";AAMA,SAAgB,EACd,GACA,GACS;AAET,QADI,CAAC,KAAS,CAAC,IAAY,KACpB,EAAI,MAAM,GAAG,GAAG,GAAG,EAAM,MAAM,GAAG,GAAG;;AAO9C,SAAgB,EACd,GACA,GACsC;CACtC,IAAM,IAAU,EAAI,MAAM;AAC1B,KAAI,MAAY,GAAI,QAAO,EAAE,SAAS,IAAO;CAC7C,IAAM,IAAQ,OAAO,EAAQ;AAG7B,QADE,OAAO,UAAU,EAAM,IAAI,KAAS,EAAK,QAAQ,EAAK,QAAQ,KAAA,KAAa,KAAS,EAAK,OAC/E;EAAE;EAAO,SAAS;EAAO,GAAG,EAAE,SAAS,IAAM"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-planmagnet",
3
- "version": "2026.926.1",
3
+ "version": "2026.926.2",
4
4
  "description": "Burdenoff PlanMagnet Microfrontend",
5
5
  "type": "module",
6
6
  "files": [