@burdenoff/microfe-planmagnet 2026.1002.2 → 2026.1002.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/wspace-operations.d.ts +574 -0
- package/dist/generated/wspace-operations.d.ts.map +1 -1
- package/dist/generated/wspace-types.d.ts +486 -16
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/index.js +8 -8
- package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoot.js +174 -168
- package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +63 -59
- package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
- package/dist/planmagnet/board/BulkActions.d.ts.map +1 -1
- package/dist/planmagnet/board/BulkActions.js +106 -55
- package/dist/planmagnet/board/BulkActions.js.map +1 -1
- package/dist/planmagnet/controls/Pickers.d.ts +3 -3
- package/dist/planmagnet/controls/Pickers.js.map +1 -1
- package/dist/planmagnet/dashboards/DashboardUI.d.ts +0 -4
- package/dist/planmagnet/dashboards/DashboardUI.d.ts.map +1 -1
- package/dist/planmagnet/dashboards/DashboardUI.js +6 -13
- package/dist/planmagnet/dashboards/DashboardUI.js.map +1 -1
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.d.ts.map +1 -1
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js +70 -59
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js.map +1 -1
- package/dist/planmagnet/forecast/ResourceForecastCard.js +1 -1
- package/dist/planmagnet/forecast/ResourceForecastCard.js.map +1 -1
- package/dist/planmagnet/index.d.ts +1 -1
- package/dist/planmagnet/index.d.ts.map +1 -1
- package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.d.ts +3 -3
- package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.js +26 -19
- package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.js.map +1 -1
- package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.d.ts +2 -2
- package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.js +18 -12
- package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.js +251 -0
- package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +94 -92
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js +116 -114
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +174 -159
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.js +159 -145
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksEditTaskPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksEditTaskPagePage.js +161 -143
- package/dist/planmagnet/pages/TasksEditTaskPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +157 -155
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/analytics/DeliveryKpisCard.d.ts.map +1 -1
- package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js +97 -99
- package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js.map +1 -1
- package/dist/planmagnet/product/FeatureLinkPanels.js +66 -66
- package/dist/planmagnet/product/ProductFeatureControls.js +38 -38
- package/dist/planmagnet/product/ProductFeatureLinkCard.js +8 -8
- package/dist/planmagnet/product/ProductLineControls.js +83 -83
- package/dist/planmagnet/product/ProductTeamsPanel.js +6 -6
- package/dist/planmagnet/product/routes.d.ts +6 -0
- package/dist/planmagnet/product/routes.d.ts.map +1 -1
- package/dist/planmagnet/product/routes.js +2 -2
- package/dist/planmagnet/product/routes.js.map +1 -1
- package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.d.ts +20 -0
- package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.js +55 -0
- package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionFigures.d.ts +30 -0
- package/dist/planmagnet/satisfaction/SatisfactionFigures.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionFigures.js +282 -0
- package/dist/planmagnet/satisfaction/SatisfactionFigures.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionImportDialog.d.ts +9 -0
- package/dist/planmagnet/satisfaction/SatisfactionImportDialog.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionImportDialog.js +390 -0
- package/dist/planmagnet/satisfaction/SatisfactionImportDialog.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionLinkFields.d.ts +17 -0
- package/dist/planmagnet/satisfaction/SatisfactionLinkFields.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionLinkFields.js +77 -0
- package/dist/planmagnet/satisfaction/SatisfactionLinkFields.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.d.ts +18 -0
- package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.js +87 -0
- package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.d.ts +13 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.js +326 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.d.ts +23 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.js +258 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.d.ts +6 -0
- package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.js +56 -0
- package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.d.ts +16 -0
- package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.js +102 -0
- package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.js.map +1 -0
- package/dist/planmagnet/satisfaction/satisfaction.d.ts +154 -0
- package/dist/planmagnet/satisfaction/satisfaction.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/satisfaction.js +210 -0
- package/dist/planmagnet/satisfaction/satisfaction.js.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionCsv.d.ts +108 -0
- package/dist/planmagnet/satisfaction/satisfactionCsv.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionCsv.js +211 -0
- package/dist/planmagnet/satisfaction/satisfactionCsv.js.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionForm.d.ts +90 -0
- package/dist/planmagnet/satisfaction/satisfactionForm.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionForm.js +90 -0
- package/dist/planmagnet/satisfaction/satisfactionForm.js.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionOperations.d.ts +116 -0
- package/dist/planmagnet/satisfaction/satisfactionOperations.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionOperations.js +86 -0
- package/dist/planmagnet/satisfaction/satisfactionOperations.js.map +1 -0
- package/dist/planmagnet/satisfaction/useSatisfactionAccess.d.ts +20 -0
- package/dist/planmagnet/satisfaction/useSatisfactionAccess.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/useSatisfactionAccess.js +31 -0
- package/dist/planmagnet/satisfaction/useSatisfactionAccess.js.map +1 -0
- package/dist/planmagnet/skills/RequiredSkillsEditor.d.ts +35 -0
- package/dist/planmagnet/skills/RequiredSkillsEditor.d.ts.map +1 -0
- package/dist/planmagnet/skills/RequiredSkillsEditor.js +124 -0
- package/dist/planmagnet/skills/RequiredSkillsEditor.js.map +1 -0
- package/dist/planmagnet/skills/SkillChips.d.ts +30 -0
- package/dist/planmagnet/skills/SkillChips.d.ts.map +1 -0
- package/dist/planmagnet/skills/SkillChips.js +57 -0
- package/dist/planmagnet/skills/SkillChips.js.map +1 -0
- package/dist/planmagnet/skills/SkillCoverageCard.d.ts +24 -0
- package/dist/planmagnet/skills/SkillCoverageCard.d.ts.map +1 -0
- package/dist/planmagnet/skills/SkillCoverageCard.js +256 -0
- package/dist/planmagnet/skills/SkillCoverageCard.js.map +1 -0
- package/dist/planmagnet/skills/WorkItemRequiredSkills.d.ts +10 -0
- package/dist/planmagnet/skills/WorkItemRequiredSkills.d.ts.map +1 -0
- package/dist/planmagnet/skills/WorkItemRequiredSkills.js +65 -0
- package/dist/planmagnet/skills/WorkItemRequiredSkills.js.map +1 -0
- package/dist/planmagnet/skills/skillOperations.d.ts +52 -0
- package/dist/planmagnet/skills/skillOperations.d.ts.map +1 -0
- package/dist/planmagnet/skills/skillOperations.js +44 -0
- package/dist/planmagnet/skills/skillOperations.js.map +1 -0
- package/dist/planmagnet/skills/skillTags.d.ts +103 -0
- package/dist/planmagnet/skills/skillTags.d.ts.map +1 -0
- package/dist/planmagnet/skills/skillTags.js +115 -0
- package/dist/planmagnet/skills/skillTags.js.map +1 -0
- package/dist/planmagnet/skills/useRequiredSkillsDraft.d.ts +28 -0
- package/dist/planmagnet/skills/useRequiredSkillsDraft.d.ts.map +1 -0
- package/dist/planmagnet/skills/useRequiredSkillsDraft.js +30 -0
- package/dist/planmagnet/skills/useRequiredSkillsDraft.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { gql as e } from "@apollo/client";
|
|
2
|
+
//#region src/planmagnet/satisfaction/satisfactionOperations.ts
|
|
3
|
+
var t = "\n productId\n releaseId\n projectId\n window {\n preset\n from\n to\n }\n csat {\n score\n responseCount\n satisfiedCount\n minimumResponses\n notEnoughResponses\n }\n nps {\n score\n responseCount\n promoterCount\n passiveCount\n detractorCount\n minimumResponses\n notEnoughResponses\n }\n", n = "\n id\n metricType\n score\n respondedAt\n comment\n isAnonymous\n respondentName\n respondentEmail\n source\n externalId\n productId\n releaseId\n projectId\n product {\n id\n name\n }\n release {\n id\n name\n version\n }\n project {\n id\n name\n }\n createdAt\n updatedAt\n", r = e`
|
|
4
|
+
query CustomerSatisfactionSummary(
|
|
5
|
+
$workspaceId: ID!
|
|
6
|
+
$filter: PlanmagnetCustomerSatisfactionFilter
|
|
7
|
+
) {
|
|
8
|
+
planmagnetCustomerSatisfaction(workspaceId: $workspaceId, filter: $filter) {
|
|
9
|
+
${t}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
`, i = e`
|
|
13
|
+
query CustomerSatisfactionResponses(
|
|
14
|
+
$workspaceId: ID!
|
|
15
|
+
$filter: PlanmagnetCustomerSatisfactionResponseFilter
|
|
16
|
+
$pagination: PlanmagnetPaginationInput
|
|
17
|
+
) {
|
|
18
|
+
planmagnetCustomerSatisfactionResponses(
|
|
19
|
+
workspaceId: $workspaceId
|
|
20
|
+
filter: $filter
|
|
21
|
+
pagination: $pagination
|
|
22
|
+
) {
|
|
23
|
+
items {
|
|
24
|
+
${n}
|
|
25
|
+
}
|
|
26
|
+
pageInfo {
|
|
27
|
+
hasNextPage
|
|
28
|
+
endCursor
|
|
29
|
+
}
|
|
30
|
+
totalCount
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
`, a = e`
|
|
34
|
+
query CustomerSatisfactionReleaseOptions(
|
|
35
|
+
$workspaceId: ID!
|
|
36
|
+
$pagination: PlanmagnetPaginationInput
|
|
37
|
+
) {
|
|
38
|
+
planmagnetReleases(workspaceId: $workspaceId, pagination: $pagination) {
|
|
39
|
+
items {
|
|
40
|
+
id
|
|
41
|
+
name
|
|
42
|
+
version
|
|
43
|
+
productId
|
|
44
|
+
}
|
|
45
|
+
pageInfo {
|
|
46
|
+
hasNextPage
|
|
47
|
+
endCursor
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
`, o = e`
|
|
52
|
+
mutation CreateCustomerSatisfactionResponse(
|
|
53
|
+
$input: PlanmagnetCreateCustomerSatisfactionResponseInput!
|
|
54
|
+
) {
|
|
55
|
+
planmagnetCreateCustomerSatisfactionResponse(input: $input) {
|
|
56
|
+
${n}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
`, s = e`
|
|
60
|
+
mutation UpdateCustomerSatisfactionResponse(
|
|
61
|
+
$id: ID!
|
|
62
|
+
$input: PlanmagnetUpdateCustomerSatisfactionResponseInput!
|
|
63
|
+
) {
|
|
64
|
+
planmagnetUpdateCustomerSatisfactionResponse(id: $id, input: $input) {
|
|
65
|
+
${n}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
`, c = e`
|
|
69
|
+
mutation DeleteCustomerSatisfactionResponse($id: ID!) {
|
|
70
|
+
planmagnetDeleteCustomerSatisfactionResponse(id: $id)
|
|
71
|
+
}
|
|
72
|
+
`, l = e`
|
|
73
|
+
mutation ImportCustomerSatisfactionResponses(
|
|
74
|
+
$input: PlanmagnetImportCustomerSatisfactionResponsesInput!
|
|
75
|
+
) {
|
|
76
|
+
planmagnetImportCustomerSatisfactionResponses(input: $input) {
|
|
77
|
+
createdCount
|
|
78
|
+
skippedCount
|
|
79
|
+
skippedExternalIds
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
`, u = ["CustomerSatisfactionSummary", "CustomerSatisfactionResponses"];
|
|
83
|
+
//#endregion
|
|
84
|
+
export { o as CREATE_CUSTOMER_SATISFACTION_RESPONSE, a as CUSTOMER_SATISFACTION_RELEASE_OPTIONS, i as CUSTOMER_SATISFACTION_RESPONSES, r as CUSTOMER_SATISFACTION_SUMMARY, c as DELETE_CUSTOMER_SATISFACTION_RESPONSE, l as IMPORT_CUSTOMER_SATISFACTION_RESPONSES, u as SATISFACTION_QUERIES, s as UPDATE_CUSTOMER_SATISFACTION_RESPONSE };
|
|
85
|
+
|
|
86
|
+
//# sourceMappingURL=satisfactionOperations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"satisfactionOperations.js","names":[],"sources":["../../../src/planmagnet/satisfaction/satisfactionOperations.ts"],"sourcesContent":["import { gql } from '@apollo/client';\nimport type { SatisfactionMetricType, WindowPreset } from './satisfaction';\n\n/**\n * ANR-21 GraphQL documents: customer-satisfaction figures (CSAT and NPS), the responses behind them, and their writes.\n * Every document here has a twin in src/operations/wspace/planmagnet/{fragments,queries,mutations}/customerSatisfaction.graphql,\n * which `bun run codegen` validates against the planmagnet schema and CI's live-supergraph drift job validates against\n * what production serves; tests/satisfactionOperations.test.ts fails when the two copies differ.\n *\n * Every read is a query of its own (the PRM-01 rule): a section that cannot be read — a role without\n * `planmagnet-product:read`, or a gateway that does not serve a field yet — fails that card only, never the page or\n * the card it sits on (the delivery KPIs card keeps its on-time and scope figures if satisfaction fails).\n */\n\nconst SUMMARY_FIELDS = `\n productId\n releaseId\n projectId\n window {\n preset\n from\n to\n }\n csat {\n score\n responseCount\n satisfiedCount\n minimumResponses\n notEnoughResponses\n }\n nps {\n score\n responseCount\n promoterCount\n passiveCount\n detractorCount\n minimumResponses\n notEnoughResponses\n }\n`;\n\nconst RESPONSE_FIELDS = `\n id\n metricType\n score\n respondedAt\n comment\n isAnonymous\n respondentName\n respondentEmail\n source\n externalId\n productId\n releaseId\n projectId\n product {\n id\n name\n }\n release {\n id\n name\n version\n }\n project {\n id\n name\n }\n createdAt\n updatedAt\n`;\n\n// ─── Queries ────────────────────────────────────────────────────────────────\n\n/** CSAT and NPS for the workspace, or one product / release / project, over a window (default: trailing 90 days). */\nexport const CUSTOMER_SATISFACTION_SUMMARY = gql`\n query CustomerSatisfactionSummary(\n $workspaceId: ID!\n $filter: PlanmagnetCustomerSatisfactionFilter\n ) {\n planmagnetCustomerSatisfaction(workspaceId: $workspaceId, filter: $filter) {\n ${SUMMARY_FIELDS}\n }\n }\n`;\n\n/** One page of responses, newest answer first. */\nexport const CUSTOMER_SATISFACTION_RESPONSES = gql`\n query CustomerSatisfactionResponses(\n $workspaceId: ID!\n $filter: PlanmagnetCustomerSatisfactionResponseFilter\n $pagination: PlanmagnetPaginationInput\n ) {\n planmagnetCustomerSatisfactionResponses(\n workspaceId: $workspaceId\n filter: $filter\n pagination: $pagination\n ) {\n items {\n ${RESPONSE_FIELDS}\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n }\n }\n`;\n\n/**\n * Releases for the release pickers, with their product: a response's release must belong to the product named with\n * it (or to no product), so the pickers offer only those. Paged — see useSatisfactionReleaseOptions.\n */\nexport const CUSTOMER_SATISFACTION_RELEASE_OPTIONS = gql`\n query CustomerSatisfactionReleaseOptions(\n $workspaceId: ID!\n $pagination: PlanmagnetPaginationInput\n ) {\n planmagnetReleases(workspaceId: $workspaceId, pagination: $pagination) {\n items {\n id\n name\n version\n productId\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n`;\n\n// ─── Mutations ──────────────────────────────────────────────────────────────\n\nexport const CREATE_CUSTOMER_SATISFACTION_RESPONSE = gql`\n mutation CreateCustomerSatisfactionResponse(\n $input: PlanmagnetCreateCustomerSatisfactionResponseInput!\n ) {\n planmagnetCreateCustomerSatisfactionResponse(input: $input) {\n ${RESPONSE_FIELDS}\n }\n }\n`;\n\nexport const UPDATE_CUSTOMER_SATISFACTION_RESPONSE = gql`\n mutation UpdateCustomerSatisfactionResponse(\n $id: ID!\n $input: PlanmagnetUpdateCustomerSatisfactionResponseInput!\n ) {\n planmagnetUpdateCustomerSatisfactionResponse(id: $id, input: $input) {\n ${RESPONSE_FIELDS}\n }\n }\n`;\n\nexport const DELETE_CUSTOMER_SATISFACTION_RESPONSE = gql`\n mutation DeleteCustomerSatisfactionResponse($id: ID!) {\n planmagnetDeleteCustomerSatisfactionResponse(id: $id)\n }\n`;\n\nexport const IMPORT_CUSTOMER_SATISFACTION_RESPONSES = gql`\n mutation ImportCustomerSatisfactionResponses(\n $input: PlanmagnetImportCustomerSatisfactionResponsesInput!\n ) {\n planmagnetImportCustomerSatisfactionResponses(input: $input) {\n createdCount\n skippedCount\n skippedExternalIds\n }\n }\n`;\n\n// ─── Refetch group ──────────────────────────────────────────────────────────\n\n/** Every query that shows a satisfaction figure or a response — refetched after any satisfaction write. */\nexport const SATISFACTION_QUERIES = [\n 'CustomerSatisfactionSummary',\n 'CustomerSatisfactionResponses',\n];\n\n// ─── Result shapes ──────────────────────────────────────────────────────────\n\nexport interface CsatFigure {\n /** satisfiedCount * 100 / responseCount, exact. Null below minimumResponses — never 0. */\n score: number | null;\n responseCount: number;\n satisfiedCount: number;\n minimumResponses: number;\n notEnoughResponses: boolean;\n}\n\nexport interface NpsFigure {\n /** (promoters - detractors) * 100 / responseCount, exact, -100..100. Null below minimumResponses. */\n score: number | null;\n responseCount: number;\n promoterCount: number;\n passiveCount: number;\n detractorCount: number;\n minimumResponses: number;\n notEnoughResponses: boolean;\n}\n\nexport interface SatisfactionWindowEcho {\n preset: WindowPreset;\n /** The start of the first UTC day counted. */\n from: string;\n /** The end of the last UTC day counted. */\n to: string;\n}\n\nexport interface CustomerSatisfactionSummary {\n productId: string | null;\n releaseId: string | null;\n projectId: string | null;\n window: SatisfactionWindowEcho;\n csat: CsatFigure;\n nps: NpsFigure;\n}\n\nexport interface CustomerSatisfactionSummaryData {\n planmagnetCustomerSatisfaction: CustomerSatisfactionSummary;\n}\n\nexport interface SatisfactionResponse {\n id: string;\n metricType: SatisfactionMetricType;\n score: number;\n respondedAt: string;\n comment: string | null;\n isAnonymous: boolean;\n respondentName: string | null;\n respondentEmail: string | null;\n /** MANUAL | IMPORT — kept a string so a value added later still renders. */\n source: string;\n externalId: string | null;\n productId: string | null;\n releaseId: string | null;\n projectId: string | null;\n /** Null when there is no product link, or the product was deleted since. */\n product: { id: string; name: string } | null;\n release: { id: string; name: string; version: string } | null;\n project: { id: string; name: string } | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface CustomerSatisfactionResponsesData {\n planmagnetCustomerSatisfactionResponses: {\n items: SatisfactionResponse[];\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n totalCount: number;\n };\n}\n\nexport interface SatisfactionReleaseOption {\n id: string;\n name: string;\n version: string;\n productId: string | null;\n}\n\nexport interface CustomerSatisfactionReleaseOptionsData {\n planmagnetReleases: {\n items: SatisfactionReleaseOption[];\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n };\n}\n\nexport interface ImportCustomerSatisfactionResult {\n createdCount: number;\n skippedCount: number;\n skippedExternalIds: string[];\n}\n"],"mappings":";;AAcA,IAAM,IAAiB,8VA2BjB,IAAkB,mUAkCX,IAAgC,CAAG;;;;;;QAMxC,EAAe;;;GAMV,IAAkC,CAAG;;;;;;;;;;;;UAYxC,EAAgB;;;;;;;;;GAeb,IAAwC,CAAG;;;;;;;;;;;;;;;;;;GAsB3C,IAAwC,CAAG;;;;;QAKhD,EAAgB;;;GAKX,IAAwC,CAAG;;;;;;QAMhD,EAAgB;;;GAKX,IAAwC,CAAG;;;;GAM3C,IAAyC,CAAG;;;;;;;;;;GAe5C,IAAuB,CAClC,+BACA,gCACD"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the viewer may do with customer-satisfaction responses. Every satisfaction operation sits on the
|
|
3
|
+
* `planmagnet-product` RBAC resource: read for viewers (Quality Lead, Contributor, Stakeholder), create / update /
|
|
4
|
+
* delete for the product managers (PM, Delivery Owner, Portfolio Lead). Viewers get a read-only page: the write
|
|
5
|
+
* controls are not rendered for a permission the role does not hold.
|
|
6
|
+
*
|
|
7
|
+
* This only decides what is SHOWN — the gateway enforces every write, and a refusal is still shown verbatim. Where the
|
|
8
|
+
* permission set cannot be read (a host that mounts no PermissionProvider, or the RBAC read failed) the controls stay,
|
|
9
|
+
* as on the other product pages: the gateway decides, and the refusal says why. While it is still loading they are
|
|
10
|
+
* hidden, so a viewer never sees a control flash in and out.
|
|
11
|
+
*/
|
|
12
|
+
export interface SatisfactionAccess {
|
|
13
|
+
canCreate: boolean;
|
|
14
|
+
canUpdate: boolean;
|
|
15
|
+
canDelete: boolean;
|
|
16
|
+
/** True only when the viewer's permission set is known and allows none of the writes. */
|
|
17
|
+
readOnly: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function useSatisfactionAccess(): SatisfactionAccess;
|
|
20
|
+
//# sourceMappingURL=useSatisfactionAccess.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSatisfactionAccess.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/satisfaction/useSatisfactionAccess.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,yFAAyF;IACzF,QAAQ,EAAE,OAAO,CAAC;CACnB;AAeD,wBAAgB,qBAAqB,IAAI,kBAAkB,CAe1D"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useSafePermissions as e } from "@burdenoff/fe-libs/shared/providers/shell/PermissionProvider";
|
|
2
|
+
//#region src/planmagnet/satisfaction/useSatisfactionAccess.ts
|
|
3
|
+
var t = {
|
|
4
|
+
canCreate: !0,
|
|
5
|
+
canUpdate: !0,
|
|
6
|
+
canDelete: !0,
|
|
7
|
+
readOnly: !1
|
|
8
|
+
}, n = {
|
|
9
|
+
canCreate: !1,
|
|
10
|
+
canUpdate: !1,
|
|
11
|
+
canDelete: !1,
|
|
12
|
+
readOnly: !1
|
|
13
|
+
};
|
|
14
|
+
function r() {
|
|
15
|
+
let r = e();
|
|
16
|
+
if (!r) return t;
|
|
17
|
+
if (r.isSuperAdmin || r.permissions) {
|
|
18
|
+
let e = r.hasPermission("planmagnet-product:create"), t = r.hasPermission("planmagnet-product:update"), n = r.hasPermission("planmagnet-product:delete");
|
|
19
|
+
return {
|
|
20
|
+
canCreate: e,
|
|
21
|
+
canUpdate: t,
|
|
22
|
+
canDelete: n,
|
|
23
|
+
readOnly: !e && !t && !n
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return r.error ? t : n;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { r as useSatisfactionAccess };
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=useSatisfactionAccess.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSatisfactionAccess.js","names":[],"sources":["../../../src/planmagnet/satisfaction/useSatisfactionAccess.ts"],"sourcesContent":["import { useSafePermissions } from '@burdenoff/fe-libs/shared/providers/shell/PermissionProvider';\n\n/**\n * What the viewer may do with customer-satisfaction responses. Every satisfaction operation sits on the\n * `planmagnet-product` RBAC resource: read for viewers (Quality Lead, Contributor, Stakeholder), create / update /\n * delete for the product managers (PM, Delivery Owner, Portfolio Lead). Viewers get a read-only page: the write\n * controls are not rendered for a permission the role does not hold.\n *\n * This only decides what is SHOWN — the gateway enforces every write, and a refusal is still shown verbatim. Where the\n * permission set cannot be read (a host that mounts no PermissionProvider, or the RBAC read failed) the controls stay,\n * as on the other product pages: the gateway decides, and the refusal says why. While it is still loading they are\n * hidden, so a viewer never sees a control flash in and out.\n */\nexport interface SatisfactionAccess {\n canCreate: boolean;\n canUpdate: boolean;\n canDelete: boolean;\n /** True only when the viewer's permission set is known and allows none of the writes. */\n readOnly: boolean;\n}\n\nconst EVERYTHING: SatisfactionAccess = {\n canCreate: true,\n canUpdate: true,\n canDelete: true,\n readOnly: false,\n};\nconst NOTHING: SatisfactionAccess = {\n canCreate: false,\n canUpdate: false,\n canDelete: false,\n readOnly: false,\n};\n\nexport function useSatisfactionAccess(): SatisfactionAccess {\n const permissions = useSafePermissions();\n // No provider (a standalone mount): the gateway decides.\n if (!permissions) return EVERYTHING;\n // A permission set is at hand (a super admin's bypass has none, and every check passes).\n if (permissions.isSuperAdmin || permissions.permissions) {\n const canCreate = permissions.hasPermission('planmagnet-product:create');\n const canUpdate = permissions.hasPermission('planmagnet-product:update');\n const canDelete = permissions.hasPermission('planmagnet-product:delete');\n return { canCreate, canUpdate, canDelete, readOnly: !canCreate && !canUpdate && !canDelete };\n }\n // The RBAC read failed: the gateway decides, and a refusal says why.\n if (permissions.error) return EVERYTHING;\n // Still loading.\n return NOTHING;\n}\n"],"mappings":";;AAqBA,IAAM,IAAiC;CACrC,WAAW;CACX,WAAW;CACX,WAAW;CACX,UAAU;CACX,EACK,IAA8B;CAClC,WAAW;CACX,WAAW;CACX,WAAW;CACX,UAAU;CACX;AAED,SAAgB,IAA4C;CAC1D,IAAM,IAAc,GAAoB;AAExC,KAAI,CAAC,EAAa,QAAO;AAEzB,KAAI,EAAY,gBAAgB,EAAY,aAAa;EACvD,IAAM,IAAY,EAAY,cAAc,4BAA4B,EAClE,IAAY,EAAY,cAAc,4BAA4B,EAClE,IAAY,EAAY,cAAc,4BAA4B;AACxE,SAAO;GAAE;GAAW;GAAW;GAAW,UAAU,CAAC,KAAa,CAAC,KAAa,CAAC;GAAW;;AAK9F,QAFI,EAAY,QAAc,IAEvB"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The required-skills tag editor (ANR-07, ANR-28) on the task create and edit pages and the board's bulk edit. It is
|
|
3
|
+
* controlled: the page owns the list and sends it with its own save. Enter, a comma or leaving the box adds what was
|
|
4
|
+
* typed (a pasted comma- or line-separated list adds several); the × on a chip removes it. The service's write rules
|
|
5
|
+
* are applied as the user types (see skillTags.ts), and a refusal the service still returns — BAD_USER_INPUT tagged
|
|
6
|
+
* `requiredSkills` — is passed back in as `serverError` and shown on the field.
|
|
7
|
+
*/
|
|
8
|
+
export declare function RequiredSkillsEditor({ id, value, onChange, disabled, serverError, hint, }: {
|
|
9
|
+
/** The text box's id, for the page's own `<label htmlFor>`. */
|
|
10
|
+
id: string;
|
|
11
|
+
value: readonly string[];
|
|
12
|
+
onChange: (next: string[]) => void;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
/** The service's reason for refusing this field on the last save, shown inline until the list changes. */
|
|
15
|
+
serverError?: string | null;
|
|
16
|
+
/** Replaces the standard hint under the box. */
|
|
17
|
+
hint?: string;
|
|
18
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
/**
|
|
20
|
+
* The labelled "Required skills" field of a task form: the editor, or — on the edit page, while the saved list is
|
|
21
|
+
* loading or when it could not be loaded — a line saying so with the editor disabled, so a list the user never saw
|
|
22
|
+
* is never edited (or sent) blind.
|
|
23
|
+
*/
|
|
24
|
+
export declare function RequiredSkillsField({ id, value, onChange, disabled, serverError, loading, loadError, }: {
|
|
25
|
+
id: string;
|
|
26
|
+
value: readonly string[];
|
|
27
|
+
onChange: (next: string[]) => void;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
serverError?: string | null;
|
|
30
|
+
/** The saved list is still loading (edit page). */
|
|
31
|
+
loading?: boolean;
|
|
32
|
+
/** The saved list could not be loaded (edit page). */
|
|
33
|
+
loadError?: string | null;
|
|
34
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
35
|
+
//# sourceMappingURL=RequiredSkillsEditor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RequiredSkillsEditor.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/skills/RequiredSkillsEditor.tsx"],"names":[],"mappings":"AAYA;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,EAAE,EACF,KAAK,EACL,QAAQ,EACR,QAAgB,EAChB,WAAkB,EAClB,IAAI,GACL,EAAE;IACD,+DAA+D;IAC/D,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0GAA0G;IAC1G,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,gDAAgD;IAChD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,2CA4GA;AA6BD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,EAAE,EACF,KAAK,EACL,QAAQ,EACR,QAAgB,EAChB,WAAkB,EAClB,OAAe,EACf,SAAgB,GACjB,EAAE;IACD,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,mDAAmD;IACnD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,2CA6BA"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { useTr as e } from "../../shared/hooks/useTr.js";
|
|
2
|
+
import { addSkillTags as t, removeSkillAt as n, skillKey as r } from "./skillTags.js";
|
|
3
|
+
import { useState as i } from "react";
|
|
4
|
+
import { Plus as a, X as o } from "lucide-react";
|
|
5
|
+
import { jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
6
|
+
import { Badge as l, Button as u, Input as d } from "@burdenoff/fe-libs/ui";
|
|
7
|
+
//#region src/planmagnet/skills/RequiredSkillsEditor.tsx
|
|
8
|
+
function f({ id: f, value: m, onChange: h, disabled: g = !1, serverError: _ = null, hint: v }) {
|
|
9
|
+
let y = e(), [b, x] = i(""), [S, C] = i(null), w = m.length >= 20, T = () => {
|
|
10
|
+
if (!b.trim()) {
|
|
11
|
+
b && x("");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
let e = t(m, b);
|
|
15
|
+
e.skills.length !== m.length && h(e.skills), x(e.rest), C(e.problem);
|
|
16
|
+
}, E = (e) => {
|
|
17
|
+
C(null), h(n(m, e));
|
|
18
|
+
}, D = (S ? p(S, y) : null) ?? _, O = `${f}-error`, k = `${f}-hint`, A = y("planmagnet.skills.editor.count", "{{count}} of {{max}}", {
|
|
19
|
+
count: m.length,
|
|
20
|
+
max: 20
|
|
21
|
+
});
|
|
22
|
+
return /* @__PURE__ */ c("div", {
|
|
23
|
+
className: "mt-1 space-y-2",
|
|
24
|
+
"data-testid": "required-skills-editor",
|
|
25
|
+
children: [
|
|
26
|
+
m.length > 0 && /* @__PURE__ */ s("ul", {
|
|
27
|
+
className: "flex flex-wrap gap-1",
|
|
28
|
+
"aria-label": y("planmagnet.skills.editor.listLabel", "Required skills"),
|
|
29
|
+
children: m.map((e, t) => /* @__PURE__ */ s("li", { children: /* @__PURE__ */ c(l, {
|
|
30
|
+
variant: "secondary",
|
|
31
|
+
className: "gap-1 pr-1 font-normal",
|
|
32
|
+
children: [/* @__PURE__ */ s("span", { children: e }), /* @__PURE__ */ s("button", {
|
|
33
|
+
type: "button",
|
|
34
|
+
className: "rounded-full p-0.5 hover:bg-muted disabled:opacity-50",
|
|
35
|
+
onClick: () => E(t),
|
|
36
|
+
disabled: g,
|
|
37
|
+
"aria-label": y("planmagnet.skills.editor.remove", "Remove {{skill}}", { skill: e }),
|
|
38
|
+
children: /* @__PURE__ */ s(o, {
|
|
39
|
+
className: "h-3 w-3",
|
|
40
|
+
"aria-hidden": !0
|
|
41
|
+
})
|
|
42
|
+
})]
|
|
43
|
+
}) }, `${r(e)}-${t}`))
|
|
44
|
+
}),
|
|
45
|
+
/* @__PURE__ */ c("div", {
|
|
46
|
+
className: "flex gap-2",
|
|
47
|
+
children: [/* @__PURE__ */ s(d, {
|
|
48
|
+
id: f,
|
|
49
|
+
value: b,
|
|
50
|
+
disabled: g || w,
|
|
51
|
+
placeholder: w ? y("planmagnet.skills.editor.fullPlaceholder", "Remove a skill to add another") : y("planmagnet.skills.editor.placeholder", "e.g. React, PostgreSQL"),
|
|
52
|
+
"aria-invalid": D ? !0 : void 0,
|
|
53
|
+
"aria-describedby": D ? O : k,
|
|
54
|
+
onChange: (e) => {
|
|
55
|
+
x(e.target.value), C(null);
|
|
56
|
+
},
|
|
57
|
+
onKeyDown: (e) => {
|
|
58
|
+
e.nativeEvent.isComposing || (e.key === "Enter" || e.key === ",") && (e.preventDefault(), T());
|
|
59
|
+
},
|
|
60
|
+
onBlur: T
|
|
61
|
+
}), /* @__PURE__ */ c(u, {
|
|
62
|
+
type: "button",
|
|
63
|
+
variant: "outline",
|
|
64
|
+
size: "sm",
|
|
65
|
+
onClick: T,
|
|
66
|
+
disabled: g || w || !b.trim(),
|
|
67
|
+
children: [/* @__PURE__ */ s(a, {
|
|
68
|
+
className: "mr-1 h-4 w-4",
|
|
69
|
+
"aria-hidden": !0
|
|
70
|
+
}), y("planmagnet.skills.editor.add", "Add")]
|
|
71
|
+
})]
|
|
72
|
+
}),
|
|
73
|
+
D ? /* @__PURE__ */ s("p", {
|
|
74
|
+
id: O,
|
|
75
|
+
role: "alert",
|
|
76
|
+
className: "text-xs text-destructive",
|
|
77
|
+
children: D
|
|
78
|
+
}) : /* @__PURE__ */ c("p", {
|
|
79
|
+
id: k,
|
|
80
|
+
className: "text-xs text-muted-foreground",
|
|
81
|
+
children: [
|
|
82
|
+
A,
|
|
83
|
+
" ·",
|
|
84
|
+
" ",
|
|
85
|
+
v ?? y("planmagnet.skills.editor.hint", "Press Enter or type a comma to add a skill. Matched against people’s skills in CrewFoundry, ignoring case.")
|
|
86
|
+
]
|
|
87
|
+
})
|
|
88
|
+
]
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function p(e, t) {
|
|
92
|
+
switch (e.kind) {
|
|
93
|
+
case "duplicate": return t("planmagnet.skills.editor.duplicate", "“{{skill}}” is already on the list.", { skill: e.skill });
|
|
94
|
+
case "tooLong": return t("planmagnet.skills.editor.tooLong", "A skill can be at most {{max}} characters long.", { max: e.max });
|
|
95
|
+
case "tooMany": return t("planmagnet.skills.editor.tooMany", "A task can require at most {{max}} skills.", { max: e.max });
|
|
96
|
+
case "invalid": return t("planmagnet.skills.editor.invalid", "A skill can only contain printable characters.");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function m({ id: t, value: n, onChange: r, disabled: i = !1, serverError: a = null, loading: o = !1, loadError: l = null }) {
|
|
100
|
+
let u = e();
|
|
101
|
+
return /* @__PURE__ */ c("div", {
|
|
102
|
+
"data-testid": "required-skills-field",
|
|
103
|
+
children: [/* @__PURE__ */ s("label", {
|
|
104
|
+
htmlFor: t,
|
|
105
|
+
className: "text-sm font-medium",
|
|
106
|
+
children: u("planmagnet.skills.editor.label", "Required skills")
|
|
107
|
+
}), l ? /* @__PURE__ */ s("p", {
|
|
108
|
+
role: "alert",
|
|
109
|
+
className: "mt-1 text-xs text-destructive",
|
|
110
|
+
children: u("planmagnet.skills.editor.loadError", "Required skills could not be loaded, so they can’t be changed here right now: {{message}}", { message: l })
|
|
111
|
+
}) : /* @__PURE__ */ s(f, {
|
|
112
|
+
id: t,
|
|
113
|
+
value: n,
|
|
114
|
+
onChange: r,
|
|
115
|
+
disabled: i || o,
|
|
116
|
+
serverError: a,
|
|
117
|
+
hint: o ? u("planmagnet.skills.editor.loading", "Loading required skills…") : void 0
|
|
118
|
+
})]
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
export { f as RequiredSkillsEditor, m as RequiredSkillsField };
|
|
123
|
+
|
|
124
|
+
//# sourceMappingURL=RequiredSkillsEditor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RequiredSkillsEditor.js","names":[],"sources":["../../../src/planmagnet/skills/RequiredSkillsEditor.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Badge, Button, Input } from '@burdenoff/fe-libs/ui';\nimport { Plus, X } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport {\n MAX_REQUIRED_SKILLS,\n addSkillTags,\n removeSkillAt,\n skillKey,\n type SkillTagProblem,\n} from './skillTags';\n\n/**\n * The required-skills tag editor (ANR-07, ANR-28) on the task create and edit pages and the board's bulk edit. It is\n * controlled: the page owns the list and sends it with its own save. Enter, a comma or leaving the box adds what was\n * typed (a pasted comma- or line-separated list adds several); the × on a chip removes it. The service's write rules\n * are applied as the user types (see skillTags.ts), and a refusal the service still returns — BAD_USER_INPUT tagged\n * `requiredSkills` — is passed back in as `serverError` and shown on the field.\n */\nexport function RequiredSkillsEditor({\n id,\n value,\n onChange,\n disabled = false,\n serverError = null,\n hint,\n}: {\n /** The text box's id, for the page's own `<label htmlFor>`. */\n id: string;\n value: readonly string[];\n onChange: (next: string[]) => void;\n disabled?: boolean;\n /** The service's reason for refusing this field on the last save, shown inline until the list changes. */\n serverError?: string | null;\n /** Replaces the standard hint under the box. */\n hint?: string;\n}) {\n const tr = useTr();\n const [text, setText] = useState('');\n const [problem, setProblem] = useState<SkillTagProblem | null>(null);\n const full = value.length >= MAX_REQUIRED_SKILLS;\n\n const commit = () => {\n if (!text.trim()) {\n if (text) setText('');\n return;\n }\n const result = addSkillTags(value, text);\n if (result.skills.length !== value.length) onChange(result.skills);\n setText(result.rest);\n setProblem(result.problem);\n };\n\n const remove = (index: number) => {\n setProblem(null);\n onChange(removeSkillAt(value, index));\n };\n\n const problemText = problem ? describeProblem(problem, tr) : null;\n const error = problemText ?? serverError;\n const errorId = `${id}-error`;\n const hintId = `${id}-hint`;\n const counter = tr('planmagnet.skills.editor.count', '{{count}} of {{max}}', {\n count: value.length,\n max: MAX_REQUIRED_SKILLS,\n });\n\n return (\n <div className=\"mt-1 space-y-2\" data-testid=\"required-skills-editor\">\n {value.length > 0 && (\n <ul\n className=\"flex flex-wrap gap-1\"\n aria-label={tr('planmagnet.skills.editor.listLabel', 'Required skills')}\n >\n {value.map((skill, index) => (\n <li key={`${skillKey(skill)}-${index}`}>\n <Badge variant=\"secondary\" className=\"gap-1 pr-1 font-normal\">\n <span>{skill}</span>\n <button\n type=\"button\"\n className=\"rounded-full p-0.5 hover:bg-muted disabled:opacity-50\"\n onClick={() => remove(index)}\n disabled={disabled}\n aria-label={tr('planmagnet.skills.editor.remove', 'Remove {{skill}}', { skill })}\n >\n <X className=\"h-3 w-3\" aria-hidden={true} />\n </button>\n </Badge>\n </li>\n ))}\n </ul>\n )}\n <div className=\"flex gap-2\">\n <Input\n id={id}\n value={text}\n disabled={disabled || full}\n placeholder={\n full\n ? tr('planmagnet.skills.editor.fullPlaceholder', 'Remove a skill to add another')\n : tr('planmagnet.skills.editor.placeholder', 'e.g. React, PostgreSQL')\n }\n aria-invalid={error ? true : undefined}\n aria-describedby={error ? errorId : hintId}\n onChange={(e) => {\n setText(e.target.value);\n setProblem(null);\n }}\n onKeyDown={(e) => {\n if (e.nativeEvent.isComposing) return;\n if (e.key === 'Enter' || e.key === ',') {\n e.preventDefault();\n commit();\n }\n }}\n onBlur={commit}\n />\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={commit}\n disabled={disabled || full || !text.trim()}\n >\n <Plus className=\"mr-1 h-4 w-4\" aria-hidden={true} />\n {tr('planmagnet.skills.editor.add', 'Add')}\n </Button>\n </div>\n {error ? (\n <p id={errorId} role=\"alert\" className=\"text-xs text-destructive\">\n {error}\n </p>\n ) : (\n <p id={hintId} className=\"text-xs text-muted-foreground\">\n {counter} ·{' '}\n {hint ??\n tr(\n 'planmagnet.skills.editor.hint',\n 'Press Enter or type a comma to add a skill. Matched against people’s skills in CrewFoundry, ignoring case.'\n )}\n </p>\n )}\n </div>\n );\n}\n\nfunction describeProblem(\n problem: SkillTagProblem,\n tr: (key: string, fallback: string, params?: Record<string, string | number>) => string\n): string {\n switch (problem.kind) {\n case 'duplicate':\n return tr('planmagnet.skills.editor.duplicate', '“{{skill}}” is already on the list.', {\n skill: problem.skill,\n });\n case 'tooLong':\n return tr(\n 'planmagnet.skills.editor.tooLong',\n 'A skill can be at most {{max}} characters long.',\n { max: problem.max }\n );\n case 'tooMany':\n return tr('planmagnet.skills.editor.tooMany', 'A task can require at most {{max}} skills.', {\n max: problem.max,\n });\n case 'invalid':\n return tr(\n 'planmagnet.skills.editor.invalid',\n 'A skill can only contain printable characters.'\n );\n }\n}\n\n/**\n * The labelled \"Required skills\" field of a task form: the editor, or — on the edit page, while the saved list is\n * loading or when it could not be loaded — a line saying so with the editor disabled, so a list the user never saw\n * is never edited (or sent) blind.\n */\nexport function RequiredSkillsField({\n id,\n value,\n onChange,\n disabled = false,\n serverError = null,\n loading = false,\n loadError = null,\n}: {\n id: string;\n value: readonly string[];\n onChange: (next: string[]) => void;\n disabled?: boolean;\n serverError?: string | null;\n /** The saved list is still loading (edit page). */\n loading?: boolean;\n /** The saved list could not be loaded (edit page). */\n loadError?: string | null;\n}) {\n const tr = useTr();\n return (\n <div data-testid=\"required-skills-field\">\n <label htmlFor={id} className=\"text-sm font-medium\">\n {tr('planmagnet.skills.editor.label', 'Required skills')}\n </label>\n {loadError ? (\n <p role=\"alert\" className=\"mt-1 text-xs text-destructive\">\n {tr(\n 'planmagnet.skills.editor.loadError',\n 'Required skills could not be loaded, so they can’t be changed here right now: {{message}}',\n { message: loadError }\n )}\n </p>\n ) : (\n <RequiredSkillsEditor\n id={id}\n value={value}\n onChange={onChange}\n disabled={disabled || loading}\n serverError={serverError}\n hint={\n loading ? tr('planmagnet.skills.editor.loading', 'Loading required skills…') : undefined\n }\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/** Read-only skill chips. Renders nothing for an empty list: the caller says what "none" means in its context. */
|
|
4
|
+
export declare function SkillChipList({ skills, label, testId, }: {
|
|
5
|
+
skills: readonly string[];
|
|
6
|
+
/** Accessible name of the list, e.g. "Required skills". */
|
|
7
|
+
label: string;
|
|
8
|
+
testId?: string;
|
|
9
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
10
|
+
/**
|
|
11
|
+
* A link to a person's CrewFoundry profile, where their skills are edited. CrewFoundry owns people's skills
|
|
12
|
+
* (owner decision 2026-10-02): PlanMagnet shows them read-only and never offers to change them itself.
|
|
13
|
+
*/
|
|
14
|
+
export declare function CrewFoundryProfileLink({ employeeId, name, children, className, }: {
|
|
15
|
+
employeeId: string;
|
|
16
|
+
name: string;
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
className?: string;
|
|
19
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
/**
|
|
21
|
+
* One person's CrewFoundry skills (read-only chips) with a link to edit them in CrewFoundry — the sprint capacity
|
|
22
|
+
* views (ANR-07). An empty list says so rather than showing nothing, since "no skills recorded" is worth knowing
|
|
23
|
+
* when planning who can take what.
|
|
24
|
+
*/
|
|
25
|
+
export declare function MemberSkills({ skills, employeeId, employeeName, }: {
|
|
26
|
+
skills: readonly string[];
|
|
27
|
+
employeeId: string;
|
|
28
|
+
employeeName: string;
|
|
29
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
30
|
+
//# sourceMappingURL=SkillChips.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SkillChips.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/skills/SkillChips.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAWvC,kHAAkH;AAClH,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,KAAK,EACL,MAAM,GACP,EAAE;IACD,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,kDAaA;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,UAAU,EACV,IAAI,EACJ,QAAQ,EACR,SAAS,GACV,EAAE;IACD,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,2CAuBA;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EACN,UAAU,EACV,YAAY,GACb,EAAE;IACD,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB,2CAiBA"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useTr as e } from "../../shared/hooks/useTr.js";
|
|
2
|
+
import { crewFoundryEmployeeUrl as t, skillKey as n } from "./skillTags.js";
|
|
3
|
+
import { ExternalLink as r } from "lucide-react";
|
|
4
|
+
import { jsx as i, jsxs as a } from "react/jsx-runtime";
|
|
5
|
+
import { Badge as o } from "@burdenoff/fe-libs/ui";
|
|
6
|
+
//#region src/planmagnet/skills/SkillChips.tsx
|
|
7
|
+
function s() {
|
|
8
|
+
return typeof window > "u" ? "" : window.location.hostname;
|
|
9
|
+
}
|
|
10
|
+
function c({ skills: e, label: t, testId: r }) {
|
|
11
|
+
return e.length === 0 ? null : /* @__PURE__ */ i("ul", {
|
|
12
|
+
className: "flex flex-wrap gap-1",
|
|
13
|
+
"aria-label": t,
|
|
14
|
+
"data-testid": r,
|
|
15
|
+
children: e.map((e, t) => /* @__PURE__ */ i("li", { children: /* @__PURE__ */ i(o, {
|
|
16
|
+
variant: "secondary",
|
|
17
|
+
className: "font-normal",
|
|
18
|
+
children: e
|
|
19
|
+
}) }, `${n(e)}-${t}`))
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function l({ employeeId: n, name: o, children: c, className: l }) {
|
|
23
|
+
let u = e()("planmagnet.skills.crewfoundry.openProfile", "Open {{name}} in CrewFoundry, where skills are edited", { name: o });
|
|
24
|
+
return /* @__PURE__ */ a("a", {
|
|
25
|
+
href: t(n, s()),
|
|
26
|
+
target: "_blank",
|
|
27
|
+
rel: "noopener noreferrer",
|
|
28
|
+
title: u,
|
|
29
|
+
"aria-label": c ? void 0 : u,
|
|
30
|
+
className: l ?? "inline-flex items-center gap-1 text-xs text-primary underline-offset-2 hover:underline",
|
|
31
|
+
children: [c, /* @__PURE__ */ i(r, {
|
|
32
|
+
className: "h-3 w-3 shrink-0",
|
|
33
|
+
"aria-hidden": !0
|
|
34
|
+
})]
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function u({ skills: t, employeeId: n, employeeName: r }) {
|
|
38
|
+
let o = e();
|
|
39
|
+
return /* @__PURE__ */ a("div", {
|
|
40
|
+
className: "mt-1 flex flex-wrap items-center gap-1",
|
|
41
|
+
"data-testid": "member-skills",
|
|
42
|
+
children: [t.length > 0 ? /* @__PURE__ */ i(c, {
|
|
43
|
+
skills: t,
|
|
44
|
+
label: o("planmagnet.skills.member.label", "{{name}}’s skills", { name: r })
|
|
45
|
+
}) : /* @__PURE__ */ i("span", {
|
|
46
|
+
className: "text-xs text-muted-foreground",
|
|
47
|
+
children: o("planmagnet.skills.member.none", "No skills in CrewFoundry")
|
|
48
|
+
}), /* @__PURE__ */ i(l, {
|
|
49
|
+
employeeId: n,
|
|
50
|
+
name: r
|
|
51
|
+
})]
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
export { l as CrewFoundryProfileLink, u as MemberSkills, c as SkillChipList };
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=SkillChips.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SkillChips.js","names":[],"sources":["../../../src/planmagnet/skills/SkillChips.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { Badge } from '@burdenoff/fe-libs/ui';\nimport { ExternalLink } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { crewFoundryEmployeeUrl, skillKey } from './skillTags';\n\n/** The page's host name, for links into CrewFoundry in the same environment. */\nfunction currentHostname(): string {\n return typeof window === 'undefined' ? '' : window.location.hostname;\n}\n\n/** Read-only skill chips. Renders nothing for an empty list: the caller says what \"none\" means in its context. */\nexport function SkillChipList({\n skills,\n label,\n testId,\n}: {\n skills: readonly string[];\n /** Accessible name of the list, e.g. \"Required skills\". */\n label: string;\n testId?: string;\n}) {\n if (skills.length === 0) return null;\n return (\n <ul className=\"flex flex-wrap gap-1\" aria-label={label} data-testid={testId}>\n {skills.map((skill, index) => (\n <li key={`${skillKey(skill)}-${index}`}>\n <Badge variant=\"secondary\" className=\"font-normal\">\n {skill}\n </Badge>\n </li>\n ))}\n </ul>\n );\n}\n\n/**\n * A link to a person's CrewFoundry profile, where their skills are edited. CrewFoundry owns people's skills\n * (owner decision 2026-10-02): PlanMagnet shows them read-only and never offers to change them itself.\n */\nexport function CrewFoundryProfileLink({\n employeeId,\n name,\n children,\n className,\n}: {\n employeeId: string;\n name: string;\n children?: ReactNode;\n className?: string;\n}) {\n const tr = useTr();\n const title = tr(\n 'planmagnet.skills.crewfoundry.openProfile',\n 'Open {{name}} in CrewFoundry, where skills are edited',\n { name }\n );\n return (\n <a\n href={crewFoundryEmployeeUrl(employeeId, currentHostname())}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n title={title}\n aria-label={children ? undefined : title}\n className={\n className ??\n 'inline-flex items-center gap-1 text-xs text-primary underline-offset-2 hover:underline'\n }\n >\n {children}\n <ExternalLink className=\"h-3 w-3 shrink-0\" aria-hidden={true} />\n </a>\n );\n}\n\n/**\n * One person's CrewFoundry skills (read-only chips) with a link to edit them in CrewFoundry — the sprint capacity\n * views (ANR-07). An empty list says so rather than showing nothing, since \"no skills recorded\" is worth knowing\n * when planning who can take what.\n */\nexport function MemberSkills({\n skills,\n employeeId,\n employeeName,\n}: {\n skills: readonly string[];\n employeeId: string;\n employeeName: string;\n}) {\n const tr = useTr();\n return (\n <div className=\"mt-1 flex flex-wrap items-center gap-1\" data-testid=\"member-skills\">\n {skills.length > 0 ? (\n <SkillChipList\n skills={skills}\n label={tr('planmagnet.skills.member.label', '{{name}}’s skills', { name: employeeName })}\n />\n ) : (\n <span className=\"text-xs text-muted-foreground\">\n {tr('planmagnet.skills.member.none', 'No skills in CrewFoundry')}\n </span>\n )}\n <CrewFoundryProfileLink employeeId={employeeId} name={employeeName} />\n </div>\n );\n}\n"],"mappings":";;;;;;AAOA,SAAS,IAA0B;AACjC,QAAO,OAAO,SAAW,MAAc,KAAK,OAAO,SAAS;;AAI9D,SAAgB,EAAc,EAC5B,WACA,UACA,aAMC;AAED,QADI,EAAO,WAAW,IAAU,OAE9B,kBAAC,MAAD;EAAI,WAAU;EAAuB,cAAY;EAAO,eAAa;YAClE,EAAO,KAAK,GAAO,MAClB,kBAAC,MAAD,EAAA,UACE,kBAAC,GAAD;GAAO,SAAQ;GAAY,WAAU;aAClC;GACK,CAAA,EACL,EAJI,GAAG,EAAS,EAAM,CAAC,GAAG,IAI1B,CACL;EACC,CAAA;;AAQT,SAAgB,EAAuB,EACrC,eACA,SACA,aACA,gBAMC;CAED,IAAM,IADK,GAAO,CAEhB,6CACA,yDACA,EAAE,SAAM,CACT;AACD,QACE,kBAAC,KAAD;EACE,MAAM,EAAuB,GAAY,GAAiB,CAAC;EAC3D,QAAO;EACP,KAAI;EACG;EACP,cAAY,IAAW,KAAA,IAAY;EACnC,WACE,KACA;YARJ,CAWG,GACD,kBAAC,GAAD;GAAc,WAAU;GAAmB,eAAa;GAAQ,CAAA,CAC9D;;;AASR,SAAgB,EAAa,EAC3B,WACA,eACA,mBAKC;CACD,IAAM,IAAK,GAAO;AAClB,QACE,kBAAC,OAAD;EAAK,WAAU;EAAyC,eAAY;YAApE,CACG,EAAO,SAAS,IACf,kBAAC,GAAD;GACU;GACR,OAAO,EAAG,kCAAkC,qBAAqB,EAAE,MAAM,GAAc,CAAC;GACxF,CAAA,GAEF,kBAAC,QAAD;GAAM,WAAU;aACb,EAAG,iCAAiC,2BAA2B;GAC3D,CAAA,EAET,kBAAC,GAAD;GAAoC;GAAY,MAAM;GAAgB,CAAA,CAClE"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills coverage (ANR-07 skill view on the project manager dashboard, ANR-28 skill utilization on the team
|
|
3
|
+
* dashboard): per skill, the open work items that require it (`WorkItem.requiredSkills`), how much of that work is
|
|
4
|
+
* assigned, and which members are qualified by their CrewFoundry skills. UNCOVERED rows come first.
|
|
5
|
+
*
|
|
6
|
+
* Honest by construction: when CrewFoundry or the workspace member list cannot be read, the service reports every
|
|
7
|
+
* skill with demand as UNKNOWN (never UNCOVERED) and this card says why above the list; a truncated scan says the
|
|
8
|
+
* demand figures are partial; an empty result says what "nothing" means here. Nothing renders as an "all good".
|
|
9
|
+
* Utilization is counts and story points only — hours-based skill utilization waits for a per-person capacity
|
|
10
|
+
* baseline in CrewFoundry (CWF-28), and the card says so. People's skills are read-only here (CrewFoundry owns
|
|
11
|
+
* them); each name links to the person's CrewFoundry profile.
|
|
12
|
+
*
|
|
13
|
+
* Reads are gated like the dashboards' other cards: the gateway enforces `planmagnet-project:read` and a refusal
|
|
14
|
+
* shows as "You do not have access" (DashboardCard).
|
|
15
|
+
*/
|
|
16
|
+
export declare function SkillCoverageCard({ workspaceId, projectId, teamId, sprintId, notReadyMessage, }: {
|
|
17
|
+
workspaceId?: string | null;
|
|
18
|
+
projectId?: string | null;
|
|
19
|
+
teamId?: string | null;
|
|
20
|
+
sprintId?: string | null;
|
|
21
|
+
/** Shown until the page's project / team is picked. */
|
|
22
|
+
notReadyMessage: string;
|
|
23
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
//# sourceMappingURL=SkillCoverageCard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SkillCoverageCard.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/skills/SkillCoverageCard.tsx"],"names":[],"mappings":"AA0BA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,WAAW,EACX,SAAS,EACT,MAAM,EACN,QAAQ,EACR,eAAe,GAChB,EAAE;IACD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,uDAAuD;IACvD,eAAe,EAAE,MAAM,CAAC;CACzB,2CA2LA"}
|