import { DeesElement, customElement, html, css, cssManager, state, type TemplateResult } from '../plugins.js'; import { deesCatalog } from '../plugins.js'; import { appState, type IAppState } from '../state/appstate.js'; import { viewHostCss } from './shared/index.js'; import type { IStatsTile } from '@design.estate/dees-catalog'; const { DeesModal, DeesToast } = deesCatalog; // --------------------------------------------------------------------------- // IVR types (mirrors ts/config.ts) // --------------------------------------------------------------------------- type TIvrAction = | { type: 'route-extension'; extensionId: string } | { type: 'route-voicemail'; boxId: string } | { type: 'submenu'; menuId: string } | { type: 'play-message'; promptId: string } | { type: 'transfer'; number: string; providerId?: string } | { type: 'repeat' } | { type: 'hangup' }; interface IIvrMenuEntry { digit: string; action: TIvrAction; } interface IIvrMenu { id: string; name: string; promptText: string; promptVoice?: string; entries: IIvrMenuEntry[]; timeoutSec?: number; maxRetries?: number; timeoutAction: TIvrAction; invalidAction: TIvrAction; } interface IIvrConfig { enabled: boolean; menus: IIvrMenu[]; entryMenuId: string; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function slugify(name: string): string { return name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') || `menu-${Date.now()}`; } const VOICE_OPTIONS = [ { option: 'af_bella (Female)', key: 'af_bella' }, { option: 'af_sarah (Female)', key: 'af_sarah' }, { option: 'am_adam (Male)', key: 'am_adam' }, { option: 'bf_alice (Female)', key: 'bf_alice' }, ]; const ACTION_TYPE_OPTIONS = [ { option: 'Route to Extension', key: 'route-extension' }, { option: 'Route to Voicemail', key: 'route-voicemail' }, { option: 'Submenu', key: 'submenu' }, { option: 'Play Message', key: 'play-message' }, { option: 'Transfer', key: 'transfer' }, { option: 'Repeat', key: 'repeat' }, { option: 'Hangup', key: 'hangup' }, ]; const DIGIT_OPTIONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '*', '#']; function describeAction(action: TIvrAction): string { switch (action.type) { case 'route-extension': return `Extension: ${action.extensionId}`; case 'route-voicemail': return `Voicemail: ${action.boxId}`; case 'submenu': return `Submenu: ${action.menuId}`; case 'play-message': return `Play: ${action.promptId}`; case 'transfer': return `Transfer: ${action.number}${action.providerId ? ` (${action.providerId})` : ''}`; case 'repeat': return 'Repeat'; case 'hangup': return 'Hangup'; default: return 'Unknown'; } } function makeDefaultAction(): TIvrAction { return { type: 'hangup' }; } // --------------------------------------------------------------------------- // View element // --------------------------------------------------------------------------- @customElement('sipproxy-view-ivr') export class SipproxyViewIvr extends DeesElement { @state() accessor appData: IAppState = appState.getState(); @state() accessor config: any = null; public static styles = [ cssManager.defaultStyles, viewHostCss, css` .view-section { margin-bottom: 24px; } `, ]; // ---- lifecycle ----------------------------------------------------------- connectedCallback() { super.connectedCallback(); this.rxSubscriptions.push({ unsubscribe: appState.subscribe((s) => { this.appData = s; }), } as any); this.loadConfig(); } private async loadConfig() { try { this.config = await appState.apiGetConfig(); } catch { // Will show empty state. } } private getIvrConfig(): IIvrConfig { return this.config?.ivr || { enabled: false, menus: [], entryMenuId: '' }; } // ---- stats tiles --------------------------------------------------------- private getStatsTiles(): IStatsTile[] { const ivr = this.getIvrConfig(); const entryMenu = ivr.menus.find((m) => m.id === ivr.entryMenuId); return [ { id: 'total-menus', title: 'Total Menus', value: ivr.menus.length, type: 'number', icon: 'lucide:list-tree', description: 'IVR menu definitions', }, { id: 'entry-menu', title: 'Entry Menu', value: entryMenu?.name || '(none)', type: 'text' as any, icon: 'lucide:door-open', description: entryMenu ? `ID: ${entryMenu.id}` : 'No entry menu set', }, { id: 'status', title: 'Status', value: ivr.enabled ? 'Enabled' : 'Disabled', type: 'text' as any, icon: ivr.enabled ? 'lucide:check-circle' : 'lucide:x-circle', color: ivr.enabled ? 'hsl(142.1 76.2% 36.3%)' : 'hsl(0 84.2% 60.2%)', description: ivr.enabled ? 'IVR is active' : 'IVR is inactive', }, ]; } // ---- table columns ------------------------------------------------------- private getColumns() { const ivr = this.getIvrConfig(); return [ { key: 'name', header: 'Name', sortable: true, renderer: (val: string, row: IIvrMenu) => { const isEntry = row.id === ivr.entryMenuId; return html` ${val} ${isEntry ? html`entry` : ''} `; }, }, { key: 'promptText', header: 'Prompt', renderer: (val: string) => { const truncated = val && val.length > 60 ? val.slice(0, 60) + '...' : val || '--'; return html`${truncated}`; }, }, { key: 'entries', header: 'Digits', renderer: (_val: any, row: IIvrMenu) => { const digits = (row.entries || []).map((e) => e.digit).join(', '); return html`${digits || '(none)'}`; }, }, { key: 'timeoutAction', header: 'Timeout Action', renderer: (_val: any, row: IIvrMenu) => { return html`${describeAction(row.timeoutAction)}`; }, }, ]; } // ---- table actions ------------------------------------------------------- private getDataActions() { return [ { name: 'Add Menu', iconName: 'lucide:plus' as any, type: ['header'] as any, actionFunc: async () => { await this.openMenuEditor(null); }, }, { name: 'Edit', iconName: 'lucide:pencil' as any, type: ['inRow'] as any, actionFunc: async ({ item }: { item: IIvrMenu }) => { await this.openMenuEditor(item); }, }, { name: 'Set as Entry', iconName: 'lucide:door-open' as any, type: ['inRow'] as any, actionFunc: async ({ item }: { item: IIvrMenu }) => { await this.setEntryMenu(item.id); }, }, { name: 'Delete', iconName: 'lucide:trash-2' as any, type: ['inRow'] as any, actionFunc: async ({ item }: { item: IIvrMenu }) => { await this.confirmDeleteMenu(item); }, }, ]; } // ---- toggle enabled ------------------------------------------------------ private async toggleEnabled() { const ivr = this.getIvrConfig(); const updated: IIvrConfig = { ...ivr, enabled: !ivr.enabled }; const result = await appState.apiSaveConfig({ ivr: updated }); if (result.ok) { DeesToast.success(updated.enabled ? 'IVR enabled' : 'IVR disabled'); await this.loadConfig(); } else { DeesToast.error('Failed to update IVR status'); } } // ---- set entry menu ------------------------------------------------------ private async setEntryMenu(menuId: string) { const ivr = this.getIvrConfig(); const updated: IIvrConfig = { ...ivr, entryMenuId: menuId }; const result = await appState.apiSaveConfig({ ivr: updated }); if (result.ok) { DeesToast.success('Entry menu updated'); await this.loadConfig(); } else { DeesToast.error('Failed to set entry menu'); } } // ---- delete menu --------------------------------------------------------- private async confirmDeleteMenu(menu: IIvrMenu) { await DeesModal.createAndShow({ heading: 'Delete IVR Menu', width: 'small', showCloseButton: true, content: html`