import { DeesElement, html, customElement, css, state, cssManager } from '@design.estate/dees-element'; import * as appstate from '../../appstate.js'; import * as shared from '../shared/index.js'; import * as interfaces from '../../../dist_ts_interfaces/index.js'; declare global { interface HTMLElementTagNameMap { 'ops-view-emails': OpsViewEmails; } } @customElement('ops-view-emails') export class OpsViewEmails extends DeesElement { @state() accessor emails: interfaces.requests.IEmail[] = []; @state() accessor selectedEmail: interfaces.requests.IEmailDetail | null = null; @state() accessor currentView: 'list' | 'detail' = 'list'; @state() accessor isLoading = false; @state() accessor traffic: interfaces.requests.IEmailLogTraffic | null = null; @state() accessor searchQuery = ''; @state() accessor selectedRange: { from: number; to: number } | null = null; private stateSubscription: any; private lastEmailOpsUpdate = 0; private detailRequestGeneration = 0; private searchDebounceTimer: number | undefined; async connectedCallback() { await super.connectedCallback(); this.stateSubscription = appstate.emailOpsStatePart.select().subscribe((state) => { this.emails = state.emails; this.traffic = state.traffic; this.searchQuery = state.searchQuery; this.selectedRange = state.selectedRange; const hasNewSnapshot = state.lastUpdated > this.lastEmailOpsUpdate; this.lastEmailOpsUpdate = Math.max(this.lastEmailOpsUpdate, state.lastUpdated); if (hasNewSnapshot && this.selectedEmail) { void this.refreshEmailDetail(this.selectedEmail.id); } this.isLoading = state.isLoading; }); // Initial fetch await appstate.emailOpsStatePart.dispatchAction(appstate.fetchAllEmailsAction, null); } async disconnectedCallback() { await super.disconnectedCallback(); if (this.stateSubscription) { this.stateSubscription.unsubscribe(); } if (this.searchDebounceTimer !== undefined) { window.clearTimeout(this.searchDebounceTimer); this.searchDebounceTimer = undefined; } } public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css` :host { display: block; height: 100%; } .viewContainer { height: 100%; } .chartStack { display: flex; flex-direction: column; gap: 8px; } .rangeSummary { display: flex; align-items: center; justify-content: space-between; min-height: 32px; padding: 0 4px; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; font-size: 13px; } .chartStack dees-chart-area { min-height: 360px; } `, ]; public render() { return html` Email Log
${this.currentView === 'detail' && this.selectedEmail ? html` ` : html` ${this.traffic ? html`
${this.selectedRange ? html`
${this.formatSelectedRange()} Clear time range
` : html``} `${valueArg}`} @range-change=${this.handleRangeChange} >
` : html``}
` }
`; } private async refreshEmailDetail(emailIdArg: string, openDetailArg = false): Promise { const requestGeneration = ++this.detailRequestGeneration; try { const email = await appstate.fetchEmailDetailSnapshot(emailIdArg); if (requestGeneration !== this.detailRequestGeneration) return; if (email) { this.selectedEmail = email; if (openDetailArg) { this.currentView = 'detail'; } } else if (this.selectedEmail?.id === emailIdArg) { this.handleBack(); } } catch (error) { console.error('Failed to fetch email detail:', error); } } private async handleEmailClick(e: CustomEvent) { await this.refreshEmailDetail(e.detail.id, true); } private handleSearchChange(eventArg: CustomEvent<{ searchQuery: string }>): void { const searchQuery = appstate.setActiveEmailSearchQuery(eventArg.detail.searchQuery); if (this.searchDebounceTimer !== undefined) { window.clearTimeout(this.searchDebounceTimer); } this.searchDebounceTimer = window.setTimeout(() => { this.searchDebounceTimer = undefined; void appstate.emailOpsStatePart.dispatchAction( appstate.fetchAllEmailsAction, { searchQuery }, ); }, 300); } private handleRangeChange = ( eventArg: CustomEvent<{ from: number; to: number }>, ): void => { void appstate.emailOpsStatePart.dispatchAction( appstate.fetchAllEmailsAction, { selectedRange: eventArg.detail }, ); }; private clearSelectedRange = (): void => { void appstate.emailOpsStatePart.dispatchAction( appstate.fetchAllEmailsAction, { selectedRange: null }, ); }; private formatSelectedRange(): string { if (!this.selectedRange) return ''; const formatter = new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short', }); return `Showing emails from ${formatter.format(this.selectedRange.from)} to ${formatter.format(this.selectedRange.to)}`; } private getEmailTrafficSeries(): Array<{ name: string; color: string; data: Array<{ x: number; y: number }>; }> { if (!this.traffic) return []; return [ { name: 'Sent', color: '#22c55e', data: this.traffic.sent.map((point) => ({ x: point.timestamp, y: point.value })), }, { name: 'Received', color: '#3b82f6', data: this.traffic.received.map((point) => ({ x: point.timestamp, y: point.value })), }, { name: 'Failed', color: '#ef4444', data: this.traffic.failed.map((point) => ({ x: point.timestamp, y: point.value })), }, ]; } private handleBack() { this.detailRequestGeneration++; this.selectedEmail = null; this.currentView = 'list'; } }