@edvak99/voice-receptionist-widget 1.1.11 → 1.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/{edvak99-voice-receptionist-widget-api-logs-page.component-jKBA1N2A.mjs → edvak99-voice-receptionist-widget-api-logs-page.component-CwXaCPpV.mjs} +5 -3
- package/fesm2022/edvak99-voice-receptionist-widget-api-logs-page.component-CwXaCPpV.mjs.map +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-Br9R0PQh.mjs +201 -0
- package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-Br9R0PQh.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.mjs +84 -7
- 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/ui/eds-search-field.component.d.ts +22 -0
- package/package.json +1 -1
- package/public-api.d.ts +1 -0
- package/fesm2022/edvak99-voice-receptionist-widget-api-logs-page.component-jKBA1N2A.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
|
@@ -7,6 +7,7 @@ import { A as ApiLogsTableComponent, a as ApiLogDetailDrawerComponent } from './
|
|
|
7
7
|
import { VOICE_RECEPTIONIST_SESSION, EdsButtonComponent, EdsIconComponent } from './edvak99-voice-receptionist-widget.mjs';
|
|
8
8
|
import { A as AiReceptionistApiService } from './edvak99-voice-receptionist-widget-ai-receptionist-api.service-DcRiHn8q.mjs';
|
|
9
9
|
import { p as providerLabel, a as providerChipClass, f as formatApiDuration } from './edvak99-voice-receptionist-widget-api-log.util-C0TOKF1q.mjs';
|
|
10
|
+
import { E as EdsSearchFieldComponent } from './edvak99-voice-receptionist-widget-eds-search-field.component-OGRDvYBp.mjs';
|
|
10
11
|
|
|
11
12
|
/** Roles that may pass `practiceId` / view all practices. */
|
|
12
13
|
function isSuperAdminRole(role) {
|
|
@@ -208,7 +209,7 @@ class ApiLogsPageComponent {
|
|
|
208
209
|
});
|
|
209
210
|
}
|
|
210
211
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ApiLogsPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
211
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: ApiLogsPageComponent, isStandalone: true, selector: "eds-api-logs-page", ngImport: i0, template: "<div class=\"eds-page api-logs-page\">\n <header class=\"api-logs-page__header mb-5 flex items-start justify-between gap-4\">\n <div>\n <h1 class=\"eds-h1\">API Logs</h1>\n <p class=\"eds-body-sm\">Edvak EHR, Vonage, OpenAI, and LiveKit request history</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 <section class=\"mb-4 grid grid-cols-2 gap-3 md:grid-cols-4\" aria-label=\"API health last 24 hours\">\n @if (stats(); as s) {\n <article class=\"flex flex-col gap-1.5 rounded-2xl border bg-white px-4 py-4\">\n <span class=\"eds-label\">Total (24h)</span>\n <strong class=\"text-3xl font-bold leading-tight text-zinc-900\">{{ s.last24h.total }}</strong>\n </article>\n <article class=\"flex flex-col gap-1.5 rounded-2xl border bg-white px-4 py-4\">\n <span class=\"eds-label\">Failed (24h)</span>\n <strong class=\"text-3xl font-bold leading-tight text-red-600\">{{ s.last24h.failed }}</strong>\n </article>\n <article class=\"flex flex-col gap-1.5 rounded-2xl border bg-white px-4 py-4\">\n <span class=\"eds-label\">Avg latency</span>\n <strong class=\"text-3xl font-bold leading-tight text-zinc-900\">{{ formatDuration(s.last24h.avgDurationMs) }}</strong>\n </article>\n <article class=\"flex flex-col gap-1.5 rounded-2xl border bg-white px-4 py-4\">\n <span class=\"eds-label\">By provider</span>\n <ul class=\"m-0 mt-1 flex list-none flex-wrap gap-1.5 p-0\">\n @for (row of s.byProvider; track row.provider) {\n <li class=\"inline-flex items-center gap-1 text-xs text-zinc-600\">\n <span class=\"api-log-chip\" [class]=\"providerChipClass(row.provider)\">{{ providerLabel(row.provider) }}</span>\n {{ row.total }}\n </li>\n } @empty {\n <li class=\"text-sm text-zinc-500\">\u2014</li>\n }\n </ul>\n </article>\n } @else if (statsError()) {\n <p class=\"col-span-full eds-body-sm text-zinc-500\">{{ statsError() }}</p>\n } @else {\n <p class=\"col-span-full eds-body-sm\">Loading stats\u2026</p>\n }\n </section>\n\n <section class=\"eds-card api-logs-page__filters mb-4 p-4 shadow-sm\">\n <div class=\"api-logs-page__filter-row mb-3\">\n <span class=\"eds-label\">Provider</span>\n <div class=\"api-logs-page__chips mt-2 flex flex-wrap gap-2\">\n @for (p of providers; track p) {\n <button\n type=\"button\"\n class=\"api-log-chip api-logs-page__chip\"\n [class]=\"'api-log-chip--' + p\"\n [class.api-logs-page__chip--on]=\"isProviderSelected(p)\"\n (click)=\"toggleProvider(p)\"\n >\n {{ p }}\n </button>\n }\n </div>\n </div>\n\n <div class=\"api-logs-page__filter-grid mb-3 grid grid-cols-2 gap-3 md:grid-cols-3\">\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">Result</span>\n <select [ngModel]=\"successFilter()\" (ngModelChange)=\"successFilter.set($event)\" autocomplete=\"off\">\n <option value=\"all\">All</option>\n <option value=\"ok\">Success</option>\n <option value=\"fail\">Failed</option>\n </select>\n </label>\n\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">Status code</span>\n <input type=\"number\" placeholder=\"e.g. 500\" [ngModel]=\"statusCode()\" (ngModelChange)=\"statusCode.set($event)\" autocomplete=\"off\" />\n </label>\n\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">Call ID</span>\n <input type=\"text\" placeholder=\"LC-4821\" [ngModel]=\"callIdFilter()\" (ngModelChange)=\"callIdFilter.set($event)\" autocomplete=\"off\" />\n </label>\n\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">Search</span>\n <input type=\"search\" placeholder=\"operation, path, error\u2026\" [ngModel]=\"search()\" (ngModelChange)=\"search.set($event)\" autocomplete=\"off\" />\n </label>\n\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">From</span>\n <input type=\"datetime-local\" [ngModel]=\"fromDate()\" (ngModelChange)=\"fromDate.set($event)\" autocomplete=\"off\" />\n </label>\n\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">To</span>\n <input type=\"datetime-local\" [ngModel]=\"toDate()\" (ngModelChange)=\"toDate.set($event)\" autocomplete=\"off\" />\n </label>\n\n @if (isSuperAdmin()) {\n <label class=\"api-logs-page__field flex flex-col gap-1 md:col-span-2\">\n <span class=\"eds-label\">Practice ID (super-admin)</span>\n <input\n type=\"text\"\n placeholder=\"Leave empty for all practices\"\n [ngModel]=\"practiceIdFilter()\"\n (ngModelChange)=\"practiceIdFilter.set($event)\"\n autocomplete=\"off\"\n />\n </label>\n }\n </div>\n\n <div class=\"api-logs-page__filter-actions flex gap-2\">\n <eds-button variant=\"primary\" (click)=\"applyFilters()\">Apply filters</eds-button>\n <eds-button variant=\"secondary\" (click)=\"clearFilters()\">Clear</eds-button>\n </div>\n </section>\n\n <article class=\"eds-card api-logs-page__table-card p-4 shadow-sm\">\n @if (loading()) {\n <p class=\"eds-body-sm\">Loading\u2026</p>\n } @else if (error()) {\n <p class=\"api-logs-page__error eds-body-sm text-red-700\" role=\"alert\">{{ error() }}</p>\n } @else if (items().length === 0) {\n <p class=\"eds-body-sm\">No API logs match your filters.</p>\n } @else {\n <eds-api-logs-table\n [items]=\"items()\"\n [showCallId]=\"true\"\n [showPracticeId]=\"showPracticeColumn()\"\n (rowSelected)=\"onRowSelected($event)\"\n />\n }\n\n <footer class=\"api-logs-page__pager mt-4 flex items-center justify-between pt-3\">\n <span class=\"eds-body-sm\">\n Page {{ page() }} of {{ totalPages() }} \u00B7 {{ total() }} total\n </span>\n <div class=\"api-logs-page__pager-actions 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 </article>\n\n <eds-api-log-detail-drawer\n [open]=\"drawerOpen()\"\n [listItem]=\"selected()\"\n (closed)=\"closeDrawer()\"\n />\n</div>\n", styles: [".api-log-chip{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600;letter-spacing:.02em;text-transform:uppercase}.api-log-chip--edvak{background:#3b82f626;color:#3b82f6}.api-log-chip--vonage{background:#8b5cf626;color:#8b5cf6}.api-log-chip--openai{background:#22c55e26;color:#22c55e}.api-log-chip--livekit{background:#f9731626;color:#f97316}.api-logs-page__chip{border:1px solid transparent;cursor:pointer;opacity:.55}.api-logs-page__chip--on{opacity:1;border-color:currentColor}.api-logs-page__field input,.api-logs-page__field select{padding:8px 10px;border-radius:8px;border:1px solid var(--eds-border-subtle, #e4e4e7);font:inherit}.api-logs-page__pager{border-top:1px solid var(--eds-border-subtle, #e4e4e7)}@media(max-width:900px){.api-logs-page__filter-grid{grid-template-columns:1fr}}\n"], dependencies: [{ 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.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { 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: ApiLogsTableComponent, selector: "eds-api-logs-table", inputs: ["items", "showCallId", "showPracticeId", "callLinkBase"], outputs: ["rowSelected"] }, { kind: "component", type: ApiLogDetailDrawerComponent, selector: "eds-api-log-detail-drawer", inputs: ["open", "listItem", "listOnly"], outputs: ["closed"] }, { 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 }); }
|
|
212
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: ApiLogsPageComponent, isStandalone: true, selector: "eds-api-logs-page", ngImport: i0, template: "<div class=\"eds-page api-logs-page\">\n <header class=\"api-logs-page__header mb-5 flex items-start justify-between gap-4\">\n <div>\n <h1 class=\"eds-h1\">API Logs</h1>\n <p class=\"eds-body-sm\">Edvak EHR, Vonage, OpenAI, and LiveKit request history</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 <section class=\"mb-4 grid grid-cols-2 gap-3 md:grid-cols-4\" aria-label=\"API health last 24 hours\">\n @if (stats(); as s) {\n <article class=\"flex flex-col gap-1.5 rounded-2xl border bg-white px-4 py-4\">\n <span class=\"eds-label\">Total (24h)</span>\n <strong class=\"text-3xl font-bold leading-tight text-zinc-900\">{{ s.last24h.total }}</strong>\n </article>\n <article class=\"flex flex-col gap-1.5 rounded-2xl border bg-white px-4 py-4\">\n <span class=\"eds-label\">Failed (24h)</span>\n <strong class=\"text-3xl font-bold leading-tight text-red-600\">{{ s.last24h.failed }}</strong>\n </article>\n <article class=\"flex flex-col gap-1.5 rounded-2xl border bg-white px-4 py-4\">\n <span class=\"eds-label\">Avg latency</span>\n <strong class=\"text-3xl font-bold leading-tight text-zinc-900\">{{ formatDuration(s.last24h.avgDurationMs) }}</strong>\n </article>\n <article class=\"flex flex-col gap-1.5 rounded-2xl border bg-white px-4 py-4\">\n <span class=\"eds-label\">By provider</span>\n <ul class=\"m-0 mt-1 flex list-none flex-wrap gap-1.5 p-0\">\n @for (row of s.byProvider; track row.provider) {\n <li class=\"inline-flex items-center gap-1 text-xs text-zinc-600\">\n <span class=\"api-log-chip\" [class]=\"providerChipClass(row.provider)\">{{ providerLabel(row.provider) }}</span>\n {{ row.total }}\n </li>\n } @empty {\n <li class=\"text-sm text-zinc-500\">\u2014</li>\n }\n </ul>\n </article>\n } @else if (statsError()) {\n <p class=\"col-span-full eds-body-sm text-zinc-500\">{{ statsError() }}</p>\n } @else {\n <p class=\"col-span-full eds-body-sm\">Loading stats\u2026</p>\n }\n </section>\n\n <section class=\"eds-card api-logs-page__filters mb-4 p-4 shadow-sm\">\n <div class=\"api-logs-page__filter-row mb-3\">\n <span class=\"eds-label\">Provider</span>\n <div class=\"api-logs-page__chips mt-2 flex flex-wrap gap-2\">\n @for (p of providers; track p) {\n <button\n type=\"button\"\n class=\"api-log-chip api-logs-page__chip\"\n [class]=\"'api-log-chip--' + p\"\n [class.api-logs-page__chip--on]=\"isProviderSelected(p)\"\n (click)=\"toggleProvider(p)\"\n >\n {{ p }}\n </button>\n }\n </div>\n </div>\n\n <div class=\"api-logs-page__filter-grid mb-3 grid grid-cols-2 gap-3 md:grid-cols-3\">\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">Result</span>\n <select [ngModel]=\"successFilter()\" (ngModelChange)=\"successFilter.set($event)\" autocomplete=\"off\">\n <option value=\"all\">All</option>\n <option value=\"ok\">Success</option>\n <option value=\"fail\">Failed</option>\n </select>\n </label>\n\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">Status code</span>\n <input type=\"number\" placeholder=\"e.g. 500\" [ngModel]=\"statusCode()\" (ngModelChange)=\"statusCode.set($event)\" autocomplete=\"off\" />\n </label>\n\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">Call ID</span>\n <eds-search-field\n type=\"text\"\n inputClass=\"rounded-md border border-zinc-200 px-3 py-2 text-sm\"\n placeholder=\"LC-4821\"\n [value]=\"callIdFilter()\"\n (valueChange)=\"callIdFilter.set($event)\"\n (enter)=\"applyFilters()\"\n />\n </label>\n\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">Search</span>\n <eds-search-field\n inputClass=\"rounded-md border border-zinc-200 px-3 py-2 text-sm\"\n placeholder=\"operation, path, error\u2026\"\n [value]=\"search()\"\n (valueChange)=\"search.set($event)\"\n (enter)=\"applyFilters()\"\n />\n </label>\n\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">From</span>\n <input type=\"datetime-local\" [ngModel]=\"fromDate()\" (ngModelChange)=\"fromDate.set($event)\" autocomplete=\"off\" />\n </label>\n\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">To</span>\n <input type=\"datetime-local\" [ngModel]=\"toDate()\" (ngModelChange)=\"toDate.set($event)\" autocomplete=\"off\" />\n </label>\n\n @if (isSuperAdmin()) {\n <label class=\"api-logs-page__field flex flex-col gap-1 md:col-span-2\">\n <span class=\"eds-label\">Practice ID (super-admin)</span>\n <eds-search-field\n type=\"text\"\n inputClass=\"rounded-md border border-zinc-200 px-3 py-2 text-sm\"\n placeholder=\"Leave empty for all practices\"\n [value]=\"practiceIdFilter()\"\n (valueChange)=\"practiceIdFilter.set($event)\"\n (enter)=\"applyFilters()\"\n />\n </label>\n }\n </div>\n\n <div class=\"api-logs-page__filter-actions flex gap-2\">\n <eds-button variant=\"primary\" (click)=\"applyFilters()\">Apply filters</eds-button>\n <eds-button variant=\"secondary\" (click)=\"clearFilters()\">Clear</eds-button>\n </div>\n </section>\n\n <article class=\"eds-card api-logs-page__table-card p-4 shadow-sm\">\n @if (loading()) {\n <p class=\"eds-body-sm\">Loading\u2026</p>\n } @else if (error()) {\n <p class=\"api-logs-page__error eds-body-sm text-red-700\" role=\"alert\">{{ error() }}</p>\n } @else if (items().length === 0) {\n <p class=\"eds-body-sm\">No API logs match your filters.</p>\n } @else {\n <eds-api-logs-table\n [items]=\"items()\"\n [showCallId]=\"true\"\n [showPracticeId]=\"showPracticeColumn()\"\n (rowSelected)=\"onRowSelected($event)\"\n />\n }\n\n <footer class=\"api-logs-page__pager mt-4 flex items-center justify-between pt-3\">\n <span class=\"eds-body-sm\">\n Page {{ page() }} of {{ totalPages() }} \u00B7 {{ total() }} total\n </span>\n <div class=\"api-logs-page__pager-actions 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 </article>\n\n <eds-api-log-detail-drawer\n [open]=\"drawerOpen()\"\n [listItem]=\"selected()\"\n (closed)=\"closeDrawer()\"\n />\n</div>\n", styles: [".api-log-chip{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600;letter-spacing:.02em;text-transform:uppercase}.api-log-chip--edvak{background:#3b82f626;color:#3b82f6}.api-log-chip--vonage{background:#8b5cf626;color:#8b5cf6}.api-log-chip--openai{background:#22c55e26;color:#22c55e}.api-log-chip--livekit{background:#f9731626;color:#f97316}.api-logs-page__chip{border:1px solid transparent;cursor:pointer;opacity:.55}.api-logs-page__chip--on{opacity:1;border-color:currentColor}.api-logs-page__field input,.api-logs-page__field select{padding:8px 10px;border-radius:8px;border:1px solid var(--eds-border-subtle, #e4e4e7);font:inherit}.api-logs-page__pager{border-top:1px solid var(--eds-border-subtle, #e4e4e7)}@media(max-width:900px){.api-logs-page__filter-grid{grid-template-columns:1fr}}\n"], dependencies: [{ 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.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { 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: ApiLogsTableComponent, selector: "eds-api-logs-table", inputs: ["items", "showCallId", "showPracticeId", "callLinkBase"], outputs: ["rowSelected"] }, { kind: "component", type: ApiLogDetailDrawerComponent, selector: "eds-api-log-detail-drawer", inputs: ["open", "listItem", "listOnly"], outputs: ["closed"] }, { kind: "component", type: EdsButtonComponent, selector: "eds-button", inputs: ["variant", "type", "disabled"] }, { kind: "component", type: EdsIconComponent, selector: "eds-icon", inputs: ["name", "size", "fill", "weight"] }, { kind: "component", type: EdsSearchFieldComponent, selector: "eds-search-field", inputs: ["value", "inputId", "type", "placeholder", "ariaLabel", "inputClass"], outputs: ["valueChange", "enter"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
212
213
|
}
|
|
213
214
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ApiLogsPageComponent, decorators: [{
|
|
214
215
|
type: Component,
|
|
@@ -218,8 +219,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
218
219
|
ApiLogDetailDrawerComponent,
|
|
219
220
|
EdsButtonComponent,
|
|
220
221
|
EdsIconComponent,
|
|
221
|
-
|
|
222
|
+
EdsSearchFieldComponent,
|
|
223
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"eds-page api-logs-page\">\n <header class=\"api-logs-page__header mb-5 flex items-start justify-between gap-4\">\n <div>\n <h1 class=\"eds-h1\">API Logs</h1>\n <p class=\"eds-body-sm\">Edvak EHR, Vonage, OpenAI, and LiveKit request history</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 <section class=\"mb-4 grid grid-cols-2 gap-3 md:grid-cols-4\" aria-label=\"API health last 24 hours\">\n @if (stats(); as s) {\n <article class=\"flex flex-col gap-1.5 rounded-2xl border bg-white px-4 py-4\">\n <span class=\"eds-label\">Total (24h)</span>\n <strong class=\"text-3xl font-bold leading-tight text-zinc-900\">{{ s.last24h.total }}</strong>\n </article>\n <article class=\"flex flex-col gap-1.5 rounded-2xl border bg-white px-4 py-4\">\n <span class=\"eds-label\">Failed (24h)</span>\n <strong class=\"text-3xl font-bold leading-tight text-red-600\">{{ s.last24h.failed }}</strong>\n </article>\n <article class=\"flex flex-col gap-1.5 rounded-2xl border bg-white px-4 py-4\">\n <span class=\"eds-label\">Avg latency</span>\n <strong class=\"text-3xl font-bold leading-tight text-zinc-900\">{{ formatDuration(s.last24h.avgDurationMs) }}</strong>\n </article>\n <article class=\"flex flex-col gap-1.5 rounded-2xl border bg-white px-4 py-4\">\n <span class=\"eds-label\">By provider</span>\n <ul class=\"m-0 mt-1 flex list-none flex-wrap gap-1.5 p-0\">\n @for (row of s.byProvider; track row.provider) {\n <li class=\"inline-flex items-center gap-1 text-xs text-zinc-600\">\n <span class=\"api-log-chip\" [class]=\"providerChipClass(row.provider)\">{{ providerLabel(row.provider) }}</span>\n {{ row.total }}\n </li>\n } @empty {\n <li class=\"text-sm text-zinc-500\">\u2014</li>\n }\n </ul>\n </article>\n } @else if (statsError()) {\n <p class=\"col-span-full eds-body-sm text-zinc-500\">{{ statsError() }}</p>\n } @else {\n <p class=\"col-span-full eds-body-sm\">Loading stats\u2026</p>\n }\n </section>\n\n <section class=\"eds-card api-logs-page__filters mb-4 p-4 shadow-sm\">\n <div class=\"api-logs-page__filter-row mb-3\">\n <span class=\"eds-label\">Provider</span>\n <div class=\"api-logs-page__chips mt-2 flex flex-wrap gap-2\">\n @for (p of providers; track p) {\n <button\n type=\"button\"\n class=\"api-log-chip api-logs-page__chip\"\n [class]=\"'api-log-chip--' + p\"\n [class.api-logs-page__chip--on]=\"isProviderSelected(p)\"\n (click)=\"toggleProvider(p)\"\n >\n {{ p }}\n </button>\n }\n </div>\n </div>\n\n <div class=\"api-logs-page__filter-grid mb-3 grid grid-cols-2 gap-3 md:grid-cols-3\">\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">Result</span>\n <select [ngModel]=\"successFilter()\" (ngModelChange)=\"successFilter.set($event)\" autocomplete=\"off\">\n <option value=\"all\">All</option>\n <option value=\"ok\">Success</option>\n <option value=\"fail\">Failed</option>\n </select>\n </label>\n\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">Status code</span>\n <input type=\"number\" placeholder=\"e.g. 500\" [ngModel]=\"statusCode()\" (ngModelChange)=\"statusCode.set($event)\" autocomplete=\"off\" />\n </label>\n\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">Call ID</span>\n <eds-search-field\n type=\"text\"\n inputClass=\"rounded-md border border-zinc-200 px-3 py-2 text-sm\"\n placeholder=\"LC-4821\"\n [value]=\"callIdFilter()\"\n (valueChange)=\"callIdFilter.set($event)\"\n (enter)=\"applyFilters()\"\n />\n </label>\n\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">Search</span>\n <eds-search-field\n inputClass=\"rounded-md border border-zinc-200 px-3 py-2 text-sm\"\n placeholder=\"operation, path, error\u2026\"\n [value]=\"search()\"\n (valueChange)=\"search.set($event)\"\n (enter)=\"applyFilters()\"\n />\n </label>\n\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">From</span>\n <input type=\"datetime-local\" [ngModel]=\"fromDate()\" (ngModelChange)=\"fromDate.set($event)\" autocomplete=\"off\" />\n </label>\n\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">To</span>\n <input type=\"datetime-local\" [ngModel]=\"toDate()\" (ngModelChange)=\"toDate.set($event)\" autocomplete=\"off\" />\n </label>\n\n @if (isSuperAdmin()) {\n <label class=\"api-logs-page__field flex flex-col gap-1 md:col-span-2\">\n <span class=\"eds-label\">Practice ID (super-admin)</span>\n <eds-search-field\n type=\"text\"\n inputClass=\"rounded-md border border-zinc-200 px-3 py-2 text-sm\"\n placeholder=\"Leave empty for all practices\"\n [value]=\"practiceIdFilter()\"\n (valueChange)=\"practiceIdFilter.set($event)\"\n (enter)=\"applyFilters()\"\n />\n </label>\n }\n </div>\n\n <div class=\"api-logs-page__filter-actions flex gap-2\">\n <eds-button variant=\"primary\" (click)=\"applyFilters()\">Apply filters</eds-button>\n <eds-button variant=\"secondary\" (click)=\"clearFilters()\">Clear</eds-button>\n </div>\n </section>\n\n <article class=\"eds-card api-logs-page__table-card p-4 shadow-sm\">\n @if (loading()) {\n <p class=\"eds-body-sm\">Loading\u2026</p>\n } @else if (error()) {\n <p class=\"api-logs-page__error eds-body-sm text-red-700\" role=\"alert\">{{ error() }}</p>\n } @else if (items().length === 0) {\n <p class=\"eds-body-sm\">No API logs match your filters.</p>\n } @else {\n <eds-api-logs-table\n [items]=\"items()\"\n [showCallId]=\"true\"\n [showPracticeId]=\"showPracticeColumn()\"\n (rowSelected)=\"onRowSelected($event)\"\n />\n }\n\n <footer class=\"api-logs-page__pager mt-4 flex items-center justify-between pt-3\">\n <span class=\"eds-body-sm\">\n Page {{ page() }} of {{ totalPages() }} \u00B7 {{ total() }} total\n </span>\n <div class=\"api-logs-page__pager-actions 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 </article>\n\n <eds-api-log-detail-drawer\n [open]=\"drawerOpen()\"\n [listItem]=\"selected()\"\n (closed)=\"closeDrawer()\"\n />\n</div>\n", styles: [".api-log-chip{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600;letter-spacing:.02em;text-transform:uppercase}.api-log-chip--edvak{background:#3b82f626;color:#3b82f6}.api-log-chip--vonage{background:#8b5cf626;color:#8b5cf6}.api-log-chip--openai{background:#22c55e26;color:#22c55e}.api-log-chip--livekit{background:#f9731626;color:#f97316}.api-logs-page__chip{border:1px solid transparent;cursor:pointer;opacity:.55}.api-logs-page__chip--on{opacity:1;border-color:currentColor}.api-logs-page__field input,.api-logs-page__field select{padding:8px 10px;border-radius:8px;border:1px solid var(--eds-border-subtle, #e4e4e7);font:inherit}.api-logs-page__pager{border-top:1px solid var(--eds-border-subtle, #e4e4e7)}@media(max-width:900px){.api-logs-page__filter-grid{grid-template-columns:1fr}}\n"] }]
|
|
222
224
|
}] });
|
|
223
225
|
|
|
224
226
|
export { ApiLogsPageComponent };
|
|
225
|
-
//# sourceMappingURL=edvak99-voice-receptionist-widget-api-logs-page.component-
|
|
227
|
+
//# sourceMappingURL=edvak99-voice-receptionist-widget-api-logs-page.component-CwXaCPpV.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edvak99-voice-receptionist-widget-api-logs-page.component-CwXaCPpV.mjs","sources":["../../../projects/voice-receptionist-widget/src/lib/models/api-log.types.ts","../../../projects/voice-receptionist-widget/src/lib/pages/api-logs-page/api-logs-page.component.ts","../../../projects/voice-receptionist-widget/src/lib/pages/api-logs-page/api-logs-page.component.html"],"sourcesContent":["export type ApiProvider = 'edvak' | 'vonage' | 'openai' | 'livekit';\n\nexport interface ApiLogListItem {\n id: string;\n callId: string | null;\n /** Present when viewing cross-practice (super-admin). */\n clientPracticeId?: string | null;\n provider: ApiProvider;\n operation: string;\n method: string;\n urlPath: string;\n statusCode: number | null;\n success: boolean;\n durationMs: number;\n toolName: string | null;\n errorMessage: string | null;\n startedAt: string;\n requestPreview: string;\n responsePreview: string;\n}\n\nexport interface ApiLogDetail extends ApiLogListItem {\n traceId: string | null;\n requestBody: Record<string, unknown> | null;\n responseBody: Record<string, unknown> | null;\n metadata: Record<string, unknown> | null;\n completedAt: string | null;\n}\n\nexport interface ApiLogStats {\n last24h: { total: number; failed: number; avgDurationMs: number };\n byProvider: { provider: ApiProvider; total: number; failed: number }[];\n recentFailures: ApiLogListItem[];\n}\n\nexport interface ApiLogListResponse {\n items: ApiLogListItem[];\n total: number;\n page?: number;\n limit?: number;\n /** Filter applied; null = all practices (super-admin). */\n practiceId?: string | null;\n}\n\nexport interface ApiLogFilters {\n provider?: string;\n success?: boolean;\n statusCode?: number;\n operation?: string;\n callId?: string;\n from?: string;\n to?: string;\n search?: string;\n page?: number;\n limit?: number;\n /** Super-admin only. Omit for all practices. */\n practiceId?: string;\n}\n\n/** Roles that may pass `practiceId` / view all practices. */\nexport function isSuperAdminRole(role: string | null | undefined): boolean {\n const key = String(role ?? '')\n .trim()\n .toLowerCase()\n .replace(/[\\s-]+/g, '_');\n return key === 'super_admin' || key === 'superadmin' || key === 'platform_admin';\n}\n","import {\n ChangeDetectionStrategy,\n Component,\n computed,\n inject,\n OnInit,\n signal,\n} from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { ActivatedRoute } from '@angular/router';\n\nimport { ApiLogDetailDrawerComponent } from '../../components/api-log-detail-drawer/api-log-detail-drawer.component';\nimport { ApiLogsTableComponent } from '../../components/api-logs-table/api-logs-table.component';\nimport { VOICE_RECEPTIONIST_SESSION } from '../../config';\nimport {\n ApiLogListItem,\n ApiLogStats,\n ApiProvider,\n isSuperAdminRole,\n} from '../../models/api-log.types';\nimport { AiReceptionistApiService } from '../../services/ai-receptionist-api.service';\nimport { formatApiDuration, providerChipClass, providerLabel } from '../../utils/api-log.util';\nimport { EdsButtonComponent } from '../../ui/eds-button.component';\nimport { EdsIconComponent } from '../../ui/eds-icon.component';\nimport { EdsSearchFieldComponent } from '../../ui/eds-search-field.component';\n\nconst PROVIDERS: ApiProvider[] = ['edvak', 'vonage', 'openai', 'livekit'];\nconst PAGE_SIZE = 50;\n\n@Component({\n selector: 'eds-api-logs-page',\n imports: [\n FormsModule,\n ApiLogsTableComponent,\n ApiLogDetailDrawerComponent,\n EdsButtonComponent,\n EdsIconComponent,\n EdsSearchFieldComponent,\n ],\n templateUrl: './api-logs-page.component.html',\n styleUrl: './api-logs-page.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class ApiLogsPageComponent implements OnInit {\n private readonly api = inject(AiReceptionistApiService);\n private readonly route = inject(ActivatedRoute);\n private readonly session = inject(VOICE_RECEPTIONIST_SESSION);\n\n protected readonly providers = PROVIDERS;\n protected readonly pageSize = PAGE_SIZE;\n protected readonly providerLabel = providerLabel;\n protected readonly providerChipClass = providerChipClass;\n protected readonly formatDuration = formatApiDuration;\n\n protected readonly items = signal<ApiLogListItem[]>([]);\n protected readonly total = signal(0);\n protected readonly page = signal(1);\n protected readonly loading = signal(true);\n protected readonly error = signal<string | null>(null);\n protected readonly stats = signal<ApiLogStats | null>(null);\n protected readonly statsError = signal<string | null>(null);\n\n protected readonly selectedProviders = signal<Set<ApiProvider>>(new Set());\n protected readonly successFilter = signal<'all' | 'ok' | 'fail'>('all');\n protected readonly statusCode = signal('');\n protected readonly search = signal('');\n protected readonly callIdFilter = signal('');\n protected readonly fromDate = signal('');\n protected readonly toDate = signal('');\n /** Empty string = all practices (super-admin). */\n protected readonly practiceIdFilter = signal('');\n\n protected readonly selected = signal<ApiLogListItem | null>(null);\n protected readonly drawerOpen = signal(false);\n\n protected readonly isSuperAdmin = computed(() => {\n if (this.session?.isSuperAdmin?.()) return true;\n const role = this.session?.role?.() ?? null;\n return isSuperAdminRole(role);\n });\n\n protected readonly showPracticeColumn = computed(\n () => this.isSuperAdmin() && !this.practiceIdFilter().trim(),\n );\n\n ngOnInit(): void {\n const callId = this.route.snapshot.queryParamMap.get('callId');\n if (callId) this.callIdFilter.set(callId);\n const practiceId = this.route.snapshot.queryParamMap.get('practiceId');\n if (practiceId && this.isSuperAdmin()) this.practiceIdFilter.set(practiceId);\n this.loadStats();\n this.load();\n }\n\n protected toggleProvider(p: ApiProvider): void {\n this.selectedProviders.update((set) => {\n const next = new Set(set);\n if (next.has(p)) next.delete(p);\n else next.add(p);\n return next;\n });\n }\n\n protected isProviderSelected(p: ApiProvider): boolean {\n return this.selectedProviders().has(p);\n }\n\n protected applyFilters(): void {\n this.page.set(1);\n this.loadStats();\n this.load();\n }\n\n protected clearFilters(): void {\n this.selectedProviders.set(new Set());\n this.successFilter.set('all');\n this.statusCode.set('');\n this.search.set('');\n this.callIdFilter.set('');\n this.fromDate.set('');\n this.toDate.set('');\n this.practiceIdFilter.set('');\n this.page.set(1);\n this.loadStats();\n this.load();\n }\n\n protected prevPage(): void {\n if (this.page() > 1) {\n this.page.update((p) => p - 1);\n this.load();\n }\n }\n\n protected nextPage(): void {\n if (this.page() < this.totalPages()) {\n this.page.update((p) => p + 1);\n this.load();\n }\n }\n\n protected onRowSelected(item: ApiLogListItem): void {\n this.selected.set(item);\n this.drawerOpen.set(true);\n }\n\n protected closeDrawer(): void {\n this.drawerOpen.set(false);\n this.selected.set(null);\n }\n\n protected exportCsv(): void {\n const header = [\n 'time',\n 'provider',\n 'operation',\n 'method',\n 'status',\n 'duration',\n 'success',\n 'callId',\n 'practiceId',\n 'error',\n ];\n const rows = this.items().map((r) => [\n r.startedAt,\n r.provider,\n r.operation,\n r.method,\n r.statusCode != null ? String(r.statusCode) : r.success ? 'OK' : 'TIMEOUT',\n String(r.durationMs),\n r.success ? 'true' : 'false',\n r.callId ?? '',\n r.clientPracticeId ?? '',\n r.errorMessage ?? '',\n ]);\n const csv = [header, ...rows]\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 = `api-logs-page-${this.page()}.csv`;\n a.click();\n URL.revokeObjectURL(url);\n }\n\n protected totalPages(): number {\n return Math.max(1, Math.ceil(this.total() / PAGE_SIZE));\n }\n\n private practiceIdParam(): string | undefined {\n if (!this.isSuperAdmin()) return undefined;\n const id = this.practiceIdFilter().trim();\n return id || undefined;\n }\n\n private loadStats(): void {\n this.statsError.set(null);\n this.api.getApiLogStats(this.practiceIdParam()).subscribe({\n next: ({ data }) => this.stats.set(data),\n error: () => {\n this.stats.set(null);\n this.statsError.set('Stats unavailable');\n },\n });\n }\n\n private load(): void {\n this.loading.set(true);\n this.error.set(null);\n const providers = [...this.selectedProviders()];\n this.api\n .getApiLogs({\n provider: providers.length ? providers.join(',') : undefined,\n success:\n this.successFilter() === 'all'\n ? undefined\n : this.successFilter() === 'ok',\n statusCode: this.statusCode() ? Number(this.statusCode()) : undefined,\n callId: this.callIdFilter() || undefined,\n from: this.fromDate() ? new Date(this.fromDate()).toISOString() : undefined,\n to: this.toDate() ? new Date(this.toDate()).toISOString() : undefined,\n search: this.search() || undefined,\n page: this.page(),\n limit: PAGE_SIZE,\n practiceId: this.practiceIdParam(),\n })\n .subscribe({\n next: ({ items, total }) => {\n this.items.set(items);\n this.total.set(total);\n this.loading.set(false);\n },\n error: (err: { status?: number; message?: string }) => {\n this.loading.set(false);\n if (err?.status === 403) {\n this.error.set('You do not have access to that practice’s API logs.');\n } else {\n this.error.set(err?.message ?? 'Failed to load API logs.');\n }\n },\n });\n }\n}\n","<div class=\"eds-page api-logs-page\">\n <header class=\"api-logs-page__header mb-5 flex items-start justify-between gap-4\">\n <div>\n <h1 class=\"eds-h1\">API Logs</h1>\n <p class=\"eds-body-sm\">Edvak EHR, Vonage, OpenAI, and LiveKit request history</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 <section class=\"mb-4 grid grid-cols-2 gap-3 md:grid-cols-4\" aria-label=\"API health last 24 hours\">\n @if (stats(); as s) {\n <article class=\"flex flex-col gap-1.5 rounded-2xl border bg-white px-4 py-4\">\n <span class=\"eds-label\">Total (24h)</span>\n <strong class=\"text-3xl font-bold leading-tight text-zinc-900\">{{ s.last24h.total }}</strong>\n </article>\n <article class=\"flex flex-col gap-1.5 rounded-2xl border bg-white px-4 py-4\">\n <span class=\"eds-label\">Failed (24h)</span>\n <strong class=\"text-3xl font-bold leading-tight text-red-600\">{{ s.last24h.failed }}</strong>\n </article>\n <article class=\"flex flex-col gap-1.5 rounded-2xl border bg-white px-4 py-4\">\n <span class=\"eds-label\">Avg latency</span>\n <strong class=\"text-3xl font-bold leading-tight text-zinc-900\">{{ formatDuration(s.last24h.avgDurationMs) }}</strong>\n </article>\n <article class=\"flex flex-col gap-1.5 rounded-2xl border bg-white px-4 py-4\">\n <span class=\"eds-label\">By provider</span>\n <ul class=\"m-0 mt-1 flex list-none flex-wrap gap-1.5 p-0\">\n @for (row of s.byProvider; track row.provider) {\n <li class=\"inline-flex items-center gap-1 text-xs text-zinc-600\">\n <span class=\"api-log-chip\" [class]=\"providerChipClass(row.provider)\">{{ providerLabel(row.provider) }}</span>\n {{ row.total }}\n </li>\n } @empty {\n <li class=\"text-sm text-zinc-500\">—</li>\n }\n </ul>\n </article>\n } @else if (statsError()) {\n <p class=\"col-span-full eds-body-sm text-zinc-500\">{{ statsError() }}</p>\n } @else {\n <p class=\"col-span-full eds-body-sm\">Loading stats…</p>\n }\n </section>\n\n <section class=\"eds-card api-logs-page__filters mb-4 p-4 shadow-sm\">\n <div class=\"api-logs-page__filter-row mb-3\">\n <span class=\"eds-label\">Provider</span>\n <div class=\"api-logs-page__chips mt-2 flex flex-wrap gap-2\">\n @for (p of providers; track p) {\n <button\n type=\"button\"\n class=\"api-log-chip api-logs-page__chip\"\n [class]=\"'api-log-chip--' + p\"\n [class.api-logs-page__chip--on]=\"isProviderSelected(p)\"\n (click)=\"toggleProvider(p)\"\n >\n {{ p }}\n </button>\n }\n </div>\n </div>\n\n <div class=\"api-logs-page__filter-grid mb-3 grid grid-cols-2 gap-3 md:grid-cols-3\">\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">Result</span>\n <select [ngModel]=\"successFilter()\" (ngModelChange)=\"successFilter.set($event)\" autocomplete=\"off\">\n <option value=\"all\">All</option>\n <option value=\"ok\">Success</option>\n <option value=\"fail\">Failed</option>\n </select>\n </label>\n\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">Status code</span>\n <input type=\"number\" placeholder=\"e.g. 500\" [ngModel]=\"statusCode()\" (ngModelChange)=\"statusCode.set($event)\" autocomplete=\"off\" />\n </label>\n\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">Call ID</span>\n <eds-search-field\n type=\"text\"\n inputClass=\"rounded-md border border-zinc-200 px-3 py-2 text-sm\"\n placeholder=\"LC-4821\"\n [value]=\"callIdFilter()\"\n (valueChange)=\"callIdFilter.set($event)\"\n (enter)=\"applyFilters()\"\n />\n </label>\n\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">Search</span>\n <eds-search-field\n inputClass=\"rounded-md border border-zinc-200 px-3 py-2 text-sm\"\n placeholder=\"operation, path, error…\"\n [value]=\"search()\"\n (valueChange)=\"search.set($event)\"\n (enter)=\"applyFilters()\"\n />\n </label>\n\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">From</span>\n <input type=\"datetime-local\" [ngModel]=\"fromDate()\" (ngModelChange)=\"fromDate.set($event)\" autocomplete=\"off\" />\n </label>\n\n <label class=\"api-logs-page__field flex flex-col gap-1\">\n <span class=\"eds-label\">To</span>\n <input type=\"datetime-local\" [ngModel]=\"toDate()\" (ngModelChange)=\"toDate.set($event)\" autocomplete=\"off\" />\n </label>\n\n @if (isSuperAdmin()) {\n <label class=\"api-logs-page__field flex flex-col gap-1 md:col-span-2\">\n <span class=\"eds-label\">Practice ID (super-admin)</span>\n <eds-search-field\n type=\"text\"\n inputClass=\"rounded-md border border-zinc-200 px-3 py-2 text-sm\"\n placeholder=\"Leave empty for all practices\"\n [value]=\"practiceIdFilter()\"\n (valueChange)=\"practiceIdFilter.set($event)\"\n (enter)=\"applyFilters()\"\n />\n </label>\n }\n </div>\n\n <div class=\"api-logs-page__filter-actions flex gap-2\">\n <eds-button variant=\"primary\" (click)=\"applyFilters()\">Apply filters</eds-button>\n <eds-button variant=\"secondary\" (click)=\"clearFilters()\">Clear</eds-button>\n </div>\n </section>\n\n <article class=\"eds-card api-logs-page__table-card p-4 shadow-sm\">\n @if (loading()) {\n <p class=\"eds-body-sm\">Loading…</p>\n } @else if (error()) {\n <p class=\"api-logs-page__error eds-body-sm text-red-700\" role=\"alert\">{{ error() }}</p>\n } @else if (items().length === 0) {\n <p class=\"eds-body-sm\">No API logs match your filters.</p>\n } @else {\n <eds-api-logs-table\n [items]=\"items()\"\n [showCallId]=\"true\"\n [showPracticeId]=\"showPracticeColumn()\"\n (rowSelected)=\"onRowSelected($event)\"\n />\n }\n\n <footer class=\"api-logs-page__pager mt-4 flex items-center justify-between pt-3\">\n <span class=\"eds-body-sm\">\n Page {{ page() }} of {{ totalPages() }} · {{ total() }} total\n </span>\n <div class=\"api-logs-page__pager-actions 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 </article>\n\n <eds-api-log-detail-drawer\n [open]=\"drawerOpen()\"\n [listItem]=\"selected()\"\n (closed)=\"closeDrawer()\"\n />\n</div>\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,201 @@
|
|
|
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
|
+
import { E as EdsSearchFieldComponent } from './edvak99-voice-receptionist-widget-eds-search-field.component-OGRDvYBp.mjs';
|
|
13
|
+
|
|
14
|
+
class CallHistoryPageComponent {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.api = inject(AiReceptionistApiService);
|
|
17
|
+
this.reviewService = inject(CallReviewService);
|
|
18
|
+
this.toast = inject(ToastService);
|
|
19
|
+
this.route = inject(ActivatedRoute);
|
|
20
|
+
this.router = inject(Router);
|
|
21
|
+
this.pageSize = DEFAULT_PAGE_SIZE;
|
|
22
|
+
this.rows = signal([]);
|
|
23
|
+
this.total = signal(0);
|
|
24
|
+
this.page = signal(1);
|
|
25
|
+
this.loading = signal(true);
|
|
26
|
+
this.reviewingId = signal(null);
|
|
27
|
+
this.search = signal('');
|
|
28
|
+
this.direction = signal('');
|
|
29
|
+
this.outcome = signal('');
|
|
30
|
+
this.location = signal('');
|
|
31
|
+
this.reviewTab = signal('all');
|
|
32
|
+
this.reviewTabs = [
|
|
33
|
+
{ id: 'all', label: 'All' },
|
|
34
|
+
{ id: 'needs', label: 'Needs review' },
|
|
35
|
+
{ id: 'reviewed', label: 'Reviewed' },
|
|
36
|
+
];
|
|
37
|
+
this.isNeedsReview = isNeedsReview;
|
|
38
|
+
this.callerMetaLine = callerMetaLine;
|
|
39
|
+
this.resultChipIcon = resultChipIcon;
|
|
40
|
+
this.historyResultChips = historyResultChips;
|
|
41
|
+
this.historyResolution = historyResolution;
|
|
42
|
+
this.historyResolutionLabel = historyResolutionLabel;
|
|
43
|
+
}
|
|
44
|
+
ngOnInit() {
|
|
45
|
+
const reviewParam = this.route.snapshot.queryParamMap.get('review');
|
|
46
|
+
if (reviewParam === 'needs' || reviewParam === 'reviewed') {
|
|
47
|
+
this.reviewTab.set(reviewParam);
|
|
48
|
+
}
|
|
49
|
+
this.load();
|
|
50
|
+
}
|
|
51
|
+
load() {
|
|
52
|
+
this.loading.set(true);
|
|
53
|
+
const tab = this.reviewTab();
|
|
54
|
+
this.api
|
|
55
|
+
.getHistory({
|
|
56
|
+
q: this.search() || undefined,
|
|
57
|
+
dir: this.direction() || undefined,
|
|
58
|
+
outcome: this.outcome() || undefined,
|
|
59
|
+
location: this.location() || undefined,
|
|
60
|
+
review: tab === 'all' ? undefined : tab,
|
|
61
|
+
page: this.page(),
|
|
62
|
+
limit: DEFAULT_PAGE_SIZE,
|
|
63
|
+
})
|
|
64
|
+
.subscribe({
|
|
65
|
+
next: ({ items, total, page }) => {
|
|
66
|
+
this.rows.set(items);
|
|
67
|
+
this.total.set(total);
|
|
68
|
+
this.page.set(page);
|
|
69
|
+
this.loading.set(false);
|
|
70
|
+
},
|
|
71
|
+
error: () => this.loading.set(false),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
applyFilters() {
|
|
75
|
+
this.page.set(1);
|
|
76
|
+
this.load();
|
|
77
|
+
}
|
|
78
|
+
setReviewTab(tab) {
|
|
79
|
+
this.reviewTab.set(tab);
|
|
80
|
+
this.page.set(1);
|
|
81
|
+
void this.router.navigate([], {
|
|
82
|
+
relativeTo: this.route,
|
|
83
|
+
queryParams: tab === 'all' ? { review: null } : { review: tab },
|
|
84
|
+
queryParamsHandling: 'merge',
|
|
85
|
+
replaceUrl: true,
|
|
86
|
+
});
|
|
87
|
+
this.load();
|
|
88
|
+
}
|
|
89
|
+
tabBadgeCount(tab) {
|
|
90
|
+
if (tab !== 'needs' || this.reviewTab() !== 'needs')
|
|
91
|
+
return null;
|
|
92
|
+
const count = this.total();
|
|
93
|
+
return count > 0 ? count : null;
|
|
94
|
+
}
|
|
95
|
+
totalPages() {
|
|
96
|
+
return totalPages(this.total(), DEFAULT_PAGE_SIZE);
|
|
97
|
+
}
|
|
98
|
+
prevPage() {
|
|
99
|
+
if (this.page() <= 1)
|
|
100
|
+
return;
|
|
101
|
+
this.page.update((p) => p - 1);
|
|
102
|
+
this.load();
|
|
103
|
+
}
|
|
104
|
+
nextPage() {
|
|
105
|
+
if (this.page() >= this.totalPages())
|
|
106
|
+
return;
|
|
107
|
+
this.page.update((p) => p + 1);
|
|
108
|
+
this.load();
|
|
109
|
+
}
|
|
110
|
+
markReviewed(row, event) {
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
event.stopPropagation();
|
|
113
|
+
if (!isNeedsReview(row.review) || this.reviewingId())
|
|
114
|
+
return;
|
|
115
|
+
this.reviewingId.set(row.id);
|
|
116
|
+
this.rows.update((list) => list.map((r) => (r.id === row.id ? { ...r, review: 'reviewed' } : r)));
|
|
117
|
+
this.reviewService.markReviewed(row.id).subscribe({
|
|
118
|
+
next: () => {
|
|
119
|
+
this.reviewingId.set(null);
|
|
120
|
+
this.toast.show('Call marked as reviewed', 'success');
|
|
121
|
+
if (this.reviewTab() === 'needs')
|
|
122
|
+
this.load();
|
|
123
|
+
},
|
|
124
|
+
error: () => {
|
|
125
|
+
this.reviewingId.set(null);
|
|
126
|
+
this.rows.update((list) => list.map((r) => (r.id === row.id ? { ...r, review: 'needs' } : r)));
|
|
127
|
+
this.toast.show('Failed to mark call as reviewed', 'error');
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
formatDuration(sec) {
|
|
132
|
+
const m = Math.floor(sec / 60);
|
|
133
|
+
const s = sec % 60;
|
|
134
|
+
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
135
|
+
}
|
|
136
|
+
openCallDetail(id) {
|
|
137
|
+
void this.router.navigate(['../history', id], { relativeTo: this.route });
|
|
138
|
+
}
|
|
139
|
+
avatarTone(row) {
|
|
140
|
+
if (!row.name?.trim() || /^unknown/i.test(row.name) || row.initials === '?')
|
|
141
|
+
return 'gray';
|
|
142
|
+
return 'brand';
|
|
143
|
+
}
|
|
144
|
+
resolutionOf(row) {
|
|
145
|
+
return historyResolution({ outcome: row.outcome, risk: row.risk });
|
|
146
|
+
}
|
|
147
|
+
exportCsv() {
|
|
148
|
+
const header = [
|
|
149
|
+
'id',
|
|
150
|
+
'name',
|
|
151
|
+
'phone',
|
|
152
|
+
'direction',
|
|
153
|
+
'goal',
|
|
154
|
+
'handledBy',
|
|
155
|
+
'outcome',
|
|
156
|
+
'results',
|
|
157
|
+
'duration',
|
|
158
|
+
'started',
|
|
159
|
+
'review',
|
|
160
|
+
];
|
|
161
|
+
const csvRows = this.rows().map((r) => [
|
|
162
|
+
r.id,
|
|
163
|
+
r.name,
|
|
164
|
+
r.phone,
|
|
165
|
+
r.direction,
|
|
166
|
+
r.reason,
|
|
167
|
+
r.handledBy ?? '',
|
|
168
|
+
r.outcome,
|
|
169
|
+
(r.results ?? []).join('; '),
|
|
170
|
+
String(r.duration),
|
|
171
|
+
String(r.started),
|
|
172
|
+
r.review,
|
|
173
|
+
]);
|
|
174
|
+
const csv = [header, ...csvRows]
|
|
175
|
+
.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(','))
|
|
176
|
+
.join('\n');
|
|
177
|
+
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
|
178
|
+
const url = URL.createObjectURL(blob);
|
|
179
|
+
const a = document.createElement('a');
|
|
180
|
+
a.href = url;
|
|
181
|
+
a.download = `call-history-page-${this.page()}.csv`;
|
|
182
|
+
a.click();
|
|
183
|
+
URL.revokeObjectURL(url);
|
|
184
|
+
}
|
|
185
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: CallHistoryPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
186
|
+
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 <eds-search-field\n placeholder=\"Search name, phone, reason\u2026\"\n [value]=\"search()\"\n (valueChange)=\"search.set($event)\"\n (enter)=\"applyFilters()\"\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 <eds-search-field\n type=\"text\"\n inputClass=\"rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm\"\n placeholder=\"Outcome\"\n [value]=\"outcome()\"\n (valueChange)=\"outcome.set($event)\"\n (enter)=\"applyFilters()\"\n />\n <eds-search-field\n type=\"text\"\n inputClass=\"rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm\"\n placeholder=\"Location\"\n [value]=\"location()\"\n (valueChange)=\"location.set($event)\"\n (enter)=\"applyFilters()\"\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.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"] }, { kind: "component", type: EdsSearchFieldComponent, selector: "eds-search-field", inputs: ["value", "inputId", "type", "placeholder", "ariaLabel", "inputClass"], outputs: ["valueChange", "enter"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
187
|
+
}
|
|
188
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: CallHistoryPageComponent, decorators: [{
|
|
189
|
+
type: Component,
|
|
190
|
+
args: [{ selector: 'eds-call-history-page', imports: [
|
|
191
|
+
DatePipe,
|
|
192
|
+
FormsModule,
|
|
193
|
+
EdsAvatarComponent,
|
|
194
|
+
EdsButtonComponent,
|
|
195
|
+
EdsIconComponent,
|
|
196
|
+
EdsSearchFieldComponent,
|
|
197
|
+
], 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 <eds-search-field\n placeholder=\"Search name, phone, reason\u2026\"\n [value]=\"search()\"\n (valueChange)=\"search.set($event)\"\n (enter)=\"applyFilters()\"\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 <eds-search-field\n type=\"text\"\n inputClass=\"rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm\"\n placeholder=\"Outcome\"\n [value]=\"outcome()\"\n (valueChange)=\"outcome.set($event)\"\n (enter)=\"applyFilters()\"\n />\n <eds-search-field\n type=\"text\"\n inputClass=\"rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm\"\n placeholder=\"Location\"\n [value]=\"location()\"\n (valueChange)=\"location.set($event)\"\n (enter)=\"applyFilters()\"\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"] }]
|
|
198
|
+
}] });
|
|
199
|
+
|
|
200
|
+
export { CallHistoryPageComponent };
|
|
201
|
+
//# sourceMappingURL=edvak99-voice-receptionist-widget-call-history-page.component-Br9R0PQh.mjs.map
|
package/fesm2022/edvak99-voice-receptionist-widget-call-history-page.component-Br9R0PQh.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edvak99-voice-receptionist-widget-call-history-page.component-Br9R0PQh.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 { EdsSearchFieldComponent } from '../../ui/eds-search-field.component';\nimport { ToastService } from '../../shared/toast.service';\n\n@Component({\n selector: 'eds-call-history-page',\n imports: [\n DatePipe,\n FormsModule,\n EdsAvatarComponent,\n EdsButtonComponent,\n EdsIconComponent,\n EdsSearchFieldComponent,\n ],\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 <eds-search-field\n placeholder=\"Search name, phone, reason…\"\n [value]=\"search()\"\n (valueChange)=\"search.set($event)\"\n (enter)=\"applyFilters()\"\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 <eds-search-field\n type=\"text\"\n inputClass=\"rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm\"\n placeholder=\"Outcome\"\n [value]=\"outcome()\"\n (valueChange)=\"outcome.set($event)\"\n (enter)=\"applyFilters()\"\n />\n <eds-search-field\n type=\"text\"\n inputClass=\"rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm\"\n placeholder=\"Location\"\n [value]=\"location()\"\n (valueChange)=\"location.set($event)\"\n (enter)=\"applyFilters()\"\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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"}
|