@edvak99/voice-receptionist-widget 1.1.2 → 1.1.4

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 (132) hide show
  1. package/README.md +3 -0
  2. package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-DT9UKvQO.mjs +307 -0
  3. package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-DT9UKvQO.mjs.map +1 -0
  4. package/fesm2022/{edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-CMhknpGf.mjs → edvak99-voice-receptionist-widget-ai-receptionist-api.service-48cifVPF.mjs} +1351 -579
  5. package/fesm2022/edvak99-voice-receptionist-widget-ai-receptionist-api.service-48cifVPF.mjs.map +1 -0
  6. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-C5inEHJw.mjs +278 -0
  7. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-C5inEHJw.mjs.map +1 -0
  8. package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-sYhM20CE.mjs +418 -0
  9. package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-sYhM20CE.mjs.map +1 -0
  10. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DpwajEOE.mjs +194 -0
  11. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DpwajEOE.mjs.map +1 -0
  12. package/fesm2022/{edvak99-voice-receptionist-widget-call-recording-player.component-AN6GcSDq.mjs → edvak99-voice-receptionist-widget-call-recording-player.component-CgkRcFOl.mjs} +5 -4
  13. package/fesm2022/{edvak99-voice-receptionist-widget-call-recording-player.component-AN6GcSDq.mjs.map → edvak99-voice-receptionist-widget-call-recording-player.component-CgkRcFOl.mjs.map} +1 -1
  14. package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-Cy_Bf7AO.mjs → edvak99-voice-receptionist-widget-call-review.service-CIMcjGKX.mjs} +2 -2
  15. package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-Cy_Bf7AO.mjs.map → edvak99-voice-receptionist-widget-call-review.service-CIMcjGKX.mjs.map} +1 -1
  16. package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-Deh3jPgo.mjs +21 -0
  17. package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-Deh3jPgo.mjs.map +1 -0
  18. package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-CxbO0eDS.mjs +440 -0
  19. package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-CxbO0eDS.mjs.map +1 -0
  20. package/fesm2022/{edvak99-voice-receptionist-widget-eds-elapsed.pipe-ouRlPpwT.mjs → edvak99-voice-receptionist-widget-dashboard-socket.service-BX91r74O.mjs} +5 -21
  21. package/fesm2022/edvak99-voice-receptionist-widget-dashboard-socket.service-BX91r74O.mjs.map +1 -0
  22. package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-CWRiJGfv.mjs +38 -0
  23. package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-CWRiJGfv.mjs.map +1 -0
  24. package/fesm2022/edvak99-voice-receptionist-widget-eds-button.component-Bg5T5BN8.mjs +27 -0
  25. package/fesm2022/edvak99-voice-receptionist-widget-eds-button.component-Bg5T5BN8.mjs.map +1 -0
  26. package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-BFCiJOT3.mjs +22 -0
  27. package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-BFCiJOT3.mjs.map +1 -0
  28. package/fesm2022/{edvak99-voice-receptionist-widget-live-calls-list.service-CDtFRW8w.mjs → edvak99-voice-receptionist-widget-eds-pulse-dot.component-BPY6xN7C.mjs} +15 -5
  29. package/fesm2022/edvak99-voice-receptionist-widget-eds-pulse-dot.component-BPY6xN7C.mjs.map +1 -0
  30. package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BsBQIS_f.mjs +203 -0
  31. package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BsBQIS_f.mjs.map +1 -0
  32. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-CaSDMxpV.mjs +81 -0
  33. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-CaSDMxpV.mjs.map +1 -0
  34. package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-Cy3ND-c-.mjs +1070 -0
  35. package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-Cy3ND-c-.mjs.map +1 -0
  36. package/fesm2022/edvak99-voice-receptionist-widget-modal.component-COfi7biN.mjs +48 -0
  37. package/fesm2022/edvak99-voice-receptionist-widget-modal.component-COfi7biN.mjs.map +1 -0
  38. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-Bdh4QfFe.mjs +127 -0
  39. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-Bdh4QfFe.mjs.map +1 -0
  40. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-fHVzpiVk.mjs +268 -0
  41. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-fHVzpiVk.mjs.map +1 -0
  42. package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-Bok64X2q.mjs +968 -0
  43. package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-Bok64X2q.mjs.map +1 -0
  44. package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-Bb9lPcYi.mjs +317 -0
  45. package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-Bb9lPcYi.mjs.map +1 -0
  46. package/fesm2022/{edvak99-voice-receptionist-widget-wizard-page.component-CJLa5pia.mjs → edvak99-voice-receptionist-widget-wizard-page.component-BZVlWLWh.mjs} +6 -4
  47. package/fesm2022/{edvak99-voice-receptionist-widget-wizard-page.component-CJLa5pia.mjs.map → edvak99-voice-receptionist-widget-wizard-page.component-BZVlWLWh.mjs.map} +1 -1
  48. package/fesm2022/edvak99-voice-receptionist-widget.mjs +1 -1
  49. package/lib/components/call-conversation-panel/call-conversation-panel.component.d.ts +9 -6
  50. package/lib/components/call-tasks-panel/call-tasks-panel.component.d.ts +8 -8
  51. package/lib/components/knowledge-test-modal/knowledge-test-modal.component.d.ts +28 -0
  52. package/lib/components/live-transcript-panel/live-transcript-panel.component.d.ts +7 -4
  53. package/lib/components/outbound-dial-modal/outbound-dial-modal.component.d.ts +26 -0
  54. package/lib/layout/eds-module-nav.component.d.ts +0 -3
  55. package/lib/mocks/agent-activity.seed.d.ts +2 -0
  56. package/lib/models/agent-activity.types.d.ts +54 -0
  57. package/lib/models/ai-receptionist.types.d.ts +26 -0
  58. package/lib/models/api-dto.types.d.ts +18 -0
  59. package/lib/models/live-transcript.types.d.ts +2 -1
  60. package/lib/models/outbound-patient.types.d.ts +23 -0
  61. package/lib/models/outbound-purpose.types.d.ts +2 -0
  62. package/lib/models/pagination.d.ts +11 -0
  63. package/lib/models/staff-media.types.d.ts +36 -0
  64. package/lib/pages/activities-page/activities-page.component.d.ts +57 -0
  65. package/lib/pages/analytics-page/analytics-page.component.d.ts +38 -14
  66. package/lib/pages/call-history-detail-page/call-history-detail-page.component.d.ts +2 -3
  67. package/lib/pages/call-history-page/call-history-page.component.d.ts +16 -9
  68. package/lib/pages/dashboard-page/dashboard-page.component.d.ts +12 -8
  69. package/lib/pages/live-calls-page/live-calls-page.component.d.ts +14 -24
  70. package/lib/pages/live-monitor-page/live-monitor-page.component.d.ts +34 -1
  71. package/lib/pages/settings-page/settings-fields.config.d.ts +16 -0
  72. package/lib/pages/settings-page/settings-page.component.d.ts +48 -14
  73. package/lib/pages/tasks-page/tasks-page.component.d.ts +6 -0
  74. package/lib/services/ai-receptionist-api.mock.d.ts +44 -9
  75. package/lib/services/ai-receptionist-api.service.d.ts +65 -10
  76. package/lib/services/live-transcript.service.d.ts +1 -0
  77. package/lib/services/staff-media.service.d.ts +52 -0
  78. package/lib/ui/eds-avatar.component.d.ts +5 -2
  79. package/lib/ui/eds-button.component.d.ts +1 -1
  80. package/lib/utils/call-history-display.util.d.ts +21 -0
  81. package/lib/utils/outbound-purpose.util.d.ts +19 -0
  82. package/lib/utils/transcript-timing.util.d.ts +1 -1
  83. package/lib/utils/welcome-message.util.d.ts +1 -1
  84. package/package.json +2 -1
  85. package/styles/_index.scss +5 -40
  86. package/styles/chat-ui.scss +18 -18
  87. package/styles/eds.scss +188 -119
  88. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-CW5OKmik.mjs +0 -113
  89. package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-CW5OKmik.mjs.map +0 -1
  90. package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-DL1GfQyF.mjs +0 -35
  91. package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-DL1GfQyF.mjs.map +0 -1
  92. package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-CTwfUHUf.mjs +0 -316
  93. package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-CTwfUHUf.mjs.map +0 -1
  94. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-Cj4wxd4e.mjs +0 -138
  95. package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-Cj4wxd4e.mjs.map +0 -1
  96. package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-DQLIw7xb.mjs +0 -70
  97. package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-DQLIw7xb.mjs.map +0 -1
  98. package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-DzdIt2A7.mjs +0 -248
  99. package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-DzdIt2A7.mjs.map +0 -1
  100. package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-PCnIWeOA.mjs +0 -27
  101. package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-PCnIWeOA.mjs.map +0 -1
  102. package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-ouRlPpwT.mjs.map +0 -1
  103. package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-CMhknpGf.mjs.map +0 -1
  104. package/fesm2022/edvak99-voice-receptionist-widget-knowledge-hub.page-cZqBuull.mjs +0 -575
  105. package/fesm2022/edvak99-voice-receptionist-widget-knowledge-hub.page-cZqBuull.mjs.map +0 -1
  106. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-list.service-CDtFRW8w.mjs.map +0 -1
  107. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-DrYzXJQW.mjs +0 -115
  108. package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-DrYzXJQW.mjs.map +0 -1
  109. package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-D7dlk0y2.mjs +0 -574
  110. package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-D7dlk0y2.mjs.map +0 -1
  111. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-BQECIuJu.mjs +0 -54
  112. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-BQECIuJu.mjs.map +0 -1
  113. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-CJ8NEjcj.mjs +0 -265
  114. package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-CJ8NEjcj.mjs.map +0 -1
  115. package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-Bo3XNK3b.mjs +0 -509
  116. package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-Bo3XNK3b.mjs.map +0 -1
  117. package/fesm2022/edvak99-voice-receptionist-widget-task.util-DdtI0CR3.mjs +0 -52
  118. package/fesm2022/edvak99-voice-receptionist-widget-task.util-DdtI0CR3.mjs.map +0 -1
  119. package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-CBwl15NP.mjs +0 -237
  120. package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-CBwl15NP.mjs.map +0 -1
  121. package/lib/knowledge/components/faq-empty-state/faq-empty-state.component.d.ts +0 -22
  122. package/lib/knowledge/components/faq-form/faq-form.component.d.ts +0 -27
  123. package/lib/knowledge/components/faq-search-tester/faq-search-tester.component.d.ts +0 -17
  124. package/lib/knowledge/mocks/practice-faq.seed.d.ts +0 -2
  125. package/lib/knowledge/models/practice-faq.model.d.ts +0 -47
  126. package/lib/knowledge/pages/knowledge-hub.page.d.ts +0 -47
  127. package/lib/knowledge/services/practice-faq-api.mock.d.ts +0 -16
  128. package/lib/knowledge/services/practice-faq-api.service.d.ts +0 -22
  129. package/lib/knowledge/utils/faq.util.d.ts +0 -3
  130. package/lib/layout/eds-topbar.component.d.ts +0 -29
  131. package/lib/pages/audit-page/audit-page.component.d.ts +0 -14
  132. package/lib/ui/eds-waveform.component.d.ts +0 -8
@@ -0,0 +1,440 @@
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