3e64514b08
Financeiro V1 (Épicos 1–4): - FKs de origem no lançamento (paciente/profissional/procedimento/agendamento/contrato/realizado), soft delete e cron "Atrasado" - prontuário de procedimentos realizados (fotos antes/depois) + regra sem-comissão (garantia/reabertura do mesmo profissional) - normalizeFinanceiro: corrige CHECK capitalizado vs enforceUppercase (lançamentos não salvavam pela UI) - baixa de pagamento (pago_em), despesas (fornecedor/centro de custo/recorrente), competência por data de conclusão - relatório financeiro (período, paciente, profissional, forma, convênio, glosa) - orçamento Assinado -> contas a receber; contrato vigente -> cobrança recorrente; renovação automática de contrato Financeiro V2 (Comissão/Repasse): - regras de % (padrão/procedimento/override por profissional) + custo de laboratório - base selecionável (recebido/produção), fechamento persistido, pagar (gera despesa) + comprovante + estorno Glosas de convênio: por item de GTO, recurso/protocolo/prazo, recuperar/estornar, impacto no relatório GTO -> Financeiro: finalizar gera RECEITA (convênio a receber); LancarGTO passa a persistir; convênios reais (planos) Contratos: modelo "Atendimento a Convênios / Repasse (Dentista Credenciado)" + variáveis de convênio Salas (redesenho): perfil "Dono de Sala"; locação sempre ativa; menus MINHAS/ALUGAR SALAS; sala como workspace isolado (seletor agrupado Clínicas x Salas, tenantGuard por dono); financeiro de locação (reserva confirmada -> recebível); Painel da Sala (KPIs, agenda visual, recebíveis, relatório por sala) Docs: BACKLOG-FINANCEIRO-V1, AUDITORIA-FUNCIONAL-SCOREODONTO, CHECKLIST-DEPLOY-PRODUCAO Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
570 lines
26 KiB
TypeScript
570 lines
26 KiB
TypeScript
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
import {
|
|
GraduationCap, Plus, X, Send, Clock, CheckCircle2,
|
|
MessageSquare, ChevronDown, ChevronUp, Loader2, User, Camera,
|
|
ShieldAlert, DollarSign, Info
|
|
} from 'lucide-react';
|
|
import { HybridBackend } from '../services/backend.ts';
|
|
import { useToast } from '../contexts/ToastContext.tsx';
|
|
|
|
// ─── Tipos ────────────────────────────────────────────────────────
|
|
|
|
interface Tutor {
|
|
id: string;
|
|
nome: string;
|
|
especialidade?: string;
|
|
bio?: string;
|
|
preco_base?: number;
|
|
sla_horas?: number;
|
|
}
|
|
|
|
interface Mensagem {
|
|
id: string;
|
|
request_id: string;
|
|
autor_tipo: 'CLINICA' | 'TUTOR';
|
|
autor_nome?: string;
|
|
conteudo: string;
|
|
created_at: string;
|
|
}
|
|
|
|
interface Solicitacao {
|
|
id: string;
|
|
tratamento_id: string;
|
|
tutor_id: string;
|
|
tutor_nome?: string;
|
|
tutor_especialidade?: string;
|
|
tutor_bio?: string;
|
|
sla_horas?: number;
|
|
tipo: string;
|
|
descricao?: string;
|
|
paciente_nome?: string;
|
|
status: string;
|
|
created_at: string;
|
|
mensagens?: Mensagem[];
|
|
fotos?: { id: string; url: string; slot: string }[];
|
|
}
|
|
|
|
// ─── Helpers ──────────────────────────────────────────────────────
|
|
|
|
const TIPO_LABELS: Record<string, string> = {
|
|
SEGUNDA_OPINIAO: 'SEGUNDA OPINIÃO',
|
|
PLANEJAMENTO_COMPLETO: 'PLANEJAMENTO COMPLETO',
|
|
DISCUSSAO_CASO: 'DISCUSSÃO DE CASO',
|
|
ALINHADORES: 'ALINHADORES',
|
|
ORTO_CIRURGICO: 'ORTO-CIRÚRGICO',
|
|
ENCAMINHAMENTO: 'ENCAMINHAMENTO',
|
|
};
|
|
|
|
const STATUS_STYLE: Record<string, { cls: string; label: string }> = {
|
|
AGUARDANDO: { cls: 'bg-amber-100 text-amber-700', label: 'AGUARDANDO' },
|
|
EM_ANALISE: { cls: 'bg-blue-100 text-blue-700', label: 'EM ANÁLISE' },
|
|
RESPONDIDO: { cls: 'bg-green-100 text-green-700', label: 'RESPONDIDO' },
|
|
FECHADO: { cls: 'bg-gray-100 text-gray-500', label: 'FECHADO' },
|
|
};
|
|
|
|
// ─── Chat inline ──────────────────────────────────────────────────
|
|
|
|
const TutorChat: React.FC<{
|
|
solicitacao: Solicitacao;
|
|
autorNome: string;
|
|
onUpdated: () => void;
|
|
}> = ({ solicitacao, autorNome, onUpdated }) => {
|
|
const toast = useToast();
|
|
const [msgs, setMsgs] = useState<Mensagem[]>(solicitacao.mensagens || []);
|
|
const [texto, setTexto] = useState('');
|
|
const [enviando, setEnviando] = useState(false);
|
|
const bottomRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [msgs]);
|
|
|
|
const send = async () => {
|
|
const txt = texto.trim();
|
|
if (!txt) return;
|
|
setEnviando(true);
|
|
try {
|
|
const res = await HybridBackend.sendMensagemTutoria(solicitacao.id, txt, 'CLINICA', autorNome);
|
|
if (!res.success) { toast.error('ERRO AO ENVIAR.'); return; }
|
|
setMsgs(prev => [...prev, res.mensagem]);
|
|
setTexto('');
|
|
onUpdated();
|
|
} catch { toast.error('ERRO AO ENVIAR.'); }
|
|
finally { setEnviando(false); }
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2">
|
|
<div className="max-h-52 overflow-y-auto space-y-2 pr-1">
|
|
{msgs.length === 0 ? (
|
|
<p className="text-[10px] text-gray-400 font-bold uppercase text-center py-4">NENHUMA MENSAGEM AINDA — INICIE A CONVERSA</p>
|
|
) : msgs.map(m => (
|
|
<div key={m.id} className={`flex ${m.autor_tipo === 'CLINICA' ? 'justify-end' : 'justify-start'}`}>
|
|
<div className={`max-w-[80%] px-3 py-2 rounded-xl text-xs font-medium ${
|
|
m.autor_tipo === 'CLINICA'
|
|
? 'bg-teal-600 text-white rounded-br-none'
|
|
: 'bg-gray-100 text-gray-800 rounded-bl-none'
|
|
}`}>
|
|
{m.autor_nome && (
|
|
<p className={`text-[9px] font-black uppercase mb-0.5 ${m.autor_tipo === 'CLINICA' ? 'text-teal-200' : 'text-gray-400'}`}>
|
|
{m.autor_nome}
|
|
</p>
|
|
)}
|
|
<p>{m.conteudo}</p>
|
|
<p className={`text-[9px] mt-1 ${m.autor_tipo === 'CLINICA' ? 'text-teal-200' : 'text-gray-400'}`}>
|
|
{new Date(m.created_at).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
<div ref={bottomRef} />
|
|
</div>
|
|
|
|
{solicitacao.status !== 'FECHADO' && (
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
value={texto}
|
|
onChange={e => setTexto(e.target.value)}
|
|
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } }}
|
|
placeholder="MENSAGEM PARA O TUTOR..."
|
|
className="flex-1 border border-gray-200 rounded-xl px-3 py-2 text-xs font-medium focus:outline-none focus:border-teal-400 uppercase placeholder:normal-case"
|
|
/>
|
|
<button
|
|
onClick={send}
|
|
disabled={enviando || !texto.trim()}
|
|
className="px-3 py-2 bg-teal-600 hover:bg-teal-700 text-white rounded-xl disabled:opacity-40 transition-colors flex items-center gap-1"
|
|
>
|
|
{enviando ? <Loader2 size={14} className="animate-spin" /> : <Send size={14} />}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ─── Card de solicitação ──────────────────────────────────────────
|
|
|
|
const SolicitacaoCard: React.FC<{
|
|
sol: Solicitacao;
|
|
autorNome: string;
|
|
onUpdated: () => void;
|
|
}> = ({ sol, autorNome, onUpdated }) => {
|
|
const [open, setOpen] = useState(false);
|
|
const [detail, setDetail] = useState<Solicitacao | null>(null);
|
|
const [loadingDetail, setLoadingDetail] = useState(false);
|
|
const st = STATUS_STYLE[sol.status] || STATUS_STYLE.AGUARDANDO;
|
|
|
|
const loadDetail = useCallback(async () => {
|
|
if (detail) return;
|
|
setLoadingDetail(true);
|
|
try {
|
|
const res = await HybridBackend.getSolicitacaoTutoria(sol.id);
|
|
if (res.success) setDetail(res.solicitacao);
|
|
} catch {}
|
|
finally { setLoadingDetail(false); }
|
|
}, [sol.id, detail]);
|
|
|
|
const toggle = () => {
|
|
if (!open) loadDetail();
|
|
setOpen(!open);
|
|
};
|
|
|
|
const refreshDetail = async () => {
|
|
const res = await HybridBackend.getSolicitacaoTutoria(sol.id);
|
|
if (res.success) setDetail(res.solicitacao);
|
|
onUpdated();
|
|
};
|
|
|
|
return (
|
|
<div className="border border-gray-200 rounded-xl overflow-hidden bg-white">
|
|
{/* Header clicável */}
|
|
<button onClick={toggle} className="w-full flex items-center justify-between px-4 py-3 hover:bg-gray-50 transition-colors text-left gap-3">
|
|
<div className="flex items-center gap-3 min-w-0 flex-1">
|
|
<div className="w-9 h-9 bg-emerald-100 rounded-xl flex items-center justify-center flex-shrink-0">
|
|
<GraduationCap size={16} className="text-emerald-600" />
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="font-black text-sm text-gray-800 truncate">{sol.tutor_nome || 'TUTOR'}</p>
|
|
<p className="text-[10px] text-gray-500 font-bold uppercase">{TIPO_LABELS[sol.tipo] || sol.tipo}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
<span className={`text-[9px] font-black px-2 py-0.5 rounded-full uppercase ${st.cls}`}>{st.label}</span>
|
|
{open ? <ChevronUp size={16} className="text-gray-400" /> : <ChevronDown size={16} className="text-gray-400" />}
|
|
</div>
|
|
</button>
|
|
|
|
{/* Expandido */}
|
|
{open && (
|
|
<div className="px-4 pb-4 border-t border-gray-100 space-y-3 pt-3">
|
|
{/* Info do tutor */}
|
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
{sol.tutor_especialidade && (
|
|
<div><p className="text-[9px] font-black text-gray-400 uppercase">ESPECIALIDADE</p><p className="font-bold text-gray-700">{sol.tutor_especialidade}</p></div>
|
|
)}
|
|
{sol.sla_horas && (
|
|
<div><p className="text-[9px] font-black text-gray-400 uppercase">SLA</p><p className="font-bold text-gray-700">ATÉ {sol.sla_horas}H</p></div>
|
|
)}
|
|
{sol.descricao && (
|
|
<div className="col-span-2"><p className="text-[9px] font-black text-gray-400 uppercase">DESCRIÇÃO</p><p className="font-medium text-gray-700">{sol.descricao}</p></div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Fotos do pacote */}
|
|
{loadingDetail ? (
|
|
<div className="flex items-center justify-center py-4"><Loader2 size={20} className="animate-spin text-gray-300" /></div>
|
|
) : detail?.fotos && detail.fotos.length > 0 && (
|
|
<div>
|
|
<p className="text-[9px] font-black text-gray-400 uppercase mb-2">FOTOS INCLUÍDAS ({detail.fotos.length})</p>
|
|
<div className="flex gap-1.5 flex-wrap">
|
|
{detail.fotos.slice(0, 8).map(f => (
|
|
<img key={f.id} src={f.url} alt={f.slot} className="w-10 h-10 object-cover rounded-lg border border-gray-200" />
|
|
))}
|
|
{detail.fotos.length > 8 && (
|
|
<div className="w-10 h-10 bg-gray-100 rounded-lg border border-gray-200 flex items-center justify-center">
|
|
<span className="text-[9px] font-black text-gray-500">+{detail.fotos.length - 8}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Chat */}
|
|
{detail && (
|
|
<TutorChat
|
|
solicitacao={detail}
|
|
autorNome={autorNome}
|
|
onUpdated={refreshDetail}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ─── Modal de aceite de responsabilidade ─────────────────────────
|
|
|
|
const TEXTO_RESPONSABILIDADE_DEFAULT = `Ao solicitar esta tutoria, o profissional solicitante declara expressamente que:
|
|
|
|
• É o RESPONSÁVEL LEGAL pelo tratamento do paciente e por todas as decisões clínicas relacionadas.
|
|
|
|
• O parecer emitido pelo tutor tem caráter estritamente CONSULTIVO e NÃO VINCULANTE.
|
|
|
|
• A decisão de executar ou não as recomendações do tutor compete EXCLUSIVAMENTE ao profissional tratante, que deve avaliá-las com base em seu conhecimento clínico e nas condições específicas do paciente.
|
|
|
|
• O tutor NÃO ASSUME qualquer responsabilidade sobre a execução do tratamento, os resultados obtidos ou quaisquer complicações decorrentes da aplicação ou não das recomendações emitidas.
|
|
|
|
• Este termo é um registro formal do entendimento mútuo sobre os limites e a natureza consultiva do serviço de tutoria.`;
|
|
|
|
const AceiteModal: React.FC<{
|
|
pacienteNome: string;
|
|
autorNome: string;
|
|
textoCustom?: string;
|
|
onAceitar: (nome: string) => void;
|
|
onCancelar: () => void;
|
|
}> = ({ pacienteNome, autorNome, textoCustom, onAceitar, onCancelar }) => {
|
|
const [nome, setNome] = useState(autorNome);
|
|
const [aceito, setAceito] = useState(false);
|
|
const texto = textoCustom || TEXTO_RESPONSABILIDADE_DEFAULT;
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/70 z-[60] flex items-center justify-center p-4 backdrop-blur-sm">
|
|
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg max-h-[90vh] flex flex-col">
|
|
<div className="flex items-center gap-3 px-6 py-4 border-b border-gray-100 flex-shrink-0">
|
|
<div className="w-9 h-9 bg-amber-100 rounded-xl flex items-center justify-center flex-shrink-0">
|
|
<ShieldAlert size={18} className="text-amber-600" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-black text-sm uppercase text-gray-800">TERMO DE RESPONSABILIDADE CLÍNICA</h3>
|
|
<p className="text-[10px] text-gray-400 font-bold uppercase">PACIENTE: {pacienteNome}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto p-6 space-y-4 min-h-0">
|
|
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
|
|
<p className="text-[11px] text-amber-800 font-medium leading-relaxed whitespace-pre-line">{texto}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">SEU NOME COMPLETO (PARA REGISTRO) *</label>
|
|
<input type="text" value={nome} onChange={e => setNome(e.target.value.toUpperCase())}
|
|
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-bold focus:outline-none focus:border-amber-400 uppercase" />
|
|
</div>
|
|
|
|
<label className="flex items-start gap-3 p-3 bg-gray-50 border-2 border-gray-200 rounded-xl cursor-pointer hover:border-amber-300 transition-colors"
|
|
onClick={() => setAceito(!aceito)}>
|
|
<div className={`w-5 h-5 rounded border-2 flex items-center justify-center flex-shrink-0 mt-0.5 transition-all ${aceito ? 'bg-amber-500 border-amber-500' : 'border-gray-300'}`}>
|
|
{aceito && <CheckCircle2 size={12} className="text-white" />}
|
|
</div>
|
|
<p className="text-[11px] font-black text-gray-700 uppercase leading-relaxed">
|
|
LI, COMPREENDI E CONCORDO COM OS TERMOS ACIMA. ESTOU CIENTE DE QUE SOU O RESPONSÁVEL PELO TRATAMENTO E PELAS DECISÕES CLÍNICAS DO PACIENTE.
|
|
</p>
|
|
</label>
|
|
</div>
|
|
|
|
<div className="px-6 pb-6 flex gap-3 flex-shrink-0 border-t border-gray-100 pt-4">
|
|
<button onClick={onCancelar} className="flex-1 py-2.5 border border-gray-200 rounded-xl text-[11px] font-black uppercase hover:bg-gray-50 transition-colors">
|
|
CANCELAR
|
|
</button>
|
|
<button onClick={() => onAceitar(nome)} disabled={!aceito || !nome.trim()}
|
|
className="flex-1 py-2.5 bg-amber-600 text-white rounded-xl text-[11px] font-black uppercase hover:bg-amber-700 transition-colors disabled:opacity-40 flex items-center justify-center gap-2">
|
|
<CheckCircle2 size={13} /> CONFIRMAR E PROSSEGUIR
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ─── Modal de nova solicitação ────────────────────────────────────
|
|
|
|
const NovoModal: React.FC<{
|
|
tratamentoId: string;
|
|
clinicaId?: string;
|
|
pacienteNome: string;
|
|
autorNome: string;
|
|
onClose: () => void;
|
|
onCreated: () => void;
|
|
}> = ({ tratamentoId, clinicaId, pacienteNome, autorNome, onClose, onCreated }) => {
|
|
const toast = useToast();
|
|
const [tutores, setTutores] = useState<Tutor[]>([]);
|
|
const [tutorId, setTutorId] = useState('');
|
|
const [tipo, setTipo] = useState('SEGUNDA_OPINIAO');
|
|
const [descricao, setDescricao] = useState('');
|
|
const [loading, setLoading] = useState(true);
|
|
const [salvando, setSalvando] = useState(false);
|
|
const [showAceite, setShowAceite] = useState(false);
|
|
const [cfgTexto, setCfgTexto] = useState('');
|
|
|
|
useEffect(() => {
|
|
Promise.all([
|
|
HybridBackend.getTutores().then(r => { if (r.success) setTutores(r.tutores || []); }),
|
|
HybridBackend.getAdminTutorConfig().then(r => { if (r.success) setCfgTexto(r.config?.texto_responsabilidade || ''); }),
|
|
]).finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
const tutorSelecionado = tutores.find(t => t.id === tutorId);
|
|
|
|
const handleSubmitComAceite = async (nomeAceite: string) => {
|
|
setShowAceite(false);
|
|
setSalvando(true);
|
|
try {
|
|
const res = await HybridBackend.createSolicitacaoTutoria({
|
|
tratamento_id: tratamentoId,
|
|
clinica_id: clinicaId,
|
|
tutor_id: tutorId,
|
|
tipo,
|
|
descricao: descricao.toUpperCase() || undefined,
|
|
paciente_nome: pacienteNome,
|
|
aceite_responsabilidade: true,
|
|
aceite_nome: nomeAceite,
|
|
aceite_texto_versao: 'v1',
|
|
});
|
|
if (!res.success) { toast.error(res.message || 'ERRO.'); return; }
|
|
toast.success(`TUTORIA SOLICITADA! ${res.fotos_incluidas} FOTOS INCLUÍDAS NO PACOTE.`);
|
|
onCreated();
|
|
} catch { toast.error('ERRO AO SOLICITAR TUTORIA.'); }
|
|
finally { setSalvando(false); }
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
|
|
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg max-h-[90vh] flex flex-col">
|
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-100 flex-shrink-0">
|
|
<div className="flex items-center gap-2">
|
|
<GraduationCap size={18} className="text-emerald-500" />
|
|
<h3 className="font-black text-sm uppercase text-gray-800">SOLICITAR TUTORIA</h3>
|
|
</div>
|
|
<button onClick={onClose} className="p-1.5 hover:bg-gray-100 rounded-lg"><X size={18} /></button>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto p-6 space-y-4 min-h-0">
|
|
{/* Paciente */}
|
|
<div className="bg-emerald-50 border border-emerald-100 rounded-xl p-3 flex items-center gap-2">
|
|
<User size={14} className="text-emerald-500" />
|
|
<span className="text-[11px] font-black text-emerald-700 uppercase">PACIENTE: {pacienteNome}</span>
|
|
</div>
|
|
|
|
{/* Tutor */}
|
|
<div>
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">TUTOR ORTODÔNTICO *</label>
|
|
{loading ? (
|
|
<div className="flex items-center gap-2 py-2 text-gray-400 text-xs"><Loader2 size={14} className="animate-spin" /> CARREGANDO TUTORES...</div>
|
|
) : tutores.length === 0 ? (
|
|
<p className="text-xs text-red-500 font-bold uppercase">NENHUM TUTOR DISPONÍVEL CADASTRADO.</p>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{tutores.map(t => (
|
|
<label key={t.id} className={`flex items-start gap-3 p-3 rounded-xl border-2 cursor-pointer transition-all ${tutorId === t.id ? 'border-emerald-500 bg-emerald-50' : 'border-gray-200 hover:border-emerald-200'}`}>
|
|
<input type="radio" name="tutor" value={t.id} checked={tutorId === t.id} onChange={() => setTutorId(t.id)} className="mt-0.5" />
|
|
<div className="flex-1">
|
|
<p className="font-black text-sm text-gray-800">{t.nome}</p>
|
|
{t.especialidade && <p className="text-[10px] text-gray-500 font-bold uppercase">{t.especialidade}</p>}
|
|
<div className="flex gap-3 mt-1 text-[10px] font-bold text-gray-400 uppercase flex-wrap">
|
|
{t.sla_horas && <span className="flex items-center gap-1"><Clock size={10} /> RESP. EM {t.sla_horas}H</span>}
|
|
{t.preco_base && (
|
|
<span className="flex items-center gap-1 text-emerald-600">
|
|
<DollarSign size={10} /> R$ {Number(t.preco_base).toLocaleString('pt-BR', { minimumFractionDigits: 2 })} / CONSULTA
|
|
</span>
|
|
)}
|
|
</div>
|
|
{t.bio && <p className="text-[10px] text-gray-500 mt-1 italic">{t.bio}</p>}
|
|
</div>
|
|
</label>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Preço estimado */}
|
|
{tutorSelecionado?.preco_base && (
|
|
<div className="bg-green-50 border border-green-100 rounded-xl p-3 flex items-center gap-2">
|
|
<DollarSign size={14} className="text-green-600 flex-shrink-0" />
|
|
<div>
|
|
<p className="text-[11px] font-black text-green-700 uppercase">
|
|
VALOR DA CONSULTORIA: R$ {Number(tutorSelecionado.preco_base).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
|
</p>
|
|
<p className="text-[9px] text-green-600 font-bold uppercase">PAGAMENTO COMBINADO DIRETAMENTE COM O TUTOR APÓS RESPOSTA</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Tipo */}
|
|
<div>
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">TIPO DE TUTORIA *</label>
|
|
<select value={tipo} onChange={e => setTipo(e.target.value)}
|
|
className="w-full border border-gray-200 rounded-xl px-3 py-2 text-sm font-bold focus:outline-none focus:border-emerald-400 uppercase">
|
|
{Object.entries(TIPO_LABELS).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Descrição */}
|
|
<div>
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">DESCRIÇÃO DO CASO</label>
|
|
<textarea value={descricao} onChange={e => setDescricao(e.target.value.toUpperCase())}
|
|
placeholder="DESCREVA SUAS DÚVIDAS OU O QUE PRECISA DE APOIO..."
|
|
className="w-full border border-gray-200 rounded-xl px-3 py-2 text-xs font-medium h-20 resize-none focus:outline-none focus:border-emerald-400 uppercase placeholder:normal-case" />
|
|
</div>
|
|
|
|
{/* Aviso pacote */}
|
|
<div className="bg-amber-50 border border-amber-100 rounded-xl p-3 flex items-start gap-2">
|
|
<Camera size={14} className="text-amber-600 flex-shrink-0 mt-0.5" />
|
|
<p className="text-[10px] text-amber-700 font-bold uppercase leading-relaxed">
|
|
O SISTEMA VAI INCLUIR AUTOMATICAMENTE TODAS AS FOTOS DO TRATAMENTO NO PACOTE ENVIADO AO TUTOR.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Aviso responsabilidade */}
|
|
<div className="bg-red-50 border border-red-100 rounded-xl p-3 flex items-start gap-2">
|
|
<ShieldAlert size={14} className="text-red-500 flex-shrink-0 mt-0.5" />
|
|
<p className="text-[10px] text-red-600 font-bold uppercase leading-relaxed">
|
|
AO PROSSEGUIR, VOCÊ DEVERÁ ASSINAR UM TERMO DE RESPONSABILIDADE CONFIRMANDO QUE É O RESPONSÁVEL PELO TRATAMENTO E PELA EXECUÇÃO OU NÃO DAS RECOMENDAÇÕES DO TUTOR.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="px-6 pb-6 flex gap-3 flex-shrink-0 border-t border-gray-100 pt-4">
|
|
<button onClick={onClose} className="flex-1 py-2.5 border border-gray-200 rounded-xl text-[11px] font-black uppercase hover:bg-gray-50 transition-colors">
|
|
CANCELAR
|
|
</button>
|
|
<button onClick={() => setShowAceite(true)} disabled={salvando || !tutorId}
|
|
className="flex-1 py-2.5 bg-emerald-600 text-white rounded-xl text-[11px] font-black uppercase hover:bg-emerald-700 transition-colors disabled:opacity-40 flex items-center justify-center gap-2">
|
|
{salvando ? <><Loader2 size={13} className="animate-spin" /> ENVIANDO...</> : <><ShieldAlert size={13} /> PROSSEGUIR E ASSINAR TERMO</>}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{showAceite && (
|
|
<AceiteModal
|
|
pacienteNome={pacienteNome}
|
|
autorNome={autorNome}
|
|
textoCustom={cfgTexto}
|
|
onAceitar={handleSubmitComAceite}
|
|
onCancelar={() => setShowAceite(false)}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
// ─── Componente principal ─────────────────────────────────────────
|
|
|
|
interface Props {
|
|
tratamentoId: string;
|
|
clinicaId?: string;
|
|
pacienteNome: string;
|
|
}
|
|
|
|
export const TutoriaSection: React.FC<Props> = ({ tratamentoId, clinicaId, pacienteNome }) => {
|
|
const [solicitacoes, setSolicitacoes] = useState<Solicitacao[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [showModal, setShowModal] = useState(false);
|
|
const autorNome = HybridBackend.getCurrentUserName() || 'CLÍNICA';
|
|
|
|
const load = useCallback(async () => {
|
|
try {
|
|
const res = await HybridBackend.getSolicitacoesTutoria(clinicaId, tratamentoId);
|
|
if (res.success) setSolicitacoes(res.solicitacoes || []);
|
|
} catch {}
|
|
finally { setLoading(false); }
|
|
}, [clinicaId, tratamentoId]);
|
|
|
|
useEffect(() => { load(); }, [load]);
|
|
|
|
return (
|
|
<div className="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden">
|
|
{/* Header */}
|
|
<div className="px-5 py-4 border-b border-gray-100 bg-gray-50 flex items-center justify-between gap-3">
|
|
<div className="flex items-center gap-2">
|
|
<GraduationCap size={16} className="text-emerald-500" />
|
|
<h3 className="font-black text-gray-700 uppercase text-sm">TUTORIA ORTODÔNTICA</h3>
|
|
{solicitacoes.length > 0 && (
|
|
<span className="text-[10px] font-black px-2 py-0.5 bg-emerald-100 text-emerald-600 rounded-full">{solicitacoes.length}</span>
|
|
)}
|
|
</div>
|
|
<button
|
|
onClick={() => setShowModal(true)}
|
|
className="flex items-center gap-1.5 px-3 py-1.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl text-[10px] font-black uppercase transition-colors"
|
|
>
|
|
<Plus size={13} /> SOLICITAR TUTORIA
|
|
</button>
|
|
</div>
|
|
|
|
{/* Conteúdo */}
|
|
<div className="p-5">
|
|
{loading ? (
|
|
<div className="flex items-center justify-center h-20"><Loader2 size={22} className="animate-spin text-gray-300" /></div>
|
|
) : solicitacoes.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center py-6 text-gray-400 gap-2">
|
|
<GraduationCap size={28} />
|
|
<p className="text-xs font-bold uppercase">NENHUMA TUTORIA SOLICITADA</p>
|
|
<p className="text-[10px] text-gray-400 text-center max-w-xs">
|
|
SOLICITE UMA SEGUNDA OPINIÃO OU MENTORIA DE UM ORTODONTISTA ESPECIALISTA.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{solicitacoes.map(s => (
|
|
<SolicitacaoCard key={s.id} sol={s} autorNome={autorNome} onUpdated={load} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Modal */}
|
|
{showModal && (
|
|
<NovoModal
|
|
tratamentoId={tratamentoId}
|
|
clinicaId={clinicaId}
|
|
pacienteNome={pacienteNome}
|
|
autorNome={autorNome}
|
|
onClose={() => setShowModal(false)}
|
|
onCreated={() => { setShowModal(false); load(); }}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|