@burdenoff/microfe-planmagnet 2026.1001.4 → 2026.1001.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/planmagnet/controls/CompetitorControls.d.ts +71 -0
- package/dist/planmagnet/controls/CompetitorControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/CompetitorControls.js +346 -12
- package/dist/planmagnet/controls/CompetitorControls.js.map +1 -1
- package/dist/planmagnet/controls/FeatureControls.d.ts +5 -0
- package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/FeatureControls.js +363 -218
- package/dist/planmagnet/controls/FeatureControls.js.map +1 -1
- package/dist/planmagnet/controls/Pickers.d.ts +15 -0
- package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
- package/dist/planmagnet/controls/Pickers.js +61 -1
- package/dist/planmagnet/controls/Pickers.js.map +1 -1
- package/dist/planmagnet/controls/PrioritizationControls.d.ts +36 -0
- package/dist/planmagnet/controls/PrioritizationControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/PrioritizationControls.js +118 -1
- package/dist/planmagnet/controls/PrioritizationControls.js.map +1 -1
- package/dist/planmagnet/dashboards/DashboardUI.js +1 -1
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.d.ts.map +1 -1
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js +67 -59
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js.map +1 -1
- package/dist/planmagnet/meetings/ScheduleMeetingDialog.d.ts.map +1 -1
- package/dist/planmagnet/meetings/ScheduleMeetingDialog.js +1 -1
- package/dist/planmagnet/meetings/ScheduleMeetingDialog.js.map +1 -1
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +289 -282
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.js +126 -63
- package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.js +14 -0
- package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.js +93 -57
- package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.js.map +1 -1
- package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.js +125 -83
- package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.js.map +1 -1
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js +226 -126
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +162 -122
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +135 -109
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +15 -3
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TeamTeamPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TeamTeamPagePage.js +59 -53
- package/dist/planmagnet/pages/TeamTeamPagePage.js.map +1 -1
- package/dist/planmagnet/pages/analytics/MetricsCard.d.ts +8 -0
- package/dist/planmagnet/pages/analytics/MetricsCard.d.ts.map +1 -0
- package/dist/planmagnet/pages/analytics/MetricsCard.js +164 -0
- package/dist/planmagnet/pages/analytics/MetricsCard.js.map +1 -0
- package/dist/planmagnet/team/UnmatchedCrewIdentities.d.ts +2 -0
- package/dist/planmagnet/team/UnmatchedCrewIdentities.d.ts.map +1 -0
- package/dist/planmagnet/team/UnmatchedCrewIdentities.js +155 -0
- package/dist/planmagnet/team/UnmatchedCrewIdentities.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PrioritizationControls.js","names":[],"sources":["../../../src/planmagnet/controls/PrioritizationControls.tsx"],"sourcesContent":["import { Badge, Input } from '@burdenoff/fe-libs/ui';\nimport { parseIntField } from '../utils/formValidation';\nimport { ErrorLine } from './ConfirmButton';\n\n/**\n * Prioritization (PRM-27): MoSCoW, Kano and Value-vs-Effort scoring shared by the feature request create/edit\n * form, the feature detail page, and the roadmap initiative (RoadmapItem) edit dialog — one presentational form\n * fragment plus the badge that shows the server-computed Value-vs-Effort quadrant, so the three surfaces (and the\n * prioritization page's own item cards) render it identically instead of three near-copies drifting apart.\n *\n * The quadrant itself is never computed client-side — `FeatureRequest.valueEffortQuadrant` /\n * `RoadmapItem.valueEffortQuadrant` are server-derived (and not stored), so `QuadrantBadge` only ever renders a\n * value that came back from a query or a mutation response.\n */\n\nexport const MOSCOW_VALUES = ['MUST', 'SHOULD', 'COULD', 'WONT'] as const;\nexport type MoscowValue = (typeof MOSCOW_VALUES)[number];\n\nexport const KANO_VALUES = ['BASIC', 'PERFORMANCE', 'DELIGHTER', 'INDIFFERENT', 'REVERSE'] as const;\nexport type KanoValue = (typeof KANO_VALUES)[number];\n\nexport const QUADRANT_VALUES = ['QUICK_WIN', 'MAJOR_PROJECT', 'FILL_IN', 'TIME_SINK'] as const;\nexport type QuadrantValue = (typeof QUADRANT_VALUES)[number];\n\ntype BadgeVariant =\n | 'default'\n | 'success'\n | 'warning'\n | 'info'\n | 'secondary'\n | 'destructive'\n | 'outline';\n\nexport const MOSCOW_META: Record<\n MoscowValue,\n { label: string; hint: string; variant: BadgeVariant }\n> = {\n MUST: {\n label: 'Must',\n hint: 'Non-negotiable — the release does not ship without it.',\n variant: 'default',\n },\n SHOULD: {\n label: 'Should',\n hint: 'Important, but the release can still ship without it.',\n variant: 'info',\n },\n COULD: { label: 'Could', hint: 'Nice to have if there is time.', variant: 'secondary' },\n WONT: { label: \"Won't\", hint: 'Explicitly out of scope this time around.', variant: 'outline' },\n};\n\nexport const KANO_META: Record<KanoValue, { label: string; hint: string; variant: BadgeVariant }> =\n {\n BASIC: {\n label: 'Basic',\n hint: 'Expected — its absence causes dissatisfaction, its presence goes unnoticed.',\n variant: 'secondary',\n },\n PERFORMANCE: {\n label: 'Performance',\n hint: 'Satisfaction scales with how well it is delivered.',\n variant: 'info',\n },\n DELIGHTER: {\n label: 'Delighter',\n hint: 'Unexpected — its absence is not missed, its presence delights.',\n variant: 'success',\n },\n INDIFFERENT: {\n label: 'Indifferent',\n hint: 'Users do not care either way.',\n variant: 'outline',\n },\n REVERSE: {\n label: 'Reverse',\n hint: 'Some users prefer this NOT be there.',\n variant: 'destructive',\n },\n };\n\nexport const QUADRANT_META: Record<\n QuadrantValue,\n { label: string; hint: string; variant: BadgeVariant }\n> = {\n QUICK_WIN: {\n label: 'Quick win',\n hint: 'High value, low effort (value ≥ 6, effort ≤ 5) — do these first.',\n variant: 'success',\n },\n MAJOR_PROJECT: {\n label: 'Major project',\n hint: 'High value, high effort (value ≥ 6, effort ≥ 6) — plan and staff deliberately.',\n variant: 'info',\n },\n FILL_IN: {\n label: 'Fill-in',\n hint: 'Low value, low effort (value ≤ 5, effort ≤ 5) — fine as filler work.',\n variant: 'secondary',\n },\n TIME_SINK: {\n label: 'Time sink',\n hint: 'Low value, high effort (value ≤ 5, effort ≥ 6) — avoid or deprioritize.',\n variant: 'destructive',\n },\n};\n\nexport const moscowLabel = (value: string | null | undefined): string =>\n value && value in MOSCOW_META ? MOSCOW_META[value as MoscowValue].label : 'Not set';\nexport const kanoLabel = (value: string | null | undefined): string =>\n value && value in KANO_META ? KANO_META[value as KanoValue].label : 'Not set';\nexport const quadrantLabel = (value: string | null | undefined): string =>\n value && value in QUADRANT_META ? QUADRANT_META[value as QuadrantValue].label : 'Unscored';\n\n/**\n * The Value-vs-Effort quadrant, read back from the server (`FeatureRequest.valueEffortQuadrant` /\n * `RoadmapItem.valueEffortQuadrant` / a `PlanmagnetPrioritizationBucket.bucket`) — never computed here. Renders\n * an \"Unscored\" badge when null (one or both of valueScore/effortScore is not set yet) unless `hideWhenUnscored`.\n */\nexport function QuadrantBadge({\n quadrant,\n hideWhenUnscored = false,\n className,\n}: {\n quadrant: string | null | undefined;\n hideWhenUnscored?: boolean;\n className?: string;\n}) {\n if (!quadrant) {\n if (hideWhenUnscored) return null;\n return (\n <Badge variant=\"outline\" className={className} data-testid=\"prio-quadrant-badge\">\n Unscored\n </Badge>\n );\n }\n const meta = quadrant in QUADRANT_META ? QUADRANT_META[quadrant as QuadrantValue] : null;\n return (\n <Badge\n variant={meta?.variant ?? 'outline'}\n className={className}\n data-testid=\"prio-quadrant-badge\"\n >\n {meta?.label ?? quadrant}\n </Badge>\n );\n}\n\nexport interface PrioritizationFormValues {\n /** '' = not set */\n moscow: string;\n /** '' = not set */\n kano: string;\n /** '' = not set, else a whole number 1-10 as text */\n valueScore: string;\n /** '' = not set, else a whole number 1-10 as text */\n effortScore: string;\n}\n\nexport const EMPTY_PRIORITIZATION_FORM: PrioritizationFormValues = {\n moscow: '',\n kano: '',\n valueScore: '',\n effortScore: '',\n};\n\nconst selectClass =\n 'mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60';\n\n/**\n * The four prioritization inputs (PRM-27): MoSCoW and Kano selects (each with a short explanation of the\n * selected value), and 1-10 value/effort number inputs. Fully controlled; clearing a select or number input back\n * to empty is how a caller sends `null` (cleared) rather than omitting the field. Shared by the feature request\n * create/edit form, the feature detail page, and the roadmap initiative edit dialog.\n */\nexport function PrioritizationScoreFields({\n idPrefix,\n values,\n onChange,\n disabled = false,\n}: {\n idPrefix: string;\n values: PrioritizationFormValues;\n onChange: (next: PrioritizationFormValues) => void;\n disabled?: boolean;\n}) {\n const value = parseIntField(values.valueScore, { min: 1, max: 10 });\n const effort = parseIntField(values.effortScore, { min: 1, max: 10 });\n\n return (\n <div className=\"space-y-3\" data-testid=\"prio-fields\">\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor={`${idPrefix}-moscow`} className=\"text-sm font-medium\">\n MoSCoW\n </label>\n <select\n id={`${idPrefix}-moscow`}\n data-testid=\"prio-field-moscow\"\n className={selectClass}\n value={values.moscow}\n disabled={disabled}\n onChange={(e) => onChange({ ...values, moscow: e.target.value })}\n >\n <option value=\"\">Not set</option>\n {MOSCOW_VALUES.map((v) => (\n <option key={v} value={v}>\n {MOSCOW_META[v].label}\n </option>\n ))}\n </select>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {values.moscow && values.moscow in MOSCOW_META\n ? MOSCOW_META[values.moscow as MoscowValue].hint\n : \"Must/Should/Could/Won't for this release.\"}\n </p>\n </div>\n <div>\n <label htmlFor={`${idPrefix}-kano`} className=\"text-sm font-medium\">\n Kano\n </label>\n <select\n id={`${idPrefix}-kano`}\n data-testid=\"prio-field-kano\"\n className={selectClass}\n value={values.kano}\n disabled={disabled}\n onChange={(e) => onChange({ ...values, kano: e.target.value })}\n >\n <option value=\"\">Not set</option>\n {KANO_VALUES.map((v) => (\n <option key={v} value={v}>\n {KANO_META[v].label}\n </option>\n ))}\n </select>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {values.kano && values.kano in KANO_META\n ? KANO_META[values.kano as KanoValue].hint\n : 'How this shapes customer satisfaction.'}\n </p>\n </div>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor={`${idPrefix}-value-score`} className=\"text-sm font-medium\">\n Value score (1-10)\n </label>\n <Input\n id={`${idPrefix}-value-score`}\n data-testid=\"prio-field-value-score\"\n type=\"number\"\n min={1}\n max={10}\n className=\"mt-1\"\n placeholder=\"Not set\"\n value={values.valueScore}\n aria-invalid={value.invalid}\n disabled={disabled}\n onChange={(e) => onChange({ ...values, valueScore: e.target.value })}\n />\n {value.invalid && <ErrorLine message=\"Value score is a whole number from 1 to 10.\" />}\n </div>\n <div>\n <label htmlFor={`${idPrefix}-effort-score`} className=\"text-sm font-medium\">\n Effort score (1-10)\n </label>\n <Input\n id={`${idPrefix}-effort-score`}\n data-testid=\"prio-field-effort-score\"\n type=\"number\"\n min={1}\n max={10}\n className=\"mt-1\"\n placeholder=\"Not set\"\n value={values.effortScore}\n aria-invalid={effort.invalid}\n disabled={disabled}\n onChange={(e) => onChange({ ...values, effortScore: e.target.value })}\n />\n {effort.invalid && <ErrorLine message=\"Effort score is a whole number from 1 to 10.\" />}\n </div>\n </div>\n </div>\n );\n}\n\n/** True when neither of the two score fields holds an out-of-range value (empty is always valid — it means \"clear\"). */\nexport function prioritizationScoresValid(values: PrioritizationFormValues): boolean {\n return (\n !parseIntField(values.valueScore, { min: 1, max: 10 }).invalid &&\n !parseIntField(values.effortScore, { min: 1, max: 10 }).invalid\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PrioritizationControls.js","names":[],"sources":["../../../src/planmagnet/controls/PrioritizationControls.tsx"],"sourcesContent":["import { Badge, Input } from '@burdenoff/fe-libs/ui';\nimport { parseIntField } from '../utils/formValidation';\nimport { ErrorLine } from './ConfirmButton';\n\n/**\n * Prioritization (PRM-27): MoSCoW, Kano and Value-vs-Effort scoring shared by the feature request create/edit\n * form, the feature detail page, and the roadmap initiative (RoadmapItem) edit dialog — one presentational form\n * fragment plus the badge that shows the server-computed Value-vs-Effort quadrant, so the three surfaces (and the\n * prioritization page's own item cards) render it identically instead of three near-copies drifting apart.\n *\n * The quadrant itself is never computed client-side — `FeatureRequest.valueEffortQuadrant` /\n * `RoadmapItem.valueEffortQuadrant` are server-derived (and not stored), so `QuadrantBadge` only ever renders a\n * value that came back from a query or a mutation response.\n */\n\nexport const MOSCOW_VALUES = ['MUST', 'SHOULD', 'COULD', 'WONT'] as const;\nexport type MoscowValue = (typeof MOSCOW_VALUES)[number];\n\nexport const KANO_VALUES = ['BASIC', 'PERFORMANCE', 'DELIGHTER', 'INDIFFERENT', 'REVERSE'] as const;\nexport type KanoValue = (typeof KANO_VALUES)[number];\n\nexport const QUADRANT_VALUES = ['QUICK_WIN', 'MAJOR_PROJECT', 'FILL_IN', 'TIME_SINK'] as const;\nexport type QuadrantValue = (typeof QUADRANT_VALUES)[number];\n\ntype BadgeVariant =\n | 'default'\n | 'success'\n | 'warning'\n | 'info'\n | 'secondary'\n | 'destructive'\n | 'outline';\n\nexport const MOSCOW_META: Record<\n MoscowValue,\n { label: string; hint: string; variant: BadgeVariant }\n> = {\n MUST: {\n label: 'Must',\n hint: 'Non-negotiable — the release does not ship without it.',\n variant: 'default',\n },\n SHOULD: {\n label: 'Should',\n hint: 'Important, but the release can still ship without it.',\n variant: 'info',\n },\n COULD: { label: 'Could', hint: 'Nice to have if there is time.', variant: 'secondary' },\n WONT: { label: \"Won't\", hint: 'Explicitly out of scope this time around.', variant: 'outline' },\n};\n\nexport const KANO_META: Record<KanoValue, { label: string; hint: string; variant: BadgeVariant }> =\n {\n BASIC: {\n label: 'Basic',\n hint: 'Expected — its absence causes dissatisfaction, its presence goes unnoticed.',\n variant: 'secondary',\n },\n PERFORMANCE: {\n label: 'Performance',\n hint: 'Satisfaction scales with how well it is delivered.',\n variant: 'info',\n },\n DELIGHTER: {\n label: 'Delighter',\n hint: 'Unexpected — its absence is not missed, its presence delights.',\n variant: 'success',\n },\n INDIFFERENT: {\n label: 'Indifferent',\n hint: 'Users do not care either way.',\n variant: 'outline',\n },\n REVERSE: {\n label: 'Reverse',\n hint: 'Some users prefer this NOT be there.',\n variant: 'destructive',\n },\n };\n\nexport const QUADRANT_META: Record<\n QuadrantValue,\n { label: string; hint: string; variant: BadgeVariant }\n> = {\n QUICK_WIN: {\n label: 'Quick win',\n hint: 'High value, low effort (value ≥ 6, effort ≤ 5) — do these first.',\n variant: 'success',\n },\n MAJOR_PROJECT: {\n label: 'Major project',\n hint: 'High value, high effort (value ≥ 6, effort ≥ 6) — plan and staff deliberately.',\n variant: 'info',\n },\n FILL_IN: {\n label: 'Fill-in',\n hint: 'Low value, low effort (value ≤ 5, effort ≤ 5) — fine as filler work.',\n variant: 'secondary',\n },\n TIME_SINK: {\n label: 'Time sink',\n hint: 'Low value, high effort (value ≤ 5, effort ≥ 6) — avoid or deprioritize.',\n variant: 'destructive',\n },\n};\n\nexport const moscowLabel = (value: string | null | undefined): string =>\n value && value in MOSCOW_META ? MOSCOW_META[value as MoscowValue].label : 'Not set';\nexport const kanoLabel = (value: string | null | undefined): string =>\n value && value in KANO_META ? KANO_META[value as KanoValue].label : 'Not set';\nexport const quadrantLabel = (value: string | null | undefined): string =>\n value && value in QUADRANT_META ? QUADRANT_META[value as QuadrantValue].label : 'Unscored';\n\n/**\n * The Value-vs-Effort quadrant, read back from the server (`FeatureRequest.valueEffortQuadrant` /\n * `RoadmapItem.valueEffortQuadrant` / a `PlanmagnetPrioritizationBucket.bucket`) — never computed here. Renders\n * an \"Unscored\" badge when null (one or both of valueScore/effortScore is not set yet) unless `hideWhenUnscored`.\n */\nexport function QuadrantBadge({\n quadrant,\n hideWhenUnscored = false,\n className,\n}: {\n quadrant: string | null | undefined;\n hideWhenUnscored?: boolean;\n className?: string;\n}) {\n if (!quadrant) {\n if (hideWhenUnscored) return null;\n return (\n <Badge variant=\"outline\" className={className} data-testid=\"prio-quadrant-badge\">\n Unscored\n </Badge>\n );\n }\n const meta = quadrant in QUADRANT_META ? QUADRANT_META[quadrant as QuadrantValue] : null;\n return (\n <Badge\n variant={meta?.variant ?? 'outline'}\n className={className}\n data-testid=\"prio-quadrant-badge\"\n >\n {meta?.label ?? quadrant}\n </Badge>\n );\n}\n\nexport interface PrioritizationFormValues {\n /** '' = not set */\n moscow: string;\n /** '' = not set */\n kano: string;\n /** '' = not set, else a whole number 1-10 as text */\n valueScore: string;\n /** '' = not set, else a whole number 1-10 as text */\n effortScore: string;\n}\n\nexport const EMPTY_PRIORITIZATION_FORM: PrioritizationFormValues = {\n moscow: '',\n kano: '',\n valueScore: '',\n effortScore: '',\n};\n\nconst selectClass =\n 'mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60';\n\n/**\n * The four prioritization inputs (PRM-27): MoSCoW and Kano selects (each with a short explanation of the\n * selected value), and 1-10 value/effort number inputs. Fully controlled; clearing a select or number input back\n * to empty is how a caller sends `null` (cleared) rather than omitting the field. Shared by the feature request\n * create/edit form, the feature detail page, and the roadmap initiative edit dialog.\n */\nexport function PrioritizationScoreFields({\n idPrefix,\n values,\n onChange,\n disabled = false,\n}: {\n idPrefix: string;\n values: PrioritizationFormValues;\n onChange: (next: PrioritizationFormValues) => void;\n disabled?: boolean;\n}) {\n const value = parseIntField(values.valueScore, { min: 1, max: 10 });\n const effort = parseIntField(values.effortScore, { min: 1, max: 10 });\n\n return (\n <div className=\"space-y-3\" data-testid=\"prio-fields\">\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor={`${idPrefix}-moscow`} className=\"text-sm font-medium\">\n MoSCoW\n </label>\n <select\n id={`${idPrefix}-moscow`}\n data-testid=\"prio-field-moscow\"\n className={selectClass}\n value={values.moscow}\n disabled={disabled}\n onChange={(e) => onChange({ ...values, moscow: e.target.value })}\n >\n <option value=\"\">Not set</option>\n {MOSCOW_VALUES.map((v) => (\n <option key={v} value={v}>\n {MOSCOW_META[v].label}\n </option>\n ))}\n </select>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {values.moscow && values.moscow in MOSCOW_META\n ? MOSCOW_META[values.moscow as MoscowValue].hint\n : \"Must/Should/Could/Won't for this release.\"}\n </p>\n </div>\n <div>\n <label htmlFor={`${idPrefix}-kano`} className=\"text-sm font-medium\">\n Kano\n </label>\n <select\n id={`${idPrefix}-kano`}\n data-testid=\"prio-field-kano\"\n className={selectClass}\n value={values.kano}\n disabled={disabled}\n onChange={(e) => onChange({ ...values, kano: e.target.value })}\n >\n <option value=\"\">Not set</option>\n {KANO_VALUES.map((v) => (\n <option key={v} value={v}>\n {KANO_META[v].label}\n </option>\n ))}\n </select>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {values.kano && values.kano in KANO_META\n ? KANO_META[values.kano as KanoValue].hint\n : 'How this shapes customer satisfaction.'}\n </p>\n </div>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor={`${idPrefix}-value-score`} className=\"text-sm font-medium\">\n Value score (1-10)\n </label>\n <Input\n id={`${idPrefix}-value-score`}\n data-testid=\"prio-field-value-score\"\n type=\"number\"\n min={1}\n max={10}\n className=\"mt-1\"\n placeholder=\"Not set\"\n value={values.valueScore}\n aria-invalid={value.invalid}\n disabled={disabled}\n onChange={(e) => onChange({ ...values, valueScore: e.target.value })}\n />\n {value.invalid && <ErrorLine message=\"Value score is a whole number from 1 to 10.\" />}\n </div>\n <div>\n <label htmlFor={`${idPrefix}-effort-score`} className=\"text-sm font-medium\">\n Effort score (1-10)\n </label>\n <Input\n id={`${idPrefix}-effort-score`}\n data-testid=\"prio-field-effort-score\"\n type=\"number\"\n min={1}\n max={10}\n className=\"mt-1\"\n placeholder=\"Not set\"\n value={values.effortScore}\n aria-invalid={effort.invalid}\n disabled={disabled}\n onChange={(e) => onChange({ ...values, effortScore: e.target.value })}\n />\n {effort.invalid && <ErrorLine message=\"Effort score is a whole number from 1 to 10.\" />}\n </div>\n </div>\n </div>\n );\n}\n\n/** True when neither of the two score fields holds an out-of-range value (empty is always valid — it means \"clear\"). */\nexport function prioritizationScoresValid(values: PrioritizationFormValues): boolean {\n return (\n !parseIntField(values.valueScore, { min: 1, max: 10 }).invalid &&\n !parseIntField(values.effortScore, { min: 1, max: 10 }).invalid\n );\n}\n\n// ─── RICE (PRM-26) ──────────────────────────────────────────────────────────\n// Deliberately NOT folded into PrioritizationFormValues/PrioritizationScoreFields above: those two are shared\n// with RoadmapControls.tsx's RoadmapItem editor, and RICE (reach/impact/confidence/riceScore) exists only on\n// FeatureRequest — a RoadmapItem mutation has no such fields to send. Kept as its own form fragment, used only\n// where a FeatureRequest is actually being edited.\n//\n// `riceScore = (reach * impact * confidence) / effortScore` is computed server-side only\n// (utils/prioritization.ts#computeRiceScore) — same \"never a live client-side guess\" rule\n// `valueEffortQuadrant` already follows here. This file has no client-side RICE calculator to match: every\n// `riceScore` reader below (FeatureControls.tsx) reads the value last read back from the server.\n\nexport interface RiceFormValues {\n /** '' = not set, else a whole number 1-10 as text */\n reach: string;\n /** '' = not set, else a whole number 1-10 as text */\n impact: string;\n /** '' = not set, else a whole number 1-10 as text */\n confidence: string;\n}\n\nexport const EMPTY_RICE_FORM: RiceFormValues = { reach: '', impact: '', confidence: '' };\n\n/** True when none of the three inputs holds an out-of-range value (empty is always valid — it means \"clear\"). */\nexport function riceInputsValid(values: RiceFormValues): boolean {\n return (\n !parseIntField(values.reach, { min: 1, max: 10 }).invalid &&\n !parseIntField(values.impact, { min: 1, max: 10 }).invalid &&\n !parseIntField(values.confidence, { min: 1, max: 10 }).invalid\n );\n}\n\n/**\n * Why `FeatureRequest.riceScore` reads null, for a feature that has one (reach/impact/confidence/effortScore\n * read straight off the record, never from in-progress form state) — null otherwise, there is nothing missing\n * to explain. Shared by the feature detail card and anywhere else riceScore is listed, so the reason is worded\n * identically everywhere it appears.\n */\nexport function riceScoreMissingHint(feature: {\n reach?: number | null;\n impact?: number | null;\n confidence?: number | null;\n effortScore?: number | null;\n}): string | null {\n const missing: string[] = [];\n if (feature.reach == null) missing.push('reach');\n if (feature.impact == null) missing.push('impact');\n if (feature.confidence == null) missing.push('confidence');\n if (feature.effortScore == null) missing.push('effort score');\n return missing.length === 0 ? null : `Missing ${missing.join(', ')}.`;\n}\n\n/**\n * Reach/impact/confidence 1-10 inputs (PRM-26), rendered next to FeatureRequest's existing effort/value scores\n * — never on its own: RICE reuses `effortScore` as its divisor, so `effortScoreMissing` tells the admin why a\n * live preview below these three inputs may still read \"—\" even once all three are filled in.\n */\nexport function RiceScoreFields({\n idPrefix,\n values,\n onChange,\n effortScoreMissing,\n disabled = false,\n}: {\n idPrefix: string;\n values: RiceFormValues;\n onChange: (next: RiceFormValues) => void;\n /** True when FeatureRequest.effortScore (PRM-27) is not set — riceScore has no divisor yet. */\n effortScoreMissing: boolean;\n disabled?: boolean;\n}) {\n const reach = parseIntField(values.reach, { min: 1, max: 10 });\n const impact = parseIntField(values.impact, { min: 1, max: 10 });\n const confidence = parseIntField(values.confidence, { min: 1, max: 10 });\n return (\n <div className=\"space-y-3\" data-testid=\"rice-fields\">\n <div className=\"grid grid-cols-3 gap-3\">\n <div>\n <label htmlFor={`${idPrefix}-rice-reach`} className=\"text-sm font-medium\">\n Reach (1-10)\n </label>\n <Input\n id={`${idPrefix}-rice-reach`}\n data-testid=\"rice-field-reach\"\n type=\"number\"\n min={1}\n max={10}\n className=\"mt-1\"\n placeholder=\"Not set\"\n value={values.reach}\n aria-invalid={reach.invalid}\n disabled={disabled}\n onChange={(e) => onChange({ ...values, reach: e.target.value })}\n />\n {reach.invalid && <ErrorLine message=\"Reach is a whole number from 1 to 10.\" />}\n </div>\n <div>\n <label htmlFor={`${idPrefix}-rice-impact`} className=\"text-sm font-medium\">\n Impact (1-10)\n </label>\n <Input\n id={`${idPrefix}-rice-impact`}\n data-testid=\"rice-field-impact\"\n type=\"number\"\n min={1}\n max={10}\n className=\"mt-1\"\n placeholder=\"Not set\"\n value={values.impact}\n aria-invalid={impact.invalid}\n disabled={disabled}\n onChange={(e) => onChange({ ...values, impact: e.target.value })}\n />\n {impact.invalid && <ErrorLine message=\"Impact is a whole number from 1 to 10.\" />}\n </div>\n <div>\n <label htmlFor={`${idPrefix}-rice-confidence`} className=\"text-sm font-medium\">\n Confidence (1-10)\n </label>\n <Input\n id={`${idPrefix}-rice-confidence`}\n data-testid=\"rice-field-confidence\"\n type=\"number\"\n min={1}\n max={10}\n className=\"mt-1\"\n placeholder=\"Not set\"\n value={values.confidence}\n aria-invalid={confidence.invalid}\n disabled={disabled}\n onChange={(e) => onChange({ ...values, confidence: e.target.value })}\n />\n {confidence.invalid && <ErrorLine message=\"Confidence is a whole number from 1 to 10.\" />}\n </div>\n </div>\n {effortScoreMissing && (values.reach || values.impact || values.confidence) && (\n <p className=\"text-xs text-muted-foreground\">\n RICE score needs an effort score too (above) — it is the denominator.\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -121,6 +121,6 @@ function _(e, t, n) {
|
|
|
121
121
|
return t > e ? `Showing ${e} of ${t} ${n}` : `${t} ${n}`;
|
|
122
122
|
}
|
|
123
123
|
//#endregion
|
|
124
|
-
export { u as DashboardCard, d as DashboardCardShell, f as NoSkillsNote, m as PercentBar, p as StatTile, _ as cappedSubtitle, g as formatHours, h as ratePercentLabel };
|
|
124
|
+
export { u as DashboardCard, d as DashboardCardShell, l as DrillDownLink, f as NoSkillsNote, m as PercentBar, p as StatTile, _ as cappedSubtitle, g as formatHours, h as ratePercentLabel };
|
|
125
125
|
|
|
126
126
|
//# sourceMappingURL=DashboardUI.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SprintCapacityCard.d.ts","sourceRoot":"","sources":["../../../../src/planmagnet/dashboards/cards/SprintCapacityCard.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"SprintCapacityCard.d.ts","sourceRoot":"","sources":["../../../../src/planmagnet/dashboards/cards/SprintCapacityCard.tsx"],"names":[],"mappings":"AAmDA,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,WAAW,GACZ,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB,2CAqHA"}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
import { usePlanMagnetNavigate as e } from "../../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { PersonAvatar as t } from "../../people/People.js";
|
|
3
|
+
import { DashboardCard as n, DrillDownLink as r } from "../DashboardUI.js";
|
|
4
|
+
import { Users as i } from "lucide-react";
|
|
5
|
+
import { gql as a } from "@apollo/client";
|
|
6
|
+
import { useQuery as o } from "@apollo/client/react";
|
|
7
|
+
import { jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
8
|
+
import { useI18n as l } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
8
9
|
//#region src/planmagnet/dashboards/cards/SprintCapacityCard.tsx
|
|
9
|
-
var
|
|
10
|
+
var u = a`
|
|
10
11
|
query DashboardSprintCapacity($sprintId: ID!) {
|
|
11
12
|
planmagnetSprintCapacity(sprintId: $sprintId) {
|
|
12
13
|
sprintId
|
|
13
14
|
crewfoundryUnavailable
|
|
14
|
-
|
|
15
|
+
unmatchedCount
|
|
15
16
|
members {
|
|
16
17
|
actorId
|
|
17
18
|
employeeName
|
|
@@ -26,100 +27,107 @@ var c = r`
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
`;
|
|
29
|
-
function
|
|
30
|
-
let { t:
|
|
31
|
-
variables: { sprintId:
|
|
32
|
-
skip: !
|
|
33
|
-
}),
|
|
34
|
-
return /* @__PURE__ */
|
|
35
|
-
icon: /* @__PURE__ */
|
|
36
|
-
title:
|
|
37
|
-
definition:
|
|
38
|
-
loading:
|
|
39
|
-
error:
|
|
40
|
-
empty: !
|
|
41
|
-
emptyMessage:
|
|
30
|
+
function d({ sprintId: a, onDrillDown: d }) {
|
|
31
|
+
let { t: f } = l(), p = e(), { data: m, loading: h, error: g } = o(u, {
|
|
32
|
+
variables: { sprintId: a },
|
|
33
|
+
skip: !a
|
|
34
|
+
}), _ = m?.planmagnetSprintCapacity;
|
|
35
|
+
return /* @__PURE__ */ s(n, {
|
|
36
|
+
icon: /* @__PURE__ */ s(i, { className: "h-4 w-4" }),
|
|
37
|
+
title: f("planmagnet.dashboards.pm.capacity.title", "Capacity"),
|
|
38
|
+
definition: f("planmagnet.dashboards.pm.capacity.definition", "Real CrewFoundry allocation for the active sprint's assignees — hours and story points, per person."),
|
|
39
|
+
loading: h && !m,
|
|
40
|
+
error: g,
|
|
41
|
+
empty: !a || !!_ && _.members.length === 0 && !_.crewfoundryUnavailable,
|
|
42
|
+
emptyMessage: a ? f("planmagnet.dashboards.pm.capacity.noMembers", "No CrewFoundry-matched assignees in this sprint.") : f("planmagnet.dashboards.pm.capacity.noSprint", "No active sprint for this project."),
|
|
42
43
|
drillDown: {
|
|
43
|
-
label:
|
|
44
|
-
onClick:
|
|
44
|
+
label: f("planmagnet.dashboards.pm.capacity.drillDown", "Open project"),
|
|
45
|
+
onClick: d
|
|
45
46
|
},
|
|
46
|
-
children:
|
|
47
|
+
children: _ ? _.crewfoundryUnavailable ? /* @__PURE__ */ s("p", {
|
|
47
48
|
className: "text-sm text-muted-foreground",
|
|
48
|
-
children:
|
|
49
|
-
}) : /* @__PURE__ */
|
|
49
|
+
children: f("planmagnet.dashboards.pm.capacity.unavailable", "Capacity data is unavailable — CrewFoundry could not be reached, or this workspace has no CrewFoundry roster.")
|
|
50
|
+
}) : /* @__PURE__ */ c("div", {
|
|
50
51
|
className: "space-y-2",
|
|
51
|
-
children: [/* @__PURE__ */
|
|
52
|
+
children: [/* @__PURE__ */ c("table", {
|
|
52
53
|
className: "w-full text-sm",
|
|
53
|
-
children: [/* @__PURE__ */
|
|
54
|
+
children: [/* @__PURE__ */ s("thead", { children: /* @__PURE__ */ c("tr", {
|
|
54
55
|
className: "text-left text-xs text-muted-foreground",
|
|
55
56
|
children: [
|
|
56
|
-
/* @__PURE__ */
|
|
57
|
+
/* @__PURE__ */ s("th", {
|
|
57
58
|
className: "py-1 pr-2 font-normal",
|
|
58
|
-
children:
|
|
59
|
+
children: f("planmagnet.dashboards.pm.capacity.person", "Person")
|
|
59
60
|
}),
|
|
60
|
-
/* @__PURE__ */
|
|
61
|
+
/* @__PURE__ */ s("th", {
|
|
61
62
|
className: "py-1 pr-2 text-right font-normal",
|
|
62
|
-
children:
|
|
63
|
+
children: f("planmagnet.dashboards.pm.capacity.allocated", "Allocated h")
|
|
63
64
|
}),
|
|
64
|
-
/* @__PURE__ */
|
|
65
|
+
/* @__PURE__ */ s("th", {
|
|
65
66
|
className: "py-1 pr-2 text-right font-normal",
|
|
66
|
-
children:
|
|
67
|
+
children: f("planmagnet.dashboards.pm.capacity.utilization", "Utilization")
|
|
67
68
|
}),
|
|
68
|
-
/* @__PURE__ */
|
|
69
|
+
/* @__PURE__ */ s("th", {
|
|
69
70
|
className: "py-1 text-right font-normal",
|
|
70
|
-
children:
|
|
71
|
+
children: f("planmagnet.dashboards.pm.capacity.items", "Items (pts)")
|
|
71
72
|
})
|
|
72
73
|
]
|
|
73
|
-
}) }), /* @__PURE__ */
|
|
74
|
+
}) }), /* @__PURE__ */ s("tbody", { children: _.members.map((e) => /* @__PURE__ */ c("tr", {
|
|
74
75
|
className: "border-t",
|
|
75
76
|
children: [
|
|
76
|
-
/* @__PURE__ */
|
|
77
|
+
/* @__PURE__ */ s("td", {
|
|
77
78
|
className: "py-1.5 pr-2",
|
|
78
|
-
children: /* @__PURE__ */
|
|
79
|
+
children: /* @__PURE__ */ c("div", {
|
|
79
80
|
className: "flex items-center gap-2",
|
|
80
|
-
children: [/* @__PURE__ */
|
|
81
|
-
id:
|
|
81
|
+
children: [/* @__PURE__ */ s(t, {
|
|
82
|
+
id: e.actorId,
|
|
82
83
|
className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary"
|
|
83
|
-
}), /* @__PURE__ */
|
|
84
|
+
}), /* @__PURE__ */ c("div", { children: [/* @__PURE__ */ s("div", {
|
|
84
85
|
className: "font-medium",
|
|
85
|
-
children:
|
|
86
|
-
}), /* @__PURE__ */
|
|
86
|
+
children: e.employeeName
|
|
87
|
+
}), /* @__PURE__ */ c("div", {
|
|
87
88
|
className: "text-xs text-muted-foreground",
|
|
88
89
|
children: [
|
|
89
|
-
|
|
90
|
+
e.position,
|
|
90
91
|
" · ",
|
|
91
|
-
|
|
92
|
+
e.department
|
|
92
93
|
]
|
|
93
94
|
})] })]
|
|
94
95
|
})
|
|
95
96
|
}),
|
|
96
|
-
/* @__PURE__ */
|
|
97
|
+
/* @__PURE__ */ s("td", {
|
|
97
98
|
className: "py-1.5 pr-2 text-right tabular-nums",
|
|
98
|
-
children:
|
|
99
|
+
children: e.allocatedHours.toFixed(1)
|
|
99
100
|
}),
|
|
100
|
-
/* @__PURE__ */
|
|
101
|
+
/* @__PURE__ */ s("td", {
|
|
101
102
|
className: "py-1.5 pr-2 text-right tabular-nums",
|
|
102
|
-
children:
|
|
103
|
+
children: e.utilizationPercent == null ? "—" : `${e.utilizationPercent}%`
|
|
103
104
|
}),
|
|
104
|
-
/* @__PURE__ */
|
|
105
|
+
/* @__PURE__ */ c("td", {
|
|
105
106
|
className: "py-1.5 text-right tabular-nums",
|
|
106
107
|
children: [
|
|
107
|
-
|
|
108
|
+
e.assignedWorkItemCount,
|
|
108
109
|
" (",
|
|
109
|
-
|
|
110
|
+
e.assignedStoryPoints,
|
|
110
111
|
")"
|
|
111
112
|
]
|
|
112
113
|
})
|
|
113
114
|
]
|
|
114
|
-
},
|
|
115
|
-
}),
|
|
116
|
-
className: "
|
|
117
|
-
|
|
115
|
+
}, e.actorId)) })]
|
|
116
|
+
}), _.unmatchedCount > 0 ? /* @__PURE__ */ c("div", {
|
|
117
|
+
className: "flex items-center justify-between gap-2",
|
|
118
|
+
"data-testid": "sprint-capacity-card-unmatched",
|
|
119
|
+
children: [/* @__PURE__ */ s("p", {
|
|
120
|
+
className: "text-xs text-muted-foreground",
|
|
121
|
+
children: f("planmagnet.dashboards.pm.capacity.unmatched", "{{count}} sprint assignee(s) could not be matched to a CrewFoundry employee record.", { count: _.unmatchedCount })
|
|
122
|
+
}), /* @__PURE__ */ s(r, {
|
|
123
|
+
label: f("planmagnet.dashboards.pm.capacity.fixUnmatched", "Fix"),
|
|
124
|
+
onClick: () => p("/team/team-page?focus=unmatched-crew-identities")
|
|
125
|
+
})]
|
|
118
126
|
}) : null]
|
|
119
127
|
}) : null
|
|
120
128
|
});
|
|
121
129
|
}
|
|
122
130
|
//#endregion
|
|
123
|
-
export {
|
|
131
|
+
export { d as SprintCapacityCard };
|
|
124
132
|
|
|
125
133
|
//# sourceMappingURL=SprintCapacityCard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SprintCapacityCard.js","names":[],"sources":["../../../../src/planmagnet/dashboards/cards/SprintCapacityCard.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Users } from 'lucide-react';\nimport { DashboardCard } from '../DashboardUI';\nimport { PersonAvatar } from '../../people/People';\n\n/** Capacity (ANR-07): `planmagnetSprintCapacity` (BOFF-4444) of the active sprint — real CrewFoundry\n * allocation/availability per assignee, cross-layer. There is no \"skill\" concept anywhere in this data:\n * capacity is hours and story points, per person, never per skill (the PM dashboard says so separately). */\nconst GET_SPRINT_CAPACITY = gql`\n query DashboardSprintCapacity($sprintId: ID!) {\n planmagnetSprintCapacity(sprintId: $sprintId) {\n sprintId\n crewfoundryUnavailable\n
|
|
1
|
+
{"version":3,"file":"SprintCapacityCard.js","names":[],"sources":["../../../../src/planmagnet/dashboards/cards/SprintCapacityCard.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Users } from 'lucide-react';\nimport { DashboardCard, DrillDownLink } from '../DashboardUI';\nimport { PersonAvatar } from '../../people/People';\nimport { usePlanMagnetNavigate } from '../../providers/PlanMagnetProvider';\n\n/** Capacity (ANR-07): `planmagnetSprintCapacity` (BOFF-4444) of the active sprint — real CrewFoundry\n * allocation/availability per assignee, cross-layer. There is no \"skill\" concept anywhere in this data:\n * capacity is hours and story points, per person, never per skill (the PM dashboard says so separately). */\nconst GET_SPRINT_CAPACITY = gql`\n query DashboardSprintCapacity($sprintId: ID!) {\n planmagnetSprintCapacity(sprintId: $sprintId) {\n sprintId\n crewfoundryUnavailable\n unmatchedCount\n members {\n actorId\n employeeName\n position\n department\n allocatedHours\n billableHours\n utilizationPercent\n assignedWorkItemCount\n assignedStoryPoints\n }\n }\n }\n`;\n\ninterface SprintCapacityData {\n planmagnetSprintCapacity: {\n sprintId: string;\n crewfoundryUnavailable: boolean;\n unmatchedCount: number;\n members: {\n actorId: string;\n employeeName: string;\n position: string;\n department: string;\n allocatedHours: number;\n billableHours: number;\n utilizationPercent: number | null;\n assignedWorkItemCount: number;\n assignedStoryPoints: number;\n }[];\n };\n}\n\nexport function SprintCapacityCard({\n sprintId,\n onDrillDown,\n}: {\n sprintId: string;\n onDrillDown: () => void;\n}) {\n const { t } = useI18n();\n const navigate = usePlanMagnetNavigate();\n const { data, loading, error } = useQuery<SprintCapacityData>(GET_SPRINT_CAPACITY, {\n variables: { sprintId },\n skip: !sprintId,\n });\n const capacity = data?.planmagnetSprintCapacity;\n\n return (\n <DashboardCard\n icon={<Users className=\"h-4 w-4\" />}\n title={t('planmagnet.dashboards.pm.capacity.title', 'Capacity')}\n definition={t(\n 'planmagnet.dashboards.pm.capacity.definition',\n \"Real CrewFoundry allocation for the active sprint's assignees — hours and story points, per person.\"\n )}\n loading={loading && !data}\n error={error}\n empty={\n !sprintId ||\n (!!capacity && capacity.members.length === 0 && !capacity.crewfoundryUnavailable)\n }\n emptyMessage={\n !sprintId\n ? t('planmagnet.dashboards.pm.capacity.noSprint', 'No active sprint for this project.')\n : t(\n 'planmagnet.dashboards.pm.capacity.noMembers',\n 'No CrewFoundry-matched assignees in this sprint.'\n )\n }\n drillDown={{\n label: t('planmagnet.dashboards.pm.capacity.drillDown', 'Open project'),\n onClick: onDrillDown,\n }}\n >\n {capacity ? (\n capacity.crewfoundryUnavailable ? (\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'planmagnet.dashboards.pm.capacity.unavailable',\n 'Capacity data is unavailable — CrewFoundry could not be reached, or this workspace has no CrewFoundry roster.'\n )}\n </p>\n ) : (\n <div className=\"space-y-2\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"text-left text-xs text-muted-foreground\">\n <th className=\"py-1 pr-2 font-normal\">\n {t('planmagnet.dashboards.pm.capacity.person', 'Person')}\n </th>\n <th className=\"py-1 pr-2 text-right font-normal\">\n {t('planmagnet.dashboards.pm.capacity.allocated', 'Allocated h')}\n </th>\n <th className=\"py-1 pr-2 text-right font-normal\">\n {t('planmagnet.dashboards.pm.capacity.utilization', 'Utilization')}\n </th>\n <th className=\"py-1 text-right font-normal\">\n {t('planmagnet.dashboards.pm.capacity.items', 'Items (pts)')}\n </th>\n </tr>\n </thead>\n <tbody>\n {capacity.members.map((m) => (\n <tr key={m.actorId} className=\"border-t\">\n <td className=\"py-1.5 pr-2\">\n <div className=\"flex items-center gap-2\">\n <PersonAvatar\n id={m.actorId}\n className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary\"\n />\n <div>\n <div className=\"font-medium\">{m.employeeName}</div>\n <div className=\"text-xs text-muted-foreground\">\n {m.position} · {m.department}\n </div>\n </div>\n </div>\n </td>\n <td className=\"py-1.5 pr-2 text-right tabular-nums\">\n {m.allocatedHours.toFixed(1)}\n </td>\n <td className=\"py-1.5 pr-2 text-right tabular-nums\">\n {m.utilizationPercent == null ? '—' : `${m.utilizationPercent}%`}\n </td>\n <td className=\"py-1.5 text-right tabular-nums\">\n {m.assignedWorkItemCount} ({m.assignedStoryPoints})\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n {capacity.unmatchedCount > 0 ? (\n <div\n className=\"flex items-center justify-between gap-2\"\n data-testid=\"sprint-capacity-card-unmatched\"\n >\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'planmagnet.dashboards.pm.capacity.unmatched',\n '{{count}} sprint assignee(s) could not be matched to a CrewFoundry employee record.',\n { count: capacity.unmatchedCount }\n )}\n </p>\n {/* CWF-30: the admin remediation panel on the Members page. */}\n <DrillDownLink\n label={t('planmagnet.dashboards.pm.capacity.fixUnmatched', 'Fix')}\n onClick={() => navigate('/team/team-page?focus=unmatched-crew-identities')}\n />\n </div>\n ) : null}\n </div>\n )\n ) : null}\n </DashboardCard>\n );\n}\n"],"mappings":";;;;;;;;;AAWA,IAAM,IAAsB,CAAG;;;;;;;;;;;;;;;;;;;;AAwC/B,SAAgB,EAAmB,EACjC,aACA,kBAIC;CACD,IAAM,EAAE,SAAM,GAAS,EACjB,IAAW,GAAuB,EAClC,EAAE,SAAM,YAAS,aAAU,EAA6B,GAAqB;EACjF,WAAW,EAAE,aAAU;EACvB,MAAM,CAAC;EACR,CAAC,EACI,IAAW,GAAM;AAEvB,QACE,kBAAC,GAAD;EACE,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;EACnC,OAAO,EAAE,2CAA2C,WAAW;EAC/D,YAAY,EACV,gDACA,sGACD;EACD,SAAS,KAAW,CAAC;EACd;EACP,OACE,CAAC,KACA,CAAC,CAAC,KAAY,EAAS,QAAQ,WAAW,KAAK,CAAC,EAAS;EAE5D,cACG,IAEG,EACE,+CACA,mDACD,GAJD,EAAE,8CAA8C,qCAAqC;EAM3F,WAAW;GACT,OAAO,EAAE,+CAA+C,eAAe;GACvE,SAAS;GACV;YAEA,IACC,EAAS,yBACP,kBAAC,KAAD;GAAG,WAAU;aACV,EACC,iDACA,gHACD;GACC,CAAA,GAEJ,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,SAAD;IAAO,WAAU;cAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;KAAI,WAAU;eAAd;MACE,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAE,4CAA4C,SAAS;OACrD,CAAA;MACL,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAE,+CAA+C,cAAc;OAC7D,CAAA;MACL,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAE,iDAAiD,cAAc;OAC/D,CAAA;MACL,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAE,2CAA2C,cAAc;OACzD,CAAA;MACF;QACC,CAAA,EACR,kBAAC,SAAD,EAAA,UACG,EAAS,QAAQ,KAAK,MACrB,kBAAC,MAAD;KAAoB,WAAU;eAA9B;MACE,kBAAC,MAAD;OAAI,WAAU;iBACZ,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SACE,IAAI,EAAE;SACN,WAAU;SACV,CAAA,EACF,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;SAAK,WAAU;mBAAe,EAAE;SAAmB,CAAA,EACnD,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACG,EAAE;UAAS;UAAI,EAAE;UACd;WACF,EAAA,CAAA,CACF;;OACH,CAAA;MACL,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAE,eAAe,QAAQ,EAAE;OACzB,CAAA;MACL,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAE,sBAAsB,OAAO,MAAM,GAAG,EAAE,mBAAmB;OAC3D,CAAA;MACL,kBAAC,MAAD;OAAI,WAAU;iBAAd;QACG,EAAE;QAAsB;QAAG,EAAE;QAAoB;QAC/C;;MACF;OAxBI,EAAE,QAwBN,CACL,EACI,CAAA,CACF;OACP,EAAS,iBAAiB,IACzB,kBAAC,OAAD;IACE,WAAU;IACV,eAAY;cAFd,CAIE,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,+CACA,uFACA,EAAE,OAAO,EAAS,gBAAgB,CACnC;KACC,CAAA,EAEJ,kBAAC,GAAD;KACE,OAAO,EAAE,kDAAkD,MAAM;KACjE,eAAe,EAAS,kDAAkD;KAC1E,CAAA,CACE;QACJ,KACA;OAEN;EACU,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ScheduleMeetingDialog.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/meetings/ScheduleMeetingDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAkBjD,OAAO,EAEL,KAAK,WAAW,EAWjB,MAAM,gBAAgB,CAAC;AA0BxB,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,QAAe,EACf,WAAkB,EAClB,WAAqB,EACrB,QAAgB,EAChB,YAAiB,EACjB,OAAO,EACP,YAAiC,EACjC,WAAW,GACZ,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,wFAAwF;IACxF,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,+GAA+G;IAC/G,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACjD,
|
|
1
|
+
{"version":3,"file":"ScheduleMeetingDialog.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/meetings/ScheduleMeetingDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAkBjD,OAAO,EAEL,KAAK,WAAW,EAWjB,MAAM,gBAAgB,CAAC;AA0BxB,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,QAAe,EACf,WAAkB,EAClB,WAAqB,EACrB,QAAgB,EAChB,YAAiB,EACjB,OAAO,EACP,YAAiC,EACjC,WAAW,GACZ,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,wFAAwF;IACxF,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,+GAA+G;IAC/G,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACjD,2CAqRA"}
|
|
@@ -212,7 +212,7 @@ function b({ projectId: b, sprintId: x = null, milestoneId: S = null, defaultKin
|
|
|
212
212
|
children: "Cancel"
|
|
213
213
|
}), !$ && /* @__PURE__ */ h(_, {
|
|
214
214
|
onClick: () => void Ce(),
|
|
215
|
-
disabled: K,
|
|
215
|
+
disabled: K || J || !Z,
|
|
216
216
|
"data-testid": "schedule-meeting-submit",
|
|
217
217
|
children: K ? "Scheduling…" : "Schedule meeting"
|
|
218
218
|
})] })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ScheduleMeetingDialog.js","names":[],"sources":["../../../src/planmagnet/meetings/ScheduleMeetingDialog.tsx"],"sourcesContent":["import { useState, type ReactNode } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Button,\n Input,\n Textarea,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { CalendarPlus } from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport { ErrorLine } from '../controls/ConfirmButton';\nimport {\n MEETING_KINDS,\n type MeetingKind,\n RECURRENCE_OPTIONS,\n RECURRENCE_LABELS,\n type RecurrenceOption,\n DURATION_OPTIONS_MINUTES,\n localInputValueToISO,\n addMinutesIso,\n defaultLocalStart,\n viewerTimeZone,\n buildRRule,\n validateMeetingDraft,\n} from './meetingModel';\nimport {\n SCHEDULE_MEETING_CALENDARS,\n CREATE_CALENDAR_EVENT,\n DELETE_CALENDAR_EVENT,\n LINK_PLANMAGNET_MEETING,\n MEETING_QUERIES,\n} from './meetingGraphql';\nimport { createAndLinkMeeting, meetingScheduleErrorMessage } from './meetingActions';\n\n/**\n * Reusable \"schedule a meeting\" dialog (GAP-06, CWF-22). Opened with a FIXED project and an optional sprint or\n * milestone — see `createAndLinkMeeting` for the create-event-then-link flow, including what happens when the\n * link is refused after the calendar event was already created.\n */\n\nconst fieldClass =\n 'mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60';\n\ninterface CalendarOption {\n id: string;\n name: string;\n isDefault: boolean;\n calendarType: string;\n}\n\nexport function ScheduleMeetingDialog({\n projectId,\n sprintId = null,\n milestoneId = null,\n defaultKind = 'OTHER',\n lockKind = false,\n defaultTitle = '',\n trigger,\n triggerLabel = 'Schedule meeting',\n onScheduled,\n}: {\n projectId: string;\n sprintId?: string | null;\n milestoneId?: string | null;\n /** Pre-selects a kind — e.g. MILESTONE_CELEBRATION/DEMO from a milestone's own page. */\n defaultKind?: MeetingKind;\n /** Hides the kind picker, for a trigger whose kind the context already decided (Schedule celebration/demo). */\n lockKind?: boolean;\n defaultTitle?: string;\n trigger?: ReactNode;\n triggerLabel?: string;\n onScheduled?: (meeting: { id: string }) => void;\n}) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [title, setTitle] = useState(defaultTitle);\n const [kind, setKind] = useState<MeetingKind>(defaultKind);\n const [calendarId, setCalendarId] = useState('');\n const [startLocal, setStartLocal] = useState<string>(() => defaultLocalStart());\n const [duration, setDuration] = useState<number>(30);\n const [recurrence, setRecurrence] = useState<RecurrenceOption>('NONE');\n const [agenda, setAgenda] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [submitting, setSubmitting] = useState(false);\n\n const { data: calendarsData, loading: calendarsLoading } = useQuery<{\n calendars: { calendars: CalendarOption[]; total: number };\n }>(SCHEDULE_MEETING_CALENDARS, {\n variables: { pagination: { limit: 100 } },\n skip: !open,\n });\n const calendars = calendarsData?.calendars.calendars ?? [];\n // The workspace default calendar, derived rather than synced through an effect: `calendarId` only ever holds\n // an EXPLICIT user choice (empty until they pick one), and this fills in the default for both the `<select>`\n // and submission until they do — so the default appears the moment calendars arrive, with no separate\n // render pass, and never fights a choice the user already made.\n const defaultCalendarId = (calendars.find((c) => c.isDefault) ?? calendars[0])?.id ?? '';\n const effectiveCalendarId = calendarId || defaultCalendarId;\n\n const [createEvent] = useMutation<{ createEvent: { id: string } }>(CREATE_CALENDAR_EVENT);\n const [linkMeeting] = useMutation<{ planmagnetLinkMeeting: { id: string } }>(\n LINK_PLANMAGNET_MEETING,\n { refetchQueries: MEETING_QUERIES, awaitRefetchQueries: true }\n );\n const [deleteEvent] = useMutation<{ deleteEvent: boolean }>(DELETE_CALENDAR_EVENT);\n\n const reset = () => {\n setTitle(defaultTitle);\n setKind(defaultKind);\n setCalendarId('');\n setStartLocal(defaultLocalStart());\n setDuration(30);\n setRecurrence('NONE');\n setAgenda('');\n setError(null);\n };\n\n const schedule = async () => {\n const problem = validateMeetingDraft({\n title,\n calendarId: effectiveCalendarId,\n startLocal,\n durationMinutes: duration,\n kind,\n milestoneId,\n });\n if (problem) {\n setError(problem);\n return;\n }\n const startIso = localInputValueToISO(startLocal);\n if (!startIso || !workspaceId) {\n setError('Pick a start time.');\n return;\n }\n setError(null);\n setSubmitting(true);\n try {\n const endIso = addMinutesIso(startIso, duration);\n const rrule = buildRRule(recurrence, startLocal);\n const meeting = await createAndLinkMeeting({\n createEvent,\n linkMeeting,\n deleteEvent,\n workspaceId,\n calendarId: effectiveCalendarId,\n title: title.trim(),\n startIso,\n endIso,\n kind,\n projectId,\n sprintId,\n milestoneId,\n agenda: agenda.trim(),\n rrule,\n timezone: viewerTimeZone(),\n });\n setOpen(false);\n reset();\n onScheduled?.(meeting);\n } catch (e) {\n setError(meetingScheduleErrorMessage(e));\n } finally {\n setSubmitting(false);\n }\n };\n\n const noCalendars = open && !calendarsLoading && calendars.length === 0;\n const contextLabel = milestoneId ? 'milestone' : sprintId ? 'sprint' : 'project';\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) reset();\n else setError(null);\n }}\n >\n <ResponsiveDialogTrigger asChild>\n {trigger ?? (\n <Button size=\"sm\" variant=\"outline\" data-testid=\"schedule-meeting-trigger\">\n <CalendarPlus className=\"mr-1 h-4 w-4\" />\n {triggerLabel}\n </Button>\n )}\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Schedule a meeting</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Creates a calendar event and links it to this {contextLabel}.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n {noCalendars ? (\n <div className=\"space-y-2 text-sm\" data-testid=\"schedule-meeting-no-calendars\">\n <p className=\"text-muted-foreground\">\n You have no calendars yet. Create one in Calendar settings, then come back to schedule\n this meeting.\n </p>\n <a\n href=\"/calendar/settings\"\n className=\"text-primary hover:underline\"\n data-testid=\"schedule-meeting-calendar-settings-link\"\n >\n Open Calendar settings\n </a>\n </div>\n ) : (\n <div className=\"space-y-3\">\n <div>\n <label htmlFor=\"schedule-meeting-title\" className=\"text-sm font-medium\">\n Title\n </label>\n <Input\n id=\"schedule-meeting-title\"\n className=\"mt-1\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n data-testid=\"schedule-meeting-title\"\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor=\"schedule-meeting-kind\" className=\"text-sm font-medium\">\n Kind\n </label>\n <select\n id=\"schedule-meeting-kind\"\n className={fieldClass}\n value={kind}\n disabled={lockKind}\n onChange={(e) => setKind(e.target.value as MeetingKind)}\n data-testid=\"schedule-meeting-kind\"\n >\n {MEETING_KINDS.map((k) => (\n <option key={k} value={k}>\n {enumLabel(k)}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"schedule-meeting-calendar\" className=\"text-sm font-medium\">\n Calendar\n </label>\n <select\n id=\"schedule-meeting-calendar\"\n className={fieldClass}\n value={effectiveCalendarId}\n disabled={calendarsLoading}\n onChange={(e) => setCalendarId(e.target.value)}\n data-testid=\"schedule-meeting-calendar\"\n >\n {calendarsLoading && <option value=\"\">Loading calendars…</option>}\n {calendars.map((c) => (\n <option key={c.id} value={c.id}>\n {c.name}\n {c.isDefault ? ' (default)' : ''}\n </option>\n ))}\n </select>\n </div>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor=\"schedule-meeting-start\" className=\"text-sm font-medium\">\n Start\n </label>\n <Input\n id=\"schedule-meeting-start\"\n type=\"datetime-local\"\n className=\"mt-1\"\n value={startLocal}\n onChange={(e) => setStartLocal(e.target.value)}\n data-testid=\"schedule-meeting-start\"\n />\n </div>\n <div>\n <label htmlFor=\"schedule-meeting-duration\" className=\"text-sm font-medium\">\n Duration\n </label>\n <select\n id=\"schedule-meeting-duration\"\n className={fieldClass}\n value={duration}\n onChange={(e) => setDuration(Number(e.target.value))}\n data-testid=\"schedule-meeting-duration\"\n >\n {DURATION_OPTIONS_MINUTES.map((m) => (\n <option key={m} value={m}>\n {m < 60 ? `${m} minutes` : `${m / 60} hour${m === 60 ? '' : 's'}`}\n </option>\n ))}\n </select>\n </div>\n </div>\n <div>\n <label htmlFor=\"schedule-meeting-recurrence\" className=\"text-sm font-medium\">\n Repeats\n </label>\n <select\n id=\"schedule-meeting-recurrence\"\n className={fieldClass}\n value={recurrence}\n onChange={(e) => setRecurrence(e.target.value as RecurrenceOption)}\n data-testid=\"schedule-meeting-recurrence\"\n >\n {RECURRENCE_OPTIONS.map((r) => (\n <option key={r} value={r}>\n {RECURRENCE_LABELS[r]}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"schedule-meeting-agenda\" className=\"text-sm font-medium\">\n Agenda (optional)\n </label>\n <Textarea\n id=\"schedule-meeting-agenda\"\n className=\"mt-1 min-h-[72px]\"\n value={agenda}\n onChange={(e) => setAgenda(e.target.value)}\n data-testid=\"schedule-meeting-agenda\"\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n )}\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setOpen(false)}>\n Cancel\n </Button>\n {!noCalendars && (\n <Button\n onClick={() => void schedule()}\n disabled={submitting}\n data-testid=\"schedule-meeting-submit\"\n >\n {submitting ? 'Scheduling…' : 'Schedule meeting'}\n </Button>\n )}\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ScheduleMeetingDialog.js","names":[],"sources":["../../../src/planmagnet/meetings/ScheduleMeetingDialog.tsx"],"sourcesContent":["import { useState, type ReactNode } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Button,\n Input,\n Textarea,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { CalendarPlus } from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport { ErrorLine } from '../controls/ConfirmButton';\nimport {\n MEETING_KINDS,\n type MeetingKind,\n RECURRENCE_OPTIONS,\n RECURRENCE_LABELS,\n type RecurrenceOption,\n DURATION_OPTIONS_MINUTES,\n localInputValueToISO,\n addMinutesIso,\n defaultLocalStart,\n viewerTimeZone,\n buildRRule,\n validateMeetingDraft,\n} from './meetingModel';\nimport {\n SCHEDULE_MEETING_CALENDARS,\n CREATE_CALENDAR_EVENT,\n DELETE_CALENDAR_EVENT,\n LINK_PLANMAGNET_MEETING,\n MEETING_QUERIES,\n} from './meetingGraphql';\nimport { createAndLinkMeeting, meetingScheduleErrorMessage } from './meetingActions';\n\n/**\n * Reusable \"schedule a meeting\" dialog (GAP-06, CWF-22). Opened with a FIXED project and an optional sprint or\n * milestone — see `createAndLinkMeeting` for the create-event-then-link flow, including what happens when the\n * link is refused after the calendar event was already created.\n */\n\nconst fieldClass =\n 'mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60';\n\ninterface CalendarOption {\n id: string;\n name: string;\n isDefault: boolean;\n calendarType: string;\n}\n\nexport function ScheduleMeetingDialog({\n projectId,\n sprintId = null,\n milestoneId = null,\n defaultKind = 'OTHER',\n lockKind = false,\n defaultTitle = '',\n trigger,\n triggerLabel = 'Schedule meeting',\n onScheduled,\n}: {\n projectId: string;\n sprintId?: string | null;\n milestoneId?: string | null;\n /** Pre-selects a kind — e.g. MILESTONE_CELEBRATION/DEMO from a milestone's own page. */\n defaultKind?: MeetingKind;\n /** Hides the kind picker, for a trigger whose kind the context already decided (Schedule celebration/demo). */\n lockKind?: boolean;\n defaultTitle?: string;\n trigger?: ReactNode;\n triggerLabel?: string;\n onScheduled?: (meeting: { id: string }) => void;\n}) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [title, setTitle] = useState(defaultTitle);\n const [kind, setKind] = useState<MeetingKind>(defaultKind);\n const [calendarId, setCalendarId] = useState('');\n const [startLocal, setStartLocal] = useState<string>(() => defaultLocalStart());\n const [duration, setDuration] = useState<number>(30);\n const [recurrence, setRecurrence] = useState<RecurrenceOption>('NONE');\n const [agenda, setAgenda] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [submitting, setSubmitting] = useState(false);\n\n const { data: calendarsData, loading: calendarsLoading } = useQuery<{\n calendars: { calendars: CalendarOption[]; total: number };\n }>(SCHEDULE_MEETING_CALENDARS, {\n variables: { pagination: { limit: 100 } },\n skip: !open,\n });\n const calendars = calendarsData?.calendars.calendars ?? [];\n // The workspace default calendar, derived rather than synced through an effect: `calendarId` only ever holds\n // an EXPLICIT user choice (empty until they pick one), and this fills in the default for both the `<select>`\n // and submission until they do — so the default appears the moment calendars arrive, with no separate\n // render pass, and never fights a choice the user already made.\n const defaultCalendarId = (calendars.find((c) => c.isDefault) ?? calendars[0])?.id ?? '';\n const effectiveCalendarId = calendarId || defaultCalendarId;\n\n const [createEvent] = useMutation<{ createEvent: { id: string } }>(CREATE_CALENDAR_EVENT);\n const [linkMeeting] = useMutation<{ planmagnetLinkMeeting: { id: string } }>(\n LINK_PLANMAGNET_MEETING,\n { refetchQueries: MEETING_QUERIES, awaitRefetchQueries: true }\n );\n const [deleteEvent] = useMutation<{ deleteEvent: boolean }>(DELETE_CALENDAR_EVENT);\n\n const reset = () => {\n setTitle(defaultTitle);\n setKind(defaultKind);\n setCalendarId('');\n setStartLocal(defaultLocalStart());\n setDuration(30);\n setRecurrence('NONE');\n setAgenda('');\n setError(null);\n };\n\n const schedule = async () => {\n const problem = validateMeetingDraft({\n title,\n calendarId: effectiveCalendarId,\n startLocal,\n durationMinutes: duration,\n kind,\n milestoneId,\n });\n if (problem) {\n setError(problem);\n return;\n }\n const startIso = localInputValueToISO(startLocal);\n if (!startIso || !workspaceId) {\n setError('Pick a start time.');\n return;\n }\n setError(null);\n setSubmitting(true);\n try {\n const endIso = addMinutesIso(startIso, duration);\n const rrule = buildRRule(recurrence, startLocal);\n const meeting = await createAndLinkMeeting({\n createEvent,\n linkMeeting,\n deleteEvent,\n workspaceId,\n calendarId: effectiveCalendarId,\n title: title.trim(),\n startIso,\n endIso,\n kind,\n projectId,\n sprintId,\n milestoneId,\n agenda: agenda.trim(),\n rrule,\n timezone: viewerTimeZone(),\n });\n setOpen(false);\n reset();\n onScheduled?.(meeting);\n } catch (e) {\n setError(meetingScheduleErrorMessage(e));\n } finally {\n setSubmitting(false);\n }\n };\n\n const noCalendars = open && !calendarsLoading && calendars.length === 0;\n const contextLabel = milestoneId ? 'milestone' : sprintId ? 'sprint' : 'project';\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) reset();\n else setError(null);\n }}\n >\n <ResponsiveDialogTrigger asChild>\n {trigger ?? (\n <Button size=\"sm\" variant=\"outline\" data-testid=\"schedule-meeting-trigger\">\n <CalendarPlus className=\"mr-1 h-4 w-4\" />\n {triggerLabel}\n </Button>\n )}\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Schedule a meeting</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Creates a calendar event and links it to this {contextLabel}.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n {noCalendars ? (\n <div className=\"space-y-2 text-sm\" data-testid=\"schedule-meeting-no-calendars\">\n <p className=\"text-muted-foreground\">\n You have no calendars yet. Create one in Calendar settings, then come back to schedule\n this meeting.\n </p>\n <a\n href=\"/calendar/settings\"\n className=\"text-primary hover:underline\"\n data-testid=\"schedule-meeting-calendar-settings-link\"\n >\n Open Calendar settings\n </a>\n </div>\n ) : (\n <div className=\"space-y-3\">\n <div>\n <label htmlFor=\"schedule-meeting-title\" className=\"text-sm font-medium\">\n Title\n </label>\n <Input\n id=\"schedule-meeting-title\"\n className=\"mt-1\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n data-testid=\"schedule-meeting-title\"\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor=\"schedule-meeting-kind\" className=\"text-sm font-medium\">\n Kind\n </label>\n <select\n id=\"schedule-meeting-kind\"\n className={fieldClass}\n value={kind}\n disabled={lockKind}\n onChange={(e) => setKind(e.target.value as MeetingKind)}\n data-testid=\"schedule-meeting-kind\"\n >\n {MEETING_KINDS.map((k) => (\n <option key={k} value={k}>\n {enumLabel(k)}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"schedule-meeting-calendar\" className=\"text-sm font-medium\">\n Calendar\n </label>\n <select\n id=\"schedule-meeting-calendar\"\n className={fieldClass}\n value={effectiveCalendarId}\n disabled={calendarsLoading}\n onChange={(e) => setCalendarId(e.target.value)}\n data-testid=\"schedule-meeting-calendar\"\n >\n {calendarsLoading && <option value=\"\">Loading calendars…</option>}\n {calendars.map((c) => (\n <option key={c.id} value={c.id}>\n {c.name}\n {c.isDefault ? ' (default)' : ''}\n </option>\n ))}\n </select>\n </div>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor=\"schedule-meeting-start\" className=\"text-sm font-medium\">\n Start\n </label>\n <Input\n id=\"schedule-meeting-start\"\n type=\"datetime-local\"\n className=\"mt-1\"\n value={startLocal}\n onChange={(e) => setStartLocal(e.target.value)}\n data-testid=\"schedule-meeting-start\"\n />\n </div>\n <div>\n <label htmlFor=\"schedule-meeting-duration\" className=\"text-sm font-medium\">\n Duration\n </label>\n <select\n id=\"schedule-meeting-duration\"\n className={fieldClass}\n value={duration}\n onChange={(e) => setDuration(Number(e.target.value))}\n data-testid=\"schedule-meeting-duration\"\n >\n {DURATION_OPTIONS_MINUTES.map((m) => (\n <option key={m} value={m}>\n {m < 60 ? `${m} minutes` : `${m / 60} hour${m === 60 ? '' : 's'}`}\n </option>\n ))}\n </select>\n </div>\n </div>\n <div>\n <label htmlFor=\"schedule-meeting-recurrence\" className=\"text-sm font-medium\">\n Repeats\n </label>\n <select\n id=\"schedule-meeting-recurrence\"\n className={fieldClass}\n value={recurrence}\n onChange={(e) => setRecurrence(e.target.value as RecurrenceOption)}\n data-testid=\"schedule-meeting-recurrence\"\n >\n {RECURRENCE_OPTIONS.map((r) => (\n <option key={r} value={r}>\n {RECURRENCE_LABELS[r]}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"schedule-meeting-agenda\" className=\"text-sm font-medium\">\n Agenda (optional)\n </label>\n <Textarea\n id=\"schedule-meeting-agenda\"\n className=\"mt-1 min-h-[72px]\"\n value={agenda}\n onChange={(e) => setAgenda(e.target.value)}\n data-testid=\"schedule-meeting-agenda\"\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n )}\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setOpen(false)}>\n Cancel\n </Button>\n {!noCalendars && (\n <Button\n onClick={() => void schedule()}\n // Not before the calendars have loaded: until then the default calendar is unknown, and a click\n // would be refused with \"Pick a calendar.\" just before the default appears in the picker.\n disabled={submitting || calendarsLoading || !effectiveCalendarId}\n data-testid=\"schedule-meeting-submit\"\n >\n {submitting ? 'Scheduling…' : 'Schedule meeting'}\n </Button>\n )}\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsAnalyticsPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/AnalyticsAnalyticsPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AnalyticsAnalyticsPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/AnalyticsAnalyticsPagePage.tsx"],"names":[],"mappings":"AAkTA,wBAAgB,0BAA0B,4CAwvBzC"}
|