@happyvertical/smrt-support 0.39.2

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 (96) hide show
  1. package/AGENTS.md +270 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/dist/__smrt-register__.d.ts +2 -0
  5. package/dist/__smrt-register__.d.ts.map +1 -0
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +4358 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/manifest.json +6770 -0
  11. package/dist/models/index.d.ts +17 -0
  12. package/dist/models/index.d.ts.map +1 -0
  13. package/dist/models/service-time-entry.d.ts +75 -0
  14. package/dist/models/service-time-entry.d.ts.map +1 -0
  15. package/dist/models/support-ai-run.d.ts +45 -0
  16. package/dist/models/support-ai-run.d.ts.map +1 -0
  17. package/dist/models/support-case-event.d.ts +25 -0
  18. package/dist/models/support-case-event.d.ts.map +1 -0
  19. package/dist/models/support-case.d.ts +125 -0
  20. package/dist/models/support-case.d.ts.map +1 -0
  21. package/dist/models/support-channel-binding.d.ts +47 -0
  22. package/dist/models/support-channel-binding.d.ts.map +1 -0
  23. package/dist/models/support-compensation-plan.d.ts +42 -0
  24. package/dist/models/support-compensation-plan.d.ts.map +1 -0
  25. package/dist/models/support-interaction.d.ts +49 -0
  26. package/dist/models/support-interaction.d.ts.map +1 -0
  27. package/dist/models/support-plan.d.ts +92 -0
  28. package/dist/models/support-plan.d.ts.map +1 -0
  29. package/dist/models/support-policy.d.ts +84 -0
  30. package/dist/models/support-policy.d.ts.map +1 -0
  31. package/dist/models/support-service-target.d.ts +90 -0
  32. package/dist/models/support-service-target.d.ts.map +1 -0
  33. package/dist/models/support-settlement.d.ts +58 -0
  34. package/dist/models/support-settlement.d.ts.map +1 -0
  35. package/dist/models/support-specialist.d.ts +71 -0
  36. package/dist/models/support-specialist.d.ts.map +1 -0
  37. package/dist/models/support-work-link.d.ts +31 -0
  38. package/dist/models/support-work-link.d.ts.map +1 -0
  39. package/dist/permissions.d.ts +32 -0
  40. package/dist/permissions.d.ts.map +1 -0
  41. package/dist/services/coverage-calendar.d.ts +53 -0
  42. package/dist/services/coverage-calendar.d.ts.map +1 -0
  43. package/dist/services/human-handoff-service.d.ts +113 -0
  44. package/dist/services/human-handoff-service.d.ts.map +1 -0
  45. package/dist/services/index.d.ts +15 -0
  46. package/dist/services/index.d.ts.map +1 -0
  47. package/dist/services/keyed-mutex.d.ts +22 -0
  48. package/dist/services/keyed-mutex.d.ts.map +1 -0
  49. package/dist/services/service-target-engine.d.ts +133 -0
  50. package/dist/services/service-target-engine.d.ts.map +1 -0
  51. package/dist/services/service-time-entry-service.d.ts +73 -0
  52. package/dist/services/service-time-entry-service.d.ts.map +1 -0
  53. package/dist/services/support-ai-workflow.d.ts +178 -0
  54. package/dist/services/support-ai-workflow.d.ts.map +1 -0
  55. package/dist/services/support-case-service.d.ts +162 -0
  56. package/dist/services/support-case-service.d.ts.map +1 -0
  57. package/dist/services/support-intake-service.d.ts +106 -0
  58. package/dist/services/support-intake-service.d.ts.map +1 -0
  59. package/dist/services/support-plan-admin-service.d.ts +45 -0
  60. package/dist/services/support-plan-admin-service.d.ts.map +1 -0
  61. package/dist/services/support-routing-service.d.ts +117 -0
  62. package/dist/services/support-routing-service.d.ts.map +1 -0
  63. package/dist/services/time-entry-approval-service.d.ts +162 -0
  64. package/dist/services/time-entry-approval-service.d.ts.map +1 -0
  65. package/dist/smrt-knowledge.json +4242 -0
  66. package/dist/svelte/components/CaseDetail.svelte +277 -0
  67. package/dist/svelte/components/CaseDetail.svelte.d.ts +10 -0
  68. package/dist/svelte/components/CaseDetail.svelte.d.ts.map +1 -0
  69. package/dist/svelte/components/CaseQueue.svelte +148 -0
  70. package/dist/svelte/components/CaseQueue.svelte.d.ts +12 -0
  71. package/dist/svelte/components/CaseQueue.svelte.d.ts.map +1 -0
  72. package/dist/svelte/components/RoutingRationale.svelte +131 -0
  73. package/dist/svelte/components/RoutingRationale.svelte.d.ts +11 -0
  74. package/dist/svelte/components/RoutingRationale.svelte.d.ts.map +1 -0
  75. package/dist/svelte/components/TargetList.svelte +127 -0
  76. package/dist/svelte/components/TargetList.svelte.d.ts +9 -0
  77. package/dist/svelte/components/TargetList.svelte.d.ts.map +1 -0
  78. package/dist/svelte/components/TimeEntryApprovalQueue.svelte +226 -0
  79. package/dist/svelte/components/TimeEntryApprovalQueue.svelte.d.ts +11 -0
  80. package/dist/svelte/components/TimeEntryApprovalQueue.svelte.d.ts.map +1 -0
  81. package/dist/svelte/components/__tests__/CaseDetail.test.js +86 -0
  82. package/dist/svelte/components/__tests__/CaseQueue.test.js +67 -0
  83. package/dist/svelte/components/__tests__/RoutingRationale.test.js +69 -0
  84. package/dist/svelte/components/__tests__/TargetList.test.js +84 -0
  85. package/dist/svelte/components/__tests__/TimeEntryApprovalQueue.test.js +105 -0
  86. package/dist/svelte/index.d.ts +25 -0
  87. package/dist/svelte/index.d.ts.map +1 -0
  88. package/dist/svelte/index.js +18 -0
  89. package/dist/svelte/types.d.ts +129 -0
  90. package/dist/svelte/types.d.ts.map +1 -0
  91. package/dist/svelte/types.js +182 -0
  92. package/dist/types.d.ts +130 -0
  93. package/dist/types.d.ts.map +1 -0
  94. package/dist/types.js +109 -0
  95. package/dist/types.js.map +1 -0
  96. package/package.json +86 -0
@@ -0,0 +1,277 @@
1
+ <script lang="ts">
2
+ /**
3
+ * CaseDetail — reusable Support Case detail surface (issue #1926): header
4
+ * with priority/status/assignment, resolution, linked work, and the merged
5
+ * interaction + audit timeline. Presentational: the host loads the case view
6
+ * and timeline (see `toSupportCaseView` / `toCaseTimelineItemView`).
7
+ */
8
+
9
+ import { StatusBadge } from '@happyvertical/smrt-ui';
10
+ import {
11
+ type CaseTimelineItemView,
12
+ caseStatusBadgeKey,
13
+ humanizeStatus,
14
+ priorityBadgeKey,
15
+ type SupportCaseView,
16
+ type SupportWorkLinkView,
17
+ } from '../types.js';
18
+
19
+ export interface CaseDetailProps {
20
+ caseView: SupportCaseView;
21
+ timeline?: CaseTimelineItemView[];
22
+ workLinks?: SupportWorkLinkView[];
23
+ }
24
+
25
+ const { caseView, timeline = [], workLinks = [] }: CaseDetailProps = $props();
26
+
27
+ function formatWhen(iso: string): string {
28
+ const date = new Date(iso);
29
+ if (Number.isNaN(date.getTime())) return '';
30
+ return date.toLocaleString(undefined, {
31
+ year: 'numeric',
32
+ month: 'short',
33
+ day: 'numeric',
34
+ hour: '2-digit',
35
+ minute: '2-digit',
36
+ });
37
+ }
38
+ </script>
39
+
40
+ <article class="case-detail">
41
+ <header class="case-detail-header">
42
+ <p class="case-detail-number">{caseView.caseNumber}</p>
43
+ <h2 class="case-detail-subject">{caseView.subject}</h2>
44
+ <div class="case-detail-badges">
45
+ <StatusBadge
46
+ status={priorityBadgeKey(caseView.priority)}
47
+ label={caseView.priority}
48
+ size="sm"
49
+ />
50
+ <StatusBadge
51
+ status={caseStatusBadgeKey(caseView.status)}
52
+ label={humanizeStatus(caseView.status)}
53
+ size="sm"
54
+ />
55
+ {#if caseView.severity}
56
+ <span class="case-detail-severity">{caseView.severity}</span>
57
+ {/if}
58
+ {#if caseView.reopenCount > 0}
59
+ <span class="case-detail-reopens">
60
+ reopened ×{caseView.reopenCount}
61
+ </span>
62
+ {/if}
63
+ </div>
64
+ </header>
65
+
66
+ <dl class="case-detail-meta">
67
+ <div>
68
+ <dt>Assigned</dt>
69
+ <dd>
70
+ {caseView.assignedSpecialistName ??
71
+ (caseView.assignedSpecialistId ? 'assigned' : 'unassigned')}
72
+ </dd>
73
+ </div>
74
+ {#if caseView.channelKind}
75
+ <div>
76
+ <dt>Channel</dt>
77
+ <dd>{caseView.channelKind}</dd>
78
+ </div>
79
+ {/if}
80
+ {#if caseView.projectId}
81
+ <div>
82
+ <dt>Project</dt>
83
+ <dd>{caseView.projectId}</dd>
84
+ </div>
85
+ {/if}
86
+ </dl>
87
+
88
+ {#if caseView.resolutionSummary}
89
+ <section class="case-detail-resolution" aria-label="Resolution">
90
+ <h3>Resolution</h3>
91
+ <p>{caseView.resolutionSummary}</p>
92
+ </section>
93
+ {/if}
94
+
95
+ {#if workLinks.length > 0}
96
+ <section class="case-detail-work" aria-label="Linked work">
97
+ <h3>Linked work</h3>
98
+ <ul>
99
+ {#each workLinks as link (link.id)}
100
+ <li>
101
+ <span class="case-detail-work-kind">
102
+ {link.linkKind === 'development_work_item'
103
+ ? 'delivery'
104
+ : 'support'}
105
+ </span>
106
+ {#if link.externalUrl}
107
+ <a href={link.externalUrl} target="_blank" rel="noreferrer">
108
+ {link.targetLabel || link.externalUrl}
109
+ </a>
110
+ {:else}
111
+ <span>{link.targetLabel}</span>
112
+ {/if}
113
+ <StatusBadge status="pending" label={link.status} size="sm" />
114
+ </li>
115
+ {/each}
116
+ </ul>
117
+ </section>
118
+ {/if}
119
+
120
+ <section class="case-detail-timeline" aria-label="Case history">
121
+ <h3>History</h3>
122
+ {#if timeline.length === 0}
123
+ <p class="case-detail-empty">No activity yet</p>
124
+ {:else}
125
+ <ol>
126
+ {#each timeline as item, index (index)}
127
+ <li class="timeline-item" data-kind={item.kind}>
128
+ <div class="timeline-item-head">
129
+ <span class="timeline-actor">{item.actorKind}</span>
130
+ <span class="timeline-summary">
131
+ {item.kind === 'event'
132
+ ? item.summary
133
+ : humanizeStatus(item.summary)}
134
+ </span>
135
+ <time datetime={item.occurredAt}>
136
+ {formatWhen(item.occurredAt)}
137
+ </time>
138
+ </div>
139
+ {#if item.body}
140
+ <p class="timeline-body">{item.body}</p>
141
+ {/if}
142
+ </li>
143
+ {/each}
144
+ </ol>
145
+ {/if}
146
+ </section>
147
+ </article>
148
+
149
+ <style>
150
+ .case-detail {
151
+ display: flex;
152
+ flex-direction: column;
153
+ gap: 1rem;
154
+ }
155
+
156
+ .case-detail-number {
157
+ margin: 0;
158
+ font-size: 0.75rem;
159
+ color: var(--smrt-color-on-surface-variant, inherit);
160
+ }
161
+
162
+ .case-detail-subject {
163
+ margin: 0.125rem 0 0.5rem;
164
+ font-size: 1.25rem;
165
+ }
166
+
167
+ .case-detail-badges {
168
+ display: flex;
169
+ align-items: center;
170
+ gap: 0.5rem;
171
+ flex-wrap: wrap;
172
+ }
173
+
174
+ .case-detail-severity,
175
+ .case-detail-reopens {
176
+ font-size: 0.75rem;
177
+ color: var(--smrt-color-on-surface-variant, inherit);
178
+ }
179
+
180
+ .case-detail-meta {
181
+ display: flex;
182
+ gap: 1.5rem;
183
+ margin: 0;
184
+ flex-wrap: wrap;
185
+ }
186
+
187
+ .case-detail-meta dt {
188
+ font-size: 0.6875rem;
189
+ text-transform: uppercase;
190
+ letter-spacing: 0.04em;
191
+ color: var(--smrt-color-on-surface-variant, inherit);
192
+ }
193
+
194
+ .case-detail-meta dd {
195
+ margin: 0;
196
+ }
197
+
198
+ .case-detail-resolution h3,
199
+ .case-detail-work h3,
200
+ .case-detail-timeline h3 {
201
+ margin: 0 0 0.5rem;
202
+ font-size: 0.875rem;
203
+ }
204
+
205
+ .case-detail-work ul {
206
+ list-style: none;
207
+ margin: 0;
208
+ padding: 0;
209
+ display: flex;
210
+ flex-direction: column;
211
+ gap: 0.375rem;
212
+ }
213
+
214
+ .case-detail-work li {
215
+ display: flex;
216
+ align-items: center;
217
+ gap: 0.5rem;
218
+ }
219
+
220
+ .case-detail-work-kind {
221
+ font-size: 0.6875rem;
222
+ text-transform: uppercase;
223
+ letter-spacing: 0.04em;
224
+ color: var(--smrt-color-on-surface-variant, inherit);
225
+ }
226
+
227
+ .case-detail-timeline ol {
228
+ list-style: none;
229
+ margin: 0;
230
+ padding: 0;
231
+ display: flex;
232
+ flex-direction: column;
233
+ gap: 0.625rem;
234
+ }
235
+
236
+ .timeline-item {
237
+ border-left: 2px solid var(--smrt-color-outline-variant, currentColor);
238
+ padding-left: 0.75rem;
239
+ }
240
+
241
+ .timeline-item[data-kind='interaction'] {
242
+ border-left-color: var(--smrt-color-primary, currentColor);
243
+ }
244
+
245
+ .timeline-item-head {
246
+ display: flex;
247
+ align-items: baseline;
248
+ gap: 0.5rem;
249
+ flex-wrap: wrap;
250
+ }
251
+
252
+ .timeline-actor {
253
+ font-size: 0.6875rem;
254
+ text-transform: uppercase;
255
+ letter-spacing: 0.04em;
256
+ color: var(--smrt-color-on-surface-variant, inherit);
257
+ }
258
+
259
+ .timeline-summary {
260
+ font-weight: 500;
261
+ }
262
+
263
+ .timeline-item time {
264
+ font-size: 0.75rem;
265
+ color: var(--smrt-color-on-surface-variant, inherit);
266
+ margin-left: auto;
267
+ }
268
+
269
+ .timeline-body {
270
+ margin: 0.25rem 0 0;
271
+ white-space: pre-wrap;
272
+ }
273
+
274
+ .case-detail-empty {
275
+ color: var(--smrt-color-on-surface-variant, inherit);
276
+ }
277
+ </style>
@@ -0,0 +1,10 @@
1
+ import { type CaseTimelineItemView, type SupportCaseView, type SupportWorkLinkView } from '../types.js';
2
+ export interface CaseDetailProps {
3
+ caseView: SupportCaseView;
4
+ timeline?: CaseTimelineItemView[];
5
+ workLinks?: SupportWorkLinkView[];
6
+ }
7
+ declare const CaseDetail: import("svelte").Component<CaseDetailProps, {}, "">;
8
+ type CaseDetail = ReturnType<typeof CaseDetail>;
9
+ export default CaseDetail;
10
+ //# sourceMappingURL=CaseDetail.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CaseDetail.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/CaseDetail.svelte.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,KAAK,oBAAoB,EAIzB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACzB,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,eAAe,CAAC;IAC1B,QAAQ,CAAC,EAAE,oBAAoB,EAAE,CAAC;IAClC,SAAS,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACnC;AAgID,QAAA,MAAM,UAAU,qDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -0,0 +1,148 @@
1
+ <script lang="ts">
2
+ /**
3
+ * CaseQueue — reusable Support Case queue list (issue #1926): priority,
4
+ * status, assignment, and channel at a glance. Presentational: the host
5
+ * fetches `SupportCaseView[]` (see `toSupportCaseView`) and wires `onselect`.
6
+ */
7
+
8
+ import { Button, StatusBadge } from '@happyvertical/smrt-ui';
9
+ import {
10
+ caseStatusBadgeKey,
11
+ humanizeStatus,
12
+ priorityBadgeKey,
13
+ type SupportCaseView,
14
+ } from '../types.js';
15
+
16
+ export interface CaseQueueProps {
17
+ cases: SupportCaseView[];
18
+ onselect?: (caseId: string) => void;
19
+ emptyMessage?: string;
20
+ /** Show the assigned-specialist column (default true). */
21
+ showAssignee?: boolean;
22
+ }
23
+
24
+ const {
25
+ cases,
26
+ onselect,
27
+ emptyMessage = 'No support cases',
28
+ showAssignee = true,
29
+ }: CaseQueueProps = $props();
30
+
31
+ function formatUpdated(iso: string | null): string {
32
+ if (!iso) return '';
33
+ const date = new Date(iso);
34
+ if (Number.isNaN(date.getTime())) return '';
35
+ return date.toLocaleString(undefined, {
36
+ month: 'short',
37
+ day: 'numeric',
38
+ hour: '2-digit',
39
+ minute: '2-digit',
40
+ });
41
+ }
42
+ </script>
43
+
44
+ {#if cases.length === 0}
45
+ <p class="case-queue-empty">{emptyMessage}</p>
46
+ {:else}
47
+ <ul class="case-queue">
48
+ {#each cases as item (item.id)}
49
+ <li class="case-queue-row">
50
+ <Button
51
+ variant="ghost"
52
+ fullWidth
53
+ onclick={() => onselect?.(item.id)}
54
+ aria-label={`Open case ${item.caseNumber}: ${item.subject}`}
55
+ >
56
+ <span class="case-queue-line">
57
+ <span class="case-queue-main">
58
+ <span class="case-queue-number">{item.caseNumber}</span>
59
+ <span class="case-queue-subject">{item.subject}</span>
60
+ </span>
61
+ <span class="case-queue-meta">
62
+ <StatusBadge
63
+ status={priorityBadgeKey(item.priority)}
64
+ label={item.priority}
65
+ size="sm"
66
+ />
67
+ <StatusBadge
68
+ status={caseStatusBadgeKey(item.status)}
69
+ label={humanizeStatus(item.status)}
70
+ size="sm"
71
+ />
72
+ {#if item.channelKind}
73
+ <span class="case-queue-channel">{item.channelKind}</span>
74
+ {/if}
75
+ {#if showAssignee}
76
+ <span class="case-queue-assignee">
77
+ {item.assignedSpecialistName ??
78
+ (item.assignedSpecialistId ? 'assigned' : 'unassigned')}
79
+ </span>
80
+ {/if}
81
+ <span class="case-queue-updated">
82
+ {formatUpdated(item.updatedAt)}
83
+ </span>
84
+ </span>
85
+ </span>
86
+ </Button>
87
+ </li>
88
+ {/each}
89
+ </ul>
90
+ {/if}
91
+
92
+ <style>
93
+ .case-queue {
94
+ list-style: none;
95
+ margin: 0;
96
+ padding: 0;
97
+ display: flex;
98
+ flex-direction: column;
99
+ gap: 0.25rem;
100
+ }
101
+
102
+ .case-queue-line {
103
+ display: flex;
104
+ width: 100%;
105
+ align-items: center;
106
+ justify-content: space-between;
107
+ gap: 0.75rem;
108
+ text-align: left;
109
+ }
110
+
111
+ .case-queue-main {
112
+ display: flex;
113
+ flex-direction: column;
114
+ gap: 0.125rem;
115
+ min-width: 0;
116
+ }
117
+
118
+ .case-queue-number {
119
+ font-size: 0.75rem;
120
+ color: var(--smrt-color-on-surface-variant, inherit);
121
+ }
122
+
123
+ .case-queue-subject {
124
+ font-weight: 500;
125
+ overflow: hidden;
126
+ text-overflow: ellipsis;
127
+ white-space: nowrap;
128
+ }
129
+
130
+ .case-queue-meta {
131
+ display: flex;
132
+ align-items: center;
133
+ gap: 0.5rem;
134
+ flex-shrink: 0;
135
+ }
136
+
137
+ .case-queue-channel,
138
+ .case-queue-assignee,
139
+ .case-queue-updated {
140
+ font-size: 0.75rem;
141
+ color: var(--smrt-color-on-surface-variant, inherit);
142
+ }
143
+
144
+ .case-queue-empty {
145
+ color: var(--smrt-color-on-surface-variant, inherit);
146
+ padding: 1rem 0;
147
+ }
148
+ </style>
@@ -0,0 +1,12 @@
1
+ import { type SupportCaseView } from '../types.js';
2
+ export interface CaseQueueProps {
3
+ cases: SupportCaseView[];
4
+ onselect?: (caseId: string) => void;
5
+ emptyMessage?: string;
6
+ /** Show the assigned-specialist column (default true). */
7
+ showAssignee?: boolean;
8
+ }
9
+ declare const CaseQueue: import("svelte").Component<CaseQueueProps, {}, "">;
10
+ type CaseQueue = ReturnType<typeof CaseQueue>;
11
+ export default CaseQueue;
12
+ //# sourceMappingURL=CaseQueue.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CaseQueue.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/CaseQueue.svelte.ts"],"names":[],"mappings":"AASA,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAmED,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -0,0 +1,131 @@
1
+ <script lang="ts">
2
+ /**
3
+ * RoutingRationale — the explainable routing ranking for a case (issue
4
+ * #1929): each specialist's score, eligibility, and the factor signals
5
+ * behind them, with an optional manual-reassign action per row.
6
+ * Presentational: the host runs `SupportRoutingService.rankSpecialists`
7
+ * (and gates reassignment with `support.reassign-case`).
8
+ */
9
+
10
+ import { Button, StatusBadge } from '@happyvertical/smrt-ui';
11
+ import type { RankedSpecialistView } from '../types.js';
12
+
13
+ export interface RoutingRationaleProps {
14
+ ranking: RankedSpecialistView[];
15
+ /** When provided, each row shows a reassign action. */
16
+ onreassign?: (specialistId: string) => void;
17
+ emptyMessage?: string;
18
+ }
19
+
20
+ const {
21
+ ranking,
22
+ onreassign,
23
+ emptyMessage = 'No specialists to rank',
24
+ }: RoutingRationaleProps = $props();
25
+
26
+ function factorChips(
27
+ factors: Record<string, number | string | boolean>,
28
+ ): string[] {
29
+ return Object.entries(factors).map(([key, value]) =>
30
+ value === true ? key : `${key}: ${value}`,
31
+ );
32
+ }
33
+ </script>
34
+
35
+ {#if ranking.length === 0}
36
+ <p class="routing-rationale-empty">{emptyMessage}</p>
37
+ {:else}
38
+ <ul class="routing-rationale">
39
+ {#each ranking as entry (entry.specialistId)}
40
+ <li class="routing-rationale-row">
41
+ <span class="routing-rationale-main">
42
+ <span class="routing-rationale-head">
43
+ <span class="routing-rationale-name">{entry.displayName}</span>
44
+ <span class="routing-rationale-score">score {entry.score}</span>
45
+ <StatusBadge
46
+ status={entry.eligible ? 'success' : 'inactive'}
47
+ label={entry.eligible ? 'eligible' : 'ineligible'}
48
+ size="sm"
49
+ />
50
+ </span>
51
+ <span class="routing-rationale-factors">
52
+ {#each factorChips(entry.factors) as chip (chip)}
53
+ <span class="routing-rationale-factor">{chip}</span>
54
+ {/each}
55
+ </span>
56
+ </span>
57
+ {#if onreassign}
58
+ <Button
59
+ variant="secondary"
60
+ size="sm"
61
+ onclick={() => onreassign?.(entry.specialistId)}
62
+ aria-label={`Reassign to ${entry.displayName}`}
63
+ >
64
+ Reassign
65
+ </Button>
66
+ {/if}
67
+ </li>
68
+ {/each}
69
+ </ul>
70
+ {/if}
71
+
72
+ <style>
73
+ .routing-rationale {
74
+ list-style: none;
75
+ margin: 0;
76
+ padding: 0;
77
+ display: flex;
78
+ flex-direction: column;
79
+ gap: 0.5rem;
80
+ }
81
+
82
+ .routing-rationale-row {
83
+ display: flex;
84
+ align-items: flex-start;
85
+ justify-content: space-between;
86
+ gap: 0.75rem;
87
+ }
88
+
89
+ .routing-rationale-main {
90
+ display: flex;
91
+ flex-direction: column;
92
+ gap: 0.25rem;
93
+ min-width: 0;
94
+ }
95
+
96
+ .routing-rationale-head {
97
+ display: flex;
98
+ align-items: center;
99
+ gap: 0.5rem;
100
+ flex-wrap: wrap;
101
+ }
102
+
103
+ .routing-rationale-name {
104
+ font-weight: 500;
105
+ }
106
+
107
+ .routing-rationale-score {
108
+ font-size: 0.75rem;
109
+ color: var(--smrt-color-on-surface-variant, inherit);
110
+ }
111
+
112
+ .routing-rationale-factors {
113
+ display: flex;
114
+ flex-wrap: wrap;
115
+ gap: 0.25rem;
116
+ }
117
+
118
+ .routing-rationale-factor {
119
+ font-size: 0.6875rem;
120
+ padding: 0.0625rem 0.375rem;
121
+ border-radius: 0.5rem;
122
+ background: var(--smrt-color-surface-container-highest, transparent);
123
+ color: var(--smrt-color-on-surface-variant, inherit);
124
+ border: 1px solid var(--smrt-color-outline-variant, currentColor);
125
+ }
126
+
127
+ .routing-rationale-empty {
128
+ color: var(--smrt-color-on-surface-variant, inherit);
129
+ padding: 1rem 0;
130
+ }
131
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { RankedSpecialistView } from '../types.js';
2
+ export interface RoutingRationaleProps {
3
+ ranking: RankedSpecialistView[];
4
+ /** When provided, each row shows a reassign action. */
5
+ onreassign?: (specialistId: string) => void;
6
+ emptyMessage?: string;
7
+ }
8
+ declare const RoutingRationale: import("svelte").Component<RoutingRationaleProps, {}, "">;
9
+ type RoutingRationale = ReturnType<typeof RoutingRationale>;
10
+ export default RoutingRationale;
11
+ //# sourceMappingURL=RoutingRationale.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RoutingRationale.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/RoutingRationale.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGxD,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAChC,uDAAuD;IACvD,UAAU,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAuDD,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}