@burdenoff/microfe-planmagnet 2026.1001.6 → 2026.1001.8

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 (94) hide show
  1. package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
  2. package/dist/planmagnet/PlanMagnetRoutes.js +10 -6
  3. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  4. package/dist/planmagnet/automation/automationRuleTypes.d.ts +31 -19
  5. package/dist/planmagnet/automation/automationRuleTypes.d.ts.map +1 -1
  6. package/dist/planmagnet/automation/automationRuleTypes.js +4 -8
  7. package/dist/planmagnet/automation/automationRuleTypes.js.map +1 -1
  8. package/dist/planmagnet/controls/FeatureControls.d.ts +18 -1
  9. package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -1
  10. package/dist/planmagnet/controls/FeatureControls.js +259 -202
  11. package/dist/planmagnet/controls/FeatureControls.js.map +1 -1
  12. package/dist/planmagnet/controls/FeedbackControls.d.ts +70 -0
  13. package/dist/planmagnet/controls/FeedbackControls.d.ts.map +1 -0
  14. package/dist/planmagnet/controls/FeedbackControls.js +153 -0
  15. package/dist/planmagnet/controls/FeedbackControls.js.map +1 -0
  16. package/dist/planmagnet/controls/SyncControls.d.ts.map +1 -1
  17. package/dist/planmagnet/controls/SyncControls.js +1 -0
  18. package/dist/planmagnet/controls/SyncControls.js.map +1 -1
  19. package/dist/planmagnet/controls/WebhookSubscriptionControls.d.ts +47 -0
  20. package/dist/planmagnet/controls/WebhookSubscriptionControls.d.ts.map +1 -0
  21. package/dist/planmagnet/controls/WebhookSubscriptionControls.js +542 -0
  22. package/dist/planmagnet/controls/WebhookSubscriptionControls.js.map +1 -0
  23. package/dist/planmagnet/dashboards/DashboardUI.js +7 -7
  24. package/dist/planmagnet/dashboards/cards/RiskSeverityCard.js +4 -4
  25. package/dist/planmagnet/forecast/DeliveryForecastCard.js +8 -8
  26. package/dist/planmagnet/forecast/ProjectOutlookCard.js +10 -10
  27. package/dist/planmagnet/forecast/ProjectRiskSummaryCard.js +9 -9
  28. package/dist/planmagnet/forecast/ResourceForecastCard.js +6 -6
  29. package/dist/planmagnet/hooks/useLiveRefresh.d.ts +33 -2
  30. package/dist/planmagnet/hooks/useLiveRefresh.d.ts.map +1 -1
  31. package/dist/planmagnet/hooks/useLiveRefresh.js +71 -42
  32. package/dist/planmagnet/hooks/useLiveRefresh.js.map +1 -1
  33. package/dist/planmagnet/integrations/syncTypes.d.ts +2 -1
  34. package/dist/planmagnet/integrations/syncTypes.d.ts.map +1 -1
  35. package/dist/planmagnet/integrations/syncTypes.js +19 -9
  36. package/dist/planmagnet/integrations/syncTypes.js.map +1 -1
  37. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +8 -8
  38. package/dist/planmagnet/pages/DashboardDashboardPagePage.js +23 -23
  39. package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
  40. package/dist/planmagnet/pages/DocsDocDetailPagePage.js +340 -248
  41. package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
  42. package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.d.ts.map +1 -1
  43. package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.js +172 -167
  44. package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.js.map +1 -1
  45. package/dist/planmagnet/pages/IntegrationsSyncsPagePage.d.ts.map +1 -1
  46. package/dist/planmagnet/pages/IntegrationsSyncsPagePage.js +92 -83
  47. package/dist/planmagnet/pages/IntegrationsSyncsPagePage.js.map +1 -1
  48. package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.d.ts +2 -0
  49. package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.d.ts.map +1 -0
  50. package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.js +175 -0
  51. package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.js.map +1 -0
  52. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
  53. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +151 -126
  54. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
  55. package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
  56. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +136 -129
  57. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
  58. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.d.ts.map +1 -1
  59. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +149 -119
  60. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
  61. package/dist/planmagnet/pages/ProductsProductsPagePage.js +13 -13
  62. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.d.ts.map +1 -1
  63. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js +153 -117
  64. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js.map +1 -1
  65. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.d.ts.map +1 -1
  66. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js +173 -138
  67. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js.map +1 -1
  68. package/dist/planmagnet/pages/ProjectsAtRiskPagePage.js +9 -9
  69. package/dist/planmagnet/pages/ProjectsBudgetsPagePage.js +8 -8
  70. package/dist/planmagnet/pages/ProjectsProjectsPagePage.js +12 -12
  71. package/dist/planmagnet/pages/ReportsReportsPagePage.js +23 -23
  72. package/dist/planmagnet/pages/TasksMyDashboardPagePage.js +18 -18
  73. package/dist/planmagnet/pages/TasksMyWorkPagePage.js +10 -10
  74. package/dist/planmagnet/pages/TasksTimeEntriesPagePage.js +5 -5
  75. package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js +8 -8
  76. package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js +6 -6
  77. package/dist/planmagnet/pages/analytics/MetricsCard.js +6 -6
  78. package/dist/planmagnet/pages/analytics/QualityMetricsCard.js +6 -6
  79. package/dist/planmagnet/pages/analytics/SprintDeliveryCards.js +8 -8
  80. package/dist/planmagnet/pages/analytics/StatusFlowCard.js +7 -7
  81. package/dist/planmagnet/pages/analytics/ThroughputCard.js +7 -7
  82. package/dist/planmagnet/pages/analytics/WorkBreakdownCard.js +7 -7
  83. package/dist/planmagnet/people/People.d.ts +14 -0
  84. package/dist/planmagnet/people/People.d.ts.map +1 -1
  85. package/dist/planmagnet/people/People.js +36 -22
  86. package/dist/planmagnet/people/People.js.map +1 -1
  87. package/dist/planmagnet/roadmaps/RoadmapNowNextLater.d.ts.map +1 -1
  88. package/dist/planmagnet/roadmaps/RoadmapNowNextLater.js +171 -153
  89. package/dist/planmagnet/roadmaps/RoadmapNowNextLater.js.map +1 -1
  90. package/dist/planmagnet/webhooks/webhookTypes.d.ts +54 -0
  91. package/dist/planmagnet/webhooks/webhookTypes.d.ts.map +1 -0
  92. package/dist/planmagnet/webhooks/webhookTypes.js +17 -0
  93. package/dist/planmagnet/webhooks/webhookTypes.js.map +1 -0
  94. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ProductsFeaturesPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProductsFeaturesPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Badge, Button } from '@burdenoff/fe-libs/ui';\nimport {\n Star,\n Plus,\n ThumbsUp,\n MessageSquare,\n Target,\n Filter,\n ArrowUpDown,\n Loader2,\n Gauge,\n} from 'lucide-react';\nimport { countLabel, useFeatureCounts } from '../hooks/useCounts';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { rowLinkProps, detailPath } from '../utils/rowNavigation';\nimport { PersonName } from '../people/People';\nimport { FeatureTriageActions, isSnoozed } from '../controls/FeatureTriage';\nimport {\n MOSCOW_VALUES,\n MOSCOW_META,\n KANO_VALUES,\n KANO_META,\n QUADRANT_VALUES,\n QUADRANT_META,\n QuadrantBadge,\n riceScoreMissingHint,\n} from '../controls/PrioritizationControls';\n\nconst LIST_FEATURE_REQUESTS = gql`\n query ListFeatureRequests(\n $workspaceId: ID!\n $filter: PlanmagnetFeatureRequestFilter\n $pagination: PlanmagnetPaginationInput\n $orderBy: PlanmagnetFeatureOrder\n ) {\n planmagnetFeatureRequests(\n workspaceId: $workspaceId\n filter: $filter\n pagination: $pagination\n orderBy: $orderBy\n ) {\n items {\n id\n title\n description\n status\n businessValue\n effortEstimate\n priorityScore\n voteCount\n submittedBy\n tags\n projectId\n snoozedUntil\n moscow\n kano\n valueEffortQuadrant\n reach\n impact\n confidence\n effortScore\n riceScore\n createdAt\n updatedAt\n }\n totalCount\n }\n }\n`;\n\ninterface FeatureRequestNode {\n id: string;\n title: string;\n description: string | null;\n status: string;\n businessValue: number | null;\n effortEstimate: string | null;\n priorityScore: number | null;\n voteCount: number;\n submittedBy: string;\n tags: string[];\n reach: number | null;\n impact: number | null;\n confidence: number | null;\n effortScore: number | null;\n riceScore: number | null;\n createdAt: string;\n updatedAt: string;\n}\n\nconst statusColor: Record<string, string> = {\n SUBMITTED: 'bg-gray-100 text-gray-800',\n UNDER_REVIEW: 'bg-yellow-100 text-yellow-800',\n PLANNED: 'bg-blue-100 text-blue-800',\n IN_PROGRESS: 'bg-purple-100 text-purple-800',\n COMPLETED: 'bg-green-100 text-green-800',\n DECLINED: 'bg-red-100 text-red-800',\n DUPLICATE: 'bg-orange-100 text-orange-800',\n};\n\nexport function ProductsFeaturesPagePage() {\n const navigate = usePlanMagnetNavigate();\n const { workspaceId } = usePlanMagnet();\n const [statusFilter, setStatusFilter] = useState<string>('all');\n // Snooze state is read against the moment the page opened (Date.now() may not run during render).\n const [now] = useState(() => Date.now());\n // Sorting happens on the server over every request (BOFF-7965: it re-sorted only the 50 loaded rows).\n const [sortBy, setSortBy] = useState<'NEWEST' | 'MOST_VOTED'>('NEWEST');\n // PRM-26: riceScore is computed, not a stored column (utils/prioritization.ts#computeRiceScore) — there is\n // no server `orderBy` value for it (PlanmagnetFeatureOrder only has NEWEST/MOST_VOTED), so this can only ever\n // re-sort the page already loaded above, never every matching request. Kept as its own toggle, independent\n // of `sortBy`, specifically so it is never confused for a third server-side order — the BOFF-7965 regression\n // this file's own comment above warns about.\n const [riceSort, setRiceSort] = useState(false);\n // PRM-27: MoSCoW/Kano/quadrant are server filters too, combined with the status tabs above — not a\n // client-side re-filter of the 50 loaded rows (same reasoning as BOFF-7965 for sorting).\n const [moscowFilter, setMoscowFilter] = useState('');\n const [kanoFilter, setKanoFilter] = useState('');\n const [quadrantFilter, setQuadrantFilter] = useState('');\n const counts = useFeatureCounts();\n\n const baseFilter =\n statusFilter === 'TRIAGE'\n ? { triage: true }\n : statusFilter === 'SNOOZED'\n ? { snoozed: true }\n : statusFilter !== 'all'\n ? { status: statusFilter }\n : {};\n const filter = {\n ...baseFilter,\n ...(moscowFilter ? { moscow: moscowFilter } : {}),\n ...(kanoFilter ? { kano: kanoFilter } : {}),\n ...(quadrantFilter ? { quadrant: quadrantFilter } : {}),\n };\n\n const {\n data,\n loading,\n error,\n } = // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n useQuery<Record<string, any>>(LIST_FEATURE_REQUESTS, {\n variables: {\n workspaceId,\n filter: Object.keys(filter).length > 0 ? filter : undefined,\n pagination: { first: 50 },\n orderBy: sortBy,\n },\n skip: !workspaceId,\n });\n\n // Only before the first load: a refetch must not blank the page (it\n\n // unmounts every component on it and loses their state).\n\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-destructive\">Failed to load features: {error.message}</div>;\n }\n\n const features: FeatureRequestNode[] = data?.planmagnetFeatureRequests?.items ?? [];\n const totalCount = data?.planmagnetFeatureRequests?.totalCount ?? 0;\n\n // PRM-26 — client-side only (see the `riceSort` state comment above): never claims to be a full re-sort of\n // every matching request, only of the up-to-50 rows the server already returned in `sortBy` order. Unscored\n // (null) rows sort last either way, never first as if they scored 0.\n const sorted = riceSort\n ? [...features].sort((a, b) => {\n if (a.riceScore == null && b.riceScore == null) return 0;\n if (a.riceScore == null) return 1;\n if (b.riceScore == null) return -1;\n return b.riceScore - a.riceScore;\n })\n : features;\n\n const statuses = [\n 'all',\n 'TRIAGE',\n 'SNOOZED',\n 'SUBMITTED',\n 'UNDER_REVIEW',\n 'PLANNED',\n 'IN_PROGRESS',\n 'COMPLETED',\n 'DECLINED',\n 'DUPLICATE',\n ];\n\n // Tiles are unfiltered server counts; the tabs change only the list (BOFF-7965).\n const stats = {\n total: countLabel(counts?.all),\n open: countLabel(counts?.open),\n inProgress: countLabel(counts?.byStatus.IN_PROGRESS),\n completed: countLabel(counts?.byStatus.COMPLETED),\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">Features</h1>\n {/* Under a tab, the count is of what matches it (BOFF-7990). */}\n <p className=\"text-sm text-muted-foreground\">\n {statusFilter === 'all'\n ? `${stats.total} feature requests`\n : statusFilter === 'TRIAGE'\n ? `${totalCount} waiting for triage`\n : `${totalCount} ${statusFilter.toLowerCase().replace('_', ' ')} match`}\n </p>\n </div>\n <Button size=\"sm\" onClick={() => navigate('/products/new-feature-page')}>\n <Plus className=\"mr-1 h-4 w-4\" />\n New Feature\n </Button>\n </div>\n\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n <Card>\n <CardContent className=\"pt-4\">\n <p className=\"text-xs text-muted-foreground\">Total Features</p>\n <p className=\"text-xl font-bold\">{stats.total}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <p className=\"text-xs text-muted-foreground\">In Progress</p>\n <p className=\"text-xl font-bold\">{stats.inProgress}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <p className=\"text-xs text-muted-foreground\">Completed</p>\n <p className=\"text-xl font-bold\">{stats.completed}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <p className=\"text-xs text-muted-foreground\">Open</p>\n <p className=\"text-xl font-bold\">{stats.open}</p>\n </CardContent>\n </Card>\n </div>\n\n <div className=\"flex items-center justify-between flex-wrap gap-2\">\n <div className=\"flex items-center gap-2\">\n <Filter className=\"h-4 w-4 text-muted-foreground\" />\n {statuses.map((s) => (\n <button\n key={s}\n onClick={() => setStatusFilter(s)}\n className={`px-2 py-1 text-xs rounded-md capitalize ${statusFilter === s ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}\n >\n {s === 'all'\n ? 'All'\n : s === 'TRIAGE'\n ? `Triage (${countLabel(counts?.triage)})`\n : s === 'SNOOZED'\n ? `Snoozed (${countLabel(counts?.snoozed)})`\n : s.toLowerCase().replace('_', ' ')}\n </button>\n ))}\n </div>\n <div className=\"flex items-center gap-2\">\n <ArrowUpDown className=\"h-4 w-4 text-muted-foreground\" />\n {(['NEWEST', 'MOST_VOTED'] as const).map((s) => (\n <button\n key={s}\n onClick={() => setSortBy(s)}\n className={`px-2 py-1 text-xs rounded-md ${!riceSort && sortBy === s ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}\n >\n {s === 'NEWEST' ? 'Newest' : 'Most voted'}\n </button>\n ))}\n {/* PRM-26 — see the `riceSort` state comment: re-sorts only the rows already loaded above, never a\n server-side order (PlanmagnetFeatureOrder has no RICE value). */}\n <button\n type=\"button\"\n aria-pressed={riceSort}\n data-testid=\"feature-sort-rice\"\n onClick={() => setRiceSort((v) => !v)}\n className={`px-2 py-1 text-xs rounded-md ${riceSort ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}\n >\n RICE score\n </button>\n </div>\n </div>\n {riceSort && (\n <p className=\"text-xs text-muted-foreground\" data-testid=\"feature-sort-rice-note\">\n Sorted by RICE score within the {sorted.length} loaded results above — not a server-wide\n sort.\n </p>\n )}\n\n {/* PRM-27: MoSCoW/Kano/Value-Effort quadrant filters — server-side, combined with the status tabs above. */}\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"flex items-center gap-1\">\n <label htmlFor=\"feature-filter-moscow\" className=\"text-xs text-muted-foreground\">\n MoSCoW\n </label>\n <select\n id=\"feature-filter-moscow\"\n data-testid=\"feature-filter-moscow\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-xs\"\n value={moscowFilter}\n onChange={(e) => setMoscowFilter(e.target.value)}\n >\n <option value=\"\">All</option>\n {MOSCOW_VALUES.map((v) => (\n <option key={v} value={v}>\n {MOSCOW_META[v].label}\n </option>\n ))}\n </select>\n </div>\n <div className=\"flex items-center gap-1\">\n <label htmlFor=\"feature-filter-kano\" className=\"text-xs text-muted-foreground\">\n Kano\n </label>\n <select\n id=\"feature-filter-kano\"\n data-testid=\"feature-filter-kano\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-xs\"\n value={kanoFilter}\n onChange={(e) => setKanoFilter(e.target.value)}\n >\n <option value=\"\">All</option>\n {KANO_VALUES.map((v) => (\n <option key={v} value={v}>\n {KANO_META[v].label}\n </option>\n ))}\n </select>\n </div>\n <div className=\"flex items-center gap-1\">\n <label htmlFor=\"feature-filter-quadrant\" className=\"text-xs text-muted-foreground\">\n Quadrant\n </label>\n <select\n id=\"feature-filter-quadrant\"\n data-testid=\"feature-filter-quadrant\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-xs\"\n value={quadrantFilter}\n onChange={(e) => setQuadrantFilter(e.target.value)}\n >\n <option value=\"\">All</option>\n {QUADRANT_VALUES.map((v) => (\n <option key={v} value={v}>\n {QUADRANT_META[v].label}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n <div className=\"space-y-3\">\n {sorted.map((feature) => (\n <Card\n key={feature.id}\n {...rowLinkProps(\n () => navigate(detailPath('/products/feature-detail-page', feature.id)),\n feature.title\n )}\n >\n <CardContent className=\"py-4\">\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex-1 space-y-2\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm font-medium\">{feature.title}</span>\n <span\n className={`text-[10px] px-1.5 py-0.5 rounded-full capitalize ${statusColor[feature.status] ?? 'bg-gray-100'}`}\n >\n {feature.status.toLowerCase().replace('_', ' ')}\n </span>\n {/* PRM-27 — hidden when unscored so most cards (not yet prioritized) stay uncluttered. */}\n <QuadrantBadge quadrant={feature.valueEffortQuadrant} hideWhenUnscored />\n </div>\n <p className=\"text-xs text-muted-foreground\">{feature.description}</p>\n <div className=\"flex items-center gap-4 text-xs text-muted-foreground\">\n <span>Effort: {feature.effortEstimate ?? 'N/A'}</span>\n {/* By name, not user UUID (BOFF-7950). */}\n <span>\n Submitted by: <PersonName id={feature.submittedBy} />\n </span>\n <span>{new Date(feature.updatedAt).toLocaleDateString()}</span>\n </div>\n {isSnoozed(feature.snoozedUntil, now) && (\n <p className=\"text-xs text-muted-foreground\">\n Snoozed until {new Date(feature.snoozedUntil).toLocaleDateString()}\n </p>\n )}\n <FeatureTriageActions feature={feature} now={now} />\n {feature.tags.length > 0 && (\n <div className=\"flex gap-1\">\n {feature.tags.map((tag) => (\n <Badge key={tag} variant=\"outline\" className=\"text-[10px]\">\n {tag}\n </Badge>\n ))}\n </div>\n )}\n </div>\n <div className=\"flex items-center gap-4 shrink-0\">\n <div className=\"text-center\">\n <div className=\"flex items-center gap-1\">\n <Target className=\"h-3 w-3 text-primary\" />\n <span className=\"text-sm font-bold\">{feature.priorityScore ?? 0}</span>\n </div>\n <p className=\"text-[10px] text-muted-foreground\">Priority</p>\n </div>\n <div className=\"text-center\">\n <div className=\"flex items-center gap-1\">\n <ThumbsUp className=\"h-3 w-3 text-green-500\" />\n <span className=\"text-sm font-bold\">{feature.voteCount}</span>\n </div>\n <p className=\"text-[10px] text-muted-foreground\">Votes</p>\n </div>\n <div className=\"text-center\">\n <div className=\"flex items-center gap-1\">\n <Star className=\"h-3 w-3 text-yellow-500\" />\n <span className=\"text-sm font-bold\">{feature.businessValue ?? 0}</span>\n </div>\n <p className=\"text-[10px] text-muted-foreground\">Value</p>\n </div>\n {/* PRM-26 */}\n <div\n className=\"text-center\"\n title={\n feature.riceScore == null\n ? (riceScoreMissingHint(feature) ?? undefined)\n : undefined\n }\n >\n <div className=\"flex items-center gap-1\">\n <Gauge className=\"h-3 w-3 text-primary\" />\n <span className=\"text-sm font-bold\" data-testid=\"feature-rice-score-cell\">\n {feature.riceScore == null ? '—' : feature.riceScore.toFixed(1)}\n </span>\n </div>\n <p className=\"text-[10px] text-muted-foreground\">RICE</p>\n </div>\n </div>\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n {sorted.length === 0 && (\n <p className=\"text-center text-muted-foreground py-8\">\n {statusFilter === 'TRIAGE'\n ? 'Nothing waits for triage.'\n : 'No features match the selected filters.'}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ProductsFeaturesPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProductsFeaturesPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Badge, Button } from '@burdenoff/fe-libs/ui';\nimport {\n Star,\n Plus,\n ThumbsUp,\n MessageSquare,\n Target,\n Filter,\n ArrowUpDown,\n Loader2,\n Gauge,\n} from 'lucide-react';\nimport { countLabel, useFeatureCounts } from '../hooks/useCounts';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { rowLinkProps, detailPath } from '../utils/rowNavigation';\nimport { PersonName } from '../people/People';\nimport { FeatureTriageActions, isSnoozed } from '../controls/FeatureTriage';\nimport { CategoryBadge, RatingDisplay } from '../controls/FeedbackControls';\nimport {\n MOSCOW_VALUES,\n MOSCOW_META,\n KANO_VALUES,\n KANO_META,\n QUADRANT_VALUES,\n QUADRANT_META,\n QuadrantBadge,\n riceScoreMissingHint,\n} from '../controls/PrioritizationControls';\n\nconst LIST_FEATURE_REQUESTS = gql`\n query ListFeatureRequests(\n $workspaceId: ID!\n $filter: PlanmagnetFeatureRequestFilter\n $pagination: PlanmagnetPaginationInput\n $orderBy: PlanmagnetFeatureOrder\n ) {\n planmagnetFeatureRequests(\n workspaceId: $workspaceId\n filter: $filter\n pagination: $pagination\n orderBy: $orderBy\n ) {\n items {\n id\n title\n description\n status\n businessValue\n effortEstimate\n priorityScore\n voteCount\n submittedBy\n tags\n projectId\n snoozedUntil\n moscow\n kano\n valueEffortQuadrant\n reach\n impact\n confidence\n effortScore\n riceScore\n rating\n category\n createdAt\n updatedAt\n }\n totalCount\n }\n }\n`;\n\ninterface FeatureRequestNode {\n id: string;\n title: string;\n description: string | null;\n status: string;\n businessValue: number | null;\n effortEstimate: string | null;\n priorityScore: number | null;\n voteCount: number;\n submittedBy: string;\n tags: string[];\n reach: number | null;\n impact: number | null;\n confidence: number | null;\n effortScore: number | null;\n riceScore: number | null;\n rating: number | null;\n category: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nconst statusColor: Record<string, string> = {\n SUBMITTED: 'bg-gray-100 text-gray-800',\n UNDER_REVIEW: 'bg-yellow-100 text-yellow-800',\n PLANNED: 'bg-blue-100 text-blue-800',\n IN_PROGRESS: 'bg-purple-100 text-purple-800',\n COMPLETED: 'bg-green-100 text-green-800',\n DECLINED: 'bg-red-100 text-red-800',\n DUPLICATE: 'bg-orange-100 text-orange-800',\n};\n\nexport function ProductsFeaturesPagePage() {\n const navigate = usePlanMagnetNavigate();\n const { workspaceId } = usePlanMagnet();\n const [statusFilter, setStatusFilter] = useState<string>('all');\n // Snooze state is read against the moment the page opened (Date.now() may not run during render).\n const [now] = useState(() => Date.now());\n // Sorting happens on the server over every request (BOFF-7965: it re-sorted only the 50 loaded rows).\n const [sortBy, setSortBy] = useState<'NEWEST' | 'MOST_VOTED'>('NEWEST');\n // PRM-26: riceScore is computed, not a stored column (utils/prioritization.ts#computeRiceScore) — there is\n // no server `orderBy` value for it (PlanmagnetFeatureOrder only has NEWEST/MOST_VOTED), so this can only ever\n // re-sort the page already loaded above, never every matching request. Kept as its own toggle, independent\n // of `sortBy`, specifically so it is never confused for a third server-side order — the BOFF-7965 regression\n // this file's own comment above warns about.\n const [riceSort, setRiceSort] = useState(false);\n // PRM-27: MoSCoW/Kano/quadrant are server filters too, combined with the status tabs above — not a\n // client-side re-filter of the 50 loaded rows (same reasoning as BOFF-7965 for sorting).\n const [moscowFilter, setMoscowFilter] = useState('');\n const [kanoFilter, setKanoFilter] = useState('');\n const [quadrantFilter, setQuadrantFilter] = useState('');\n const counts = useFeatureCounts();\n\n const baseFilter =\n statusFilter === 'TRIAGE'\n ? { triage: true }\n : statusFilter === 'SNOOZED'\n ? { snoozed: true }\n : statusFilter !== 'all'\n ? { status: statusFilter }\n : {};\n const filter = {\n ...baseFilter,\n ...(moscowFilter ? { moscow: moscowFilter } : {}),\n ...(kanoFilter ? { kano: kanoFilter } : {}),\n ...(quadrantFilter ? { quadrant: quadrantFilter } : {}),\n };\n\n const {\n data,\n loading,\n error,\n } = // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n useQuery<Record<string, any>>(LIST_FEATURE_REQUESTS, {\n variables: {\n workspaceId,\n filter: Object.keys(filter).length > 0 ? filter : undefined,\n pagination: { first: 50 },\n orderBy: sortBy,\n },\n skip: !workspaceId,\n });\n\n // Only before the first load: a refetch must not blank the page (it\n\n // unmounts every component on it and loses their state).\n\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-destructive\">Failed to load features: {error.message}</div>;\n }\n\n const features: FeatureRequestNode[] = data?.planmagnetFeatureRequests?.items ?? [];\n const totalCount = data?.planmagnetFeatureRequests?.totalCount ?? 0;\n\n // PRM-26 — client-side only (see the `riceSort` state comment above): never claims to be a full re-sort of\n // every matching request, only of the up-to-50 rows the server already returned in `sortBy` order. Unscored\n // (null) rows sort last either way, never first as if they scored 0.\n const sorted = riceSort\n ? [...features].sort((a, b) => {\n if (a.riceScore == null && b.riceScore == null) return 0;\n if (a.riceScore == null) return 1;\n if (b.riceScore == null) return -1;\n return b.riceScore - a.riceScore;\n })\n : features;\n\n const statuses = [\n 'all',\n 'TRIAGE',\n 'SNOOZED',\n 'SUBMITTED',\n 'UNDER_REVIEW',\n 'PLANNED',\n 'IN_PROGRESS',\n 'COMPLETED',\n 'DECLINED',\n 'DUPLICATE',\n ];\n\n // Tiles are unfiltered server counts; the tabs change only the list (BOFF-7965).\n const stats = {\n total: countLabel(counts?.all),\n open: countLabel(counts?.open),\n inProgress: countLabel(counts?.byStatus.IN_PROGRESS),\n completed: countLabel(counts?.byStatus.COMPLETED),\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">Features</h1>\n {/* Under a tab, the count is of what matches it (BOFF-7990). */}\n <p className=\"text-sm text-muted-foreground\">\n {statusFilter === 'all'\n ? `${stats.total} feature requests`\n : statusFilter === 'TRIAGE'\n ? `${totalCount} waiting for triage`\n : `${totalCount} ${statusFilter.toLowerCase().replace('_', ' ')} match`}\n </p>\n </div>\n <Button size=\"sm\" onClick={() => navigate('/products/new-feature-page')}>\n <Plus className=\"mr-1 h-4 w-4\" />\n New Feature\n </Button>\n </div>\n\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n <Card>\n <CardContent className=\"pt-4\">\n <p className=\"text-xs text-muted-foreground\">Total Features</p>\n <p className=\"text-xl font-bold\">{stats.total}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <p className=\"text-xs text-muted-foreground\">In Progress</p>\n <p className=\"text-xl font-bold\">{stats.inProgress}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <p className=\"text-xs text-muted-foreground\">Completed</p>\n <p className=\"text-xl font-bold\">{stats.completed}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <p className=\"text-xs text-muted-foreground\">Open</p>\n <p className=\"text-xl font-bold\">{stats.open}</p>\n </CardContent>\n </Card>\n </div>\n\n <div className=\"flex items-center justify-between flex-wrap gap-2\">\n <div className=\"flex items-center gap-2\">\n <Filter className=\"h-4 w-4 text-muted-foreground\" />\n {statuses.map((s) => (\n <button\n key={s}\n onClick={() => setStatusFilter(s)}\n className={`px-2 py-1 text-xs rounded-md capitalize ${statusFilter === s ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}\n >\n {s === 'all'\n ? 'All'\n : s === 'TRIAGE'\n ? `Triage (${countLabel(counts?.triage)})`\n : s === 'SNOOZED'\n ? `Snoozed (${countLabel(counts?.snoozed)})`\n : s.toLowerCase().replace('_', ' ')}\n </button>\n ))}\n </div>\n <div className=\"flex items-center gap-2\">\n <ArrowUpDown className=\"h-4 w-4 text-muted-foreground\" />\n {(['NEWEST', 'MOST_VOTED'] as const).map((s) => (\n <button\n key={s}\n onClick={() => setSortBy(s)}\n className={`px-2 py-1 text-xs rounded-md ${!riceSort && sortBy === s ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}\n >\n {s === 'NEWEST' ? 'Newest' : 'Most voted'}\n </button>\n ))}\n {/* PRM-26 — see the `riceSort` state comment: re-sorts only the rows already loaded above, never a\n server-side order (PlanmagnetFeatureOrder has no RICE value). */}\n <button\n type=\"button\"\n aria-pressed={riceSort}\n data-testid=\"feature-sort-rice\"\n onClick={() => setRiceSort((v) => !v)}\n className={`px-2 py-1 text-xs rounded-md ${riceSort ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}\n >\n RICE score\n </button>\n </div>\n </div>\n {riceSort && (\n <p className=\"text-xs text-muted-foreground\" data-testid=\"feature-sort-rice-note\">\n Sorted by RICE score within the {sorted.length} loaded results above — not a server-wide\n sort.\n </p>\n )}\n\n {/* PRM-27: MoSCoW/Kano/Value-Effort quadrant filters — server-side, combined with the status tabs above. */}\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"flex items-center gap-1\">\n <label htmlFor=\"feature-filter-moscow\" className=\"text-xs text-muted-foreground\">\n MoSCoW\n </label>\n <select\n id=\"feature-filter-moscow\"\n data-testid=\"feature-filter-moscow\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-xs\"\n value={moscowFilter}\n onChange={(e) => setMoscowFilter(e.target.value)}\n >\n <option value=\"\">All</option>\n {MOSCOW_VALUES.map((v) => (\n <option key={v} value={v}>\n {MOSCOW_META[v].label}\n </option>\n ))}\n </select>\n </div>\n <div className=\"flex items-center gap-1\">\n <label htmlFor=\"feature-filter-kano\" className=\"text-xs text-muted-foreground\">\n Kano\n </label>\n <select\n id=\"feature-filter-kano\"\n data-testid=\"feature-filter-kano\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-xs\"\n value={kanoFilter}\n onChange={(e) => setKanoFilter(e.target.value)}\n >\n <option value=\"\">All</option>\n {KANO_VALUES.map((v) => (\n <option key={v} value={v}>\n {KANO_META[v].label}\n </option>\n ))}\n </select>\n </div>\n <div className=\"flex items-center gap-1\">\n <label htmlFor=\"feature-filter-quadrant\" className=\"text-xs text-muted-foreground\">\n Quadrant\n </label>\n <select\n id=\"feature-filter-quadrant\"\n data-testid=\"feature-filter-quadrant\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-xs\"\n value={quadrantFilter}\n onChange={(e) => setQuadrantFilter(e.target.value)}\n >\n <option value=\"\">All</option>\n {QUADRANT_VALUES.map((v) => (\n <option key={v} value={v}>\n {QUADRANT_META[v].label}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n <div className=\"space-y-3\">\n {sorted.map((feature) => (\n <Card\n key={feature.id}\n {...rowLinkProps(\n () => navigate(detailPath('/products/feature-detail-page', feature.id)),\n feature.title\n )}\n >\n <CardContent className=\"py-4\">\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex-1 space-y-2\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm font-medium\">{feature.title}</span>\n <span\n className={`text-[10px] px-1.5 py-0.5 rounded-full capitalize ${statusColor[feature.status] ?? 'bg-gray-100'}`}\n >\n {feature.status.toLowerCase().replace('_', ' ')}\n </span>\n {/* PRM-27 — hidden when unscored so most cards (not yet prioritized) stay uncluttered. */}\n <QuadrantBadge quadrant={feature.valueEffortQuadrant} hideWhenUnscored />\n </div>\n <p className=\"text-xs text-muted-foreground\">{feature.description}</p>\n <div className=\"flex items-center gap-4 text-xs text-muted-foreground\">\n <span>Effort: {feature.effortEstimate ?? 'N/A'}</span>\n {/* By name, not user UUID (BOFF-7950). */}\n <span>\n Submitted by: <PersonName id={feature.submittedBy} />\n </span>\n <span>{new Date(feature.updatedAt).toLocaleDateString()}</span>\n </div>\n {/* CWF-15: structured feedback — rating/category, hidden when neither is set. */}\n {(feature.rating != null || feature.category) && (\n <div className=\"flex items-center gap-2\">\n {feature.rating != null && <RatingDisplay rating={feature.rating} />}\n <CategoryBadge category={feature.category} />\n </div>\n )}\n {isSnoozed(feature.snoozedUntil, now) && (\n <p className=\"text-xs text-muted-foreground\">\n Snoozed until {new Date(feature.snoozedUntil).toLocaleDateString()}\n </p>\n )}\n <FeatureTriageActions feature={feature} now={now} />\n {feature.tags.length > 0 && (\n <div className=\"flex gap-1\">\n {feature.tags.map((tag) => (\n <Badge key={tag} variant=\"outline\" className=\"text-[10px]\">\n {tag}\n </Badge>\n ))}\n </div>\n )}\n </div>\n <div className=\"flex items-center gap-4 shrink-0\">\n <div className=\"text-center\">\n <div className=\"flex items-center gap-1\">\n <Target className=\"h-3 w-3 text-primary\" />\n <span className=\"text-sm font-bold\">{feature.priorityScore ?? 0}</span>\n </div>\n <p className=\"text-[10px] text-muted-foreground\">Priority</p>\n </div>\n <div className=\"text-center\">\n <div className=\"flex items-center gap-1\">\n <ThumbsUp className=\"h-3 w-3 text-green-500\" />\n <span className=\"text-sm font-bold\">{feature.voteCount}</span>\n </div>\n <p className=\"text-[10px] text-muted-foreground\">Votes</p>\n </div>\n <div className=\"text-center\">\n <div className=\"flex items-center gap-1\">\n <Star className=\"h-3 w-3 text-yellow-500\" />\n <span className=\"text-sm font-bold\">{feature.businessValue ?? 0}</span>\n </div>\n <p className=\"text-[10px] text-muted-foreground\">Value</p>\n </div>\n {/* PRM-26 */}\n <div\n className=\"text-center\"\n title={\n feature.riceScore == null\n ? (riceScoreMissingHint(feature) ?? undefined)\n : undefined\n }\n >\n <div className=\"flex items-center gap-1\">\n <Gauge className=\"h-3 w-3 text-primary\" />\n <span className=\"text-sm font-bold\" data-testid=\"feature-rice-score-cell\">\n {feature.riceScore == null ? '—' : feature.riceScore.toFixed(1)}\n </span>\n </div>\n <p className=\"text-[10px] text-muted-foreground\">RICE</p>\n </div>\n </div>\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n {sorted.length === 0 && (\n <p className=\"text-center text-muted-foreground py-8\">\n {statusFilter === 'TRIAGE'\n ? 'Nothing waits for triage.'\n : 'No features match the selected filters.'}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ProductsNewFeaturePagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsNewFeaturePagePage.tsx"],"names":[],"mappings":"AAmCA,wBAAgB,0BAA0B,4CAkSzC"}
1
+ {"version":3,"file":"ProductsNewFeaturePagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsNewFeaturePagePage.tsx"],"names":[],"mappings":"AA4CA,wBAAgB,0BAA0B,4CAsUzC"}
@@ -3,14 +3,15 @@ import { throwOnError as n } from "../utils/mutationResult.js";
3
3
  import { ProjectSelect as r, WorkItemSearchSelect as i } from "../controls/Pickers.js";
4
4
  import { parseIntField as a } from "../utils/formValidation.js";
5
5
  import { EMPTY_PRIORITIZATION_FORM as o, EMPTY_RICE_FORM as s, PrioritizationScoreFields as c, RiceScoreFields as l, prioritizationScoresValid as u, riceInputsValid as d } from "../controls/PrioritizationControls.js";
6
- import { useState as f } from "react";
7
- import { Loader2 as p, Save as m, Star as h, X as g } from "lucide-react";
8
- import { gql as _ } from "@apollo/client";
9
- import { useMutation as v } from "@apollo/client/react";
10
- import { jsx as y, jsxs as b } from "react/jsx-runtime";
11
- import { Button as x, Card as S, CardContent as C, CardHeader as w, CardTitle as T, Input as E } from "@burdenoff/fe-libs/ui";
6
+ import { AnonymousSubmissionCheckbox as f, EMPTY_FEEDBACK_FORM as p, FeedbackFields as m } from "../controls/FeedbackControls.js";
7
+ import { useState as h } from "react";
8
+ import { Loader2 as g, Save as _, Star as v, X as ee } from "lucide-react";
9
+ import { gql as y } from "@apollo/client";
10
+ import { useMutation as b } from "@apollo/client/react";
11
+ import { jsx as x, jsxs as S } from "react/jsx-runtime";
12
+ import { Button as C, Card as w, CardContent as T, CardHeader as E, CardTitle as D, Input as O } from "@burdenoff/fe-libs/ui";
12
13
  //#region src/planmagnet/pages/ProductsNewFeaturePagePage.tsx
13
- var D = _`
14
+ var k = y`
14
15
  mutation CreateFeatureRequest($input: PlanmagnetCreateFeatureRequestInput!) {
15
16
  planmagnetCreateFeatureRequest(input: $input) {
16
17
  id
@@ -19,11 +20,14 @@ var D = _`
19
20
  businessValue
20
21
  effortEstimate
21
22
  priorityScore
23
+ rating
24
+ category
25
+ isAnonymous
22
26
  }
23
27
  }
24
28
  `;
25
- function O() {
26
- let { workspaceId: _ } = e(), O = t(), [k, A] = f({
29
+ function A() {
30
+ let { workspaceId: y } = e(), A = t(), [j, M] = h({
27
31
  title: "",
28
32
  description: "",
29
33
  status: "SUBMITTED",
@@ -32,118 +36,124 @@ function O() {
32
36
  tags: "",
33
37
  projectId: "",
34
38
  workItemId: ""
35
- }), [j, M] = f(o), [N, P] = f(s), [F, { loading: I }] = v(D, { refetchQueries: ["ListFeatureRequests"] }), [L, R] = f(null), z = a(k.businessValue, {
39
+ }), [N, P] = h(o), [F, I] = h(s), [L, R] = h(p), [z, B] = h(!1), [V, { loading: H }] = b(k, { refetchQueries: ["ListFeatureRequests"] }), [U, W] = h(null), G = a(j.businessValue, {
36
40
  min: 0,
37
41
  max: 100
38
- }), B = u(j), V = a(j.valueScore, {
42
+ }), K = u(N), q = a(N.valueScore, {
39
43
  min: 1,
40
44
  max: 10
41
- }), H = a(j.effortScore, {
45
+ }), J = a(N.effortScore, {
42
46
  min: 1,
43
47
  max: 10
44
- }), U = d(N), W = a(N.reach, {
48
+ }), Y = d(F), X = a(F.reach, {
45
49
  min: 1,
46
50
  max: 10
47
- }), G = a(N.impact, {
51
+ }), Z = a(F.impact, {
48
52
  min: 1,
49
53
  max: 10
50
- }), K = a(N.confidence, {
54
+ }), Q = a(F.confidence, {
51
55
  min: 1,
52
56
  max: 10
57
+ }), $ = a(L.rating, {
58
+ min: 1,
59
+ max: 5
53
60
  });
54
- return /* @__PURE__ */ b("div", {
61
+ return /* @__PURE__ */ S("div", {
55
62
  className: "space-y-6 p-6 max-w-4xl",
56
63
  children: [
57
- /* @__PURE__ */ b("div", {
64
+ /* @__PURE__ */ S("div", {
58
65
  className: "flex items-center justify-between",
59
- children: [/* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("h1", {
66
+ children: [/* @__PURE__ */ S("div", { children: [/* @__PURE__ */ x("h1", {
60
67
  className: "text-2xl font-semibold",
61
68
  children: "New Feature"
62
- }), /* @__PURE__ */ y("p", {
69
+ }), /* @__PURE__ */ x("p", {
63
70
  className: "text-sm text-muted-foreground",
64
71
  children: "Define a new feature request with business value metrics."
65
- })] }), /* @__PURE__ */ b("div", {
72
+ })] }), /* @__PURE__ */ S("div", {
66
73
  className: "flex gap-2",
67
- children: [/* @__PURE__ */ b(x, {
74
+ children: [/* @__PURE__ */ S(C, {
68
75
  variant: "outline",
69
76
  size: "sm",
70
- onClick: () => O("/products/features-page"),
71
- children: [/* @__PURE__ */ y(g, { className: "mr-1 h-4 w-4" }), "Cancel"]
72
- }), /* @__PURE__ */ b(x, {
77
+ onClick: () => A("/products/features-page"),
78
+ children: [/* @__PURE__ */ x(ee, { className: "mr-1 h-4 w-4" }), "Cancel"]
79
+ }), /* @__PURE__ */ S(C, {
73
80
  size: "sm",
74
81
  onClick: async () => {
75
- if (!(!k.title.trim() || z.invalid || !B || !U)) {
76
- R(null);
82
+ if (!(!j.title.trim() || G.invalid || !K || !Y || $.invalid)) {
83
+ W(null);
77
84
  try {
78
- await n(F({ variables: { input: {
79
- title: k.title,
80
- description: k.description || void 0,
81
- status: k.status,
82
- businessValue: z.value,
83
- effortEstimate: k.effortEstimate,
84
- projectId: k.projectId || void 0,
85
- workItemId: k.workItemId || void 0,
86
- tags: k.tags ? k.tags.split(",").map((e) => e.trim()).filter(Boolean) : void 0,
87
- moscow: j.moscow || void 0,
88
- kano: j.kano || void 0,
89
- valueScore: V.value,
90
- effortScore: H.value,
91
- reach: W.value,
92
- impact: G.value,
93
- confidence: K.value
94
- } } })), O("/products/features-page");
85
+ await n(V({ variables: { input: {
86
+ title: j.title,
87
+ description: j.description || void 0,
88
+ status: j.status,
89
+ businessValue: G.value,
90
+ effortEstimate: j.effortEstimate,
91
+ projectId: j.projectId || void 0,
92
+ workItemId: j.workItemId || void 0,
93
+ tags: j.tags ? j.tags.split(",").map((e) => e.trim()).filter(Boolean) : void 0,
94
+ moscow: N.moscow || void 0,
95
+ kano: N.kano || void 0,
96
+ valueScore: q.value,
97
+ effortScore: J.value,
98
+ reach: X.value,
99
+ impact: Z.value,
100
+ confidence: Q.value,
101
+ rating: $.value,
102
+ category: L.category.trim() || void 0,
103
+ isAnonymous: z
104
+ } } })), A("/products/features-page");
95
105
  } catch (e) {
96
- R(e instanceof Error ? e.message : "The feature could not be created.");
106
+ W(e instanceof Error ? e.message : "The feature could not be created.");
97
107
  }
98
108
  }
99
109
  },
100
- disabled: I || !k.title.trim() || z.invalid || !B || !U,
101
- children: [I ? /* @__PURE__ */ y(p, { className: "mr-1 h-4 w-4 animate-spin" }) : /* @__PURE__ */ y(m, { className: "mr-1 h-4 w-4" }), "Create Feature"]
110
+ disabled: H || !j.title.trim() || G.invalid || !K || !Y || $.invalid,
111
+ children: [H ? /* @__PURE__ */ x(g, { className: "mr-1 h-4 w-4 animate-spin" }) : /* @__PURE__ */ x(_, { className: "mr-1 h-4 w-4" }), "Create Feature"]
102
112
  })]
103
113
  })]
104
114
  }),
105
- L && /* @__PURE__ */ b("div", {
115
+ U && /* @__PURE__ */ S("div", {
106
116
  role: "alert",
107
117
  className: "p-3 rounded-md bg-destructive/10 text-destructive text-sm",
108
- children: ["Failed to create feature: ", L]
118
+ children: ["Failed to create feature: ", U]
109
119
  }),
110
- /* @__PURE__ */ b(S, { children: [/* @__PURE__ */ y(w, { children: /* @__PURE__ */ y(T, { children: "Feature Details" }) }), /* @__PURE__ */ b(C, {
120
+ /* @__PURE__ */ S(w, { children: [/* @__PURE__ */ x(E, { children: /* @__PURE__ */ x(D, { children: "Feature Details" }) }), /* @__PURE__ */ S(T, {
111
121
  className: "space-y-4",
112
122
  children: [
113
- /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("label", {
123
+ /* @__PURE__ */ S("div", { children: [/* @__PURE__ */ x("label", {
114
124
  className: "text-sm font-medium",
115
125
  children: "Title"
116
- }), /* @__PURE__ */ y(E, {
117
- value: k.title,
118
- onChange: (e) => A({
119
- ...k,
126
+ }), /* @__PURE__ */ x(O, {
127
+ value: j.title,
128
+ onChange: (e) => M({
129
+ ...j,
120
130
  title: e.target.value
121
131
  }),
122
132
  className: "mt-1",
123
133
  placeholder: "e.g. Advanced Search Filters"
124
134
  })] }),
125
- /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("label", {
135
+ /* @__PURE__ */ S("div", { children: [/* @__PURE__ */ x("label", {
126
136
  className: "text-sm font-medium",
127
137
  children: "Description"
128
- }), /* @__PURE__ */ y("textarea", {
138
+ }), /* @__PURE__ */ x("textarea", {
129
139
  className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[100px]",
130
- value: k.description,
131
- onChange: (e) => A({
132
- ...k,
140
+ value: j.description,
141
+ onChange: (e) => M({
142
+ ...j,
133
143
  description: e.target.value
134
144
  }),
135
145
  placeholder: "Describe the feature and its user value..."
136
146
  })] }),
137
- /* @__PURE__ */ b("div", {
147
+ /* @__PURE__ */ S("div", {
138
148
  className: "grid grid-cols-2 gap-4",
139
- children: [/* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("label", {
149
+ children: [/* @__PURE__ */ S("div", { children: [/* @__PURE__ */ x("label", {
140
150
  className: "text-sm font-medium",
141
151
  children: "Status"
142
- }), /* @__PURE__ */ y("select", {
152
+ }), /* @__PURE__ */ x("select", {
143
153
  className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm capitalize",
144
- value: k.status,
145
- onChange: (e) => A({
146
- ...k,
154
+ value: j.status,
155
+ onChange: (e) => M({
156
+ ...j,
147
157
  status: e.target.value
148
158
  }),
149
159
  children: [
@@ -152,18 +162,18 @@ function O() {
152
162
  "PLANNED",
153
163
  "IN_PROGRESS",
154
164
  "COMPLETED"
155
- ].map((e) => /* @__PURE__ */ y("option", {
165
+ ].map((e) => /* @__PURE__ */ x("option", {
156
166
  value: e,
157
167
  children: e.toLowerCase().replace("_", " ")
158
168
  }, e))
159
- })] }), /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("label", {
169
+ })] }), /* @__PURE__ */ S("div", { children: [/* @__PURE__ */ x("label", {
160
170
  className: "text-sm font-medium",
161
171
  children: "Effort Estimate"
162
- }), /* @__PURE__ */ y("select", {
172
+ }), /* @__PURE__ */ x("select", {
163
173
  className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm capitalize",
164
- value: k.effortEstimate,
165
- onChange: (e) => A({
166
- ...k,
174
+ value: j.effortEstimate,
175
+ onChange: (e) => M({
176
+ ...j,
167
177
  effortEstimate: e.target.value
168
178
  }),
169
179
  children: [
@@ -171,54 +181,54 @@ function O() {
171
181
  "medium",
172
182
  "large",
173
183
  "epic"
174
- ].map((e) => /* @__PURE__ */ y("option", {
184
+ ].map((e) => /* @__PURE__ */ x("option", {
175
185
  value: e,
176
186
  children: e
177
187
  }, e))
178
188
  })] })]
179
189
  }),
180
- /* @__PURE__ */ b("div", {
190
+ /* @__PURE__ */ S("div", {
181
191
  className: "grid grid-cols-2 gap-4",
182
- children: [/* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("label", {
192
+ children: [/* @__PURE__ */ S("div", { children: [/* @__PURE__ */ x("label", {
183
193
  htmlFor: "feature-project",
184
194
  className: "text-sm font-medium",
185
195
  children: "Project"
186
- }), /* @__PURE__ */ y("div", {
196
+ }), /* @__PURE__ */ x("div", {
187
197
  className: "mt-1",
188
- children: /* @__PURE__ */ y(r, {
198
+ children: /* @__PURE__ */ x(r, {
189
199
  id: "feature-project",
190
- value: k.projectId,
200
+ value: j.projectId,
191
201
  noneLabel: "No project",
192
- onChange: (e) => A({
193
- ...k,
202
+ onChange: (e) => M({
203
+ ...j,
194
204
  projectId: e,
195
205
  workItemId: ""
196
206
  })
197
207
  })
198
- })] }), /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("label", {
208
+ })] }), /* @__PURE__ */ S("div", { children: [/* @__PURE__ */ x("label", {
199
209
  htmlFor: "feature-work-item",
200
210
  className: "text-sm font-medium",
201
211
  children: "Delivered by work item"
202
- }), /* @__PURE__ */ y("div", {
212
+ }), /* @__PURE__ */ x("div", {
203
213
  className: "mt-1",
204
- children: /* @__PURE__ */ y(i, {
214
+ children: /* @__PURE__ */ x(i, {
205
215
  id: "feature-work-item",
206
- projectId: k.projectId || void 0,
207
- value: k.workItemId,
208
- onChange: (e) => A({
209
- ...k,
216
+ projectId: j.projectId || void 0,
217
+ value: j.workItemId,
218
+ onChange: (e) => M({
219
+ ...j,
210
220
  workItemId: e
211
221
  })
212
222
  })
213
223
  })] })]
214
224
  }),
215
- /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("label", {
225
+ /* @__PURE__ */ S("div", { children: [/* @__PURE__ */ x("label", {
216
226
  className: "text-sm font-medium",
217
227
  children: "Tags (comma-separated)"
218
- }), /* @__PURE__ */ y(E, {
219
- value: k.tags,
220
- onChange: (e) => A({
221
- ...k,
228
+ }), /* @__PURE__ */ x(O, {
229
+ value: j.tags,
230
+ onChange: (e) => M({
231
+ ...j,
222
232
  tags: e.target.value
223
233
  }),
224
234
  className: "mt-1",
@@ -226,70 +236,90 @@ function O() {
226
236
  })] })
227
237
  ]
228
238
  })] }),
229
- /* @__PURE__ */ b(S, { children: [/* @__PURE__ */ y(w, { children: /* @__PURE__ */ y(T, { children: "Prioritization" }) }), /* @__PURE__ */ b(C, {
239
+ /* @__PURE__ */ S(w, { children: [/* @__PURE__ */ x(E, { children: /* @__PURE__ */ x(D, { children: "Prioritization" }) }), /* @__PURE__ */ S(T, {
230
240
  className: "space-y-4",
231
- children: [/* @__PURE__ */ y(c, {
241
+ children: [/* @__PURE__ */ x(c, {
232
242
  idPrefix: "feature-new",
233
- values: j,
234
- onChange: M
235
- }), /* @__PURE__ */ b("div", {
243
+ values: N,
244
+ onChange: P
245
+ }), /* @__PURE__ */ S("div", {
236
246
  className: "border-t pt-3",
237
- children: [/* @__PURE__ */ y("p", {
247
+ children: [/* @__PURE__ */ x("p", {
238
248
  className: "text-sm font-medium mb-1",
239
249
  children: "RICE"
240
- }), /* @__PURE__ */ y(l, {
250
+ }), /* @__PURE__ */ x(l, {
241
251
  idPrefix: "feature-new",
242
- values: N,
243
- onChange: P,
244
- effortScoreMissing: H.value == null
252
+ values: F,
253
+ onChange: I,
254
+ effortScoreMissing: J.value == null
245
255
  })]
246
256
  })]
247
257
  })] }),
248
- /* @__PURE__ */ b(S, { children: [/* @__PURE__ */ y(w, { children: /* @__PURE__ */ b(T, {
258
+ /* @__PURE__ */ S(w, { children: [/* @__PURE__ */ x(E, { children: /* @__PURE__ */ x(D, { children: "Feedback" }) }), /* @__PURE__ */ S(T, {
259
+ className: "space-y-4",
260
+ children: [
261
+ /* @__PURE__ */ x(m, {
262
+ idPrefix: "feature-new",
263
+ values: L,
264
+ onChange: R
265
+ }),
266
+ $.invalid && /* @__PURE__ */ x("p", {
267
+ role: "alert",
268
+ className: "text-xs text-destructive",
269
+ children: "Rating is a whole number from 1 to 5."
270
+ }),
271
+ /* @__PURE__ */ x(f, {
272
+ idPrefix: "feature-new",
273
+ checked: z,
274
+ onChange: B
275
+ })
276
+ ]
277
+ })] }),
278
+ /* @__PURE__ */ S(w, { children: [/* @__PURE__ */ x(E, { children: /* @__PURE__ */ S(D, {
249
279
  className: "flex items-center gap-2",
250
- children: [/* @__PURE__ */ y(h, { className: "h-4 w-4" }), "Business Value"]
251
- }) }), /* @__PURE__ */ b(C, {
280
+ children: [/* @__PURE__ */ x(v, { className: "h-4 w-4" }), "Business Value"]
281
+ }) }), /* @__PURE__ */ S(T, {
252
282
  className: "space-y-4",
253
- children: [/* @__PURE__ */ b("div", { children: [
254
- /* @__PURE__ */ y("label", {
283
+ children: [/* @__PURE__ */ S("div", { children: [
284
+ /* @__PURE__ */ x("label", {
255
285
  htmlFor: "feature-business-value",
256
286
  className: "text-sm font-medium",
257
287
  children: "Business Value (0-100)"
258
288
  }),
259
- /* @__PURE__ */ y(E, {
289
+ /* @__PURE__ */ x(O, {
260
290
  id: "feature-business-value",
261
291
  type: "number",
262
292
  min: 0,
263
293
  max: 100,
264
294
  step: 1,
265
- value: k.businessValue,
266
- onChange: (e) => A({
267
- ...k,
295
+ value: j.businessValue,
296
+ onChange: (e) => M({
297
+ ...j,
268
298
  businessValue: e.target.value
269
299
  }),
270
- "aria-invalid": z.invalid,
300
+ "aria-invalid": G.invalid,
271
301
  className: "mt-1"
272
302
  }),
273
- z.invalid ? /* @__PURE__ */ y("p", {
303
+ G.invalid ? /* @__PURE__ */ x("p", {
274
304
  role: "alert",
275
305
  className: "text-xs text-destructive mt-1",
276
306
  children: "Business value must be a whole number from 0 to 100."
277
- }) : /* @__PURE__ */ y("p", {
307
+ }) : /* @__PURE__ */ x("p", {
278
308
  className: "text-xs text-muted-foreground mt-1",
279
309
  children: "How much business value does this feature deliver?"
280
310
  })
281
- ] }), /* @__PURE__ */ b("div", {
311
+ ] }), /* @__PURE__ */ S("div", {
282
312
  className: "p-4 bg-muted rounded-md text-center",
283
313
  children: [
284
- /* @__PURE__ */ y("p", {
314
+ /* @__PURE__ */ x("p", {
285
315
  className: "text-xs text-muted-foreground",
286
316
  children: "Business Value Score"
287
317
  }),
288
- /* @__PURE__ */ y("p", {
318
+ /* @__PURE__ */ x("p", {
289
319
  className: "text-3xl font-bold text-primary mt-2",
290
- children: z.invalid || z.value === void 0 ? "—" : z.value
320
+ children: G.invalid || G.value === void 0 ? "—" : G.value
291
321
  }),
292
- /* @__PURE__ */ y("p", {
322
+ /* @__PURE__ */ x("p", {
293
323
  className: "text-xs text-muted-foreground",
294
324
  children: "out of 100"
295
325
  })
@@ -300,6 +330,6 @@ function O() {
300
330
  });
301
331
  }
302
332
  //#endregion
303
- export { O as ProductsNewFeaturePagePage };
333
+ export { A as ProductsNewFeaturePagePage };
304
334
 
305
335
  //# sourceMappingURL=ProductsNewFeaturePagePage.js.map