@edvak99/voice-receptionist-widget 1.1.11 → 1.1.13
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 +23 -14
- package/fesm2022/{edvak99-voice-receptionist-widget-activities-page.component-CACfxM4x.mjs → edvak99-voice-receptionist-widget-activities-page.component-Br2Vi1Im.mjs} +3 -4
- package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-Br2Vi1Im.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-analytics-page.component-Dj9vw97B.mjs → edvak99-voice-receptionist-widget-analytics-page.component-CglgGV6H.mjs} +2 -3
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-CglgGV6H.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-api-log.util-C0TOKF1q.mjs.map +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-api-logs-page.component-jKBA1N2A.mjs → edvak99-voice-receptionist-widget-api-logs-page.component-DPJiuNjk.mjs} +7 -6
- package/fesm2022/edvak99-voice-receptionist-widget-api-logs-page.component-DPJiuNjk.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-api-logs-table.component-CEf4NPe5.mjs → edvak99-voice-receptionist-widget-api-logs-table.component-CiNc65dO.mjs} +2 -3
- package/fesm2022/edvak99-voice-receptionist-widget-api-logs-table.component-CiNc65dO.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-call-history-detail-page.component-DEplWBbz.mjs → edvak99-voice-receptionist-widget-call-history-detail-page.component-ENq2c-sm.mjs} +5 -7
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-ENq2c-sm.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DoYJFpSa.mjs +200 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-DoYJFpSa.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-call-recording-player.component-j1mY5wU0.mjs → edvak99-voice-receptionist-widget-call-recording-player.component-GVg1g-Vq.mjs} +2 -3
- package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-GVg1g-Vq.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-call-review.service-DQ4TT_Su.mjs → edvak99-voice-receptionist-widget-call-review.service-WG30w6Jl.mjs} +2 -2
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.service-WG30w6Jl.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.util-Deh3jPgo.mjs.map +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-dashboard-page.component-DPxQ-wst.mjs → edvak99-voice-receptionist-widget-dashboard-page.component-B_EYljRs.mjs} +4 -5
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-B_EYljRs.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-avatar.component-BJs_TH8X.mjs.map +1 -1
- package/fesm2022/edvak99-voice-receptionist-widget-eds-elapsed.pipe-BFCiJOT3.mjs.map +1 -1
- package/fesm2022/edvak99-voice-receptionist-widget-eds-pulse-dot.component-B8yxY-7p.mjs +14 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-pulse-dot.component-B8yxY-7p.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-search-field.component-OGRDvYBp.mjs +83 -0
- package/fesm2022/edvak99-voice-receptionist-widget-eds-search-field.component-OGRDvYBp.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-ai-receptionist-api.service-DcRiHn8q.mjs → edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-pS2jeB4x.mjs} +612 -8
- package/fesm2022/edvak99-voice-receptionist-widget-edvak99-voice-receptionist-widget-pS2jeB4x.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-1woGuh0J.mjs +74 -0
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-1woGuh0J.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-live-monitor-page.component-BOQeTD3P.mjs → edvak99-voice-receptionist-widget-live-monitor-page.component-ihwm1LNn.mjs} +5 -7
- package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-ihwm1LNn.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-modal.component-uYApl1KF.mjs.map +1 -1
- package/fesm2022/{edvak99-voice-receptionist-widget-outbound-purposes-page.component-D407n7l0.mjs → edvak99-voice-receptionist-widget-outbound-purposes-page.component-B2koBFU3.mjs} +2 -3
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-B2koBFU3.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-settings-page.component-YAvQ_XV6.mjs → edvak99-voice-receptionist-widget-settings-page.component-C6F-Hgmk.mjs} +2 -3
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-C6F-Hgmk.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-task-detail-drawer.component-B3KmFJNH.mjs → edvak99-voice-receptionist-widget-task-detail-drawer.component-BojdiNNx.mjs} +2 -2
- package/fesm2022/edvak99-voice-receptionist-widget-task-detail-drawer.component-BojdiNNx.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-tasks-page.component-BIOGiTgM.mjs → edvak99-voice-receptionist-widget-tasks-page.component-Cf76XyCz.mjs} +3 -4
- package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-Cf76XyCz.mjs.map +1 -0
- package/fesm2022/{edvak99-voice-receptionist-widget-wizard-page.component-DpF6y1go.mjs → edvak99-voice-receptionist-widget-wizard-page.component-BQTlz2F5.mjs} +2 -3
- package/fesm2022/edvak99-voice-receptionist-widget-wizard-page.component-BQTlz2F5.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget.mjs +1 -262
- package/fesm2022/edvak99-voice-receptionist-widget.mjs.map +1 -1
- package/lib/layout/eds-app-shell.component.d.ts +5 -0
- package/lib/layout/eds-route-progress.component.d.ts +15 -0
- package/lib/models/ai-receptionist.types.d.ts +2 -0
- package/lib/models/api-dto.types.d.ts +2 -0
- package/lib/pages/live-calls-page/live-calls-page.component.d.ts +1 -4
- package/lib/pages/live-calls-page/live-calls-shell.component.d.ts +13 -0
- package/lib/services/live-calls-list.service.d.ts +2 -0
- package/lib/ui/eds-search-field.component.d.ts +22 -0
- package/lib/utils/call-history-display.util.d.ts +1 -0
- package/package.json +1 -1
- package/public-api.d.ts +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-activities-page.component-CACfxM4x.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-ai-receptionist-api.service-DcRiHn8q.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-analytics-page.component-Dj9vw97B.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-api-logs-page.component-jKBA1N2A.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-api-logs-table.component-CEf4NPe5.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-detail-page.component-DEplWBbz.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-CkjKCteR.mjs +0 -193
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-CkjKCteR.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-j1mY5wU0.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-call-review.service-DQ4TT_Su.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-DPxQ-wst.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-socket.service-tTQIsZO6.mjs +0 -120
- package/fesm2022/edvak99-voice-receptionist-widget-dashboard-socket.service-tTQIsZO6.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-eds-pulse-dot.component-CUWZfYRR.mjs +0 -130
- package/fesm2022/edvak99-voice-receptionist-widget-eds-pulse-dot.component-CUWZfYRR.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-BG9cB3zW.mjs +0 -81
- package/fesm2022/edvak99-voice-receptionist-widget-live-calls-page.component-BG9cB3zW.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-live-monitor-page.component-BOQeTD3P.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-outbound-purposes-page.component-D407n7l0.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-settings-page.component-YAvQ_XV6.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-task-detail-drawer.component-B3KmFJNH.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-tasks-page.component-BIOGiTgM.mjs.map +0 -1
- package/fesm2022/edvak99-voice-receptionist-widget-wizard-page.component-DpF6y1go.mjs.map +0 -1
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, signal, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
-
import { ActivatedRoute, Router } from '@angular/router';
|
|
4
|
-
import { DatePipe } from '@angular/common';
|
|
5
|
-
import * as i1 from '@angular/forms';
|
|
6
|
-
import { FormsModule } from '@angular/forms';
|
|
7
|
-
import { A as AiReceptionistApiService, D as DEFAULT_PAGE_SIZE, c as callerMetaLine, r as resultChipIcon, h as historyResultChips, b as historyResolution, a as historyResolutionLabel, g as totalPages } from './edvak99-voice-receptionist-widget-ai-receptionist-api.service-DcRiHn8q.mjs';
|
|
8
|
-
import { C as CallReviewService } from './edvak99-voice-receptionist-widget-call-review.service-DQ4TT_Su.mjs';
|
|
9
|
-
import { i as isNeedsReview } from './edvak99-voice-receptionist-widget-call-review.util-Deh3jPgo.mjs';
|
|
10
|
-
import { E as EdsAvatarComponent } from './edvak99-voice-receptionist-widget-eds-avatar.component-BJs_TH8X.mjs';
|
|
11
|
-
import { ToastService, EdsButtonComponent, EdsIconComponent } from './edvak99-voice-receptionist-widget.mjs';
|
|
12
|
-
|
|
13
|
-
class CallHistoryPageComponent {
|
|
14
|
-
constructor() {
|
|
15
|
-
this.api = inject(AiReceptionistApiService);
|
|
16
|
-
this.reviewService = inject(CallReviewService);
|
|
17
|
-
this.toast = inject(ToastService);
|
|
18
|
-
this.route = inject(ActivatedRoute);
|
|
19
|
-
this.router = inject(Router);
|
|
20
|
-
this.pageSize = DEFAULT_PAGE_SIZE;
|
|
21
|
-
this.rows = signal([]);
|
|
22
|
-
this.total = signal(0);
|
|
23
|
-
this.page = signal(1);
|
|
24
|
-
this.loading = signal(true);
|
|
25
|
-
this.reviewingId = signal(null);
|
|
26
|
-
this.search = signal('');
|
|
27
|
-
this.direction = signal('');
|
|
28
|
-
this.outcome = signal('');
|
|
29
|
-
this.location = signal('');
|
|
30
|
-
this.reviewTab = signal('all');
|
|
31
|
-
this.reviewTabs = [
|
|
32
|
-
{ id: 'all', label: 'All' },
|
|
33
|
-
{ id: 'needs', label: 'Needs review' },
|
|
34
|
-
{ id: 'reviewed', label: 'Reviewed' },
|
|
35
|
-
];
|
|
36
|
-
this.isNeedsReview = isNeedsReview;
|
|
37
|
-
this.callerMetaLine = callerMetaLine;
|
|
38
|
-
this.resultChipIcon = resultChipIcon;
|
|
39
|
-
this.historyResultChips = historyResultChips;
|
|
40
|
-
this.historyResolution = historyResolution;
|
|
41
|
-
this.historyResolutionLabel = historyResolutionLabel;
|
|
42
|
-
}
|
|
43
|
-
ngOnInit() {
|
|
44
|
-
const reviewParam = this.route.snapshot.queryParamMap.get('review');
|
|
45
|
-
if (reviewParam === 'needs' || reviewParam === 'reviewed') {
|
|
46
|
-
this.reviewTab.set(reviewParam);
|
|
47
|
-
}
|
|
48
|
-
this.load();
|
|
49
|
-
}
|
|
50
|
-
load() {
|
|
51
|
-
this.loading.set(true);
|
|
52
|
-
const tab = this.reviewTab();
|
|
53
|
-
this.api
|
|
54
|
-
.getHistory({
|
|
55
|
-
q: this.search() || undefined,
|
|
56
|
-
dir: this.direction() || undefined,
|
|
57
|
-
outcome: this.outcome() || undefined,
|
|
58
|
-
location: this.location() || undefined,
|
|
59
|
-
review: tab === 'all' ? undefined : tab,
|
|
60
|
-
page: this.page(),
|
|
61
|
-
limit: DEFAULT_PAGE_SIZE,
|
|
62
|
-
})
|
|
63
|
-
.subscribe({
|
|
64
|
-
next: ({ items, total, page }) => {
|
|
65
|
-
this.rows.set(items);
|
|
66
|
-
this.total.set(total);
|
|
67
|
-
this.page.set(page);
|
|
68
|
-
this.loading.set(false);
|
|
69
|
-
},
|
|
70
|
-
error: () => this.loading.set(false),
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
applyFilters() {
|
|
74
|
-
this.page.set(1);
|
|
75
|
-
this.load();
|
|
76
|
-
}
|
|
77
|
-
setReviewTab(tab) {
|
|
78
|
-
this.reviewTab.set(tab);
|
|
79
|
-
this.page.set(1);
|
|
80
|
-
void this.router.navigate([], {
|
|
81
|
-
relativeTo: this.route,
|
|
82
|
-
queryParams: tab === 'all' ? { review: null } : { review: tab },
|
|
83
|
-
queryParamsHandling: 'merge',
|
|
84
|
-
replaceUrl: true,
|
|
85
|
-
});
|
|
86
|
-
this.load();
|
|
87
|
-
}
|
|
88
|
-
tabBadgeCount(tab) {
|
|
89
|
-
if (tab !== 'needs' || this.reviewTab() !== 'needs')
|
|
90
|
-
return null;
|
|
91
|
-
const count = this.total();
|
|
92
|
-
return count > 0 ? count : null;
|
|
93
|
-
}
|
|
94
|
-
totalPages() {
|
|
95
|
-
return totalPages(this.total(), DEFAULT_PAGE_SIZE);
|
|
96
|
-
}
|
|
97
|
-
prevPage() {
|
|
98
|
-
if (this.page() <= 1)
|
|
99
|
-
return;
|
|
100
|
-
this.page.update((p) => p - 1);
|
|
101
|
-
this.load();
|
|
102
|
-
}
|
|
103
|
-
nextPage() {
|
|
104
|
-
if (this.page() >= this.totalPages())
|
|
105
|
-
return;
|
|
106
|
-
this.page.update((p) => p + 1);
|
|
107
|
-
this.load();
|
|
108
|
-
}
|
|
109
|
-
markReviewed(row, event) {
|
|
110
|
-
event.preventDefault();
|
|
111
|
-
event.stopPropagation();
|
|
112
|
-
if (!isNeedsReview(row.review) || this.reviewingId())
|
|
113
|
-
return;
|
|
114
|
-
this.reviewingId.set(row.id);
|
|
115
|
-
this.rows.update((list) => list.map((r) => (r.id === row.id ? { ...r, review: 'reviewed' } : r)));
|
|
116
|
-
this.reviewService.markReviewed(row.id).subscribe({
|
|
117
|
-
next: () => {
|
|
118
|
-
this.reviewingId.set(null);
|
|
119
|
-
this.toast.show('Call marked as reviewed', 'success');
|
|
120
|
-
if (this.reviewTab() === 'needs')
|
|
121
|
-
this.load();
|
|
122
|
-
},
|
|
123
|
-
error: () => {
|
|
124
|
-
this.reviewingId.set(null);
|
|
125
|
-
this.rows.update((list) => list.map((r) => (r.id === row.id ? { ...r, review: 'needs' } : r)));
|
|
126
|
-
this.toast.show('Failed to mark call as reviewed', 'error');
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
formatDuration(sec) {
|
|
131
|
-
const m = Math.floor(sec / 60);
|
|
132
|
-
const s = sec % 60;
|
|
133
|
-
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
134
|
-
}
|
|
135
|
-
openCallDetail(id) {
|
|
136
|
-
void this.router.navigate(['../history', id], { relativeTo: this.route });
|
|
137
|
-
}
|
|
138
|
-
avatarTone(row) {
|
|
139
|
-
if (!row.name?.trim() || /^unknown/i.test(row.name) || row.initials === '?')
|
|
140
|
-
return 'gray';
|
|
141
|
-
return 'brand';
|
|
142
|
-
}
|
|
143
|
-
resolutionOf(row) {
|
|
144
|
-
return historyResolution({ outcome: row.outcome, risk: row.risk });
|
|
145
|
-
}
|
|
146
|
-
exportCsv() {
|
|
147
|
-
const header = [
|
|
148
|
-
'id',
|
|
149
|
-
'name',
|
|
150
|
-
'phone',
|
|
151
|
-
'direction',
|
|
152
|
-
'goal',
|
|
153
|
-
'handledBy',
|
|
154
|
-
'outcome',
|
|
155
|
-
'results',
|
|
156
|
-
'duration',
|
|
157
|
-
'started',
|
|
158
|
-
'review',
|
|
159
|
-
];
|
|
160
|
-
const csvRows = this.rows().map((r) => [
|
|
161
|
-
r.id,
|
|
162
|
-
r.name,
|
|
163
|
-
r.phone,
|
|
164
|
-
r.direction,
|
|
165
|
-
r.reason,
|
|
166
|
-
r.handledBy ?? '',
|
|
167
|
-
r.outcome,
|
|
168
|
-
(r.results ?? []).join('; '),
|
|
169
|
-
String(r.duration),
|
|
170
|
-
String(r.started),
|
|
171
|
-
r.review,
|
|
172
|
-
]);
|
|
173
|
-
const csv = [header, ...csvRows]
|
|
174
|
-
.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(','))
|
|
175
|
-
.join('\n');
|
|
176
|
-
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
|
177
|
-
const url = URL.createObjectURL(blob);
|
|
178
|
-
const a = document.createElement('a');
|
|
179
|
-
a.href = url;
|
|
180
|
-
a.download = `call-history-page-${this.page()}.csv`;
|
|
181
|
-
a.click();
|
|
182
|
-
URL.revokeObjectURL(url);
|
|
183
|
-
}
|
|
184
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: CallHistoryPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
185
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: CallHistoryPageComponent, isStandalone: true, selector: "eds-call-history-page", ngImport: i0, template: "<div class=\"eds-page call-history-page\">\n <header class=\"mb-5 flex items-start justify-between\">\n <div>\n <h1 class=\"eds-h1\">Call History</h1>\n <p class=\"eds-body-sm\">Search and review completed conversations</p>\n </div>\n <eds-button variant=\"secondary\" (click)=\"exportCsv()\">\n <eds-icon name=\"download\" [size]=\"18\" /> Export CSV\n </eds-button>\n </header>\n\n <div class=\"eds-card mb-4 flex flex-wrap items-center gap-3 p-4 shadow-sm\">\n <input\n type=\"search\"\n class=\"min-w-52 flex-1 rounded-md border border-zinc-200 px-3 py-2 text-sm\"\n placeholder=\"Search name, phone, reason\u2026\"\n [ngModel]=\"search()\"\n (ngModelChange)=\"search.set($event)\"\n (keyup.enter)=\"applyFilters()\"\n autocomplete=\"off\"\n />\n <select\n class=\"rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm\"\n [ngModel]=\"direction()\"\n (ngModelChange)=\"direction.set($event)\"\n autocomplete=\"off\"\n >\n <option value=\"\">All directions</option>\n <option value=\"inbound\">Inbound</option>\n <option value=\"outbound\">Outbound</option>\n </select>\n <input\n type=\"text\"\n class=\"rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm\"\n placeholder=\"Outcome\"\n [ngModel]=\"outcome()\"\n (ngModelChange)=\"outcome.set($event)\"\n autocomplete=\"off\"\n />\n <input\n type=\"text\"\n class=\"rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm\"\n placeholder=\"Location\"\n [ngModel]=\"location()\"\n (ngModelChange)=\"location.set($event)\"\n autocomplete=\"off\"\n />\n <eds-button variant=\"primary\" (click)=\"applyFilters()\">Apply</eds-button>\n </div>\n\n @if (loading()) {\n <p class=\"eds-body-sm\">Loading history\u2026</p>\n } @else {\n <div class=\"eds-card overflow-x-auto p-0 shadow-sm\">\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 rows(); track row.id) {\n <tr\n class=\"call-history-page__row cursor-pointer select-none transition-colors hover:bg-zinc-50 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600\"\n [class.call-history-page__row--needs-review]=\"isNeedsReview(row.review)\"\n tabindex=\"0\"\n role=\"link\"\n [attr.aria-label]=\"'View call details for ' + row.name\"\n (click)=\"openCallDetail(row.id)\"\n (keyup.enter)=\"openCallDetail(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=\"call-history-page__avatar\"\n [initials]=\"row.initials || '?'\"\n [tone]=\"avatarTone(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 } @else {\n <button\n type=\"button\"\n class=\"cursor-pointer rounded-md border-none bg-transparent p-1 leading-none text-zinc-400 hover:bg-green-600/10 hover:text-green-600 disabled:cursor-wait disabled:opacity-50\"\n title=\"Mark as reviewed\"\n [disabled]=\"reviewingId() === row.id\"\n (click)=\"markReviewed(row, $event)\"\n >\n <eds-icon name=\"radio_button_unchecked\" [size]=\"20\" />\n </button>\n }\n </td>\n </tr>\n } @empty {\n <tr>\n <td colspan=\"9\" class=\"px-4 py-7 text-center text-sm text-zinc-500\">No calls match this filter.</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n <footer class=\"mt-4 flex items-center justify-between border-t border-zinc-200 pt-3\">\n <span class=\"eds-body-sm\">\n Page {{ page() }} of {{ totalPages() }} \u00B7 {{ total() }} total\n </span>\n <div class=\"flex gap-2\">\n <eds-button variant=\"secondary\" [disabled]=\"page() <= 1\" (click)=\"prevPage()\">Previous</eds-button>\n <eds-button variant=\"secondary\" [disabled]=\"page() >= totalPages()\" (click)=\"nextPage()\">Next</eds-button>\n </div>\n </footer>\n</div>\n", styles: [".call-history-page__row td{background:transparent;transition:background-color .12s ease;-webkit-user-select:none;user-select:none}.call-history-page__row:hover td{background:#fafafa}.call-history-page__row--needs-review td:first-child{box-shadow:inset 3px 0 #f59e0b}.call-history-page__avatar ::ng-deep .eds-avatar{width:36px;height:36px;border-radius:10px;font-size:12px}\n"], dependencies: [{ kind: "pipe", type: DatePipe, name: "date" }, { kind: "ngmodule", type: FormsModule }, { 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.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
186
|
-
}
|
|
187
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: CallHistoryPageComponent, decorators: [{
|
|
188
|
-
type: Component,
|
|
189
|
-
args: [{ selector: 'eds-call-history-page', imports: [DatePipe, FormsModule, EdsAvatarComponent, EdsButtonComponent, EdsIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"eds-page call-history-page\">\n <header class=\"mb-5 flex items-start justify-between\">\n <div>\n <h1 class=\"eds-h1\">Call History</h1>\n <p class=\"eds-body-sm\">Search and review completed conversations</p>\n </div>\n <eds-button variant=\"secondary\" (click)=\"exportCsv()\">\n <eds-icon name=\"download\" [size]=\"18\" /> Export CSV\n </eds-button>\n </header>\n\n <div class=\"eds-card mb-4 flex flex-wrap items-center gap-3 p-4 shadow-sm\">\n <input\n type=\"search\"\n class=\"min-w-52 flex-1 rounded-md border border-zinc-200 px-3 py-2 text-sm\"\n placeholder=\"Search name, phone, reason\u2026\"\n [ngModel]=\"search()\"\n (ngModelChange)=\"search.set($event)\"\n (keyup.enter)=\"applyFilters()\"\n autocomplete=\"off\"\n />\n <select\n class=\"rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm\"\n [ngModel]=\"direction()\"\n (ngModelChange)=\"direction.set($event)\"\n autocomplete=\"off\"\n >\n <option value=\"\">All directions</option>\n <option value=\"inbound\">Inbound</option>\n <option value=\"outbound\">Outbound</option>\n </select>\n <input\n type=\"text\"\n class=\"rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm\"\n placeholder=\"Outcome\"\n [ngModel]=\"outcome()\"\n (ngModelChange)=\"outcome.set($event)\"\n autocomplete=\"off\"\n />\n <input\n type=\"text\"\n class=\"rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm\"\n placeholder=\"Location\"\n [ngModel]=\"location()\"\n (ngModelChange)=\"location.set($event)\"\n autocomplete=\"off\"\n />\n <eds-button variant=\"primary\" (click)=\"applyFilters()\">Apply</eds-button>\n </div>\n\n @if (loading()) {\n <p class=\"eds-body-sm\">Loading history\u2026</p>\n } @else {\n <div class=\"eds-card overflow-x-auto p-0 shadow-sm\">\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 rows(); track row.id) {\n <tr\n class=\"call-history-page__row cursor-pointer select-none transition-colors hover:bg-zinc-50 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600\"\n [class.call-history-page__row--needs-review]=\"isNeedsReview(row.review)\"\n tabindex=\"0\"\n role=\"link\"\n [attr.aria-label]=\"'View call details for ' + row.name\"\n (click)=\"openCallDetail(row.id)\"\n (keyup.enter)=\"openCallDetail(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=\"call-history-page__avatar\"\n [initials]=\"row.initials || '?'\"\n [tone]=\"avatarTone(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 } @else {\n <button\n type=\"button\"\n class=\"cursor-pointer rounded-md border-none bg-transparent p-1 leading-none text-zinc-400 hover:bg-green-600/10 hover:text-green-600 disabled:cursor-wait disabled:opacity-50\"\n title=\"Mark as reviewed\"\n [disabled]=\"reviewingId() === row.id\"\n (click)=\"markReviewed(row, $event)\"\n >\n <eds-icon name=\"radio_button_unchecked\" [size]=\"20\" />\n </button>\n }\n </td>\n </tr>\n } @empty {\n <tr>\n <td colspan=\"9\" class=\"px-4 py-7 text-center text-sm text-zinc-500\">No calls match this filter.</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n <footer class=\"mt-4 flex items-center justify-between border-t border-zinc-200 pt-3\">\n <span class=\"eds-body-sm\">\n Page {{ page() }} of {{ totalPages() }} \u00B7 {{ total() }} total\n </span>\n <div class=\"flex gap-2\">\n <eds-button variant=\"secondary\" [disabled]=\"page() <= 1\" (click)=\"prevPage()\">Previous</eds-button>\n <eds-button variant=\"secondary\" [disabled]=\"page() >= totalPages()\" (click)=\"nextPage()\">Next</eds-button>\n </div>\n </footer>\n</div>\n", styles: [".call-history-page__row td{background:transparent;transition:background-color .12s ease;-webkit-user-select:none;user-select:none}.call-history-page__row:hover td{background:#fafafa}.call-history-page__row--needs-review td:first-child{box-shadow:inset 3px 0 #f59e0b}.call-history-page__avatar ::ng-deep .eds-avatar{width:36px;height:36px;border-radius:10px;font-size:12px}\n"] }]
|
|
190
|
-
}] });
|
|
191
|
-
|
|
192
|
-
export { CallHistoryPageComponent };
|
|
193
|
-
//# sourceMappingURL=edvak99-voice-receptionist-widget-call-history-page.component-CkjKCteR.mjs.map
|
package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-CkjKCteR.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"edvak99-voice-receptionist-widget-call-history-page.component-CkjKCteR.mjs","sources":["../../../projects/voice-receptionist-widget/src/lib/pages/call-history-page/call-history-page.component.ts","../../../projects/voice-receptionist-widget/src/lib/pages/call-history-page/call-history-page.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { DatePipe } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\n\nimport { HistoryCall } from '../../models/ai-receptionist.types';\nimport { DEFAULT_PAGE_SIZE, totalPages as calcTotalPages } from '../../models/pagination';\nimport { AiReceptionistApiService } from '../../services/ai-receptionist-api.service';\nimport { CallReviewService } from '../../services/call-review.service';\nimport {\n callerMetaLine,\n historyResolution,\n historyResolutionLabel,\n historyResultChips,\n resultChipIcon,\n} from '../../utils/call-history-display.util';\nimport { isNeedsReview, ReviewFilterTab } from '../../utils/call-review.util';\nimport { EdsAvatarComponent } from '../../ui/eds-avatar.component';\nimport { EdsButtonComponent } from '../../ui/eds-button.component';\nimport { EdsIconComponent } from '../../ui/eds-icon.component';\nimport { ToastService } from '../../shared/toast.service';\n\n@Component({\n selector: 'eds-call-history-page',\n imports: [DatePipe, FormsModule, EdsAvatarComponent, EdsButtonComponent, EdsIconComponent],\n templateUrl: './call-history-page.component.html',\n styleUrl: './call-history-page.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class CallHistoryPageComponent implements OnInit {\n private readonly api = inject(AiReceptionistApiService);\n private readonly reviewService = inject(CallReviewService);\n private readonly toast = inject(ToastService);\n private readonly route = inject(ActivatedRoute);\n private readonly router = inject(Router);\n\n protected readonly pageSize = DEFAULT_PAGE_SIZE;\n protected readonly rows = signal<HistoryCall[]>([]);\n protected readonly total = signal(0);\n protected readonly page = signal(1);\n protected readonly loading = signal(true);\n protected readonly reviewingId = signal<string | null>(null);\n protected readonly search = signal('');\n protected readonly direction = signal('');\n protected readonly outcome = signal('');\n protected readonly location = signal('');\n protected readonly reviewTab = signal<ReviewFilterTab>('all');\n\n protected readonly reviewTabs: { id: ReviewFilterTab; label: string }[] = [\n { id: 'all', label: 'All' },\n { id: 'needs', label: 'Needs review' },\n { id: 'reviewed', label: 'Reviewed' },\n ];\n\n protected isNeedsReview = isNeedsReview;\n protected callerMetaLine = callerMetaLine;\n protected resultChipIcon = resultChipIcon;\n protected historyResultChips = historyResultChips;\n protected historyResolution = historyResolution;\n protected historyResolutionLabel = historyResolutionLabel;\n\n ngOnInit(): void {\n const reviewParam = this.route.snapshot.queryParamMap.get('review');\n if (reviewParam === 'needs' || reviewParam === 'reviewed') {\n this.reviewTab.set(reviewParam);\n }\n this.load();\n }\n\n protected load(): void {\n this.loading.set(true);\n const tab = this.reviewTab();\n this.api\n .getHistory({\n q: this.search() || undefined,\n dir: this.direction() || undefined,\n outcome: this.outcome() || undefined,\n location: this.location() || undefined,\n review: tab === 'all' ? undefined : tab,\n page: this.page(),\n limit: DEFAULT_PAGE_SIZE,\n })\n .subscribe({\n next: ({ items, total, page }) => {\n this.rows.set(items);\n this.total.set(total);\n this.page.set(page);\n this.loading.set(false);\n },\n error: () => this.loading.set(false),\n });\n }\n\n protected applyFilters(): void {\n this.page.set(1);\n this.load();\n }\n\n protected setReviewTab(tab: ReviewFilterTab): void {\n this.reviewTab.set(tab);\n this.page.set(1);\n void this.router.navigate([], {\n relativeTo: this.route,\n queryParams: tab === 'all' ? { review: null } : { review: tab },\n queryParamsHandling: 'merge',\n replaceUrl: true,\n });\n this.load();\n }\n\n protected tabBadgeCount(tab: ReviewFilterTab): number | null {\n if (tab !== 'needs' || this.reviewTab() !== 'needs') return null;\n const count = this.total();\n return count > 0 ? count : null;\n }\n\n protected totalPages(): number {\n return calcTotalPages(this.total(), DEFAULT_PAGE_SIZE);\n }\n\n protected prevPage(): void {\n if (this.page() <= 1) return;\n this.page.update((p) => p - 1);\n this.load();\n }\n\n protected nextPage(): void {\n if (this.page() >= this.totalPages()) return;\n this.page.update((p) => p + 1);\n this.load();\n }\n\n protected markReviewed(row: HistoryCall, event: Event): void {\n event.preventDefault();\n event.stopPropagation();\n if (!isNeedsReview(row.review) || this.reviewingId()) return;\n\n this.reviewingId.set(row.id);\n this.rows.update((list) =>\n list.map((r) => (r.id === row.id ? { ...r, review: 'reviewed' as const } : r)),\n );\n\n this.reviewService.markReviewed(row.id).subscribe({\n next: () => {\n this.reviewingId.set(null);\n this.toast.show('Call marked as reviewed', 'success');\n if (this.reviewTab() === 'needs') this.load();\n },\n error: () => {\n this.reviewingId.set(null);\n this.rows.update((list) =>\n list.map((r) => (r.id === row.id ? { ...r, review: 'needs' as const } : r)),\n );\n this.toast.show('Failed to mark call as reviewed', 'error');\n },\n });\n }\n\n protected formatDuration(sec: number): string {\n const m = Math.floor(sec / 60);\n const s = sec % 60;\n return `${m}:${s.toString().padStart(2, '0')}`;\n }\n\n protected openCallDetail(id: string): void {\n void this.router.navigate(['../history', id], { relativeTo: this.route });\n }\n\n protected avatarTone(row: HistoryCall): 'gray' | 'brand' {\n if (!row.name?.trim() || /^unknown/i.test(row.name) || row.initials === '?') return 'gray';\n return 'brand';\n }\n\n protected resolutionOf(row: HistoryCall) {\n return historyResolution({ outcome: row.outcome, risk: row.risk });\n }\n\n protected exportCsv(): void {\n const header = [\n 'id',\n 'name',\n 'phone',\n 'direction',\n 'goal',\n 'handledBy',\n 'outcome',\n 'results',\n 'duration',\n 'started',\n 'review',\n ];\n const csvRows = this.rows().map((r) => [\n r.id,\n r.name,\n r.phone,\n r.direction,\n r.reason,\n r.handledBy ?? '',\n r.outcome,\n (r.results ?? []).join('; '),\n String(r.duration),\n String(r.started),\n r.review,\n ]);\n const csv = [header, ...csvRows]\n .map((row) => row.map((cell) => `\"${String(cell).replace(/\"/g, '\"\"')}\"`).join(','))\n .join('\\n');\n const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = `call-history-page-${this.page()}.csv`;\n a.click();\n URL.revokeObjectURL(url);\n }\n}\n","<div class=\"eds-page call-history-page\">\n <header class=\"mb-5 flex items-start justify-between\">\n <div>\n <h1 class=\"eds-h1\">Call History</h1>\n <p class=\"eds-body-sm\">Search and review completed conversations</p>\n </div>\n <eds-button variant=\"secondary\" (click)=\"exportCsv()\">\n <eds-icon name=\"download\" [size]=\"18\" /> Export CSV\n </eds-button>\n </header>\n\n <div class=\"eds-card mb-4 flex flex-wrap items-center gap-3 p-4 shadow-sm\">\n <input\n type=\"search\"\n class=\"min-w-52 flex-1 rounded-md border border-zinc-200 px-3 py-2 text-sm\"\n placeholder=\"Search name, phone, reason…\"\n [ngModel]=\"search()\"\n (ngModelChange)=\"search.set($event)\"\n (keyup.enter)=\"applyFilters()\"\n autocomplete=\"off\"\n />\n <select\n class=\"rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm\"\n [ngModel]=\"direction()\"\n (ngModelChange)=\"direction.set($event)\"\n autocomplete=\"off\"\n >\n <option value=\"\">All directions</option>\n <option value=\"inbound\">Inbound</option>\n <option value=\"outbound\">Outbound</option>\n </select>\n <input\n type=\"text\"\n class=\"rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm\"\n placeholder=\"Outcome\"\n [ngModel]=\"outcome()\"\n (ngModelChange)=\"outcome.set($event)\"\n autocomplete=\"off\"\n />\n <input\n type=\"text\"\n class=\"rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm\"\n placeholder=\"Location\"\n [ngModel]=\"location()\"\n (ngModelChange)=\"location.set($event)\"\n autocomplete=\"off\"\n />\n <eds-button variant=\"primary\" (click)=\"applyFilters()\">Apply</eds-button>\n </div>\n\n @if (loading()) {\n <p class=\"eds-body-sm\">Loading history…</p>\n } @else {\n <div class=\"eds-card overflow-x-auto p-0 shadow-sm\">\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 rows(); track row.id) {\n <tr\n class=\"call-history-page__row cursor-pointer select-none transition-colors hover:bg-zinc-50 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600\"\n [class.call-history-page__row--needs-review]=\"isNeedsReview(row.review)\"\n tabindex=\"0\"\n role=\"link\"\n [attr.aria-label]=\"'View call details for ' + row.name\"\n (click)=\"openCallDetail(row.id)\"\n (keyup.enter)=\"openCallDetail(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 || '—' }}</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=\"call-history-page__avatar\"\n [initials]=\"row.initials || '?'\"\n [tone]=\"avatarTone(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 || '—' }}</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\">—</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 } @else {\n <button\n type=\"button\"\n class=\"cursor-pointer rounded-md border-none bg-transparent p-1 leading-none text-zinc-400 hover:bg-green-600/10 hover:text-green-600 disabled:cursor-wait disabled:opacity-50\"\n title=\"Mark as reviewed\"\n [disabled]=\"reviewingId() === row.id\"\n (click)=\"markReviewed(row, $event)\"\n >\n <eds-icon name=\"radio_button_unchecked\" [size]=\"20\" />\n </button>\n }\n </td>\n </tr>\n } @empty {\n <tr>\n <td colspan=\"9\" class=\"px-4 py-7 text-center text-sm text-zinc-500\">No calls match this filter.</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n <footer class=\"mt-4 flex items-center justify-between border-t border-zinc-200 pt-3\">\n <span class=\"eds-body-sm\">\n Page {{ page() }} of {{ totalPages() }} · {{ total() }} total\n </span>\n <div class=\"flex gap-2\">\n <eds-button variant=\"secondary\" [disabled]=\"page() <= 1\" (click)=\"prevPage()\">Previous</eds-button>\n <eds-button variant=\"secondary\" [disabled]=\"page() >= totalPages()\" (click)=\"nextPage()\">Next</eds-button>\n </div>\n </footer>\n</div>\n"],"names":["calcTotalPages"],"mappings":"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"}
|
package/fesm2022/edvak99-voice-receptionist-widget-call-recording-player.component-j1mY5wU0.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"edvak99-voice-receptionist-widget-call-recording-player.component-j1mY5wU0.mjs","sources":["../../../projects/voice-receptionist-widget/src/lib/components/call-recording-player/call-recording-player.component.ts","../../../projects/voice-receptionist-widget/src/lib/components/call-recording-player/call-recording-player.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n ElementRef,\n inject,\n input,\n output,\n signal,\n viewChild,\n} from '@angular/core';\nimport { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';\nimport { catchError, distinctUntilChanged, EMPTY, map, switchMap, tap } from 'rxjs';\n\nimport { CallRecordingStatus } from '../../models/ai-receptionist.types';\nimport { AiReceptionistApiService } from '../../services/ai-receptionist-api.service';\nimport { EdsIconComponent } from '../../ui/eds-icon.component';\n\ntype PlayerPhase = 'loading' | CallRecordingStatus | 'error';\n\n@Component({\n selector: 'eds-call-recording-player',\n imports: [EdsIconComponent],\n templateUrl: './call-recording-player.component.html',\n styleUrl: './call-recording-player.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class CallRecordingPlayerComponent {\n readonly callId = input.required<string>();\n readonly currentTimeChange = output<number>();\n readonly durationChange = output<number>();\n\n private readonly audioRef = viewChild<ElementRef<HTMLAudioElement>>('audioEl');\n\n private readonly api = inject(AiReceptionistApiService);\n private readonly destroyRef = inject(DestroyRef);\n\n protected readonly phase = signal<PlayerPhase>('loading');\n protected readonly blobUrl = signal<string | null>(null);\n protected readonly durationSeconds = signal<number | null>(null);\n protected readonly message = signal<string | null>(null);\n\n /** Tracked outside signals so cleanup never reads blobUrl (avoids effect/subscription loops). */\n private objectUrl: string | null = null;\n\n constructor() {\n toObservable(this.callId)\n .pipe(\n distinctUntilChanged(),\n tap(() => this.revokeObjectUrl()),\n switchMap((id) => {\n if (!id) {\n this.resetUi();\n return EMPTY;\n }\n this.phase.set('loading');\n this.message.set(null);\n return this.api.getCallRecording(id).pipe(\n switchMap((meta) => {\n if (meta.status !== 'ready' || !meta.url) {\n this.phase.set(meta.status);\n this.message.set(meta.message ?? null);\n this.durationSeconds.set(meta.durationSeconds ?? null);\n return EMPTY;\n }\n this.durationSeconds.set(meta.durationSeconds ?? null);\n return this.api.getCallRecordingBlob(meta.url).pipe(\n map((blob) => blob),\n catchError(() => {\n this.phase.set('error');\n this.message.set('Could not load recording audio.');\n return EMPTY;\n }),\n );\n }),\n catchError(() => {\n this.phase.set('unavailable');\n this.message.set('Could not load recording metadata.');\n return EMPTY;\n }),\n );\n }),\n takeUntilDestroyed(),\n )\n .subscribe((blob) => {\n this.revokeObjectUrl();\n this.objectUrl = URL.createObjectURL(blob);\n this.blobUrl.set(this.objectUrl);\n this.phase.set('ready');\n this.message.set(null);\n const dur = this.durationSeconds();\n if (dur != null && dur > 0) {\n this.durationChange.emit(dur);\n }\n });\n\n this.destroyRef.onDestroy(() => this.revokeObjectUrl());\n }\n\n protected formatDuration(seconds: number): string {\n const m = Math.floor(seconds / 60);\n const s = seconds % 60;\n return `${m}:${s.toString().padStart(2, '0')}`;\n }\n\n protected onAudioError(): void {\n this.phase.set('error');\n this.message.set('Playback error — try reloading the page.');\n }\n\n protected onTimeUpdate(event: Event): void {\n const el = event.target as HTMLAudioElement;\n this.currentTimeChange.emit(el.currentTime);\n }\n\n protected onLoadedMetadata(event: Event): void {\n const el = event.target as HTMLAudioElement;\n if (Number.isFinite(el.duration) && el.duration > 0) {\n this.durationChange.emit(el.duration);\n }\n }\n\n /** Seek playback — used by synced transcript lines. */\n seekTo(seconds: number): void {\n const el = this.audioRef()?.nativeElement;\n if (!el) return;\n el.currentTime = Math.max(0, seconds);\n void el.play().catch(() => undefined);\n this.currentTimeChange.emit(el.currentTime);\n }\n\n private revokeObjectUrl(): void {\n if (this.objectUrl) {\n URL.revokeObjectURL(this.objectUrl);\n this.objectUrl = null;\n }\n this.blobUrl.set(null);\n }\n\n private resetUi(): void {\n this.phase.set('loading');\n this.durationSeconds.set(null);\n this.message.set(null);\n }\n}\n","<section class=\"call-recording-player flex flex-col gap-3 rounded-[var(--eds-radius-md)] border p-4\" aria-labelledby=\"call-recording-title\">\n <header class=\"flex items-center gap-2\">\n <eds-icon name=\"graphic_eq\" [size]=\"20\" />\n <h3 id=\"call-recording-title\" class=\"eds-h3 m-0 flex-1\">Call recording</h3>\n @if (durationSeconds(); as dur) {\n <span class=\"eds-mono-sm call-recording-player__duration\">{{ formatDuration(dur) }}</span>\n }\n </header>\n\n @switch (phase()) {\n @case ('loading') {\n <p class=\"call-recording-player__status eds-body-sm m-0\">Loading recording…</p>\n }\n @case ('ready') {\n @if (blobUrl(); as src) {\n <audio\n #audioEl\n class=\"h-10 w-full\"\n controls\n [src]=\"src\"\n (error)=\"onAudioError()\"\n (timeupdate)=\"onTimeUpdate($event)\"\n (loadedmetadata)=\"onLoadedMetadata($event)\"\n >\n Your browser does not support audio playback.\n </audio>\n }\n }\n @case ('pending') {\n <p class=\"call-recording-player__status eds-body-sm m-0\">\n {{ message() ?? 'Recording is still processing…' }}\n </p>\n }\n @case ('unavailable') {\n <p class=\"call-recording-player__status eds-body-sm call-recording-player__status--muted m-0\">\n {{ message() ?? 'No recording available for this call.' }}\n </p>\n }\n @case ('failed') {\n <p class=\"call-recording-player__status eds-body-sm call-recording-player__status--error m-0\">\n {{ message() ?? 'Recording could not be retrieved.' }}\n </p>\n }\n @case ('error') {\n <p class=\"call-recording-player__status eds-body-sm call-recording-player__status--error m-0\">\n {{ message() ?? 'Playback error.' }}\n </p>\n }\n }\n</section>\n"],"names":[],"mappings":";;;;;;;MA2Ba,4BAA4B,CAAA;AAkBvC,IAAA,WAAA,GAAA;AAjBS,QAAA,IAAA,CAAA,MAAM,GAAG,KAAK,CAAC,QAAQ,EAAU;QACjC,IAAA,CAAA,iBAAiB,GAAG,MAAM,EAAU;QACpC,IAAA,CAAA,cAAc,GAAG,MAAM,EAAU;AAEzB,QAAA,IAAA,CAAA,QAAQ,GAAG,SAAS,CAA+B,SAAS,CAAC;AAE7D,QAAA,IAAA,CAAA,GAAG,GAAG,MAAM,CAAC,wBAAwB,CAAC;AACtC,QAAA,IAAA,CAAA,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;AAE7B,QAAA,IAAA,CAAA,KAAK,GAAG,MAAM,CAAc,SAAS,CAAC;AACtC,QAAA,IAAA,CAAA,OAAO,GAAG,MAAM,CAAgB,IAAI,CAAC;AACrC,QAAA,IAAA,CAAA,eAAe,GAAG,MAAM,CAAgB,IAAI,CAAC;AAC7C,QAAA,IAAA,CAAA,OAAO,GAAG,MAAM,CAAgB,IAAI,CAAC;;QAGhD,IAAA,CAAA,SAAS,GAAkB,IAAI;AAGrC,QAAA,YAAY,CAAC,IAAI,CAAC,MAAM;aACrB,IAAI,CACH,oBAAoB,EAAE,EACtB,GAAG,CAAC,MAAM,IAAI,CAAC,eAAe,EAAE,CAAC,EACjC,SAAS,CAAC,CAAC,EAAE,KAAI;YACf,IAAI,CAAC,EAAE,EAAE;gBACP,IAAI,CAAC,OAAO,EAAE;AACd,gBAAA,OAAO,KAAK;YACd;AACA,YAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC;AACzB,YAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;AACtB,YAAA,OAAO,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC,IAAI,CACvC,SAAS,CAAC,CAAC,IAAI,KAAI;gBACjB,IAAI,IAAI,CAAC,MAAM,KAAK,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE;oBACxC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC;oBAC3B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC;oBACtC,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,eAAe,IAAI,IAAI,CAAC;AACtD,oBAAA,OAAO,KAAK;gBACd;gBACA,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,eAAe,IAAI,IAAI,CAAC;gBACtD,OAAO,IAAI,CAAC,GAAG,CAAC,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CACjD,GAAG,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,EACnB,UAAU,CAAC,MAAK;AACd,oBAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC;AACvB,oBAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC;AACnD,oBAAA,OAAO,KAAK;gBACd,CAAC,CAAC,CACH;AACH,YAAA,CAAC,CAAC,EACF,UAAU,CAAC,MAAK;AACd,gBAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC;AAC7B,gBAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC;AACtD,gBAAA,OAAO,KAAK;YACd,CAAC,CAAC,CACH;AACH,QAAA,CAAC,CAAC,EACF,kBAAkB,EAAE;AAErB,aAAA,SAAS,CAAC,CAAC,IAAI,KAAI;YAClB,IAAI,CAAC,eAAe,EAAE;YACtB,IAAI,CAAC,SAAS,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC;YAC1C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC;AAChC,YAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC;AACvB,YAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;AACtB,YAAA,MAAM,GAAG,GAAG,IAAI,CAAC,eAAe,EAAE;YAClC,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,GAAG,CAAC,EAAE;AAC1B,gBAAA,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC;YAC/B;AACF,QAAA,CAAC,CAAC;AAEJ,QAAA,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,MAAM,IAAI,CAAC,eAAe,EAAE,CAAC;IACzD;AAEU,IAAA,cAAc,CAAC,OAAe,EAAA;QACtC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC;AAClC,QAAA,MAAM,CAAC,GAAG,OAAO,GAAG,EAAE;AACtB,QAAA,OAAO,CAAA,EAAG,CAAC,CAAA,CAAA,EAAI,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE;IAChD;IAEU,YAAY,GAAA;AACpB,QAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC;AACvB,QAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,0CAA0C,CAAC;IAC9D;AAEU,IAAA,YAAY,CAAC,KAAY,EAAA;AACjC,QAAA,MAAM,EAAE,GAAG,KAAK,CAAC,MAA0B;QAC3C,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC;IAC7C;AAEU,IAAA,gBAAgB,CAAC,KAAY,EAAA;AACrC,QAAA,MAAM,EAAE,GAAG,KAAK,CAAC,MAA0B;AAC3C,QAAA,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,GAAG,CAAC,EAAE;YACnD,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC;QACvC;IACF;;AAGA,IAAA,MAAM,CAAC,OAAe,EAAA;QACpB,MAAM,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,EAAE,aAAa;AACzC,QAAA,IAAI,CAAC,EAAE;YAAE;QACT,EAAE,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC;AACrC,QAAA,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM,SAAS,CAAC;QACrC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC;IAC7C;IAEQ,eAAe,GAAA;AACrB,QAAA,IAAI,IAAI,CAAC,SAAS,EAAE;AAClB,YAAA,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,SAAS,CAAC;AACnC,YAAA,IAAI,CAAC,SAAS,GAAG,IAAI;QACvB;AACA,QAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;IACxB;IAEQ,OAAO,GAAA;AACb,QAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC;AACzB,QAAA,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC;AAC9B,QAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;IACxB;+GApHW,4BAA4B,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;mGAA5B,4BAA4B,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,2BAAA,EAAA,MAAA,EAAA,EAAA,MAAA,EAAA,EAAA,iBAAA,EAAA,QAAA,EAAA,UAAA,EAAA,QAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,IAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,OAAA,EAAA,EAAA,iBAAA,EAAA,mBAAA,EAAA,cAAA,EAAA,gBAAA,EAAA,EAAA,WAAA,EAAA,CAAA,EAAA,YAAA,EAAA,UAAA,EAAA,KAAA,EAAA,IAAA,EAAA,SAAA,EAAA,CAAA,SAAA,CAAA,EAAA,WAAA,EAAA,IAAA,EAAA,QAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,EC3BzC,k1DAkDA,EAAA,MAAA,EAAA,CAAA,2QAAA,CAAA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,ED5Bc,gBAAgB,EAAA,QAAA,EAAA,UAAA,EAAA,MAAA,EAAA,CAAA,MAAA,EAAA,MAAA,EAAA,MAAA,EAAA,QAAA,CAAA,EAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA,CAAA;;4FAKjB,4BAA4B,EAAA,UAAA,EAAA,CAAA;kBAPxC,SAAS;AACI,YAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,2BAA2B,WAC5B,CAAC,gBAAgB,CAAC,EAAA,eAAA,EAGV,uBAAuB,CAAC,MAAM,EAAA,QAAA,EAAA,k1DAAA,EAAA,MAAA,EAAA,CAAA,2QAAA,CAAA,EAAA;;;;;"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"edvak99-voice-receptionist-widget-call-review.service-DQ4TT_Su.mjs","sources":["../../../projects/voice-receptionist-widget/src/lib/services/call-review.service.ts"],"sourcesContent":["import { Injectable, inject, signal } from '@angular/core';\nimport { Observable, map, tap } from 'rxjs';\n\nimport { CallReviewStatus } from '../utils/call-review.util';\nimport { AiReceptionistApiService } from './ai-receptionist-api.service';\n\n@Injectable({ providedIn: 'root' })\nexport class CallReviewService {\n private readonly api = inject(AiReceptionistApiService);\n\n /** Cached from dashboard summary; decremented optimistically on review. */\n readonly needsReviewCount = signal<number | null>(null);\n\n setNeedsReviewCount(count: number): void {\n this.needsReviewCount.set(count);\n }\n\n markReviewed(callId: string): Observable<CallReviewStatus> {\n return this.api.postReview(callId).pipe(\n tap(() => {\n this.needsReviewCount.update((c) => (c != null && c > 0 ? c - 1 : c));\n }),\n map(() => 'reviewed'),\n );\n }\n}\n"],"names":[],"mappings":";;;;;MAOa,iBAAiB,CAAA;AAD9B,IAAA,WAAA,GAAA;AAEmB,QAAA,IAAA,CAAA,GAAG,GAAG,MAAM,CAAC,wBAAwB,CAAC;;AAG9C,QAAA,IAAA,CAAA,gBAAgB,GAAG,MAAM,CAAgB,IAAI,CAAC;AAcxD,IAAA;AAZC,IAAA,mBAAmB,CAAC,KAAa,EAAA;AAC/B,QAAA,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC;IAClC;AAEA,IAAA,YAAY,CAAC,MAAc,EAAA;AACzB,QAAA,OAAO,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,CACrC,GAAG,CAAC,MAAK;AACP,YAAA,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QACvE,CAAC,CAAC,EACF,GAAG,CAAC,MAAM,UAAU,CAAC,CACtB;IACH;+GAjBW,iBAAiB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,UAAA,EAAA,CAAA,CAAA;AAAjB,IAAA,SAAA,IAAA,CAAA,KAAA,GAAA,EAAA,CAAA,qBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,SAAA,EAAA,QAAA,EAAA,EAAA,EAAA,IAAA,EAAA,iBAAiB,cADJ,MAAM,EAAA,CAAA,CAAA;;4FACnB,iBAAiB,EAAA,UAAA,EAAA,CAAA;kBAD7B,UAAU;mBAAC,EAAE,UAAU,EAAE,MAAM,EAAE;;;;;"}
|
package/fesm2022/edvak99-voice-receptionist-widget-dashboard-page.component-DPxQ-wst.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"edvak99-voice-receptionist-widget-dashboard-page.component-DPxQ-wst.mjs","sources":["../../../projects/voice-receptionist-widget/src/lib/components/api-health-widget/api-health-widget.component.ts","../../../projects/voice-receptionist-widget/src/lib/components/api-health-widget/api-health-widget.component.html","../../../projects/voice-receptionist-widget/src/lib/pages/dashboard-page/dashboard-page.component.ts","../../../projects/voice-receptionist-widget/src/lib/pages/dashboard-page/dashboard-page.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core';\nimport { DatePipe } from '@angular/common';\nimport { RouterLink } from '@angular/router';\n\nimport { ApiLogStats } from '../../models/api-log.types';\nimport { AiReceptionistApiService } from '../../services/ai-receptionist-api.service';\nimport { formatApiDuration, providerChipClass, providerLabel } from '../../utils/api-log.util';\n\n@Component({\n selector: 'eds-api-health-widget',\n standalone: true,\n imports: [DatePipe, RouterLink],\n templateUrl: './api-health-widget.component.html',\n styleUrl: './api-health-widget.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class ApiHealthWidgetComponent implements OnInit {\n private readonly api = inject(AiReceptionistApiService);\n\n protected readonly stats = signal<ApiLogStats | null>(null);\n protected readonly loading = signal(true);\n protected readonly error = signal<string | null>(null);\n\n protected providerLabel = providerLabel;\n protected providerChipClass = providerChipClass;\n protected formatDuration = formatApiDuration;\n\n ngOnInit(): void {\n this.api.getApiLogStats().subscribe({\n next: ({ data }) => {\n this.stats.set(data);\n this.loading.set(false);\n },\n error: () => {\n this.loading.set(false);\n this.error.set('Stats unavailable');\n },\n });\n }\n}\n","<article class=\"eds-card api-health-widget p-4 shadow-sm\">\n <header class=\"mb-3 flex items-center justify-between\">\n <h2 class=\"eds-h2\">API health (24h)</h2>\n <a routerLink=\"../api-logs\" class=\"eds-body-sm\">View all</a>\n </header>\n\n @if (loading()) {\n <p class=\"eds-body-sm\">Loading…</p>\n } @else if (error()) {\n <p class=\"eds-body-sm api-health-widget__muted\">{{ error() }}</p>\n } @else if (stats()) {\n <div class=\"mb-4 grid grid-cols-3 gap-3\">\n <div>\n <span class=\"eds-label\">Total</span>\n <span class=\"api-health-widget__value mt-1 block text-[22px] font-semibold\">{{ stats()!.last24h.total }}</span>\n </div>\n <div>\n <span class=\"eds-label\">Failed</span>\n <span class=\"api-health-widget__value api-health-widget__value--fail mt-1 block text-[22px] font-semibold\">{{ stats()!.last24h.failed }}</span>\n </div>\n <div>\n <span class=\"eds-label\">Avg duration</span>\n <span class=\"api-health-widget__value mt-1 block text-[22px] font-semibold\">{{ formatDuration(stats()!.last24h.avgDurationMs) }}</span>\n </div>\n </div>\n\n <h3 class=\"eds-label mb-2\">Recent failures</h3>\n <ul class=\"flex flex-col gap-2\">\n @for (f of stats()!.recentFailures.slice(0, 3); track f.id) {\n <li class=\"flex flex-wrap items-center gap-2\">\n <span class=\"api-log-chip\" [class]=\"providerChipClass(f.provider)\">\n {{ providerLabel(f.provider) }}\n </span>\n <span class=\"eds-body-sm-bold\">{{ f.operation }}</span>\n <span class=\"eds-mono-sm\">{{ f.startedAt | date: 'shortTime' }}</span>\n </li>\n } @empty {\n <li class=\"eds-body-sm api-health-widget__muted\">No failures in the last 24 hours.</li>\n }\n </ul>\n }\n</article>\n","import { ChangeDetectionStrategy, Component, inject, OnDestroy, OnInit, signal } from '@angular/core';\nimport { ActivatedRoute, Router, RouterLink } from '@angular/router';\nimport { DatePipe } from '@angular/common';\n\nimport { ApiHealthWidgetComponent } from '../../components/api-health-widget/api-health-widget.component';\nimport { DashboardData, HistoryCall } from '../../models/ai-receptionist.types';\nimport { VOICE_RECEPTIONIST_CONFIG } from '../../config';\nimport { AiReceptionistApiService } from '../../services/ai-receptionist-api.service';\nimport { LiveCallsListService } from '../../services/live-calls-list.service';\nimport {\n callerMetaLine,\n historyResolution,\n historyResolutionLabel,\n historyResultChips,\n resultChipIcon,\n} from '../../utils/call-history-display.util';\nimport { isNeedsReview } from '../../utils/call-review.util';\nimport { EdsAvatarComponent } from '../../ui/eds-avatar.component';\nimport { EdsButtonComponent } from '../../ui/eds-button.component';\nimport { EdsIconComponent } from '../../ui/eds-icon.component';\nimport { EdsPulseDotComponent } from '../../ui/eds-pulse-dot.component';\nimport { EdsElapsedPipe } from '../../pipes/eds-elapsed.pipe';\n\n@Component({\n selector: 'eds-dashboard-page',\n imports: [\n RouterLink,\n DatePipe,\n EdsAvatarComponent,\n EdsButtonComponent,\n EdsIconComponent,\n EdsPulseDotComponent,\n EdsElapsedPipe,\n ApiHealthWidgetComponent,\n ],\n templateUrl: './dashboard-page.component.html',\n styleUrl: './dashboard-page.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class DashboardPageComponent implements OnInit, OnDestroy {\n private readonly api = inject(AiReceptionistApiService);\n private readonly liveCallsList = inject(LiveCallsListService);\n private readonly router = inject(Router);\n private readonly route = inject(ActivatedRoute);\n private readonly config = inject(VOICE_RECEPTIONIST_CONFIG);\n\n /** Standalone dashboard only. */\n protected readonly showApiLogs = !!this.config.showModuleNav;\n\n protected readonly data = signal<DashboardData | null>(null);\n protected readonly loading = signal(true);\n\n protected readonly liveCalls = this.liveCallsList.calls;\n protected readonly liveCallsLoading = this.liveCallsList.loading;\n protected readonly liveCallsError = this.liveCallsList.error;\n\n protected readonly statTiles = [\n { key: 'activeCalls' as const, label: 'Live calls', icon: 'graphic_eq' },\n { key: 'answeredToday' as const, label: 'Calls today', icon: 'call' },\n { key: 'tasksCreated' as const, label: 'Open tasks', icon: 'task_alt' },\n {\n key: 'appointmentsBooked' as const,\n label: 'Appointments booked',\n icon: 'event_available',\n },\n { key: 'completionRate' as const, label: 'AI handled', icon: 'trending_up', suffix: '%' },\n ];\n\n protected isNeedsReview = isNeedsReview;\n protected callerMetaLine = callerMetaLine;\n protected resultChipIcon = resultChipIcon;\n protected historyResultChips = historyResultChips;\n protected historyResolutionLabel = historyResolutionLabel;\n\n ngOnInit(): void {\n this.liveCallsList.start();\n this.api.getDashboard().subscribe(({ data }) => {\n this.data.set(data);\n this.loading.set(false);\n });\n }\n\n ngOnDestroy(): void {\n this.liveCallsList.stop();\n }\n\n protected statValue(key: keyof DashboardData['stats']): number {\n if (key === 'activeCalls') {\n return this.liveCallsList.calls().length;\n }\n return this.data()?.stats[key] ?? 0;\n }\n\n protected statusTone(status: string): 'brand' | 'pos' | 'notice' | 'neg' | 'violet' | 'gray' {\n const normalized = status || 'ai';\n const map: Record<string, 'brand' | 'pos' | 'notice' | 'neg' | 'violet'> = {\n ai: 'brand',\n 'needs-help': 'notice',\n 'human-requested': 'violet',\n urgent: 'neg',\n takeover: 'violet',\n transferring: 'notice',\n };\n return map[normalized] ?? 'brand';\n }\n\n protected statusLabel(status: string): string {\n return status || 'ai';\n }\n\n protected historyAvatarTone(row: HistoryCall): 'gray' | 'brand' {\n if (!row.name?.trim() || /^unknown/i.test(row.name) || row.initials === '?') return 'gray';\n return 'brand';\n }\n\n protected resolutionOf(row: HistoryCall) {\n return historyResolution({ outcome: row.outcome, risk: row.risk });\n }\n\n protected formatDuration(sec: number): string {\n const m = Math.floor(sec / 60);\n const s = sec % 60;\n return `${m}:${s.toString().padStart(2, '0')}`;\n }\n\n protected exportCsv(): void {\n // mock export\n }\n\n protected openHistoryDetail(id: string): void {\n void this.router.navigate(['../history', id], { relativeTo: this.route });\n }\n\n protected callDetailLink(callId: string): string[] {\n return ['../history', callId];\n }\n\n protected appointmentDetailLink(callId: string | null | undefined): string[] {\n return callId ? ['../history', callId] : ['../history'];\n }\n}\n","<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\">Home</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 </div>\n </header>\n\n @if (loading()) {\n <p class=\"eds-body-sm\">Loading dashboard…</p>\n } @else if (data()) {\n <section class=\"mb-5 grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-5\" aria-label=\"Overview KPIs\">\n @for (tile of statTiles; track tile.key) {\n @if (tile.key === 'appointmentsBooked') {\n <a\n routerLink=\"../appointments\"\n class=\"flex cursor-pointer flex-col gap-1.5 rounded-2xl border bg-white px-4 py-4 no-underline text-inherit transition-colors hover:border-blue-500\"\n >\n <span class=\"eds-label\">{{ tile.label }}</span>\n <strong class=\"text-3xl font-bold leading-tight text-zinc-900\">\n {{ statValue(tile.key) }}{{ tile.suffix ?? '' }}\n </strong>\n </a>\n } @else if (tile.key === 'activeCalls') {\n <a\n routerLink=\"../live\"\n class=\"flex cursor-pointer flex-col gap-1.5 rounded-2xl border bg-white px-4 py-4 no-underline text-inherit transition-colors hover:border-blue-500\"\n >\n <span class=\"eds-label\">{{ tile.label }}</span>\n <strong class=\"text-3xl font-bold leading-tight text-zinc-900\">\n {{ statValue(tile.key) }}{{ tile.suffix ?? '' }}\n </strong>\n </a>\n } @else {\n <article class=\"flex flex-col gap-1.5 rounded-2xl border bg-white px-4 py-4\">\n <span class=\"eds-label\">{{ tile.label }}</span>\n <strong class=\"text-3xl font-bold leading-tight text-zinc-900\">\n {{ statValue(tile.key) }}{{ tile.suffix ?? '' }}\n </strong>\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…</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=\"../activities\" [queryParams]=\"{ tab: '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) : ['../activities']\"\n [queryParams]=\"task.callId ? null : { tab: 'tasks' }\"\n class=\"grid grid-cols-[1fr_auto] items-center gap-2.5 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 }} · {{ 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 · {{ 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 @if (showApiLogs) {\n <section class=\"mb-5\">\n <eds-api-health-widget />\n </section>\n }\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 || '—' }}</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 || '—' }}</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\">—</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"],"names":[],"mappings":";;;;;;;;;;;;MAgBa,wBAAwB,CAAA;AARrC,IAAA,WAAA,GAAA;AASmB,QAAA,IAAA,CAAA,GAAG,GAAG,MAAM,CAAC,wBAAwB,CAAC;AAEpC,QAAA,IAAA,CAAA,KAAK,GAAG,MAAM,CAAqB,IAAI,CAAC;AACxC,QAAA,IAAA,CAAA,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC;AACtB,QAAA,IAAA,CAAA,KAAK,GAAG,MAAM,CAAgB,IAAI,CAAC;QAE5C,IAAA,CAAA,aAAa,GAAG,aAAa;QAC7B,IAAA,CAAA,iBAAiB,GAAG,iBAAiB;QACrC,IAAA,CAAA,cAAc,GAAG,iBAAiB;AAc7C,IAAA;IAZC,QAAQ,GAAA;AACN,QAAA,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC,SAAS,CAAC;AAClC,YAAA,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,KAAI;AACjB,gBAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;AACpB,gBAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;YACzB,CAAC;YACD,KAAK,EAAE,MAAK;AACV,gBAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;AACvB,gBAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,mBAAmB,CAAC;YACrC,CAAC;AACF,SAAA,CAAC;IACJ;+GAtBW,wBAAwB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;AAAxB,IAAA,SAAA,IAAA,CAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,SAAA,EAAA,IAAA,EAAA,wBAAwB,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,uBAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,EChBrC,w0DA0CA,EAAA,MAAA,EAAA,CAAA,sjBAAA,CAAA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,MAAA,EAAA,IAAA,ED/BY,QAAQ,6CAAE,UAAU,EAAA,QAAA,EAAA,cAAA,EAAA,MAAA,EAAA,CAAA,QAAA,EAAA,aAAA,EAAA,UAAA,EAAA,qBAAA,EAAA,OAAA,EAAA,MAAA,EAAA,YAAA,EAAA,kBAAA,EAAA,oBAAA,EAAA,YAAA,EAAA,YAAA,CAAA,EAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA,CAAA;;4FAKnB,wBAAwB,EAAA,UAAA,EAAA,CAAA;kBARpC,SAAS;+BACE,uBAAuB,EAAA,UAAA,EACrB,IAAI,EAAA,OAAA,EACP,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAA,eAAA,EAGd,uBAAuB,CAAC,MAAM,EAAA,QAAA,EAAA,w0DAAA,EAAA,MAAA,EAAA,CAAA,sjBAAA,CAAA,EAAA;;;MEyBpC,sBAAsB,CAAA;AAhBnC,IAAA,WAAA,GAAA;AAiBmB,QAAA,IAAA,CAAA,GAAG,GAAG,MAAM,CAAC,wBAAwB,CAAC;AACtC,QAAA,IAAA,CAAA,aAAa,GAAG,MAAM,CAAC,oBAAoB,CAAC;AAC5C,QAAA,IAAA,CAAA,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;AACvB,QAAA,IAAA,CAAA,KAAK,GAAG,MAAM,CAAC,cAAc,CAAC;AAC9B,QAAA,IAAA,CAAA,MAAM,GAAG,MAAM,CAAC,yBAAyB,CAAC;;QAGxC,IAAA,CAAA,WAAW,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa;AAEzC,QAAA,IAAA,CAAA,IAAI,GAAG,MAAM,CAAuB,IAAI,CAAC;AACzC,QAAA,IAAA,CAAA,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC;AAEtB,QAAA,IAAA,CAAA,SAAS,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK;AACpC,QAAA,IAAA,CAAA,gBAAgB,GAAG,IAAI,CAAC,aAAa,CAAC,OAAO;AAC7C,QAAA,IAAA,CAAA,cAAc,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK;AAEzC,QAAA,IAAA,CAAA,SAAS,GAAG;YAC7B,EAAE,GAAG,EAAE,aAAsB,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE;YACxE,EAAE,GAAG,EAAE,eAAwB,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE;YACrE,EAAE,GAAG,EAAE,cAAuB,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE;AACvE,YAAA;AACE,gBAAA,GAAG,EAAE,oBAA6B;AAClC,gBAAA,KAAK,EAAE,qBAAqB;AAC5B,gBAAA,IAAI,EAAE,iBAAiB;AACxB,aAAA;AACD,YAAA,EAAE,GAAG,EAAE,gBAAyB,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,EAAE;SAC1F;QAES,IAAA,CAAA,aAAa,GAAG,aAAa;QAC7B,IAAA,CAAA,cAAc,GAAG,cAAc;QAC/B,IAAA,CAAA,cAAc,GAAG,cAAc;QAC/B,IAAA,CAAA,kBAAkB,GAAG,kBAAkB;QACvC,IAAA,CAAA,sBAAsB,GAAG,sBAAsB;AAoE1D,IAAA;IAlEC,QAAQ,GAAA;AACN,QAAA,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE;AAC1B,QAAA,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,KAAI;AAC7C,YAAA,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;AACnB,YAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;AACzB,QAAA,CAAC,CAAC;IACJ;IAEA,WAAW,GAAA;AACT,QAAA,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE;IAC3B;AAEU,IAAA,SAAS,CAAC,GAAiC,EAAA;AACnD,QAAA,IAAI,GAAG,KAAK,aAAa,EAAE;YACzB,OAAO,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC,MAAM;QAC1C;QACA,OAAO,IAAI,CAAC,IAAI,EAAE,EAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;IACrC;AAEU,IAAA,UAAU,CAAC,MAAc,EAAA;AACjC,QAAA,MAAM,UAAU,GAAG,MAAM,IAAI,IAAI;AACjC,QAAA,MAAM,GAAG,GAAkE;AACzE,YAAA,EAAE,EAAE,OAAO;AACX,YAAA,YAAY,EAAE,QAAQ;AACtB,YAAA,iBAAiB,EAAE,QAAQ;AAC3B,YAAA,MAAM,EAAE,KAAK;AACb,YAAA,QAAQ,EAAE,QAAQ;AAClB,YAAA,YAAY,EAAE,QAAQ;SACvB;AACD,QAAA,OAAO,GAAG,CAAC,UAAU,CAAC,IAAI,OAAO;IACnC;AAEU,IAAA,WAAW,CAAC,MAAc,EAAA;QAClC,OAAO,MAAM,IAAI,IAAI;IACvB;AAEU,IAAA,iBAAiB,CAAC,GAAgB,EAAA;QAC1C,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,QAAQ,KAAK,GAAG;AAAE,YAAA,OAAO,MAAM;AAC1F,QAAA,OAAO,OAAO;IAChB;AAEU,IAAA,YAAY,CAAC,GAAgB,EAAA;AACrC,QAAA,OAAO,iBAAiB,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;IACpE;AAEU,IAAA,cAAc,CAAC,GAAW,EAAA;QAClC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,EAAE,CAAC;AAC9B,QAAA,MAAM,CAAC,GAAG,GAAG,GAAG,EAAE;AAClB,QAAA,OAAO,CAAA,EAAG,CAAC,CAAA,CAAA,EAAI,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE;IAChD;IAEU,SAAS,GAAA;;IAEnB;AAEU,IAAA,iBAAiB,CAAC,EAAU,EAAA;QACpC,KAAK,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC;IAC3E;AAEU,IAAA,cAAc,CAAC,MAAc,EAAA;AACrC,QAAA,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC;IAC/B;AAEU,IAAA,qBAAqB,CAAC,MAAiC,EAAA;AAC/D,QAAA,OAAO,MAAM,GAAG,CAAC,YAAY,EAAE,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC;IACzD;+GApGW,sBAAsB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;AAAtB,IAAA,SAAA,IAAA,CAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,SAAA,EAAA,IAAA,EAAA,sBAAsB,8ECvCnC,81dAuSA,EAAA,MAAA,EAAA,CAAA,oMAAA,CAAA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,ED7QI,UAAU,EAAA,QAAA,EAAA,cAAA,EAAA,MAAA,EAAA,CAAA,QAAA,EAAA,aAAA,EAAA,UAAA,EAAA,qBAAA,EAAA,OAAA,EAAA,MAAA,EAAA,YAAA,EAAA,kBAAA,EAAA,oBAAA,EAAA,YAAA,EAAA,YAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,MAAA,EAAA,IAAA,EACV,QAAQ,EAAA,IAAA,EAAA,MAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EACR,kBAAkB,EAAA,QAAA,EAAA,YAAA,EAAA,MAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,OAAA,EAAA,UAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAClB,kBAAkB,gGAClB,gBAAgB,EAAA,QAAA,EAAA,UAAA,EAAA,MAAA,EAAA,CAAA,MAAA,EAAA,MAAA,EAAA,MAAA,EAAA,QAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAChB,oBAAoB,EAAA,QAAA,EAAA,eAAA,EAAA,EAAA,EAAA,IAAA,EAAA,MAAA,EAAA,IAAA,EACpB,cAAc,mDACd,wBAAwB,EAAA,QAAA,EAAA,uBAAA,EAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA,CAAA;;4FAMf,sBAAsB,EAAA,UAAA,EAAA,CAAA;kBAhBlC,SAAS;AACE,YAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,oBAAoB,EAAA,OAAA,EACrB;wBACP,UAAU;wBACV,QAAQ;wBACR,kBAAkB;wBAClB,kBAAkB;wBAClB,gBAAgB;wBAChB,oBAAoB;wBACpB,cAAc;wBACd,wBAAwB;qBACzB,EAAA,eAAA,EAGgB,uBAAuB,CAAC,MAAM,EAAA,QAAA,EAAA,81dAAA,EAAA,MAAA,EAAA,CAAA,oMAAA,CAAA,EAAA;;;;;"}
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, signal, Injectable } from '@angular/core';
|
|
3
|
-
import { io } from 'socket.io-client';
|
|
4
|
-
import { k as createVoiceReceptionistPaths } from './edvak99-voice-receptionist-widget-ai-receptionist-api.service-DcRiHn8q.mjs';
|
|
5
|
-
import { VOICE_RECEPTIONIST_CONFIG } from './edvak99-voice-receptionist-widget.mjs';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Single shared Socket.io connection to the `/dashboard` namespace.
|
|
9
|
-
* Used by live calls list + per-call transcript stream.
|
|
10
|
-
*/
|
|
11
|
-
class DashboardSocketService {
|
|
12
|
-
constructor() {
|
|
13
|
-
this.config = inject(VOICE_RECEPTIONIST_CONFIG);
|
|
14
|
-
this.paths = createVoiceReceptionistPaths(this.config);
|
|
15
|
-
this.connected = signal(false);
|
|
16
|
-
this.socket = null;
|
|
17
|
-
this.refCount = 0;
|
|
18
|
-
this.handlers = new Map();
|
|
19
|
-
this.connectCallbacks = new Set();
|
|
20
|
-
}
|
|
21
|
-
acquire() {
|
|
22
|
-
this.refCount++;
|
|
23
|
-
if (this.refCount === 1) {
|
|
24
|
-
this.connect();
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
release() {
|
|
28
|
-
this.refCount = Math.max(0, this.refCount - 1);
|
|
29
|
-
if (this.refCount === 0) {
|
|
30
|
-
this.disconnect();
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
onConnect(callback) {
|
|
34
|
-
this.connectCallbacks.add(callback);
|
|
35
|
-
if (this.connected()) {
|
|
36
|
-
callback();
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
offConnect(callback) {
|
|
40
|
-
this.connectCallbacks.delete(callback);
|
|
41
|
-
}
|
|
42
|
-
on(event, handler) {
|
|
43
|
-
const wrapped = (payload) => handler(payload);
|
|
44
|
-
if (!this.handlers.has(event)) {
|
|
45
|
-
this.handlers.set(event, new Map());
|
|
46
|
-
}
|
|
47
|
-
this.handlers.get(event).set(handler, wrapped);
|
|
48
|
-
if (this.socket) {
|
|
49
|
-
this.socket.on(event, wrapped);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
off(event, handler) {
|
|
53
|
-
const eventHandlers = this.handlers.get(event);
|
|
54
|
-
const wrapped = eventHandlers?.get(handler);
|
|
55
|
-
if (wrapped) {
|
|
56
|
-
this.socket?.off(event, wrapped);
|
|
57
|
-
eventHandlers.delete(handler);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
emit(event, payload) {
|
|
61
|
-
this.socket?.emit(event, payload);
|
|
62
|
-
}
|
|
63
|
-
connect() {
|
|
64
|
-
if (this.config.useMockApi) {
|
|
65
|
-
this.connected.set(true);
|
|
66
|
-
this.fireConnectCallbacks();
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
this.socket = io(`${this.paths.apiRoot()}/dashboard`, {
|
|
70
|
-
path: '/socket.io',
|
|
71
|
-
transports: ['websocket'],
|
|
72
|
-
autoConnect: false,
|
|
73
|
-
});
|
|
74
|
-
this.socket.on('connect', () => {
|
|
75
|
-
this.connected.set(true);
|
|
76
|
-
this.fireConnectCallbacks();
|
|
77
|
-
});
|
|
78
|
-
this.socket.on('disconnect', () => {
|
|
79
|
-
this.connected.set(false);
|
|
80
|
-
});
|
|
81
|
-
this.socket.io.on('reconnect', () => {
|
|
82
|
-
this.fireConnectCallbacks();
|
|
83
|
-
});
|
|
84
|
-
for (const [event, handlerMap] of this.handlers) {
|
|
85
|
-
for (const wrapped of handlerMap.values()) {
|
|
86
|
-
this.socket.on(event, wrapped);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
this.socket.connect();
|
|
90
|
-
}
|
|
91
|
-
disconnect() {
|
|
92
|
-
if (!this.socket) {
|
|
93
|
-
this.connected.set(false);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
for (const [event, handlerMap] of this.handlers) {
|
|
97
|
-
for (const wrapped of handlerMap.values()) {
|
|
98
|
-
this.socket.off(event, wrapped);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
this.socket.removeAllListeners();
|
|
102
|
-
this.socket.disconnect();
|
|
103
|
-
this.socket = null;
|
|
104
|
-
this.connected.set(false);
|
|
105
|
-
}
|
|
106
|
-
fireConnectCallbacks() {
|
|
107
|
-
for (const cb of this.connectCallbacks) {
|
|
108
|
-
cb();
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: DashboardSocketService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
112
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: DashboardSocketService, providedIn: 'root' }); }
|
|
113
|
-
}
|
|
114
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: DashboardSocketService, decorators: [{
|
|
115
|
-
type: Injectable,
|
|
116
|
-
args: [{ providedIn: 'root' }]
|
|
117
|
-
}] });
|
|
118
|
-
|
|
119
|
-
export { DashboardSocketService as D };
|
|
120
|
-
//# sourceMappingURL=edvak99-voice-receptionist-widget-dashboard-socket.service-tTQIsZO6.mjs.map
|
package/fesm2022/edvak99-voice-receptionist-widget-dashboard-socket.service-tTQIsZO6.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"edvak99-voice-receptionist-widget-dashboard-socket.service-tTQIsZO6.mjs","sources":["../../../projects/voice-receptionist-widget/src/lib/services/dashboard-socket.service.ts"],"sourcesContent":["import { Injectable, inject, signal } from '@angular/core';\nimport { io, Socket } from 'socket.io-client';\n\nimport { createVoiceReceptionistPaths } from '../api-paths';\nimport { VOICE_RECEPTIONIST_CONFIG } from '../config';\n\ntype SocketHandler = (...args: unknown[]) => void;\n\n/**\n * Single shared Socket.io connection to the `/dashboard` namespace.\n * Used by live calls list + per-call transcript stream.\n */\n@Injectable({ providedIn: 'root' })\nexport class DashboardSocketService {\n private readonly config = inject(VOICE_RECEPTIONIST_CONFIG);\n private readonly paths = createVoiceReceptionistPaths(this.config);\n\n readonly connected = signal(false);\n\n private socket: Socket | null = null;\n private refCount = 0;\n private readonly handlers = new Map<string, Map<SocketHandler, SocketHandler>>();\n private readonly connectCallbacks = new Set<() => void>();\n\n acquire(): void {\n this.refCount++;\n if (this.refCount === 1) {\n this.connect();\n }\n }\n\n release(): void {\n this.refCount = Math.max(0, this.refCount - 1);\n if (this.refCount === 0) {\n this.disconnect();\n }\n }\n\n onConnect(callback: () => void): void {\n this.connectCallbacks.add(callback);\n if (this.connected()) {\n callback();\n }\n }\n\n offConnect(callback: () => void): void {\n this.connectCallbacks.delete(callback);\n }\n\n on<T>(event: string, handler: (payload: T) => void): void {\n const wrapped: SocketHandler = (payload) => handler(payload as T);\n if (!this.handlers.has(event)) {\n this.handlers.set(event, new Map());\n }\n this.handlers.get(event)!.set(handler as SocketHandler, wrapped);\n\n if (this.socket) {\n this.socket.on(event, wrapped);\n }\n }\n\n off<T>(event: string, handler: (payload: T) => void): void {\n const eventHandlers = this.handlers.get(event);\n const wrapped = eventHandlers?.get(handler as SocketHandler);\n if (wrapped) {\n this.socket?.off(event, wrapped);\n eventHandlers!.delete(handler as SocketHandler);\n }\n }\n\n emit(event: string, payload?: unknown): void {\n this.socket?.emit(event, payload);\n }\n\n private connect(): void {\n if (this.config.useMockApi) {\n this.connected.set(true);\n this.fireConnectCallbacks();\n return;\n }\n\n this.socket = io(`${this.paths.apiRoot()}/dashboard`, {\n path: '/socket.io',\n transports: ['websocket'],\n autoConnect: false,\n });\n\n this.socket.on('connect', () => {\n this.connected.set(true);\n this.fireConnectCallbacks();\n });\n\n this.socket.on('disconnect', () => {\n this.connected.set(false);\n });\n\n this.socket.io.on('reconnect', () => {\n this.fireConnectCallbacks();\n });\n\n for (const [event, handlerMap] of this.handlers) {\n for (const wrapped of handlerMap.values()) {\n this.socket.on(event, wrapped);\n }\n }\n\n this.socket.connect();\n }\n\n private disconnect(): void {\n if (!this.socket) {\n this.connected.set(false);\n return;\n }\n\n for (const [event, handlerMap] of this.handlers) {\n for (const wrapped of handlerMap.values()) {\n this.socket.off(event, wrapped);\n }\n }\n\n this.socket.removeAllListeners();\n this.socket.disconnect();\n this.socket = null;\n this.connected.set(false);\n }\n\n private fireConnectCallbacks(): void {\n for (const cb of this.connectCallbacks) {\n cb();\n }\n }\n}\n"],"names":[],"mappings":"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"}
|