Files
scoreodonto.com/frontend/views/plugins/ProfissionaisPlugin.tsx
T
VPS 4 Builder 17f83e5c86 feat(marketplace): exigir endereço/CEP da origem antes da busca por proximidade
- Profissionais e Salas: aba de busca bloqueia se a origem não tiver cep+cidade+estado
- clínica/consultório (donoclinica/donoconsultorio/admin/funcionário): checa endereço
  da clínica ativa → CTA p/ Configurações
- dentista/biomédico/protético: checa o próprio perfil → CTA p/ completar perfil
- fail-safe: em erro de checagem não bloqueia

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 03:23:32 +02:00

546 lines
30 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react';
import {
UserSearch, Stethoscope, Wrench, Sparkles, MapPin, Search, Mail, Phone, RefreshCw,
Save, BadgeCheck, Inbox, Send, CheckCircle2, XCircle, CircleDollarSign, UserCircle2, X
} from 'lucide-react';
import { PageHeader } from '../../components/PageHeader.tsx';
import { useToast } from '../../contexts/ToastContext.tsx';
import { HybridBackend } from '../../services/backend.ts';
import { buscarCep } from '../../services/viacep.ts';
const API_URL = (window as any).__API_URL__ || '';
const UF_LIST = ['', 'AC', 'AL', 'AM', 'AP', 'BA', 'CE', 'DF', 'ES', 'GO', 'MA', 'MG', 'MS', 'MT', 'PA', 'PB', 'PE', 'PI', 'PR', 'RJ', 'RN', 'RO', 'RR', 'RS', 'SC', 'SE', 'SP', 'TO'];
const COLOR = '#ea580c'; // laranja — identidade do plugin
const apiFetch = (path: string, opts: RequestInit = {}) =>
fetch(`${API_URL}${path}`, {
...opts,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.getItem('SCOREODONTO_AUTH_TOKEN')}`,
...(opts.headers || {}),
},
});
// ─── Types ───────────────────────────────────────────────────────────────────
interface Profissional {
id: string;
nome: string;
email: string;
celular?: string;
role: 'dentista' | 'biomedico' | 'protetico';
estado: string;
cidade: string;
bairro: string;
cep?: string;
especialidade: string;
raio_atuacao_km?: number | null;
bio_profissional?: string | null;
dist_km?: number | string | null;
}
interface Proposta {
id: string;
clinica_id: string;
clinica_nome?: string;
profissional_id: string;
profissional_nome?: string;
profissional_email?: string;
profissional_role?: string;
enviada_por_nome?: string;
mensagem?: string | null;
status: 'pendente' | 'aceita' | 'recusada' | 'cancelada';
created_at: string;
}
const ROLE_META: Record<string, { label: string; icon: any; cls: string }> = {
dentista: { label: 'Dentista', icon: Stethoscope, cls: 'bg-blue-100 text-blue-700' },
biomedico: { label: 'Biomédico(a)', icon: Sparkles, cls: 'bg-pink-100 text-pink-700' },
protetico: { label: 'Protético', icon: Wrench, cls: 'bg-violet-100 text-violet-700' },
};
const STATUS_STYLE: Record<string, string> = {
pendente: 'bg-amber-100 text-amber-700',
aceita: 'bg-emerald-100 text-emerald-700',
recusada: 'bg-red-100 text-red-600',
cancelada: 'bg-gray-100 text-gray-500',
};
const inputCls = 'w-full px-3 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs font-bold text-gray-700 outline-none focus:border-orange-400';
const labelCls = 'text-[10px] font-black text-gray-400 uppercase tracking-widest block mb-1';
// ─── Modal: enviar proposta ──────────────────────────────────────────────────
const PropostaModal: React.FC<{ prof: Profissional; onClose: () => void; onSent: () => void }> = ({ prof, onClose, onSent }) => {
const toast = useToast();
const ws = HybridBackend.getActiveWorkspace();
const [mensagem, setMensagem] = useState('');
const [saving, setSaving] = useState(false);
const handleSend = async () => {
if (!ws?.id) { toast.error('SELECIONE UMA CLÍNICA/CONSULTÓRIO ATIVO PARA ENVIAR PROPOSTAS.'); return; }
setSaving(true);
try {
const res = await apiFetch('/api/propostas', {
method: 'POST',
body: JSON.stringify({ clinicaId: ws.id, profissionalId: prof.id, mensagem: mensagem || null }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Erro ao enviar proposta.');
toast.success('PROPOSTA ENVIADA! O PROFISSIONAL SERÁ NOTIFICADO.');
onSent();
onClose();
} catch (e: any) { toast.error(e.message.toUpperCase()); }
finally { setSaving(false); }
};
return (
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center backdrop-blur-sm p-4">
<div className="bg-white rounded-2xl w-full max-w-md shadow-2xl overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
<div>
<h3 className="text-sm font-black text-gray-900 uppercase">PROPOSTA PARA {prof.nome}</h3>
<p className="text-[10px] font-bold uppercase tracking-widest" style={{ color: COLOR }}>EM NOME DE {ws?.nome || '—'}</p>
</div>
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-xl transition-colors"><X size={18} /></button>
</div>
<div className="p-6">
<label className={labelCls}>Mensagem (opcional)</label>
<textarea className={inputCls} rows={4} value={mensagem} onChange={e => setMensagem(e.target.value)}
placeholder="Ex: Buscamos um(a) especialista para atender às quartas-feiras. Podemos conversar?" />
</div>
<div className="px-6 pb-6 flex gap-3">
<button onClick={onClose} className="flex-1 py-3 border border-gray-200 rounded-xl text-xs font-black uppercase text-gray-500 hover:bg-gray-50 transition-colors">CANCELAR</button>
<button onClick={handleSend} disabled={saving} className="flex-1 py-3 text-white rounded-xl text-xs font-black uppercase transition-colors flex items-center justify-center gap-2 disabled:opacity-60" style={{ backgroundColor: COLOR }}>
{saving ? <RefreshCw size={14} className="animate-spin" /> : <Send size={14} />} ENVIAR PROPOSTA
</button>
</div>
</div>
</div>
);
};
// ─── Main view ───────────────────────────────────────────────────────────────
type Tab = 'buscar' | 'perfil' | 'propostas';
// Bloqueio quando falta endereço/CEP da origem (clínica ou perfil) — a busca é por proximidade.
const GateEndereco: React.FC<{ isClinic: boolean; onCompletarPerfil: () => void }> = ({ isClinic, onCompletarPerfil }) => (
<div className="bg-white rounded-2xl border border-amber-200 shadow-sm p-8 text-center">
<div className="w-14 h-14 rounded-2xl bg-amber-50 flex items-center justify-center mx-auto mb-4">
<MapPin size={26} className="text-amber-500" />
</div>
<h3 className="text-base font-black uppercase tracking-tight text-gray-900">Complete seu endereço</h3>
<p className="text-sm text-gray-500 mt-2 max-w-md mx-auto leading-relaxed">
A busca de profissionais é por proximidade. Preencha {isClinic
? 'a localização e o CEP da sua clínica/consultório (em Configurações)'
: 'a localização e o CEP do seu perfil'} para continuar.
</p>
<button onClick={() => isClinic ? window.location.assign('/configuracoes') : onCompletarPerfil()}
className="mt-5 inline-flex items-center gap-2 px-5 py-3 rounded-xl text-white text-xs font-black uppercase tracking-widest hover:opacity-90 transition-opacity"
style={{ backgroundColor: COLOR }}>
{isClinic ? 'Ir para Configurações' : 'Completar meu perfil'}
</button>
</div>
);
export const ProfissionaisPlugin: React.FC = () => {
const toast = useToast();
const role = HybridBackend.getCurrentRole();
const ws = HybridBackend.getActiveWorkspace();
const isProfissional = ['dentista', 'biomedico', 'protetico'].includes(role);
const [tab, setTab] = useState<Tab>('buscar');
const [loading, setLoading] = useState(false);
// buscar
const [lista, setLista] = useState<Profissional[]>([]);
const [fTipo, setFTipo] = useState('');
const [fEstado, setFEstado] = useState('');
const [fCidade, setFCidade] = useState('');
const [fEspecialidade, setFEspecialidade] = useState('');
const [fCep, setFCep] = useState('');
const [fRaio, setFRaio] = useState('');
// perfil
const [perfil, setPerfil] = useState<any>(null);
const [savingPerfil, setSavingPerfil] = useState(false);
// propostas
const [propostas, setPropostas] = useState<{ enviadas: Proposta[]; recebidas: Proposta[] }>({ enviadas: [], recebidas: [] });
const [proposingTo, setProposingTo] = useState<Profissional | null>(null);
// null = checando; true/false = endereço da origem (clínica ou perfil) preenchido?
const [enderecoOk, setEnderecoOk] = useState<boolean | null>(null);
const checkEndereco = useCallback(async () => {
try {
if (isProfissional) {
const res = await apiFetch('/api/profissionais/perfil');
const p = res.ok ? await res.json() : {};
setEnderecoOk(!!(p?.cep && p?.cidade && p?.estado));
} else if (ws?.id) {
const r = await HybridBackend.getClinicaProfile(ws.id);
const c: any = r?.clinica || {};
setEnderecoOk(!!(c.cep && c.cidade && c.estado));
} else {
setEnderecoOk(true); // sem contexto claro → não bloqueia
}
} catch { setEnderecoOk(true); } // em erro, não bloqueia a tela
}, [isProfissional, ws?.id]);
useEffect(() => { checkEndereco(); }, [checkEndereco]);
const fetchLista = useCallback(async () => {
setLoading(true);
try {
const params = new URLSearchParams();
if (fTipo) params.set('tipo', fTipo);
if (fEstado) params.set('estado', fEstado);
if (fCidade) params.set('cidade', fCidade);
if (fEspecialidade) params.set('especialidade', fEspecialidade);
// Origem automática: clínica ativa (backend cai no geo do usuário se faltar) → mais próximos primeiro.
const wsId = HybridBackend.getActiveWorkspace()?.id;
if (wsId) params.set('clinicaId', wsId);
if (fCep) params.set('cep', fCep); // CEP digitado sobrepõe a origem automática
if (fCep && Number(fRaio) > 0) params.set('raio', fRaio);
const res = await apiFetch(`/api/profissionais/marketplace?${params}`);
if (res.ok) setLista(await res.json());
} catch { /* silently fail */ }
finally { setLoading(false); }
}, [fTipo, fEstado, fCidade, fEspecialidade, fCep, fRaio]);
const fetchPerfil = useCallback(async () => {
setLoading(true);
try { const res = await apiFetch('/api/profissionais/perfil'); if (res.ok) setPerfil(await res.json()); }
catch { /* silently fail */ }
finally { setLoading(false); }
}, []);
const fetchPropostas = useCallback(async () => {
setLoading(true);
try {
// Recebidas (como profissional)
const rRes = await apiFetch('/api/propostas');
const recebidas = rRes.ok ? (await rRes.json()).recebidas : [];
// Enviadas (pela unidade ativa, se houver)
let enviadas: Proposta[] = [];
if (ws?.id) {
const eRes = await apiFetch(`/api/propostas?clinicaId=${encodeURIComponent(ws.id)}`);
if (eRes.ok) enviadas = (await eRes.json()).enviadas;
}
setPropostas({ enviadas, recebidas });
} catch { /* silently fail */ }
finally { setLoading(false); }
}, [ws?.id]);
useEffect(() => {
if (tab === 'buscar') fetchLista();
if (tab === 'perfil') fetchPerfil();
if (tab === 'propostas') fetchPropostas();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tab]);
const handleSavePerfil = async () => {
if (perfil?.disponivel_diretorio && (!perfil.estado || !perfil.cidade)) {
toast.error('ESTADO E CIDADE SÃO OBRIGATÓRIOS PARA APARECER NO MARKETPLACE.');
return;
}
setSavingPerfil(true);
try {
const res = await apiFetch('/api/profissionais/perfil', {
method: 'PUT',
body: JSON.stringify({
estado: perfil.estado, cidade: perfil.cidade, bairro: perfil.bairro, cep: perfil.cep,
especialidade: perfil.especialidade_dir, raio_atuacao_km: perfil.raio_atuacao_km,
bio_profissional: perfil.bio_profissional, disponivel_diretorio: perfil.disponivel_diretorio === true,
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Erro ao salvar.');
toast.success('PERFIL PROFISSIONAL ATUALIZADO!');
} catch (e: any) { toast.error(e.message.toUpperCase()); }
finally { setSavingPerfil(false); }
};
const handlePropostaAction = async (p: Proposta, acao: 'aceitar' | 'recusar' | 'cancelar') => {
if (acao === 'cancelar' && !window.confirm('CANCELAR ESTA PROPOSTA?')) return;
try {
const res = await apiFetch(`/api/propostas/${p.id}`, { method: 'PUT', body: JSON.stringify({ acao }) });
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Erro.');
toast.success(`PROPOSTA ${data.status?.toUpperCase()}.`);
fetchPropostas();
} catch (e: any) { toast.error(e.message.toUpperCase()); }
};
const setP = (k: string, v: any) => setPerfil((prev: any) => ({ ...prev, [k]: v }));
const TABS: Array<{ id: Tab; label: string; icon: any; show: boolean }> = [
{ id: 'buscar', label: 'BUSCAR PROFISSIONAIS', icon: Search, show: true },
{ id: 'perfil', label: 'MEU PERFIL', icon: UserCircle2, show: isProfissional },
{ id: 'propostas', label: 'PROPOSTAS', icon: CircleDollarSign, show: true },
];
const pendentesRecebidas = propostas.recebidas.filter(p => p.status === 'pendente').length;
return (
<div className="space-y-8 animate-in fade-in duration-500">
<PageHeader
title="MARKETPLACE DE PROFISSIONAIS"
description="Encontre e contrate dentistas, biomédicos e protéticos — ou divulgue seu perfil e receba propostas."
/>
{/* Tabs */}
<div className="flex gap-2 flex-wrap">
{TABS.filter(t => t.show).map(t => {
const Icon = t.icon;
const active = tab === t.id;
return (
<button key={t.id} onClick={() => setTab(t.id)}
className={`flex items-center gap-2 px-5 py-2.5 rounded-xl text-[11px] font-black uppercase tracking-wider transition-all ${active ? 'text-white shadow-md' : 'bg-white text-gray-500 border border-gray-200 hover:bg-gray-50'}`}
style={active ? { backgroundColor: COLOR } : {}}>
<Icon size={14} /> {t.label}
{t.id === 'propostas' && pendentesRecebidas > 0 && (
<span className={`text-[9px] font-black px-1.5 py-0.5 rounded-full ${active ? 'bg-white/25' : 'bg-amber-100 text-amber-700'}`}>{pendentesRecebidas}</span>
)}
</button>
);
})}
</div>
{/* BUSCAR — bloqueia se faltar endereço/CEP da origem */}
{tab === 'buscar' && enderecoOk === false && (
<GateEndereco isClinic={!isProfissional} onCompletarPerfil={() => setTab('perfil')} />
)}
{tab === 'buscar' && enderecoOk !== false && (
<>
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-5">
<div className="grid grid-cols-2 md:grid-cols-5 gap-3 mb-4">
<div>
<label className={labelCls}>Tipo</label>
<select className={inputCls} value={fTipo} onChange={e => setFTipo(e.target.value)}>
<option value="">Todos</option>
<option value="dentista">Dentistas</option>
<option value="biomedico">Biomédicos(as)</option>
<option value="protetico">Protéticos</option>
</select>
</div>
<div>
<label className={labelCls}>Estado</label>
<select className={inputCls} value={fEstado} onChange={e => setFEstado(e.target.value)}>
{UF_LIST.map(uf => <option key={uf} value={uf}>{uf || 'Todos'}</option>)}
</select>
</div>
<div>
<label className={labelCls}>Cidade</label>
<input className={`${inputCls} uppercase`} value={fCidade} onChange={e => setFCidade(e.target.value.toUpperCase())} placeholder="CAMPO GRANDE" />
</div>
<div>
<label className={labelCls}>Especialidade</label>
<input className={`${inputCls} uppercase`} value={fEspecialidade} onChange={e => setFEspecialidade(e.target.value.toUpperCase())} placeholder="ORTODONTIA" />
</div>
<div>
<label className={labelCls}>Meu CEP</label>
<input className={inputCls} value={fCep} onChange={e => setFCep(e.target.value)} placeholder="79000-000" />
</div>
<div>
<label className={labelCls}>Raio (km)</label>
<input type="number" min="1" className={inputCls} value={fRaio} onChange={e => setFRaio(e.target.value)} placeholder="20" />
</div>
</div>
<button onClick={fetchLista} disabled={loading}
className="flex items-center gap-2 px-5 py-2.5 text-white rounded-xl text-xs font-black uppercase tracking-widest transition-colors disabled:opacity-60 hover:opacity-90" style={{ backgroundColor: COLOR }}>
{loading ? <RefreshCw size={14} className="animate-spin" /> : <Search size={14} />} BUSCAR
</button>
</div>
{loading ? (
<div className="flex justify-center py-12"><RefreshCw size={24} className="animate-spin" style={{ color: COLOR }} /></div>
) : lista.length === 0 ? (
<div className="text-center py-12 text-gray-400">
<UserSearch size={40} className="mx-auto mb-3 opacity-40" />
<p className="font-bold text-sm uppercase">Nenhum profissional encontrado</p>
<p className="text-xs mt-1">Ajuste os filtros ou aguarde novos cadastros</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{lista.map(p => {
const meta = ROLE_META[p.role] || ROLE_META.dentista;
const Icon = meta.icon;
return (
<div key={p.id} className="bg-white rounded-2xl border border-gray-100 shadow-sm p-5 hover:shadow-md transition-all">
<div className="flex items-start gap-3 mb-3">
<div className={`w-10 h-10 rounded-xl flex items-center justify-center shrink-0 ${meta.cls.split(' ')[0]}`}>
<Icon size={18} className={meta.cls.split(' ')[1]} />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-black text-gray-900 text-sm uppercase truncate">{p.nome}</span>
<span className={`text-[9px] font-black px-2 py-0.5 rounded-full uppercase ${meta.cls}`}>{meta.label}</span>
</div>
{p.especialidade && <p className="text-xs text-gray-500 font-medium mt-0.5 uppercase">{p.especialidade}</p>}
</div>
</div>
{p.bio_profissional && <p className="text-xs text-gray-500 font-medium mb-2 line-clamp-2">{p.bio_profissional}</p>}
<div className="space-y-1.5 mb-4">
{(p.cidade || p.estado) && (
<div className="flex items-center gap-1.5 text-xs text-gray-500 font-medium">
<MapPin size={12} className="shrink-0 text-gray-400" />
<span>{[p.bairro, p.cidade, p.estado].filter(Boolean).join(' · ')}{p.raio_atuacao_km ? ` · ATENDE EM ATÉ ${p.raio_atuacao_km} KM` : ''}</span>
{p.dist_km != null && <span className="ml-auto shrink-0 text-[9px] font-black px-2 py-0.5 rounded-full bg-orange-100 text-orange-700">~{p.dist_km} KM</span>}
</div>
)}
<div className="flex items-center gap-1.5 text-xs text-gray-500 font-medium">
<Mail size={12} className="shrink-0 text-gray-400" /><span>{p.email}</span>
</div>
{p.celular && (
<div className="flex items-center gap-1.5 text-xs text-gray-500 font-medium">
<Phone size={12} className="shrink-0 text-gray-400" /><span>{p.celular}</span>
</div>
)}
</div>
{ws?.id && (
<button onClick={() => setProposingTo(p)}
className="w-full py-2.5 rounded-xl text-white text-[10px] font-black uppercase flex items-center justify-center gap-1.5 transition-colors hover:opacity-90" style={{ backgroundColor: COLOR }}>
<Send size={13} /> ENVIAR PROPOSTA
</button>
)}
</div>
);
})}
</div>
)}
</>
)}
{/* MEU PERFIL */}
{tab === 'perfil' && (
loading || !perfil ? (
<div className="flex justify-center py-12"><RefreshCw size={24} className="animate-spin" style={{ color: COLOR }} /></div>
) : (
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6 max-w-2xl space-y-4">
<div className="flex items-center gap-3 pb-2 border-b border-gray-50">
<BadgeCheck size={18} style={{ color: COLOR }} />
<div>
<p className="text-sm font-black text-gray-900 uppercase">{perfil.nome}</p>
<p className="text-[10px] text-gray-400 font-bold uppercase">{(ROLE_META[perfil.role]?.label || perfil.role)} · {perfil.email}</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className={labelCls}>Estado *</label>
<select className={inputCls} value={perfil.estado || ''} onChange={e => setP('estado', e.target.value)}>
{UF_LIST.map(uf => <option key={uf} value={uf}>{uf || 'SELECIONE'}</option>)}
</select>
</div>
<div>
<label className={labelCls}>Cidade *</label>
<input className={`${inputCls} uppercase`} value={perfil.cidade || ''} onChange={e => setP('cidade', e.target.value.toUpperCase())} />
</div>
<div>
<label className={labelCls}>Bairro</label>
<input className={`${inputCls} uppercase`} value={perfil.bairro || ''} onChange={e => setP('bairro', e.target.value.toUpperCase())} />
</div>
<div>
<label className={labelCls}>CEP</label>
<input className={inputCls} value={perfil.cep || ''} onChange={async e => {
const v = e.target.value;
setP('cep', v);
if (v.replace(/\D/g, '').length === 8) {
const r = await buscarCep(v);
if (r) { setPerfil((prev: any) => ({ ...prev, bairro: r.bairro, cidade: r.cidade, estado: r.estado })); toast.success('LOCALIZAÇÃO PREENCHIDA PELO CEP!'); }
else toast.error('CEP NÃO ENCONTRADO.');
}
}} placeholder="79000-000" />
</div>
<div>
<label className={labelCls}>Especialidade principal</label>
<input className={`${inputCls} uppercase`} value={perfil.especialidade_dir || ''} onChange={e => setP('especialidade_dir', e.target.value.toUpperCase())} placeholder="EX: IMPLANTODONTIA" />
</div>
<div>
<label className={labelCls}>Raio de atuação (km)</label>
<input type="number" min="0" max="2000" className={inputCls} value={perfil.raio_atuacao_km ?? ''} onChange={e => setP('raio_atuacao_km', e.target.value === '' ? null : +e.target.value)} placeholder="EX: 100" />
</div>
</div>
<div>
<label className={labelCls}>Apresentação / currículo resumido</label>
<textarea className={inputCls} rows={3} value={perfil.bio_profissional || ''} onChange={e => setP('bio_profissional', e.target.value)}
placeholder="Ex: Ortodontista com 10 anos de experiência, atendo clínicas e consultórios na região de Campo Grande." />
</div>
<label className="flex items-center gap-3 p-3 rounded-xl border border-gray-100 bg-gray-50/60 cursor-pointer">
<input type="checkbox" checked={perfil.disponivel_diretorio === true} onChange={e => setP('disponivel_diretorio', e.target.checked)}
className="w-4 h-4 accent-orange-600" />
<span className="text-[11px] font-black text-gray-600 uppercase">Aparecer no marketplace (disponível para propostas)</span>
</label>
<button onClick={handleSavePerfil} disabled={savingPerfil}
className="flex items-center gap-2 px-6 py-3 text-white rounded-xl text-xs font-black uppercase tracking-widest transition-colors disabled:opacity-60 hover:opacity-90" style={{ backgroundColor: COLOR }}>
{savingPerfil ? <RefreshCw size={14} className="animate-spin" /> : <Save size={14} />} SALVAR PERFIL
</button>
</div>
)
)}
{/* PROPOSTAS */}
{tab === 'propostas' && (
loading ? (
<div className="flex justify-center py-12"><RefreshCw size={24} className="animate-spin" style={{ color: COLOR }} /></div>
) : (
<div className="space-y-8">
{isProfissional && (
<div>
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-3 flex items-center gap-1.5"><Inbox size={12} /> RECEBIDAS</p>
{propostas.recebidas.length === 0
? <p className="text-xs text-gray-300 font-bold uppercase">Nenhuma proposta recebida</p>
: <div className="space-y-3">
{propostas.recebidas.map(p => (
<div key={p.id} className="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex flex-col md:flex-row md:items-center gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-black text-gray-900 text-xs uppercase">{p.clinica_nome || 'UNIDADE'}</span>
<span className={`text-[9px] font-black px-2 py-0.5 rounded-full uppercase ${STATUS_STYLE[p.status]}`}>{p.status}</span>
</div>
{p.mensagem && <p className="text-[11px] text-gray-500 font-medium mt-1">{p.mensagem}</p>}
<p className="text-[10px] text-gray-400 font-medium uppercase mt-0.5">POR {p.enviada_por_nome || '—'} · {new Date(p.created_at).toLocaleDateString('pt-BR')}</p>
</div>
{p.status === 'pendente' && (
<div className="flex gap-2 shrink-0">
<button onClick={() => handlePropostaAction(p, 'aceitar')} className="px-3 py-2 rounded-xl bg-emerald-500 text-white text-[10px] font-black uppercase flex items-center gap-1 hover:bg-emerald-600 transition-colors"><CheckCircle2 size={12} /> ACEITAR</button>
<button onClick={() => handlePropostaAction(p, 'recusar')} className="px-3 py-2 rounded-xl border border-red-200 text-red-500 text-[10px] font-black uppercase flex items-center gap-1 hover:bg-red-50 transition-colors"><XCircle size={12} /> RECUSAR</button>
</div>
)}
</div>
))}
</div>}
</div>
)}
{ws?.id && (
<div>
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-3 flex items-center gap-1.5"><Send size={12} /> ENVIADAS POR {ws.nome?.toUpperCase() || 'MINHA UNIDADE'}</p>
{propostas.enviadas.length === 0
? <p className="text-xs text-gray-300 font-bold uppercase">Nenhuma proposta enviada</p>
: <div className="space-y-3">
{propostas.enviadas.map(p => (
<div key={p.id} className="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex flex-col md:flex-row md:items-center gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-black text-gray-900 text-xs uppercase">{p.profissional_nome || p.profissional_id}</span>
{p.profissional_role && <span className={`text-[9px] font-black px-2 py-0.5 rounded-full uppercase ${ROLE_META[p.profissional_role]?.cls || 'bg-gray-100 text-gray-500'}`}>{ROLE_META[p.profissional_role]?.label || p.profissional_role}</span>}
<span className={`text-[9px] font-black px-2 py-0.5 rounded-full uppercase ${STATUS_STYLE[p.status]}`}>{p.status}</span>
</div>
{p.mensagem && <p className="text-[11px] text-gray-500 font-medium mt-1">{p.mensagem}</p>}
<p className="text-[10px] text-gray-400 font-medium uppercase mt-0.5">{p.profissional_email || ''} · {new Date(p.created_at).toLocaleDateString('pt-BR')}</p>
</div>
{p.status === 'pendente' && (
<button onClick={() => handlePropostaAction(p, 'cancelar')} className="px-3 py-2 rounded-xl border border-gray-200 text-gray-400 text-[10px] font-black uppercase hover:bg-gray-50 transition-colors shrink-0">CANCELAR</button>
)}
</div>
))}
</div>}
</div>
)}
</div>
)
)}
{proposingTo && <PropostaModal prof={proposingTo} onClose={() => setProposingTo(null)} onSent={fetchPropostas} />}
</div>
);
};