import { DeesElement, property, html, customElement, type TemplateResult, 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'; import { appRouter } from '../router.js'; declare global { interface HTMLElementTagNameMap { 'ops-view-emails': OpsViewEmails; } } type TEmailFolder = 'queued' | 'sent' | 'failed' | 'received' | 'security'; @customElement('ops-view-emails') export class OpsViewEmails extends DeesElement { @state() accessor selectedFolder: TEmailFolder = 'queued'; @state() accessor queuedEmails: interfaces.requests.IEmailQueueItem[] = []; @state() accessor sentEmails: interfaces.requests.IEmailQueueItem[] = []; @state() accessor failedEmails: interfaces.requests.IEmailQueueItem[] = []; @state() accessor securityIncidents: interfaces.requests.ISecurityIncident[] = []; @state() accessor selectedEmail: interfaces.requests.IEmailQueueItem | null = null; @state() accessor selectedIncident: interfaces.requests.ISecurityIncident | null = null; @state() accessor showCompose = false; @state() accessor isLoading = false; @state() accessor searchTerm = ''; @state() accessor emailDomains: string[] = []; private stateSubscription: any; constructor() { super(); this.loadData(); this.loadEmailDomains(); } async connectedCallback() { await super.connectedCallback(); // Subscribe to state changes this.stateSubscription = appstate.emailOpsStatePart.state.subscribe((state) => { this.queuedEmails = state.queuedEmails; this.sentEmails = state.sentEmails; this.failedEmails = state.failedEmails; this.securityIncidents = state.securityIncidents; this.isLoading = state.isLoading; // Sync folder from state (e.g., when URL changes) if (state.currentView !== this.selectedFolder) { this.selectedFolder = state.currentView as TEmailFolder; this.loadFolderData(state.currentView as TEmailFolder); } }); } async disconnectedCallback() { await super.disconnectedCallback(); if (this.stateSubscription) { this.stateSubscription.unsubscribe(); } } public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css` :host { display: block; height: 100%; } .emailLayout { display: flex; gap: 16px; height: 100%; min-height: 600px; } .sidebar { flex-shrink: 0; width: 280px; } .mainArea { flex: 1; display: flex; flex-direction: column; gap: 16px; overflow: hidden; } .emailToolbar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } .searchBox { flex: 1; min-width: 200px; max-width: 400px; } .emailList { flex: 1; overflow: hidden; } .emailPreview { flex: 1; display: flex; flex-direction: column; background: ${cssManager.bdTheme('#fff', '#222')}; border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')}; border-radius: 8px; overflow: hidden; } .emailHeader { padding: 24px; border-bottom: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')}; } .emailSubject { font-size: 24px; font-weight: 600; margin-bottom: 16px; color: ${cssManager.bdTheme('#333', '#ccc')}; } .emailMeta { display: flex; flex-direction: column; gap: 8px; font-size: 14px; color: ${cssManager.bdTheme('#666', '#999')}; } .emailMetaRow { display: flex; gap: 8px; } .emailMetaLabel { font-weight: 600; min-width: 80px; } .emailBody { flex: 1; padding: 24px; overflow-y: auto; font-size: 15px; line-height: 1.6; } .emailActions { display: flex; gap: 8px; padding: 16px 24px; border-top: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')}; background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')}; } .emptyState { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 400px; color: ${cssManager.bdTheme('#999', '#666')}; } .emptyIcon { font-size: 64px; margin-bottom: 16px; opacity: 0.3; } .emptyText { font-size: 18px; } .status-pending { color: ${cssManager.bdTheme('#f59e0b', '#fbbf24')}; } .status-processing { color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; } .status-delivered { color: ${cssManager.bdTheme('#10b981', '#34d399')}; } .status-failed { color: ${cssManager.bdTheme('#ef4444', '#f87171')}; } .status-deferred { color: ${cssManager.bdTheme('#f97316', '#fb923c')}; } .severity-info { color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; } .severity-warn { color: ${cssManager.bdTheme('#f59e0b', '#fbbf24')}; } .severity-error { color: ${cssManager.bdTheme('#ef4444', '#f87171')}; } .severity-critical { color: ${cssManager.bdTheme('#dc2626', '#ef4444')}; font-weight: bold; } .incidentDetails { padding: 24px; background: ${cssManager.bdTheme('#fff', '#222')}; border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')}; border-radius: 8px; } .incidentHeader { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 16px; } .incidentTitle { font-size: 20px; font-weight: 600; } .incidentMeta { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; margin-top: 16px; } .incidentField { padding: 12px; background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')}; border-radius: 6px; } .incidentFieldLabel { font-size: 12px; color: ${cssManager.bdTheme('#666', '#999')}; margin-bottom: 4px; } .incidentFieldValue { font-size: 14px; word-break: break-all; } `, ]; public render() { if (this.selectedEmail) { return this.renderEmailDetail(); } if (this.selectedIncident) { return this.renderIncidentDetail(); } return html` Email Operations
this.openComposeModal()} type="highlighted"> Compose this.searchTerm = (e.target as any).value} > this.refreshData()}> ${this.isLoading ? html`` : html``} Refresh
this.selectFolder('queued')} .type=${this.selectedFolder === 'queued' ? 'highlighted' : 'normal'} > Queued ${this.queuedEmails.length > 0 ? `(${this.queuedEmails.length})` : ''} this.selectFolder('sent')} .type=${this.selectedFolder === 'sent' ? 'highlighted' : 'normal'} > Sent this.selectFolder('failed')} .type=${this.selectedFolder === 'failed' ? 'highlighted' : 'normal'} > Failed ${this.failedEmails.length > 0 ? `(${this.failedEmails.length})` : ''} this.selectFolder('security')} .type=${this.selectedFolder === 'security' ? 'highlighted' : 'normal'} > Security ${this.securityIncidents.length > 0 ? `(${this.securityIncidents.length})` : ''}
${this.renderContent()} `; } private renderContent() { switch (this.selectedFolder) { case 'queued': return this.renderEmailTable(this.queuedEmails, 'Queued Emails', 'Emails waiting to be delivered'); case 'sent': return this.renderEmailTable(this.sentEmails, 'Sent Emails', 'Successfully delivered emails'); case 'failed': return this.renderEmailTable(this.failedEmails, 'Failed Emails', 'Emails that failed to deliver', true); case 'security': return this.renderSecurityIncidents(); default: return this.renderEmptyState('Select a folder'); } } private renderEmailTable( emails: interfaces.requests.IEmailQueueItem[], heading1: string, heading2: string, showResend = false ) { const filteredEmails = this.filterEmails(emails); if (filteredEmails.length === 0) { return this.renderEmptyState(`No emails in ${this.selectedFolder}`); } const actions = [ { name: 'View Details', iconName: 'lucide:eye', type: ['doubleClick', 'inRow'] as any, actionFunc: async (actionData: any) => { this.selectedEmail = actionData.item; } } ]; if (showResend) { actions.push({ name: 'Resend', iconName: 'lucide:send', type: ['inRow'] as any, actionFunc: async (actionData: any) => { await this.resendEmail(actionData.item.id); } }); } return html` ({ 'Status': html`${email.status}`, 'From': email.from || 'N/A', 'To': email.to?.join(', ') || 'N/A', 'Subject': email.subject || 'No subject', 'Attempts': email.attempts, 'Created': this.formatDate(email.createdAt), })} .dataActions=${actions} .selectionMode=${'single'} heading1=${heading1} heading2=${`${filteredEmails.length} emails - ${heading2}`} > `; } private renderSecurityIncidents() { const incidents = this.securityIncidents; if (incidents.length === 0) { return this.renderEmptyState('No security incidents'); } return html` ({ 'Severity': html`${incident.level.toUpperCase()}`, 'Type': incident.type, 'Message': incident.message, 'IP': incident.ipAddress || 'N/A', 'Domain': incident.domain || 'N/A', 'Time': this.formatDate(incident.timestamp), })} .dataActions=${[ { name: 'View Details', iconName: 'lucide:eye', type: ['doubleClick', 'inRow'], actionFunc: async (actionData: any) => { this.selectedIncident = actionData.item; } } ]} .selectionMode=${'single'} heading1="Security Incidents" heading2=${`${incidents.length} incidents`} > `; } private renderEmailDetail() { if (!this.selectedEmail) return ''; return html` Email Details
${this.selectedEmail.subject || 'No subject'}
Status: ${this.selectedEmail.status}
From: ${this.selectedEmail.from || 'N/A'}
To: ${this.selectedEmail.to?.join(', ') || 'N/A'}
Mode: ${this.selectedEmail.processingMode}
Attempts: ${this.selectedEmail.attempts}
Created: ${new Date(this.selectedEmail.createdAt).toLocaleString()}
${this.selectedEmail.deliveredAt ? html`
Delivered: ${new Date(this.selectedEmail.deliveredAt).toLocaleString()}
` : ''} ${this.selectedEmail.lastError ? html`
Last Error: ${this.selectedEmail.lastError}
` : ''}
${this.selectedEmail.status === 'failed' ? html` this.resendEmail(this.selectedEmail!.id)} type="highlighted"> Resend ` : ''} this.selectedEmail = null}> Close
`; } private renderIncidentDetail() { if (!this.selectedIncident) return ''; const incident = this.selectedIncident; return html` Security Incident Details
this.selectedIncident = null} type="secondary"> Back to List
${incident.message}
${new Date(incident.timestamp).toLocaleString()}
${incident.level.toUpperCase()}
Type
${incident.type}
${incident.ipAddress ? html`
IP Address
${incident.ipAddress}
` : ''} ${incident.domain ? html`
Domain
${incident.domain}
` : ''} ${incident.emailId ? html`
Email ID
${incident.emailId}
` : ''} ${incident.userId ? html`
User ID
${incident.userId}
` : ''} ${incident.action ? html`
Action
${incident.action}
` : ''} ${incident.result ? html`
Result
${incident.result}
` : ''} ${incident.success !== undefined ? html`
Success
${incident.success ? 'Yes' : 'No'}
` : ''}
${incident.details ? html`
Details
${JSON.stringify(incident.details, null, 2)}
            
` : ''}
`; } private renderEmptyState(message: string) { return html`
${message}
`; } private async openComposeModal() { const { DeesModal } = await import('@design.estate/dees-catalog'); // Ensure domains are loaded before opening modal if (this.emailDomains.length === 0) { await this.loadEmailDomains(); } await DeesModal.createAndShow({ heading: 'New Email', width: 'large', content: html`
{ await this.sendEmail(e.detail); const modals = document.querySelectorAll('dees-modal'); modals.forEach(m => (m as any).destroy?.()); }}>
@ 0 ? this.emailDomains.map(domain => ({ key: domain, value: domain })) : [{ key: 'dcrouter.local', value: 'dcrouter.local' }]} .selectedKey=${this.emailDomains[0] || 'dcrouter.local'} required style="flex: 1;" >
`, menuOptions: [ { name: 'Send', iconName: 'lucide:send', action: async (modalArg) => { const form = modalArg.shadowRoot?.querySelector('dees-form') as any; form?.submit(); } }, { name: 'Cancel', iconName: 'lucide:x', action: async (modalArg) => await modalArg.destroy() } ] }); } private filterEmails(emails: interfaces.requests.IEmailQueueItem[]): interfaces.requests.IEmailQueueItem[] { if (!this.searchTerm) { return emails; } const search = this.searchTerm.toLowerCase(); return emails.filter(e => (e.subject?.toLowerCase().includes(search)) || (e.from?.toLowerCase().includes(search)) || (e.to?.some(t => t.toLowerCase().includes(search))) ); } private selectFolder(folder: TEmailFolder) { // Use router for navigation to update URL appRouter.navigateToEmailFolder(folder); // Clear selections this.selectedEmail = null; this.selectedIncident = null; } private formatDate(timestamp: number): string { const date = new Date(timestamp); const now = new Date(); const diff = now.getTime() - date.getTime(); const hours = diff / (1000 * 60 * 60); if (hours < 24) { return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } else if (hours < 168) { // 7 days return date.toLocaleDateString([], { weekday: 'short', hour: '2-digit', minute: '2-digit' }); } else { return date.toLocaleDateString([], { month: 'short', day: 'numeric' }); } } private async loadData() { this.isLoading = true; await this.loadFolderData(this.selectedFolder); this.isLoading = false; } private async loadFolderData(folder: TEmailFolder) { switch (folder) { case 'queued': await appstate.emailOpsStatePart.dispatchAction(appstate.fetchQueuedEmailsAction, null); break; case 'sent': await appstate.emailOpsStatePart.dispatchAction(appstate.fetchSentEmailsAction, null); break; case 'failed': await appstate.emailOpsStatePart.dispatchAction(appstate.fetchFailedEmailsAction, null); break; case 'security': await appstate.emailOpsStatePart.dispatchAction(appstate.fetchSecurityIncidentsAction, null); break; } } private async loadEmailDomains() { try { await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null); const config = appstate.configStatePart.getState().config; if (config?.email?.domains && Array.isArray(config.email.domains) && config.email.domains.length > 0) { this.emailDomains = config.email.domains; } else { this.emailDomains = ['dcrouter.local']; } } catch (error) { console.error('Failed to load email domains:', error); this.emailDomains = ['dcrouter.local']; } } private async refreshData() { this.isLoading = true; await this.loadFolderData(this.selectedFolder); this.isLoading = false; } private async sendEmail(formData: any) { try { console.log('Sending email:', formData); // TODO: Implement actual email sending via API // For now, just log the data const fromEmail = `${formData.fromUsername || 'admin'}@${formData.fromDomain || this.emailDomains[0] || 'dcrouter.local'}`; console.log('From:', fromEmail); console.log('To:', formData.to); console.log('Subject:', formData.subject); } catch (error: any) { console.error('Failed to send email', error); } } private async resendEmail(emailId: string) { try { await appstate.emailOpsStatePart.dispatchAction(appstate.resendEmailAction, emailId); this.selectedEmail = null; } catch (error) { console.error('Failed to resend email:', error); } } }