@edvak99/voice-receptionist-widget 1.1.6 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-4UrqnTE3.mjs +306 -0
- package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-4UrqnTE3.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-DUR5f5nc.mjs +277 -0
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-DUR5f5nc.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-call-history-detail-page.component-Bw_Ade1s.mjs → edvak99-voice-receptionist-widget-call-history-detail-page.component-A-vx_M3l.mjs} +5 -5
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-A-vx_M3l.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-gROZVIt-.mjs +193 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-gROZVIt-.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-E30a8p3d.mjs +439 -0
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-E30a8p3d.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-BDtAQSyF.mjs +81 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-BDtAQSyF.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-live-monitor-page.component-DWbA5yAG.mjs → edvak99-voice-receptionist-widget-live-monitor-page.component-7e1HY6sB.mjs} +5 -5
- package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-7e1HY6sB.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-modal.component-uYApl1KF.mjs +48 -0
- package/fesm2022/edvak99-voice-receptionist-widget-modal.component-uYApl1KF.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-outbound-purposes-page.component-DhoyrHSe.mjs → edvak99-voice-receptionist-widget-outbound-purposes-page.component-2GHT8NT9.mjs} +6 -6
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-2GHT8NT9.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-BwCz1dlx.mjs +967 -0
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-BwCz1dlx.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-tasks-page.component-DyNG2DZZ.mjs → edvak99-voice-receptionist-widget-tasks-page.component-Mqo1UmfK.mjs} +5 -5
- package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-Mqo1UmfK.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-wizard-page.component-DskeqJwB.mjs +46 -0
- package/fesm2022/edvak99-voice-receptionist-widget-wizard-page.component-DskeqJwB.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget.mjs +20 -20
- package/fesm2022/edvak99-voice-receptionist-widget.mjs.map +1 -1
- package/package.json +1 -1
- package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-CdrW0He4.mjs +0 -306
- package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-CdrW0He4.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-4GK1vsom.mjs +0 -277
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-4GK1vsom.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-Bw_Ade1s.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DnwYnjIE.mjs +0 -193
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DnwYnjIE.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-DVz0romd.mjs +0 -439
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-DVz0romd.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-Bx7WQ2dg.mjs +0 -81
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-Bx7WQ2dg.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-DWbA5yAG.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-modal.component-Cb-FoQ1N.mjs +0 -48
- package/fesm2022/edvak99-voice-receptionist-widget-modal.component-Cb-FoQ1N.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-DhoyrHSe.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-CZUDgZ1_.mjs +0 -967
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-CZUDgZ1_.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-DyNG2DZZ.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-wizard-page.component-DjaurkbO.mjs +0 -46
- package/fesm2022/edvak99-voice-receptionist-widget-wizard-page.component-DjaurkbO.mjs.map +0 -1
|
@@ -0,0 +1,439 @@
|
|
|
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-DokIb-9r.mjs';
|
|
6
|
+
import { L as LiveCallsListService, E as EdsPulseDotComponent } from './edvak99-voice-receptionist-widget-eds-pulse-dot.component-Ig0Eoq1z.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 { ToastService, EdsButtonComponent, EdsIconComponent } from './edvak99-voice-receptionist-widget.mjs';
|
|
14
|
+
import { M as ModalComponent } from './edvak99-voice-receptionist-widget-modal.component-uYApl1KF.mjs';
|
|
15
|
+
import { E as EdsAvatarComponent } from './edvak99-voice-receptionist-widget-eds-avatar.component-BJs_TH8X.mjs';
|
|
16
|
+
import { E as EdsElapsedPipe } from './edvak99-voice-receptionist-widget-eds-elapsed.pipe-BFCiJOT3.mjs';
|
|
17
|
+
|
|
18
|
+
const E164_PATTERN = /^\+[1-9]\d{6,14}$/;
|
|
19
|
+
class OutboundDialModalComponent {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.api = inject(AiReceptionistApiService);
|
|
22
|
+
this.toast = inject(ToastService);
|
|
23
|
+
this.destroyRef = inject(DestroyRef);
|
|
24
|
+
this.open = false;
|
|
25
|
+
this.closed = new EventEmitter();
|
|
26
|
+
this.dialed = new EventEmitter();
|
|
27
|
+
this.purposes = signal([]);
|
|
28
|
+
this.loading = signal(true);
|
|
29
|
+
this.submitting = signal(false);
|
|
30
|
+
this.serverError = signal(null);
|
|
31
|
+
this.patientHits = signal([]);
|
|
32
|
+
this.searchingPatients = signal(false);
|
|
33
|
+
this.selectedPatient = signal(null);
|
|
34
|
+
this.appointments = signal([]);
|
|
35
|
+
this.loadingAppointments = signal(false);
|
|
36
|
+
this.selectedAppointmentId = signal(null);
|
|
37
|
+
this.showManualEntry = signal(false);
|
|
38
|
+
this.practiceName = signal('');
|
|
39
|
+
/** Bumps OnPush re-render when plain ngModel fields change. */
|
|
40
|
+
this.formEpoch = signal(0);
|
|
41
|
+
this.patientQuery = '';
|
|
42
|
+
this.phone = '';
|
|
43
|
+
this.name = '';
|
|
44
|
+
this.purposeId = '';
|
|
45
|
+
this.appointmentDate = '';
|
|
46
|
+
this.appointmentTime = '';
|
|
47
|
+
this.providerName = '';
|
|
48
|
+
this.customScript = '';
|
|
49
|
+
this.requiredFieldLabel = requiredFieldLabel;
|
|
50
|
+
this.search$ = new Subject();
|
|
51
|
+
this.search$
|
|
52
|
+
.pipe(debounceTime(280), distinctUntilChanged(), switchMap((q) => {
|
|
53
|
+
const query = q.trim();
|
|
54
|
+
if (query.length < 2) {
|
|
55
|
+
this.searchingPatients.set(false);
|
|
56
|
+
this.patientHits.set([]);
|
|
57
|
+
return of([]);
|
|
58
|
+
}
|
|
59
|
+
this.searchingPatients.set(true);
|
|
60
|
+
return this.api.searchPatientsForOutbound(query, 10).pipe(catchError(() => of([])));
|
|
61
|
+
}), takeUntilDestroyed(this.destroyRef))
|
|
62
|
+
.subscribe((hits) => {
|
|
63
|
+
this.searchingPatients.set(false);
|
|
64
|
+
if (!this.selectedPatient()) {
|
|
65
|
+
this.patientHits.set(hits);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
ngOnChanges(changes) {
|
|
70
|
+
if (changes['open']?.currentValue) {
|
|
71
|
+
this.resetForm();
|
|
72
|
+
this.loadPurposes();
|
|
73
|
+
this.loadPracticeName();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
get selectedPurpose() {
|
|
77
|
+
return this.purposes().find((p) => p.purposeId === this.purposeId) ?? null;
|
|
78
|
+
}
|
|
79
|
+
get selectedAppointment() {
|
|
80
|
+
const id = this.selectedAppointmentId();
|
|
81
|
+
if (!id)
|
|
82
|
+
return null;
|
|
83
|
+
return this.appointments().find((a) => a.appointmentId === id) ?? null;
|
|
84
|
+
}
|
|
85
|
+
get previewText() {
|
|
86
|
+
this.formEpoch(); // depend for CD when form fields change
|
|
87
|
+
const p = this.selectedPurpose;
|
|
88
|
+
if (!p)
|
|
89
|
+
return '—';
|
|
90
|
+
const appt = this.selectedAppointment;
|
|
91
|
+
const dateSource = appt?.startTime || this.appointmentDate;
|
|
92
|
+
const timeSource = appt?.startTime || this.appointmentTime;
|
|
93
|
+
return renderLiveOpeningPreview(p, {
|
|
94
|
+
patientName: this.name.trim() || this.selectedPatient()?.fullName || '',
|
|
95
|
+
appointmentDate: formatOpeningDate(dateSource),
|
|
96
|
+
appointmentTime: formatOpeningTime(this.appointmentTime || timeSource, appt?.startTime),
|
|
97
|
+
providerName: this.providerName.trim() || appt?.providerName || '',
|
|
98
|
+
practiceName: this.practiceName(),
|
|
99
|
+
locationName: appt?.locationName || '',
|
|
100
|
+
visitType: appt?.visitType || '',
|
|
101
|
+
customScript: this.customScript.trim(),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
showsField(field) {
|
|
105
|
+
return this.selectedPurpose?.requiredFields.includes(field) ?? false;
|
|
106
|
+
}
|
|
107
|
+
showsCustomScript() {
|
|
108
|
+
const p = this.selectedPurpose;
|
|
109
|
+
return p ? purposeShowsCustomScript(p) : false;
|
|
110
|
+
}
|
|
111
|
+
customScriptRequired() {
|
|
112
|
+
const p = this.selectedPurpose;
|
|
113
|
+
return p ? isCustomScriptRequired(p) : false;
|
|
114
|
+
}
|
|
115
|
+
canPlaceCall() {
|
|
116
|
+
if (!E164_PATTERN.test(this.phone.trim()))
|
|
117
|
+
return false;
|
|
118
|
+
return isPurposeFormValid(this.selectedPurpose, {
|
|
119
|
+
phone: this.phone,
|
|
120
|
+
name: this.name,
|
|
121
|
+
appointmentDate: this.appointmentDate,
|
|
122
|
+
appointmentTime: this.appointmentTime,
|
|
123
|
+
providerName: this.providerName,
|
|
124
|
+
customScript: this.customScript,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
touchForm() {
|
|
128
|
+
this.formEpoch.update((n) => n + 1);
|
|
129
|
+
}
|
|
130
|
+
onPatientQueryChange(value) {
|
|
131
|
+
this.patientQuery = value;
|
|
132
|
+
if (this.selectedPatient())
|
|
133
|
+
return;
|
|
134
|
+
this.search$.next(value);
|
|
135
|
+
}
|
|
136
|
+
selectPatient(patient) {
|
|
137
|
+
this.selectedPatient.set(patient);
|
|
138
|
+
this.patientHits.set([]);
|
|
139
|
+
this.patientQuery = patient.fullName;
|
|
140
|
+
this.name = patient.fullName;
|
|
141
|
+
this.phone = toE164Phone(patient.phone) || '';
|
|
142
|
+
this.showManualEntry.set(!this.phone);
|
|
143
|
+
this.selectedAppointmentId.set(null);
|
|
144
|
+
this.clearApptDerivedFields();
|
|
145
|
+
this.touchForm();
|
|
146
|
+
this.loadAppointments(patient.patientId);
|
|
147
|
+
}
|
|
148
|
+
clearPatient() {
|
|
149
|
+
this.selectedPatient.set(null);
|
|
150
|
+
this.patientQuery = '';
|
|
151
|
+
this.patientHits.set([]);
|
|
152
|
+
this.appointments.set([]);
|
|
153
|
+
this.selectedAppointmentId.set(null);
|
|
154
|
+
this.name = '';
|
|
155
|
+
this.phone = '';
|
|
156
|
+
this.clearApptDerivedFields();
|
|
157
|
+
this.touchForm();
|
|
158
|
+
}
|
|
159
|
+
selectAppointment(appt) {
|
|
160
|
+
this.selectedAppointmentId.set(appt?.appointmentId ?? null);
|
|
161
|
+
if (!appt) {
|
|
162
|
+
this.clearApptDerivedFields();
|
|
163
|
+
this.touchForm();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.applyAppointmentFields(appt);
|
|
167
|
+
this.preferAppointmentPurpose();
|
|
168
|
+
this.touchForm();
|
|
169
|
+
}
|
|
170
|
+
onPurposeChange() {
|
|
171
|
+
const appt = this.selectedAppointment;
|
|
172
|
+
if (appt) {
|
|
173
|
+
this.applyAppointmentFields(appt);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
this.clearApptDerivedFields();
|
|
177
|
+
}
|
|
178
|
+
this.customScript = '';
|
|
179
|
+
this.serverError.set(null);
|
|
180
|
+
this.touchForm();
|
|
181
|
+
}
|
|
182
|
+
close() {
|
|
183
|
+
this.closed.emit();
|
|
184
|
+
}
|
|
185
|
+
submit() {
|
|
186
|
+
this.serverError.set(null);
|
|
187
|
+
if (!this.canPlaceCall() || !this.selectedPurpose)
|
|
188
|
+
return;
|
|
189
|
+
this.submitting.set(true);
|
|
190
|
+
const patient = this.selectedPatient();
|
|
191
|
+
const apptId = this.selectedAppointmentId();
|
|
192
|
+
const body = {
|
|
193
|
+
phone: this.phone.trim(),
|
|
194
|
+
name: this.name.trim(),
|
|
195
|
+
purpose: this.purposeId,
|
|
196
|
+
...(this.appointmentDate ? { appointmentDate: this.appointmentDate } : {}),
|
|
197
|
+
...(this.appointmentTime ? { appointmentTime: this.appointmentTime } : {}),
|
|
198
|
+
...(this.providerName ? { providerName: this.providerName.trim() } : {}),
|
|
199
|
+
...(this.customScript ? { customScript: this.customScript.trim() } : {}),
|
|
200
|
+
...(patient?.patientId ? { patientId: patient.patientId } : {}),
|
|
201
|
+
...(apptId ? { appointmentId: apptId } : {}),
|
|
202
|
+
};
|
|
203
|
+
this.api.placeOutboundCall(body).subscribe({
|
|
204
|
+
next: (res) => {
|
|
205
|
+
this.submitting.set(false);
|
|
206
|
+
const msg = res.message ?? 'Outbound call started.';
|
|
207
|
+
this.toast.show(msg, 'success', 5000);
|
|
208
|
+
this.dialed.emit();
|
|
209
|
+
this.close();
|
|
210
|
+
},
|
|
211
|
+
error: (err) => {
|
|
212
|
+
this.submitting.set(false);
|
|
213
|
+
const msg = err?.message ?? 'Failed to place outbound call.';
|
|
214
|
+
this.serverError.set(msg);
|
|
215
|
+
this.toast.show(msg, 'error', 5000);
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
preferAppointmentPurpose() {
|
|
220
|
+
const current = this.selectedPurpose;
|
|
221
|
+
const needsAppt = !!current &&
|
|
222
|
+
(current.requiredFields.includes('appointmentDate') ||
|
|
223
|
+
current.requiredFields.includes('appointmentTime'));
|
|
224
|
+
if (needsAppt)
|
|
225
|
+
return;
|
|
226
|
+
const confirm = this.purposes().find((p) => p.isActive &&
|
|
227
|
+
p.requiredFields.includes('appointmentDate') &&
|
|
228
|
+
p.requiredFields.includes('appointmentTime')) ??
|
|
229
|
+
this.purposes().find((p) => p.isActive && /confirm/i.test(p.label + p.purposeId));
|
|
230
|
+
if (confirm) {
|
|
231
|
+
this.purposeId = confirm.purposeId;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
applyAppointmentFields(appt) {
|
|
235
|
+
const start = appt.startTime ? new Date(appt.startTime) : null;
|
|
236
|
+
if (start && !Number.isNaN(start.getTime())) {
|
|
237
|
+
const y = start.getFullYear();
|
|
238
|
+
const m = String(start.getMonth() + 1).padStart(2, '0');
|
|
239
|
+
const d = String(start.getDate()).padStart(2, '0');
|
|
240
|
+
const hh = String(start.getHours()).padStart(2, '0');
|
|
241
|
+
const mm = String(start.getMinutes()).padStart(2, '0');
|
|
242
|
+
this.appointmentDate = `${y}-${m}-${d}`;
|
|
243
|
+
this.appointmentTime = `${hh}:${mm}`;
|
|
244
|
+
}
|
|
245
|
+
if (appt.providerName) {
|
|
246
|
+
this.providerName = appt.providerName;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
clearApptDerivedFields() {
|
|
250
|
+
this.appointmentDate = '';
|
|
251
|
+
this.appointmentTime = '';
|
|
252
|
+
this.providerName = '';
|
|
253
|
+
}
|
|
254
|
+
loadAppointments(patientId) {
|
|
255
|
+
this.loadingAppointments.set(true);
|
|
256
|
+
this.appointments.set([]);
|
|
257
|
+
this.api.listPatientAppointmentsForOutbound(patientId, 20).subscribe({
|
|
258
|
+
next: (rows) => {
|
|
259
|
+
this.appointments.set(rows);
|
|
260
|
+
this.loadingAppointments.set(false);
|
|
261
|
+
if (rows.length === 1) {
|
|
262
|
+
this.selectAppointment(rows[0]);
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
error: () => {
|
|
266
|
+
this.loadingAppointments.set(false);
|
|
267
|
+
this.appointments.set([]);
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
loadPracticeName() {
|
|
272
|
+
this.api.getSettings().subscribe({
|
|
273
|
+
next: ({ data }) => {
|
|
274
|
+
const name = String(data.general?.['practiceName'] ?? '').trim();
|
|
275
|
+
this.practiceName.set(name || 'our office');
|
|
276
|
+
},
|
|
277
|
+
error: () => this.practiceName.set('our office'),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
resetForm() {
|
|
281
|
+
this.patientQuery = '';
|
|
282
|
+
this.phone = '';
|
|
283
|
+
this.name = '';
|
|
284
|
+
this.purposeId = '';
|
|
285
|
+
this.appointmentDate = '';
|
|
286
|
+
this.appointmentTime = '';
|
|
287
|
+
this.providerName = '';
|
|
288
|
+
this.customScript = '';
|
|
289
|
+
this.patientHits.set([]);
|
|
290
|
+
this.selectedPatient.set(null);
|
|
291
|
+
this.appointments.set([]);
|
|
292
|
+
this.selectedAppointmentId.set(null);
|
|
293
|
+
this.searchingPatients.set(false);
|
|
294
|
+
this.loadingAppointments.set(false);
|
|
295
|
+
this.showManualEntry.set(false);
|
|
296
|
+
this.serverError.set(null);
|
|
297
|
+
this.submitting.set(false);
|
|
298
|
+
this.formEpoch.set(0);
|
|
299
|
+
}
|
|
300
|
+
loadPurposes() {
|
|
301
|
+
this.loading.set(true);
|
|
302
|
+
this.api.getOutboundPurposes(false).subscribe({
|
|
303
|
+
next: ({ purposes }) => {
|
|
304
|
+
this.purposes.set(purposes);
|
|
305
|
+
if (purposes.length && !this.purposeId) {
|
|
306
|
+
this.purposeId = purposes[0].purposeId;
|
|
307
|
+
}
|
|
308
|
+
this.loading.set(false);
|
|
309
|
+
},
|
|
310
|
+
error: () => {
|
|
311
|
+
this.loading.set(false);
|
|
312
|
+
this.serverError.set('Failed to load outbound purposes.');
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: OutboundDialModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
317
|
+
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=\"flex flex-col gap-3\" (ngSubmit)=\"submit()\" novalidate autocomplete=\"off\">\n <div class=\"dial-form__field dial-form__typeahead relative flex flex-col gap-2\">\n <span class=\"eds-label\">Patient <span class=\"text-[#dc2626]\">*</span></span>\n\n @if (selectedPatient(); as patient) {\n <div class=\"flex items-center justify-between gap-3 rounded-lg border border-[var(--eds-border-subtle,#e4e4e7)] bg-[var(--eds-surface-muted,#f8fafc)] px-3 py-3\">\n <div class=\"flex min-w-0 flex-col gap-1\">\n <strong>{{ patient.fullName }}</strong>\n <span class=\"dial-form__chip-meta text-xs\">\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 shrink-0 cursor-pointer border-none bg-transparent p-1 text-[13px] font-[inherit]\" (click)=\"clearPatient()\">\n Change\n </button>\n </div>\n } @else {\n <input\n type=\"search\"\n class=\"dial-form__input rounded-lg border px-3 py-2 font-[inherit]\"\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 text-xs\">Searching\u2026</span>\n }\n @if (patientHits().length) {\n <ul class=\"dial-form__hits absolute left-0 right-0 top-[calc(100%+4px)] z-[5] max-h-[220px] overflow-hidden overflow-y-auto rounded-lg border\" role=\"listbox\">\n @for (hit of patientHits(); track hit.patientId) {\n <li>\n <button\n type=\"button\"\n class=\"dial-form__hit flex w-full cursor-pointer flex-col items-start gap-1 border-none border-b px-3 py-3 text-left font-[inherit] last:border-b-0\"\n role=\"option\"\n (click)=\"selectPatient(hit)\"\n >\n <span class=\"font-semibold\">{{ hit.fullName }}</span>\n <span class=\"dial-form__hit-meta text-xs\">\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=\"flex flex-col gap-2\">\n <span class=\"eds-label\">Appointment</span>\n @if (loadingAppointments()) {\n <span class=\"dial-form__hint text-xs\">Loading appointments\u2026</span>\n } @else if (!appointments().length) {\n <span class=\"dial-form__hint text-xs\">No upcoming appointments \u2014 dial without one, or enter details below.</span>\n } @else {\n <ul class=\"max-h-[220px] overflow-hidden overflow-y-auto rounded-lg border\">\n <li>\n <label class=\"dial-form__appt flex cursor-pointer items-start gap-3 border-b px-3 py-3 last:border-b-0\">\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 flex cursor-pointer items-start gap-3 border-b px-3 py-3 last:border-b-0\">\n <input\n type=\"radio\"\n name=\"appointmentId\"\n [checked]=\"selectedAppointmentId() === appt.appointmentId\"\n (change)=\"selectAppointment(appt)\"\n autocomplete=\"off\"\n />\n <span class=\"flex flex-col gap-1\">\n <strong>\n {{ appt.startTime ? (appt.startTime | date: 'medium') : 'TBD' }}\n </strong>\n <span class=\"dial-form__hit-meta text-xs\">\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 self-start cursor-pointer border-none bg-transparent p-0 text-[13px] font-[inherit]\"\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=\"flex flex-col gap-2\">\n <span class=\"eds-label\">Phone <span class=\"text-[#dc2626]\">*</span></span>\n <input\n type=\"tel\"\n class=\"dial-form__input rounded-lg border px-3 py-2 font-[inherit]\"\n [(ngModel)]=\"phone\"\n (ngModelChange)=\"touchForm()\"\n name=\"phone\"\n placeholder=\"+14155550100\"\n autocomplete=\"off\"\n required\n />\n <span class=\"dial-form__hint text-xs\">E.164 format, e.g. +14155550100</span>\n </label>\n\n <label class=\"flex flex-col gap-2\">\n <span class=\"eds-label\">Patient name <span class=\"text-[#dc2626]\">*</span></span>\n <input\n type=\"text\"\n class=\"dial-form__input rounded-lg border px-3 py-2 font-[inherit]\"\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=\"flex flex-col gap-2\">\n <span class=\"eds-label\">Purpose <span class=\"text-[#dc2626]\">*</span></span>\n <select\n class=\"dial-form__input rounded-lg border px-3 py-2 font-[inherit]\"\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=\"flex flex-col gap-2\">\n <span class=\"eds-label\">{{ requiredFieldLabel('appointmentDate') }} <span class=\"text-[#dc2626]\">*</span></span>\n <input\n type=\"date\"\n class=\"dial-form__input rounded-lg border px-3 py-2 font-[inherit]\"\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=\"flex flex-col gap-2\">\n <span class=\"eds-label\">{{ requiredFieldLabel('appointmentTime') }} <span class=\"text-[#dc2626]\">*</span></span>\n <input\n type=\"time\"\n class=\"dial-form__input rounded-lg border px-3 py-2 font-[inherit]\"\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=\"flex flex-col gap-2\">\n <span class=\"eds-label\">{{ requiredFieldLabel('providerName') }} <span class=\"text-[#dc2626]\">*</span></span>\n <input\n type=\"text\"\n class=\"dial-form__input rounded-lg border px-3 py-2 font-[inherit]\"\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 text-xs\">\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=\"flex flex-col gap-2\">\n <span class=\"eds-label\">\n Custom script\n @if (customScriptRequired()) {\n <span class=\"text-[#dc2626]\">*</span>\n }\n </span>\n <textarea\n class=\"dial-form__textarea resize-y rounded-lg border px-3 py-2 font-[inherit]\"\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 rounded-lg p-3 shadow-sm\">\n <h3 class=\"eds-label\">Opening preview</h3>\n <p class=\"dial-form__preview-text mt-2 whitespace-pre-wrap text-sm leading-normal\">{{ previewText }}</p>\n @if (selectedAppointmentId()) {\n <p class=\"dial-form__hint text-xs\">\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 text-xs\">\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=\"m-0 text-[13px] text-[#b91c1c]\" role=\"alert\">{{ serverError() }}</p>\n }\n\n <footer class=\"mt-1 flex justify-end gap-2\">\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__input,.dial-form__textarea{border-color:var(--eds-border-subtle, #e4e4e7)}.dial-form__hint{color:var(--eds-text-secondary)}.dial-form__chip-clear{color:var(--eds-primary, #2563eb)}.dial-form__chip-meta,.dial-form__hit-meta{color:var(--eds-text-secondary)}.dial-form__hits,.dial-form__appts{list-style:none;margin:0;padding:0;border-color:var(--eds-border-subtle, #e4e4e7)}.dial-form__hits{background:var(--eds-surface, #fff);box-shadow:0 8px 24px #0f172a1f}.dial-form__hit{background:transparent;border-bottom-color:var(--eds-border-subtle, #f1f5f9)}.dial-form__hit:hover{background:var(--eds-surface-muted, #f8fafc)}.dial-form__appt{border-bottom-color:var(--eds-border-subtle, #f1f5f9)}.dial-form__toggle{color:var(--eds-primary, #2563eb)}.dial-form__preview{background:var(--eds-surface-muted, #f8fafc)}\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 }); }
|
|
318
|
+
}
|
|
319
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: OutboundDialModalComponent, decorators: [{
|
|
320
|
+
type: Component,
|
|
321
|
+
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=\"flex flex-col gap-3\" (ngSubmit)=\"submit()\" novalidate autocomplete=\"off\">\n <div class=\"dial-form__field dial-form__typeahead relative flex flex-col gap-2\">\n <span class=\"eds-label\">Patient <span class=\"text-[#dc2626]\">*</span></span>\n\n @if (selectedPatient(); as patient) {\n <div class=\"flex items-center justify-between gap-3 rounded-lg border border-[var(--eds-border-subtle,#e4e4e7)] bg-[var(--eds-surface-muted,#f8fafc)] px-3 py-3\">\n <div class=\"flex min-w-0 flex-col gap-1\">\n <strong>{{ patient.fullName }}</strong>\n <span class=\"dial-form__chip-meta text-xs\">\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 shrink-0 cursor-pointer border-none bg-transparent p-1 text-[13px] font-[inherit]\" (click)=\"clearPatient()\">\n Change\n </button>\n </div>\n } @else {\n <input\n type=\"search\"\n class=\"dial-form__input rounded-lg border px-3 py-2 font-[inherit]\"\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 text-xs\">Searching\u2026</span>\n }\n @if (patientHits().length) {\n <ul class=\"dial-form__hits absolute left-0 right-0 top-[calc(100%+4px)] z-[5] max-h-[220px] overflow-hidden overflow-y-auto rounded-lg border\" role=\"listbox\">\n @for (hit of patientHits(); track hit.patientId) {\n <li>\n <button\n type=\"button\"\n class=\"dial-form__hit flex w-full cursor-pointer flex-col items-start gap-1 border-none border-b px-3 py-3 text-left font-[inherit] last:border-b-0\"\n role=\"option\"\n (click)=\"selectPatient(hit)\"\n >\n <span class=\"font-semibold\">{{ hit.fullName }}</span>\n <span class=\"dial-form__hit-meta text-xs\">\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=\"flex flex-col gap-2\">\n <span class=\"eds-label\">Appointment</span>\n @if (loadingAppointments()) {\n <span class=\"dial-form__hint text-xs\">Loading appointments\u2026</span>\n } @else if (!appointments().length) {\n <span class=\"dial-form__hint text-xs\">No upcoming appointments \u2014 dial without one, or enter details below.</span>\n } @else {\n <ul class=\"max-h-[220px] overflow-hidden overflow-y-auto rounded-lg border\">\n <li>\n <label class=\"dial-form__appt flex cursor-pointer items-start gap-3 border-b px-3 py-3 last:border-b-0\">\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 flex cursor-pointer items-start gap-3 border-b px-3 py-3 last:border-b-0\">\n <input\n type=\"radio\"\n name=\"appointmentId\"\n [checked]=\"selectedAppointmentId() === appt.appointmentId\"\n (change)=\"selectAppointment(appt)\"\n autocomplete=\"off\"\n />\n <span class=\"flex flex-col gap-1\">\n <strong>\n {{ appt.startTime ? (appt.startTime | date: 'medium') : 'TBD' }}\n </strong>\n <span class=\"dial-form__hit-meta text-xs\">\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 self-start cursor-pointer border-none bg-transparent p-0 text-[13px] font-[inherit]\"\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=\"flex flex-col gap-2\">\n <span class=\"eds-label\">Phone <span class=\"text-[#dc2626]\">*</span></span>\n <input\n type=\"tel\"\n class=\"dial-form__input rounded-lg border px-3 py-2 font-[inherit]\"\n [(ngModel)]=\"phone\"\n (ngModelChange)=\"touchForm()\"\n name=\"phone\"\n placeholder=\"+14155550100\"\n autocomplete=\"off\"\n required\n />\n <span class=\"dial-form__hint text-xs\">E.164 format, e.g. +14155550100</span>\n </label>\n\n <label class=\"flex flex-col gap-2\">\n <span class=\"eds-label\">Patient name <span class=\"text-[#dc2626]\">*</span></span>\n <input\n type=\"text\"\n class=\"dial-form__input rounded-lg border px-3 py-2 font-[inherit]\"\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=\"flex flex-col gap-2\">\n <span class=\"eds-label\">Purpose <span class=\"text-[#dc2626]\">*</span></span>\n <select\n class=\"dial-form__input rounded-lg border px-3 py-2 font-[inherit]\"\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=\"flex flex-col gap-2\">\n <span class=\"eds-label\">{{ requiredFieldLabel('appointmentDate') }} <span class=\"text-[#dc2626]\">*</span></span>\n <input\n type=\"date\"\n class=\"dial-form__input rounded-lg border px-3 py-2 font-[inherit]\"\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=\"flex flex-col gap-2\">\n <span class=\"eds-label\">{{ requiredFieldLabel('appointmentTime') }} <span class=\"text-[#dc2626]\">*</span></span>\n <input\n type=\"time\"\n class=\"dial-form__input rounded-lg border px-3 py-2 font-[inherit]\"\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=\"flex flex-col gap-2\">\n <span class=\"eds-label\">{{ requiredFieldLabel('providerName') }} <span class=\"text-[#dc2626]\">*</span></span>\n <input\n type=\"text\"\n class=\"dial-form__input rounded-lg border px-3 py-2 font-[inherit]\"\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 text-xs\">\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=\"flex flex-col gap-2\">\n <span class=\"eds-label\">\n Custom script\n @if (customScriptRequired()) {\n <span class=\"text-[#dc2626]\">*</span>\n }\n </span>\n <textarea\n class=\"dial-form__textarea resize-y rounded-lg border px-3 py-2 font-[inherit]\"\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 rounded-lg p-3 shadow-sm\">\n <h3 class=\"eds-label\">Opening preview</h3>\n <p class=\"dial-form__preview-text mt-2 whitespace-pre-wrap text-sm leading-normal\">{{ previewText }}</p>\n @if (selectedAppointmentId()) {\n <p class=\"dial-form__hint text-xs\">\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 text-xs\">\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=\"m-0 text-[13px] text-[#b91c1c]\" role=\"alert\">{{ serverError() }}</p>\n }\n\n <footer class=\"mt-1 flex justify-end gap-2\">\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__input,.dial-form__textarea{border-color:var(--eds-border-subtle, #e4e4e7)}.dial-form__hint{color:var(--eds-text-secondary)}.dial-form__chip-clear{color:var(--eds-primary, #2563eb)}.dial-form__chip-meta,.dial-form__hit-meta{color:var(--eds-text-secondary)}.dial-form__hits,.dial-form__appts{list-style:none;margin:0;padding:0;border-color:var(--eds-border-subtle, #e4e4e7)}.dial-form__hits{background:var(--eds-surface, #fff);box-shadow:0 8px 24px #0f172a1f}.dial-form__hit{background:transparent;border-bottom-color:var(--eds-border-subtle, #f1f5f9)}.dial-form__hit:hover{background:var(--eds-surface-muted, #f8fafc)}.dial-form__appt{border-bottom-color:var(--eds-border-subtle, #f1f5f9)}.dial-form__toggle{color:var(--eds-primary, #2563eb)}.dial-form__preview{background:var(--eds-surface-muted, #f8fafc)}\n"] }]
|
|
322
|
+
}], ctorParameters: () => [], propDecorators: { open: [{
|
|
323
|
+
type: Input
|
|
324
|
+
}], closed: [{
|
|
325
|
+
type: Output
|
|
326
|
+
}], dialed: [{
|
|
327
|
+
type: Output
|
|
328
|
+
}] } });
|
|
329
|
+
|
|
330
|
+
class DashboardPageComponent {
|
|
331
|
+
constructor() {
|
|
332
|
+
this.api = inject(AiReceptionistApiService);
|
|
333
|
+
this.liveCallsList = inject(LiveCallsListService);
|
|
334
|
+
this.router = inject(Router);
|
|
335
|
+
this.route = inject(ActivatedRoute);
|
|
336
|
+
this.data = signal(null);
|
|
337
|
+
this.loading = signal(true);
|
|
338
|
+
this.liveCalls = this.liveCallsList.calls;
|
|
339
|
+
this.liveCallsLoading = this.liveCallsList.loading;
|
|
340
|
+
this.liveCallsError = this.liveCallsList.error;
|
|
341
|
+
this.outboundOpen = signal(false);
|
|
342
|
+
this.statTiles = [
|
|
343
|
+
{ key: 'activeCalls', label: 'Live calls', icon: 'graphic_eq' },
|
|
344
|
+
{ key: 'answeredToday', label: 'Calls today', icon: 'call' },
|
|
345
|
+
{ key: 'tasksCreated', label: 'Open tasks', icon: 'task_alt' },
|
|
346
|
+
{
|
|
347
|
+
key: 'appointmentsBooked',
|
|
348
|
+
label: 'Appointments booked',
|
|
349
|
+
icon: 'event_available',
|
|
350
|
+
},
|
|
351
|
+
{ key: 'completionRate', label: 'AI handled', icon: 'trending_up', suffix: '%' },
|
|
352
|
+
];
|
|
353
|
+
this.isNeedsReview = isNeedsReview;
|
|
354
|
+
this.callerMetaLine = callerMetaLine;
|
|
355
|
+
this.resultChipIcon = resultChipIcon;
|
|
356
|
+
this.historyResultChips = historyResultChips;
|
|
357
|
+
this.historyResolutionLabel = historyResolutionLabel;
|
|
358
|
+
}
|
|
359
|
+
ngOnInit() {
|
|
360
|
+
this.liveCallsList.start();
|
|
361
|
+
this.api.getDashboard().subscribe(({ data }) => {
|
|
362
|
+
this.data.set(data);
|
|
363
|
+
this.loading.set(false);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
ngOnDestroy() {
|
|
367
|
+
this.liveCallsList.stop();
|
|
368
|
+
}
|
|
369
|
+
statValue(key) {
|
|
370
|
+
if (key === 'activeCalls') {
|
|
371
|
+
return this.liveCallsList.calls().length;
|
|
372
|
+
}
|
|
373
|
+
return this.data()?.stats[key] ?? 0;
|
|
374
|
+
}
|
|
375
|
+
statusTone(status) {
|
|
376
|
+
const normalized = status || 'ai';
|
|
377
|
+
const map = {
|
|
378
|
+
ai: 'brand',
|
|
379
|
+
'needs-help': 'notice',
|
|
380
|
+
'human-requested': 'violet',
|
|
381
|
+
urgent: 'neg',
|
|
382
|
+
takeover: 'violet',
|
|
383
|
+
transferring: 'notice',
|
|
384
|
+
};
|
|
385
|
+
return map[normalized] ?? 'brand';
|
|
386
|
+
}
|
|
387
|
+
statusLabel(status) {
|
|
388
|
+
return status || 'ai';
|
|
389
|
+
}
|
|
390
|
+
historyAvatarTone(row) {
|
|
391
|
+
if (!row.name?.trim() || /^unknown/i.test(row.name) || row.initials === '?')
|
|
392
|
+
return 'gray';
|
|
393
|
+
return 'brand';
|
|
394
|
+
}
|
|
395
|
+
resolutionOf(row) {
|
|
396
|
+
return historyResolution({ outcome: row.outcome, risk: row.risk });
|
|
397
|
+
}
|
|
398
|
+
formatDuration(sec) {
|
|
399
|
+
const m = Math.floor(sec / 60);
|
|
400
|
+
const s = sec % 60;
|
|
401
|
+
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
402
|
+
}
|
|
403
|
+
exportCsv() {
|
|
404
|
+
// mock export
|
|
405
|
+
}
|
|
406
|
+
openOutboundDial() {
|
|
407
|
+
this.outboundOpen.set(true);
|
|
408
|
+
}
|
|
409
|
+
closeOutboundDial() {
|
|
410
|
+
this.outboundOpen.set(false);
|
|
411
|
+
}
|
|
412
|
+
openHistoryDetail(id) {
|
|
413
|
+
void this.router.navigate(['../history', id], { relativeTo: this.route });
|
|
414
|
+
}
|
|
415
|
+
callDetailLink(callId) {
|
|
416
|
+
return ['../history', callId];
|
|
417
|
+
}
|
|
418
|
+
appointmentDetailLink(callId) {
|
|
419
|
+
return callId ? ['../history', callId] : ['../history'];
|
|
420
|
+
}
|
|
421
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: DashboardPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
422
|
+
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=\"mb-6 flex items-start justify-between gap-4\">\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=\"flex shrink-0 gap-2\">\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=\"mb-5 grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-5\">\n @for (tile of statTiles; track tile.key) {\n @if (tile.key === 'appointmentsBooked') {\n <a routerLink=\"../appointments\" class=\"eds-card flex cursor-pointer flex-col gap-2 p-4 no-underline text-inherit transition-colors hover:border-blue-600 shadow-sm\">\n <eds-icon [name]=\"tile.icon\" [size]=\"22\" />\n <span class=\"eds-label\">{{ tile.label }}</span>\n <span class=\"text-3xl font-semibold tracking-tight text-zinc-900\">\n {{ statValue(tile.key) }}{{ tile.suffix ?? '' }}\n </span>\n </a>\n } @else if (tile.key === 'activeCalls') {\n <a routerLink=\"../live\" class=\"eds-card flex cursor-pointer flex-col gap-2 p-4 no-underline text-inherit transition-colors hover:border-blue-600 shadow-sm\">\n <eds-icon [name]=\"tile.icon\" [size]=\"22\" />\n <span class=\"eds-label\">{{ tile.label }}</span>\n <span class=\"text-3xl font-semibold tracking-tight text-zinc-900\">\n {{ statValue(tile.key) }}{{ tile.suffix ?? '' }}\n </span>\n </a>\n } @else {\n <article class=\"eds-card flex flex-col gap-2 p-4 shadow-sm\">\n <eds-icon [name]=\"tile.icon\" [size]=\"22\" />\n <span class=\"eds-label\">{{ tile.label }}</span>\n <span class=\"text-3xl font-semibold tracking-tight text-zinc-900\">\n {{ statValue(tile.key) }}{{ tile.suffix ?? '' }}\n </span>\n </article>\n }\n }\n </section>\n\n <section class=\"mb-5 grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <article class=\"eds-card p-4 shadow-sm\">\n <header class=\"mb-3 flex items-center justify-between gap-2\">\n <h2 class=\"eds-h2\">Live calls</h2>\n <eds-pulse-dot />\n </header>\n <ul class=\"m-0 flex list-none flex-col gap-2 p-0\">\n @if (liveCallsError()) {\n <li class=\"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=\"grid grid-cols-[auto_1fr_auto] items-center gap-3 rounded-md p-2 no-underline text-inherit hover:bg-zinc-50\">\n <eds-avatar [initials]=\"call.initials\" tone=\"brand\" />\n <div class=\"flex min-w-0 flex-col\">\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 p-4 shadow-sm\">\n <header class=\"mb-3 flex items-center justify-between gap-2\">\n <h2 class=\"eds-h2\">Tasks created</h2>\n <a routerLink=\"../tasks\" class=\"eds-body-sm\">View all</a>\n </header>\n <ul class=\"m-0 flex list-none flex-col gap-2 p-0\">\n @for (task of data()!.latestTasks; track task.id) {\n <li class=\"border-b border-zinc-200 last:border-b-0\">\n <a\n [routerLink]=\"task.callId ? callDetailLink(task.callId) : ['../tasks']\"\n class=\"grid grid-cols-[1fr_auto] items-center gap-3 rounded-md p-2 no-underline text-inherit hover:bg-zinc-50\"\n [attr.aria-label]=\"'View task: ' + task.title\"\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 p-4 shadow-sm\">\n <header class=\"mb-3 flex items-center justify-between gap-2\">\n <h2 class=\"eds-h2\">Appointments created</h2>\n <a routerLink=\"../appointments\" class=\"eds-body-sm\">View all</a>\n </header>\n <ul class=\"m-0 flex list-none flex-col gap-2 p-0\">\n @for (appointment of data()!.latestAppointments; track appointment.id) {\n <li class=\"border-b border-zinc-200 last:border-b-0\">\n <a\n [routerLink]=\"appointmentDetailLink(appointment.callId)\"\n class=\"grid grid-cols-[1fr_auto] items-center gap-3 rounded-md p-2 no-underline text-inherit hover:bg-zinc-50\"\n [attr.aria-label]=\"'View appointment for ' + appointment.patientName\"\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 overflow-x-auto p-4 shadow-sm\">\n <header class=\"mb-3 flex items-center justify-between gap-2\">\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=\"overflow-x-auto\">\n <table class=\"w-full min-w-5xl border-collapse\">\n <thead>\n <tr>\n <th class=\"whitespace-nowrap border-b border-zinc-200 bg-zinc-100 px-4 py-3 text-left text-sm font-semibold text-zinc-600\">Time</th>\n <th class=\"whitespace-nowrap border-b border-zinc-200 bg-zinc-100 px-4 py-3 text-left text-sm font-semibold text-zinc-600\">Patient/Caller</th>\n <th class=\"whitespace-nowrap border-b border-zinc-200 bg-zinc-100 px-4 py-3 text-left text-sm font-semibold text-zinc-600\"></th>\n <th class=\"whitespace-nowrap border-b border-zinc-200 bg-zinc-100 px-4 py-3 text-left text-sm font-semibold text-zinc-600\">Goal Timeline</th>\n <th class=\"whitespace-nowrap border-b border-zinc-200 bg-zinc-100 px-4 py-3 text-left text-sm font-semibold text-zinc-600\">Handled By</th>\n <th class=\"whitespace-nowrap border-b border-zinc-200 bg-zinc-100 px-4 py-3 text-left text-sm font-semibold text-zinc-600\">Resolution</th>\n <th class=\"whitespace-nowrap border-b border-zinc-200 bg-zinc-100 px-4 py-3 text-left text-sm font-semibold text-zinc-600\">Result</th>\n <th class=\"whitespace-nowrap border-b border-zinc-200 bg-zinc-100 px-4 py-3 text-left text-sm font-semibold text-zinc-600\">Duration</th>\n <th class=\"whitespace-nowrap border-b border-zinc-200 bg-zinc-100 px-4 py-3 text-left text-sm font-semibold text-zinc-600\">Reviewed</th>\n </tr>\n </thead>\n <tbody>\n @for (row of data()!.recentHistory; track row.id) {\n <tr\n class=\"cursor-pointer hover:[&>td]:bg-zinc-50 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600\"\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 class=\"border-b border-slate-100 px-4 py-4 align-middle text-sm text-zinc-900\">\n <div class=\"flex flex-col gap-1 whitespace-nowrap\">\n @if (row.started) {\n <span class=\"text-sm font-semibold\">{{ row.started | date: 'shortTime' }}</span>\n <span class=\"text-xs text-zinc-500\">{{ row.started | date: 'MMM d, y' }}</span>\n } @else {\n <span class=\"text-sm font-semibold\">{{ row.whenLabel || '\u2014' }}</span>\n }\n </div>\n </td>\n\n <td class=\"border-b border-slate-100 px-4 py-4 align-middle text-sm text-zinc-900\">\n <div class=\"flex min-w-52 items-center gap-3\">\n <eds-avatar\n class=\"dashboard-page__avatar\"\n [initials]=\"row.initials || '?'\"\n [tone]=\"historyAvatarTone(row)\"\n />\n <div class=\"flex min-w-0 flex-col gap-1\">\n <span class=\"text-sm font-semibold text-zinc-900\">{{ row.name || 'Unknown Caller' }}</span>\n <span class=\"whitespace-nowrap text-xs text-zinc-500\">\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 class=\"border-b border-slate-100 px-4 py-4 align-middle text-sm text-zinc-900\">\n <span\n class=\"inline-flex h-7 w-7 items-center justify-center rounded-lg\"\n [class.bg-blue-50]=\"row.direction === 'inbound'\"\n [class.text-blue-600]=\"row.direction === 'inbound'\"\n [class.bg-violet-50]=\"row.direction === 'outbound'\"\n [class.text-violet-600]=\"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 class=\"border-b border-slate-100 px-4 py-4 align-middle text-sm text-zinc-900\">\n <span class=\"text-sm text-zinc-700\">{{ row.reason || '\u2014' }}</span>\n </td>\n\n <td class=\"border-b border-slate-100 px-4 py-4 align-middle text-sm text-zinc-900\">\n <span\n class=\"inline-flex items-center rounded-md px-2 py-1 text-xs font-medium\"\n [class.bg-violet-50]=\"(row.handledBy || 'ai') === 'ai'\"\n [class.text-violet-600]=\"(row.handledBy || 'ai') === 'ai'\"\n [class.bg-blue-50]=\"row.handledBy === 'staff'\"\n [class.text-blue-600]=\"row.handledBy === 'staff'\"\n >\n {{ row.handledBy === 'staff' ? 'STAFF' : 'AI' }}\n </span>\n </td>\n\n <td class=\"border-b border-slate-100 px-4 py-4 align-middle text-sm text-zinc-900\">\n @if (resolutionOf(row); as resolution) {\n <span\n class=\"inline-flex items-center whitespace-nowrap rounded-full px-3 py-1 text-xs font-medium\"\n [class.bg-emerald-50]=\"resolution === 'resolved'\"\n [class.text-emerald-700]=\"resolution === 'resolved'\"\n [class.bg-orange-50]=\"resolution === 'needs_attention'\"\n [class.text-orange-700]=\"resolution === 'needs_attention'\"\n >\n {{ historyResolutionLabel(resolution) }}\n </span>\n }\n </td>\n\n <td class=\"border-b border-slate-100 px-4 py-4 align-middle text-sm text-zinc-900\">\n <div class=\"flex max-w-64 flex-wrap gap-2\">\n @for (chip of historyResultChips(row.results, row.outcome); track chip) {\n <span class=\"inline-flex items-center gap-1 whitespace-nowrap rounded-md bg-zinc-100 px-2 py-1 text-xs font-medium text-zinc-600\">\n <eds-icon [name]=\"resultChipIcon(chip)\" [size]=\"14\" />\n {{ chip }}\n </span>\n } @empty {\n <span class=\"text-sm text-zinc-500\">\u2014</span>\n }\n </div>\n </td>\n\n <td class=\"whitespace-nowrap border-b border-slate-100 px-4 py-4 align-middle text-sm tabular-nums text-zinc-700\">\n {{ row.durationLabel ?? formatDuration(row.duration) }}\n </td>\n\n <td class=\"w-14 border-b border-slate-100 px-4 py-4 text-center align-middle text-sm text-zinc-900\">\n @if (row.review === 'reviewed') {\n <span class=\"inline-flex text-green-600\" 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=\"px-4 py-4 text-sm text-zinc-500\">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__history-row--needs-review td:first-child{box-shadow:inset 3px 0 #f59e0b}.dashboard-page__avatar ::ng-deep .eds-avatar{width:36px;height:36px;border-radius:10px;font-size:12px}\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 }); }
|
|
423
|
+
}
|
|
424
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: DashboardPageComponent, decorators: [{
|
|
425
|
+
type: Component,
|
|
426
|
+
args: [{ selector: 'eds-dashboard-page', imports: [
|
|
427
|
+
RouterLink,
|
|
428
|
+
DatePipe,
|
|
429
|
+
EdsAvatarComponent,
|
|
430
|
+
EdsButtonComponent,
|
|
431
|
+
EdsIconComponent,
|
|
432
|
+
EdsPulseDotComponent,
|
|
433
|
+
EdsElapsedPipe,
|
|
434
|
+
OutboundDialModalComponent,
|
|
435
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"eds-page dashboard-page\">\n <header class=\"mb-6 flex items-start justify-between gap-4\">\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=\"flex shrink-0 gap-2\">\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=\"mb-5 grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-5\">\n @for (tile of statTiles; track tile.key) {\n @if (tile.key === 'appointmentsBooked') {\n <a routerLink=\"../appointments\" class=\"eds-card flex cursor-pointer flex-col gap-2 p-4 no-underline text-inherit transition-colors hover:border-blue-600 shadow-sm\">\n <eds-icon [name]=\"tile.icon\" [size]=\"22\" />\n <span class=\"eds-label\">{{ tile.label }}</span>\n <span class=\"text-3xl font-semibold tracking-tight text-zinc-900\">\n {{ statValue(tile.key) }}{{ tile.suffix ?? '' }}\n </span>\n </a>\n } @else if (tile.key === 'activeCalls') {\n <a routerLink=\"../live\" class=\"eds-card flex cursor-pointer flex-col gap-2 p-4 no-underline text-inherit transition-colors hover:border-blue-600 shadow-sm\">\n <eds-icon [name]=\"tile.icon\" [size]=\"22\" />\n <span class=\"eds-label\">{{ tile.label }}</span>\n <span class=\"text-3xl font-semibold tracking-tight text-zinc-900\">\n {{ statValue(tile.key) }}{{ tile.suffix ?? '' }}\n </span>\n </a>\n } @else {\n <article class=\"eds-card flex flex-col gap-2 p-4 shadow-sm\">\n <eds-icon [name]=\"tile.icon\" [size]=\"22\" />\n <span class=\"eds-label\">{{ tile.label }}</span>\n <span class=\"text-3xl font-semibold tracking-tight text-zinc-900\">\n {{ statValue(tile.key) }}{{ tile.suffix ?? '' }}\n </span>\n </article>\n }\n }\n </section>\n\n <section class=\"mb-5 grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <article class=\"eds-card p-4 shadow-sm\">\n <header class=\"mb-3 flex items-center justify-between gap-2\">\n <h2 class=\"eds-h2\">Live calls</h2>\n <eds-pulse-dot />\n </header>\n <ul class=\"m-0 flex list-none flex-col gap-2 p-0\">\n @if (liveCallsError()) {\n <li class=\"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=\"grid grid-cols-[auto_1fr_auto] items-center gap-3 rounded-md p-2 no-underline text-inherit hover:bg-zinc-50\">\n <eds-avatar [initials]=\"call.initials\" tone=\"brand\" />\n <div class=\"flex min-w-0 flex-col\">\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 p-4 shadow-sm\">\n <header class=\"mb-3 flex items-center justify-between gap-2\">\n <h2 class=\"eds-h2\">Tasks created</h2>\n <a routerLink=\"../tasks\" class=\"eds-body-sm\">View all</a>\n </header>\n <ul class=\"m-0 flex list-none flex-col gap-2 p-0\">\n @for (task of data()!.latestTasks; track task.id) {\n <li class=\"border-b border-zinc-200 last:border-b-0\">\n <a\n [routerLink]=\"task.callId ? callDetailLink(task.callId) : ['../tasks']\"\n class=\"grid grid-cols-[1fr_auto] items-center gap-3 rounded-md p-2 no-underline text-inherit hover:bg-zinc-50\"\n [attr.aria-label]=\"'View task: ' + task.title\"\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 p-4 shadow-sm\">\n <header class=\"mb-3 flex items-center justify-between gap-2\">\n <h2 class=\"eds-h2\">Appointments created</h2>\n <a routerLink=\"../appointments\" class=\"eds-body-sm\">View all</a>\n </header>\n <ul class=\"m-0 flex list-none flex-col gap-2 p-0\">\n @for (appointment of data()!.latestAppointments; track appointment.id) {\n <li class=\"border-b border-zinc-200 last:border-b-0\">\n <a\n [routerLink]=\"appointmentDetailLink(appointment.callId)\"\n class=\"grid grid-cols-[1fr_auto] items-center gap-3 rounded-md p-2 no-underline text-inherit hover:bg-zinc-50\"\n [attr.aria-label]=\"'View appointment for ' + appointment.patientName\"\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 overflow-x-auto p-4 shadow-sm\">\n <header class=\"mb-3 flex items-center justify-between gap-2\">\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=\"overflow-x-auto\">\n <table class=\"w-full min-w-5xl border-collapse\">\n <thead>\n <tr>\n <th class=\"whitespace-nowrap border-b border-zinc-200 bg-zinc-100 px-4 py-3 text-left text-sm font-semibold text-zinc-600\">Time</th>\n <th class=\"whitespace-nowrap border-b border-zinc-200 bg-zinc-100 px-4 py-3 text-left text-sm font-semibold text-zinc-600\">Patient/Caller</th>\n <th class=\"whitespace-nowrap border-b border-zinc-200 bg-zinc-100 px-4 py-3 text-left text-sm font-semibold text-zinc-600\"></th>\n <th class=\"whitespace-nowrap border-b border-zinc-200 bg-zinc-100 px-4 py-3 text-left text-sm font-semibold text-zinc-600\">Goal Timeline</th>\n <th class=\"whitespace-nowrap border-b border-zinc-200 bg-zinc-100 px-4 py-3 text-left text-sm font-semibold text-zinc-600\">Handled By</th>\n <th class=\"whitespace-nowrap border-b border-zinc-200 bg-zinc-100 px-4 py-3 text-left text-sm font-semibold text-zinc-600\">Resolution</th>\n <th class=\"whitespace-nowrap border-b border-zinc-200 bg-zinc-100 px-4 py-3 text-left text-sm font-semibold text-zinc-600\">Result</th>\n <th class=\"whitespace-nowrap border-b border-zinc-200 bg-zinc-100 px-4 py-3 text-left text-sm font-semibold text-zinc-600\">Duration</th>\n <th class=\"whitespace-nowrap border-b border-zinc-200 bg-zinc-100 px-4 py-3 text-left text-sm font-semibold text-zinc-600\">Reviewed</th>\n </tr>\n </thead>\n <tbody>\n @for (row of data()!.recentHistory; track row.id) {\n <tr\n class=\"cursor-pointer hover:[&>td]:bg-zinc-50 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600\"\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 class=\"border-b border-slate-100 px-4 py-4 align-middle text-sm text-zinc-900\">\n <div class=\"flex flex-col gap-1 whitespace-nowrap\">\n @if (row.started) {\n <span class=\"text-sm font-semibold\">{{ row.started | date: 'shortTime' }}</span>\n <span class=\"text-xs text-zinc-500\">{{ row.started | date: 'MMM d, y' }}</span>\n } @else {\n <span class=\"text-sm font-semibold\">{{ row.whenLabel || '\u2014' }}</span>\n }\n </div>\n </td>\n\n <td class=\"border-b border-slate-100 px-4 py-4 align-middle text-sm text-zinc-900\">\n <div class=\"flex min-w-52 items-center gap-3\">\n <eds-avatar\n class=\"dashboard-page__avatar\"\n [initials]=\"row.initials || '?'\"\n [tone]=\"historyAvatarTone(row)\"\n />\n <div class=\"flex min-w-0 flex-col gap-1\">\n <span class=\"text-sm font-semibold text-zinc-900\">{{ row.name || 'Unknown Caller' }}</span>\n <span class=\"whitespace-nowrap text-xs text-zinc-500\">\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 class=\"border-b border-slate-100 px-4 py-4 align-middle text-sm text-zinc-900\">\n <span\n class=\"inline-flex h-7 w-7 items-center justify-center rounded-lg\"\n [class.bg-blue-50]=\"row.direction === 'inbound'\"\n [class.text-blue-600]=\"row.direction === 'inbound'\"\n [class.bg-violet-50]=\"row.direction === 'outbound'\"\n [class.text-violet-600]=\"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 class=\"border-b border-slate-100 px-4 py-4 align-middle text-sm text-zinc-900\">\n <span class=\"text-sm text-zinc-700\">{{ row.reason || '\u2014' }}</span>\n </td>\n\n <td class=\"border-b border-slate-100 px-4 py-4 align-middle text-sm text-zinc-900\">\n <span\n class=\"inline-flex items-center rounded-md px-2 py-1 text-xs font-medium\"\n [class.bg-violet-50]=\"(row.handledBy || 'ai') === 'ai'\"\n [class.text-violet-600]=\"(row.handledBy || 'ai') === 'ai'\"\n [class.bg-blue-50]=\"row.handledBy === 'staff'\"\n [class.text-blue-600]=\"row.handledBy === 'staff'\"\n >\n {{ row.handledBy === 'staff' ? 'STAFF' : 'AI' }}\n </span>\n </td>\n\n <td class=\"border-b border-slate-100 px-4 py-4 align-middle text-sm text-zinc-900\">\n @if (resolutionOf(row); as resolution) {\n <span\n class=\"inline-flex items-center whitespace-nowrap rounded-full px-3 py-1 text-xs font-medium\"\n [class.bg-emerald-50]=\"resolution === 'resolved'\"\n [class.text-emerald-700]=\"resolution === 'resolved'\"\n [class.bg-orange-50]=\"resolution === 'needs_attention'\"\n [class.text-orange-700]=\"resolution === 'needs_attention'\"\n >\n {{ historyResolutionLabel(resolution) }}\n </span>\n }\n </td>\n\n <td class=\"border-b border-slate-100 px-4 py-4 align-middle text-sm text-zinc-900\">\n <div class=\"flex max-w-64 flex-wrap gap-2\">\n @for (chip of historyResultChips(row.results, row.outcome); track chip) {\n <span class=\"inline-flex items-center gap-1 whitespace-nowrap rounded-md bg-zinc-100 px-2 py-1 text-xs font-medium text-zinc-600\">\n <eds-icon [name]=\"resultChipIcon(chip)\" [size]=\"14\" />\n {{ chip }}\n </span>\n } @empty {\n <span class=\"text-sm text-zinc-500\">\u2014</span>\n }\n </div>\n </td>\n\n <td class=\"whitespace-nowrap border-b border-slate-100 px-4 py-4 align-middle text-sm tabular-nums text-zinc-700\">\n {{ row.durationLabel ?? formatDuration(row.duration) }}\n </td>\n\n <td class=\"w-14 border-b border-slate-100 px-4 py-4 text-center align-middle text-sm text-zinc-900\">\n @if (row.review === 'reviewed') {\n <span class=\"inline-flex text-green-600\" 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=\"px-4 py-4 text-sm text-zinc-500\">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__history-row--needs-review td:first-child{box-shadow:inset 3px 0 #f59e0b}.dashboard-page__avatar ::ng-deep .eds-avatar{width:36px;height:36px;border-radius:10px;font-size:12px}\n"] }]
|
|
436
|
+
}] });
|
|
437
|
+
|
|
438
|
+
export { DashboardPageComponent };
|
|
439
|
+
//# sourceMappingURL=edvak99-voice-receptionist-widget-dashboard-page.component-E30a8p3d.mjs.map
|