'use client'; import * as React from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Plus, KeyRound, Trash2, Pencil, Copy, Eye, EyeOff } from 'lucide-react'; import { PageHeader } from '@/components/layout/page-header'; import { RequirePermission } from '@/components/require-permission'; import { DataTable, type DataTableColumn } from '@/components/data-table/data-table'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog'; import { useToast } from '@/components/ui/toast'; import { useAuth } from '@/hooks/use-auth'; import { useDebounce } from '@/hooks/use-debounce'; import { extensionsService, type ExtensionInput } from '@/services/extensions'; import type { Extension } from '@/types'; import { errorMessage } from '@/lib/error-message'; import { formatDateTime } from '@/lib/utils'; function ExtensionFormDialog({ open, onOpenChange, extension, }: { open: boolean; onOpenChange: (v: boolean) => void; extension: Extension | null; }) { const isEdit = Boolean(extension); const queryClient = useQueryClient(); const { toast } = useToast(); const [form, setForm] = React.useState({ number: '', name: '', callerId: '', enabled: true, }); const [createdPassword, setCreatedPassword] = React.useState(null); const [showPassword, setShowPassword] = React.useState(false); // Compara com o valor buscado do servidor para só mandar sipPassword no // update quando o operador realmente mudou o campo (evita re-provisionar // o PJSIP e gerar log de "senha alterada" à toa a cada salvar). const originalPasswordRef = React.useRef(undefined); const passwordQuery = useQuery({ queryKey: ['extension-password', extension?.id], queryFn: () => extensionsService.getPassword(extension!.id), enabled: open && isEdit && Boolean(extension), }); React.useEffect(() => { if (open) { setCreatedPassword(null); setShowPassword(false); originalPasswordRef.current = undefined; setForm( extension ? { name: extension.name, callerId: extension.callerId ?? '', enabled: extension.enabled, } : { number: '', name: '', callerId: '', enabled: true }, ); } }, [open, extension]); React.useEffect(() => { if (passwordQuery.data) { originalPasswordRef.current = passwordQuery.data.sipPassword; setForm((f) => ({ ...f, sipPassword: passwordQuery.data.sipPassword })); } }, [passwordQuery.data]); const mutation = useMutation({ mutationFn: () => { if (isEdit && extension) { const changedPassword = form.sipPassword && form.sipPassword !== originalPasswordRef.current ? form.sipPassword : undefined; return extensionsService.update(extension.id, { ...form, sipPassword: changedPassword }); } return extensionsService.create(form); }, onSuccess: (result) => { queryClient.invalidateQueries({ queryKey: ['extensions'] }); queryClient.invalidateQueries({ queryKey: ['extension-password', extension?.id] }); if (!isEdit && 'sipPassword' in result && result.sipPassword) { setCreatedPassword(result.sipPassword); toast({ title: 'Ramal criado', variant: 'success' }); } else { toast({ title: 'Ramal salvo', variant: 'success' }); onOpenChange(false); } }, onError: (err) => toast({ title: 'Erro', description: errorMessage(err), variant: 'destructive' }), }); return ( {isEdit ? 'Editar ramal' : 'Novo ramal'} {createdPassword ? (

Senha SIP gerada (exibida apenas uma vez, copie agora):

{createdPassword}
) : (
{ e.preventDefault(); mutation.mutate(); }} className="flex flex-col gap-4" > {!isEdit && (
setForm((f) => ({ ...f, number: e.target.value }))} />
)}
setForm((f) => ({ ...f, name: e.target.value }))} />
setForm((f) => ({ ...f, callerId: e.target.value }))} />
{isEdit && (
setForm((f) => ({ ...f, sipPassword: e.target.value }))} />

Use isso para configurar o softphone. Editar aqui troca a senha de verdade (reaplica no Asterisk ao salvar).

)}
)}
); } function ResetPasswordDialog({ result, onOpenChange, }: { result: { number: string; password: string } | null; onOpenChange: (v: boolean) => void; }) { return ( Senha SIP redefinida — ramal {result?.number}

Exibida apenas uma vez, copie agora:

{result?.password}
); } function ExtensionsContent() { const { can } = useAuth(); const { toast } = useToast(); const queryClient = useQueryClient(); const [search, setSearch] = React.useState(''); const debouncedSearch = useDebounce(search); const [dialogOpen, setDialogOpen] = React.useState(false); const [editing, setEditing] = React.useState(null); const [resetResult, setResetResult] = React.useState<{ number: string; password: string } | null>( null, ); const { data, isLoading, isError, refetch } = useQuery({ queryKey: ['extensions'], queryFn: extensionsService.list, }); const resetPassword = useMutation({ mutationFn: (id: string) => extensionsService.resetPassword(id), onSuccess: (result) => { queryClient.invalidateQueries({ queryKey: ['extensions'] }); setResetResult({ number: result.number, password: result.sipPassword }); }, onError: (err) => toast({ title: 'Erro', description: errorMessage(err), variant: 'destructive' }), }); const remove = useMutation({ mutationFn: (id: string) => extensionsService.remove(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['extensions'] }); toast({ title: 'Ramal removido', variant: 'success' }); }, onError: (err) => toast({ title: 'Erro', description: errorMessage(err), variant: 'destructive' }), }); const filtered = (data ?? []).filter( (e) => e.number.includes(debouncedSearch) || e.name.toLowerCase().includes(debouncedSearch.toLowerCase()), ); const columns: DataTableColumn[] = [ { key: 'number', header: 'Ramal', render: (r) => r.number }, { key: 'name', header: 'Nome', render: (r) => r.name }, { key: 'context', header: 'Contexto', render: (r) => r.context }, { key: 'enabled', header: 'Status', render: (r) => ( {r.enabled ? 'Ativo' : 'Inativo'} ), }, { key: 'updatedAt', header: 'Atualizado em', render: (r) => formatDateTime(r.updatedAt) }, { key: 'actions', header: '', className: 'text-right', render: (r) => (
{can('extensions.update') && ( <> )} {can('extensions.delete') && ( )}
), }, ]; return ( <> { setEditing(null); setDialogOpen(true); }} > Novo ramal ) } /> refetch()} rowKey={(r) => r.id} searchValue={search} onSearchChange={setSearch} searchPlaceholder="Buscar por número ou nome..." emptyMessage="Nenhum ramal cadastrado." /> !v && setResetResult(null)} /> ); } export default function ExtensionsPage() { return ( ); }