diff --git a/src/app/app-modules/auth/dashboard/components/activity-panel.component.ts b/src/app/app-modules/auth/dashboard/components/activity-panel.component.ts index e8f694c..7a7475e 100644 --- a/src/app/app-modules/auth/dashboard/components/activity-panel.component.ts +++ b/src/app/app-modules/auth/dashboard/components/activity-panel.component.ts @@ -20,28 +20,38 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + inject, + OnInit, + signal, +} from '@angular/core'; +import { Router } from '@angular/router'; import { NgIcon, provideIcons } from '@ng-icons/core'; -import { lucideGraduationCap } from '@ng-icons/lucide'; +import { lucideGraduationCap, lucidePhoneOutgoing } from '@ng-icons/lucide'; import { switchMap } from 'rxjs/operators'; import { cardImports } from '@common-ui/ui/card'; +import { CtiService } from '@/app-modules/core/services/cti.service'; import { NotificationApiService } from '@/app-modules/core/services/notification-api.service'; +import { NotificationService } from '@/app-modules/core/services/notification.service'; +import { CallStore } from '@/app-modules/core/state/call.store'; import { SessionStore } from '@/app-modules/core/state/session.store'; const KM_TYPE = 'KM'; /** - * "Activity for this week" panel. Faithful to the old `activity-this-week`: shows the - * count of Training Resources (KM docs) for the current role/service. The training-doc - * dialog and the outbound-worklist link are deferred to later phases. + * "Activity for this week" panel (old `activity-this-week`): Training Resources count and + * the CO-on-OUTBOUND "Outbound Worklist" link. The training-doc dialog is a later phase. */ @Component({ selector: 'app-activity-panel', imports: [...cardImports, NgIcon], changeDetection: ChangeDetectionStrategy.OnPush, - viewProviders: [provideIcons({ lucideGraduationCap })], + viewProviders: [provideIcons({ lucideGraduationCap, lucidePhoneOutgoing })], template: ` @@ -51,6 +61,16 @@ const KM_TYPE = 'KM'; + @if (showOutboundLink()) { + + } @let c = trainingCount();
Training Resources @@ -71,8 +91,34 @@ const KM_TYPE = 'KM'; export class ActivityPanelComponent implements OnInit { private readonly notificationApi = inject(NotificationApiService); private readonly sessionStore = inject(SessionStore); + private readonly callStore = inject(CallStore); + private readonly cti = inject(CtiService); + private readonly notify = inject(NotificationService); + private readonly router = inject(Router); protected readonly trainingCount = signal(0); + protected readonly showOutboundLink = computed( + () => + this.sessionStore.currentRole() === 'CO' && + this.callStore.currentCampaign() === 'OUTBOUND', + ); + + /** Old `agentLoginStatus()` — the "already in MANUAL mode" error still navigates. */ + protected openOutboundWorklist(): void { + this.cti.switchToOutbound().subscribe({ + next: () => { + this.callStore.setCurrentCampaign('OUTBOUND'); + this.router.navigate(['/MultiRoleScreenComponent/OutboundCallWorklistsComponent']); + }, + error: (err: { errorMessage?: string }) => { + if ((err?.errorMessage ?? '').includes('already in MANUAL mode')) { + this.router.navigate(['/MultiRoleScreenComponent/OutboundCallWorklistsComponent']); + } else { + this.notify.alert(err?.errorMessage ?? 'Failed to switch to outbound', 'error'); + } + }, + }); + } ngOnInit(): void { const serviceId = this.sessionStore.currentServiceId(); diff --git a/src/app/app-modules/auth/dashboard/dashboard.component.ts b/src/app/app-modules/auth/dashboard/dashboard.component.ts index 1e10d1e..241a763 100644 --- a/src/app/app-modules/auth/dashboard/dashboard.component.ts +++ b/src/app/app-modules/auth/dashboard/dashboard.component.ts @@ -20,11 +20,8 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { ChangeDetectionStrategy, Component, computed, DestroyRef, inject } from '@angular/core'; -import { Router } from '@angular/router'; +import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; -import { NotificationService } from '@/app-modules/core/services/notification.service'; -import { CALL_SCREEN_ROUTE, CallStore } from '@/app-modules/core/state/call.store'; import { SessionStore } from '@/app-modules/core/state/session.store'; import { ActivityPanelComponent } from './components/activity-panel.component'; @@ -89,71 +86,6 @@ import { RatingPanelComponent } from './components/rating-panel.component'; }) export class DashboardComponent { private readonly sessionStore = inject(SessionStore); - private readonly callStore = inject(CallStore); - private readonly router = inject(Router); - private readonly notify = inject(NotificationService); - private readonly destroyRef = inject(DestroyRef); protected readonly isSupervisor = computed(() => this.sessionStore.currentRole() === 'Supervisor'); - - constructor() { - // The CZentrix iframe announces calls via window.postMessage — old dashboard `listener`. - const listener = (event: Event) => this.onCtiMessage(event); - window.addEventListener('message', listener, false); - this.destroyRef.onDestroy(() => window.removeEventListener('message', listener, false)); - } - - /** - * Old `listener(event)`: parse the pipe-delimited CTI event - * `"{Action}|{phone}|{sessionId}|{INBOUND|OUTBOUND}"` (from `event.data`, or - * `event.detail.data` for CustomEvents). Handle it when it carries a session id we don't - * already have, or is an explicit Accept. - */ - private onCtiMessage(event: Event): void { - const raw = - (event as MessageEvent).data ?? (event as CustomEvent<{ data?: unknown }>).detail?.data; - if (typeof raw !== 'string') { - // Browsers/devtools post non-CZentrix objects on window too; only pipe strings matter. - return; - } - const parts = raw.split('|'); - const sessionId = parts[2]; - if (sessionId === undefined || sessionId === 'undefined' || sessionId === null || sessionId === '') { - return; - } - // Single dispatch (review fix): the old app's two independent `if` blocks invoked - // handleEvent twice for an Accept with a new session id — harmless only by accident - // (idempotent store writes, same-URL navigation). Same trigger conditions, one call. - const known = this.callStore.sessionId(); - const isNewSession = !known || known !== sessionId; - const isAccept = parts[0]?.toLowerCase() === 'accept'; - if (isNewSession || isAccept) { - this.handleCtiEvent(parts); - } - } - - /** Old `handleEvent()`: validate, persist the call flags, open the call screen. */ - private handleCtiEvent(parts: string[]): void { - if (parts.length <= 2) { - return; - } - const mobileNumber = (parts[1] ?? '').replace(/\D/g, ''); - const checkNumber = /^\d+$/; - const sessionVar = /^\d{10}\.\d{10}$/; - // Review fix (deviation from the old app, declared on PR #6): the pattern is anchored — - // the old `^(INBOUND)|(OUTBOUND)$` accepted e.g. "INBOUNDxyz". Real events carry bare - // tokens (the old innerpage compared `=== 'OUTBOUND'` exactly); verified at the - // live-call milestone together with the deferred origin check. - const checkCallType = /^(INBOUND|OUTBOUND)$/i; - - if (checkNumber.test(mobileNumber) && sessionVar.test(parts[2]) && checkCallType.test(parts[3])) { - // Review fix: isOnCall is set only for a VALID call (startCall sets it) — the old app - // set it before validating, stranding the agent behind the mid-call guards when a - // malformed event arrived (flag set, no call, no navigation, logout blocked). - this.callStore.startCall(parts[1], parts[2], parts[3]); - this.router.navigate([CALL_SCREEN_ROUTE]); - } else { - this.notify.alert('Invalid call. Please check.', 'error'); - } - } } diff --git a/src/app/app-modules/auth/shell/shell.component.ts b/src/app/app-modules/auth/shell/shell.component.ts index a4f652e..3ce0d84 100644 --- a/src/app/app-modules/auth/shell/shell.component.ts +++ b/src/app/app-modules/auth/shell/shell.component.ts @@ -20,7 +20,14 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + DestroyRef, + inject, + signal, +} from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router'; import { NgIcon, provideIcons } from '@ng-icons/core'; @@ -34,6 +41,7 @@ import { ZardDialogService } from '@common-ui/ui/dialog'; import { APP_VERSION } from '@/app-modules/core/app-version'; import { AuthService } from '@/app-modules/core/auth/auth.service'; import { ConfigService } from '@/app-modules/core/services/config.service'; +import { CtiCallEventsService } from '@/app-modules/core/services/cti-call-events.service'; import { CtiService } from '@/app-modules/core/services/cti.service'; import { NotificationService } from '@/app-modules/core/services/notification.service'; import { @@ -70,6 +78,13 @@ export class ShellComponent { private readonly router = inject(Router); private readonly route = inject(ActivatedRoute); private readonly auth = inject(AuthService); + private readonly ctiEvents = inject(CtiCallEventsService); + private readonly shellDestroyRef = inject(DestroyRef); + + constructor() { + // Shell-wide CZentrix call-event listener (see CtiCallEventsService for why). + this.ctiEvents.attach(this.shellDestroyRef); + } private readonly cti = inject(CtiService); private readonly config = inject(ConfigService); private readonly dialog = inject(ZardDialogService); diff --git a/src/app/app-modules/call/closure/closure.component.ts b/src/app/app-modules/call/closure/closure.component.ts index f674e0d..9681642 100644 --- a/src/app/app-modules/call/closure/closure.component.ts +++ b/src/app/app-modules/call/closure/closure.component.ts @@ -38,6 +38,7 @@ import { ZardSelectImports } from '@common-ui/ui/select'; import { CallApiService } from '@/app-modules/core/services/call-api.service'; import { NotificationService } from '@/app-modules/core/services/notification.service'; +import { OutboundApiService } from '@/app-modules/core/services/outbound-api.service'; import { ENCRYPTED_KEYS, SessionStorageService, @@ -73,7 +74,7 @@ import { SessionStore } from '@/app-modules/core/state/session.store';