import { DeesElement, customElement, html, css, cssManager, state, type TemplateResult } from '../plugins.js'; import { deesCatalog } from '../plugins.js'; import { appState, type IAppState, type IContact } from '../state/appstate.js'; import { appRouter } from '../router.js'; import { viewHostCss } from './shared/index.js'; import type { IStatsTile } from '@design.estate/dees-catalog'; @customElement('sipproxy-view-contacts') export class SipproxyViewContacts extends DeesElement { @state() accessor appData: IAppState = appState.getState(); @state() accessor saving = false; public static styles = [ cssManager.defaultStyles, viewHostCss, css` :host { display: block; padding: 16px; } .view-section { margin-bottom: 24px; } `, ]; connectedCallback() { super.connectedCallback(); this.rxSubscriptions.push({ unsubscribe: appState.subscribe((s) => { this.appData = s; }), } as any); } // ---------- CRUD operations ---------- private async openAddModal() { const { DeesModal } = await import('@design.estate/dees-catalog'); const formData = { name: '', number: '', company: '', notes: '' }; await DeesModal.createAndShow({ heading: 'Add Contact', content: html`
{ formData.name = (e.target as any).value; }} > { formData.number = (e.target as any).value; }} > { formData.company = (e.target as any).value; }} > { formData.notes = (e.target as any).value; }} >
`, menuOptions: [ { name: 'Cancel', action: async (modal) => { modal.destroy(); }, }, { name: 'Add Contact', action: async (modal) => { const name = formData.name.trim(); const number = formData.number.trim(); if (!name || !number) { deesCatalog.DeesToast.error('Name and number are required', 3000); return; } const newContact: IContact = { id: `contact-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, name, number, company: formData.company.trim() || undefined, notes: formData.notes.trim() || undefined, }; await this.saveContacts([...this.appData.contacts, newContact]); modal.destroy(); deesCatalog.DeesToast.info('Contact added'); }, }, ], }); } private async openEditModal(contact: IContact) { const { DeesModal } = await import('@design.estate/dees-catalog'); const formData = { name: contact.name, number: contact.number, company: contact.company || '', notes: contact.notes || '', }; await DeesModal.createAndShow({ heading: 'Edit Contact', content: html`
{ formData.name = (e.target as any).value; }} > { formData.number = (e.target as any).value; }} > { formData.company = (e.target as any).value; }} > { formData.notes = (e.target as any).value; }} >
`, menuOptions: [ { name: 'Cancel', action: async (modal) => { modal.destroy(); }, }, { name: 'Save Changes', action: async (modal) => { const name = formData.name.trim(); const number = formData.number.trim(); if (!name || !number) { deesCatalog.DeesToast.error('Name and number are required', 3000); return; } const updatedContacts = this.appData.contacts.map((c) => c.id === contact.id ? { ...c, name, number, company: formData.company.trim() || undefined, notes: formData.notes.trim() || undefined } : c, ); await this.saveContacts(updatedContacts); modal.destroy(); deesCatalog.DeesToast.info('Contact updated'); }, }, ], }); } private async deleteContact(contact: IContact) { const { DeesModal } = await import('@design.estate/dees-catalog'); await DeesModal.createAndShow({ heading: 'Delete Contact', content: html`

Are you sure you want to delete ${contact.name}?

This action cannot be undone.

`, menuOptions: [ { name: 'Cancel', action: async (modal) => { modal.destroy(); }, }, { name: 'Delete', action: async (modal) => { const updatedContacts = this.appData.contacts.filter((c) => c.id !== contact.id); await this.saveContacts(updatedContacts); modal.destroy(); deesCatalog.DeesToast.info('Contact deleted'); }, }, ], }); } private async toggleStar(contact: IContact) { const updatedContacts = this.appData.contacts.map((c) => c.id === contact.id ? { ...c, starred: !c.starred } : c, ); await this.saveContacts(updatedContacts); } private async saveContacts(contacts: IContact[]) { this.saving = true; try { const config = await appState.apiGetConfig(); config.contacts = contacts; await appState.apiSaveConfig(config); } catch (e: any) { deesCatalog.DeesToast.error(`Save failed: ${e.message}`, 4000); } finally { this.saving = false; } } private getColumns() { return [ { key: 'starred', header: '', sortable: false, renderer: (val: boolean | undefined, row: IContact) => { const starred = val === true; return html` { e.stopPropagation(); this.toggleStar(row); }} >${starred ? '\u2605' : '\u2606'} `; }, }, { key: 'name', header: 'Name', sortable: true, }, { key: 'number', header: 'Number', renderer: (val: string) => html`${val}`, }, { key: 'company', header: 'Company', renderer: (val: string | undefined) => val || '-', }, { key: 'notes', header: 'Notes', renderer: (val: string | undefined) => html`${val || '-'}`, }, ]; } private getDataActions() { return [ { name: 'Call', iconName: 'lucide:phone' as any, type: ['inRow'] as any, actionFunc: async ({ item }: { item: IContact }) => { appState.selectContact(item); appRouter.navigateTo('phone' as any); }, }, { name: 'Star', iconName: 'lucide:star' as any, type: ['inRow'] as any, actionFunc: async ({ item }: { item: IContact }) => { await this.toggleStar(item); }, }, { name: 'Edit', iconName: 'lucide:pencil' as any, type: ['inRow'] as any, actionFunc: async ({ item }: { item: IContact }) => { await this.openEditModal(item); }, }, { name: 'Delete', iconName: 'lucide:trash2' as any, type: ['inRow'] as any, actionFunc: async ({ item }: { item: IContact }) => { await this.deleteContact(item); }, }, { name: 'Add Contact', iconName: 'lucide:plus' as any, type: ['header'] as any, actionFunc: async () => { await this.openAddModal(); }, }, ]; } // ---------- Render ---------- public render(): TemplateResult { const contacts = this.appData.contacts || []; const companies = new Set( contacts.map((c) => c.company?.trim()).filter((c) => c && c.length > 0), ); const tiles: IStatsTile[] = [ { id: 'total', title: 'Total Contacts', value: contacts.length, type: 'number', icon: 'lucide:contactRound', description: contacts.length === 1 ? '1 contact' : `${contacts.length} contacts`, }, { id: 'starred', title: 'Starred', value: contacts.filter((c) => c.starred).length, type: 'number', icon: 'lucide:star', color: 'hsl(45 93% 47%)', description: 'Quick-dial contacts', }, { id: 'companies', title: 'Companies', value: companies.size, type: 'number', icon: 'lucide:building2', description: `${companies.size} unique`, }, ]; return html`
`; } private sortedContacts(contacts: IContact[]): IContact[] { return [...contacts].sort((a, b) => { if (a.starred && !b.starred) return -1; if (!a.starred && b.starred) return 1; return a.name.localeCompare(b.name); }); } }