@edvak99/voice-receptionist-widget 1.1.4 → 1.1.6

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 (77) hide show
  1. package/README.md +37 -0
  2. package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-CdrW0He4.mjs +306 -0
  3. package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-CdrW0He4.mjs.map +1 -0
  4. package/fesm2022/{edvak99-voice-receptionist-widget-ai-receptionist-api.service-48cifVPF.mjs → edvak99-voice-receptionist-widget-ai-receptionist-api.service-DokIb-9r.mjs} +2 -2
  5. package/fesm2022/{edvak99-voice-receptionist-widget-ai-receptionist-api.service-48cifVPF.mjs.map → edvak99-voice-receptionist-widget-ai-receptionist-api.service-DokIb-9r.mjs.map} +1 -1
  6. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-4GK1vsom.mjs +277 -0
  7. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-4GK1vsom.mjs.map +1 -0
  8. package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-Bw_Ade1s.mjs +416 -0
  9. package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-Bw_Ade1s.mjs.map +1 -0
  10. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DnwYnjIE.mjs +193 -0
  11. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DnwYnjIE.mjs.map +1 -0
  12. package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-Pla2UgeZ.mjs +111 -0
  13. package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-Pla2UgeZ.mjs.map +1 -0
  14. package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-CIMcjGKX.mjs → edvak99-voice-receptionist-widget-call-review.service-Di7GLG9k.mjs} +2 -2
  15. package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-CIMcjGKX.mjs.map → edvak99-voice-receptionist-widget-call-review.service-Di7GLG9k.mjs.map} +1 -1
  16. package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-DVz0romd.mjs +439 -0
  17. package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-DVz0romd.mjs.map +1 -0
  18. package/fesm2022/{edvak99-voice-receptionist-widget-dashboard-socket.service-BX91r74O.mjs → edvak99-voice-receptionist-widget-dashboard-socket.service-D2pelzu4.mjs} +3 -3
  19. package/fesm2022/{edvak99-voice-receptionist-widget-dashboard-socket.service-BX91r74O.mjs.map → edvak99-voice-receptionist-widget-dashboard-socket.service-D2pelzu4.mjs.map} +1 -1
  20. package/fesm2022/{edvak99-voice-receptionist-widget-eds-avatar.component-CWRiJGfv.mjs → edvak99-voice-receptionist-widget-eds-avatar.component-BJs_TH8X.mjs} +5 -5
  21. package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-BJs_TH8X.mjs.map +1 -0
  22. package/fesm2022/{edvak99-voice-receptionist-widget-eds-pulse-dot.component-BPY6xN7C.mjs → edvak99-voice-receptionist-widget-eds-pulse-dot.component-Ig0Eoq1z.mjs} +6 -6
  23. package/fesm2022/edvak99-voice-receptionist-widget-eds-pulse-dot.component-Ig0Eoq1z.mjs.map +1 -0
  24. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-Bx7WQ2dg.mjs +81 -0
  25. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-Bx7WQ2dg.mjs.map +1 -0
  26. package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-DWbA5yAG.mjs +1059 -0
  27. package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-DWbA5yAG.mjs.map +1 -0
  28. package/fesm2022/edvak99-voice-receptionist-widget-modal.component-Cb-FoQ1N.mjs +48 -0
  29. package/fesm2022/edvak99-voice-receptionist-widget-modal.component-Cb-FoQ1N.mjs.map +1 -0
  30. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-DhoyrHSe.mjs +267 -0
  31. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-DhoyrHSe.mjs.map +1 -0
  32. package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-CZUDgZ1_.mjs +967 -0
  33. package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-CZUDgZ1_.mjs.map +1 -0
  34. package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-DyNG2DZZ.mjs +316 -0
  35. package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-DyNG2DZZ.mjs.map +1 -0
  36. package/fesm2022/edvak99-voice-receptionist-widget-wizard-page.component-DjaurkbO.mjs +46 -0
  37. package/fesm2022/edvak99-voice-receptionist-widget-wizard-page.component-DjaurkbO.mjs.map +1 -0
  38. package/fesm2022/edvak99-voice-receptionist-widget.mjs +228 -1
  39. package/fesm2022/edvak99-voice-receptionist-widget.mjs.map +1 -1
  40. package/lib/components/call-conversation-panel/call-conversation-panel.component.d.ts +1 -2
  41. package/lib/components/live-transcript-panel/live-transcript-panel.component.d.ts +0 -3
  42. package/package.json +1 -1
  43. package/public-api.d.ts +3 -0
  44. package/styles/chat-ui.scss +0 -1
  45. package/styles/eds.scss +0 -10
  46. package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-DT9UKvQO.mjs +0 -307
  47. package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-DT9UKvQO.mjs.map +0 -1
  48. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-C5inEHJw.mjs +0 -278
  49. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-C5inEHJw.mjs.map +0 -1
  50. package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-sYhM20CE.mjs +0 -418
  51. package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-sYhM20CE.mjs.map +0 -1
  52. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DpwajEOE.mjs +0 -194
  53. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DpwajEOE.mjs.map +0 -1
  54. package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-CgkRcFOl.mjs +0 -111
  55. package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-CgkRcFOl.mjs.map +0 -1
  56. package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-CxbO0eDS.mjs +0 -440
  57. package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-CxbO0eDS.mjs.map +0 -1
  58. package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-CWRiJGfv.mjs.map +0 -1
  59. package/fesm2022/edvak99-voice-receptionist-widget-eds-button.component-Bg5T5BN8.mjs +0 -27
  60. package/fesm2022/edvak99-voice-receptionist-widget-eds-button.component-Bg5T5BN8.mjs.map +0 -1
  61. package/fesm2022/edvak99-voice-receptionist-widget-eds-pulse-dot.component-BPY6xN7C.mjs.map +0 -1
  62. package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BsBQIS_f.mjs +0 -203
  63. package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BsBQIS_f.mjs.map +0 -1
  64. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-CaSDMxpV.mjs +0 -81
  65. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-CaSDMxpV.mjs.map +0 -1
  66. package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-Cy3ND-c-.mjs +0 -1070
  67. package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-Cy3ND-c-.mjs.map +0 -1
  68. package/fesm2022/edvak99-voice-receptionist-widget-modal.component-COfi7biN.mjs +0 -48
  69. package/fesm2022/edvak99-voice-receptionist-widget-modal.component-COfi7biN.mjs.map +0 -1
  70. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-fHVzpiVk.mjs +0 -268
  71. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-fHVzpiVk.mjs.map +0 -1
  72. package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-Bok64X2q.mjs +0 -968
  73. package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-Bok64X2q.mjs.map +0 -1
  74. package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-Bb9lPcYi.mjs +0 -317
  75. package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-Bb9lPcYi.mjs.map +0 -1
  76. package/fesm2022/edvak99-voice-receptionist-widget-wizard-page.component-BZVlWLWh.mjs +0 -47
  77. package/fesm2022/edvak99-voice-receptionist-widget-wizard-page.component-BZVlWLWh.mjs.map +0 -1
@@ -1,440 +0,0 @@
1
- import * as i0 from '@angular/core';
2
- import { inject, DestroyRef, EventEmitter, signal, Output, Input, ChangeDetectionStrategy, Component } from '@angular/core';
3
- import { Router, ActivatedRoute, RouterLink } from '@angular/router';
4
- import { DatePipe } from '@angular/common';
5
- import { A as AiReceptionistApiService, t as toE164Phone, c as callerMetaLine, r as resultChipIcon, h as historyResultChips, a as historyResolutionLabel, b as historyResolution } from './edvak99-voice-receptionist-widget-ai-receptionist-api.service-48cifVPF.mjs';
6
- import { L as LiveCallsListService, E as EdsPulseDotComponent } from './edvak99-voice-receptionist-widget-eds-pulse-dot.component-BPY6xN7C.mjs';
7
- import { i as isNeedsReview } from './edvak99-voice-receptionist-widget-call-review.util-Deh3jPgo.mjs';
8
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
9
- import * as i1 from '@angular/forms';
10
- import { FormsModule } from '@angular/forms';
11
- import { Subject, debounceTime, distinctUntilChanged, switchMap, of, catchError } from 'rxjs';
12
- import { r as requiredFieldLabel, a as renderLiveOpeningPreview, f as formatOpeningTime, b as formatOpeningDate, p as purposeShowsCustomScript, i as isCustomScriptRequired, c as isPurposeFormValid } from './edvak99-voice-receptionist-widget-outbound-purpose.util-Bdh4QfFe.mjs';
13
- import { E as EdsButtonComponent } from './edvak99-voice-receptionist-widget-eds-button.component-Bg5T5BN8.mjs';
14
- import { M as ModalComponent } from './edvak99-voice-receptionist-widget-modal.component-COfi7biN.mjs';
15
- import { T as ToastService, E as EdsIconComponent } from './edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BsBQIS_f.mjs';
16
- import { E as EdsAvatarComponent } from './edvak99-voice-receptionist-widget-eds-avatar.component-CWRiJGfv.mjs';
17
- import { E as EdsElapsedPipe } from './edvak99-voice-receptionist-widget-eds-elapsed.pipe-BFCiJOT3.mjs';
18
-
19
- const E164_PATTERN = /^\+[1-9]\d{6,14}$/;
20
- class OutboundDialModalComponent {
21
- constructor() {
22
- this.api = inject(AiReceptionistApiService);
23
- this.toast = inject(ToastService);
24
- this.destroyRef = inject(DestroyRef);
25
- this.open = false;
26
- this.closed = new EventEmitter();
27
- this.dialed = new EventEmitter();
28
- this.purposes = signal([]);
29
- this.loading = signal(true);
30
- this.submitting = signal(false);
31
- this.serverError = signal(null);
32
- this.patientHits = signal([]);
33
- this.searchingPatients = signal(false);
34
- this.selectedPatient = signal(null);
35
- this.appointments = signal([]);
36
- this.loadingAppointments = signal(false);
37
- this.selectedAppointmentId = signal(null);
38
- this.showManualEntry = signal(false);
39
- this.practiceName = signal('');
40
- /** Bumps OnPush re-render when plain ngModel fields change. */
41
- this.formEpoch = signal(0);
42
- this.patientQuery = '';
43
- this.phone = '';
44
- this.name = '';
45
- this.purposeId = '';
46
- this.appointmentDate = '';
47
- this.appointmentTime = '';
48
- this.providerName = '';
49
- this.customScript = '';
50
- this.requiredFieldLabel = requiredFieldLabel;
51
- this.search$ = new Subject();
52
- this.search$
53
- .pipe(debounceTime(280), distinctUntilChanged(), switchMap((q) => {
54
- const query = q.trim();
55
- if (query.length < 2) {
56
- this.searchingPatients.set(false);
57
- this.patientHits.set([]);
58
- return of([]);
59
- }
60
- this.searchingPatients.set(true);
61
- return this.api.searchPatientsForOutbound(query, 10).pipe(catchError(() => of([])));
62
- }), takeUntilDestroyed(this.destroyRef))
63
- .subscribe((hits) => {
64
- this.searchingPatients.set(false);
65
- if (!this.selectedPatient()) {
66
- this.patientHits.set(hits);
67
- }
68
- });
69
- }
70
- ngOnChanges(changes) {
71
- if (changes['open']?.currentValue) {
72
- this.resetForm();
73
- this.loadPurposes();
74
- this.loadPracticeName();
75
- }
76
- }
77
- get selectedPurpose() {
78
- return this.purposes().find((p) => p.purposeId === this.purposeId) ?? null;
79
- }
80
- get selectedAppointment() {
81
- const id = this.selectedAppointmentId();
82
- if (!id)
83
- return null;
84
- return this.appointments().find((a) => a.appointmentId === id) ?? null;
85
- }
86
- get previewText() {
87
- this.formEpoch(); // depend for CD when form fields change
88
- const p = this.selectedPurpose;
89
- if (!p)
90
- return '—';
91
- const appt = this.selectedAppointment;
92
- const dateSource = appt?.startTime || this.appointmentDate;
93
- const timeSource = appt?.startTime || this.appointmentTime;
94
- return renderLiveOpeningPreview(p, {
95
- patientName: this.name.trim() || this.selectedPatient()?.fullName || '',
96
- appointmentDate: formatOpeningDate(dateSource),
97
- appointmentTime: formatOpeningTime(this.appointmentTime || timeSource, appt?.startTime),
98
- providerName: this.providerName.trim() || appt?.providerName || '',
99
- practiceName: this.practiceName(),
100
- locationName: appt?.locationName || '',
101
- visitType: appt?.visitType || '',
102
- customScript: this.customScript.trim(),
103
- });
104
- }
105
- showsField(field) {
106
- return this.selectedPurpose?.requiredFields.includes(field) ?? false;
107
- }
108
- showsCustomScript() {
109
- const p = this.selectedPurpose;
110
- return p ? purposeShowsCustomScript(p) : false;
111
- }
112
- customScriptRequired() {
113
- const p = this.selectedPurpose;
114
- return p ? isCustomScriptRequired(p) : false;
115
- }
116
- canPlaceCall() {
117
- if (!E164_PATTERN.test(this.phone.trim()))
118
- return false;
119
- return isPurposeFormValid(this.selectedPurpose, {
120
- phone: this.phone,
121
- name: this.name,
122
- appointmentDate: this.appointmentDate,
123
- appointmentTime: this.appointmentTime,
124
- providerName: this.providerName,
125
- customScript: this.customScript,
126
- });
127
- }
128
- touchForm() {
129
- this.formEpoch.update((n) => n + 1);
130
- }
131
- onPatientQueryChange(value) {
132
- this.patientQuery = value;
133
- if (this.selectedPatient())
134
- return;
135
- this.search$.next(value);
136
- }
137
- selectPatient(patient) {
138
- this.selectedPatient.set(patient);
139
- this.patientHits.set([]);
140
- this.patientQuery = patient.fullName;
141
- this.name = patient.fullName;
142
- this.phone = toE164Phone(patient.phone) || '';
143
- this.showManualEntry.set(!this.phone);
144
- this.selectedAppointmentId.set(null);
145
- this.clearApptDerivedFields();
146
- this.touchForm();
147
- this.loadAppointments(patient.patientId);
148
- }
149
- clearPatient() {
150
- this.selectedPatient.set(null);
151
- this.patientQuery = '';
152
- this.patientHits.set([]);
153
- this.appointments.set([]);
154
- this.selectedAppointmentId.set(null);
155
- this.name = '';
156
- this.phone = '';
157
- this.clearApptDerivedFields();
158
- this.touchForm();
159
- }
160
- selectAppointment(appt) {
161
- this.selectedAppointmentId.set(appt?.appointmentId ?? null);
162
- if (!appt) {
163
- this.clearApptDerivedFields();
164
- this.touchForm();
165
- return;
166
- }
167
- this.applyAppointmentFields(appt);
168
- this.preferAppointmentPurpose();
169
- this.touchForm();
170
- }
171
- onPurposeChange() {
172
- const appt = this.selectedAppointment;
173
- if (appt) {
174
- this.applyAppointmentFields(appt);
175
- }
176
- else {
177
- this.clearApptDerivedFields();
178
- }
179
- this.customScript = '';
180
- this.serverError.set(null);
181
- this.touchForm();
182
- }
183
- close() {
184
- this.closed.emit();
185
- }
186
- submit() {
187
- this.serverError.set(null);
188
- if (!this.canPlaceCall() || !this.selectedPurpose)
189
- return;
190
- this.submitting.set(true);
191
- const patient = this.selectedPatient();
192
- const apptId = this.selectedAppointmentId();
193
- const body = {
194
- phone: this.phone.trim(),
195
- name: this.name.trim(),
196
- purpose: this.purposeId,
197
- ...(this.appointmentDate ? { appointmentDate: this.appointmentDate } : {}),
198
- ...(this.appointmentTime ? { appointmentTime: this.appointmentTime } : {}),
199
- ...(this.providerName ? { providerName: this.providerName.trim() } : {}),
200
- ...(this.customScript ? { customScript: this.customScript.trim() } : {}),
201
- ...(patient?.patientId ? { patientId: patient.patientId } : {}),
202
- ...(apptId ? { appointmentId: apptId } : {}),
203
- };
204
- this.api.placeOutboundCall(body).subscribe({
205
- next: (res) => {
206
- this.submitting.set(false);
207
- const msg = res.message ?? 'Outbound call started.';
208
- this.toast.show(msg, 'success', 5000);
209
- this.dialed.emit();
210
- this.close();
211
- },
212
- error: (err) => {
213
- this.submitting.set(false);
214
- const msg = err?.message ?? 'Failed to place outbound call.';
215
- this.serverError.set(msg);
216
- this.toast.show(msg, 'error', 5000);
217
- },
218
- });
219
- }
220
- preferAppointmentPurpose() {
221
- const current = this.selectedPurpose;
222
- const needsAppt = !!current &&
223
- (current.requiredFields.includes('appointmentDate') ||
224
- current.requiredFields.includes('appointmentTime'));
225
- if (needsAppt)
226
- return;
227
- const confirm = this.purposes().find((p) => p.isActive &&
228
- p.requiredFields.includes('appointmentDate') &&
229
- p.requiredFields.includes('appointmentTime')) ??
230
- this.purposes().find((p) => p.isActive && /confirm/i.test(p.label + p.purposeId));
231
- if (confirm) {
232
- this.purposeId = confirm.purposeId;
233
- }
234
- }
235
- applyAppointmentFields(appt) {
236
- const start = appt.startTime ? new Date(appt.startTime) : null;
237
- if (start && !Number.isNaN(start.getTime())) {
238
- const y = start.getFullYear();
239
- const m = String(start.getMonth() + 1).padStart(2, '0');
240
- const d = String(start.getDate()).padStart(2, '0');
241
- const hh = String(start.getHours()).padStart(2, '0');
242
- const mm = String(start.getMinutes()).padStart(2, '0');
243
- this.appointmentDate = `${y}-${m}-${d}`;
244
- this.appointmentTime = `${hh}:${mm}`;
245
- }
246
- if (appt.providerName) {
247
- this.providerName = appt.providerName;
248
- }
249
- }
250
- clearApptDerivedFields() {
251
- this.appointmentDate = '';
252
- this.appointmentTime = '';
253
- this.providerName = '';
254
- }
255
- loadAppointments(patientId) {
256
- this.loadingAppointments.set(true);
257
- this.appointments.set([]);
258
- this.api.listPatientAppointmentsForOutbound(patientId, 20).subscribe({
259
- next: (rows) => {
260
- this.appointments.set(rows);
261
- this.loadingAppointments.set(false);
262
- if (rows.length === 1) {
263
- this.selectAppointment(rows[0]);
264
- }
265
- },
266
- error: () => {
267
- this.loadingAppointments.set(false);
268
- this.appointments.set([]);
269
- },
270
- });
271
- }
272
- loadPracticeName() {
273
- this.api.getSettings().subscribe({
274
- next: ({ data }) => {
275
- const name = String(data.general?.['practiceName'] ?? '').trim();
276
- this.practiceName.set(name || 'our office');
277
- },
278
- error: () => this.practiceName.set('our office'),
279
- });
280
- }
281
- resetForm() {
282
- this.patientQuery = '';
283
- this.phone = '';
284
- this.name = '';
285
- this.purposeId = '';
286
- this.appointmentDate = '';
287
- this.appointmentTime = '';
288
- this.providerName = '';
289
- this.customScript = '';
290
- this.patientHits.set([]);
291
- this.selectedPatient.set(null);
292
- this.appointments.set([]);
293
- this.selectedAppointmentId.set(null);
294
- this.searchingPatients.set(false);
295
- this.loadingAppointments.set(false);
296
- this.showManualEntry.set(false);
297
- this.serverError.set(null);
298
- this.submitting.set(false);
299
- this.formEpoch.set(0);
300
- }
301
- loadPurposes() {
302
- this.loading.set(true);
303
- this.api.getOutboundPurposes(false).subscribe({
304
- next: ({ purposes }) => {
305
- this.purposes.set(purposes);
306
- if (purposes.length && !this.purposeId) {
307
- this.purposeId = purposes[0].purposeId;
308
- }
309
- this.loading.set(false);
310
- },
311
- error: () => {
312
- this.loading.set(false);
313
- this.serverError.set('Failed to load outbound purposes.');
314
- },
315
- });
316
- }
317
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: OutboundDialModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
318
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: OutboundDialModalComponent, isStandalone: true, selector: "eds-outbound-dial-modal", inputs: { open: "open" }, outputs: { closed: "closed", dialed: "dialed" }, usesOnChanges: true, ngImport: i0, template: "@if (open) {\n <app-modal\n title=\"Outbound call\"\n subtitle=\"Search a patient, pick an appointment, then dial\"\n maxWidth=\"600px\"\n (closed)=\"close()\"\n >\n @if (loading()) {\n <p class=\"eds-body-sm\">Loading purposes\u2026</p>\n } @else {\n <form class=\"dial-form\" (ngSubmit)=\"submit()\" novalidate autocomplete=\"off\">\n <div class=\"dial-form__field dial-form__typeahead\">\n <span class=\"eds-label\">Patient <span class=\"dial-form__req\">*</span></span>\n\n @if (selectedPatient(); as patient) {\n <div class=\"dial-form__chip\">\n <div class=\"dial-form__chip-main\">\n <strong>{{ patient.fullName }}</strong>\n <span class=\"dial-form__chip-meta\">\n @if (patient.phone) {\n {{ patient.phone }}\n }\n @if (patient.dob) {\n \u00B7 DOB {{ patient.dob }}\n }\n </span>\n </div>\n <button type=\"button\" class=\"dial-form__chip-clear\" (click)=\"clearPatient()\">\n Change\n </button>\n </div>\n } @else {\n <input\n type=\"search\"\n class=\"dial-form__input\"\n [ngModel]=\"patientQuery\"\n (ngModelChange)=\"onPatientQueryChange($event)\"\n name=\"patientQuery\"\n placeholder=\"Search by name or phone\"\n autocomplete=\"off\"\n />\n @if (searchingPatients()) {\n <span class=\"dial-form__hint\">Searching\u2026</span>\n }\n @if (patientHits().length) {\n <ul class=\"dial-form__hits\" role=\"listbox\">\n @for (hit of patientHits(); track hit.patientId) {\n <li>\n <button\n type=\"button\"\n class=\"dial-form__hit\"\n role=\"option\"\n (click)=\"selectPatient(hit)\"\n >\n <span class=\"dial-form__hit-name\">{{ hit.fullName }}</span>\n <span class=\"dial-form__hit-meta\">\n {{ hit.phone || 'No phone' }}\n @if (hit.dob) {\n \u00B7 {{ hit.dob }}\n }\n </span>\n </button>\n </li>\n }\n </ul>\n }\n }\n </div>\n\n @if (selectedPatient()) {\n <div class=\"dial-form__field\">\n <span class=\"eds-label\">Appointment</span>\n @if (loadingAppointments()) {\n <span class=\"dial-form__hint\">Loading appointments\u2026</span>\n } @else if (!appointments().length) {\n <span class=\"dial-form__hint\">No upcoming appointments \u2014 dial without one, or enter details below.</span>\n } @else {\n <ul class=\"dial-form__appts\">\n <li>\n <label class=\"dial-form__appt\">\n <input\n type=\"radio\"\n name=\"appointmentId\"\n [checked]=\"!selectedAppointmentId()\"\n (change)=\"selectAppointment(null)\"\n autocomplete=\"off\"\n />\n <span>No appointment (purpose-only)</span>\n </label>\n </li>\n @for (appt of appointments(); track appt.appointmentId) {\n <li>\n <label class=\"dial-form__appt\">\n <input\n type=\"radio\"\n name=\"appointmentId\"\n [checked]=\"selectedAppointmentId() === appt.appointmentId\"\n (change)=\"selectAppointment(appt)\"\n autocomplete=\"off\"\n />\n <span class=\"dial-form__appt-body\">\n <strong>\n {{ appt.startTime ? (appt.startTime | date: 'medium') : 'TBD' }}\n </strong>\n <span class=\"dial-form__hit-meta\">\n {{ appt.providerName || 'Provider TBD' }}\n @if (appt.locationName) {\n \u00B7 {{ appt.locationName }}\n }\n @if (appt.visitType) {\n \u00B7 {{ appt.visitType }}\n }\n @if (appt.status) {\n \u00B7 {{ appt.status }}\n }\n </span>\n </span>\n </label>\n </li>\n }\n </ul>\n }\n </div>\n }\n\n <button\n type=\"button\"\n class=\"dial-form__toggle\"\n (click)=\"showManualEntry.set(!showManualEntry())\"\n >\n {{ showManualEntry() ? 'Hide' : 'Show' }} phone / name\n @if (!selectedPatient()) {\n (required if no patient selected)\n }\n </button>\n\n @if (showManualEntry() || !selectedPatient()) {\n <label class=\"dial-form__field\">\n <span class=\"eds-label\">Phone <span class=\"dial-form__req\">*</span></span>\n <input\n type=\"tel\"\n class=\"dial-form__input\"\n [(ngModel)]=\"phone\"\n (ngModelChange)=\"touchForm()\"\n name=\"phone\"\n placeholder=\"+14155550100\"\n autocomplete=\"off\"\n required\n />\n <span class=\"dial-form__hint\">E.164 format, e.g. +14155550100</span>\n </label>\n\n <label class=\"dial-form__field\">\n <span class=\"eds-label\">Patient name <span class=\"dial-form__req\">*</span></span>\n <input\n type=\"text\"\n class=\"dial-form__input\"\n [(ngModel)]=\"name\"\n (ngModelChange)=\"touchForm()\"\n name=\"name\"\n placeholder=\"Maria Garcia\"\n required\n autocomplete=\"off\"\n />\n </label>\n }\n\n <label class=\"dial-form__field\">\n <span class=\"eds-label\">Purpose <span class=\"dial-form__req\">*</span></span>\n <select\n class=\"dial-form__input\"\n [(ngModel)]=\"purposeId\"\n name=\"purposeId\"\n (ngModelChange)=\"onPurposeChange()\"\n required\n autocomplete=\"off\"\n >\n @for (p of purposes(); track p.purposeId) {\n <option [value]=\"p.purposeId\">{{ p.label }}</option>\n }\n </select>\n </label>\n\n @if (showsField('appointmentDate') && !selectedAppointmentId()) {\n <label class=\"dial-form__field\">\n <span class=\"eds-label\">{{ requiredFieldLabel('appointmentDate') }} <span class=\"dial-form__req\">*</span></span>\n <input\n type=\"date\"\n class=\"dial-form__input\"\n [(ngModel)]=\"appointmentDate\"\n (ngModelChange)=\"touchForm()\"\n name=\"appointmentDate\"\n required\n autocomplete=\"off\"\n />\n </label>\n }\n\n @if (showsField('appointmentTime') && !selectedAppointmentId()) {\n <label class=\"dial-form__field\">\n <span class=\"eds-label\">{{ requiredFieldLabel('appointmentTime') }} <span class=\"dial-form__req\">*</span></span>\n <input\n type=\"time\"\n class=\"dial-form__input\"\n [(ngModel)]=\"appointmentTime\"\n (ngModelChange)=\"touchForm()\"\n name=\"appointmentTime\"\n required\n autocomplete=\"off\"\n />\n </label>\n }\n\n @if (showsField('providerName') && !selectedAppointmentId()) {\n <label class=\"dial-form__field\">\n <span class=\"eds-label\">{{ requiredFieldLabel('providerName') }} <span class=\"dial-form__req\">*</span></span>\n <input\n type=\"text\"\n class=\"dial-form__input\"\n [(ngModel)]=\"providerName\"\n (ngModelChange)=\"touchForm()\"\n name=\"providerName\"\n placeholder=\"Dr. Patel\"\n required\n autocomplete=\"off\"\n />\n </label>\n }\n\n @if (selectedAppointmentId() && (showsField('appointmentDate') || showsField('appointmentTime') || showsField('providerName'))) {\n <p class=\"dial-form__hint\">\n Date, time, and provider are taken from the selected appointment\n @if (appointmentDate || appointmentTime || providerName) {\n ({{ appointmentDate }}\n @if (appointmentTime) {\n {{ appointmentTime }}\n }\n @if (providerName) {\n \u00B7 {{ providerName }}\n })\n }.\n </p>\n }\n\n @if (showsCustomScript()) {\n <label class=\"dial-form__field\">\n <span class=\"eds-label\">\n Custom script\n @if (customScriptRequired()) {\n <span class=\"dial-form__req\">*</span>\n }\n </span>\n <textarea\n class=\"dial-form__textarea\"\n rows=\"4\"\n [(ngModel)]=\"customScript\"\n (ngModelChange)=\"touchForm()\"\n name=\"customScript\"\n [required]=\"customScriptRequired()\"\n placeholder=\"What should the agent say or do?\"\n autocomplete=\"off\"\n ></textarea>\n </label>\n }\n\n <section class=\"dial-form__preview eds-card\">\n <h3 class=\"eds-label\">Opening preview</h3>\n <p class=\"dial-form__preview-text\">{{ previewText }}</p>\n @if (selectedAppointmentId()) {\n <p class=\"dial-form__hint\">\n Filled from the selected appointment and purpose template. Switch purpose to change the script.\n </p>\n } @else {\n <p class=\"dial-form__hint\">\n Select an appointment to fill date, time, and provider into this message.\n </p>\n }\n </section>\n\n @if (serverError()) {\n <p class=\"dial-form__error\" role=\"alert\">{{ serverError() }}</p>\n }\n\n <footer class=\"dial-form__footer\">\n <eds-button variant=\"secondary\" type=\"button\" (click)=\"close()\" [disabled]=\"submitting()\">\n Cancel\n </eds-button>\n <eds-button variant=\"primary\" type=\"submit\" [disabled]=\"submitting() || !canPlaceCall()\">\n {{ submitting() ? 'Dialing\u2026' : 'Place call' }}\n </eds-button>\n </footer>\n </form>\n }\n </app-modal>\n}\n", styles: [".dial-form{display:flex;flex-direction:column;gap:12px}.dial-form__field{display:flex;flex-direction:column;gap:6px}.dial-form__typeahead{position:relative}.dial-form__input,.dial-form__textarea{padding:8px 10px;border:1px solid var(--eds-border-subtle, #e4e4e7);border-radius:8px;font:inherit}.dial-form__textarea{resize:vertical}.dial-form__hint{font-size:12px;color:var(--eds-text-secondary)}.dial-form__req{color:#dc2626}.dial-form__chip{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 12px;border:1px solid var(--eds-border-subtle, #e4e4e7);border-radius:8px;background:var(--eds-surface-muted, #f8fafc)}.dial-form__chip-main{display:flex;flex-direction:column;gap:2px;min-width:0}.dial-form__chip-meta,.dial-form__hit-meta{font-size:12px;color:var(--eds-text-secondary)}.dial-form__chip-clear{flex-shrink:0;border:none;background:transparent;color:var(--eds-primary, #2563eb);font:inherit;font-size:13px;cursor:pointer;padding:4px}.dial-form__hits,.dial-form__appts{list-style:none;margin:0;padding:0;border:1px solid var(--eds-border-subtle, #e4e4e7);border-radius:8px;overflow:hidden;max-height:220px;overflow-y:auto}.dial-form__hits{position:absolute;z-index:5;left:0;right:0;top:calc(100% + 4px);background:var(--eds-surface, #fff);box-shadow:0 8px 24px #0f172a1f}.dial-form__hit{display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:100%;padding:10px 12px;border:none;border-bottom:1px solid var(--eds-border-subtle, #f1f5f9);background:transparent;text-align:left;font:inherit;cursor:pointer}.dial-form__hit:hover{background:var(--eds-surface-muted, #f8fafc)}.dial-form__hit-name{font-weight:600}.dial-form__appt{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border-bottom:1px solid var(--eds-border-subtle, #f1f5f9);cursor:pointer}.dial-form__appt:last-child,.dial-form__hit:last-child{border-bottom:none}.dial-form__appt-body{display:flex;flex-direction:column;gap:2px}.dial-form__toggle{align-self:flex-start;border:none;background:transparent;color:var(--eds-primary, #2563eb);font:inherit;font-size:13px;cursor:pointer;padding:0}.dial-form__preview{padding:12px;background:var(--eds-surface-muted, #f8fafc)}.dial-form__preview-text{margin:8px 0 0;font-size:14px;line-height:1.5;white-space:pre-wrap}.dial-form__error{color:#b91c1c;font-size:13px;margin:0}.dial-form__footer{display:flex;justify-content:flex-end;gap:8px;margin-top:4px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: ModalComponent, selector: "app-modal", inputs: ["title", "subtitle", "maxWidth"], outputs: ["closed"] }, { kind: "component", type: EdsButtonComponent, selector: "eds-button", inputs: ["variant", "type", "disabled"] }, { kind: "pipe", type: DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
319
- }
320
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: OutboundDialModalComponent, decorators: [{
321
- type: Component,
322
- args: [{ selector: 'eds-outbound-dial-modal', imports: [FormsModule, ModalComponent, EdsButtonComponent, DatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (open) {\n <app-modal\n title=\"Outbound call\"\n subtitle=\"Search a patient, pick an appointment, then dial\"\n maxWidth=\"600px\"\n (closed)=\"close()\"\n >\n @if (loading()) {\n <p class=\"eds-body-sm\">Loading purposes\u2026</p>\n } @else {\n <form class=\"dial-form\" (ngSubmit)=\"submit()\" novalidate autocomplete=\"off\">\n <div class=\"dial-form__field dial-form__typeahead\">\n <span class=\"eds-label\">Patient <span class=\"dial-form__req\">*</span></span>\n\n @if (selectedPatient(); as patient) {\n <div class=\"dial-form__chip\">\n <div class=\"dial-form__chip-main\">\n <strong>{{ patient.fullName }}</strong>\n <span class=\"dial-form__chip-meta\">\n @if (patient.phone) {\n {{ patient.phone }}\n }\n @if (patient.dob) {\n \u00B7 DOB {{ patient.dob }}\n }\n </span>\n </div>\n <button type=\"button\" class=\"dial-form__chip-clear\" (click)=\"clearPatient()\">\n Change\n </button>\n </div>\n } @else {\n <input\n type=\"search\"\n class=\"dial-form__input\"\n [ngModel]=\"patientQuery\"\n (ngModelChange)=\"onPatientQueryChange($event)\"\n name=\"patientQuery\"\n placeholder=\"Search by name or phone\"\n autocomplete=\"off\"\n />\n @if (searchingPatients()) {\n <span class=\"dial-form__hint\">Searching\u2026</span>\n }\n @if (patientHits().length) {\n <ul class=\"dial-form__hits\" role=\"listbox\">\n @for (hit of patientHits(); track hit.patientId) {\n <li>\n <button\n type=\"button\"\n class=\"dial-form__hit\"\n role=\"option\"\n (click)=\"selectPatient(hit)\"\n >\n <span class=\"dial-form__hit-name\">{{ hit.fullName }}</span>\n <span class=\"dial-form__hit-meta\">\n {{ hit.phone || 'No phone' }}\n @if (hit.dob) {\n \u00B7 {{ hit.dob }}\n }\n </span>\n </button>\n </li>\n }\n </ul>\n }\n }\n </div>\n\n @if (selectedPatient()) {\n <div class=\"dial-form__field\">\n <span class=\"eds-label\">Appointment</span>\n @if (loadingAppointments()) {\n <span class=\"dial-form__hint\">Loading appointments\u2026</span>\n } @else if (!appointments().length) {\n <span class=\"dial-form__hint\">No upcoming appointments \u2014 dial without one, or enter details below.</span>\n } @else {\n <ul class=\"dial-form__appts\">\n <li>\n <label class=\"dial-form__appt\">\n <input\n type=\"radio\"\n name=\"appointmentId\"\n [checked]=\"!selectedAppointmentId()\"\n (change)=\"selectAppointment(null)\"\n autocomplete=\"off\"\n />\n <span>No appointment (purpose-only)</span>\n </label>\n </li>\n @for (appt of appointments(); track appt.appointmentId) {\n <li>\n <label class=\"dial-form__appt\">\n <input\n type=\"radio\"\n name=\"appointmentId\"\n [checked]=\"selectedAppointmentId() === appt.appointmentId\"\n (change)=\"selectAppointment(appt)\"\n autocomplete=\"off\"\n />\n <span class=\"dial-form__appt-body\">\n <strong>\n {{ appt.startTime ? (appt.startTime | date: 'medium') : 'TBD' }}\n </strong>\n <span class=\"dial-form__hit-meta\">\n {{ appt.providerName || 'Provider TBD' }}\n @if (appt.locationName) {\n \u00B7 {{ appt.locationName }}\n }\n @if (appt.visitType) {\n \u00B7 {{ appt.visitType }}\n }\n @if (appt.status) {\n \u00B7 {{ appt.status }}\n }\n </span>\n </span>\n </label>\n </li>\n }\n </ul>\n }\n </div>\n }\n\n <button\n type=\"button\"\n class=\"dial-form__toggle\"\n (click)=\"showManualEntry.set(!showManualEntry())\"\n >\n {{ showManualEntry() ? 'Hide' : 'Show' }} phone / name\n @if (!selectedPatient()) {\n (required if no patient selected)\n }\n </button>\n\n @if (showManualEntry() || !selectedPatient()) {\n <label class=\"dial-form__field\">\n <span class=\"eds-label\">Phone <span class=\"dial-form__req\">*</span></span>\n <input\n type=\"tel\"\n class=\"dial-form__input\"\n [(ngModel)]=\"phone\"\n (ngModelChange)=\"touchForm()\"\n name=\"phone\"\n placeholder=\"+14155550100\"\n autocomplete=\"off\"\n required\n />\n <span class=\"dial-form__hint\">E.164 format, e.g. +14155550100</span>\n </label>\n\n <label class=\"dial-form__field\">\n <span class=\"eds-label\">Patient name <span class=\"dial-form__req\">*</span></span>\n <input\n type=\"text\"\n class=\"dial-form__input\"\n [(ngModel)]=\"name\"\n (ngModelChange)=\"touchForm()\"\n name=\"name\"\n placeholder=\"Maria Garcia\"\n required\n autocomplete=\"off\"\n />\n </label>\n }\n\n <label class=\"dial-form__field\">\n <span class=\"eds-label\">Purpose <span class=\"dial-form__req\">*</span></span>\n <select\n class=\"dial-form__input\"\n [(ngModel)]=\"purposeId\"\n name=\"purposeId\"\n (ngModelChange)=\"onPurposeChange()\"\n required\n autocomplete=\"off\"\n >\n @for (p of purposes(); track p.purposeId) {\n <option [value]=\"p.purposeId\">{{ p.label }}</option>\n }\n </select>\n </label>\n\n @if (showsField('appointmentDate') && !selectedAppointmentId()) {\n <label class=\"dial-form__field\">\n <span class=\"eds-label\">{{ requiredFieldLabel('appointmentDate') }} <span class=\"dial-form__req\">*</span></span>\n <input\n type=\"date\"\n class=\"dial-form__input\"\n [(ngModel)]=\"appointmentDate\"\n (ngModelChange)=\"touchForm()\"\n name=\"appointmentDate\"\n required\n autocomplete=\"off\"\n />\n </label>\n }\n\n @if (showsField('appointmentTime') && !selectedAppointmentId()) {\n <label class=\"dial-form__field\">\n <span class=\"eds-label\">{{ requiredFieldLabel('appointmentTime') }} <span class=\"dial-form__req\">*</span></span>\n <input\n type=\"time\"\n class=\"dial-form__input\"\n [(ngModel)]=\"appointmentTime\"\n (ngModelChange)=\"touchForm()\"\n name=\"appointmentTime\"\n required\n autocomplete=\"off\"\n />\n </label>\n }\n\n @if (showsField('providerName') && !selectedAppointmentId()) {\n <label class=\"dial-form__field\">\n <span class=\"eds-label\">{{ requiredFieldLabel('providerName') }} <span class=\"dial-form__req\">*</span></span>\n <input\n type=\"text\"\n class=\"dial-form__input\"\n [(ngModel)]=\"providerName\"\n (ngModelChange)=\"touchForm()\"\n name=\"providerName\"\n placeholder=\"Dr. Patel\"\n required\n autocomplete=\"off\"\n />\n </label>\n }\n\n @if (selectedAppointmentId() && (showsField('appointmentDate') || showsField('appointmentTime') || showsField('providerName'))) {\n <p class=\"dial-form__hint\">\n Date, time, and provider are taken from the selected appointment\n @if (appointmentDate || appointmentTime || providerName) {\n ({{ appointmentDate }}\n @if (appointmentTime) {\n {{ appointmentTime }}\n }\n @if (providerName) {\n \u00B7 {{ providerName }}\n })\n }.\n </p>\n }\n\n @if (showsCustomScript()) {\n <label class=\"dial-form__field\">\n <span class=\"eds-label\">\n Custom script\n @if (customScriptRequired()) {\n <span class=\"dial-form__req\">*</span>\n }\n </span>\n <textarea\n class=\"dial-form__textarea\"\n rows=\"4\"\n [(ngModel)]=\"customScript\"\n (ngModelChange)=\"touchForm()\"\n name=\"customScript\"\n [required]=\"customScriptRequired()\"\n placeholder=\"What should the agent say or do?\"\n autocomplete=\"off\"\n ></textarea>\n </label>\n }\n\n <section class=\"dial-form__preview eds-card\">\n <h3 class=\"eds-label\">Opening preview</h3>\n <p class=\"dial-form__preview-text\">{{ previewText }}</p>\n @if (selectedAppointmentId()) {\n <p class=\"dial-form__hint\">\n Filled from the selected appointment and purpose template. Switch purpose to change the script.\n </p>\n } @else {\n <p class=\"dial-form__hint\">\n Select an appointment to fill date, time, and provider into this message.\n </p>\n }\n </section>\n\n @if (serverError()) {\n <p class=\"dial-form__error\" role=\"alert\">{{ serverError() }}</p>\n }\n\n <footer class=\"dial-form__footer\">\n <eds-button variant=\"secondary\" type=\"button\" (click)=\"close()\" [disabled]=\"submitting()\">\n Cancel\n </eds-button>\n <eds-button variant=\"primary\" type=\"submit\" [disabled]=\"submitting() || !canPlaceCall()\">\n {{ submitting() ? 'Dialing\u2026' : 'Place call' }}\n </eds-button>\n </footer>\n </form>\n }\n </app-modal>\n}\n", styles: [".dial-form{display:flex;flex-direction:column;gap:12px}.dial-form__field{display:flex;flex-direction:column;gap:6px}.dial-form__typeahead{position:relative}.dial-form__input,.dial-form__textarea{padding:8px 10px;border:1px solid var(--eds-border-subtle, #e4e4e7);border-radius:8px;font:inherit}.dial-form__textarea{resize:vertical}.dial-form__hint{font-size:12px;color:var(--eds-text-secondary)}.dial-form__req{color:#dc2626}.dial-form__chip{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 12px;border:1px solid var(--eds-border-subtle, #e4e4e7);border-radius:8px;background:var(--eds-surface-muted, #f8fafc)}.dial-form__chip-main{display:flex;flex-direction:column;gap:2px;min-width:0}.dial-form__chip-meta,.dial-form__hit-meta{font-size:12px;color:var(--eds-text-secondary)}.dial-form__chip-clear{flex-shrink:0;border:none;background:transparent;color:var(--eds-primary, #2563eb);font:inherit;font-size:13px;cursor:pointer;padding:4px}.dial-form__hits,.dial-form__appts{list-style:none;margin:0;padding:0;border:1px solid var(--eds-border-subtle, #e4e4e7);border-radius:8px;overflow:hidden;max-height:220px;overflow-y:auto}.dial-form__hits{position:absolute;z-index:5;left:0;right:0;top:calc(100% + 4px);background:var(--eds-surface, #fff);box-shadow:0 8px 24px #0f172a1f}.dial-form__hit{display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:100%;padding:10px 12px;border:none;border-bottom:1px solid var(--eds-border-subtle, #f1f5f9);background:transparent;text-align:left;font:inherit;cursor:pointer}.dial-form__hit:hover{background:var(--eds-surface-muted, #f8fafc)}.dial-form__hit-name{font-weight:600}.dial-form__appt{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border-bottom:1px solid var(--eds-border-subtle, #f1f5f9);cursor:pointer}.dial-form__appt:last-child,.dial-form__hit:last-child{border-bottom:none}.dial-form__appt-body{display:flex;flex-direction:column;gap:2px}.dial-form__toggle{align-self:flex-start;border:none;background:transparent;color:var(--eds-primary, #2563eb);font:inherit;font-size:13px;cursor:pointer;padding:0}.dial-form__preview{padding:12px;background:var(--eds-surface-muted, #f8fafc)}.dial-form__preview-text{margin:8px 0 0;font-size:14px;line-height:1.5;white-space:pre-wrap}.dial-form__error{color:#b91c1c;font-size:13px;margin:0}.dial-form__footer{display:flex;justify-content:flex-end;gap:8px;margin-top:4px}\n"] }]
323
- }], ctorParameters: () => [], propDecorators: { open: [{
324
- type: Input
325
- }], closed: [{
326
- type: Output
327
- }], dialed: [{
328
- type: Output
329
- }] } });
330
-
331
- class DashboardPageComponent {
332
- constructor() {
333
- this.api = inject(AiReceptionistApiService);
334
- this.liveCallsList = inject(LiveCallsListService);
335
- this.router = inject(Router);
336
- this.route = inject(ActivatedRoute);
337
- this.data = signal(null);
338
- this.loading = signal(true);
339
- this.liveCalls = this.liveCallsList.calls;
340
- this.liveCallsLoading = this.liveCallsList.loading;
341
- this.liveCallsError = this.liveCallsList.error;
342
- this.outboundOpen = signal(false);
343
- this.statTiles = [
344
- { key: 'activeCalls', label: 'Live calls', icon: 'graphic_eq' },
345
- { key: 'answeredToday', label: 'Calls today', icon: 'call' },
346
- { key: 'tasksCreated', label: 'Open tasks', icon: 'task_alt' },
347
- {
348
- key: 'appointmentsBooked',
349
- label: 'Appointments booked',
350
- icon: 'event_available',
351
- },
352
- { key: 'completionRate', label: 'AI handled', icon: 'trending_up', suffix: '%' },
353
- ];
354
- this.isNeedsReview = isNeedsReview;
355
- this.callerMetaLine = callerMetaLine;
356
- this.resultChipIcon = resultChipIcon;
357
- this.historyResultChips = historyResultChips;
358
- this.historyResolutionLabel = historyResolutionLabel;
359
- }
360
- ngOnInit() {
361
- this.liveCallsList.start();
362
- this.api.getDashboard().subscribe(({ data }) => {
363
- this.data.set(data);
364
- this.loading.set(false);
365
- });
366
- }
367
- ngOnDestroy() {
368
- this.liveCallsList.stop();
369
- }
370
- statValue(key) {
371
- if (key === 'activeCalls') {
372
- return this.liveCallsList.calls().length;
373
- }
374
- return this.data()?.stats[key] ?? 0;
375
- }
376
- statusTone(status) {
377
- const normalized = status || 'ai';
378
- const map = {
379
- ai: 'brand',
380
- 'needs-help': 'notice',
381
- 'human-requested': 'violet',
382
- urgent: 'neg',
383
- takeover: 'violet',
384
- transferring: 'notice',
385
- };
386
- return map[normalized] ?? 'brand';
387
- }
388
- statusLabel(status) {
389
- return status || 'ai';
390
- }
391
- historyAvatarTone(row) {
392
- if (!row.name?.trim() || /^unknown/i.test(row.name) || row.initials === '?')
393
- return 'gray';
394
- return 'brand';
395
- }
396
- resolutionOf(row) {
397
- return historyResolution({ outcome: row.outcome, risk: row.risk });
398
- }
399
- formatDuration(sec) {
400
- const m = Math.floor(sec / 60);
401
- const s = sec % 60;
402
- return `${m}:${s.toString().padStart(2, '0')}`;
403
- }
404
- exportCsv() {
405
- // mock export
406
- }
407
- openOutboundDial() {
408
- this.outboundOpen.set(true);
409
- }
410
- closeOutboundDial() {
411
- this.outboundOpen.set(false);
412
- }
413
- openHistoryDetail(id) {
414
- void this.router.navigate(['../history', id], { relativeTo: this.route });
415
- }
416
- callDetailLink(callId) {
417
- return ['../history', callId];
418
- }
419
- appointmentDetailLink(callId) {
420
- return callId ? ['../history', callId] : ['../history'];
421
- }
422
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: DashboardPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
423
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: DashboardPageComponent, isStandalone: true, selector: "eds-dashboard-page", ngImport: i0, template: "<div class=\"eds-page dashboard-page\">\n <header class=\"dashboard-page__header\">\n <div>\n <h1 class=\"eds-h1\">Dashboard</h1>\n <p class=\"eds-body-sm\">AI receptionist overview and live activity</p>\n </div>\n <div class=\"dashboard-page__actions\">\n <eds-button variant=\"secondary\" (click)=\"exportCsv()\">\n <eds-icon name=\"download\" [size]=\"18\" /> Export\n </eds-button>\n <eds-button variant=\"primary\" (click)=\"openOutboundDial()\">\n <eds-icon name=\"call_made\" [size]=\"18\" /> New outbound call\n </eds-button>\n </div>\n </header>\n\n @if (loading()) {\n <p class=\"eds-body-sm\">Loading dashboard\u2026</p>\n } @else if (data()) {\n <section class=\"dashboard-page__stats\">\n @for (tile of statTiles; track tile.key) {\n @if (tile.key === 'appointmentsBooked') {\n <a routerLink=\"../appointments\" class=\"eds-card dashboard-page__stat dashboard-page__stat--link\">\n <eds-icon [name]=\"tile.icon\" [size]=\"22\" />\n <span class=\"eds-label\">{{ tile.label }}</span>\n <span class=\"dashboard-page__stat-value\">\n {{ statValue(tile.key) }}{{ tile.suffix ?? '' }}\n </span>\n </a>\n } @else if (tile.key === 'activeCalls') {\n <a routerLink=\"../live\" class=\"eds-card dashboard-page__stat dashboard-page__stat--link\">\n <eds-icon [name]=\"tile.icon\" [size]=\"22\" />\n <span class=\"eds-label\">{{ tile.label }}</span>\n <span class=\"dashboard-page__stat-value\">\n {{ statValue(tile.key) }}{{ tile.suffix ?? '' }}\n </span>\n </a>\n } @else {\n <article class=\"eds-card dashboard-page__stat\">\n <eds-icon [name]=\"tile.icon\" [size]=\"22\" />\n <span class=\"eds-label\">{{ tile.label }}</span>\n <span class=\"dashboard-page__stat-value\">\n {{ statValue(tile.key) }}{{ tile.suffix ?? '' }}\n </span>\n </article>\n }\n }\n </section>\n\n <section class=\"dashboard-page__grid\">\n <article class=\"eds-card dashboard-page__panel\">\n <header class=\"dashboard-page__panel-head\">\n <h2 class=\"eds-h2\">Live calls</h2>\n <eds-pulse-dot />\n </header>\n <ul class=\"dashboard-page__live-list\">\n @if (liveCallsError()) {\n <li class=\"dashboard-page__live-error eds-body-sm\" role=\"alert\">{{ liveCallsError() }}</li>\n }\n @if (liveCallsLoading() && liveCalls().length === 0) {\n <li class=\"eds-body-sm\">Loading live calls\u2026</li>\n }\n @for (call of liveCalls(); track call.id) {\n <li>\n <a [routerLink]=\"['../live', call.id]\" class=\"dashboard-page__live-row\">\n <eds-avatar [initials]=\"call.initials\" tone=\"brand\" />\n <div class=\"dashboard-page__live-meta\">\n <span class=\"eds-body-sm-bold\">{{ call.name }}</span>\n <span class=\"eds-body-sm\">{{ call.reason }}</span>\n </div>\n <span class=\"eds-mono-sm\">{{ call.started | edsElapsed }}</span>\n </a>\n </li>\n } @empty {\n @if (!liveCallsLoading()) {\n <li class=\"eds-body-sm\">No active calls right now.</li>\n }\n }\n </ul>\n </article>\n\n <article class=\"eds-card dashboard-page__panel\">\n <header class=\"dashboard-page__panel-head\">\n <h2 class=\"eds-h2\">Tasks created</h2>\n <a routerLink=\"../tasks\" class=\"eds-body-sm\">View all</a>\n </header>\n <ul class=\"dashboard-page__attention-list\">\n @for (task of data()!.latestTasks; track task.id) {\n <li>\n <a\n [routerLink]=\"task.callId ? callDetailLink(task.callId) : ['../tasks']\"\n class=\"dashboard-page__attention-row\"\n [attr.aria-label]=\"'View task: ' + task.title\"\n >\n \n <div>\n <span class=\"eds-body-sm-bold\">{{ task.title }}</span>\n <span class=\"eds-body-sm\">\n {{ task.patient }} \u00B7 {{ task.created | date: 'short' }}\n </span>\n </div>\n <eds-icon name=\"chevron_right\" [size]=\"20\" />\n </a>\n </li>\n } @empty {\n <li class=\"eds-body-sm\">No AI-created tasks yet.</li>\n }\n </ul>\n </article>\n\n <article class=\"eds-card dashboard-page__panel\">\n <header class=\"dashboard-page__panel-head\">\n <h2 class=\"eds-h2\">Appointments created</h2>\n <a routerLink=\"../appointments\" class=\"eds-body-sm\">View all</a>\n </header>\n <ul class=\"dashboard-page__attention-list\">\n @for (appointment of data()!.latestAppointments; track appointment.id) {\n <li>\n <a\n [routerLink]=\"appointmentDetailLink(appointment.callId)\"\n class=\"dashboard-page__attention-row\"\n [attr.aria-label]=\"'View appointment for ' + appointment.patientName\"\n >\n \n <div>\n <span class=\"eds-body-sm-bold\">{{ appointment.patientName }}</span>\n <span class=\"eds-body-sm\">\n {{ appointment.providerName ?? 'Provider not assigned' }}\n \u00B7 {{ appointment.startTime ? (appointment.startTime | date: 'short') : 'Time pending' }}\n </span>\n </div>\n <eds-icon name=\"chevron_right\" [size]=\"20\" />\n </a>\n </li>\n } @empty {\n <li class=\"eds-body-sm\">No AI-booked appointments yet.</li>\n }\n </ul>\n </article>\n\n </section>\n\n <article class=\"eds-card dashboard-page__history\">\n <header class=\"dashboard-page__panel-head\">\n <h2 class=\"eds-h2\">Recent Call History</h2>\n <a routerLink=\"../history\" class=\"eds-body-sm\">View all</a>\n </header>\n <div class=\"dashboard-page__history-wrap\">\n <table class=\"dashboard-page__history-table\">\n <thead>\n <tr>\n <th>Time</th>\n <th>Patient/Caller</th>\n <th></th>\n <th>Goal Timeline</th>\n <th>Handled By</th>\n <th>Resolution</th>\n <th>Result</th>\n <th>Duration</th>\n <th>Reviewed</th>\n </tr>\n </thead>\n <tbody>\n @for (row of data()!.recentHistory; track row.id) {\n <tr\n class=\"dashboard-page__history-row\"\n [class.dashboard-page__history-row--needs-review]=\"isNeedsReview(row.review)\"\n tabindex=\"0\"\n role=\"link\"\n [attr.aria-label]=\"'View call details for ' + row.name\"\n (click)=\"openHistoryDetail(row.id)\"\n (keyup.enter)=\"openHistoryDetail(row.id)\"\n >\n <td>\n <div class=\"dashboard-page__time\">\n @if (row.started) {\n <span class=\"dashboard-page__time-main\">{{ row.started | date: 'shortTime' }}</span>\n <span class=\"dashboard-page__time-sub\">{{ row.started | date: 'MMM d, y' }}</span>\n } @else {\n <span class=\"dashboard-page__time-main\">{{ row.whenLabel || '\u2014' }}</span>\n }\n </div>\n </td>\n\n <td>\n <div class=\"dashboard-page__caller\">\n <eds-avatar\n class=\"dashboard-page__avatar\"\n [initials]=\"row.initials || '?'\"\n [tone]=\"historyAvatarTone(row)\"\n />\n <div class=\"dashboard-page__caller-text\">\n <span class=\"dashboard-page__caller-name\">{{ row.name || 'Unknown Caller' }}</span>\n <span class=\"dashboard-page__caller-meta\">\n {{\n callerMetaLine({\n dob: row.dob,\n age: row.age,\n gender: row.gender,\n phone: row.phone,\n name: row.name,\n })\n }}\n </span>\n </div>\n </div>\n </td>\n\n <td>\n <span\n class=\"dashboard-page__dir\"\n [class.dashboard-page__dir--in]=\"row.direction === 'inbound'\"\n [class.dashboard-page__dir--out]=\"row.direction === 'outbound'\"\n [attr.title]=\"row.direction === 'inbound' ? 'Inbound' : 'Outbound'\"\n >\n <eds-icon\n [name]=\"row.direction === 'inbound' ? 'call_received' : 'call_made'\"\n [size]=\"16\"\n />\n </span>\n </td>\n\n <td>\n <span class=\"dashboard-page__goal\">{{ row.reason || '\u2014' }}</span>\n </td>\n\n <td>\n <span\n class=\"dashboard-page__handler\"\n [class.dashboard-page__handler--ai]=\"(row.handledBy || 'ai') === 'ai'\"\n [class.dashboard-page__handler--staff]=\"row.handledBy === 'staff'\"\n >\n {{ row.handledBy === 'staff' ? 'STAFF' : 'AI' }}\n </span>\n </td>\n\n <td>\n @if (resolutionOf(row); as resolution) {\n <span\n class=\"dashboard-page__resolution\"\n [class.dashboard-page__resolution--ok]=\"resolution === 'resolved'\"\n [class.dashboard-page__resolution--attn]=\"resolution === 'needs_attention'\"\n >\n {{ historyResolutionLabel(resolution) }}\n </span>\n }\n </td>\n\n <td>\n <div class=\"dashboard-page__results\">\n @for (chip of historyResultChips(row.results, row.outcome); track chip) {\n <span class=\"dashboard-page__result-chip\">\n <eds-icon [name]=\"resultChipIcon(chip)\" [size]=\"14\" />\n {{ chip }}\n </span>\n } @empty {\n <span class=\"dashboard-page__muted\">\u2014</span>\n }\n </div>\n </td>\n\n <td class=\"dashboard-page__duration\">\n {{ row.durationLabel ?? formatDuration(row.duration) }}\n </td>\n\n <td class=\"dashboard-page__reviewed\">\n @if (row.review === 'reviewed') {\n <span class=\"dashboard-page__reviewed-ok\" title=\"Reviewed\">\n <eds-icon name=\"check_circle\" [size]=\"20\" />\n </span>\n }\n </td>\n </tr>\n } @empty {\n <tr>\n <td colspan=\"9\" class=\"dashboard-page__muted\">No recent calls.</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n </article>\n }\n</div>\n\n<eds-outbound-dial-modal\n [open]=\"outboundOpen()\"\n (closed)=\"closeOutboundDial()\"\n (dialed)=\"closeOutboundDial()\"\n/>\n", styles: [".dashboard-page__header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:24px}.dashboard-page__actions{display:flex;gap:8px;flex-shrink:0}.dashboard-page__stats{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin-bottom:20px}.dashboard-page__stat{display:flex;flex-direction:column;gap:6px;padding:16px}.dashboard-page__stat--link{text-decoration:none;color:inherit;cursor:pointer;transition:border-color .15s ease}.dashboard-page__stat--link:hover{border-color:var(--eds-text-brand)}.dashboard-page__review-queue{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 20px;margin-bottom:20px;border-left:4px solid #f59e0b}.dashboard-page__review-link{white-space:nowrap;font-weight:600;text-decoration:none;color:var(--eds-text-brand)}.dashboard-page__row--needs-review{border-left:3px solid #f59e0b}.dashboard-page__history-wrap{overflow-x:auto}.dashboard-page__history-table{width:100%;border-collapse:collapse;min-width:980px}.dashboard-page__history-table thead th{text-align:left;padding:12px 14px;font-family:var(--eds-font-inter, \"Inter\", sans-serif);font-size:13px;font-weight:600;letter-spacing:normal;text-transform:none;color:var(--eds-text-table-header, #4d5573);background:#f4f4f5;border-bottom:1px solid var(--eds-border-default, #e4e4e7);white-space:nowrap}.dashboard-page__history-table tbody td{padding:14px;vertical-align:middle;border-bottom:1px solid #f1f5f9;font-family:var(--eds-font-inter, \"Inter\", sans-serif);font-size:13px;font-weight:400;color:var(--eds-text-primary, #18181b)}.dashboard-page__history-row{cursor:pointer}.dashboard-page__history-row:hover td{background:#fafafa}.dashboard-page__history-row:focus-visible{outline:2px solid var(--eds-brand-500, #2563eb);outline-offset:-2px}.dashboard-page__history-row--needs-review td:first-child{box-shadow:inset 3px 0 #f59e0b}.dashboard-page__time{display:flex;flex-direction:column;gap:2px;white-space:nowrap}.dashboard-page__time-main{font-weight:600;font-size:13px}.dashboard-page__time-sub{font-size:12px;color:var(--eds-text-secondary, #71717a)}.dashboard-page__caller{display:flex;align-items:center;gap:10px;min-width:200px}.dashboard-page__avatar ::ng-deep .eds-avatar{width:36px;height:36px;border-radius:10px;font-size:12px}.dashboard-page__caller-text{display:flex;flex-direction:column;gap:2px;min-width:0}.dashboard-page__caller-name{font-weight:600;font-size:13px;color:#18181b}.dashboard-page__caller-meta{font-size:12px;color:var(--eds-text-secondary, #71717a);white-space:nowrap}.dashboard-page__dir{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:8px}.dashboard-page__dir--in{background:#eff6ff;color:#2563eb}.dashboard-page__dir--out{background:#f5f3ff;color:#7c3aed}.dashboard-page__goal{font-size:13px;color:#3f3f46}.dashboard-page__handler{display:inline-flex;align-items:center;padding:3px 8px;border-radius:6px;font-size:12px;font-weight:500}.dashboard-page__handler--ai{background:#f5f3ff;color:#7c3aed}.dashboard-page__handler--staff{background:#eff6ff;color:#2563eb}.dashboard-page__resolution{display:inline-flex;align-items:center;padding:4px 10px;border-radius:999px;font-size:12px;font-weight:500;white-space:nowrap}.dashboard-page__resolution--ok{background:#ecfdf5;color:#047857}.dashboard-page__resolution--attn{background:#fff7ed;color:#c2410c}.dashboard-page__results{display:flex;flex-wrap:wrap;gap:6px;max-width:260px}.dashboard-page__result-chip{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;border-radius:6px;background:#f4f4f5;color:#52525b;font-size:11px;font-weight:500;white-space:nowrap}.dashboard-page__duration{font-variant-numeric:tabular-nums;font-size:13px;color:#3f3f46;white-space:nowrap}.dashboard-page__reviewed{text-align:center;width:56px}.dashboard-page__reviewed-ok{display:inline-flex;color:#16a34a}.dashboard-page__muted{color:var(--eds-text-secondary, #71717a);font-size:13px}.dashboard-page__attention-row{display:grid;grid-template-columns:auto 1fr auto;gap:10px;align-items:center;padding:8px;border-radius:var(--eds-radius-md);text-decoration:none;color:inherit;border-bottom:1px solid var(--eds-border-default)}.dashboard-page__attention-row:hover{background:var(--eds-surface-l1)}.dashboard-page__attention-row:last-child{border-bottom:none}.dashboard-page__stat-value{font-size:28px;font-weight:600;color:var(--eds-text-primary);letter-spacing:-.02em}.dashboard-page__grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-bottom:20px}.dashboard-page__panel{padding:16px}.dashboard-page__panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px}.dashboard-page__live-list,.dashboard-page__attention-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.dashboard-page__live-row{display:grid;grid-template-columns:auto 1fr auto auto;align-items:center;gap:10px;padding:8px;border-radius:var(--eds-radius-md);text-decoration:none;color:inherit}.dashboard-page__live-row:hover{background:var(--eds-surface-l1)}.dashboard-page__live-meta{display:flex;flex-direction:column;min-width:0}.dashboard-page__history{padding:16px;overflow-x:auto}@media(max-width:1200px){.dashboard-page__stats{grid-template-columns:repeat(3,1fr)}}@media(max-width:900px){.dashboard-page__stats{grid-template-columns:repeat(2,1fr)}.dashboard-page__grid{grid-template-columns:1fr}}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "pipe", type: DatePipe, name: "date" }, { kind: "component", type: EdsAvatarComponent, selector: "eds-avatar", inputs: ["initials", "tone", "shape", "gradient"] }, { kind: "component", type: EdsButtonComponent, selector: "eds-button", inputs: ["variant", "type", "disabled"] }, { kind: "component", type: EdsIconComponent, selector: "eds-icon", inputs: ["name", "size", "fill", "weight"] }, { kind: "component", type: EdsPulseDotComponent, selector: "eds-pulse-dot" }, { kind: "pipe", type: EdsElapsedPipe, name: "edsElapsed" }, { kind: "component", type: OutboundDialModalComponent, selector: "eds-outbound-dial-modal", inputs: ["open"], outputs: ["closed", "dialed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
424
- }
425
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: DashboardPageComponent, decorators: [{
426
- type: Component,
427
- args: [{ selector: 'eds-dashboard-page', imports: [
428
- RouterLink,
429
- DatePipe,
430
- EdsAvatarComponent,
431
- EdsButtonComponent,
432
- EdsIconComponent,
433
- EdsPulseDotComponent,
434
- EdsElapsedPipe,
435
- OutboundDialModalComponent,
436
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"eds-page dashboard-page\">\n <header class=\"dashboard-page__header\">\n <div>\n <h1 class=\"eds-h1\">Dashboard</h1>\n <p class=\"eds-body-sm\">AI receptionist overview and live activity</p>\n </div>\n <div class=\"dashboard-page__actions\">\n <eds-button variant=\"secondary\" (click)=\"exportCsv()\">\n <eds-icon name=\"download\" [size]=\"18\" /> Export\n </eds-button>\n <eds-button variant=\"primary\" (click)=\"openOutboundDial()\">\n <eds-icon name=\"call_made\" [size]=\"18\" /> New outbound call\n </eds-button>\n </div>\n </header>\n\n @if (loading()) {\n <p class=\"eds-body-sm\">Loading dashboard\u2026</p>\n } @else if (data()) {\n <section class=\"dashboard-page__stats\">\n @for (tile of statTiles; track tile.key) {\n @if (tile.key === 'appointmentsBooked') {\n <a routerLink=\"../appointments\" class=\"eds-card dashboard-page__stat dashboard-page__stat--link\">\n <eds-icon [name]=\"tile.icon\" [size]=\"22\" />\n <span class=\"eds-label\">{{ tile.label }}</span>\n <span class=\"dashboard-page__stat-value\">\n {{ statValue(tile.key) }}{{ tile.suffix ?? '' }}\n </span>\n </a>\n } @else if (tile.key === 'activeCalls') {\n <a routerLink=\"../live\" class=\"eds-card dashboard-page__stat dashboard-page__stat--link\">\n <eds-icon [name]=\"tile.icon\" [size]=\"22\" />\n <span class=\"eds-label\">{{ tile.label }}</span>\n <span class=\"dashboard-page__stat-value\">\n {{ statValue(tile.key) }}{{ tile.suffix ?? '' }}\n </span>\n </a>\n } @else {\n <article class=\"eds-card dashboard-page__stat\">\n <eds-icon [name]=\"tile.icon\" [size]=\"22\" />\n <span class=\"eds-label\">{{ tile.label }}</span>\n <span class=\"dashboard-page__stat-value\">\n {{ statValue(tile.key) }}{{ tile.suffix ?? '' }}\n </span>\n </article>\n }\n }\n </section>\n\n <section class=\"dashboard-page__grid\">\n <article class=\"eds-card dashboard-page__panel\">\n <header class=\"dashboard-page__panel-head\">\n <h2 class=\"eds-h2\">Live calls</h2>\n <eds-pulse-dot />\n </header>\n <ul class=\"dashboard-page__live-list\">\n @if (liveCallsError()) {\n <li class=\"dashboard-page__live-error eds-body-sm\" role=\"alert\">{{ liveCallsError() }}</li>\n }\n @if (liveCallsLoading() && liveCalls().length === 0) {\n <li class=\"eds-body-sm\">Loading live calls\u2026</li>\n }\n @for (call of liveCalls(); track call.id) {\n <li>\n <a [routerLink]=\"['../live', call.id]\" class=\"dashboard-page__live-row\">\n <eds-avatar [initials]=\"call.initials\" tone=\"brand\" />\n <div class=\"dashboard-page__live-meta\">\n <span class=\"eds-body-sm-bold\">{{ call.name }}</span>\n <span class=\"eds-body-sm\">{{ call.reason }}</span>\n </div>\n <span class=\"eds-mono-sm\">{{ call.started | edsElapsed }}</span>\n </a>\n </li>\n } @empty {\n @if (!liveCallsLoading()) {\n <li class=\"eds-body-sm\">No active calls right now.</li>\n }\n }\n </ul>\n </article>\n\n <article class=\"eds-card dashboard-page__panel\">\n <header class=\"dashboard-page__panel-head\">\n <h2 class=\"eds-h2\">Tasks created</h2>\n <a routerLink=\"../tasks\" class=\"eds-body-sm\">View all</a>\n </header>\n <ul class=\"dashboard-page__attention-list\">\n @for (task of data()!.latestTasks; track task.id) {\n <li>\n <a\n [routerLink]=\"task.callId ? callDetailLink(task.callId) : ['../tasks']\"\n class=\"dashboard-page__attention-row\"\n [attr.aria-label]=\"'View task: ' + task.title\"\n >\n \n <div>\n <span class=\"eds-body-sm-bold\">{{ task.title }}</span>\n <span class=\"eds-body-sm\">\n {{ task.patient }} \u00B7 {{ task.created | date: 'short' }}\n </span>\n </div>\n <eds-icon name=\"chevron_right\" [size]=\"20\" />\n </a>\n </li>\n } @empty {\n <li class=\"eds-body-sm\">No AI-created tasks yet.</li>\n }\n </ul>\n </article>\n\n <article class=\"eds-card dashboard-page__panel\">\n <header class=\"dashboard-page__panel-head\">\n <h2 class=\"eds-h2\">Appointments created</h2>\n <a routerLink=\"../appointments\" class=\"eds-body-sm\">View all</a>\n </header>\n <ul class=\"dashboard-page__attention-list\">\n @for (appointment of data()!.latestAppointments; track appointment.id) {\n <li>\n <a\n [routerLink]=\"appointmentDetailLink(appointment.callId)\"\n class=\"dashboard-page__attention-row\"\n [attr.aria-label]=\"'View appointment for ' + appointment.patientName\"\n >\n \n <div>\n <span class=\"eds-body-sm-bold\">{{ appointment.patientName }}</span>\n <span class=\"eds-body-sm\">\n {{ appointment.providerName ?? 'Provider not assigned' }}\n \u00B7 {{ appointment.startTime ? (appointment.startTime | date: 'short') : 'Time pending' }}\n </span>\n </div>\n <eds-icon name=\"chevron_right\" [size]=\"20\" />\n </a>\n </li>\n } @empty {\n <li class=\"eds-body-sm\">No AI-booked appointments yet.</li>\n }\n </ul>\n </article>\n\n </section>\n\n <article class=\"eds-card dashboard-page__history\">\n <header class=\"dashboard-page__panel-head\">\n <h2 class=\"eds-h2\">Recent Call History</h2>\n <a routerLink=\"../history\" class=\"eds-body-sm\">View all</a>\n </header>\n <div class=\"dashboard-page__history-wrap\">\n <table class=\"dashboard-page__history-table\">\n <thead>\n <tr>\n <th>Time</th>\n <th>Patient/Caller</th>\n <th></th>\n <th>Goal Timeline</th>\n <th>Handled By</th>\n <th>Resolution</th>\n <th>Result</th>\n <th>Duration</th>\n <th>Reviewed</th>\n </tr>\n </thead>\n <tbody>\n @for (row of data()!.recentHistory; track row.id) {\n <tr\n class=\"dashboard-page__history-row\"\n [class.dashboard-page__history-row--needs-review]=\"isNeedsReview(row.review)\"\n tabindex=\"0\"\n role=\"link\"\n [attr.aria-label]=\"'View call details for ' + row.name\"\n (click)=\"openHistoryDetail(row.id)\"\n (keyup.enter)=\"openHistoryDetail(row.id)\"\n >\n <td>\n <div class=\"dashboard-page__time\">\n @if (row.started) {\n <span class=\"dashboard-page__time-main\">{{ row.started | date: 'shortTime' }}</span>\n <span class=\"dashboard-page__time-sub\">{{ row.started | date: 'MMM d, y' }}</span>\n } @else {\n <span class=\"dashboard-page__time-main\">{{ row.whenLabel || '\u2014' }}</span>\n }\n </div>\n </td>\n\n <td>\n <div class=\"dashboard-page__caller\">\n <eds-avatar\n class=\"dashboard-page__avatar\"\n [initials]=\"row.initials || '?'\"\n [tone]=\"historyAvatarTone(row)\"\n />\n <div class=\"dashboard-page__caller-text\">\n <span class=\"dashboard-page__caller-name\">{{ row.name || 'Unknown Caller' }}</span>\n <span class=\"dashboard-page__caller-meta\">\n {{\n callerMetaLine({\n dob: row.dob,\n age: row.age,\n gender: row.gender,\n phone: row.phone,\n name: row.name,\n })\n }}\n </span>\n </div>\n </div>\n </td>\n\n <td>\n <span\n class=\"dashboard-page__dir\"\n [class.dashboard-page__dir--in]=\"row.direction === 'inbound'\"\n [class.dashboard-page__dir--out]=\"row.direction === 'outbound'\"\n [attr.title]=\"row.direction === 'inbound' ? 'Inbound' : 'Outbound'\"\n >\n <eds-icon\n [name]=\"row.direction === 'inbound' ? 'call_received' : 'call_made'\"\n [size]=\"16\"\n />\n </span>\n </td>\n\n <td>\n <span class=\"dashboard-page__goal\">{{ row.reason || '\u2014' }}</span>\n </td>\n\n <td>\n <span\n class=\"dashboard-page__handler\"\n [class.dashboard-page__handler--ai]=\"(row.handledBy || 'ai') === 'ai'\"\n [class.dashboard-page__handler--staff]=\"row.handledBy === 'staff'\"\n >\n {{ row.handledBy === 'staff' ? 'STAFF' : 'AI' }}\n </span>\n </td>\n\n <td>\n @if (resolutionOf(row); as resolution) {\n <span\n class=\"dashboard-page__resolution\"\n [class.dashboard-page__resolution--ok]=\"resolution === 'resolved'\"\n [class.dashboard-page__resolution--attn]=\"resolution === 'needs_attention'\"\n >\n {{ historyResolutionLabel(resolution) }}\n </span>\n }\n </td>\n\n <td>\n <div class=\"dashboard-page__results\">\n @for (chip of historyResultChips(row.results, row.outcome); track chip) {\n <span class=\"dashboard-page__result-chip\">\n <eds-icon [name]=\"resultChipIcon(chip)\" [size]=\"14\" />\n {{ chip }}\n </span>\n } @empty {\n <span class=\"dashboard-page__muted\">\u2014</span>\n }\n </div>\n </td>\n\n <td class=\"dashboard-page__duration\">\n {{ row.durationLabel ?? formatDuration(row.duration) }}\n </td>\n\n <td class=\"dashboard-page__reviewed\">\n @if (row.review === 'reviewed') {\n <span class=\"dashboard-page__reviewed-ok\" title=\"Reviewed\">\n <eds-icon name=\"check_circle\" [size]=\"20\" />\n </span>\n }\n </td>\n </tr>\n } @empty {\n <tr>\n <td colspan=\"9\" class=\"dashboard-page__muted\">No recent calls.</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n </article>\n }\n</div>\n\n<eds-outbound-dial-modal\n [open]=\"outboundOpen()\"\n (closed)=\"closeOutboundDial()\"\n (dialed)=\"closeOutboundDial()\"\n/>\n", styles: [".dashboard-page__header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:24px}.dashboard-page__actions{display:flex;gap:8px;flex-shrink:0}.dashboard-page__stats{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin-bottom:20px}.dashboard-page__stat{display:flex;flex-direction:column;gap:6px;padding:16px}.dashboard-page__stat--link{text-decoration:none;color:inherit;cursor:pointer;transition:border-color .15s ease}.dashboard-page__stat--link:hover{border-color:var(--eds-text-brand)}.dashboard-page__review-queue{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 20px;margin-bottom:20px;border-left:4px solid #f59e0b}.dashboard-page__review-link{white-space:nowrap;font-weight:600;text-decoration:none;color:var(--eds-text-brand)}.dashboard-page__row--needs-review{border-left:3px solid #f59e0b}.dashboard-page__history-wrap{overflow-x:auto}.dashboard-page__history-table{width:100%;border-collapse:collapse;min-width:980px}.dashboard-page__history-table thead th{text-align:left;padding:12px 14px;font-family:var(--eds-font-inter, \"Inter\", sans-serif);font-size:13px;font-weight:600;letter-spacing:normal;text-transform:none;color:var(--eds-text-table-header, #4d5573);background:#f4f4f5;border-bottom:1px solid var(--eds-border-default, #e4e4e7);white-space:nowrap}.dashboard-page__history-table tbody td{padding:14px;vertical-align:middle;border-bottom:1px solid #f1f5f9;font-family:var(--eds-font-inter, \"Inter\", sans-serif);font-size:13px;font-weight:400;color:var(--eds-text-primary, #18181b)}.dashboard-page__history-row{cursor:pointer}.dashboard-page__history-row:hover td{background:#fafafa}.dashboard-page__history-row:focus-visible{outline:2px solid var(--eds-brand-500, #2563eb);outline-offset:-2px}.dashboard-page__history-row--needs-review td:first-child{box-shadow:inset 3px 0 #f59e0b}.dashboard-page__time{display:flex;flex-direction:column;gap:2px;white-space:nowrap}.dashboard-page__time-main{font-weight:600;font-size:13px}.dashboard-page__time-sub{font-size:12px;color:var(--eds-text-secondary, #71717a)}.dashboard-page__caller{display:flex;align-items:center;gap:10px;min-width:200px}.dashboard-page__avatar ::ng-deep .eds-avatar{width:36px;height:36px;border-radius:10px;font-size:12px}.dashboard-page__caller-text{display:flex;flex-direction:column;gap:2px;min-width:0}.dashboard-page__caller-name{font-weight:600;font-size:13px;color:#18181b}.dashboard-page__caller-meta{font-size:12px;color:var(--eds-text-secondary, #71717a);white-space:nowrap}.dashboard-page__dir{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:8px}.dashboard-page__dir--in{background:#eff6ff;color:#2563eb}.dashboard-page__dir--out{background:#f5f3ff;color:#7c3aed}.dashboard-page__goal{font-size:13px;color:#3f3f46}.dashboard-page__handler{display:inline-flex;align-items:center;padding:3px 8px;border-radius:6px;font-size:12px;font-weight:500}.dashboard-page__handler--ai{background:#f5f3ff;color:#7c3aed}.dashboard-page__handler--staff{background:#eff6ff;color:#2563eb}.dashboard-page__resolution{display:inline-flex;align-items:center;padding:4px 10px;border-radius:999px;font-size:12px;font-weight:500;white-space:nowrap}.dashboard-page__resolution--ok{background:#ecfdf5;color:#047857}.dashboard-page__resolution--attn{background:#fff7ed;color:#c2410c}.dashboard-page__results{display:flex;flex-wrap:wrap;gap:6px;max-width:260px}.dashboard-page__result-chip{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;border-radius:6px;background:#f4f4f5;color:#52525b;font-size:11px;font-weight:500;white-space:nowrap}.dashboard-page__duration{font-variant-numeric:tabular-nums;font-size:13px;color:#3f3f46;white-space:nowrap}.dashboard-page__reviewed{text-align:center;width:56px}.dashboard-page__reviewed-ok{display:inline-flex;color:#16a34a}.dashboard-page__muted{color:var(--eds-text-secondary, #71717a);font-size:13px}.dashboard-page__attention-row{display:grid;grid-template-columns:auto 1fr auto;gap:10px;align-items:center;padding:8px;border-radius:var(--eds-radius-md);text-decoration:none;color:inherit;border-bottom:1px solid var(--eds-border-default)}.dashboard-page__attention-row:hover{background:var(--eds-surface-l1)}.dashboard-page__attention-row:last-child{border-bottom:none}.dashboard-page__stat-value{font-size:28px;font-weight:600;color:var(--eds-text-primary);letter-spacing:-.02em}.dashboard-page__grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-bottom:20px}.dashboard-page__panel{padding:16px}.dashboard-page__panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px}.dashboard-page__live-list,.dashboard-page__attention-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.dashboard-page__live-row{display:grid;grid-template-columns:auto 1fr auto auto;align-items:center;gap:10px;padding:8px;border-radius:var(--eds-radius-md);text-decoration:none;color:inherit}.dashboard-page__live-row:hover{background:var(--eds-surface-l1)}.dashboard-page__live-meta{display:flex;flex-direction:column;min-width:0}.dashboard-page__history{padding:16px;overflow-x:auto}@media(max-width:1200px){.dashboard-page__stats{grid-template-columns:repeat(3,1fr)}}@media(max-width:900px){.dashboard-page__stats{grid-template-columns:repeat(2,1fr)}.dashboard-page__grid{grid-template-columns:1fr}}\n"] }]
437
- }] });
438
-
439
- export { DashboardPageComponent };
440
- //# sourceMappingURL=edvak99-voice-receptionist-widget-dashboard-page.component-CxbO0eDS.mjs.map