2025-06-12 08:04:30 +00:00
|
|
|
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
|
|
|
|
|
import * as appstate from '../appstate.js';
|
2025-06-12 11:22:18 +00:00
|
|
|
import * as shared from './shared/index.js';
|
2026-02-02 00:36:19 +00:00
|
|
|
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
|
|
|
|
import { appRouter } from '../router.js';
|
2025-06-12 08:04:30 +00:00
|
|
|
|
|
|
|
|
declare global {
|
|
|
|
|
interface HTMLElementTagNameMap {
|
|
|
|
|
'ops-view-emails': OpsViewEmails;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 00:36:19 +00:00
|
|
|
type TEmailFolder = 'queued' | 'sent' | 'failed' | 'received' | 'security';
|
2025-06-12 08:04:30 +00:00
|
|
|
|
|
|
|
|
@customElement('ops-view-emails')
|
|
|
|
|
export class OpsViewEmails extends DeesElement {
|
|
|
|
|
@state()
|
2026-02-02 00:36:19 +00:00
|
|
|
accessor selectedFolder: TEmailFolder = 'queued';
|
|
|
|
|
|
|
|
|
|
@state()
|
|
|
|
|
accessor queuedEmails: interfaces.requests.IEmailQueueItem[] = [];
|
2025-06-12 08:04:30 +00:00
|
|
|
|
|
|
|
|
@state()
|
2026-02-02 00:36:19 +00:00
|
|
|
accessor sentEmails: interfaces.requests.IEmailQueueItem[] = [];
|
2025-06-12 08:04:30 +00:00
|
|
|
|
|
|
|
|
@state()
|
2026-02-02 00:36:19 +00:00
|
|
|
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;
|
2025-06-12 08:04:30 +00:00
|
|
|
|
|
|
|
|
@state()
|
2026-02-01 19:21:37 +00:00
|
|
|
accessor showCompose = false;
|
2025-06-12 08:04:30 +00:00
|
|
|
|
|
|
|
|
@state()
|
2026-02-01 19:21:37 +00:00
|
|
|
accessor isLoading = false;
|
2025-06-12 08:04:30 +00:00
|
|
|
|
|
|
|
|
@state()
|
2026-02-01 19:21:37 +00:00
|
|
|
accessor searchTerm = '';
|
2025-06-12 08:04:30 +00:00
|
|
|
|
2025-06-29 18:47:44 +00:00
|
|
|
@state()
|
2026-02-01 19:21:37 +00:00
|
|
|
accessor emailDomains: string[] = [];
|
2025-06-29 18:47:44 +00:00
|
|
|
|
2026-02-02 00:36:19 +00:00
|
|
|
private stateSubscription: any;
|
|
|
|
|
|
2025-06-12 08:04:30 +00:00
|
|
|
constructor() {
|
|
|
|
|
super();
|
2026-02-02 00:36:19 +00:00
|
|
|
this.loadData();
|
2025-06-29 18:47:44 +00:00
|
|
|
this.loadEmailDomains();
|
2025-06-12 08:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 00:36:19 +00:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-12 08:04:30 +00:00
|
|
|
public static styles = [
|
|
|
|
|
cssManager.defaultStyles,
|
2025-06-12 11:22:18 +00:00
|
|
|
shared.viewHostCss,
|
2025-06-12 08:04:30 +00:00
|
|
|
css`
|
|
|
|
|
:host {
|
|
|
|
|
display: block;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-19 12:14:52 +00:00
|
|
|
.emailLayout {
|
|
|
|
|
display: flex;
|
2025-06-12 11:22:18 +00:00
|
|
|
gap: 16px;
|
|
|
|
|
height: 100%;
|
|
|
|
|
min-height: 600px;
|
2025-06-12 08:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sidebar {
|
2025-06-19 12:14:52 +00:00
|
|
|
flex-shrink: 0;
|
|
|
|
|
width: 280px;
|
2025-06-12 08:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
2025-06-19 12:14:52 +00:00
|
|
|
.mainArea {
|
|
|
|
|
flex: 1;
|
2025-06-12 08:04:30 +00:00
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
2025-06-12 11:22:18 +00:00
|
|
|
gap: 16px;
|
|
|
|
|
overflow: hidden;
|
2025-06-12 08:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.emailToolbar {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
2025-06-12 11:22:18 +00:00
|
|
|
gap: 12px;
|
|
|
|
|
flex-wrap: wrap;
|
2025-06-12 08:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.searchBox {
|
|
|
|
|
flex: 1;
|
2025-06-17 14:37:05 +00:00
|
|
|
min-width: 200px;
|
2025-06-12 08:04:30 +00:00
|
|
|
max-width: 400px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.emailList {
|
|
|
|
|
flex: 1;
|
2025-06-17 14:37:05 +00:00
|
|
|
overflow: hidden;
|
2025-06-12 08:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.emailPreview {
|
2025-06-17 14:37:05 +00:00
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
2025-06-19 12:14:52 +00:00
|
|
|
background: ${cssManager.bdTheme('#fff', '#222')};
|
|
|
|
|
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
2025-06-12 08:04:30 +00:00
|
|
|
border-radius: 8px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.emailHeader {
|
2025-06-17 14:37:05 +00:00
|
|
|
padding: 24px;
|
2025-06-19 12:14:52 +00:00
|
|
|
border-bottom: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
2025-06-12 08:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.emailSubject {
|
2025-06-17 14:37:05 +00:00
|
|
|
font-size: 24px;
|
2025-06-12 08:04:30 +00:00
|
|
|
font-weight: 600;
|
2025-06-17 14:37:05 +00:00
|
|
|
margin-bottom: 16px;
|
2025-06-19 12:14:52 +00:00
|
|
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
2025-06-12 08:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.emailMeta {
|
|
|
|
|
display: flex;
|
2025-06-17 14:37:05 +00:00
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 8px;
|
2025-06-12 08:04:30 +00:00
|
|
|
font-size: 14px;
|
2025-06-19 12:14:52 +00:00
|
|
|
color: ${cssManager.bdTheme('#666', '#999')};
|
2025-06-12 08:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
2025-06-17 14:37:05 +00:00
|
|
|
.emailMetaRow {
|
2025-06-12 08:04:30 +00:00
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-17 14:37:05 +00:00
|
|
|
.emailMetaLabel {
|
2025-06-12 08:04:30 +00:00
|
|
|
font-weight: 600;
|
2026-02-02 00:36:19 +00:00
|
|
|
min-width: 80px;
|
2025-06-12 08:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
2025-06-17 14:37:05 +00:00
|
|
|
.emailBody {
|
2025-06-12 08:04:30 +00:00
|
|
|
flex: 1;
|
2025-06-17 14:37:05 +00:00
|
|
|
padding: 24px;
|
2025-06-12 08:04:30 +00:00
|
|
|
overflow-y: auto;
|
2025-06-17 14:37:05 +00:00
|
|
|
font-size: 15px;
|
|
|
|
|
line-height: 1.6;
|
2025-06-12 08:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
2025-06-17 14:37:05 +00:00
|
|
|
.emailActions {
|
2025-06-12 08:04:30 +00:00
|
|
|
display: flex;
|
2025-06-17 14:37:05 +00:00
|
|
|
gap: 8px;
|
|
|
|
|
padding: 16px 24px;
|
2025-06-19 12:14:52 +00:00
|
|
|
border-top: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
|
|
|
background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')};
|
2025-06-12 08:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.emptyState {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
2026-02-02 00:36:19 +00:00
|
|
|
height: 400px;
|
2025-06-19 12:14:52 +00:00
|
|
|
color: ${cssManager.bdTheme('#999', '#666')};
|
2025-06-12 08:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.emptyIcon {
|
|
|
|
|
font-size: 64px;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
opacity: 0.3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.emptyText {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
}
|
2025-06-19 12:14:52 +00:00
|
|
|
|
2026-02-02 00:36:19 +00:00
|
|
|
.status-pending {
|
|
|
|
|
color: ${cssManager.bdTheme('#f59e0b', '#fbbf24')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-processing {
|
|
|
|
|
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
2025-06-19 12:14:52 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 00:36:19 +00:00
|
|
|
.status-delivered {
|
|
|
|
|
color: ${cssManager.bdTheme('#10b981', '#34d399')};
|
2025-06-19 12:14:52 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 00:36:19 +00:00
|
|
|
.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;
|
2025-06-19 12:14:52 +00:00
|
|
|
color: ${cssManager.bdTheme('#666', '#999')};
|
2026-02-02 00:36:19 +00:00
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.incidentFieldValue {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
word-break: break-all;
|
2025-06-19 12:14:52 +00:00
|
|
|
}
|
2025-06-12 08:04:30 +00:00
|
|
|
`,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
public render() {
|
2025-06-19 12:14:52 +00:00
|
|
|
if (this.selectedEmail) {
|
2026-02-02 00:36:19 +00:00
|
|
|
return this.renderEmailDetail();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.selectedIncident) {
|
|
|
|
|
return this.renderIncidentDetail();
|
2025-06-19 12:14:52 +00:00
|
|
|
}
|
2025-06-12 08:04:30 +00:00
|
|
|
|
|
|
|
|
return html`
|
2026-02-02 00:36:19 +00:00
|
|
|
<ops-sectionheading>Email Operations</ops-sectionheading>
|
|
|
|
|
|
2025-06-17 14:37:05 +00:00
|
|
|
<!-- Toolbar -->
|
2025-06-19 12:14:52 +00:00
|
|
|
<div class="emailToolbar" style="margin-bottom: 16px;">
|
|
|
|
|
<dees-button @click=${() => this.openComposeModal()} type="highlighted">
|
2026-02-02 00:36:19 +00:00
|
|
|
<dees-icon icon="lucide:penLine" slot="iconSlot"></dees-icon>
|
2025-06-19 12:14:52 +00:00
|
|
|
Compose
|
|
|
|
|
</dees-button>
|
|
|
|
|
|
2025-06-17 14:37:05 +00:00
|
|
|
<dees-input-text
|
|
|
|
|
class="searchBox"
|
2026-02-02 00:36:19 +00:00
|
|
|
placeholder="Search..."
|
2025-06-17 14:37:05 +00:00
|
|
|
.value=${this.searchTerm}
|
|
|
|
|
@input=${(e: Event) => this.searchTerm = (e.target as any).value}
|
|
|
|
|
>
|
2026-02-02 00:36:19 +00:00
|
|
|
<dees-icon icon="lucide:search" slot="iconSlot"></dees-icon>
|
2025-06-17 14:37:05 +00:00
|
|
|
</dees-input-text>
|
|
|
|
|
|
2026-02-02 00:36:19 +00:00
|
|
|
<dees-button @click=${() => this.refreshData()}>
|
|
|
|
|
${this.isLoading ? html`<dees-spinner slot="iconSlot" size="small"></dees-spinner>` : html`<dees-icon slot="iconSlot" icon="lucide:refreshCw"></dees-icon>`}
|
2025-06-19 12:14:52 +00:00
|
|
|
Refresh
|
2025-06-17 14:37:05 +00:00
|
|
|
</dees-button>
|
|
|
|
|
|
2025-06-19 12:14:52 +00:00
|
|
|
<div style="margin-left: auto; display: flex; gap: 8px;">
|
|
|
|
|
<dees-button-group>
|
2026-02-02 00:36:19 +00:00
|
|
|
<dees-button
|
|
|
|
|
@click=${() => this.selectFolder('queued')}
|
|
|
|
|
.type=${this.selectedFolder === 'queued' ? 'highlighted' : 'normal'}
|
2025-06-19 12:14:52 +00:00
|
|
|
>
|
2026-02-02 00:36:19 +00:00
|
|
|
Queued ${this.queuedEmails.length > 0 ? `(${this.queuedEmails.length})` : ''}
|
2025-06-19 12:14:52 +00:00
|
|
|
</dees-button>
|
2026-02-02 00:36:19 +00:00
|
|
|
<dees-button
|
2025-06-19 12:14:52 +00:00
|
|
|
@click=${() => this.selectFolder('sent')}
|
|
|
|
|
.type=${this.selectedFolder === 'sent' ? 'highlighted' : 'normal'}
|
|
|
|
|
>
|
|
|
|
|
Sent
|
|
|
|
|
</dees-button>
|
2026-02-02 00:36:19 +00:00
|
|
|
<dees-button
|
|
|
|
|
@click=${() => this.selectFolder('failed')}
|
|
|
|
|
.type=${this.selectedFolder === 'failed' ? 'highlighted' : 'normal'}
|
2025-06-19 12:14:52 +00:00
|
|
|
>
|
2026-02-02 00:36:19 +00:00
|
|
|
Failed ${this.failedEmails.length > 0 ? `(${this.failedEmails.length})` : ''}
|
2025-06-19 12:14:52 +00:00
|
|
|
</dees-button>
|
2026-02-02 00:36:19 +00:00
|
|
|
<dees-button
|
|
|
|
|
@click=${() => this.selectFolder('security')}
|
|
|
|
|
.type=${this.selectedFolder === 'security' ? 'highlighted' : 'normal'}
|
2025-06-19 12:14:52 +00:00
|
|
|
>
|
2026-02-02 00:36:19 +00:00
|
|
|
Security ${this.securityIncidents.length > 0 ? `(${this.securityIncidents.length})` : ''}
|
2025-06-19 12:14:52 +00:00
|
|
|
</dees-button>
|
|
|
|
|
</dees-button-group>
|
|
|
|
|
</div>
|
2025-06-12 08:04:30 +00:00
|
|
|
</div>
|
2025-06-17 14:37:05 +00:00
|
|
|
|
2026-02-02 00:36:19 +00:00
|
|
|
${this.renderContent()}
|
2025-06-12 08:04:30 +00:00
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 00:36:19 +00:00
|
|
|
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');
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-19 12:14:52 +00:00
|
|
|
|
2026-02-02 00:36:19 +00:00
|
|
|
private renderEmailTable(
|
|
|
|
|
emails: interfaces.requests.IEmailQueueItem[],
|
|
|
|
|
heading1: string,
|
|
|
|
|
heading2: string,
|
|
|
|
|
showResend = false
|
|
|
|
|
) {
|
|
|
|
|
const filteredEmails = this.filterEmails(emails);
|
2025-06-12 08:04:30 +00:00
|
|
|
|
|
|
|
|
if (filteredEmails.length === 0) {
|
2026-02-02 00:36:19 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-06-12 08:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return html`
|
2025-06-17 14:37:05 +00:00
|
|
|
<dees-table
|
|
|
|
|
.data=${filteredEmails}
|
2026-02-02 00:36:19 +00:00
|
|
|
.displayFunction=${(email: interfaces.requests.IEmailQueueItem) => ({
|
|
|
|
|
'Status': html`<span class="status-${email.status}">${email.status}</span>`,
|
|
|
|
|
'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}`}
|
|
|
|
|
></dees-table>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderSecurityIncidents() {
|
|
|
|
|
const incidents = this.securityIncidents;
|
|
|
|
|
|
|
|
|
|
if (incidents.length === 0) {
|
|
|
|
|
return this.renderEmptyState('No security incidents');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return html`
|
|
|
|
|
<dees-table
|
|
|
|
|
.data=${incidents}
|
|
|
|
|
.displayFunction=${(incident: interfaces.requests.ISecurityIncident) => ({
|
|
|
|
|
'Severity': html`<span class="severity-${incident.level}">${incident.level.toUpperCase()}</span>`,
|
|
|
|
|
'Type': incident.type,
|
|
|
|
|
'Message': incident.message,
|
|
|
|
|
'IP': incident.ipAddress || 'N/A',
|
|
|
|
|
'Domain': incident.domain || 'N/A',
|
|
|
|
|
'Time': this.formatDate(incident.timestamp),
|
2025-06-17 14:37:05 +00:00
|
|
|
})}
|
|
|
|
|
.dataActions=${[
|
|
|
|
|
{
|
2026-02-02 00:36:19 +00:00
|
|
|
name: 'View Details',
|
|
|
|
|
iconName: 'lucide:eye',
|
2025-06-17 14:37:05 +00:00
|
|
|
type: ['doubleClick', 'inRow'],
|
2026-02-02 00:36:19 +00:00
|
|
|
actionFunc: async (actionData: any) => {
|
|
|
|
|
this.selectedIncident = actionData.item;
|
2025-06-17 14:37:05 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]}
|
|
|
|
|
.selectionMode=${'single'}
|
2026-02-02 00:36:19 +00:00
|
|
|
heading1="Security Incidents"
|
|
|
|
|
heading2=${`${incidents.length} incidents`}
|
2025-06-17 14:37:05 +00:00
|
|
|
></dees-table>
|
2025-06-12 08:04:30 +00:00
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 00:36:19 +00:00
|
|
|
private renderEmailDetail() {
|
2025-06-12 08:04:30 +00:00
|
|
|
if (!this.selectedEmail) return '';
|
|
|
|
|
|
|
|
|
|
return html`
|
2026-02-02 00:36:19 +00:00
|
|
|
<ops-sectionheading>Email Details</ops-sectionheading>
|
|
|
|
|
<div class="emailLayout">
|
|
|
|
|
<div class="sidebar">
|
|
|
|
|
<dees-windowbox>
|
|
|
|
|
<dees-button @click=${() => this.selectedEmail = null} type="secondary" style="width: 100%;">
|
|
|
|
|
<dees-icon icon="lucide:arrowLeft" slot="iconSlot"></dees-icon>
|
|
|
|
|
Back to List
|
|
|
|
|
</dees-button>
|
|
|
|
|
</dees-windowbox>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mainArea">
|
|
|
|
|
<div class="emailPreview">
|
|
|
|
|
<div class="emailHeader">
|
|
|
|
|
<div class="emailSubject">${this.selectedEmail.subject || 'No subject'}</div>
|
|
|
|
|
<div class="emailMeta">
|
|
|
|
|
<div class="emailMetaRow">
|
|
|
|
|
<span class="emailMetaLabel">Status:</span>
|
|
|
|
|
<span class="status-${this.selectedEmail.status}">${this.selectedEmail.status}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="emailMetaRow">
|
|
|
|
|
<span class="emailMetaLabel">From:</span>
|
|
|
|
|
<span>${this.selectedEmail.from || 'N/A'}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="emailMetaRow">
|
|
|
|
|
<span class="emailMetaLabel">To:</span>
|
|
|
|
|
<span>${this.selectedEmail.to?.join(', ') || 'N/A'}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="emailMetaRow">
|
|
|
|
|
<span class="emailMetaLabel">Mode:</span>
|
|
|
|
|
<span>${this.selectedEmail.processingMode}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="emailMetaRow">
|
|
|
|
|
<span class="emailMetaLabel">Attempts:</span>
|
|
|
|
|
<span>${this.selectedEmail.attempts}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="emailMetaRow">
|
|
|
|
|
<span class="emailMetaLabel">Created:</span>
|
|
|
|
|
<span>${new Date(this.selectedEmail.createdAt).toLocaleString()}</span>
|
|
|
|
|
</div>
|
|
|
|
|
${this.selectedEmail.deliveredAt ? html`
|
|
|
|
|
<div class="emailMetaRow">
|
|
|
|
|
<span class="emailMetaLabel">Delivered:</span>
|
|
|
|
|
<span>${new Date(this.selectedEmail.deliveredAt).toLocaleString()}</span>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
${this.selectedEmail.lastError ? html`
|
|
|
|
|
<div class="emailMetaRow">
|
|
|
|
|
<span class="emailMetaLabel">Last Error:</span>
|
|
|
|
|
<span style="color: #ef4444;">${this.selectedEmail.lastError}</span>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
2025-06-17 14:37:05 +00:00
|
|
|
</div>
|
2026-02-02 00:36:19 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="emailActions">
|
|
|
|
|
${this.selectedEmail.status === 'failed' ? html`
|
|
|
|
|
<dees-button @click=${() => this.resendEmail(this.selectedEmail!.id)} type="highlighted">
|
|
|
|
|
<dees-icon icon="lucide:send" slot="iconSlot"></dees-icon>
|
|
|
|
|
Resend
|
|
|
|
|
</dees-button>
|
|
|
|
|
` : ''}
|
|
|
|
|
<dees-button @click=${() => this.selectedEmail = null}>
|
|
|
|
|
<dees-icon icon="lucide:x" slot="iconSlot"></dees-icon>
|
|
|
|
|
Close
|
|
|
|
|
</dees-button>
|
2025-06-17 14:37:05 +00:00
|
|
|
</div>
|
2025-06-12 08:04:30 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-02 00:36:19 +00:00
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
2025-06-12 08:04:30 +00:00
|
|
|
|
2026-02-02 00:36:19 +00:00
|
|
|
private renderIncidentDetail() {
|
|
|
|
|
if (!this.selectedIncident) return '';
|
|
|
|
|
|
|
|
|
|
const incident = this.selectedIncident;
|
|
|
|
|
|
|
|
|
|
return html`
|
|
|
|
|
<ops-sectionheading>Security Incident Details</ops-sectionheading>
|
|
|
|
|
<div style="margin-bottom: 16px;">
|
|
|
|
|
<dees-button @click=${() => this.selectedIncident = null} type="secondary">
|
|
|
|
|
<dees-icon icon="lucide:arrowLeft" slot="iconSlot"></dees-icon>
|
|
|
|
|
Back to List
|
|
|
|
|
</dees-button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="incidentDetails">
|
|
|
|
|
<div class="incidentHeader">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="incidentTitle">${incident.message}</div>
|
|
|
|
|
<div style="margin-top: 8px; color: #666;">
|
|
|
|
|
${new Date(incident.timestamp).toLocaleString()}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="severity-${incident.level}" style="font-size: 16px; padding: 4px 12px; background: rgba(0,0,0,0.1); border-radius: 4px;">
|
|
|
|
|
${incident.level.toUpperCase()}
|
|
|
|
|
</span>
|
2025-06-12 08:04:30 +00:00
|
|
|
</div>
|
|
|
|
|
|
2026-02-02 00:36:19 +00:00
|
|
|
<div class="incidentMeta">
|
|
|
|
|
<div class="incidentField">
|
|
|
|
|
<div class="incidentFieldLabel">Type</div>
|
|
|
|
|
<div class="incidentFieldValue">${incident.type}</div>
|
2025-06-17 14:37:05 +00:00
|
|
|
</div>
|
2026-02-02 00:36:19 +00:00
|
|
|
${incident.ipAddress ? html`
|
|
|
|
|
<div class="incidentField">
|
|
|
|
|
<div class="incidentFieldLabel">IP Address</div>
|
|
|
|
|
<div class="incidentFieldValue">${incident.ipAddress}</div>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
${incident.domain ? html`
|
|
|
|
|
<div class="incidentField">
|
|
|
|
|
<div class="incidentFieldLabel">Domain</div>
|
|
|
|
|
<div class="incidentFieldValue">${incident.domain}</div>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
${incident.emailId ? html`
|
|
|
|
|
<div class="incidentField">
|
|
|
|
|
<div class="incidentFieldLabel">Email ID</div>
|
|
|
|
|
<div class="incidentFieldValue">${incident.emailId}</div>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
${incident.userId ? html`
|
|
|
|
|
<div class="incidentField">
|
|
|
|
|
<div class="incidentFieldLabel">User ID</div>
|
|
|
|
|
<div class="incidentFieldValue">${incident.userId}</div>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
${incident.action ? html`
|
|
|
|
|
<div class="incidentField">
|
|
|
|
|
<div class="incidentFieldLabel">Action</div>
|
|
|
|
|
<div class="incidentFieldValue">${incident.action}</div>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
${incident.result ? html`
|
|
|
|
|
<div class="incidentField">
|
|
|
|
|
<div class="incidentFieldLabel">Result</div>
|
|
|
|
|
<div class="incidentFieldValue">${incident.result}</div>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
${incident.success !== undefined ? html`
|
|
|
|
|
<div class="incidentField">
|
|
|
|
|
<div class="incidentFieldLabel">Success</div>
|
|
|
|
|
<div class="incidentFieldValue">${incident.success ? 'Yes' : 'No'}</div>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
2025-06-12 08:04:30 +00:00
|
|
|
</div>
|
2026-02-02 00:36:19 +00:00
|
|
|
|
|
|
|
|
${incident.details ? html`
|
|
|
|
|
<div style="margin-top: 24px;">
|
|
|
|
|
<div class="incidentFieldLabel" style="margin-bottom: 8px;">Details</div>
|
|
|
|
|
<pre style="background: #1a1a1a; color: #e5e5e5; padding: 16px; border-radius: 6px; overflow-x: auto; font-size: 13px;">
|
|
|
|
|
${JSON.stringify(incident.details, null, 2)}
|
|
|
|
|
</pre>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderEmptyState(message: string) {
|
|
|
|
|
return html`
|
|
|
|
|
<div class="emptyState">
|
|
|
|
|
<dees-icon class="emptyIcon" icon="lucide:inbox"></dees-icon>
|
|
|
|
|
<div class="emptyText">${message}</div>
|
2025-06-12 08:04:30 +00:00
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 00:36:19 +00:00
|
|
|
private async openComposeModal() {
|
2025-06-17 14:37:05 +00:00
|
|
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
2026-02-02 00:36:19 +00:00
|
|
|
|
2025-06-29 18:47:44 +00:00
|
|
|
// Ensure domains are loaded before opening modal
|
|
|
|
|
if (this.emailDomains.length === 0) {
|
|
|
|
|
await this.loadEmailDomains();
|
|
|
|
|
}
|
2026-02-02 00:36:19 +00:00
|
|
|
|
2025-06-17 14:37:05 +00:00
|
|
|
await DeesModal.createAndShow({
|
2026-02-02 00:36:19 +00:00
|
|
|
heading: 'New Email',
|
2025-06-27 09:28:07 +00:00
|
|
|
width: 'large',
|
2025-06-17 14:37:05 +00:00
|
|
|
content: html`
|
2025-06-27 09:28:07 +00:00
|
|
|
<div>
|
2025-06-17 14:37:05 +00:00
|
|
|
<dees-form @formData=${async (e: CustomEvent) => {
|
|
|
|
|
await this.sendEmail(e.detail);
|
|
|
|
|
const modals = document.querySelectorAll('dees-modal');
|
|
|
|
|
modals.forEach(m => (m as any).destroy?.());
|
|
|
|
|
}}>
|
2025-06-29 18:47:44 +00:00
|
|
|
<div style="display: flex; gap: 8px; align-items: flex-end;">
|
|
|
|
|
<dees-input-text
|
|
|
|
|
key="fromUsername"
|
|
|
|
|
label="From"
|
|
|
|
|
placeholder="username"
|
|
|
|
|
.value=${'admin'}
|
|
|
|
|
required
|
|
|
|
|
style="flex: 1;"
|
|
|
|
|
></dees-input-text>
|
|
|
|
|
<span style="padding-bottom: 12px; font-size: 18px; color: #666;">@</span>
|
|
|
|
|
<dees-input-dropdown
|
|
|
|
|
key="fromDomain"
|
|
|
|
|
label=" "
|
2026-02-02 00:36:19 +00:00
|
|
|
.options=${this.emailDomains.length > 0
|
2025-06-29 18:47:44 +00:00
|
|
|
? this.emailDomains.map(domain => ({ key: domain, value: domain }))
|
|
|
|
|
: [{ key: 'dcrouter.local', value: 'dcrouter.local' }]}
|
|
|
|
|
.selectedKey=${this.emailDomains[0] || 'dcrouter.local'}
|
|
|
|
|
required
|
|
|
|
|
style="flex: 1;"
|
|
|
|
|
></dees-input-dropdown>
|
|
|
|
|
</div>
|
2026-02-02 00:36:19 +00:00
|
|
|
|
|
|
|
|
<dees-input-tags
|
|
|
|
|
key="to"
|
|
|
|
|
label="To"
|
2025-06-17 14:37:05 +00:00
|
|
|
placeholder="Enter recipient email addresses..."
|
|
|
|
|
required
|
|
|
|
|
></dees-input-tags>
|
2026-02-02 00:36:19 +00:00
|
|
|
|
|
|
|
|
<dees-input-tags
|
|
|
|
|
key="cc"
|
|
|
|
|
label="CC"
|
2025-06-17 14:37:05 +00:00
|
|
|
placeholder="Enter CC recipients..."
|
|
|
|
|
></dees-input-tags>
|
2026-02-02 00:36:19 +00:00
|
|
|
|
|
|
|
|
<dees-input-text
|
|
|
|
|
key="subject"
|
|
|
|
|
label="Subject"
|
2025-06-17 14:37:05 +00:00
|
|
|
placeholder="Enter email subject..."
|
|
|
|
|
required
|
|
|
|
|
></dees-input-text>
|
2026-02-02 00:36:19 +00:00
|
|
|
|
|
|
|
|
<dees-input-wysiwyg
|
|
|
|
|
key="body"
|
2025-06-17 14:37:05 +00:00
|
|
|
label="Message"
|
2025-06-27 09:28:07 +00:00
|
|
|
outputFormat="html"
|
|
|
|
|
></dees-input-wysiwyg>
|
2025-06-17 14:37:05 +00:00
|
|
|
</dees-form>
|
2025-06-12 08:04:30 +00:00
|
|
|
</div>
|
2025-06-17 14:37:05 +00:00
|
|
|
`,
|
|
|
|
|
menuOptions: [
|
|
|
|
|
{
|
|
|
|
|
name: 'Send',
|
2026-02-02 00:36:19 +00:00
|
|
|
iconName: 'lucide:send',
|
2025-06-17 14:37:05 +00:00
|
|
|
action: async (modalArg) => {
|
|
|
|
|
const form = modalArg.shadowRoot?.querySelector('dees-form') as any;
|
|
|
|
|
form?.submit();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'Cancel',
|
2026-02-02 00:36:19 +00:00
|
|
|
iconName: 'lucide:x',
|
2025-06-17 14:37:05 +00:00
|
|
|
action: async (modalArg) => await modalArg.destroy()
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
});
|
2025-06-12 08:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 00:36:19 +00:00
|
|
|
private filterEmails(emails: interfaces.requests.IEmailQueueItem[]): interfaces.requests.IEmailQueueItem[] {
|
|
|
|
|
if (!this.searchTerm) {
|
|
|
|
|
return emails;
|
2025-06-12 08:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 00:36:19 +00:00
|
|
|
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)))
|
|
|
|
|
);
|
2025-06-12 08:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 00:36:19 +00:00
|
|
|
private selectFolder(folder: TEmailFolder) {
|
|
|
|
|
// Use router for navigation to update URL
|
|
|
|
|
appRouter.navigateToEmailFolder(folder);
|
|
|
|
|
// Clear selections
|
2025-06-12 08:04:30 +00:00
|
|
|
this.selectedEmail = null;
|
2026-02-02 00:36:19 +00:00
|
|
|
this.selectedIncident = null;
|
2025-06-12 08:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
2026-02-02 00:36:19 +00:00
|
|
|
|
2025-06-12 08:04:30 +00:00
|
|
|
if (hours < 24) {
|
|
|
|
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
|
|
|
} else if (hours < 168) { // 7 days
|
2026-02-02 00:36:19 +00:00
|
|
|
return date.toLocaleDateString([], { weekday: 'short', hour: '2-digit', minute: '2-digit' });
|
2025-06-12 08:04:30 +00:00
|
|
|
} else {
|
|
|
|
|
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 00:36:19 +00:00
|
|
|
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;
|
|
|
|
|
}
|
2025-06-12 08:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
2025-06-29 18:47:44 +00:00
|
|
|
private async loadEmailDomains() {
|
|
|
|
|
try {
|
|
|
|
|
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
|
|
|
|
|
const config = appstate.configStatePart.getState().config;
|
2026-02-02 00:36:19 +00:00
|
|
|
|
2025-06-29 18:47:44 +00:00
|
|
|
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'];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 00:36:19 +00:00
|
|
|
private async refreshData() {
|
2025-06-12 08:04:30 +00:00
|
|
|
this.isLoading = true;
|
2026-02-02 00:36:19 +00:00
|
|
|
await this.loadFolderData(this.selectedFolder);
|
2025-06-12 08:04:30 +00:00
|
|
|
this.isLoading = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async sendEmail(formData: any) {
|
|
|
|
|
try {
|
|
|
|
|
console.log('Sending email:', formData);
|
2026-02-02 00:36:19 +00:00
|
|
|
// TODO: Implement actual email sending via API
|
|
|
|
|
// For now, just log the data
|
2025-06-29 18:47:44 +00:00
|
|
|
const fromEmail = `${formData.fromUsername || 'admin'}@${formData.fromDomain || this.emailDomains[0] || 'dcrouter.local'}`;
|
2026-02-02 00:36:19 +00:00
|
|
|
console.log('From:', fromEmail);
|
|
|
|
|
console.log('To:', formData.to);
|
|
|
|
|
console.log('Subject:', formData.subject);
|
2025-06-12 08:04:30 +00:00
|
|
|
} catch (error: any) {
|
|
|
|
|
console.error('Failed to send email', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 00:36:19 +00:00
|
|
|
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);
|
2025-06-12 08:04:30 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 00:36:19 +00:00
|
|
|
}
|