@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,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANR-21 customer satisfaction — the pure rules and formatting behind the Customer satisfaction page, its CSV import
|
|
3
|
+
* and the CSAT/NPS cards. No React here, so every rule is unit-tested (tests/satisfaction.test.ts).
|
|
4
|
+
*
|
|
5
|
+
* The service is the gate for every rule below (wspace-planmagnet-svc satisfaction-input.ts and
|
|
6
|
+
* satisfaction-scoring.ts, main fdb868c). These copies only let a form or the import preview say what is wrong before
|
|
7
|
+
* anything is sent, so each one is at most as strict as the service — never stricter.
|
|
8
|
+
*
|
|
9
|
+
* Owner decisions (2026-10-02): CSAT is the share of 4s and 5s on a 1-5 scale; NPS is % promoters (9-10) minus %
|
|
10
|
+
* detractors (0-6) on 0-10; below 5 responses a metric reads "Not enough responses", never 0 or a made-up score; every
|
|
11
|
+
* figure shows its response count and its window; the default window is the trailing 90 days. A feature request's
|
|
12
|
+
* rating is not customer satisfaction and is never shown as one.
|
|
13
|
+
*/
|
|
14
|
+
export type SatisfactionMetricType = 'CSAT' | 'NPS';
|
|
15
|
+
export declare const METRIC_TYPES: readonly SatisfactionMetricType[];
|
|
16
|
+
/** The inclusive answer scale of each metric type. */
|
|
17
|
+
export declare const SCORE_SCALE: Readonly<Record<SatisfactionMetricType, {
|
|
18
|
+
min: number;
|
|
19
|
+
max: number;
|
|
20
|
+
}>>;
|
|
21
|
+
export declare function isMetricType(value: unknown): value is SatisfactionMetricType;
|
|
22
|
+
/** Every answer on a metric's scale, lowest first. */
|
|
23
|
+
export declare function scaleValues(metricType: SatisfactionMetricType): number[];
|
|
24
|
+
/** True when `score` is a whole number on the metric's own scale (CSAT 1-5, NPS 0-10). */
|
|
25
|
+
export declare function isOnScale(metricType: SatisfactionMetricType, score: number): boolean;
|
|
26
|
+
export type ScoreCategory = 'satisfied' | 'notSatisfied' | 'promoter' | 'passive' | 'detractor';
|
|
27
|
+
/** How one answer counts: a CSAT 4-5 is satisfied; an NPS 9-10 promotes, 7-8 is passive, 0-6 detracts. */
|
|
28
|
+
export declare function scoreCategory(metricType: SatisfactionMetricType, score: number): ScoreCategory;
|
|
29
|
+
export declare const COMMENT_MAX = 5000;
|
|
30
|
+
export declare const RESPONDENT_NAME_MAX = 200;
|
|
31
|
+
/** RFC 5321's longest deliverable address. */
|
|
32
|
+
export declare const RESPONDENT_EMAIL_MAX = 254;
|
|
33
|
+
export declare const EXTERNAL_ID_MAX = 200;
|
|
34
|
+
/** One import call takes 1 to this many rows. */
|
|
35
|
+
export declare const MAX_IMPORT_ROWS = 500;
|
|
36
|
+
/** The earliest respondedAt the service accepts (it catches the epoch from an empty cell, a two-digit year…). */
|
|
37
|
+
export declare const RESPONDED_AT_EARLIEST = "2000-01-01";
|
|
38
|
+
/**
|
|
39
|
+
* The service's email typo gate (satisfaction-input.ts `looksLikeEmail`), copied exactly: one `@` with something on
|
|
40
|
+
* both sides, a dot inside the domain, no whitespace. Not deliverability.
|
|
41
|
+
*/
|
|
42
|
+
export declare function looksLikeEmail(email: string): boolean;
|
|
43
|
+
/** True for a real calendar day written YYYY-MM-DD (2026-02-30 is not one). */
|
|
44
|
+
export declare function isCalendarDay(text: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The instant a respondedAt text stands for, read the way the DateTime scalar reads it: a calendar day (YYYY-MM-DD) is
|
|
47
|
+
* that day's UTC midnight; a date-time must carry its zone (Z or ±hh:mm). Null for anything the scalar refuses.
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseRespondedAt(text: string): Date | null;
|
|
50
|
+
export type RespondedAtProblem = 'required' | 'format' | 'future' | 'tooOld';
|
|
51
|
+
/** Why the service would refuse this respondedAt, or null when it would take it. */
|
|
52
|
+
export declare function respondedAtProblem(text: string, now: Date): RespondedAtProblem | null;
|
|
53
|
+
/** The UTC calendar day (YYYY-MM-DD) of an ISO instant — the day every window counts it under. */
|
|
54
|
+
export declare function utcDay(iso: string): string | null;
|
|
55
|
+
/** Today in the viewer's own calendar (YYYY-MM-DD) — what a date input means by "today". */
|
|
56
|
+
export declare function localDay(now?: Date): string;
|
|
57
|
+
/**
|
|
58
|
+
* An instant shown as its UTC calendar day ("2 Oct 2026" in the viewer's locale). respondedAt and the window bounds
|
|
59
|
+
* are counted by UTC day — a date entered as 2026-10-02 is stored as that day's UTC midnight — so the day shown is the
|
|
60
|
+
* day the figures count it under. Formatting in the viewer's zone would show a UTC-midnight date as the day before
|
|
61
|
+
* anywhere west of Greenwich.
|
|
62
|
+
*/
|
|
63
|
+
export declare function formatUtcDay(iso: string, locale?: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* The releases a response about `productId` may name: that product's own and the ones with no product — the service
|
|
66
|
+
* refuses a release of another product. Every release when no product is chosen.
|
|
67
|
+
*/
|
|
68
|
+
export declare function releasesForProduct<T extends {
|
|
69
|
+
productId: string | null;
|
|
70
|
+
}>(releases: readonly T[], productId: string): T[];
|
|
71
|
+
/** "2.4.0 — Spring launch", or just the version when the name repeats it. */
|
|
72
|
+
export declare function releaseLabel(release: {
|
|
73
|
+
name: string;
|
|
74
|
+
version: string;
|
|
75
|
+
}): string;
|
|
76
|
+
export type WindowPreset = 'LAST_30_DAYS' | 'LAST_90_DAYS' | 'LAST_180_DAYS' | 'LAST_365_DAYS' | 'CUSTOM';
|
|
77
|
+
export declare const WINDOW_PRESETS: readonly WindowPreset[];
|
|
78
|
+
/** Owner decision 2026-10-02: a figure covers the trailing 90 days unless someone picks another window. */
|
|
79
|
+
export declare const DEFAULT_WINDOW_PRESET: WindowPreset;
|
|
80
|
+
export declare const PRESET_DAYS: Readonly<Record<Exclude<WindowPreset, 'CUSTOM'>, number>>;
|
|
81
|
+
export declare function isWindowPreset(value: unknown): value is WindowPreset;
|
|
82
|
+
/** The window a page asks for: a preset, or CUSTOM with its own two days (YYYY-MM-DD, both included). */
|
|
83
|
+
export interface WindowChoice {
|
|
84
|
+
preset: WindowPreset;
|
|
85
|
+
from: string;
|
|
86
|
+
to: string;
|
|
87
|
+
}
|
|
88
|
+
/** The `window` input of a satisfaction filter. */
|
|
89
|
+
export interface SatisfactionWindowInput {
|
|
90
|
+
preset: WindowPreset;
|
|
91
|
+
from?: string;
|
|
92
|
+
to?: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The `window` input for a choice. A preset sends only its name (the service refuses from/to with any preset but
|
|
96
|
+
* CUSTOM). CUSTOM sends its two days as calendar dates: the DateTime scalar reads a date as that day's UTC midnight and
|
|
97
|
+
* the service counts whole UTC days, so the figure covers exactly the days picked. Null while a custom pair is
|
|
98
|
+
* incomplete or reversed — the caller keeps its last valid window rather than send one the service refuses.
|
|
99
|
+
*/
|
|
100
|
+
export declare function windowInput(choice: WindowChoice): SatisfactionWindowInput | null;
|
|
101
|
+
/**
|
|
102
|
+
* The UTC days a preset covers today — the N days ending today, today included, exactly as the service resolves it.
|
|
103
|
+
* Used to seed the custom date inputs when someone switches from a preset to CUSTOM.
|
|
104
|
+
*/
|
|
105
|
+
export declare function presetDays(preset: Exclude<WindowPreset, 'CUSTOM'>, now: Date): {
|
|
106
|
+
from: string;
|
|
107
|
+
to: string;
|
|
108
|
+
};
|
|
109
|
+
/** What a satisfaction figure or list is about: the workspace (all empty), or one product / release / project. */
|
|
110
|
+
export interface SatisfactionScope {
|
|
111
|
+
productId?: string;
|
|
112
|
+
releaseId?: string;
|
|
113
|
+
projectId?: string;
|
|
114
|
+
}
|
|
115
|
+
/** The Customer satisfaction page's state, as its URL carries it. */
|
|
116
|
+
export interface SatisfactionPageState {
|
|
117
|
+
window: WindowChoice;
|
|
118
|
+
productId: string;
|
|
119
|
+
releaseId: string;
|
|
120
|
+
projectId: string;
|
|
121
|
+
/** '' = both metrics (the responses list only; the figures always show both). */
|
|
122
|
+
metricType: SatisfactionMetricType | '';
|
|
123
|
+
}
|
|
124
|
+
/** The Customer satisfaction page for a scope (and a window other than the default), as a navigate() path. */
|
|
125
|
+
export declare function satisfactionPageLink(scope: SatisfactionScope, window?: WindowChoice): string;
|
|
126
|
+
/**
|
|
127
|
+
* The page state a URL query asks for (`?window=&from=&to=&product=&release=&project=&metric=`). Anything unreadable
|
|
128
|
+
* falls back to the default — the trailing 90 days, every product, release and project, both metrics.
|
|
129
|
+
*/
|
|
130
|
+
export declare function pageStateFromSearch(search: string, now: Date): SatisfactionPageState;
|
|
131
|
+
/** CSAT as a whole percentage ("82%"). Null stays null: the caller says "Not enough responses", never "0%". */
|
|
132
|
+
export declare function formatCsat(score: number | null, locale?: string): string | null;
|
|
133
|
+
/** NPS as a signed whole number ("+34", "-12", "0"). Null stays null. */
|
|
134
|
+
export declare function formatNps(score: number | null, locale?: string): string | null;
|
|
135
|
+
/** `count` as a whole percentage of `total` — 0 when there is nothing to divide by. */
|
|
136
|
+
export declare function shareOf(count: number, total: number): number;
|
|
137
|
+
/** `extensions.code` of the first error that has one (BAD_USER_INPUT, NOT_FOUND, CONFLICT, FORBIDDEN…). */
|
|
138
|
+
export declare function errorCode(error: unknown): string | null;
|
|
139
|
+
/** The input the service named in `extensions.field` (score, respondedAt, productId…), if any. */
|
|
140
|
+
export declare function errorField(error: unknown): string | null;
|
|
141
|
+
/** One refused import row, as the service reports it in `extensions.rowErrors`. `row` is 1-based, in the order sent. */
|
|
142
|
+
export interface ImportRowError {
|
|
143
|
+
row: number;
|
|
144
|
+
field: string | null;
|
|
145
|
+
code: string;
|
|
146
|
+
/** The service's own sentence, already in the caller's language. */
|
|
147
|
+
message: string;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The refused rows of a failed import, sorted by row — or null when the refusal was not about rows (a role refusal,
|
|
151
|
+
* an empty or oversized batch, a network fault), in which case the caller shows the error's own message.
|
|
152
|
+
*/
|
|
153
|
+
export declare function readImportRowErrors(error: unknown): ImportRowError[] | null;
|
|
154
|
+
//# sourceMappingURL=satisfaction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"satisfaction.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/satisfaction/satisfaction.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { CombinedGraphQLErrors as e } from "../../node_modules/@apollo/client/errors/CombinedGraphQLErrors.js";
|
|
2
|
+
import { ServerError as t } from "../../node_modules/@apollo/client/errors/ServerError.js";
|
|
3
|
+
import { CUSTOMER_SATISFACTION_PATH as n } from "../product/routes.js";
|
|
4
|
+
//#region src/planmagnet/satisfaction/satisfaction.ts
|
|
5
|
+
var r = ["CSAT", "NPS"], i = {
|
|
6
|
+
CSAT: {
|
|
7
|
+
min: 1,
|
|
8
|
+
max: 5
|
|
9
|
+
},
|
|
10
|
+
NPS: {
|
|
11
|
+
min: 0,
|
|
12
|
+
max: 10
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
function a(e) {
|
|
16
|
+
return e === "CSAT" || e === "NPS";
|
|
17
|
+
}
|
|
18
|
+
function o(e) {
|
|
19
|
+
let { min: t, max: n } = i[e];
|
|
20
|
+
return Array.from({ length: n - t + 1 }, (e, n) => t + n);
|
|
21
|
+
}
|
|
22
|
+
function s(e, t) {
|
|
23
|
+
let { min: n, max: r } = i[e];
|
|
24
|
+
return Number.isInteger(t) && t >= n && t <= r;
|
|
25
|
+
}
|
|
26
|
+
function c(e, t) {
|
|
27
|
+
return e === "CSAT" ? t >= 4 ? "satisfied" : "notSatisfied" : t >= 9 ? "promoter" : t >= 7 ? "passive" : "detractor";
|
|
28
|
+
}
|
|
29
|
+
var l = 5e3, u = "2000-01-01";
|
|
30
|
+
function d(e) {
|
|
31
|
+
let t = e.indexOf("@");
|
|
32
|
+
if (t <= 0 || t !== e.lastIndexOf("@") || /\s/.test(e)) return !1;
|
|
33
|
+
let n = e.slice(t + 1);
|
|
34
|
+
return n.indexOf(".") > 0 && !n.endsWith(".");
|
|
35
|
+
}
|
|
36
|
+
var f = /^\d{4}-\d{2}-\d{2}$/, p = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/, m = /(?:Z|[+-]\d{2}:\d{2})$/, h = 3600 * 1e3, g = 24 * h, _ = 15 * h;
|
|
37
|
+
function v(e) {
|
|
38
|
+
if (!f.test(e)) return !1;
|
|
39
|
+
let t = /* @__PURE__ */ new Date(`${e}T00:00:00.000Z`);
|
|
40
|
+
return !Number.isNaN(t.getTime()) && t.toISOString().slice(0, 10) === e;
|
|
41
|
+
}
|
|
42
|
+
function y(e) {
|
|
43
|
+
let t = e.trim();
|
|
44
|
+
if (v(t)) return /* @__PURE__ */ new Date(`${t}T00:00:00.000Z`);
|
|
45
|
+
if (p.test(t) && m.test(t)) {
|
|
46
|
+
let e = new Date(t);
|
|
47
|
+
return Number.isNaN(e.getTime()) ? null : e;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function b(e, t) {
|
|
52
|
+
if (e.trim() === "") return "required";
|
|
53
|
+
let n = y(e);
|
|
54
|
+
return n ? n.getTime() > t.getTime() + _ ? "future" : n.getTime() < Date.parse("2000-01-01T00:00:00.000Z") ? "tooOld" : null : "format";
|
|
55
|
+
}
|
|
56
|
+
function x(e) {
|
|
57
|
+
let t = new Date(e);
|
|
58
|
+
return Number.isNaN(t.getTime()) ? null : t.toISOString().slice(0, 10);
|
|
59
|
+
}
|
|
60
|
+
function S(e = /* @__PURE__ */ new Date()) {
|
|
61
|
+
let t = String(e.getMonth() + 1).padStart(2, "0"), n = String(e.getDate()).padStart(2, "0");
|
|
62
|
+
return `${e.getFullYear()}-${t}-${n}`;
|
|
63
|
+
}
|
|
64
|
+
function C(e, t) {
|
|
65
|
+
let n = new Date(e);
|
|
66
|
+
return Number.isNaN(n.getTime()) ? "—" : n.toLocaleDateString(t, {
|
|
67
|
+
timeZone: "UTC",
|
|
68
|
+
day: "numeric",
|
|
69
|
+
month: "short",
|
|
70
|
+
year: "numeric"
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function w(e, t) {
|
|
74
|
+
return t ? e.filter((e) => !e.productId || e.productId === t) : [...e];
|
|
75
|
+
}
|
|
76
|
+
function T(e) {
|
|
77
|
+
return e.name && e.name !== e.version ? `${e.version} — ${e.name}` : e.version;
|
|
78
|
+
}
|
|
79
|
+
var E = [
|
|
80
|
+
"LAST_30_DAYS",
|
|
81
|
+
"LAST_90_DAYS",
|
|
82
|
+
"LAST_180_DAYS",
|
|
83
|
+
"LAST_365_DAYS",
|
|
84
|
+
"CUSTOM"
|
|
85
|
+
], D = "LAST_90_DAYS", O = {
|
|
86
|
+
LAST_30_DAYS: 30,
|
|
87
|
+
LAST_90_DAYS: 90,
|
|
88
|
+
LAST_180_DAYS: 180,
|
|
89
|
+
LAST_365_DAYS: 365
|
|
90
|
+
};
|
|
91
|
+
function k(e) {
|
|
92
|
+
return typeof e == "string" && E.includes(e);
|
|
93
|
+
}
|
|
94
|
+
function A(e) {
|
|
95
|
+
return e.preset === "CUSTOM" ? !v(e.from) || !v(e.to) || e.from > e.to ? null : {
|
|
96
|
+
preset: "CUSTOM",
|
|
97
|
+
from: e.from,
|
|
98
|
+
to: e.to
|
|
99
|
+
} : { preset: e.preset };
|
|
100
|
+
}
|
|
101
|
+
function j(e, t) {
|
|
102
|
+
let n = t.toISOString().slice(0, 10);
|
|
103
|
+
return {
|
|
104
|
+
from: (/* @__PURE__ */ new Date(t.getTime() - (O[e] - 1) * g)).toISOString().slice(0, 10),
|
|
105
|
+
to: n
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function M(e, t) {
|
|
109
|
+
let r = new URLSearchParams();
|
|
110
|
+
t && t.preset !== "LAST_90_DAYS" && (r.set("window", t.preset), t.preset === "CUSTOM" && (r.set("from", t.from), r.set("to", t.to))), e.productId && r.set("product", e.productId), e.releaseId && r.set("release", e.releaseId), e.projectId && r.set("project", e.projectId);
|
|
111
|
+
let i = r.toString();
|
|
112
|
+
return `/${n}${i ? `?${i}` : ""}`;
|
|
113
|
+
}
|
|
114
|
+
function N(e, t) {
|
|
115
|
+
let n = new URLSearchParams(e), r = n.get("window"), i = n.get("from") ?? "", o = n.get("to") ?? "", s;
|
|
116
|
+
if (r === "CUSTOM" && A({
|
|
117
|
+
preset: "CUSTOM",
|
|
118
|
+
from: i,
|
|
119
|
+
to: o
|
|
120
|
+
})) s = {
|
|
121
|
+
preset: "CUSTOM",
|
|
122
|
+
from: i,
|
|
123
|
+
to: o
|
|
124
|
+
};
|
|
125
|
+
else {
|
|
126
|
+
let e = k(r) && r !== "CUSTOM" ? r : "LAST_90_DAYS";
|
|
127
|
+
s = {
|
|
128
|
+
preset: e,
|
|
129
|
+
...j(e, t)
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
let c = n.get("metric");
|
|
133
|
+
return {
|
|
134
|
+
window: s,
|
|
135
|
+
productId: n.get("product") ?? "",
|
|
136
|
+
releaseId: n.get("release") ?? "",
|
|
137
|
+
projectId: n.get("project") ?? "",
|
|
138
|
+
metricType: a(c) ? c : ""
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function P(e, t) {
|
|
142
|
+
return e === null || !Number.isFinite(e) ? null : new Intl.NumberFormat(t, {
|
|
143
|
+
style: "percent",
|
|
144
|
+
maximumFractionDigits: 0
|
|
145
|
+
}).format(e / 100);
|
|
146
|
+
}
|
|
147
|
+
function F(e, t) {
|
|
148
|
+
if (e === null || !Number.isFinite(e)) return null;
|
|
149
|
+
let n = Math.round(e);
|
|
150
|
+
return new Intl.NumberFormat(t, {
|
|
151
|
+
signDisplay: "exceptZero",
|
|
152
|
+
maximumFractionDigits: 0
|
|
153
|
+
}).format(n === 0 ? 0 : n);
|
|
154
|
+
}
|
|
155
|
+
function I(e, t) {
|
|
156
|
+
return t > 0 ? Math.round(e * 100 / t) : 0;
|
|
157
|
+
}
|
|
158
|
+
function L(n) {
|
|
159
|
+
if (e.is(n)) return [...n.errors];
|
|
160
|
+
if (t.is(n)) try {
|
|
161
|
+
let e = JSON.parse(n.bodyText);
|
|
162
|
+
return (Array.isArray(e) ? e : [e]).flatMap((e) => {
|
|
163
|
+
let t = e?.errors;
|
|
164
|
+
return Array.isArray(t) ? t : [];
|
|
165
|
+
});
|
|
166
|
+
} catch {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
function R(e) {
|
|
172
|
+
return e.extensions && typeof e.extensions == "object" ? e.extensions : {};
|
|
173
|
+
}
|
|
174
|
+
function z(e) {
|
|
175
|
+
for (let t of L(e)) {
|
|
176
|
+
let e = R(t).code;
|
|
177
|
+
if (typeof e == "string" && e) return e;
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
function B(e) {
|
|
182
|
+
for (let t of L(e)) {
|
|
183
|
+
let e = R(t).field;
|
|
184
|
+
if (typeof e == "string" && e) return e;
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
function V(e) {
|
|
189
|
+
for (let t of L(e)) {
|
|
190
|
+
let e = R(t).rowErrors;
|
|
191
|
+
if (!Array.isArray(e)) continue;
|
|
192
|
+
let n = [];
|
|
193
|
+
for (let t of e) {
|
|
194
|
+
if (!t || typeof t != "object") continue;
|
|
195
|
+
let { row: e, field: r, code: i, message: a } = t;
|
|
196
|
+
typeof e != "number" || !Number.isInteger(e) || e < 1 || typeof a != "string" || !a || n.push({
|
|
197
|
+
row: e,
|
|
198
|
+
field: typeof r == "string" && r ? r : null,
|
|
199
|
+
code: typeof i == "string" ? i : "",
|
|
200
|
+
message: a
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
if (n.length > 0) return n.sort((e, t) => e.row - t.row);
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
//#endregion
|
|
208
|
+
export { l as COMMENT_MAX, D as DEFAULT_WINDOW_PRESET, r as METRIC_TYPES, u as RESPONDED_AT_EARLIEST, i as SCORE_SCALE, E as WINDOW_PRESETS, z as errorCode, B as errorField, P as formatCsat, F as formatNps, C as formatUtcDay, v as isCalendarDay, s as isOnScale, S as localDay, d as looksLikeEmail, N as pageStateFromSearch, j as presetDays, V as readImportRowErrors, T as releaseLabel, w as releasesForProduct, b as respondedAtProblem, M as satisfactionPageLink, o as scaleValues, c as scoreCategory, I as shareOf, x as utcDay, A as windowInput };
|
|
209
|
+
|
|
210
|
+
//# sourceMappingURL=satisfaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"satisfaction.js","names":[],"sources":["../../../src/planmagnet/satisfaction/satisfaction.ts"],"sourcesContent":["import { CombinedGraphQLErrors, ServerError } from '@apollo/client/errors';\nimport { CUSTOMER_SATISFACTION_PATH } from '../product/routes';\n\n/**\n * ANR-21 customer satisfaction — the pure rules and formatting behind the Customer satisfaction page, its CSV import\n * and the CSAT/NPS cards. No React here, so every rule is unit-tested (tests/satisfaction.test.ts).\n *\n * The service is the gate for every rule below (wspace-planmagnet-svc satisfaction-input.ts and\n * satisfaction-scoring.ts, main fdb868c). These copies only let a form or the import preview say what is wrong before\n * anything is sent, so each one is at most as strict as the service — never stricter.\n *\n * Owner decisions (2026-10-02): CSAT is the share of 4s and 5s on a 1-5 scale; NPS is % promoters (9-10) minus %\n * detractors (0-6) on 0-10; below 5 responses a metric reads \"Not enough responses\", never 0 or a made-up score; every\n * figure shows its response count and its window; the default window is the trailing 90 days. A feature request's\n * rating is not customer satisfaction and is never shown as one.\n */\n\n// ─── Metrics and scales ─────────────────────────────────────────────────────\n\nexport type SatisfactionMetricType = 'CSAT' | 'NPS';\n\nexport const METRIC_TYPES: readonly SatisfactionMetricType[] = ['CSAT', 'NPS'];\n\n/** The inclusive answer scale of each metric type. */\nexport const SCORE_SCALE: Readonly<Record<SatisfactionMetricType, { min: number; max: number }>> = {\n CSAT: { min: 1, max: 5 },\n NPS: { min: 0, max: 10 },\n};\n\nexport function isMetricType(value: unknown): value is SatisfactionMetricType {\n return value === 'CSAT' || value === 'NPS';\n}\n\n/** Every answer on a metric's scale, lowest first. */\nexport function scaleValues(metricType: SatisfactionMetricType): number[] {\n const { min, max } = SCORE_SCALE[metricType];\n return Array.from({ length: max - min + 1 }, (_, i) => min + i);\n}\n\n/** True when `score` is a whole number on the metric's own scale (CSAT 1-5, NPS 0-10). */\nexport function isOnScale(metricType: SatisfactionMetricType, score: number): boolean {\n const { min, max } = SCORE_SCALE[metricType];\n return Number.isInteger(score) && score >= min && score <= max;\n}\n\nexport type ScoreCategory = 'satisfied' | 'notSatisfied' | 'promoter' | 'passive' | 'detractor';\n\n/** How one answer counts: a CSAT 4-5 is satisfied; an NPS 9-10 promotes, 7-8 is passive, 0-6 detracts. */\nexport function scoreCategory(metricType: SatisfactionMetricType, score: number): ScoreCategory {\n if (metricType === 'CSAT') return score >= 4 ? 'satisfied' : 'notSatisfied';\n if (score >= 9) return 'promoter';\n return score >= 7 ? 'passive' : 'detractor';\n}\n\n// ─── Field limits (the service's) ───────────────────────────────────────────\n\nexport const COMMENT_MAX = 5000;\nexport const RESPONDENT_NAME_MAX = 200;\n/** RFC 5321's longest deliverable address. */\nexport const RESPONDENT_EMAIL_MAX = 254;\nexport const EXTERNAL_ID_MAX = 200;\n/** One import call takes 1 to this many rows. */\nexport const MAX_IMPORT_ROWS = 500;\n/** The earliest respondedAt the service accepts (it catches the epoch from an empty cell, a two-digit year…). */\nexport const RESPONDED_AT_EARLIEST = '2000-01-01';\n\n/**\n * The service's email typo gate (satisfaction-input.ts `looksLikeEmail`), copied exactly: one `@` with something on\n * both sides, a dot inside the domain, no whitespace. Not deliverability.\n */\nexport function looksLikeEmail(email: string): boolean {\n const at = email.indexOf('@');\n if (at <= 0 || at !== email.lastIndexOf('@') || /\\s/.test(email)) return false;\n const domain = email.slice(at + 1);\n const dot = domain.indexOf('.');\n return dot > 0 && !domain.endsWith('.');\n}\n\n// ─── Dates ──────────────────────────────────────────────────────────────────\n\nconst CALENDAR_DATE = /^\\d{4}-\\d{2}-\\d{2}$/;\n// The DateTime scalar's date-time shape: a fixed prefix plus an explicit zone. A bare local time is refused there,\n// since the server would read it in its own zone.\nconst ISO_DATE_TIME_PREFIX = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}/;\nconst ISO_ZONE_SUFFIX = /(?:Z|[+-]\\d{2}:\\d{2})$/;\nconst HOUR_MS = 60 * 60 * 1000;\nconst DAY_MS = 24 * HOUR_MS;\n/**\n * The service accepts a respondedAt up to 14 hours past its own clock — a date picked as \"today\" anywhere on Earth\n * (UTC+14) is that day's UTC midnight. One more hour here absorbs a browser clock running behind the server's, so a\n * preview never refuses a date the service would accept; anything the service still refuses comes back by row.\n */\nconst FUTURE_TOLERANCE_MS = 15 * HOUR_MS;\n\n/** True for a real calendar day written YYYY-MM-DD (2026-02-30 is not one). */\nexport function isCalendarDay(text: string): boolean {\n if (!CALENDAR_DATE.test(text)) return false;\n const date = new Date(`${text}T00:00:00.000Z`);\n return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === text;\n}\n\n/**\n * The instant a respondedAt text stands for, read the way the DateTime scalar reads it: a calendar day (YYYY-MM-DD) is\n * that day's UTC midnight; a date-time must carry its zone (Z or ±hh:mm). Null for anything the scalar refuses.\n */\nexport function parseRespondedAt(text: string): Date | null {\n const value = text.trim();\n if (isCalendarDay(value)) return new Date(`${value}T00:00:00.000Z`);\n if (ISO_DATE_TIME_PREFIX.test(value) && ISO_ZONE_SUFFIX.test(value)) {\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? null : date;\n }\n return null;\n}\n\nexport type RespondedAtProblem = 'required' | 'format' | 'future' | 'tooOld';\n\n/** Why the service would refuse this respondedAt, or null when it would take it. */\nexport function respondedAtProblem(text: string, now: Date): RespondedAtProblem | null {\n if (text.trim() === '') return 'required';\n const date = parseRespondedAt(text);\n if (!date) return 'format';\n if (date.getTime() > now.getTime() + FUTURE_TOLERANCE_MS) return 'future';\n if (date.getTime() < Date.parse(`${RESPONDED_AT_EARLIEST}T00:00:00.000Z`)) return 'tooOld';\n return null;\n}\n\n/** The UTC calendar day (YYYY-MM-DD) of an ISO instant — the day every window counts it under. */\nexport function utcDay(iso: string): string | null {\n const date = new Date(iso);\n return Number.isNaN(date.getTime()) ? null : date.toISOString().slice(0, 10);\n}\n\n/** Today in the viewer's own calendar (YYYY-MM-DD) — what a date input means by \"today\". */\nexport function localDay(now: Date = new Date()): string {\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const day = String(now.getDate()).padStart(2, '0');\n return `${now.getFullYear()}-${month}-${day}`;\n}\n\n/**\n * An instant shown as its UTC calendar day (\"2 Oct 2026\" in the viewer's locale). respondedAt and the window bounds\n * are counted by UTC day — a date entered as 2026-10-02 is stored as that day's UTC midnight — so the day shown is the\n * day the figures count it under. Formatting in the viewer's zone would show a UTC-midnight date as the day before\n * anywhere west of Greenwich.\n */\nexport function formatUtcDay(iso: string, locale?: string): string {\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return '—';\n return date.toLocaleDateString(locale, {\n timeZone: 'UTC',\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n });\n}\n\n// ─── Links ──────────────────────────────────────────────────────────────────\n\n/**\n * The releases a response about `productId` may name: that product's own and the ones with no product — the service\n * refuses a release of another product. Every release when no product is chosen.\n */\nexport function releasesForProduct<T extends { productId: string | null }>(\n releases: readonly T[],\n productId: string\n): T[] {\n if (!productId) return [...releases];\n return releases.filter((r) => !r.productId || r.productId === productId);\n}\n\n/** \"2.4.0 — Spring launch\", or just the version when the name repeats it. */\nexport function releaseLabel(release: { name: string; version: string }): string {\n return release.name && release.name !== release.version\n ? `${release.version} — ${release.name}`\n : release.version;\n}\n\n// ─── Windows ────────────────────────────────────────────────────────────────\n\nexport type WindowPreset =\n | 'LAST_30_DAYS'\n | 'LAST_90_DAYS'\n | 'LAST_180_DAYS'\n | 'LAST_365_DAYS'\n | 'CUSTOM';\n\nexport const WINDOW_PRESETS: readonly WindowPreset[] = [\n 'LAST_30_DAYS',\n 'LAST_90_DAYS',\n 'LAST_180_DAYS',\n 'LAST_365_DAYS',\n 'CUSTOM',\n];\n\n/** Owner decision 2026-10-02: a figure covers the trailing 90 days unless someone picks another window. */\nexport const DEFAULT_WINDOW_PRESET: WindowPreset = 'LAST_90_DAYS';\n\nexport const PRESET_DAYS: Readonly<Record<Exclude<WindowPreset, 'CUSTOM'>, number>> = {\n LAST_30_DAYS: 30,\n LAST_90_DAYS: 90,\n LAST_180_DAYS: 180,\n LAST_365_DAYS: 365,\n};\n\nexport function isWindowPreset(value: unknown): value is WindowPreset {\n return typeof value === 'string' && (WINDOW_PRESETS as readonly string[]).includes(value);\n}\n\n/** The window a page asks for: a preset, or CUSTOM with its own two days (YYYY-MM-DD, both included). */\nexport interface WindowChoice {\n preset: WindowPreset;\n from: string;\n to: string;\n}\n\n/** The `window` input of a satisfaction filter. */\nexport interface SatisfactionWindowInput {\n preset: WindowPreset;\n from?: string;\n to?: string;\n}\n\n/**\n * The `window` input for a choice. A preset sends only its name (the service refuses from/to with any preset but\n * CUSTOM). CUSTOM sends its two days as calendar dates: the DateTime scalar reads a date as that day's UTC midnight and\n * the service counts whole UTC days, so the figure covers exactly the days picked. Null while a custom pair is\n * incomplete or reversed — the caller keeps its last valid window rather than send one the service refuses.\n */\nexport function windowInput(choice: WindowChoice): SatisfactionWindowInput | null {\n if (choice.preset !== 'CUSTOM') return { preset: choice.preset };\n if (!isCalendarDay(choice.from) || !isCalendarDay(choice.to) || choice.from > choice.to) {\n return null;\n }\n return { preset: 'CUSTOM', from: choice.from, to: choice.to };\n}\n\n/**\n * The UTC days a preset covers today — the N days ending today, today included, exactly as the service resolves it.\n * Used to seed the custom date inputs when someone switches from a preset to CUSTOM.\n */\nexport function presetDays(\n preset: Exclude<WindowPreset, 'CUSTOM'>,\n now: Date\n): { from: string; to: string } {\n const to = now.toISOString().slice(0, 10);\n const from = new Date(now.getTime() - (PRESET_DAYS[preset] - 1) * DAY_MS)\n .toISOString()\n .slice(0, 10);\n return { from, to };\n}\n\n// ─── Page links ─────────────────────────────────────────────────────────────\n\n/** What a satisfaction figure or list is about: the workspace (all empty), or one product / release / project. */\nexport interface SatisfactionScope {\n productId?: string;\n releaseId?: string;\n projectId?: string;\n}\n\n/** The Customer satisfaction page's state, as its URL carries it. */\nexport interface SatisfactionPageState {\n window: WindowChoice;\n productId: string;\n releaseId: string;\n projectId: string;\n /** '' = both metrics (the responses list only; the figures always show both). */\n metricType: SatisfactionMetricType | '';\n}\n\n/** The Customer satisfaction page for a scope (and a window other than the default), as a navigate() path. */\nexport function satisfactionPageLink(scope: SatisfactionScope, window?: WindowChoice): string {\n const params = new URLSearchParams();\n if (window && window.preset !== DEFAULT_WINDOW_PRESET) {\n params.set('window', window.preset);\n if (window.preset === 'CUSTOM') {\n params.set('from', window.from);\n params.set('to', window.to);\n }\n }\n if (scope.productId) params.set('product', scope.productId);\n if (scope.releaseId) params.set('release', scope.releaseId);\n if (scope.projectId) params.set('project', scope.projectId);\n const query = params.toString();\n return `/${CUSTOMER_SATISFACTION_PATH}${query ? `?${query}` : ''}`;\n}\n\n/**\n * The page state a URL query asks for (`?window=&from=&to=&product=&release=&project=&metric=`). Anything unreadable\n * falls back to the default — the trailing 90 days, every product, release and project, both metrics.\n */\nexport function pageStateFromSearch(search: string, now: Date): SatisfactionPageState {\n const params = new URLSearchParams(search);\n const preset = params.get('window');\n const from = params.get('from') ?? '';\n const to = params.get('to') ?? '';\n let window: WindowChoice;\n if (preset === 'CUSTOM' && windowInput({ preset: 'CUSTOM', from, to })) {\n window = { preset: 'CUSTOM', from, to };\n } else {\n const chosen: Exclude<WindowPreset, 'CUSTOM'> =\n isWindowPreset(preset) && preset !== 'CUSTOM' ? preset : 'LAST_90_DAYS';\n window = { preset: chosen, ...presetDays(chosen, now) };\n }\n const metric = params.get('metric');\n return {\n window,\n productId: params.get('product') ?? '',\n releaseId: params.get('release') ?? '',\n projectId: params.get('project') ?? '',\n metricType: isMetricType(metric) ? metric : '',\n };\n}\n\n// ─── Figures ────────────────────────────────────────────────────────────────\n\n/** CSAT as a whole percentage (\"82%\"). Null stays null: the caller says \"Not enough responses\", never \"0%\". */\nexport function formatCsat(score: number | null, locale?: string): string | null {\n if (score === null || !Number.isFinite(score)) return null;\n return new Intl.NumberFormat(locale, { style: 'percent', maximumFractionDigits: 0 }).format(\n score / 100\n );\n}\n\n/** NPS as a signed whole number (\"+34\", \"-12\", \"0\"). Null stays null. */\nexport function formatNps(score: number | null, locale?: string): string | null {\n if (score === null || !Number.isFinite(score)) return null;\n const rounded = Math.round(score);\n // Math.round(-0.4) is -0, which would print as \"-0\".\n return new Intl.NumberFormat(locale, {\n signDisplay: 'exceptZero',\n maximumFractionDigits: 0,\n }).format(rounded === 0 ? 0 : rounded);\n}\n\n/** `count` as a whole percentage of `total` — 0 when there is nothing to divide by. */\nexport function shareOf(count: number, total: number): number {\n return total > 0 ? Math.round((count * 100) / total) : 0;\n}\n\n// ─── Refusals ───────────────────────────────────────────────────────────────\n\ninterface ErrorLike {\n message?: unknown;\n extensions?: unknown;\n}\n\n/** The GraphQL errors of a failed operation, from either shape Apollo surfaces them in. */\nfunction graphqlErrorsOf(error: unknown): ErrorLike[] {\n if (CombinedGraphQLErrors.is(error)) return [...error.errors];\n if (ServerError.is(error)) {\n // A refusal sent with a non-2xx status reaches Apollo as a ServerError carrying the JSON body (a batched request\n // answers with an array of results).\n try {\n const body: unknown = JSON.parse(error.bodyText);\n const results = Array.isArray(body) ? body : [body];\n return results.flatMap((result) => {\n const errors = (result as { errors?: unknown } | null)?.errors;\n return Array.isArray(errors) ? (errors as ErrorLike[]) : [];\n });\n } catch {\n return [];\n }\n }\n return [];\n}\n\nfunction extensionsOf(error: ErrorLike): Record<string, unknown> {\n return error.extensions && typeof error.extensions === 'object'\n ? (error.extensions as Record<string, unknown>)\n : {};\n}\n\n/** `extensions.code` of the first error that has one (BAD_USER_INPUT, NOT_FOUND, CONFLICT, FORBIDDEN…). */\nexport function errorCode(error: unknown): string | null {\n for (const e of graphqlErrorsOf(error)) {\n const code = extensionsOf(e).code;\n if (typeof code === 'string' && code) return code;\n }\n return null;\n}\n\n/** The input the service named in `extensions.field` (score, respondedAt, productId…), if any. */\nexport function errorField(error: unknown): string | null {\n for (const e of graphqlErrorsOf(error)) {\n const field = extensionsOf(e).field;\n if (typeof field === 'string' && field) return field;\n }\n return null;\n}\n\n/** One refused import row, as the service reports it in `extensions.rowErrors`. `row` is 1-based, in the order sent. */\nexport interface ImportRowError {\n row: number;\n field: string | null;\n code: string;\n /** The service's own sentence, already in the caller's language. */\n message: string;\n}\n\n/**\n * The refused rows of a failed import, sorted by row — or null when the refusal was not about rows (a role refusal,\n * an empty or oversized batch, a network fault), in which case the caller shows the error's own message.\n */\nexport function readImportRowErrors(error: unknown): ImportRowError[] | null {\n for (const e of graphqlErrorsOf(error)) {\n const list = extensionsOf(e).rowErrors;\n if (!Array.isArray(list)) continue;\n const rows: ImportRowError[] = [];\n for (const entry of list) {\n if (!entry || typeof entry !== 'object') continue;\n const { row, field, code, message } = entry as Record<string, unknown>;\n if (typeof row !== 'number' || !Number.isInteger(row) || row < 1) continue;\n if (typeof message !== 'string' || !message) continue;\n rows.push({\n row,\n field: typeof field === 'string' && field ? field : null,\n code: typeof code === 'string' ? code : '',\n message,\n });\n }\n if (rows.length > 0) return rows.sort((a, b) => a.row - b.row);\n }\n return null;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { SatisfactionMetricType } from './satisfaction';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ANR-21 CSV import, read in the browser: parse the file, map its header, and turn each row into exactly the
|
|
5
|
+
* `PlanmagnetCustomerSatisfactionImportRow` the service takes — with every problem the service would refuse it for,
|
|
6
|
+
* so the preview can show them by row before anything is sent. The service re-checks every row (and whether each
|
|
7
|
+
* linked product, release and project exists) and is the only gate; the import is all or nothing there.
|
|
8
|
+
*
|
|
9
|
+
* Rows are numbered from 1 in the order they are sent, which is the order of the data rows in the file (blank lines
|
|
10
|
+
* dropped) — the same numbering the service uses in `extensions.rowErrors`. No React here: tests/satisfactionCsv.test.ts.
|
|
11
|
+
*/
|
|
12
|
+
/** The columns of an import file, in template order. */
|
|
13
|
+
export declare const IMPORT_COLUMNS: readonly ["externalId", "metricType", "score", "respondedAt", "productId", "releaseId", "projectId", "isAnonymous", "respondentName", "respondentEmail", "comment"];
|
|
14
|
+
export type ImportColumn = (typeof IMPORT_COLUMNS)[number];
|
|
15
|
+
export declare const REQUIRED_COLUMNS: readonly ImportColumn[];
|
|
16
|
+
/** A file larger than this is refused before it is read: 500 rows of 5000-character comments still fit. */
|
|
17
|
+
export declare const MAX_FILE_BYTES: number;
|
|
18
|
+
/**
|
|
19
|
+
* The delimiter of a file: the one of comma, semicolon (spreadsheets in many locales) or tab that occurs most often in
|
|
20
|
+
* its first line outside quotes. Comma when there is none.
|
|
21
|
+
*/
|
|
22
|
+
export declare function detectDelimiter(text: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* RFC 4180 records: quoted fields may hold the delimiter, line breaks and doubled quotes; CRLF, LF and CR all end a
|
|
25
|
+
* record. A leading byte-order mark is dropped. Lines with nothing but delimiters and spaces are dropped. `unterminated`
|
|
26
|
+
* is true when the file ends inside a quoted field — the file is malformed and nothing in it can be trusted.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseCsv(input: string): {
|
|
29
|
+
records: string[][];
|
|
30
|
+
unterminated: boolean;
|
|
31
|
+
};
|
|
32
|
+
export type FileProblem = {
|
|
33
|
+
code: 'unterminatedQuote';
|
|
34
|
+
} | {
|
|
35
|
+
code: 'empty';
|
|
36
|
+
} | {
|
|
37
|
+
code: 'missingColumns';
|
|
38
|
+
columns: ImportColumn[];
|
|
39
|
+
} | {
|
|
40
|
+
code: 'duplicateColumn';
|
|
41
|
+
column: ImportColumn;
|
|
42
|
+
} | {
|
|
43
|
+
code: 'noRows';
|
|
44
|
+
} | {
|
|
45
|
+
code: 'tooManyRows';
|
|
46
|
+
count: number;
|
|
47
|
+
max: number;
|
|
48
|
+
};
|
|
49
|
+
export interface ParsedImportFile {
|
|
50
|
+
/** The position of each column the header names. */
|
|
51
|
+
columns: Partial<Record<ImportColumn, number>>;
|
|
52
|
+
/** Header cells that name no column: ignored, and listed so a misspelt header is visible. */
|
|
53
|
+
ignoredColumns: string[];
|
|
54
|
+
/** The data records, header excluded, in file order. */
|
|
55
|
+
records: string[][];
|
|
56
|
+
/** Why nothing in the file can be imported, or null. */
|
|
57
|
+
problem: FileProblem | null;
|
|
58
|
+
}
|
|
59
|
+
/** Parse a whole import file and check its shape: a header with the required columns, then 1-500 rows. */
|
|
60
|
+
export declare function parseImportFile(text: string): ParsedImportFile;
|
|
61
|
+
/** Exactly the fields of `PlanmagnetCustomerSatisfactionImportRow` — nothing else may ride in the variables. */
|
|
62
|
+
export interface ImportRowInput {
|
|
63
|
+
externalId: string;
|
|
64
|
+
metricType: SatisfactionMetricType;
|
|
65
|
+
score: number;
|
|
66
|
+
respondedAt: string;
|
|
67
|
+
productId?: string;
|
|
68
|
+
releaseId?: string;
|
|
69
|
+
projectId?: string;
|
|
70
|
+
isAnonymous: boolean;
|
|
71
|
+
respondentName?: string;
|
|
72
|
+
respondentEmail?: string;
|
|
73
|
+
comment?: string;
|
|
74
|
+
}
|
|
75
|
+
export type RowProblemCode = 'externalIdRequired' | 'externalIdTooLong' | 'externalIdDuplicate' | 'metricTypeRequired' | 'metricTypeInvalid' | 'scoreRequired' | 'scoreNotWholeNumber' | 'scoreOutOfRange' | 'respondedAtRequired' | 'respondedAtFormat' | 'respondedAtInFuture' | 'respondedAtTooOld' | 'anonymousInvalid' | 'anonymousWithIdentity' | 'respondentNameTooLong' | 'respondentEmailInvalid' | 'commentTooLong' | 'linkRequired';
|
|
76
|
+
export interface RowProblem {
|
|
77
|
+
code: RowProblemCode;
|
|
78
|
+
/** Values for the message ({{max}}, {{row}}, {{metric}}, {{min}}). */
|
|
79
|
+
params?: Record<string, string | number>;
|
|
80
|
+
}
|
|
81
|
+
/** The links a row without any of its own gets ('' = none). Set in the import dialog, never inferred. */
|
|
82
|
+
export interface DefaultLinks {
|
|
83
|
+
productId: string;
|
|
84
|
+
releaseId: string;
|
|
85
|
+
projectId: string;
|
|
86
|
+
}
|
|
87
|
+
export interface PreviewRow {
|
|
88
|
+
/** 1-based, in sending order — the number the service reports a refused row by. */
|
|
89
|
+
row: number;
|
|
90
|
+
/** Each column's text, trimmed ('' when the file has no such column). */
|
|
91
|
+
cells: Record<ImportColumn, string>;
|
|
92
|
+
/** True when the row names no product, release or project and takes the dialog's default links. */
|
|
93
|
+
usesDefaultLinks: boolean;
|
|
94
|
+
/** What would be sent for this row; null while it has a problem. */
|
|
95
|
+
input: ImportRowInput | null;
|
|
96
|
+
problems: RowProblem[];
|
|
97
|
+
}
|
|
98
|
+
/** True when an isAnonymous cell says yes (true / yes / y / 1, any case). */
|
|
99
|
+
export declare function cellIsTrue(text: string): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Every data row as it would be sent, with the problems the service would refuse it for. `defaults` fill the links of
|
|
102
|
+
* a row that names no product, release or project at all — a row with any link of its own keeps exactly its own, so a
|
|
103
|
+
* default product can never contradict a release the file names.
|
|
104
|
+
*/
|
|
105
|
+
export declare function previewRows(file: ParsedImportFile, defaults: DefaultLinks, now: Date): PreviewRow[];
|
|
106
|
+
/** The template: the header row only — an example row imported by mistake would count as a real answer. */
|
|
107
|
+
export declare function templateCsv(): string;
|
|
108
|
+
//# sourceMappingURL=satisfactionCsv.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"satisfactionCsv.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/satisfaction/satisfactionCsv.ts"],"names":[],"mappings":"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"}
|