feat(protese): Modo 1 fatia 1c — gestão do link na UI da clínica (status/acessos/revogar)
Fecha o Modo 1 (link seguro do protético sem conta): - GET /api/protese/os/:id/link/status: link ativo, nº de acessos, último acesso e expiração. - UI (aba Monitoramento, modo clínica): mostra status do link com Copiar / WhatsApp / Revogar e os contadores de acesso/expiração; gera sob demanda. Conversão por herança é nativa (a OS já aponta para o protetico_id do destinatário). Validado em DEV: acessos contados, expiração 60 dias, revogação → status inativo + leitura pública 404. Modo 1 concluído (1a leitura + 1b ação + 1c gestão). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -558,31 +558,41 @@ const OcorrenciasSecao: React.FC<{ os: any; onChanged: () => void }> = ({ os, on
|
||||
// ── Seção: Compartilhar link de acompanhamento com o protético (Modo 1) ─────
|
||||
const CompartilharLinkSecao: React.FC<{ os: any }> = ({ os }) => {
|
||||
const toast = useToast();
|
||||
const [url, setUrl] = useState('');
|
||||
const confirm = useConfirm();
|
||||
const [st, setSt] = useState<any>(null); // null=carregando | {ativo,...}
|
||||
const [busy, setBusy] = useState(false);
|
||||
const gerar = async () => {
|
||||
const carregar = useCallback(() => HybridBackend.getProteseLinkStatus(os.id).then(setSt).catch(() => setSt({ ativo: false })), [os.id]);
|
||||
useEffect(() => { carregar(); }, [carregar]);
|
||||
const acao = async (fn: () => Promise<any>, ok: string) => {
|
||||
setBusy(true);
|
||||
try { const r = await HybridBackend.gerarProteseLink(os.id); setUrl(r.url); }
|
||||
try { await fn(); await carregar(); toast.success(ok); }
|
||||
catch (e: any) { toast.error((e.message || 'ERRO').toUpperCase()); } finally { setBusy(false); }
|
||||
};
|
||||
const copiar = async () => { try { await navigator.clipboard.writeText(url); toast.success('LINK COPIADO.'); } catch { toast.error('NÃO FOI POSSÍVEL COPIAR.'); } };
|
||||
const waText = encodeURIComponent(`Acompanhe a prótese (${os.tipo}) do paciente ${os.paciente_nome || ''}: ${url}`);
|
||||
const copiar = async () => { try { await navigator.clipboard.writeText(st.url); toast.success('LINK COPIADO.'); } catch { toast.error('NÃO FOI POSSÍVEL COPIAR.'); } };
|
||||
const revogar = async () => { if (await confirm('REVOGAR O LINK? O PROTÉTICO PERDE O ACESSO.')) acao(() => HybridBackend.revogarProteseLink(os.id), 'LINK REVOGADO.'); };
|
||||
const waText = st?.url ? encodeURIComponent(`Acompanhe a prótese (${os.tipo}) do paciente ${os.paciente_nome || ''}: ${st.url}`) : '';
|
||||
return (
|
||||
<div className="border-t border-gray-100 pt-4 space-y-2">
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase flex items-center gap-1.5"><Share2 size={12} /> Acompanhamento do protético</p>
|
||||
{!url ? (
|
||||
{!st ? (
|
||||
<p className="text-[11px] text-gray-300 uppercase font-bold">Carregando…</p>
|
||||
) : !st.ativo ? (
|
||||
<>
|
||||
<p className="text-[11px] text-gray-400">Gere um link para o protético acompanhar esta OS <b>sem precisar de conta</b>.</p>
|
||||
<button disabled={busy} onClick={gerar} className="px-3 py-2 rounded-lg bg-[#2d6a4f] text-white text-[11px] font-black uppercase disabled:opacity-50 flex items-center gap-1.5"><Share2 size={13} /> Gerar link de acompanhamento</button>
|
||||
<button disabled={busy} onClick={() => acao(() => HybridBackend.gerarProteseLink(os.id), 'LINK GERADO.')} className="px-3 py-2 rounded-lg bg-[#2d6a4f] text-white text-[11px] font-black uppercase disabled:opacity-50 flex items-center gap-1.5"><Share2 size={13} /> Gerar link de acompanhamento</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<input readOnly value={url} onClick={e => e.currentTarget.select()} className="w-full p-2 bg-gray-50 border border-gray-100 rounded-lg text-[11px] text-gray-600 outline-none" />
|
||||
<input readOnly value={st.url} onClick={e => e.currentTarget.select()} className="w-full p-2 bg-gray-50 border border-gray-100 rounded-lg text-[11px] text-gray-600 outline-none" />
|
||||
<div className="flex gap-2">
|
||||
<a href={`https://wa.me/?text=${waText}`} target="_blank" rel="noreferrer" className="flex-1 py-2 rounded-lg bg-green-600 text-white text-[11px] font-black uppercase text-center">WhatsApp</a>
|
||||
<button onClick={copiar} className="flex-1 py-2 rounded-lg border border-gray-200 text-gray-600 text-[11px] font-black uppercase">Copiar</button>
|
||||
</div>
|
||||
<p className="text-[9px] text-gray-400 uppercase">Somente leitura · expira em 60 dias · sem CPF nem valores do paciente.</p>
|
||||
<div className="flex items-center justify-between text-[9px] font-bold text-gray-400 uppercase">
|
||||
<span>{st.acessos || 0} acesso(s){st.last_access_at ? ` · último ${dataBR(st.last_access_at)}` : ' · ainda não aberto'}</span>
|
||||
<button disabled={busy} onClick={revogar} className="text-red-500 hover:text-red-600 disabled:opacity-50">Revogar</button>
|
||||
</div>
|
||||
<p className="text-[9px] text-gray-400 uppercase">{st.expires_at ? `Expira em ${dataBR(st.expires_at)} · ` : ''}sem CPF nem valores do paciente.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user