import React, { useState, useEffect } from 'react'; import { Plus, Search, FileText, Pencil, Printer, Trash2, Loader2, Zap, X, UserSearch } from 'lucide-react'; import { HybridBackend } from '../services/backend.ts'; import { Orcamento } from '../types.ts'; import { useHybridBackend } from '../hooks/useHybridBackend.ts'; import { useToast } from '../contexts/ToastContext.tsx'; import { useConfirm } from '../contexts/ConfirmContext.tsx'; import { PageHeader } from '../components/PageHeader.tsx'; import { OrcamentoModal, DEFAULT_CLAUSULAS } from './OrcamentoModal.tsx'; type StatusFilter = 'TODOS' | 'Ativo' | 'Rascunho' | 'Assinado' | 'Encerrado'; const STATUS_TABS: StatusFilter[] = ['TODOS', 'Ativo', 'Rascunho', 'Assinado', 'Encerrado']; const STATUS_COLORS: Record = { Ativo: 'bg-green-100 text-green-700', Rascunho: 'bg-amber-100 text-amber-700', Assinado: 'bg-blue-100 text-blue-700', Encerrado: 'bg-gray-100 text-gray-600', }; const fmt = (v: number) => v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }); export const OrcamentosView: React.FC = () => { const toast = useToast(); const confirm = useConfirm(); const { data: contratos, isLoading, refresh } = useHybridBackend(HybridBackend.getOrcamentos); const list = contratos ?? []; const [search, setSearch] = useState(''); const [statusFilter, setStatusFilter] = useState('TODOS'); const [modalOpen, setModalOpen] = useState(false); const [modalOrcamento, setModalOrcamento] = useState(null); const [modalView, setModalView] = useState<'editor' | 'print'>('editor'); // Gerar automático: escolher paciente → cria contrato com cláusulas padrão → abre a prévia. const [autoOpen, setAutoOpen] = useState(false); const [autoSearch, setAutoSearch] = useState(''); const [autoPacientes, setAutoPacientes] = useState<{ id: string; nome: string }[]>([]); const [autoLoading, setAutoLoading] = useState(false); const [autoGerando, setAutoGerando] = useState(false); useEffect(() => { if (!autoOpen) return; let cancel = false; setAutoLoading(true); const t = setTimeout(async () => { try { const r = await HybridBackend.getPacientes(autoSearch); if (!cancel) setAutoPacientes((r?.data ?? []).map((p: any) => ({ id: p.id, nome: p.nome }))); } catch { if (!cancel) setAutoPacientes([]); } finally { if (!cancel) setAutoLoading(false); } }, 300); return () => { cancel = true; clearTimeout(t); }; }, [autoOpen, autoSearch]); const filtered = list.filter(c => { const matchSearch = !search.trim() || c.pacienteNome.toLowerCase().includes(search.toLowerCase()) || c.titulo.toLowerCase().includes(search.toLowerCase()); const matchStatus = statusFilter === 'TODOS' || c.status === statusFilter; return matchSearch && matchStatus; }); const stats = { total: list.length, ativos: list.filter(c => c.status === 'Ativo').length, rascunhos: list.filter(c => c.status === 'Rascunho').length, encerrados: list.filter(c => c.status === 'Encerrado' || c.status === 'Assinado').length, }; const handleDelete = async (id: string, nome: string) => { if (!await confirm(`EXCLUIR o orçamento de ${nome}?`)) return; try { await HybridBackend.deleteOrcamento(id); toast.success('ORÇAMENTO EXCLUÍDO.'); refresh(); } catch { toast.error('ERRO AO EXCLUIR ORÇAMENTO.'); } }; const openNew = () => { setModalOrcamento(null); setModalView('editor'); setModalOpen(true); }; const openEdit = (c: Orcamento) => { setModalOrcamento(c); setModalView('editor'); setModalOpen(true); }; const openPrint = (c: Orcamento) => { setModalOrcamento(c); setModalView('print'); setModalOpen(true); }; // Gera um contrato automaticamente para o paciente (cláusulas padrão) e abre a prévia. const gerarAuto = async (p: { id: string; nome: string }) => { setAutoGerando(true); try { const hoje = new Date().toISOString().split('T')[0]; const res = await HybridBackend.saveOrcamento({ titulo: 'CONTRATO DE SERVIÇOS ODONTOLÓGICOS', status: 'Rascunho', paciente_id: p.id, paciente_nome: p.nome, data_inicio: hoje, clausulas: DEFAULT_CLAUSULAS, items: [], } as any); const novo = { id: (res as any)?.id, titulo: 'CONTRATO DE SERVIÇOS ODONTOLÓGICOS', status: 'Rascunho', pacienteId: p.id, pacienteNome: p.nome, dataInicio: hoje, clausulas: DEFAULT_CLAUSULAS, items: [], } as unknown as Orcamento; toast.success('ORÇAMENTO GERADO! REVISE E IMPRIMA.'); setAutoOpen(false); setAutoSearch(''); refresh(); setModalOrcamento(novo); setModalView('print'); setModalOpen(true); } catch { toast.error('ERRO AO GERAR ORÇAMENTO.'); } finally { setAutoGerando(false); } }; return (
{/* Stats bar */}
{[ { label: 'Total', value: stats.total, color: 'bg-emerald-50 text-emerald-700' }, { label: 'Ativos', value: stats.ativos, color: 'bg-green-50 text-green-700' }, { label: 'Rascunhos', value: stats.rascunhos, color: 'bg-amber-50 text-amber-700' }, { label: 'Encerrados/Assinados', value: stats.encerrados, color: 'bg-gray-50 text-gray-600' }, ].map(s => (

{s.value}

{s.label}

))}
{/* Search + status filters */}
setSearch(e.target.value)} />
{STATUS_TABS.map(s => ( ))}
{/* Loading */} {isLoading && (
)} {/* Empty state */} {!isLoading && filtered.length === 0 && (

Nenhum orçamento encontrado

{search || statusFilter !== 'TODOS' ? 'Tente limpar os filtros.' : 'Clique em "NOVO ORÇAMENTO" para começar.'}

{!search && statusFilter === 'TODOS' && ( )}
)} {/* Grid */} {!isLoading && filtered.length > 0 && (
{filtered.map(c => (
{/* Header row */}

{c.pacienteNome}

{c.status}

{c.titulo}

{c.especialidadeNome && ( {c.especialidadeNome} )} {c.dentistaNome && ( {c.dentistaNome} )}
Valor Total {fmt(c.valorTotal)}
Itens {c.items?.length ?? 0} proc.
{c.dataInicio && (
Início {new Date(c.dataInicio + 'T12:00').toLocaleDateString('pt-BR')}
)} {c.dataFim && (
Fim {new Date(c.dataFim + 'T12:00').toLocaleDateString('pt-BR')}
)}
{/* Actions */}
))}
)} {/* Modal */} {modalOpen && ( { setModalOpen(false); setModalOrcamento(null); setModalView('editor'); }} onSaved={refresh} /> )} {/* Gerar automático: picker de paciente */} {autoOpen && (
{ if (e.target === e.currentTarget && !autoGerando) setAutoOpen(false); }}>

Gerar automático

Escolha o paciente · cláusulas padrão

setAutoSearch(e.target.value)} placeholder="Buscar paciente por nome, CPF ou telefone…" className="w-full pl-9 pr-3 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm outline-none focus:border-teal-400 focus:bg-white" />
{autoLoading ? (
) : autoPacientes.length === 0 ? (

{autoSearch ? 'Nenhum paciente encontrado' : 'Digite para buscar um paciente'}

) : autoPacientes.map(p => ( ))}
)}
); };