681 lines
20 KiB
TypeScript
681 lines
20 KiB
TypeScript
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';
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'ops-view-emails': OpsViewEmails;
|
|
}
|
|
}
|
|
|
|
interface IEmail {
|
|
id: string;
|
|
from: string;
|
|
to: string[];
|
|
cc?: string[];
|
|
bcc?: string[];
|
|
subject: string;
|
|
body: string;
|
|
html?: string;
|
|
attachments?: Array<{
|
|
filename: string;
|
|
size: number;
|
|
contentType: string;
|
|
}>;
|
|
date: number;
|
|
read: boolean;
|
|
folder: 'inbox' | 'sent' | 'draft' | 'trash';
|
|
flags?: string[];
|
|
messageId?: string;
|
|
inReplyTo?: string;
|
|
}
|
|
|
|
@customElement('ops-view-emails')
|
|
export class OpsViewEmails extends DeesElement {
|
|
@state()
|
|
private selectedFolder: 'inbox' | 'sent' | 'draft' | 'trash' = 'inbox';
|
|
|
|
@state()
|
|
private emails: IEmail[] = [];
|
|
|
|
@state()
|
|
private selectedEmail: IEmail | null = null;
|
|
|
|
@state()
|
|
private showCompose = false;
|
|
|
|
@state()
|
|
private isLoading = false;
|
|
|
|
@state()
|
|
private searchTerm = '';
|
|
|
|
constructor() {
|
|
super();
|
|
this.loadEmails();
|
|
}
|
|
|
|
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: 60px;
|
|
}
|
|
|
|
.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: 100%;
|
|
color: ${cssManager.bdTheme('#999', '#666')};
|
|
}
|
|
|
|
.emptyIcon {
|
|
font-size: 64px;
|
|
margin-bottom: 16px;
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.emptyText {
|
|
font-size: 18px;
|
|
}
|
|
|
|
.email-read {
|
|
color: ${cssManager.bdTheme('#999', '#666')};
|
|
}
|
|
|
|
.email-unread {
|
|
color: ${cssManager.bdTheme('#1976d2', '#4a90e2')};
|
|
}
|
|
|
|
.attachment-icon {
|
|
color: ${cssManager.bdTheme('#666', '#999')};
|
|
}
|
|
`,
|
|
];
|
|
|
|
public render() {
|
|
if (this.selectedEmail) {
|
|
return html`
|
|
<ops-sectionheading>Emails</ops-sectionheading>
|
|
<div class="emailLayout">
|
|
<div class="sidebar">
|
|
<dees-windowbox>
|
|
<dees-button @click=${() => this.selectedEmail = null} type="secondary" style="width: 100%;">
|
|
<dees-icon name="arrowLeft" slot="iconSlot"></dees-icon>
|
|
Back to List
|
|
</dees-button>
|
|
<dees-menu style="margin-top: 16px;">
|
|
<dees-menu-item
|
|
.active=${this.selectedFolder === 'inbox'}
|
|
@click=${() => { this.selectFolder('inbox'); this.selectedEmail = null; }}
|
|
.iconName=${'inbox'}
|
|
.label=${'Inbox'}
|
|
.badgeText=${this.getEmailCount('inbox') > 0 ? String(this.getEmailCount('inbox')) : ''}
|
|
></dees-menu-item>
|
|
<dees-menu-item
|
|
.active=${this.selectedFolder === 'sent'}
|
|
@click=${() => { this.selectFolder('sent'); this.selectedEmail = null; }}
|
|
.iconName=${'paperPlane'}
|
|
.label=${'Sent'}
|
|
.badgeText=${this.getEmailCount('sent') > 0 ? String(this.getEmailCount('sent')) : ''}
|
|
></dees-menu-item>
|
|
<dees-menu-item
|
|
.active=${this.selectedFolder === 'draft'}
|
|
@click=${() => { this.selectFolder('draft'); this.selectedEmail = null; }}
|
|
.iconName=${'file'}
|
|
.label=${'Drafts'}
|
|
.badgeText=${this.getEmailCount('draft') > 0 ? String(this.getEmailCount('draft')) : ''}
|
|
></dees-menu-item>
|
|
<dees-menu-item
|
|
.active=${this.selectedFolder === 'trash'}
|
|
@click=${() => { this.selectFolder('trash'); this.selectedEmail = null; }}
|
|
.iconName=${'trash'}
|
|
.label=${'Trash'}
|
|
.badgeText=${this.getEmailCount('trash') > 0 ? String(this.getEmailCount('trash')) : ''}
|
|
></dees-menu-item>
|
|
</dees-menu>
|
|
</dees-windowbox>
|
|
</div>
|
|
<div class="mainArea">
|
|
${this.renderEmailPreview()}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return html`
|
|
<ops-sectionheading>Emails</ops-sectionheading>
|
|
|
|
<!-- Toolbar -->
|
|
<div class="emailToolbar" style="margin-bottom: 16px;">
|
|
<dees-button @click=${() => this.openComposeModal()} type="highlighted">
|
|
<dees-icon name="penToSquare" slot="iconSlot"></dees-icon>
|
|
Compose
|
|
</dees-button>
|
|
|
|
<dees-input-text
|
|
class="searchBox"
|
|
placeholder="Search emails..."
|
|
.value=${this.searchTerm}
|
|
@input=${(e: Event) => this.searchTerm = (e.target as any).value}
|
|
>
|
|
<dees-icon name="magnifyingGlass" slot="iconSlot"></dees-icon>
|
|
</dees-input-text>
|
|
|
|
<dees-button @click=${() => this.refreshEmails()}>
|
|
${this.isLoading ? html`<dees-spinner slot="iconSlot" size="small"></dees-spinner>` : html`<dees-icon slot="iconSlot" name="arrowsRotate"></dees-icon>`}
|
|
Refresh
|
|
</dees-button>
|
|
|
|
<dees-button @click=${() => this.markAllAsRead()}>
|
|
<dees-icon name="envelopeOpen" slot="iconSlot"></dees-icon>
|
|
Mark all read
|
|
</dees-button>
|
|
|
|
<div style="margin-left: auto; display: flex; gap: 8px;">
|
|
<dees-button-group>
|
|
<dees-button
|
|
@click=${() => this.selectFolder('inbox')}
|
|
.type=${this.selectedFolder === 'inbox' ? 'highlighted' : 'normal'}
|
|
>
|
|
Inbox ${this.getEmailCount('inbox') > 0 ? `(${this.getEmailCount('inbox')})` : ''}
|
|
</dees-button>
|
|
<dees-button
|
|
@click=${() => this.selectFolder('sent')}
|
|
.type=${this.selectedFolder === 'sent' ? 'highlighted' : 'normal'}
|
|
>
|
|
Sent
|
|
</dees-button>
|
|
<dees-button
|
|
@click=${() => this.selectFolder('draft')}
|
|
.type=${this.selectedFolder === 'draft' ? 'highlighted' : 'normal'}
|
|
>
|
|
Drafts ${this.getEmailCount('draft') > 0 ? `(${this.getEmailCount('draft')})` : ''}
|
|
</dees-button>
|
|
<dees-button
|
|
@click=${() => this.selectFolder('trash')}
|
|
.type=${this.selectedFolder === 'trash' ? 'highlighted' : 'normal'}
|
|
>
|
|
Trash
|
|
</dees-button>
|
|
</dees-button-group>
|
|
</div>
|
|
</div>
|
|
|
|
${this.renderEmailList()}
|
|
`;
|
|
}
|
|
|
|
|
|
private renderEmailList() {
|
|
const filteredEmails = this.getFilteredEmails();
|
|
|
|
if (filteredEmails.length === 0) {
|
|
return html`
|
|
<div class="emptyState">
|
|
<dees-icon class="emptyIcon" name="envelope"></dees-icon>
|
|
<div class="emptyText">No emails in ${this.selectedFolder}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return html`
|
|
<dees-table
|
|
.data=${filteredEmails}
|
|
.displayFunction=${(email: IEmail) => ({
|
|
'Status': html`<dees-icon name="${email.read ? 'envelopeOpen' : 'envelope'}" class="${email.read ? 'email-read' : 'email-unread'}"></dees-icon>`,
|
|
From: email.from,
|
|
Subject: html`<strong style="${!email.read ? 'font-weight: 600' : ''}">${email.subject}</strong>`,
|
|
Date: this.formatDate(email.date),
|
|
'Attach': html`
|
|
${email.attachments?.length ? html`<dees-icon name="paperclip" class="attachment-icon"></dees-icon>` : ''}
|
|
`,
|
|
})}
|
|
.dataActions=${[
|
|
{
|
|
name: 'Read',
|
|
iconName: 'eye',
|
|
type: ['doubleClick', 'inRow'],
|
|
actionFunc: async (actionData) => {
|
|
this.selectedEmail = actionData.item;
|
|
if (!actionData.item.read) {
|
|
this.markAsRead(actionData.item.id);
|
|
}
|
|
}
|
|
},
|
|
{
|
|
name: 'Reply',
|
|
iconName: 'reply',
|
|
type: ['contextmenu'],
|
|
actionFunc: async (actionData) => {
|
|
this.replyToEmail(actionData.item);
|
|
}
|
|
},
|
|
{
|
|
name: 'Forward',
|
|
iconName: 'share',
|
|
type: ['contextmenu'],
|
|
actionFunc: async (actionData) => {
|
|
this.forwardEmail(actionData.item);
|
|
}
|
|
},
|
|
{
|
|
name: 'Delete',
|
|
iconName: 'trash',
|
|
type: ['contextmenu'],
|
|
actionFunc: async (actionData) => {
|
|
this.deleteEmail(actionData.item.id);
|
|
}
|
|
}
|
|
]}
|
|
.selectionMode=${'single'}
|
|
heading1=${this.selectedFolder.charAt(0).toUpperCase() + this.selectedFolder.slice(1)}
|
|
heading2=${`${filteredEmails.length} emails`}
|
|
></dees-table>
|
|
`;
|
|
}
|
|
|
|
private renderEmailPreview() {
|
|
if (!this.selectedEmail) return '';
|
|
|
|
return html`
|
|
<div class="emailPreview">
|
|
<div class="emailHeader">
|
|
<div class="emailSubject">${this.selectedEmail.subject}</div>
|
|
<div class="emailMeta">
|
|
<div class="emailMetaRow">
|
|
<span class="emailMetaLabel">From:</span>
|
|
<span>${this.selectedEmail.from}</span>
|
|
</div>
|
|
<div class="emailMetaRow">
|
|
<span class="emailMetaLabel">To:</span>
|
|
<span>${this.selectedEmail.to.join(', ')}</span>
|
|
</div>
|
|
${this.selectedEmail.cc?.length ? html`
|
|
<div class="emailMetaRow">
|
|
<span class="emailMetaLabel">CC:</span>
|
|
<span>${this.selectedEmail.cc.join(', ')}</span>
|
|
</div>
|
|
` : ''}
|
|
<div class="emailMetaRow">
|
|
<span class="emailMetaLabel">Date:</span>
|
|
<span>${new Date(this.selectedEmail.date).toLocaleString()}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="emailBody">
|
|
${this.selectedEmail.html ?
|
|
html`<div .innerHTML=${this.selectedEmail.html}></div>` :
|
|
html`<div style="white-space: pre-wrap;">${this.selectedEmail.body}</div>`
|
|
}
|
|
</div>
|
|
|
|
<div class="emailActions">
|
|
<div style="display: flex; gap: 8px;">
|
|
<dees-button @click=${() => this.replyToEmail(this.selectedEmail!)}>
|
|
<dees-icon name="reply" slot="iconSlot"></dees-icon>
|
|
Reply
|
|
</dees-button>
|
|
<dees-button @click=${() => this.replyAllToEmail(this.selectedEmail!)}>
|
|
<dees-icon name="replyAll" slot="iconSlot"></dees-icon>
|
|
Reply All
|
|
</dees-button>
|
|
<dees-button @click=${() => this.forwardEmail(this.selectedEmail!)}>
|
|
<dees-icon name="share" slot="iconSlot"></dees-icon>
|
|
Forward
|
|
</dees-button>
|
|
<dees-button @click=${() => this.deleteEmail(this.selectedEmail!.id)} type="danger">
|
|
<dees-icon name="trash" slot="iconSlot"></dees-icon>
|
|
Delete
|
|
</dees-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private async openComposeModal(replyTo?: IEmail, replyAll = false, forward = false) {
|
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
|
|
await DeesModal.createAndShow({
|
|
heading: forward ? 'Forward Email' : replyTo ? 'Reply to Email' : 'New Email',
|
|
content: html`
|
|
<div style="width: 700px; max-width: 90vw;">
|
|
<dees-form @formData=${async (e: CustomEvent) => {
|
|
await this.sendEmail(e.detail);
|
|
// Close modal after sending
|
|
const modals = document.querySelectorAll('dees-modal');
|
|
modals.forEach(m => (m as any).destroy?.());
|
|
}}>
|
|
<dees-input-tags
|
|
key="to"
|
|
label="To"
|
|
placeholder="Enter recipient email addresses..."
|
|
.value=${replyTo ? (replyAll ? [replyTo.from, ...replyTo.to].filter((v, i, a) => a.indexOf(v) === i) : [replyTo.from]) : []}
|
|
required
|
|
></dees-input-tags>
|
|
|
|
<dees-input-tags
|
|
key="cc"
|
|
label="CC"
|
|
placeholder="Enter CC recipients..."
|
|
.value=${replyAll && replyTo?.cc ? replyTo.cc : []}
|
|
></dees-input-tags>
|
|
|
|
<dees-input-tags
|
|
key="bcc"
|
|
label="BCC"
|
|
placeholder="Enter BCC recipients..."
|
|
></dees-input-tags>
|
|
|
|
<dees-input-text
|
|
key="subject"
|
|
label="Subject"
|
|
placeholder="Enter email subject..."
|
|
.value=${replyTo ? `${forward ? 'Fwd' : 'Re'}: ${replyTo.subject}` : ''}
|
|
required
|
|
></dees-input-text>
|
|
|
|
<dees-editor
|
|
key="body"
|
|
label="Message"
|
|
.mode=${'markdown'}
|
|
.height=${400}
|
|
.value=${replyTo && !forward ? `\n\n---\nOn ${new Date(replyTo.date).toLocaleString()}, ${replyTo.from} wrote:\n\n${replyTo.body}` : replyTo && forward ? replyTo.body : ''}
|
|
></dees-editor>
|
|
|
|
<dees-input-fileupload
|
|
key="attachments"
|
|
label="Attachments"
|
|
multiple
|
|
></dees-input-fileupload>
|
|
</dees-form>
|
|
</div>
|
|
`,
|
|
menuOptions: [
|
|
{
|
|
name: 'Send',
|
|
iconName: 'paperPlane',
|
|
action: async (modalArg) => {
|
|
const form = modalArg.shadowRoot?.querySelector('dees-form') as any;
|
|
form?.submit();
|
|
}
|
|
},
|
|
{
|
|
name: 'Cancel',
|
|
iconName: 'xmark',
|
|
action: async (modalArg) => await modalArg.destroy()
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
private getFilteredEmails(): IEmail[] {
|
|
let emails = this.emails.filter(e => e.folder === this.selectedFolder);
|
|
|
|
if (this.searchTerm) {
|
|
const search = this.searchTerm.toLowerCase();
|
|
emails = emails.filter(e =>
|
|
e.subject.toLowerCase().includes(search) ||
|
|
e.from.toLowerCase().includes(search) ||
|
|
e.body.toLowerCase().includes(search)
|
|
);
|
|
}
|
|
|
|
return emails.sort((a, b) => b.date - a.date);
|
|
}
|
|
|
|
private getEmailCount(folder: string): number {
|
|
return this.emails.filter(e => e.folder === folder && !e.read).length;
|
|
}
|
|
|
|
private selectFolder(folder: 'inbox' | 'sent' | 'draft' | 'trash') {
|
|
this.selectedFolder = folder;
|
|
this.selectedEmail = 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' });
|
|
} else {
|
|
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
}
|
|
}
|
|
|
|
private async loadEmails() {
|
|
// TODO: Load real emails from server
|
|
// For now, generate mock data
|
|
this.generateMockEmails();
|
|
}
|
|
|
|
private async refreshEmails() {
|
|
this.isLoading = true;
|
|
await this.loadEmails();
|
|
this.isLoading = false;
|
|
}
|
|
|
|
private async sendEmail(formData: any) {
|
|
try {
|
|
// TODO: Implement actual email sending via API
|
|
console.log('Sending email:', formData);
|
|
|
|
// Add to sent folder (mock)
|
|
const newEmail: IEmail = {
|
|
id: `email-${Date.now()}`,
|
|
from: 'me@dcrouter.local',
|
|
to: formData.to || [],
|
|
cc: formData.cc || [],
|
|
bcc: formData.bcc || [],
|
|
subject: formData.subject,
|
|
body: formData.body,
|
|
date: Date.now(),
|
|
read: true,
|
|
folder: 'sent',
|
|
};
|
|
|
|
this.emails = [...this.emails, newEmail];
|
|
|
|
// Show success notification
|
|
console.log('Email sent successfully');
|
|
// TODO: Show toast notification when interface is available
|
|
} catch (error: any) {
|
|
console.error('Failed to send email', error);
|
|
// TODO: Show error toast notification when interface is available
|
|
}
|
|
}
|
|
|
|
private async markAsRead(emailId: string) {
|
|
const email = this.emails.find(e => e.id === emailId);
|
|
if (email) {
|
|
email.read = true;
|
|
this.emails = [...this.emails];
|
|
}
|
|
}
|
|
|
|
private async markAllAsRead() {
|
|
this.emails = this.emails.map(e =>
|
|
e.folder === this.selectedFolder ? { ...e, read: true } : e
|
|
);
|
|
}
|
|
|
|
private async deleteEmail(emailId: string) {
|
|
const email = this.emails.find(e => e.id === emailId);
|
|
if (email) {
|
|
if (email.folder === 'trash') {
|
|
// Permanently delete
|
|
this.emails = this.emails.filter(e => e.id !== emailId);
|
|
} else {
|
|
// Move to trash
|
|
email.folder = 'trash';
|
|
this.emails = [...this.emails];
|
|
}
|
|
|
|
if (this.selectedEmail?.id === emailId) {
|
|
this.selectedEmail = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async replyToEmail(email: IEmail) {
|
|
this.openComposeModal(email, false, false);
|
|
}
|
|
|
|
private async replyAllToEmail(email: IEmail) {
|
|
this.openComposeModal(email, true, false);
|
|
}
|
|
|
|
private async forwardEmail(email: IEmail) {
|
|
this.openComposeModal(email, false, true);
|
|
}
|
|
|
|
private generateMockEmails() {
|
|
const subjects = [
|
|
'Server Alert: High CPU Usage',
|
|
'Daily Report - Network Activity',
|
|
'Security Update Required',
|
|
'New User Registration',
|
|
'Backup Completed Successfully',
|
|
'DNS Query Spike Detected',
|
|
'SSL Certificate Renewal Notice',
|
|
'Monthly Usage Summary',
|
|
];
|
|
|
|
const senders = [
|
|
'monitoring@dcrouter.local',
|
|
'alerts@system.local',
|
|
'admin@company.com',
|
|
'noreply@service.com',
|
|
'support@vendor.com',
|
|
];
|
|
|
|
const bodies = [
|
|
'This is an automated alert regarding your server status.',
|
|
'Please review the attached report for detailed information.',
|
|
'Action required: Update your security settings.',
|
|
'Your daily summary is ready for review.',
|
|
'All systems are operating normally.',
|
|
];
|
|
|
|
this.emails = Array.from({ length: 50 }, (_, i) => ({
|
|
id: `email-${i}`,
|
|
from: senders[Math.floor(Math.random() * senders.length)],
|
|
to: ['admin@dcrouter.local'],
|
|
subject: subjects[Math.floor(Math.random() * subjects.length)],
|
|
body: bodies[Math.floor(Math.random() * bodies.length)],
|
|
date: Date.now() - (i * 3600000), // 1 hour apart
|
|
read: Math.random() > 0.3,
|
|
folder: i < 40 ? 'inbox' : i < 45 ? 'sent' : 'trash',
|
|
attachments: Math.random() > 0.8 ? [{
|
|
filename: 'report.pdf',
|
|
size: 1024 * 1024,
|
|
contentType: 'application/pdf',
|
|
}] : undefined,
|
|
}));
|
|
}
|
|
} |