import React, { useState, useEffect, useRef } from 'react'; import { X, Search, Plus, Trash2, GripVertical, Loader2, ChevronLeft, ChevronRight, Save, Printer, FileText, Users, Minus } from 'lucide-react'; import { DragDropContext, Droppable, Draggable, DropResult } from '@hello-pangea/dnd'; import { HybridBackend } from '../services/backend.ts'; import { Orcamento, OrcamentoItem, Dentista, Especialidade, Paciente, Procedimento } from '../types.ts'; import { useHybridBackend } from '../hooks/useHybridBackend.ts'; import { useToast } from '../contexts/ToastContext.tsx'; // ----------- Local AutocompleteField ----------- function useDebounce(value: string, delay: number) { const [dv, setDv] = useState(value); useEffect(() => { const t = setTimeout(() => setDv(value), delay); return () => clearTimeout(t); }, [value, delay]); return dv; } function AutocompleteField({ label, placeholder, value, onSelect, onClear, fetchFn }: { label: string; placeholder: string; value?: { id: string; nome: string } | null; onSelect: (item: T) => void; onClear: () => void; fetchFn: (q: string) => Promise; }) { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const debounced = useDebounce(query, 300); const ref = useRef(null); useEffect(() => { if (!debounced.trim() || value) { setResults([]); setOpen(false); return; } setLoading(true); fetchFn(debounced).then(r => { const arr = Array.isArray(r) ? r : []; setResults(arr); setOpen(arr.length > 0); }).finally(() => setLoading(false)); }, [debounced]); useEffect(() => { const close = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', close); return () => document.removeEventListener('mousedown', close); }, []); if (value) return (
{value.nome}
); return (
setQuery(e.target.value)} placeholder={placeholder} className="w-full pl-7 pr-3 py-2 border border-gray-200 rounded-lg text-xs focus:ring-2 focus:ring-teal-100 focus:border-teal-300 outline-none" /> {loading && }
{open && results.length > 0 && (
{results.map(item => ( ))}
)}
); } // ----------- Default Clauses ----------- export const DEFAULT_CLAUSULAS = `CLÁUSULA 1ª – DO OBJETO O presente contrato tem por objeto a prestação de serviços odontológicos conforme os procedimentos descritos neste instrumento, obrigando-se o profissional a realizá-los com toda a técnica e diligência necessárias. CLÁUSULA 2ª – DAS OBRIGAÇÕES DO CONTRATANTE O(A) paciente obriga-se a: comparecer às consultas agendadas; comunicar cancelamentos com antecedência mínima de 24 horas; seguir as orientações clínicas do profissional; e efetuar os pagamentos nos prazos acordados. CLÁUSULA 3ª – DO PAGAMENTO O valor total do tratamento, a forma de pagamento e o parcelamento estão descritos neste contrato. Em caso de atraso no pagamento, incidirão multa de 2% (dois por cento) e juros de 1% (um por cento) ao mês sobre o valor devido. CLÁUSULA 4ª – DA VIGÊNCIA O presente contrato vigorará pelo período necessário à conclusão do tratamento acordado, podendo ser prorrogado mediante aditivo escrito firmado por ambas as partes. CLÁUSULA 5ª – DO CANCELAMENTO E DESISTÊNCIA Em caso de desistência do tratamento pelo contratante, serão devidos os valores correspondentes aos procedimentos já realizados. Os valores pagos a maior serão restituídos no prazo de 30 dias. CLÁUSULA 6ª – DA PROTEÇÃO DE DADOS (LGPD) Os dados pessoais e de saúde do contratante serão tratados em conformidade com a Lei nº 13.709/2018 (LGPD), utilizados exclusivamente para fins relacionados ao tratamento odontológico. CLÁUSULA 7ª – DO FORO Fica eleito o foro da comarca do domicílio do contratado para dirimir quaisquer controvérsias oriundas do presente contrato, com renúncia expressa de qualquer outro.`; // ----------- Types ----------- interface LancItem { id: string; procedimentoNome: string; quantidade: number; valorUnitario: number; valorTotal: number; descricao?: string; ordem: number; } interface FormState { titulo: string; status: Orcamento['status']; pacienteId: string; pacienteNome: string; dentistaId: string; dentistaNome: string; especialidadeId: string; especialidadeNome: string; dataInicio: string; dataFim: string; observacoes: string; entrada: number; parcelas: number; formaPagamento: string; clausulas: string; } const STEPS = ['DADOS', 'ITENS', 'CLÁUSULAS']; const fmt = (v: number) => v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }); // ----------- Print View ----------- const PrintView: React.FC<{ form: FormState; items: LancItem[]; valorTotal: number; orcamentoId?: string; onBack: () => void; }> = ({ form, items, valorTotal, orcamentoId, onBack }) => { const hoje = new Date().toLocaleDateString('pt-BR', { day: '2-digit', month: 'long', year: 'numeric' }); const restante = valorTotal - form.entrada; const valorParcela = form.parcelas > 0 && restante > 0 ? restante / form.parcelas : 0; return (
{/* Print toolbar - hidden when printing */}
{/* Printable content */}
{/* Header */}

CONTRATO DE PRESTAÇÃO DE SERVIÇOS ODONTOLÓGICOS

Nome da Clínica

{orcamentoId &&

Orcamento Nº {orcamentoId}

}
{/* Parties */}

CONTRATANTE (PACIENTE)

{form.pacienteNome || '—'}

CPF: ___________________________________

RG: ____________________________________

Endereço: ______________________________

CONTRATADO (DENTISTA)

{form.dentistaNome || '—'}

CRO: ___________________________________

Especialidade: {form.especialidadeNome || '—'}

Data de início: {form.dataInicio ? new Date(form.dataInicio + 'T12:00').toLocaleDateString('pt-BR') : '—'}

{/* Object */}

OBJETO DO CONTRATO — {form.titulo}

{items.length > 0 ? ( {items.map((item, idx) => ( ))}
Procedimento Qtd Unit. Total
{item.procedimentoNome} {item.quantidade} {fmt(item.valorUnitario)} {fmt(item.valorTotal)}
VALOR TOTAL {fmt(valorTotal)}
) : (

Nenhum procedimento listado.

)}
{/* Financial */}

RESUMO FINANCEIRO

Valor Total

{fmt(valorTotal)}

Forma de Pagamento

{form.formaPagamento || '—'}

Entrada

{fmt(form.entrada)}

Restante

{fmt(restante)}

Parcelas

{form.parcelas}x

Valor por Parcela

{fmt(valorParcela)}

{/* Clauses */} {form.clausulas && (

CLÁUSULAS CONTRATUAIS

{form.clausulas}
)} {/* Signature section */}

Estando de pleno acordo com as cláusulas e condições acima, as partes assinam o presente contrato.

___________________, {hoje}.

{[ { title: 'CONTRATANTE (PACIENTE)', name: form.pacienteNome }, { title: 'CONTRATADO (DENTISTA)', name: form.dentistaNome }, { title: 'TESTEMUNHA', name: '' }, ].map((sig, idx) => (

{sig.title}

{sig.name &&

{sig.name}

}

CPF: __________________________

))}
{/* Footer */} {orcamentoId && (

ID: {orcamentoId}

Gerado em {hoje}

)}
); }; // ----------- Main Modal ----------- interface OrcamentoModalProps { contrato?: Orcamento | null; paciente?: Paciente | null; initialView?: 'editor' | 'print'; onClose: () => void; onSaved: () => void; } export const OrcamentoModal: React.FC = ({ contrato, paciente, initialView = 'editor', onClose, onSaved }) => { const toast = useToast(); const { data: dentistas } = useHybridBackend(HybridBackend.getDentistas); const { data: especialidades } = useHybridBackend(HybridBackend.getEspecialidades); const { data: procedimentos } = useHybridBackend(HybridBackend.getProcedimentos); const [page, setPage] = useState(0); const [view, setView] = useState<'editor' | 'print'>(initialView); const [saving, setSaving] = useState(false); // Form state const [form, setForm] = useState({ titulo: contrato?.titulo || 'CONTRATO DE SERVIÇOS ODONTOLÓGICOS', status: contrato?.status || 'Rascunho', pacienteId: contrato?.pacienteId || paciente?.id || '', pacienteNome: contrato?.pacienteNome || paciente?.nome || '', dentistaId: contrato?.dentistaId || '', dentistaNome: contrato?.dentistaNome || '', especialidadeId: contrato?.especialidadeId || '', especialidadeNome: contrato?.especialidadeNome || '', dataInicio: contrato?.dataInicio || new Date().toISOString().split('T')[0], dataFim: contrato?.dataFim || '', observacoes: contrato?.observacoes || '', entrada: contrato?.entrada || 0, parcelas: contrato?.parcelas || 1, formaPagamento: contrato?.formaPagamento || 'Pix', clausulas: contrato?.clausulas || DEFAULT_CLAUSULAS, }); // Items state const [items, setItems] = useState( contrato?.items?.map((i, idx) => ({ id: i.id, procedimentoNome: i.procedimentoNome, quantidade: i.quantidade, valorUnitario: i.valorUnitario, valorTotal: i.valorTotal, descricao: i.descricao, ordem: i.ordem ?? idx, })) ?? [] ); // Patient selection (when no pre-filled patient) const [selectedPaciente, setSelectedPaciente] = useState<{ id: string; nome: string } | null>( paciente ? { id: paciente.id, nome: paciente.nome } : (contrato?.pacienteId ? { id: contrato.pacienteId, nome: contrato.pacienteNome } : null) ); // Procedure catalog search const [procSearch, setProcSearch] = useState(''); const valorTotal = items.length > 0 ? items.reduce((s, i) => s + i.valorTotal, 0) : 0; const restante = valorTotal - form.entrada; const valorParcela = form.parcelas > 0 && restante > 0 ? restante / form.parcelas : 0; useEffect(() => { const h = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', h); return () => document.removeEventListener('keydown', h); }, [onClose]); // Auto-fill title when specialty changes const handleEspecialidadeChange = (id: string) => { const esp = especialidades?.find(e => e.id === id); setForm(f => ({ ...f, especialidadeId: id, especialidadeNome: esp?.nome || '', titulo: esp ? `CONTRATO DE SERVIÇOS ODONTOLÓGICOS — ${esp.nome.toUpperCase()}` : f.titulo, })); }; // Add procedure from catalog const addProcedimento = (proc: Procedimento) => { const existing = items.find(i => i.procedimentoNome === proc.nome); if (existing) { setItems(prev => prev.map(i => i.id === existing.id ? { ...i, quantidade: i.quantidade + 1, valorTotal: (i.quantidade + 1) * i.valorUnitario } : i )); return; } const newItem: LancItem = { id: Math.random().toString(36).substring(2, 10), procedimentoNome: proc.nome, quantidade: 1, valorUnitario: proc.valorParticular, valorTotal: proc.valorParticular, ordem: items.length, }; setItems(prev => [...prev, newItem]); }; // Add manual item const addManualItem = () => { const newItem: LancItem = { id: Math.random().toString(36).substring(2, 10), procedimentoNome: '', quantidade: 1, valorUnitario: 0, valorTotal: 0, ordem: items.length, }; setItems(prev => [...prev, newItem]); }; // Update item field const updateItem = (id: string, field: keyof LancItem, value: any) => { setItems(prev => prev.map(i => { if (i.id !== id) return i; const updated = { ...i, [field]: value }; if (field === 'quantidade' || field === 'valorUnitario') { updated.valorTotal = updated.quantidade * updated.valorUnitario; } return updated; })); }; const removeItem = (id: string) => setItems(prev => prev.filter(i => i.id !== id)); const onDragEnd = (result: DropResult) => { if (!result.destination) return; const reordered = [...items]; const [removed] = reordered.splice(result.source.index, 1); reordered.splice(result.destination.index, 0, removed); setItems(reordered.map((i, idx) => ({ ...i, ordem: idx }))); }; const handleSave = async () => { if (!form.pacienteNome && !selectedPaciente) { toast.error('SELECIONE UM PACIENTE.'); return; } setSaving(true); try { const payload = { id: contrato?.id, titulo: form.titulo, status: form.status, paciente_id: selectedPaciente?.id || form.pacienteId || undefined, paciente_nome: selectedPaciente?.nome || form.pacienteNome, dentista_id: form.dentistaId || undefined, dentista_nome: form.dentistaNome || undefined, especialidade_id: form.especialidadeId || undefined, especialidade_nome: form.especialidadeNome || undefined, data_inicio: form.dataInicio || undefined, data_fim: form.dataFim || undefined, valor_total: valorTotal, entrada: form.entrada, parcelas: form.parcelas, forma_pagamento: form.formaPagamento, clausulas: form.clausulas, observacoes: form.observacoes || undefined, items: items.map((i, idx) => ({ id: i.id, procedimento_nome: i.procedimentoNome, quantidade: i.quantidade, valor_unitario: i.valorUnitario, valor_total: i.valorTotal, descricao: i.descricao || undefined, ordem: idx, })), }; let res: any = null; if (contrato?.id) { res = await HybridBackend.updateOrcamento({ ...contrato, ...payload as any, items: contrato.items }); } else { await HybridBackend.saveOrcamento(payload as any); } if (res?.lancamentos_gerados > 0) { toast.success(`ORÇAMENTO ASSINADO — ${res.lancamentos_gerados} PARCELA(S) LANÇADA(S) NO FINANCEIRO!`); } else { toast.success('ORÇAMENTO SALVO COM SUCESSO!'); } onSaved(); onClose(); } catch { toast.error('ERRO AO SALVAR ORÇAMENTO.'); } finally { setSaving(false); } }; // Grouped procedures for catalog const groupedProcs = React.useMemo(() => { const all = procedimentos ?? []; const filtered = procSearch.trim() ? all.filter(p => p.nome.toLowerCase().includes(procSearch.toLowerCase()) || p.especialidadeNome.toLowerCase().includes(procSearch.toLowerCase())) : all; const groups: Record = {}; filtered.forEach(p => { const key = p.especialidadeNome || 'Outros'; if (!groups[key]) groups[key] = []; groups[key].push(p); }); return groups; }, [procedimentos, procSearch]); if (view === 'print') { return (
Visualização de Impressão
setView('editor')} />
); } return (
{/* Header */}

{contrato ? 'Editar Orcamento' : 'Novo Orcamento'}

{form.pacienteNome || 'Paciente não selecionado'}

{/* Step indicator */}
{STEPS.map((s, i) => ( {i < STEPS.length - 1 && (
)} ))}
{/* Content */}
{/* ===== TAB 0: DADOS ===== */} {page === 0 && (
{/* Patient */} {paciente ? (
{paciente.nome} (PRÉ-SELECIONADO)
) : ( label="Paciente" placeholder="Buscar paciente..." value={selectedPaciente} onSelect={p => { setSelectedPaciente({ id: p.id, nome: p.nome }); setForm(f => ({ ...f, pacienteId: p.id, pacienteNome: p.nome })); }} onClear={() => { setSelectedPaciente(null); setForm(f => ({ ...f, pacienteId: '', pacienteNome: '' })); }} fetchFn={q => HybridBackend.getPacientes(q).then(r => r.data)} /> )}
{/* Dentista */}
{/* Especialidade */}
{/* Título */}
setForm(f => ({ ...f, titulo: e.target.value.toUpperCase() }))} className="w-full border border-gray-200 rounded-lg p-2.5 text-xs focus:ring-2 focus:ring-emerald-100 focus:border-emerald-300 outline-none" placeholder="CONTRATO DE SERVIÇOS ODONTOLÓGICOS" />
{/* Status */}
{/* Data Início */}
setForm(f => ({ ...f, dataInicio: e.target.value }))} className="w-full border border-gray-200 rounded-lg p-2.5 text-xs focus:ring-2 focus:ring-emerald-100 focus:border-emerald-300 outline-none" />
{/* Data Fim */}
setForm(f => ({ ...f, dataFim: e.target.value }))} className="w-full border border-gray-200 rounded-lg p-2.5 text-xs focus:ring-2 focus:ring-emerald-100 focus:border-emerald-300 outline-none" />
{/* Observações */}