@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.
- package/README.md +3 -0
- package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-DT9UKvQO.mjs +307 -0
- package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-DT9UKvQO.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-CMhknpGf.mjs → edvak99-voice-receptionist-widget-ai-receptionist-api.service-48cifVPF.mjs} +1351 -579
- package/fesm2022/edvak99-voice-receptionist-widget-ai-receptionist-api.service-48cifVPF.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-C5inEHJw.mjs +278 -0
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-C5inEHJw.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-sYhM20CE.mjs +418 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-sYhM20CE.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DpwajEOE.mjs +194 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DpwajEOE.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-call-recording-player.component-AN6GcSDq.mjs → edvak99-voice-receptionist-widget-call-recording-player.component-CgkRcFOl.mjs} +5 -4
- 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
- package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-Cy_Bf7AO.mjs → edvak99-voice-receptionist-widget-call-review.service-CIMcjGKX.mjs} +2 -2
- 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
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-Deh3jPgo.mjs +21 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-Deh3jPgo.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-CxbO0eDS.mjs +440 -0
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-CxbO0eDS.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-eds-elapsed.pipe-ouRlPpwT.mjs → edvak99-voice-receptionist-widget-dashboard-socket.service-BX91r74O.mjs} +5 -21
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-socket.service-BX91r74O.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-CWRiJGfv.mjs +38 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-CWRiJGfv.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-button.component-Bg5T5BN8.mjs +27 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-button.component-Bg5T5BN8.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-BFCiJOT3.mjs +22 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-BFCiJOT3.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-live-calls-list.service-CDtFRW8w.mjs → edvak99-voice-receptionist-widget-eds-pulse-dot.component-BPY6xN7C.mjs} +15 -5
- package/fesm2022/edvak99-voice-receptionist-widget-eds-pulse-dot.component-BPY6xN7C.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BsBQIS_f.mjs +203 -0
- package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-BsBQIS_f.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-CaSDMxpV.mjs +81 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-CaSDMxpV.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-Cy3ND-c-.mjs +1070 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-Cy3ND-c-.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-modal.component-COfi7biN.mjs +48 -0
- package/fesm2022/edvak99-voice-receptionist-widget-modal.component-COfi7biN.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-Bdh4QfFe.mjs +127 -0
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-Bdh4QfFe.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-fHVzpiVk.mjs +268 -0
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-fHVzpiVk.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-Bok64X2q.mjs +968 -0
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-Bok64X2q.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-Bb9lPcYi.mjs +317 -0
- package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-Bb9lPcYi.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-wizard-page.component-CJLa5pia.mjs → edvak99-voice-receptionist-widget-wizard-page.component-BZVlWLWh.mjs} +6 -4
- package/fesm2022/{edvak99-voice-receptionist-widget-wizard-page.component-CJLa5pia.mjs.map → edvak99-voice-receptionist-widget-wizard-page.component-BZVlWLWh.mjs.map} +1 -1
- package/fesm2022/edvak99-voice-receptionist-widget.mjs +1 -1
- package/lib/components/call-conversation-panel/call-conversation-panel.component.d.ts +9 -6
- package/lib/components/call-tasks-panel/call-tasks-panel.component.d.ts +8 -8
- package/lib/components/knowledge-test-modal/knowledge-test-modal.component.d.ts +28 -0
- package/lib/components/live-transcript-panel/live-transcript-panel.component.d.ts +7 -4
- package/lib/components/outbound-dial-modal/outbound-dial-modal.component.d.ts +26 -0
- package/lib/layout/eds-module-nav.component.d.ts +0 -3
- package/lib/mocks/agent-activity.seed.d.ts +2 -0
- package/lib/models/agent-activity.types.d.ts +54 -0
- package/lib/models/ai-receptionist.types.d.ts +26 -0
- package/lib/models/api-dto.types.d.ts +18 -0
- package/lib/models/live-transcript.types.d.ts +2 -1
- package/lib/models/outbound-patient.types.d.ts +23 -0
- package/lib/models/outbound-purpose.types.d.ts +2 -0
- package/lib/models/pagination.d.ts +11 -0
- package/lib/models/staff-media.types.d.ts +36 -0
- package/lib/pages/activities-page/activities-page.component.d.ts +57 -0
- package/lib/pages/analytics-page/analytics-page.component.d.ts +38 -14
- package/lib/pages/call-history-detail-page/call-history-detail-page.component.d.ts +2 -3
- package/lib/pages/call-history-page/call-history-page.component.d.ts +16 -9
- package/lib/pages/dashboard-page/dashboard-page.component.d.ts +12 -8
- package/lib/pages/live-calls-page/live-calls-page.component.d.ts +14 -24
- package/lib/pages/live-monitor-page/live-monitor-page.component.d.ts +34 -1
- package/lib/pages/settings-page/settings-fields.config.d.ts +16 -0
- package/lib/pages/settings-page/settings-page.component.d.ts +48 -14
- package/lib/pages/tasks-page/tasks-page.component.d.ts +6 -0
- package/lib/services/ai-receptionist-api.mock.d.ts +44 -9
- package/lib/services/ai-receptionist-api.service.d.ts +65 -10
- package/lib/services/live-transcript.service.d.ts +1 -0
- package/lib/services/staff-media.service.d.ts +52 -0
- package/lib/ui/eds-avatar.component.d.ts +5 -2
- package/lib/ui/eds-button.component.d.ts +1 -1
- package/lib/utils/call-history-display.util.d.ts +21 -0
- package/lib/utils/outbound-purpose.util.d.ts +19 -0
- package/lib/utils/transcript-timing.util.d.ts +1 -1
- package/lib/utils/welcome-message.util.d.ts +1 -1
- package/package.json +2 -1
- package/styles/_index.scss +5 -40
- package/styles/chat-ui.scss +18 -18
- package/styles/eds.scss +188 -119
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-CW5OKmik.mjs +0 -113
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-CW5OKmik.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-DL1GfQyF.mjs +0 -35
- package/fesm2022/edvak99-voice-receptionist-widget-audit-page.component-DL1GfQyF.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-CTwfUHUf.mjs +0 -316
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-CTwfUHUf.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-Cj4wxd4e.mjs +0 -138
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-Cj4wxd4e.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-DQLIw7xb.mjs +0 -70
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-DQLIw7xb.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-DzdIt2A7.mjs +0 -248
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-DzdIt2A7.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-PCnIWeOA.mjs +0 -27
- package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-PCnIWeOA.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-ouRlPpwT.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-CMhknpGf.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-knowledge-hub.page-cZqBuull.mjs +0 -575
- package/fesm2022/edvak99-voice-receptionist-widget-knowledge-hub.page-cZqBuull.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-list.service-CDtFRW8w.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-DrYzXJQW.mjs +0 -115
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-DrYzXJQW.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-D7dlk0y2.mjs +0 -574
- package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-D7dlk0y2.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-BQECIuJu.mjs +0 -54
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purpose.util-BQECIuJu.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-CJ8NEjcj.mjs +0 -265
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-CJ8NEjcj.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-Bo3XNK3b.mjs +0 -509
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-Bo3XNK3b.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-task.util-DdtI0CR3.mjs +0 -52
- package/fesm2022/edvak99-voice-receptionist-widget-task.util-DdtI0CR3.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-CBwl15NP.mjs +0 -237
- package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-CBwl15NP.mjs.map +0 -1
- package/lib/knowledge/components/faq-empty-state/faq-empty-state.component.d.ts +0 -22
- package/lib/knowledge/components/faq-form/faq-form.component.d.ts +0 -27
- package/lib/knowledge/components/faq-search-tester/faq-search-tester.component.d.ts +0 -17
- package/lib/knowledge/mocks/practice-faq.seed.d.ts +0 -2
- package/lib/knowledge/models/practice-faq.model.d.ts +0 -47
- package/lib/knowledge/pages/knowledge-hub.page.d.ts +0 -47
- package/lib/knowledge/services/practice-faq-api.mock.d.ts +0 -16
- package/lib/knowledge/services/practice-faq-api.service.d.ts +0 -22
- package/lib/knowledge/utils/faq.util.d.ts +0 -3
- package/lib/layout/eds-topbar.component.d.ts +0 -29
- package/lib/pages/audit-page/audit-page.component.d.ts +0 -14
- 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
|