feat(comunicacao-interna): notificação de número compartilhado + agenda de contatos

Opção (b) — mesma pessoa/número servindo a papéis diferentes, roteando por situação:
- GET /api/comunicacao/numero-lookup: ao digitar um número, avisa se ele já é de outra
  conta/papel da clínica (ex.: número na conta dentista aparece ao editar a de dono).
  Modal mostra banner "pode compartilhar — descreva as situações; a Secretária roteia".
- Bridge GET /contatos: agenda de contatos internos (nome, papel, situações de cada
  número, janela atual) p/ a Secretária rotear por assunto. Não expõe o número cru.

Verificado E2E: lookup como dono acha o nº na conta dentista; /contatos lista papel+situações.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-07-07 22:48:42 +02:00
parent eafc1578e3
commit b253feb4aa
3 changed files with 81 additions and 1 deletions
@@ -106,6 +106,20 @@ export const ComunicacaoInternaModal: React.FC<{
const updNum = (i: number, patch: Partial<Numero>) => setNumeros(ns => ns.map((n, k) => (k === i ? { ...n, ...patch } : n)));
const addNum = () => setNumeros(ns => [...ns, { numero: '', descricao: '', situacoes: '', janelas: [{ dias: [1, 2, 3, 4, 5], inicio: '08:00', fim: '18:00' }] }]);
// Notificação de número compartilhado: ao sair do campo, verifica se o número já é de
// outra conta/papel da clínica (a mesma pessoa pode ser dono E dentista, por ex.).
const [avisos, setAvisos] = useState<Record<number, { nome: string; role: string }[]>>({});
const roleLabel = (r: string) => (({ dentista: 'dentista', donoclinica: 'dono', donoconsultorio: 'dono', funcionario: 'funcionário', admin: 'admin' } as Record<string, string>)[r] || r || 'conta');
const checkNumero = async (i: number, numero: string) => {
const tel = (numero || '').replace(/\D/g, '');
if (tel.length < 10 || !clinicaId) { setAvisos(a => ({ ...a, [i]: [] })); return; }
try {
const r = await api(`/api/comunicacao/numero-lookup?clinicaId=${encodeURIComponent(clinicaId)}&numero=${encodeURIComponent(tel)}`);
const d = await r.json();
setAvisos(a => ({ ...a, [i]: Array.isArray(d.encontrados) ? d.encontrados : [] }));
} catch { /* silencioso */ }
};
// Completude (espelha o backend) — orienta o preenchimento e destrava o gate.
const enderecoOk = !!(endereco.cidade && endereco.estado && (endereco.cep || endereco.logradouro));
const numerosOk = numeros.length >= 1 && numeros.every(n => (n.numero || '').replace(/\D/g, '').length >= 10 && (n.descricao || '').trim() && (n.janelas?.length ?? 0) >= 1);
@@ -209,7 +223,7 @@ export const ComunicacaoInternaModal: React.FC<{
<div className="flex-1 grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className={label}>Número</label>
<input className={input} value={n.numero} onChange={e => updNum(i, { numero: e.target.value })} placeholder="(67) 99999-9999" />
<input className={input} value={n.numero} onChange={e => updNum(i, { numero: e.target.value })} onBlur={e => checkNumero(i, e.target.value)} placeholder="(67) 99999-9999" />
</div>
<div>
<label className={label}>Descrição</label>
@@ -218,6 +232,12 @@ export const ComunicacaoInternaModal: React.FC<{
</div>
<button type="button" onClick={() => setNumeros(ns => ns.filter((_, k) => k !== i))} className="p-2 text-gray-300 hover:text-red-500 mt-5"><Trash2 size={16} /></button>
</div>
{(avisos[i]?.length ?? 0) > 0 && (
<div className="rounded-xl bg-amber-50 border border-amber-200 p-2.5 text-[11px] text-amber-700 font-medium flex gap-2">
<AlertTriangle size={14} className="shrink-0 mt-0.5 text-amber-500" />
<span>Este número está cadastrado por <b>{avisos[i].map(x => `${x.nome} (${roleLabel(x.role)})`).join(', ')}</b>. Pode compartilhar o mesmo número descreva abaixo as situações deste papel; a Secretária identifica pelo assunto quem deve responder.</span>
</div>
)}
<div>
<label className={label}>Situações em que a Secretária pode mandar msg neste número</label>
<textarea className={input} rows={2} value={n.situacoes} onChange={e => updNum(i, { situacoes: e.target.value })} placeholder="Ex.: só confirmações de encaixe e avisos urgentes; nunca assuntos pessoais." />