"use client"; import { useEffect, useState, useCallback, useMemo } from "react"; import { useAuth } from "@/contexts/AuthContext"; import { useApi } from "@/hooks/use-api"; import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Skeleton } from "@/components/ui/skeleton"; import { Badge } from "@/components/ui/badge"; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"; import { RefreshCw, Inbox, Folder, Download, Play, Pause, Loader2, PhoneIncoming, PhoneCall, Trash2 } from "lucide-react"; import { format } from "date-fns"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; const FOLDERS = ['INBOX', 'Family', 'Friends', 'Old', 'Work', 'Urgent']; const FRIENDLY_FOLDER = (f) => (f === 'INBOX' ? 'New' : f); function formatDuration(sec) { if (isNaN(sec)) return '0:00'; const m = Math.floor(sec / 60); const s = Math.floor(sec % 60).toString().padStart(2, '0'); return `${m}:${s}`; } function formatBytes(bytes) { if (!bytes && bytes !== 0) return '—'; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / 1024 / 1024).toFixed(2)} MB`; } function formatVoicemailDate(dateStr) { try { return format(new Date(dateStr), "MMM d, yyyy 'at' h:mm a"); } catch { return dateStr; } } export function VoicemailPanel({ extensionId: extProp }) { const { token } = useAuth(); const apiFetch = useApi(); const [extensionId, setExtensionId] = useState(extProp || null); const [loadingExt, setLoadingExt] = useState(!extProp); const [loading, setLoading] = useState(true); const [voicemails, setVoicemails] = useState([]); const [activeFolder, setActiveFolder] = useState('INBOX'); const [search, setSearch] = useState(''); const [expandedId, setExpandedId] = useState(null); const [audioMap, setAudioMap] = useState({}); // messageId -> { url, loading, error } const [movingId, setMovingId] = useState(null); const [refreshing, setRefreshing] = useState(false); const [vmToDelete, setVmToDelete] = useState(null); useEffect(() => { if (extProp || !token) return; const load = async () => { setLoadingExt(true); try { const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/me`); if (res.ok) { const data = await res.json(); setExtensionId(data.extensionId); } } catch (e) { if (e.message !== 'Unauthorized') { // silent } } finally { setLoadingExt(false); } }; load(); }, [token, extProp, apiFetch]); const fetchVoicemails = useCallback(async () => { if (!token || !extensionId) return; setRefreshing(true); if (voicemails.length === 0) setLoading(true); try { const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/${extensionId}/voicemails`); if (res.ok) { const data = await res.json(); setVoicemails(Array.isArray(data) ? data : []); } else { setVoicemails([]); } } catch (e) { if (e.message !== 'Unauthorized') { setVoicemails([]); } } finally { setLoading(false); setRefreshing(false); } }, [token, extensionId, voicemails.length, apiFetch]); useEffect(() => { if (extensionId) fetchVoicemails(); }, [extensionId, fetchVoicemails]); const filtered = useMemo(() => { return voicemails .filter(v => v.folder === activeFolder) .filter(v => { if (!search) return true; const t = search.toLowerCase(); return ( v.callerIdName?.toLowerCase().includes(t) || v.callerIdNum?.toLowerCase().includes(t) || v.messageId?.toLowerCase().includes(t) ); }) .sort((a, b) => new Date(b.date) - new Date(a.date)); }, [voicemails, activeFolder, search]); const toggleExpand = async (vm) => { const isOpening = expandedId !== vm.messageId; setExpandedId(isOpening ? vm.messageId : null); if (isOpening && vm.hasAudio && !audioMap[vm.messageId]?.url) { setAudioMap(m => ({ ...m, [vm.messageId]: { url: null, loading: true } })); try { const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/${extensionId}/voicemails/${vm.messageId}/download`); if (!res.ok) throw new Error('Download failed'); const blob = await res.blob(); const url = URL.createObjectURL(blob); setAudioMap(m => ({ ...m, [vm.messageId]: { url, loading: false } })); } catch(e) { if (e.message !== 'Unauthorized') { setAudioMap(m => ({ ...m, [vm.messageId]: { url: null, loading: false, error: true } })); } } } }; const moveVoicemail = async (vm, target) => { if (target === vm.folder) return; setMovingId(vm.messageId); try { await apiFetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/${extensionId}/voicemails/${vm.messageId}/move`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ targetFolder: target }) }); setVoicemails(list => list.map(x => x.messageId === vm.messageId ? { ...x, folder: target } : x)); if (target !== activeFolder) { setExpandedId(null); } } catch(e) { if (e.message !== 'Unauthorized') { // silent fail } } finally { setMovingId(null); } }; const downloadVoicemail = async (vm) => { if (!vm.hasAudio) return; try { const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/${extensionId}/voicemails/${vm.messageId}/download`); if (!res.ok) return; const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${vm.messageId}.wav`; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } catch(e) { if (e.message !== 'Unauthorized') { // ignore } } }; const handleDelete = async () => { if (!vmToDelete || !token || !extensionId) return; try { const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/${extensionId}/voicemails/${vmToDelete.messageId}`, { method: 'DELETE', }); if (res.ok) { setVoicemails(list => list.filter(v => v.messageId !== vmToDelete.messageId)); if (expandedId === vmToDelete.messageId) setExpandedId(null); } } catch(e) { if (e.message !== 'Unauthorized') { // silent } } finally { setVmToDelete(null); } }; const loadingState = loading || loadingExt; return ( <>
Voicemail (VVM) {extensionId ? `Extension ${extensionId} visual voicemail` : 'Loading extension...'}
{FOLDERS.map(folder => { const count = voicemails.filter(v => v.folder === folder).length; const active = folder === activeFolder; return ( ); })}
setSearch(e.target.value)} className="bg-gray-900 border-gray-700" disabled={loadingState} />
{loadingState ? (
{[...Array(5)].map((_, i) => ( ))}
) : filtered.length === 0 ? (
No voicemails in {FRIENDLY_FOLDER(activeFolder)}{search && ' matching your search'}.
) : (
{filtered.map(vm => { const expanded = expandedId === vm.messageId; const audioInfo = audioMap[vm.messageId]; return (
{/* Actions */}
{movingId === vm.messageId && ( )}
{expanded && (
{/* Audio / status */} {vm.hasAudio ? ( audioInfo?.loading ? (
Loading audio...
) : audioInfo?.error ? (
Failed to load audio.
) : audioInfo?.url ? (
)}
); })}
)}
!open && setVmToDelete(null)}> Delete voicemail? This permanently deletes the voicemail from{' '} {vmToDelete?.callerIdName || 'Unknown Caller'} {' '} ({vmToDelete?.callerIdNum || 'No Number'}). This action cannot be undone. Cancel Delete ); } export default VoicemailPanel;