@burdenoff/microfe-planmagnet 2026.1002.2 → 2026.1008.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/dist/generated/wspace-operations.d.ts +574 -0
  2. package/dist/generated/wspace-operations.d.ts.map +1 -1
  3. package/dist/generated/wspace-types.d.ts +486 -16
  4. package/dist/generated/wspace-types.d.ts.map +1 -1
  5. package/dist/index.js +8 -8
  6. package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
  7. package/dist/planmagnet/PlanMagnetRoot.js +174 -168
  8. package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
  9. package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
  10. package/dist/planmagnet/PlanMagnetRoutes.js +63 -59
  11. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  12. package/dist/planmagnet/board/BulkActions.d.ts.map +1 -1
  13. package/dist/planmagnet/board/BulkActions.js +106 -55
  14. package/dist/planmagnet/board/BulkActions.js.map +1 -1
  15. package/dist/planmagnet/controls/Pickers.d.ts +3 -3
  16. package/dist/planmagnet/controls/Pickers.js.map +1 -1
  17. package/dist/planmagnet/dashboards/DashboardUI.d.ts +0 -4
  18. package/dist/planmagnet/dashboards/DashboardUI.d.ts.map +1 -1
  19. package/dist/planmagnet/dashboards/DashboardUI.js +6 -13
  20. package/dist/planmagnet/dashboards/DashboardUI.js.map +1 -1
  21. package/dist/planmagnet/dashboards/cards/SprintCapacityCard.d.ts.map +1 -1
  22. package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js +70 -59
  23. package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js.map +1 -1
  24. package/dist/planmagnet/forecast/ResourceForecastCard.js +1 -1
  25. package/dist/planmagnet/forecast/ResourceForecastCard.js.map +1 -1
  26. package/dist/planmagnet/index.d.ts +1 -1
  27. package/dist/planmagnet/index.d.ts.map +1 -1
  28. package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.d.ts +3 -3
  29. package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.d.ts.map +1 -1
  30. package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.js +26 -19
  31. package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.js.map +1 -1
  32. package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.d.ts +2 -2
  33. package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.d.ts.map +1 -1
  34. package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.js +18 -12
  35. package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.js.map +1 -1
  36. package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.d.ts +2 -0
  37. package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.d.ts.map +1 -0
  38. package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.js +251 -0
  39. package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.js.map +1 -0
  40. package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
  41. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +94 -92
  42. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
  43. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.d.ts.map +1 -1
  44. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js +116 -114
  45. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js.map +1 -1
  46. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
  47. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +174 -159
  48. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
  49. package/dist/planmagnet/pages/TasksCreateTaskPagePage.d.ts.map +1 -1
  50. package/dist/planmagnet/pages/TasksCreateTaskPagePage.js +159 -145
  51. package/dist/planmagnet/pages/TasksCreateTaskPagePage.js.map +1 -1
  52. package/dist/planmagnet/pages/TasksEditTaskPagePage.d.ts.map +1 -1
  53. package/dist/planmagnet/pages/TasksEditTaskPagePage.js +161 -143
  54. package/dist/planmagnet/pages/TasksEditTaskPagePage.js.map +1 -1
  55. package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
  56. package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +157 -155
  57. package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
  58. package/dist/planmagnet/pages/TeamInviteMemberPagePage.d.ts.map +1 -1
  59. package/dist/planmagnet/pages/TeamInviteMemberPagePage.js +142 -82
  60. package/dist/planmagnet/pages/TeamInviteMemberPagePage.js.map +1 -1
  61. package/dist/planmagnet/pages/analytics/DeliveryKpisCard.d.ts.map +1 -1
  62. package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js +97 -99
  63. package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js.map +1 -1
  64. package/dist/planmagnet/product/FeatureLinkPanels.js +66 -66
  65. package/dist/planmagnet/product/ProductFeatureControls.js +38 -38
  66. package/dist/planmagnet/product/ProductFeatureLinkCard.js +8 -8
  67. package/dist/planmagnet/product/ProductLineControls.js +83 -83
  68. package/dist/planmagnet/product/ProductTeamsPanel.js +6 -6
  69. package/dist/planmagnet/product/routes.d.ts +6 -0
  70. package/dist/planmagnet/product/routes.d.ts.map +1 -1
  71. package/dist/planmagnet/product/routes.js +2 -2
  72. package/dist/planmagnet/product/routes.js.map +1 -1
  73. package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.d.ts +20 -0
  74. package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.d.ts.map +1 -0
  75. package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.js +55 -0
  76. package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.js.map +1 -0
  77. package/dist/planmagnet/satisfaction/SatisfactionFigures.d.ts +30 -0
  78. package/dist/planmagnet/satisfaction/SatisfactionFigures.d.ts.map +1 -0
  79. package/dist/planmagnet/satisfaction/SatisfactionFigures.js +282 -0
  80. package/dist/planmagnet/satisfaction/SatisfactionFigures.js.map +1 -0
  81. package/dist/planmagnet/satisfaction/SatisfactionImportDialog.d.ts +9 -0
  82. package/dist/planmagnet/satisfaction/SatisfactionImportDialog.d.ts.map +1 -0
  83. package/dist/planmagnet/satisfaction/SatisfactionImportDialog.js +390 -0
  84. package/dist/planmagnet/satisfaction/SatisfactionImportDialog.js.map +1 -0
  85. package/dist/planmagnet/satisfaction/SatisfactionLinkFields.d.ts +17 -0
  86. package/dist/planmagnet/satisfaction/SatisfactionLinkFields.d.ts.map +1 -0
  87. package/dist/planmagnet/satisfaction/SatisfactionLinkFields.js +77 -0
  88. package/dist/planmagnet/satisfaction/SatisfactionLinkFields.js.map +1 -0
  89. package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.d.ts +18 -0
  90. package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.d.ts.map +1 -0
  91. package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.js +87 -0
  92. package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.js.map +1 -0
  93. package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.d.ts +13 -0
  94. package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.d.ts.map +1 -0
  95. package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.js +326 -0
  96. package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.js.map +1 -0
  97. package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.d.ts +23 -0
  98. package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.d.ts.map +1 -0
  99. package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.js +258 -0
  100. package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.js.map +1 -0
  101. package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.d.ts +6 -0
  102. package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.d.ts.map +1 -0
  103. package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.js +56 -0
  104. package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.js.map +1 -0
  105. package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.d.ts +16 -0
  106. package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.d.ts.map +1 -0
  107. package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.js +102 -0
  108. package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.js.map +1 -0
  109. package/dist/planmagnet/satisfaction/satisfaction.d.ts +154 -0
  110. package/dist/planmagnet/satisfaction/satisfaction.d.ts.map +1 -0
  111. package/dist/planmagnet/satisfaction/satisfaction.js +210 -0
  112. package/dist/planmagnet/satisfaction/satisfaction.js.map +1 -0
  113. package/dist/planmagnet/satisfaction/satisfactionCsv.d.ts +108 -0
  114. package/dist/planmagnet/satisfaction/satisfactionCsv.d.ts.map +1 -0
  115. package/dist/planmagnet/satisfaction/satisfactionCsv.js +211 -0
  116. package/dist/planmagnet/satisfaction/satisfactionCsv.js.map +1 -0
  117. package/dist/planmagnet/satisfaction/satisfactionForm.d.ts +90 -0
  118. package/dist/planmagnet/satisfaction/satisfactionForm.d.ts.map +1 -0
  119. package/dist/planmagnet/satisfaction/satisfactionForm.js +90 -0
  120. package/dist/planmagnet/satisfaction/satisfactionForm.js.map +1 -0
  121. package/dist/planmagnet/satisfaction/satisfactionOperations.d.ts +116 -0
  122. package/dist/planmagnet/satisfaction/satisfactionOperations.d.ts.map +1 -0
  123. package/dist/planmagnet/satisfaction/satisfactionOperations.js +86 -0
  124. package/dist/planmagnet/satisfaction/satisfactionOperations.js.map +1 -0
  125. package/dist/planmagnet/satisfaction/useSatisfactionAccess.d.ts +20 -0
  126. package/dist/planmagnet/satisfaction/useSatisfactionAccess.d.ts.map +1 -0
  127. package/dist/planmagnet/satisfaction/useSatisfactionAccess.js +31 -0
  128. package/dist/planmagnet/satisfaction/useSatisfactionAccess.js.map +1 -0
  129. package/dist/planmagnet/skills/RequiredSkillsEditor.d.ts +35 -0
  130. package/dist/planmagnet/skills/RequiredSkillsEditor.d.ts.map +1 -0
  131. package/dist/planmagnet/skills/RequiredSkillsEditor.js +124 -0
  132. package/dist/planmagnet/skills/RequiredSkillsEditor.js.map +1 -0
  133. package/dist/planmagnet/skills/SkillChips.d.ts +30 -0
  134. package/dist/planmagnet/skills/SkillChips.d.ts.map +1 -0
  135. package/dist/planmagnet/skills/SkillChips.js +57 -0
  136. package/dist/planmagnet/skills/SkillChips.js.map +1 -0
  137. package/dist/planmagnet/skills/SkillCoverageCard.d.ts +24 -0
  138. package/dist/planmagnet/skills/SkillCoverageCard.d.ts.map +1 -0
  139. package/dist/planmagnet/skills/SkillCoverageCard.js +256 -0
  140. package/dist/planmagnet/skills/SkillCoverageCard.js.map +1 -0
  141. package/dist/planmagnet/skills/WorkItemRequiredSkills.d.ts +10 -0
  142. package/dist/planmagnet/skills/WorkItemRequiredSkills.d.ts.map +1 -0
  143. package/dist/planmagnet/skills/WorkItemRequiredSkills.js +65 -0
  144. package/dist/planmagnet/skills/WorkItemRequiredSkills.js.map +1 -0
  145. package/dist/planmagnet/skills/skillOperations.d.ts +52 -0
  146. package/dist/planmagnet/skills/skillOperations.d.ts.map +1 -0
  147. package/dist/planmagnet/skills/skillOperations.js +44 -0
  148. package/dist/planmagnet/skills/skillOperations.js.map +1 -0
  149. package/dist/planmagnet/skills/skillTags.d.ts +103 -0
  150. package/dist/planmagnet/skills/skillTags.d.ts.map +1 -0
  151. package/dist/planmagnet/skills/skillTags.js +115 -0
  152. package/dist/planmagnet/skills/skillTags.js.map +1 -0
  153. package/dist/planmagnet/skills/useRequiredSkillsDraft.d.ts +28 -0
  154. package/dist/planmagnet/skills/useRequiredSkillsDraft.d.ts.map +1 -0
  155. package/dist/planmagnet/skills/useRequiredSkillsDraft.js +30 -0
  156. package/dist/planmagnet/skills/useRequiredSkillsDraft.js.map +1 -0
  157. package/dist/planmagnet/team/PendingInvitations.d.ts +27 -0
  158. package/dist/planmagnet/team/PendingInvitations.d.ts.map +1 -0
  159. package/dist/planmagnet/team/PendingInvitations.js +220 -0
  160. package/dist/planmagnet/team/PendingInvitations.js.map +1 -0
  161. package/dist/planmagnet/team/invitationEmail.d.ts +35 -0
  162. package/dist/planmagnet/team/invitationEmail.d.ts.map +1 -0
  163. package/dist/planmagnet/team/invitationEmail.js +73 -0
  164. package/dist/planmagnet/team/invitationEmail.js.map +1 -0
  165. package/package.json +1 -1
@@ -0,0 +1,258 @@
1
+ import { usePlanMagnetNavigate as e } from "../providers/PlanMagnetProvider.js";
2
+ import { useTr as t } from "../../shared/hooks/useTr.js";
3
+ import { isForbidden as n } from "../utils/graphqlErrors.js";
4
+ import { throwOnError as r } from "../utils/mutationResult.js";
5
+ import { ConfirmButton as i, ErrorLine as a } from "../controls/ConfirmButton.js";
6
+ import { SCORE_SCALE as o, errorCode as s, formatUtcDay as c, releaseLabel as l, scoreCategory as u } from "./satisfaction.js";
7
+ import { CATEGORY_CLASS as d, categoryLabel as f } from "./SatisfactionFigures.js";
8
+ import { CUSTOMER_SATISFACTION_RESPONSES as p, DELETE_CUSTOMER_SATISFACTION_RESPONSE as m, SATISFACTION_QUERIES as h } from "./satisfactionOperations.js";
9
+ import { detailPath as g } from "../utils/rowNavigation.js";
10
+ import { refusalMessage as _ } from "../product/refusal.js";
11
+ import { SatisfactionResponseDialog as v } from "./SatisfactionResponseDialog.js";
12
+ import { useState as y } from "react";
13
+ import { ChevronLeft as b, ChevronRight as x, Loader2 as S, Trash2 as C } from "lucide-react";
14
+ import { useMutation as w, useQuery as T } from "@apollo/client/react";
15
+ import { jsx as E, jsxs as D } from "react/jsx-runtime";
16
+ import { Badge as O, Button as k, Table as A, TableBody as j, TableCell as M, TableHead as N, TableHeader as P, TableRow as F } from "@burdenoff/fe-libs/ui";
17
+ //#region src/planmagnet/satisfaction/SatisfactionResponsesTable.tsx
18
+ var I = 25, L = 180;
19
+ function R({ text: e }) {
20
+ let n = t(), [r, i] = y(!1), a = e.length > L;
21
+ return /* @__PURE__ */ D("div", {
22
+ className: "min-w-48 max-w-md",
23
+ children: [/* @__PURE__ */ E("p", {
24
+ className: "whitespace-pre-wrap break-words text-sm",
25
+ children: a && !r ? `${e.slice(0, L).trimEnd()}…` : e
26
+ }), a && /* @__PURE__ */ E("button", {
27
+ type: "button",
28
+ className: "text-xs text-primary hover:underline",
29
+ "aria-expanded": r,
30
+ onClick: () => i((e) => !e),
31
+ children: r ? n("planmagnet.satisfaction.list.showLess", "Show less") : n("planmagnet.satisfaction.list.showMore", "Show more")
32
+ })]
33
+ });
34
+ }
35
+ function z({ label: e, onClick: t }) {
36
+ return /* @__PURE__ */ E("button", {
37
+ type: "button",
38
+ className: "block max-w-56 truncate text-left text-sm text-primary hover:underline",
39
+ onClick: t,
40
+ title: e,
41
+ children: e
42
+ });
43
+ }
44
+ function B({ response: n }) {
45
+ let r = t(), i = e(), a = (e) => /* @__PURE__ */ E("p", {
46
+ className: "text-sm text-muted-foreground",
47
+ children: e
48
+ });
49
+ return /* @__PURE__ */ D("div", {
50
+ className: "space-y-0.5",
51
+ children: [
52
+ n.product ? /* @__PURE__ */ E(z, {
53
+ label: n.product.name,
54
+ onClick: () => i(g("/products/product-detail-page", n.product?.id))
55
+ }) : n.productId && a(r("planmagnet.satisfaction.list.productDeleted", "Deleted product")),
56
+ n.release ? /* @__PURE__ */ E(z, {
57
+ label: l(n.release),
58
+ onClick: () => i(g("/products/release-detail-page", n.release?.id))
59
+ }) : n.releaseId && a(r("planmagnet.satisfaction.list.releaseDeleted", "Deleted release")),
60
+ n.project ? /* @__PURE__ */ E(z, {
61
+ label: n.project.name,
62
+ onClick: () => i(g("/projects/project-detail-page", n.project?.id))
63
+ }) : n.projectId && a(r("planmagnet.satisfaction.list.projectDeleted", "Deleted project"))
64
+ ]
65
+ });
66
+ }
67
+ function V({ response: e }) {
68
+ let n = t();
69
+ return e.isAnonymous ? /* @__PURE__ */ E("span", {
70
+ className: "text-sm italic text-muted-foreground",
71
+ children: n("planmagnet.satisfaction.list.anonymous", "Anonymous")
72
+ }) : !e.respondentName && !e.respondentEmail ? /* @__PURE__ */ E("span", {
73
+ className: "text-sm text-muted-foreground",
74
+ children: n("planmagnet.satisfaction.list.notRecorded", "Not recorded")
75
+ }) : /* @__PURE__ */ D("div", {
76
+ className: "max-w-56 text-sm",
77
+ children: [e.respondentName && /* @__PURE__ */ E("p", {
78
+ className: "truncate",
79
+ children: e.respondentName
80
+ }), e.respondentEmail && /* @__PURE__ */ E("p", {
81
+ className: "truncate text-xs text-muted-foreground",
82
+ children: e.respondentEmail
83
+ })]
84
+ });
85
+ }
86
+ function H({ response: e, onDeleted: n, onFailed: a, onStale: o }) {
87
+ let l = t(), [u, d] = w(m, {
88
+ refetchQueries: h,
89
+ awaitRefetchQueries: !0
90
+ }), f = async () => {
91
+ try {
92
+ await r(u({ variables: { id: e.id } })), n();
93
+ } catch (e) {
94
+ a(_(e, l("planmagnet.satisfaction.errors.delete", "The response could not be deleted."), l("planmagnet.satisfaction.errors.forbidden", "Your role cannot change customer satisfaction responses in this workspace."))), s(e) === "NOT_FOUND" && o();
95
+ }
96
+ }, p = c(e.respondedAt);
97
+ return /* @__PURE__ */ E(i, {
98
+ label: "",
99
+ ariaLabel: l("planmagnet.satisfaction.actions.deleteNamed", "Delete the response of {{date}}", { date: p }),
100
+ variant: "outline",
101
+ icon: /* @__PURE__ */ E(C, { className: "h-4 w-4" }),
102
+ testId: "satisfaction-response-delete",
103
+ title: l("planmagnet.satisfaction.confirmDelete.title", "Delete this response?"),
104
+ description: l("planmagnet.satisfaction.confirmDelete.description", "The {{metric}} answer of {{date}} stops counting in every customer satisfaction figure. This cannot be undone here.", {
105
+ metric: e.metricType,
106
+ date: p
107
+ }),
108
+ confirmLabel: l("planmagnet.satisfaction.confirmDelete.confirm", "Delete response"),
109
+ cancelLabel: l("planmagnet.satisfaction.confirmDelete.cancel", "Keep it"),
110
+ destructive: !0,
111
+ disabled: d.loading,
112
+ onConfirm: () => void f()
113
+ });
114
+ }
115
+ function U({ workspaceId: e, filter: r, access: i, links: s, filtered: l, onNotice: m, onStale: h }) {
116
+ let g = t(), [_, C] = y([null]), w = _[_.length - 1], { data: L, previousData: z, loading: U, error: W } = T(p, { variables: {
117
+ workspaceId: e,
118
+ filter: r,
119
+ pagination: {
120
+ first: I,
121
+ ...w ? { after: w } : {}
122
+ }
123
+ } }), G = (L ?? z)?.planmagnetCustomerSatisfactionResponses, K = G?.items ?? [], q = G?.totalCount ?? 0, J = (_.length - 1) * I, Y = i.canUpdate || i.canDelete;
124
+ return U && !G ? /* @__PURE__ */ D("div", {
125
+ className: "flex items-center gap-2 py-8 text-sm text-muted-foreground",
126
+ children: [/* @__PURE__ */ E(S, { className: "h-4 w-4 animate-spin" }), g("planmagnet.satisfaction.list.loading", "Loading responses…")]
127
+ }) : W && !G ? /* @__PURE__ */ E(a, { message: n(W) ? g("planmagnet.satisfaction.errors.readForbidden", "Your role cannot see customer satisfaction in this workspace.") : g("planmagnet.satisfaction.list.error", "The responses could not be loaded: {{message}}", { message: W.message }) }) : /* @__PURE__ */ D("div", {
128
+ className: "space-y-3",
129
+ "data-testid": "satisfaction-responses",
130
+ children: [
131
+ W && /* @__PURE__ */ E(a, { message: g("planmagnet.satisfaction.list.refreshError", "The list could not be refreshed: {{message}}", { message: W.message }) }),
132
+ K.length === 0 ? /* @__PURE__ */ E("p", {
133
+ className: "py-8 text-center text-sm text-muted-foreground",
134
+ "data-testid": "satisfaction-responses-empty",
135
+ children: _.length > 1 ? g("planmagnet.satisfaction.list.emptyPage", "This page has no responses left — go back to the previous page.") : l ? g("planmagnet.satisfaction.list.emptyFiltered", "No response matches these filters in this window.") : g("planmagnet.satisfaction.list.empty", "No responses in this window yet.")
136
+ }) : /* @__PURE__ */ E("div", {
137
+ className: "overflow-x-auto",
138
+ children: /* @__PURE__ */ D(A, { children: [/* @__PURE__ */ E(P, { children: /* @__PURE__ */ D(F, { children: [
139
+ /* @__PURE__ */ E(N, { children: g("planmagnet.satisfaction.list.answered", "Answered") }),
140
+ /* @__PURE__ */ E(N, { children: g("planmagnet.satisfaction.list.score", "Score") }),
141
+ /* @__PURE__ */ E(N, { children: g("planmagnet.satisfaction.list.respondent", "Respondent") }),
142
+ /* @__PURE__ */ E(N, { children: g("planmagnet.satisfaction.list.about", "About") }),
143
+ /* @__PURE__ */ E(N, { children: g("planmagnet.satisfaction.list.comment", "Comment") }),
144
+ /* @__PURE__ */ E(N, { children: g("planmagnet.satisfaction.list.source", "Source") }),
145
+ Y && /* @__PURE__ */ E(N, {
146
+ className: "text-right",
147
+ children: /* @__PURE__ */ E("span", {
148
+ className: "sr-only",
149
+ children: g("planmagnet.satisfaction.list.actions", "Actions")
150
+ })
151
+ })
152
+ ] }) }), /* @__PURE__ */ E(j, { children: K.map((e) => {
153
+ let t = u(e.metricType, e.score);
154
+ return /* @__PURE__ */ D(F, {
155
+ "data-testid": "satisfaction-response-row",
156
+ children: [
157
+ /* @__PURE__ */ E(M, {
158
+ className: "whitespace-nowrap text-sm",
159
+ children: c(e.respondedAt)
160
+ }),
161
+ /* @__PURE__ */ E(M, {
162
+ className: "whitespace-nowrap",
163
+ children: /* @__PURE__ */ D("div", {
164
+ className: "flex items-center gap-2",
165
+ children: [
166
+ /* @__PURE__ */ E(O, {
167
+ variant: "outline",
168
+ className: "text-xs",
169
+ children: e.metricType
170
+ }),
171
+ /* @__PURE__ */ E("span", {
172
+ className: "font-medium tabular-nums",
173
+ children: g("planmagnet.satisfaction.list.scoreOf", "{{score}} / {{max}}", {
174
+ score: e.score,
175
+ max: o[e.metricType].max
176
+ })
177
+ }),
178
+ /* @__PURE__ */ E("span", {
179
+ className: `rounded px-1.5 py-0.5 text-xs ${d[t]}`,
180
+ children: f(g, t)
181
+ })
182
+ ]
183
+ })
184
+ }),
185
+ /* @__PURE__ */ E(M, { children: /* @__PURE__ */ E(V, { response: e }) }),
186
+ /* @__PURE__ */ E(M, { children: /* @__PURE__ */ E(B, { response: e }) }),
187
+ /* @__PURE__ */ E(M, { children: e.comment ? /* @__PURE__ */ E(R, { text: e.comment }) : /* @__PURE__ */ E("span", {
188
+ className: "text-sm text-muted-foreground",
189
+ children: "—"
190
+ }) }),
191
+ /* @__PURE__ */ D(M, {
192
+ className: "text-xs text-muted-foreground",
193
+ children: [/* @__PURE__ */ E("p", { children: e.source === "IMPORT" ? g("planmagnet.satisfaction.list.sourceImport", "Imported") : g("planmagnet.satisfaction.list.sourceManual", "Entered") }), e.externalId && /* @__PURE__ */ E("p", {
194
+ className: "max-w-40 truncate font-mono",
195
+ title: e.externalId,
196
+ children: e.externalId
197
+ })]
198
+ }),
199
+ Y && /* @__PURE__ */ E(M, {
200
+ className: "text-right",
201
+ children: /* @__PURE__ */ D("div", {
202
+ className: "flex justify-end gap-1",
203
+ children: [i.canUpdate && /* @__PURE__ */ E(v, {
204
+ response: e,
205
+ links: s,
206
+ onSaved: () => m(g("planmagnet.satisfaction.notice.updated", "The response was saved."), "status"),
207
+ onStale: h
208
+ }), i.canDelete && /* @__PURE__ */ E(H, {
209
+ response: e,
210
+ onDeleted: () => m(g("planmagnet.satisfaction.notice.deleted", "The response was deleted. It no longer counts in any figure."), "status"),
211
+ onFailed: (e) => m(e, "alert"),
212
+ onStale: h
213
+ })]
214
+ })
215
+ })
216
+ ]
217
+ }, e.id);
218
+ }) })] })
219
+ }),
220
+ (q > 0 || _.length > 1) && /* @__PURE__ */ D("div", {
221
+ className: "flex flex-wrap items-center justify-between gap-2",
222
+ children: [/* @__PURE__ */ E("p", {
223
+ className: "text-xs text-muted-foreground",
224
+ "data-testid": "satisfaction-responses-range",
225
+ children: K.length > 0 ? g("planmagnet.satisfaction.list.range", "Showing {{from}}–{{to}} of {{total}}", {
226
+ from: J + 1,
227
+ to: J + K.length,
228
+ total: q
229
+ }) : ""
230
+ }), /* @__PURE__ */ D("div", {
231
+ className: "flex gap-2",
232
+ children: [/* @__PURE__ */ D(k, {
233
+ size: "sm",
234
+ variant: "outline",
235
+ disabled: _.length === 1 || U,
236
+ onClick: () => C((e) => e.slice(0, -1)),
237
+ "data-testid": "satisfaction-responses-previous",
238
+ children: [/* @__PURE__ */ E(b, { className: "mr-1 h-4 w-4" }), g("planmagnet.satisfaction.list.previous", "Previous")]
239
+ }), /* @__PURE__ */ D(k, {
240
+ size: "sm",
241
+ variant: "outline",
242
+ disabled: !G?.pageInfo.hasNextPage || !G.pageInfo.endCursor || U,
243
+ onClick: () => {
244
+ let e = G?.pageInfo.endCursor;
245
+ e && C((t) => [...t, e]);
246
+ },
247
+ "data-testid": "satisfaction-responses-next",
248
+ children: [g("planmagnet.satisfaction.list.next", "Next"), /* @__PURE__ */ E(x, { className: "ml-1 h-4 w-4" })]
249
+ })]
250
+ })]
251
+ })
252
+ ]
253
+ });
254
+ }
255
+ //#endregion
256
+ export { U as SatisfactionResponsesTable };
257
+
258
+ //# sourceMappingURL=SatisfactionResponsesTable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SatisfactionResponsesTable.js","names":[],"sources":["../../../src/planmagnet/satisfaction/SatisfactionResponsesTable.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Badge,\n Button,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@burdenoff/fe-libs/ui';\nimport { ChevronLeft, ChevronRight, Loader2, Trash2 } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { ConfirmButton, ErrorLine } from '../controls/ConfirmButton';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { refusalMessage } from '../product/refusal';\nimport { isForbidden } from '../utils/graphqlErrors';\nimport { throwOnError } from '../utils/mutationResult';\nimport { detailPath } from '../utils/rowNavigation';\nimport { CATEGORY_CLASS, categoryLabel } from './SatisfactionFigures';\nimport { SatisfactionResponseDialog } from './SatisfactionResponseDialog';\nimport {\n SCORE_SCALE,\n errorCode,\n formatUtcDay,\n releaseLabel,\n scoreCategory,\n type SatisfactionMetricType,\n type SatisfactionWindowInput,\n} from './satisfaction';\nimport type { FormLinks } from './satisfactionForm';\nimport {\n CUSTOMER_SATISFACTION_RESPONSES,\n DELETE_CUSTOMER_SATISFACTION_RESPONSE,\n SATISFACTION_QUERIES,\n type CustomerSatisfactionResponsesData,\n type SatisfactionResponse,\n} from './satisfactionOperations';\nimport type { SatisfactionAccess } from './useSatisfactionAccess';\n\n/**\n * The responses behind the figures, newest answer first, a page at a time — the same window and filters as the figures\n * above them, so the list and the numbers always agree. Product managers edit and delete from here; viewers read.\n */\n\nconst PAGE_SIZE = 25;\n/** Longer comments are cut here until \"Show more\". */\nconst COMMENT_PREVIEW = 180;\n\nexport interface ResponsesFilter {\n productId?: string;\n releaseId?: string;\n projectId?: string;\n metricType?: SatisfactionMetricType;\n window: SatisfactionWindowInput;\n}\n\nfunction Comment({ text }: { text: string }) {\n const tr = useTr();\n const [expanded, setExpanded] = useState(false);\n const long = text.length > COMMENT_PREVIEW;\n return (\n <div className=\"min-w-48 max-w-md\">\n <p className=\"whitespace-pre-wrap break-words text-sm\">\n {long && !expanded ? `${text.slice(0, COMMENT_PREVIEW).trimEnd()}…` : text}\n </p>\n {long && (\n <button\n type=\"button\"\n className=\"text-xs text-primary hover:underline\"\n aria-expanded={expanded}\n onClick={() => setExpanded((v) => !v)}\n >\n {expanded\n ? tr('planmagnet.satisfaction.list.showLess', 'Show less')\n : tr('planmagnet.satisfaction.list.showMore', 'Show more')}\n </button>\n )}\n </div>\n );\n}\n\nfunction LinkButton({ label, onClick }: { label: string; onClick: () => void }) {\n return (\n <button\n type=\"button\"\n className=\"block max-w-56 truncate text-left text-sm text-primary hover:underline\"\n onClick={onClick}\n title={label}\n >\n {label}\n </button>\n );\n}\n\nfunction About({ response }: { response: SatisfactionResponse }) {\n const tr = useTr();\n const navigate = usePlanMagnetNavigate();\n const deleted = (text: string) => <p className=\"text-sm text-muted-foreground\">{text}</p>;\n return (\n <div className=\"space-y-0.5\">\n {response.product ? (\n <LinkButton\n label={response.product.name}\n onClick={() =>\n navigate(detailPath('/products/product-detail-page', response.product?.id))\n }\n />\n ) : (\n response.productId &&\n deleted(tr('planmagnet.satisfaction.list.productDeleted', 'Deleted product'))\n )}\n {response.release ? (\n <LinkButton\n label={releaseLabel(response.release)}\n onClick={() =>\n navigate(detailPath('/products/release-detail-page', response.release?.id))\n }\n />\n ) : (\n response.releaseId &&\n deleted(tr('planmagnet.satisfaction.list.releaseDeleted', 'Deleted release'))\n )}\n {response.project ? (\n <LinkButton\n label={response.project.name}\n onClick={() =>\n navigate(detailPath('/projects/project-detail-page', response.project?.id))\n }\n />\n ) : (\n response.projectId &&\n deleted(tr('planmagnet.satisfaction.list.projectDeleted', 'Deleted project'))\n )}\n </div>\n );\n}\n\nfunction Respondent({ response }: { response: SatisfactionResponse }) {\n const tr = useTr();\n if (response.isAnonymous) {\n return (\n <span className=\"text-sm italic text-muted-foreground\">\n {tr('planmagnet.satisfaction.list.anonymous', 'Anonymous')}\n </span>\n );\n }\n if (!response.respondentName && !response.respondentEmail) {\n return (\n <span className=\"text-sm text-muted-foreground\">\n {tr('planmagnet.satisfaction.list.notRecorded', 'Not recorded')}\n </span>\n );\n }\n return (\n <div className=\"max-w-56 text-sm\">\n {response.respondentName && <p className=\"truncate\">{response.respondentName}</p>}\n {response.respondentEmail && (\n <p className=\"truncate text-xs text-muted-foreground\">{response.respondentEmail}</p>\n )}\n </div>\n );\n}\n\nfunction DeleteResponse({\n response,\n onDeleted,\n onFailed,\n onStale,\n}: {\n response: SatisfactionResponse;\n onDeleted: () => void;\n onFailed: (message: string) => void;\n onStale: () => void;\n}) {\n const tr = useTr();\n const [remove, removeState] = useMutation(DELETE_CUSTOMER_SATISFACTION_RESPONSE, {\n refetchQueries: SATISFACTION_QUERIES,\n awaitRefetchQueries: true,\n });\n const onConfirm = async () => {\n try {\n await throwOnError(remove({ variables: { id: response.id } }));\n onDeleted();\n } catch (e) {\n onFailed(\n refusalMessage(\n e,\n tr('planmagnet.satisfaction.errors.delete', 'The response could not be deleted.'),\n tr(\n 'planmagnet.satisfaction.errors.forbidden',\n 'Your role cannot change customer satisfaction responses in this workspace.'\n )\n )\n );\n if (errorCode(e) === 'NOT_FOUND') onStale();\n }\n };\n const date = formatUtcDay(response.respondedAt);\n return (\n <ConfirmButton\n label=\"\"\n ariaLabel={tr(\n 'planmagnet.satisfaction.actions.deleteNamed',\n 'Delete the response of {{date}}',\n {\n date,\n }\n )}\n variant=\"outline\"\n icon={<Trash2 className=\"h-4 w-4\" />}\n testId=\"satisfaction-response-delete\"\n title={tr('planmagnet.satisfaction.confirmDelete.title', 'Delete this response?')}\n description={tr(\n 'planmagnet.satisfaction.confirmDelete.description',\n 'The {{metric}} answer of {{date}} stops counting in every customer satisfaction figure. This cannot be undone here.',\n { metric: response.metricType, date }\n )}\n confirmLabel={tr('planmagnet.satisfaction.confirmDelete.confirm', 'Delete response')}\n cancelLabel={tr('planmagnet.satisfaction.confirmDelete.cancel', 'Keep it')}\n destructive\n disabled={removeState.loading}\n onConfirm={() => void onConfirm()}\n />\n );\n}\n\nexport function SatisfactionResponsesTable({\n workspaceId,\n filter,\n access,\n links,\n filtered,\n onNotice,\n onStale,\n}: {\n workspaceId: string;\n filter: ResponsesFilter;\n access: SatisfactionAccess;\n /** A new response's starting links (the page's filters). */\n links: FormLinks;\n /** True when any product / release / project / metric filter is set — for the empty-state wording. */\n filtered: boolean;\n onNotice: (message: string, tone: 'status' | 'alert') => void;\n onStale: () => void;\n}) {\n const tr = useTr();\n // Cursors of the pages visited so far; the last one is the page on screen (null = the first page).\n const [cursors, setCursors] = useState<(string | null)[]>([null]);\n const after = cursors[cursors.length - 1];\n const { data, previousData, loading, error } = useQuery<CustomerSatisfactionResponsesData>(\n CUSTOMER_SATISFACTION_RESPONSES,\n {\n variables: {\n workspaceId,\n filter,\n pagination: { first: PAGE_SIZE, ...(after ? { after } : {}) },\n },\n }\n );\n // Paging keeps the current rows on screen until the next page arrives.\n const list = (data ?? previousData)?.planmagnetCustomerSatisfactionResponses;\n const items = list?.items ?? [];\n const total = list?.totalCount ?? 0;\n const firstIndex = (cursors.length - 1) * PAGE_SIZE;\n const writes = access.canUpdate || access.canDelete;\n\n if (loading && !list) {\n return (\n <div className=\"flex items-center gap-2 py-8 text-sm text-muted-foreground\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n {tr('planmagnet.satisfaction.list.loading', 'Loading responses…')}\n </div>\n );\n }\n if (error && !list) {\n return (\n <ErrorLine\n message={\n isForbidden(error)\n ? tr(\n 'planmagnet.satisfaction.errors.readForbidden',\n 'Your role cannot see customer satisfaction in this workspace.'\n )\n : tr(\n 'planmagnet.satisfaction.list.error',\n 'The responses could not be loaded: {{message}}',\n {\n message: error.message,\n }\n )\n }\n />\n );\n }\n\n return (\n <div className=\"space-y-3\" data-testid=\"satisfaction-responses\">\n {error && (\n <ErrorLine\n message={tr(\n 'planmagnet.satisfaction.list.refreshError',\n 'The list could not be refreshed: {{message}}',\n { message: error.message }\n )}\n />\n )}\n {items.length === 0 ? (\n <p\n className=\"py-8 text-center text-sm text-muted-foreground\"\n data-testid=\"satisfaction-responses-empty\"\n >\n {cursors.length > 1\n ? tr(\n 'planmagnet.satisfaction.list.emptyPage',\n 'This page has no responses left — go back to the previous page.'\n )\n : filtered\n ? tr(\n 'planmagnet.satisfaction.list.emptyFiltered',\n 'No response matches these filters in this window.'\n )\n : tr('planmagnet.satisfaction.list.empty', 'No responses in this window yet.')}\n </p>\n ) : (\n <div className=\"overflow-x-auto\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>{tr('planmagnet.satisfaction.list.answered', 'Answered')}</TableHead>\n <TableHead>{tr('planmagnet.satisfaction.list.score', 'Score')}</TableHead>\n <TableHead>{tr('planmagnet.satisfaction.list.respondent', 'Respondent')}</TableHead>\n <TableHead>{tr('planmagnet.satisfaction.list.about', 'About')}</TableHead>\n <TableHead>{tr('planmagnet.satisfaction.list.comment', 'Comment')}</TableHead>\n <TableHead>{tr('planmagnet.satisfaction.list.source', 'Source')}</TableHead>\n {writes && (\n <TableHead className=\"text-right\">\n <span className=\"sr-only\">\n {tr('planmagnet.satisfaction.list.actions', 'Actions')}\n </span>\n </TableHead>\n )}\n </TableRow>\n </TableHeader>\n <TableBody>\n {items.map((response) => {\n const category = scoreCategory(response.metricType, response.score);\n return (\n <TableRow key={response.id} data-testid=\"satisfaction-response-row\">\n <TableCell className=\"whitespace-nowrap text-sm\">\n {formatUtcDay(response.respondedAt)}\n </TableCell>\n <TableCell className=\"whitespace-nowrap\">\n <div className=\"flex items-center gap-2\">\n <Badge variant=\"outline\" className=\"text-xs\">\n {response.metricType}\n </Badge>\n <span className=\"font-medium tabular-nums\">\n {tr('planmagnet.satisfaction.list.scoreOf', '{{score}} / {{max}}', {\n score: response.score,\n max: SCORE_SCALE[response.metricType].max,\n })}\n </span>\n <span\n className={`rounded px-1.5 py-0.5 text-xs ${CATEGORY_CLASS[category]}`}\n >\n {categoryLabel(tr, category)}\n </span>\n </div>\n </TableCell>\n <TableCell>\n <Respondent response={response} />\n </TableCell>\n <TableCell>\n <About response={response} />\n </TableCell>\n <TableCell>\n {response.comment ? (\n <Comment text={response.comment} />\n ) : (\n <span className=\"text-sm text-muted-foreground\">—</span>\n )}\n </TableCell>\n <TableCell className=\"text-xs text-muted-foreground\">\n <p>\n {response.source === 'IMPORT'\n ? tr('planmagnet.satisfaction.list.sourceImport', 'Imported')\n : tr('planmagnet.satisfaction.list.sourceManual', 'Entered')}\n </p>\n {response.externalId && (\n <p className=\"max-w-40 truncate font-mono\" title={response.externalId}>\n {response.externalId}\n </p>\n )}\n </TableCell>\n {writes && (\n <TableCell className=\"text-right\">\n <div className=\"flex justify-end gap-1\">\n {access.canUpdate && (\n <SatisfactionResponseDialog\n response={response}\n links={links}\n onSaved={() =>\n onNotice(\n tr(\n 'planmagnet.satisfaction.notice.updated',\n 'The response was saved.'\n ),\n 'status'\n )\n }\n onStale={onStale}\n />\n )}\n {access.canDelete && (\n <DeleteResponse\n response={response}\n onDeleted={() =>\n onNotice(\n tr(\n 'planmagnet.satisfaction.notice.deleted',\n 'The response was deleted. It no longer counts in any figure.'\n ),\n 'status'\n )\n }\n onFailed={(message) => onNotice(message, 'alert')}\n onStale={onStale}\n />\n )}\n </div>\n </TableCell>\n )}\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n </div>\n )}\n\n {(total > 0 || cursors.length > 1) && (\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <p className=\"text-xs text-muted-foreground\" data-testid=\"satisfaction-responses-range\">\n {items.length > 0\n ? tr('planmagnet.satisfaction.list.range', 'Showing {{from}}–{{to}} of {{total}}', {\n from: firstIndex + 1,\n to: firstIndex + items.length,\n total,\n })\n : ''}\n </p>\n <div className=\"flex gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={cursors.length === 1 || loading}\n onClick={() => setCursors((c) => c.slice(0, -1))}\n data-testid=\"satisfaction-responses-previous\"\n >\n <ChevronLeft className=\"mr-1 h-4 w-4\" />\n {tr('planmagnet.satisfaction.list.previous', 'Previous')}\n </Button>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={!list?.pageInfo.hasNextPage || !list.pageInfo.endCursor || loading}\n onClick={() => {\n const next = list?.pageInfo.endCursor;\n if (next) setCursors((c) => [...c, next]);\n }}\n data-testid=\"satisfaction-responses-next\"\n >\n {tr('planmagnet.satisfaction.list.next', 'Next')}\n <ChevronRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import { SatisfactionScope } from './satisfaction';
2
+
3
+ export declare function SatisfactionSummaryCard({ scope }: {
4
+ scope: SatisfactionScope;
5
+ }): import("react/jsx-runtime").JSX.Element;
6
+ //# sourceMappingURL=SatisfactionSummaryCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SatisfactionSummaryCard.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/satisfaction/SatisfactionSummaryCard.tsx"],"names":[],"mappings":"AAOA,OAAO,EAGL,KAAK,iBAAiB,EAEvB,MAAM,gBAAgB,CAAC;AAcxB,wBAAgB,uBAAuB,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,iBAAiB,CAAA;CAAE,2CAkE9E"}
@@ -0,0 +1,56 @@
1
+ import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/PlanMagnetProvider.js";
2
+ import { useTr as n } from "../../shared/hooks/useTr.js";
3
+ import { isForbidden as r } from "../utils/graphqlErrors.js";
4
+ import { DEFAULT_WINDOW_PRESET as i, satisfactionPageLink as a } from "./satisfaction.js";
5
+ import { CompactSatisfactionFigures as o } from "./SatisfactionFigures.js";
6
+ import { CUSTOMER_SATISFACTION_SUMMARY as s } from "./satisfactionOperations.js";
7
+ import { ArrowRight as c, Loader2 as l, Smile as u } from "lucide-react";
8
+ import { useQuery as d } from "@apollo/client/react";
9
+ import { jsx as f, jsxs as p } from "react/jsx-runtime";
10
+ import { Button as m, Card as h, CardContent as g, CardHeader as _, CardTitle as v } from "@burdenoff/fe-libs/ui";
11
+ //#region src/planmagnet/satisfaction/SatisfactionSummaryCard.tsx
12
+ var y = { preset: i };
13
+ function b({ scope: i }) {
14
+ let b = n(), x = i.releaseId ? b("planmagnet.satisfaction.card.releaseScope", "Responses about this release.") : i.productId ? b("planmagnet.satisfaction.card.productScope", "Responses about this product, and about its releases that name no product of their own.") : b("planmagnet.satisfaction.card.projectScope", "Responses about this project."), { workspaceId: S } = e(), C = t(), { data: w, loading: T, error: E } = d(s, {
15
+ variables: {
16
+ workspaceId: S ?? "",
17
+ filter: {
18
+ ...i,
19
+ window: y
20
+ }
21
+ },
22
+ skip: !S
23
+ }), D = w?.planmagnetCustomerSatisfaction;
24
+ return /* @__PURE__ */ p(h, {
25
+ "data-testid": "satisfaction-summary-card",
26
+ children: [/* @__PURE__ */ p(_, {
27
+ className: "flex flex-row items-start justify-between gap-2 space-y-0",
28
+ children: [/* @__PURE__ */ p("div", { children: [/* @__PURE__ */ p(v, {
29
+ className: "flex items-center gap-2",
30
+ children: [/* @__PURE__ */ f(u, {
31
+ className: "h-4 w-4",
32
+ "aria-hidden": !0
33
+ }), b("planmagnet.satisfaction.card.title", "Customer satisfaction")]
34
+ }), /* @__PURE__ */ f("p", {
35
+ className: "mt-1 text-xs text-muted-foreground",
36
+ children: x
37
+ })] }), /* @__PURE__ */ p(m, {
38
+ size: "sm",
39
+ variant: "ghost",
40
+ onClick: () => C(a(i)),
41
+ "data-testid": "satisfaction-summary-open",
42
+ children: [b("planmagnet.satisfaction.card.open", "Responses"), /* @__PURE__ */ f(c, { className: "ml-1 h-4 w-4" })]
43
+ })]
44
+ }), /* @__PURE__ */ f(g, { children: T && !D ? /* @__PURE__ */ p("p", {
45
+ className: "flex items-center gap-2 text-sm text-muted-foreground",
46
+ children: [/* @__PURE__ */ f(l, { className: "h-4 w-4 animate-spin" }), b("planmagnet.satisfaction.loading", "Loading…")]
47
+ }) : E && !D ? /* @__PURE__ */ f("p", {
48
+ className: "text-sm text-status-error-text",
49
+ children: r(E) ? b("planmagnet.satisfaction.errors.readForbidden", "Your role cannot see customer satisfaction in this workspace.") : b("planmagnet.satisfaction.errors.load", "Customer satisfaction could not be loaded: {{message}}", { message: E.message })
50
+ }) : D ? /* @__PURE__ */ f(o, { summary: D }) : null })]
51
+ });
52
+ }
53
+ //#endregion
54
+ export { b as SatisfactionSummaryCard };
55
+
56
+ //# sourceMappingURL=SatisfactionSummaryCard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SatisfactionSummaryCard.js","names":[],"sources":["../../../src/planmagnet/satisfaction/SatisfactionSummaryCard.tsx"],"sourcesContent":["import { useQuery } from '@apollo/client/react';\nimport { Button, Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { ArrowRight, Loader2, Smile } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { isForbidden } from '../utils/graphqlErrors';\nimport { CompactSatisfactionFigures } from './SatisfactionFigures';\nimport {\n DEFAULT_WINDOW_PRESET,\n satisfactionPageLink,\n type SatisfactionScope,\n type SatisfactionWindowInput,\n} from './satisfaction';\nimport {\n CUSTOMER_SATISFACTION_SUMMARY,\n type CustomerSatisfactionSummaryData,\n} from './satisfactionOperations';\n\n/**\n * CSAT and NPS for one scope, as a card on another page — the product page (responses about the product, and about\n * its releases that name no product of their own) and the release page (responses about that release). Its own query:\n * a role that cannot read customer satisfaction, or any failure here, leaves the rest of the page alone.\n */\n\nconst DEFAULT_WINDOW: SatisfactionWindowInput = { preset: DEFAULT_WINDOW_PRESET };\n\nexport function SatisfactionSummaryCard({ scope }: { scope: SatisfactionScope }) {\n const tr = useTr();\n const description = scope.releaseId\n ? tr('planmagnet.satisfaction.card.releaseScope', 'Responses about this release.')\n : scope.productId\n ? tr(\n 'planmagnet.satisfaction.card.productScope',\n 'Responses about this product, and about its releases that name no product of their own.'\n )\n : tr('planmagnet.satisfaction.card.projectScope', 'Responses about this project.');\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const { data, loading, error } = useQuery<CustomerSatisfactionSummaryData>(\n CUSTOMER_SATISFACTION_SUMMARY,\n {\n variables: { workspaceId: workspaceId ?? '', filter: { ...scope, window: DEFAULT_WINDOW } },\n skip: !workspaceId,\n }\n );\n const summary = data?.planmagnetCustomerSatisfaction;\n\n return (\n <Card data-testid=\"satisfaction-summary-card\">\n <CardHeader className=\"flex flex-row items-start justify-between gap-2 space-y-0\">\n <div>\n <CardTitle className=\"flex items-center gap-2\">\n <Smile className=\"h-4 w-4\" aria-hidden={true} />\n {tr('planmagnet.satisfaction.card.title', 'Customer satisfaction')}\n </CardTitle>\n <p className=\"mt-1 text-xs text-muted-foreground\">{description}</p>\n </div>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => navigate(satisfactionPageLink(scope))}\n data-testid=\"satisfaction-summary-open\"\n >\n {tr('planmagnet.satisfaction.card.open', 'Responses')}\n <ArrowRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </CardHeader>\n <CardContent>\n {loading && !summary ? (\n <p className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n {tr('planmagnet.satisfaction.loading', 'Loading…')}\n </p>\n ) : error && !summary ? (\n <p className=\"text-sm text-status-error-text\">\n {isForbidden(error)\n ? tr(\n 'planmagnet.satisfaction.errors.readForbidden',\n 'Your role cannot see customer satisfaction in this workspace.'\n )\n : tr(\n 'planmagnet.satisfaction.errors.load',\n 'Customer satisfaction could not be loaded: {{message}}',\n { message: error.message }\n )}\n </p>\n ) : summary ? (\n <CompactSatisfactionFigures summary={summary} />\n ) : null}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":";;;;;;;;;;;AAwBA,IAAM,IAA0C,EAAE,QAAQ,GAAuB;AAEjF,SAAgB,EAAwB,EAAE,YAAuC;CAC/E,IAAM,IAAK,GAAO,EACZ,IAAc,EAAM,YACtB,EAAG,6CAA6C,gCAAgC,GAChF,EAAM,YACJ,EACE,6CACA,0FACD,GACD,EAAG,6CAA6C,gCAAgC,EAChF,EAAE,mBAAgB,GAAe,EACjC,IAAW,GAAuB,EAClC,EAAE,SAAM,YAAS,aAAU,EAC/B,GACA;EACE,WAAW;GAAE,aAAa,KAAe;GAAI,QAAQ;IAAE,GAAG;IAAO,QAAQ;IAAgB;GAAE;EAC3F,MAAM,CAAC;EACR,CACF,EACK,IAAU,GAAM;AAEtB,QACE,kBAAC,GAAD;EAAM,eAAY;YAAlB,CACE,kBAAC,GAAD;GAAY,WAAU;aAAtB,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,GAAD;IAAW,WAAU;cAArB,CACE,kBAAC,GAAD;KAAO,WAAU;KAAU,eAAa;KAAQ,CAAA,EAC/C,EAAG,sCAAsC,wBAAwB,CACxD;OACZ,kBAAC,KAAD;IAAG,WAAU;cAAsC;IAAgB,CAAA,CAC/D,EAAA,CAAA,EACN,kBAAC,GAAD;IACE,MAAK;IACL,SAAQ;IACR,eAAe,EAAS,EAAqB,EAAM,CAAC;IACpD,eAAY;cAJd,CAMG,EAAG,qCAAqC,YAAY,EACrD,kBAAC,GAAD,EAAY,WAAU,gBAAiB,CAAA,CAChC;MACE;MACb,kBAAC,GAAD,EAAA,UACG,KAAW,CAAC,IACX,kBAAC,KAAD;GAAG,WAAU;aAAb,CACE,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,EAC3C,EAAG,mCAAmC,WAAW,CAChD;OACF,KAAS,CAAC,IACZ,kBAAC,KAAD;GAAG,WAAU;aACV,EAAY,EAAM,GACf,EACE,gDACA,gEACD,GACD,EACE,uCACA,0DACA,EAAE,SAAS,EAAM,SAAS,CAC3B;GACH,CAAA,GACF,IACF,kBAAC,GAAD,EAAqC,YAAW,CAAA,GAC9C,MACQ,CAAA,CACT"}
@@ -0,0 +1,16 @@
1
+ import { WindowChoice } from './satisfaction';
2
+ import { SatisfactionWindowEcho } from './satisfactionOperations';
3
+
4
+ /**
5
+ * The window a satisfaction figure covers: the trailing 30 / 90 / 180 / 365 days (90 by default), or custom days.
6
+ * Custom days are whole UTC days, both included — the same days the service counts. Switching to custom starts from
7
+ * the days the figures currently cover, so nothing jumps.
8
+ */
9
+ export declare function SatisfactionWindowPicker({ value, onChange, current, idPrefix, }: {
10
+ value: WindowChoice;
11
+ onChange: (next: WindowChoice) => void;
12
+ /** The window the figures on screen cover, as the service resolved it. */
13
+ current: SatisfactionWindowEcho | null;
14
+ idPrefix?: string;
15
+ }): import("react/jsx-runtime").JSX.Element;
16
+ //# sourceMappingURL=SatisfactionWindowPicker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SatisfactionWindowPicker.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/satisfaction/SatisfactionWindowPicker.tsx"],"names":[],"mappings":"AAGA,OAAO,EAKL,KAAK,YAAY,EAElB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAEvE;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,KAAK,EACL,QAAQ,EACR,OAAO,EACP,QAAgC,GACjC,EAAE;IACD,KAAK,EAAE,YAAY,CAAC;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IACvC,0EAA0E;IAC1E,OAAO,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,2CAyFA"}
@@ -0,0 +1,102 @@
1
+ import { useTr as e } from "../../shared/hooks/useTr.js";
2
+ import { WINDOW_PRESETS as t, isCalendarDay as n, presetDays as r, utcDay as i } from "./satisfaction.js";
3
+ import { windowPresetLabel as a } from "./SatisfactionFigures.js";
4
+ import { jsx as o, jsxs as s } from "react/jsx-runtime";
5
+ import { Input as c } from "@burdenoff/fe-libs/ui";
6
+ //#region src/planmagnet/satisfaction/SatisfactionWindowPicker.tsx
7
+ function l({ value: l, onChange: u, current: d, idPrefix: f = "satisfaction-window" }) {
8
+ let p = e(), m = l.preset === "CUSTOM", h = m && (!n(l.from) || !n(l.to)), g = m && !h && l.from > l.to, _ = (e) => {
9
+ if (e !== "CUSTOM") {
10
+ u({
11
+ preset: e,
12
+ from: l.from,
13
+ to: l.to
14
+ });
15
+ return;
16
+ }
17
+ if (l.preset === "CUSTOM") return;
18
+ let t = d ? i(d.from) : null, n = d ? i(d.to) : null;
19
+ u({
20
+ preset: "CUSTOM",
21
+ ...t && n ? {
22
+ from: t,
23
+ to: n
24
+ } : r("LAST_90_DAYS", /* @__PURE__ */ new Date())
25
+ });
26
+ };
27
+ return /* @__PURE__ */ s("div", {
28
+ className: "flex flex-wrap items-center gap-2",
29
+ "data-testid": "satisfaction-window",
30
+ children: [
31
+ /* @__PURE__ */ o("div", {
32
+ className: "flex flex-wrap items-center gap-1 rounded-md border p-0.5",
33
+ role: "group",
34
+ "aria-label": p("planmagnet.satisfaction.window.label", "Time window"),
35
+ children: t.map((e) => /* @__PURE__ */ o("button", {
36
+ type: "button",
37
+ "aria-pressed": l.preset === e,
38
+ onClick: () => _(e),
39
+ "data-testid": `satisfaction-window-${e}`,
40
+ className: `rounded px-2.5 py-1 text-xs ${l.preset === e ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted"}`,
41
+ children: a(p, e)
42
+ }, e))
43
+ }),
44
+ m && /* @__PURE__ */ s("div", {
45
+ className: "flex items-center gap-1.5",
46
+ children: [
47
+ /* @__PURE__ */ o("label", {
48
+ htmlFor: `${f}-from`,
49
+ className: "sr-only",
50
+ children: p("planmagnet.satisfaction.window.from", "From")
51
+ }),
52
+ /* @__PURE__ */ o(c, {
53
+ id: `${f}-from`,
54
+ type: "date",
55
+ className: "h-8 w-40",
56
+ value: l.from,
57
+ max: l.to || void 0,
58
+ onChange: (e) => u({
59
+ ...l,
60
+ from: e.target.value
61
+ }),
62
+ "data-testid": "satisfaction-window-from"
63
+ }),
64
+ /* @__PURE__ */ o("span", {
65
+ className: "text-sm text-muted-foreground",
66
+ children: "–"
67
+ }),
68
+ /* @__PURE__ */ o("label", {
69
+ htmlFor: `${f}-to`,
70
+ className: "sr-only",
71
+ children: p("planmagnet.satisfaction.window.to", "To")
72
+ }),
73
+ /* @__PURE__ */ o(c, {
74
+ id: `${f}-to`,
75
+ type: "date",
76
+ className: "h-8 w-40",
77
+ value: l.to,
78
+ min: l.from || void 0,
79
+ onChange: (e) => u({
80
+ ...l,
81
+ to: e.target.value
82
+ }),
83
+ "data-testid": "satisfaction-window-to"
84
+ }),
85
+ /* @__PURE__ */ o("span", {
86
+ className: "text-xs text-muted-foreground",
87
+ children: p("planmagnet.satisfaction.window.utcNote", "UTC days")
88
+ })
89
+ ]
90
+ }),
91
+ (g || h) && /* @__PURE__ */ o("p", {
92
+ role: "alert",
93
+ className: "text-xs text-status-error-text",
94
+ children: g ? p("planmagnet.satisfaction.window.reversed", "“From” must be on or before “To”. The figures still show the last valid window.") : p("planmagnet.satisfaction.window.incomplete", "Pick both days. The figures still show the last valid window.")
95
+ })
96
+ ]
97
+ });
98
+ }
99
+ //#endregion
100
+ export { l as SatisfactionWindowPicker };
101
+
102
+ //# sourceMappingURL=SatisfactionWindowPicker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SatisfactionWindowPicker.js","names":[],"sources":["../../../src/planmagnet/satisfaction/SatisfactionWindowPicker.tsx"],"sourcesContent":["import { Input } from '@burdenoff/fe-libs/ui';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { windowPresetLabel } from './SatisfactionFigures';\nimport {\n WINDOW_PRESETS,\n isCalendarDay,\n presetDays,\n utcDay,\n type WindowChoice,\n type WindowPreset,\n} from './satisfaction';\nimport type { SatisfactionWindowEcho } from './satisfactionOperations';\n\n/**\n * The window a satisfaction figure covers: the trailing 30 / 90 / 180 / 365 days (90 by default), or custom days.\n * Custom days are whole UTC days, both included — the same days the service counts. Switching to custom starts from\n * the days the figures currently cover, so nothing jumps.\n */\nexport function SatisfactionWindowPicker({\n value,\n onChange,\n current,\n idPrefix = 'satisfaction-window',\n}: {\n value: WindowChoice;\n onChange: (next: WindowChoice) => void;\n /** The window the figures on screen cover, as the service resolved it. */\n current: SatisfactionWindowEcho | null;\n idPrefix?: string;\n}) {\n const tr = useTr();\n const custom = value.preset === 'CUSTOM';\n const incomplete = custom && (!isCalendarDay(value.from) || !isCalendarDay(value.to));\n const reversed = custom && !incomplete && value.from > value.to;\n\n const choose = (preset: WindowPreset) => {\n if (preset !== 'CUSTOM') {\n onChange({ preset, from: value.from, to: value.to });\n return;\n }\n if (value.preset === 'CUSTOM') return;\n const from = current ? utcDay(current.from) : null;\n const to = current ? utcDay(current.to) : null;\n const seed = from && to ? { from, to } : presetDays('LAST_90_DAYS', new Date());\n onChange({ preset: 'CUSTOM', ...seed });\n };\n\n return (\n <div className=\"flex flex-wrap items-center gap-2\" data-testid=\"satisfaction-window\">\n <div\n className=\"flex flex-wrap items-center gap-1 rounded-md border p-0.5\"\n role=\"group\"\n aria-label={tr('planmagnet.satisfaction.window.label', 'Time window')}\n >\n {WINDOW_PRESETS.map((preset) => (\n <button\n key={preset}\n type=\"button\"\n aria-pressed={value.preset === preset}\n onClick={() => choose(preset)}\n data-testid={`satisfaction-window-${preset}`}\n className={`rounded px-2.5 py-1 text-xs ${\n value.preset === preset\n ? 'bg-primary text-primary-foreground'\n : 'text-muted-foreground hover:bg-muted'\n }`}\n >\n {windowPresetLabel(tr, preset)}\n </button>\n ))}\n </div>\n {custom && (\n <div className=\"flex items-center gap-1.5\">\n <label htmlFor={`${idPrefix}-from`} className=\"sr-only\">\n {tr('planmagnet.satisfaction.window.from', 'From')}\n </label>\n <Input\n id={`${idPrefix}-from`}\n type=\"date\"\n className=\"h-8 w-40\"\n value={value.from}\n max={value.to || undefined}\n onChange={(e) => onChange({ ...value, from: e.target.value })}\n data-testid=\"satisfaction-window-from\"\n />\n <span className=\"text-sm text-muted-foreground\">–</span>\n <label htmlFor={`${idPrefix}-to`} className=\"sr-only\">\n {tr('planmagnet.satisfaction.window.to', 'To')}\n </label>\n <Input\n id={`${idPrefix}-to`}\n type=\"date\"\n className=\"h-8 w-40\"\n value={value.to}\n min={value.from || undefined}\n onChange={(e) => onChange({ ...value, to: e.target.value })}\n data-testid=\"satisfaction-window-to\"\n />\n <span className=\"text-xs text-muted-foreground\">\n {tr('planmagnet.satisfaction.window.utcNote', 'UTC days')}\n </span>\n </div>\n )}\n {(reversed || incomplete) && (\n <p role=\"alert\" className=\"text-xs text-status-error-text\">\n {reversed\n ? tr(\n 'planmagnet.satisfaction.window.reversed',\n '“From” must be on or before “To”. The figures still show the last valid window.'\n )\n : tr(\n 'planmagnet.satisfaction.window.incomplete',\n 'Pick both days. The figures still show the last valid window.'\n )}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;AAkBA,SAAgB,EAAyB,EACvC,UACA,aACA,YACA,cAAW,yBAOV;CACD,IAAM,IAAK,GAAO,EACZ,IAAS,EAAM,WAAW,UAC1B,IAAa,MAAW,CAAC,EAAc,EAAM,KAAK,IAAI,CAAC,EAAc,EAAM,GAAG,GAC9E,IAAW,KAAU,CAAC,KAAc,EAAM,OAAO,EAAM,IAEvD,KAAU,MAAyB;AACvC,MAAI,MAAW,UAAU;AACvB,KAAS;IAAE;IAAQ,MAAM,EAAM;IAAM,IAAI,EAAM;IAAI,CAAC;AACpD;;AAEF,MAAI,EAAM,WAAW,SAAU;EAC/B,IAAM,IAAO,IAAU,EAAO,EAAQ,KAAK,GAAG,MACxC,IAAK,IAAU,EAAO,EAAQ,GAAG,GAAG;AAE1C,IAAS;GAAE,QAAQ;GAAU,GADhB,KAAQ,IAAK;IAAE;IAAM;IAAI,GAAG,EAAW,gCAAgB,IAAI,MAAM,CAAC;GACzC,CAAC;;AAGzC,QACE,kBAAC,OAAD;EAAK,WAAU;EAAoC,eAAY;YAA/D;GACE,kBAAC,OAAD;IACE,WAAU;IACV,MAAK;IACL,cAAY,EAAG,wCAAwC,cAAc;cAEpE,EAAe,KAAK,MACnB,kBAAC,UAAD;KAEE,MAAK;KACL,gBAAc,EAAM,WAAW;KAC/B,eAAe,EAAO,EAAO;KAC7B,eAAa,uBAAuB;KACpC,WAAW,+BACT,EAAM,WAAW,IACb,uCACA;eAGL,EAAkB,GAAI,EAAO;KACvB,EAZF,EAYE,CACT;IACE,CAAA;GACL,KACC,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,SAAD;MAAO,SAAS,GAAG,EAAS;MAAQ,WAAU;gBAC3C,EAAG,uCAAuC,OAAO;MAC5C,CAAA;KACR,kBAAC,GAAD;MACE,IAAI,GAAG,EAAS;MAChB,MAAK;MACL,WAAU;MACV,OAAO,EAAM;MACb,KAAK,EAAM,MAAM,KAAA;MACjB,WAAW,MAAM,EAAS;OAAE,GAAG;OAAO,MAAM,EAAE,OAAO;OAAO,CAAC;MAC7D,eAAY;MACZ,CAAA;KACF,kBAAC,QAAD;MAAM,WAAU;gBAAgC;MAAQ,CAAA;KACxD,kBAAC,SAAD;MAAO,SAAS,GAAG,EAAS;MAAM,WAAU;gBACzC,EAAG,qCAAqC,KAAK;MACxC,CAAA;KACR,kBAAC,GAAD;MACE,IAAI,GAAG,EAAS;MAChB,MAAK;MACL,WAAU;MACV,OAAO,EAAM;MACb,KAAK,EAAM,QAAQ,KAAA;MACnB,WAAW,MAAM,EAAS;OAAE,GAAG;OAAO,IAAI,EAAE,OAAO;OAAO,CAAC;MAC3D,eAAY;MACZ,CAAA;KACF,kBAAC,QAAD;MAAM,WAAU;gBACb,EAAG,0CAA0C,WAAW;MACpD,CAAA;KACH;;IAEN,KAAY,MACZ,kBAAC,KAAD;IAAG,MAAK;IAAQ,WAAU;cACvB,IACG,EACE,2CACA,kFACD,GACD,EACE,6CACA,gEACD;IACH,CAAA;GAEF"}