Files
scoreodonto.com/frontend/views/newwhats/components/InboxHeader.tsx
T
VPS 4 Builder 89588c2674 feat(wa-inbox): nome do paciente, fila, power por chat, agenda, secretária
- Nome do paciente (base) no lugar do número/pushname; multi-paciente → titular.
- Aba Fila (pedidos da Secretária) + selo pendente; botão de pedidos e overlay
  da agenda no header do chat.
- Power da Secretária: por sessão (fail-closed, só dono) e override por CHAT
  (ChatSecretariaPower); botão de pausa reflete o estado efetivo (AiPauseButton).
- secretariaApi/chatApiService: chat-power + handoff com sec_enabled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 06:07:13 +02:00

222 lines
10 KiB
TypeScript

'use client';
import React from 'react';
import {
ArrowLeft,
Search,
MoreVertical,
RefreshCw,
Tag,
X,
ClipboardList,
CalendarClock,
CalendarDays,
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import {
formatPhone,
} from '../utils/inboxUtils';
import Avatar from './ui/Avatar';
import { ChatSecretariaPower } from './ChatSecretariaPower';
interface InboxHeaderProps {
selectedChat: any;
isMobile: boolean;
isTyping: boolean;
isChatMenuOpen: boolean;
isSyncingAvatar: boolean;
isProtocolOpen?: boolean;
hasActiveProtocol?: boolean;
isOwner?: boolean; // só o dono do workspace vê "Limpar conversa" (ação destrutiva). Fail-closed.
nomeNosso?: string; // nome da NOSSA base (paciente) — precede o pushname do WhatsApp
activeInstanceId?: string | null; // instância atual (p/ override da Secretária por chat)
pedidosCount?: number; // badge do botão de fila (0 = sem badge)
onOpenPedidos?: () => void; // abre o modal "Pedidos aguardando confirmação"
onOpenAgenda?: () => void; // abre a agenda (overlay) sem sair do inbox
onClearConversation?: () => void;
onManageLabels?: () => void;
onBack: () => void;
onSearchClick: () => void;
onToggleMenu: () => void;
onSyncAvatar: () => void;
onAvatarClick?: () => void;
onProtocolClick?: () => void;
}
const InboxHeader: React.FC<InboxHeaderProps> = ({
selectedChat,
isMobile,
isTyping,
isChatMenuOpen,
isSyncingAvatar,
isProtocolOpen,
hasActiveProtocol,
isOwner,
nomeNosso,
activeInstanceId,
pedidosCount = 0,
onOpenPedidos,
onOpenAgenda,
onClearConversation,
onManageLabels,
onBack,
onSearchClick,
onToggleMenu,
onSyncAvatar,
onAvatarClick,
onProtocolClick,
}) => {
return (
<header className="h-[60px] px-4 bg-[#f0f2f5] flex items-center justify-between border-b border-[#d1d7db] z-30 relative shrink-0">
<div className="flex items-center gap-3 flex-1 min-w-0">
{isMobile && (
<button
onClick={onBack}
className="p-1 -ml-1 mr-1 hover:bg-black/5 rounded-full transition-colors active:scale-90"
>
<ArrowLeft className="w-6 h-6 text-[#54656f]" />
</button>
)}
{/* Identity Area - Clickable */}
<div
onClick={onAvatarClick}
className="flex items-center gap-3 min-w-0 cursor-pointer group/header hover:bg-black/5 px-2 py-1 -ml-2 rounded-md transition-all active:scale-[0.98]"
>
{/* Avatar */}
<Avatar chat={selectedChat} size={40} className="flex-shrink-0" />
<div className="flex flex-col min-w-0">
<h3 className="text-[16px] leading-[21px] font-bold text-[#111b21] flex items-center gap-2 truncate group-hover/header:text-[#00a884] transition-colors">
{nomeNosso || selectedChat.lead_name || selectedChat.subject || formatPhone(selectedChat.phone, selectedChat.remote_jid)}
</h3>
<div className="flex items-center gap-1 min-w-0">
{isTyping ? (
<span className="text-[13px] text-[#00a884] font-medium animate-pulse">
digitando...
</span>
) : (
<span className="text-[12.5px] text-[#667781] truncate max-w-[300px] font-normal">
{selectedChat.bio || (
selectedChat.remote_jid?.endsWith('@g.us')
? `Clique para dados do grupo`
: 'Clique para informações do contato'
)}
</span>
)}
</div>
</div>
</div>
</div>
<div className="flex items-center gap-1">
{/* Secretária IA só neste chat (override do dono, precedência máxima) */}
<ChatSecretariaPower instanceId={activeInstanceId ?? null} jid={selectedChat?.remote_jid ?? null} canPower={isOwner} />
{/* Agenda — abre a agenda (overlay) p/ conferir dias/horários vagos sem sair do chat */}
{onOpenAgenda && (
<button
onClick={onOpenAgenda}
className="p-2 rounded-full transition-all text-[#54656f] hover:bg-black/5 hover:text-[#128C7E]"
title="Abrir agenda"
>
<CalendarDays className="w-5 h-5" />
</button>
)}
{/* Fila da Secretária — modal "Pedidos aguardando confirmação" (mesmo da agenda) */}
{onOpenPedidos && (
<button
onClick={onOpenPedidos}
className="relative p-2 rounded-full transition-all text-[#54656f] hover:bg-black/5 hover:text-amber-600"
title="Pedidos aguardando confirmação"
>
<CalendarClock className="w-5 h-5" />
{pedidosCount > 0 && (
<span className="absolute top-1 right-1 min-w-[15px] h-[15px] px-1 rounded-full bg-amber-500 text-white text-[9px] font-black flex items-center justify-center pointer-events-none">
{pedidosCount > 99 ? '99+' : pedidosCount}
</span>
)}
</button>
)}
{onProtocolClick && (
<button
onClick={onProtocolClick}
className={`p-2 rounded-full transition-all relative ${isProtocolOpen ? 'bg-black/10 text-[#111b21]' : 'hover:bg-black/5 text-[#54656f] hover:text-[#111b21]'}`}
title="Protocolos de atendimento"
>
<ClipboardList className="w-5 h-5" />
{hasActiveProtocol && !isProtocolOpen && (
<span className="absolute top-1.5 right-1.5 w-2 h-2 rounded-full bg-brand-500 border border-white" />
)}
</button>
)}
<button
onClick={onSearchClick}
className="p-2 hover:bg-black/5 rounded-full transition-all text-[#54656f] hover:text-[#111b21]"
title="Procurar na conversa"
>
<Search className="w-5 h-5" />
</button>
<div className="relative">
<button
onClick={onToggleMenu}
className={`p-2 rounded-full transition-all ${isChatMenuOpen ? 'bg-black/10 text-[#111b21]' : 'hover:bg-black/5 text-[#54656f]'}`}
>
<MoreVertical className="w-5 h-5" />
</button>
<AnimatePresence>
{isChatMenuOpen && (
<>
{/* Backdrop: fecha o menu ao clicar fora */}
<div className="fixed inset-0 z-40" onClick={onToggleMenu} />
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 10 }}
className="absolute right-0 top-12 w-56 bg-white rounded-lg shadow-2xl border border-gray-100 py-2 z-50 overflow-hidden"
>
<button
onClick={() => {
onSyncAvatar();
onToggleMenu();
}}
disabled={isSyncingAvatar}
className="w-full flex items-center gap-3 px-4 py-2.5 text-[14px] text-[#111b21] hover:bg-[#f5f6f6] transition-colors disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 ${isSyncingAvatar ? 'animate-spin' : ''}`} />
Sincronizar Foto
</button>
<button
onClick={() => { onToggleMenu(); onManageLabels?.(); }}
className="w-full flex items-center gap-3 px-4 py-2.5 text-[14px] text-[#111b21] hover:bg-[#f5f6f6] transition-colors"
>
<Tag className="w-4 h-4" />
Gerenciar etiquetas
</button>
{/* Ação destrutiva — só o dono do workspace (dentista/protético/
biomédico dono da clínica/consultório). Secretária/agente não vê. */}
{isOwner && (
<>
<div className="border-t border-gray-100 my-1"></div>
<button
onClick={() => { onToggleMenu(); onClearConversation?.(); }}
className="w-full flex items-center gap-3 px-4 py-2.5 text-[14px] font-bold text-red-600 hover:bg-red-50 transition-colors"
>
<X className="w-4 h-4" />
Limpar conversa
</button>
</>
)}
</motion.div>
</>
)}
</AnimatePresence>
</div>
</div>
</header>
);
};
export default InboxHeader;