feat(newwhats): ownership de sessões, auto-provisionamento, assinatura e drawer na agenda

- Drawer de WhatsApp focado (WhatsChatDrawer) aberto do slot do agendamento, com
  dropdown de números do paciente + grupo familiar e fluxo de iniciar conversa
- Auto-provisionamento de conta no motor: eager no cadastro/convite + self-heal no
  proxy (404 "conta não encontrada" → provisiona e refaz o request)
- Ownership por sessão (wa_session_authz): dono do workspace (owner_id ou vínculo
  donoclinica/donoconsultorio) tem poder total; delega re-scan, excluir sessão,
  apagar conversa/mensagem, acesso ao Inbox e à Secretária — por conta
- Enforcement no proxy: guardSessionAction (scan/rescan/delete sessão),
  guardAreaAccess (inbox/secretaria), guardInboxDestructive (apagar conversa/msg)
- Endpoints /api/nw/authz (dono gerencia) e /api/nw/access (acesso agregado)
- Assinatura do operador nas mensagens (*Nome*, desambiguação de homônimos → *Rui C.*)
- UI: painel "Gerenciar acesso" com 5 toggles; menu e botões de apagar condicionados
- Proxy de mídia /api/nw/media + re-download sob demanda

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-07-02 23:26:31 +02:00
parent 372431c0a1
commit 21bebd3256
17 changed files with 1265 additions and 57 deletions
@@ -10,6 +10,7 @@ import { ptBR } from 'date-fns/locale';
import { normalizeJid, parseSafeDate } from '../utils/inboxUtils';
import { Message } from '../types/inboxTypes';
import { mediaApi } from '../services/chatApiService';
import { useChatStore } from '../store/chatStore';
// Message interface removed and moved to types.ts
@@ -31,6 +32,7 @@ interface MessageAreaProps {
onEdit?: (msg: Message) => void;
quickReactions: string[];
getGroupSenderLabel: (msg: Message) => string;
canDelete?: boolean;
}
const MessageArea: React.FC<MessageAreaProps> = ({
@@ -51,6 +53,7 @@ const MessageArea: React.FC<MessageAreaProps> = ({
onEdit,
quickReactions,
getGroupSenderLabel,
canDelete = true,
}) => {
const [showScrollBottom, setShowScrollBottom] = useState(false);
const lastScrollTop = useRef(0);
@@ -60,12 +63,17 @@ const MessageArea: React.FC<MessageAreaProps> = ({
const [viewerMsgId, setViewerMsgId] = useState<string | null>(null)
// ── Re-download de mídia não baixada ─────────────────────────────────────
// ~95% das mídias vêm com mediaUrl NULL (lazy). O motor re-baixa do WhatsApp
// e devolve { mediaUrl }; atualizamos o store direto pelo retorno (o WS
// message:media_ready não trafega pela ponte ext do satélite).
const updateMediaReady = useChatStore((s) => s.updateMediaReady)
const handleRedownloadMedia = useCallback(async (msg: Message) => {
// selectedChat no formato legado usa instance_name (não instanceId)
const instanceId = selectedChat?.instance_name ?? selectedChat?.instanceId
if (!instanceId || !msg.id) return
await mediaApi.redownload(instanceId, String(msg.id))
}, [selectedChat?.instance_name, selectedChat?.instanceId])
const { mediaUrl } = await mediaApi.redownload(instanceId, String(msg.id))
if (mediaUrl) updateMediaReady(String(msg.id), mediaUrl)
}, [selectedChat?.instance_name, selectedChat?.instanceId, updateMediaReady])
const mediaMessages = messages.filter(m => m.type === 'image' || m.type === 'video')
const handleInternalScroll = useCallback(() => {
@@ -204,6 +212,7 @@ const MessageArea: React.FC<MessageAreaProps> = ({
onReact={onReact}
onForward={onForward}
onDelete={onDelete}
canDelete={canDelete}
onStar={onStar}
onEdit={onEdit}
quickReactions={quickReactions}