feat(procedimentos+menu): aba Procedimentos em Horários + Secretária dentro do WhatsApp
- HorariosConfig: nova aba "Procedimentos" — lista marcável (atende/não atende); ao desmarcar, campos motivo + como falar. GET/PUT /clinica/:id/procedimentos-atende. - Menu: "SECRETÁRIA IA" sai do menu lateral; agora é acessível por um botão (ícone Bot) no header do WhatsApp/Inbox, e o "voltar" da Secretária retorna ao inbox. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
// horários dos dentistas. Consome /api/nw/agenda-config (o backend garante o
|
||||
// ownership: horário/feriado da clínica = dono; horário do dentista = ele ou dono).
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { X, Clock, CalendarOff, Plus, Trash2, Save, Loader2, User, Copy, AlertTriangle } from 'lucide-react';
|
||||
import { X, Clock, CalendarOff, Plus, Trash2, Save, Loader2, User, Copy, AlertTriangle, Check } from 'lucide-react';
|
||||
import { SegmentedTabs } from './SegmentedTabs.tsx';
|
||||
|
||||
const API = (import.meta as any).env?.VITE_API_URL || '/api';
|
||||
@@ -89,8 +89,8 @@ const GradeSemanal: React.FC<{ faixas: Faixa[]; onChange: (f: Faixa[]) => void;
|
||||
);
|
||||
};
|
||||
|
||||
export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; clinicaId?: string; initialTab?: 'clinica' | 'dentistas' | 'situacoes'; soDentista?: boolean }> = ({ isOpen, onClose, clinicaId, initialTab, soDentista }) => {
|
||||
const [aba, setAba] = useState<'clinica' | 'dentistas' | 'situacoes'>(initialTab || 'clinica');
|
||||
export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; clinicaId?: string; initialTab?: 'clinica' | 'dentistas' | 'situacoes' | 'procedimentos'; soDentista?: boolean }> = ({ isOpen, onClose, clinicaId, initialTab, soDentista }) => {
|
||||
const [aba, setAba] = useState<'clinica' | 'dentistas' | 'situacoes' | 'procedimentos'>(initialTab || 'clinica');
|
||||
useEffect(() => { if (isOpen && initialTab) setAba(initialTab); }, [isOpen, initialTab]);
|
||||
const [erro, setErro] = useState<string | null>(null);
|
||||
const [ok, setOk] = useState<string | null>(null);
|
||||
@@ -110,6 +110,7 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl
|
||||
|
||||
// Situações (regras que a Secretária IA segue)
|
||||
const [situacoes, setSituacoes] = useState<string[]>([]);
|
||||
const [procedimentos, setProcedimentos] = useState<Array<{ nome: string; atende: boolean; motivo: string; como_falar: string }>>([]);
|
||||
|
||||
const flash = (setter: (v: string | null) => void, msg: string) => { setter(msg); setTimeout(() => setter(null), 3000); };
|
||||
|
||||
@@ -138,10 +139,20 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl
|
||||
const carregarSituacoes = useCallback(async () => {
|
||||
if (!clinicaId) return;
|
||||
try { const s = await req(`/clinica/${clinicaId}/situacoes`); setSituacoes((s.situacoes || []).map((r: any) => r.texto)); }
|
||||
finally { /* noop */ }
|
||||
}, [clinicaId]);
|
||||
const carregarProcedimentos = useCallback(async () => {
|
||||
if (!clinicaId) return;
|
||||
try { const d = await req(`/clinica/${clinicaId}/procedimentos-atende`); setProcedimentos((d.procedimentos || []).map((r: any) => ({ nome: r.nome, atende: r.atende !== 0 && r.atende !== false, motivo: r.motivo || '', como_falar: r.como_falar || '' }))); }
|
||||
catch (e: any) { flash(setErro, e.message); }
|
||||
}, [clinicaId]);
|
||||
|
||||
useEffect(() => { if (isOpen) { carregarClinica(); carregarDentistas(); if (!soDentista) carregarSituacoes(); } }, [isOpen, carregarClinica, carregarDentistas, carregarSituacoes, soDentista]);
|
||||
useEffect(() => { if (isOpen) { carregarClinica(); carregarDentistas(); if (!soDentista) { carregarSituacoes(); carregarProcedimentos(); } } }, [isOpen, carregarClinica, carregarDentistas, carregarSituacoes, carregarProcedimentos, soDentista]);
|
||||
const salvarProcedimentos = async () => {
|
||||
setSaving(true);
|
||||
try { await req(`/clinica/${clinicaId}/procedimentos-atende`, { method: 'PUT', body: JSON.stringify({ procedimentos: procedimentos.map((p) => ({ nome: p.nome.trim(), atende: p.atende, motivo: p.motivo, como_falar: p.como_falar })).filter((p) => p.nome) }) }); flash(setOk, 'Procedimentos salvos!'); }
|
||||
catch (e: any) { flash(setErro, e.message); } finally { setSaving(false); }
|
||||
};
|
||||
const carregarFolgas = useCallback((id: string) => {
|
||||
req(`/dentista/${id}/folgas`).then((f) => setFolgas(f.folgas || [])).catch(() => setFolgas([]));
|
||||
}, []);
|
||||
@@ -207,7 +218,7 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl
|
||||
{/* Tabs */}
|
||||
<div className="px-8 pt-4">
|
||||
<SegmentedTabs
|
||||
tabs={(soDentista ? (['dentistas'] as const) : (['clinica', 'dentistas', 'situacoes'] as const)).map((t) => ({ id: t, label: t === 'clinica' ? 'Clínica' : t === 'dentistas' ? 'Dentistas' : 'Situações' }))}
|
||||
tabs={(soDentista ? (['dentistas'] as const) : (['clinica', 'dentistas', 'situacoes', 'procedimentos'] as const)).map((t) => ({ id: t, label: t === 'clinica' ? 'Clínica' : t === 'dentistas' ? 'Dentistas' : t === 'situacoes' ? 'Situações' : 'Procedimentos' }))}
|
||||
value={aba} onChange={setAba} />
|
||||
</div>
|
||||
|
||||
@@ -299,7 +310,7 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
) : aba === 'situacoes' ? (
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-black text-gray-800 uppercase tracking-tight flex items-center gap-2"><AlertTriangle size={16} className="text-teal-600" /> Situações e regras da Secretária IA</h3>
|
||||
@@ -323,6 +334,47 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl
|
||||
</div>
|
||||
{situacoes.length === 0 && <p className="text-[11px] text-gray-400 italic">Nenhuma situação cadastrada. A IA seguirá apenas o roteiro padrão.</p>}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-black text-gray-800 uppercase tracking-tight flex items-center gap-2"><AlertTriangle size={16} className="text-teal-600" /> Procedimentos que a clínica atende</h3>
|
||||
<button onClick={salvarProcedimentos} disabled={saving} className="bg-teal-600 hover:bg-teal-700 text-white text-xs font-black uppercase tracking-wide px-4 py-2 rounded-xl flex items-center gap-2 disabled:opacity-50">
|
||||
{saving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />} Salvar
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-500 leading-relaxed bg-amber-50 rounded-2xl p-4">
|
||||
Marque o que a clínica <b>atende</b>. Ao <b>desmarcar</b>, a Secretária IA não vai agendar aquele procedimento — preencha o <b>motivo</b> e o <b>como falar</b> (o que ela deve responder ao paciente).
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{procedimentos.map((p, i) => (
|
||||
<div key={i} className="rounded-2xl border border-gray-100 p-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<button type="button" onClick={() => setProcedimentos(procedimentos.map((x, j) => j === i ? { ...x, atende: !x.atende } : x))}
|
||||
className={`w-6 h-6 rounded-lg flex items-center justify-center border-2 shrink-0 transition-colors ${p.atende ? 'bg-teal-600 border-teal-600 text-white' : 'bg-white border-gray-300 text-transparent'}`}>
|
||||
<Check size={14} />
|
||||
</button>
|
||||
<input value={p.nome} onChange={(e) => setProcedimentos(procedimentos.map((x, j) => j === i ? { ...x, nome: e.target.value } : x))}
|
||||
placeholder="Nome do procedimento"
|
||||
className="flex-1 bg-gray-50 border-2 border-transparent focus:border-teal-600 rounded-xl px-3 py-2 text-sm font-bold text-gray-800 outline-none" />
|
||||
<span className={`text-[10px] font-black uppercase ${p.atende ? 'text-teal-600' : 'text-red-500'}`}>{p.atende ? 'Atende' : 'Não atende'}</span>
|
||||
<button onClick={() => setProcedimentos(procedimentos.filter((_, j) => j !== i))} className="text-gray-300 hover:text-red-600"><Trash2 size={16} /></button>
|
||||
</div>
|
||||
{!p.atende && (
|
||||
<div className="mt-2 pl-9 space-y-2">
|
||||
<input value={p.motivo} onChange={(e) => setProcedimentos(procedimentos.map((x, j) => j === i ? { ...x, motivo: e.target.value } : x))}
|
||||
placeholder="Motivo de não atender (uso interno)"
|
||||
className="w-full bg-gray-50 border-2 border-transparent focus:border-teal-600 rounded-xl px-3 py-2 text-xs font-medium text-gray-700 outline-none" />
|
||||
<textarea value={p.como_falar} rows={2} onChange={(e) => setProcedimentos(procedimentos.map((x, j) => j === i ? { ...x, como_falar: e.target.value } : x))}
|
||||
placeholder="Como a Secretária deve falar isso ao paciente…"
|
||||
className="w-full bg-gray-50 border-2 border-transparent focus:border-teal-600 rounded-xl px-3 py-2 text-xs font-medium text-gray-700 outline-none resize-y" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<button onClick={() => setProcedimentos([...procedimentos, { nome: '', atende: true, motivo: '', como_falar: '' }])} className="bg-gray-900 hover:bg-black text-white text-xs font-black uppercase px-4 py-2.5 rounded-xl flex items-center gap-1"><Plus size={14} /> Novo procedimento</button>
|
||||
</div>
|
||||
{procedimentos.length === 0 && <p className="text-[11px] text-gray-400 italic">Nenhum procedimento na lista. Adicione os que a clínica atende (ou não).</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ activeTab, setActiveTab }) =>
|
||||
if (canSeeInbox) pluginMenuItems.push({ id: 'wa-inbox', label: 'WHATSAPP', icon: MessageCircle, color: '#16a34a' });
|
||||
// INSTÂNCIAS (gestão/QR) só para o dono; os demais operam via Inbox.
|
||||
if (!waAccess || waAccess.isOwner) pluginMenuItems.push({ id: 'wa-sessions', label: 'INSTÂNCIAS', icon: Smartphone, color: '#0ea5e9' });
|
||||
if (canSeeSecretaria) pluginMenuItems.push({ id: 'wa-secretaria', label: 'SECRETÁRIA IA', icon: Bot, color: '#2563eb' });
|
||||
// SECRETÁRIA IA saiu do menu — vive agora como aba DENTRO do WhatsApp (wa-inbox).
|
||||
}
|
||||
// Locação de Salas: menus de LOCADOR (administra as próprias salas) e LOCATÁRIO (aluga de
|
||||
// terceiros). São decisões de NEGÓCIO do titular — não cabem a funcionário (subordinado) nem
|
||||
|
||||
@@ -10,7 +10,10 @@ import {
|
||||
MessageSquarePlus,
|
||||
RotateCw,
|
||||
ArrowLeft,
|
||||
Info,
|
||||
Bot,
|
||||
} from 'lucide-react';
|
||||
import { roleMeta } from './utils/roleMeta';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
@@ -22,11 +25,16 @@ import { useChatFilter } from './hooks/inbox/useChatFilter';
|
||||
import { useChatWindow } from './hooks/inbox/useChatWindow';
|
||||
import { useMessageInput } from './hooks/inbox/useMessageInput';
|
||||
import { nw } from './services/nwClient';
|
||||
import { chatApi } from './services/chatApiService';
|
||||
import { useChatStore } from './store/chatStore';
|
||||
import ForwardModal from './components/ForwardModal';
|
||||
import LabelManagerModal from './components/LabelManagerModal';
|
||||
import { useProtocolPanel } from './hooks/useProtocolPanel';
|
||||
import ChatSidebar from './components/ChatSidebar';
|
||||
import ThinSidebar, { TabType } from './components/ThinSidebar';
|
||||
import InboxHeader from './components/InboxHeader';
|
||||
import MessageArea from './components/MessageArea';
|
||||
import ConversationSearch from './components/ConversationSearch';
|
||||
import SettingsPanel from './components/SettingsPanel';
|
||||
import InputBar from './components/InputBar';
|
||||
import NewConversationModal from './components/NewConversationModal';
|
||||
@@ -63,6 +71,7 @@ export function InboxView({ onNavigate }: { onNavigate?: (view: string) => void
|
||||
handleSendText,
|
||||
handleLoadMoreHistory,
|
||||
handleMenuAction,
|
||||
handleDeleteMessage,
|
||||
} = useNewInboxBridge()
|
||||
|
||||
// ── Filtros de chat (hook dedicado) ───────────────────────────────────────
|
||||
@@ -81,7 +90,6 @@ export function InboxView({ onNavigate }: { onNavigate?: (view: string) => void
|
||||
// ── Chat window (hook dedicado) ───────────────────────────────────────────
|
||||
const {
|
||||
scrollRef,
|
||||
isTyping,
|
||||
replyingTo,
|
||||
reactionPickerFor,
|
||||
hasMoreHistory,
|
||||
@@ -100,20 +108,45 @@ export function InboxView({ onNavigate }: { onNavigate?: (view: string) => void
|
||||
handleSendAudio,
|
||||
handleSyncAvatar,
|
||||
toggleReactionPicker,
|
||||
handleClearConversation,
|
||||
editingMessage,
|
||||
setEditingMessage,
|
||||
forwardingMessage,
|
||||
setForwardingMessage,
|
||||
} = useChatWindow({
|
||||
selectedChat,
|
||||
activeInstanceId: activeInstance?.instance ?? null,
|
||||
onLoadMoreHistory: handleLoadMoreHistory,
|
||||
})
|
||||
|
||||
// "digitando..." do CONTATO (presença composing/recording do chat aberto).
|
||||
const activePresence = selectedChat?.remote_jid ? presenceByJid[selectedChat.remote_jid] : undefined
|
||||
const isTyping = !!activePresence && (Date.now() - activePresence.ts <= 8000) && (activePresence.state === 'composing' || activePresence.state === 'recording')
|
||||
|
||||
// ── Mensagem de entrada (hook dedicado) ───────────────────────────────────
|
||||
const { newMessage, setNewMessage, mentions, setMentions, handleSendMessage } = useMessageInput({
|
||||
selectedChat,
|
||||
replyingTo,
|
||||
onClearReply: () => setReplyingTo(null),
|
||||
onSendText: handleSendText,
|
||||
editingMessage,
|
||||
onClearEdit: () => setEditingMessage(null),
|
||||
onEditText: async (messageId, text) => {
|
||||
if (!selectedChat?.id) return
|
||||
try {
|
||||
await chatApi.editMessage(String(selectedChat.id), messageId, text)
|
||||
useChatStore.getState().patchMessage(messageId, { body: text })
|
||||
} catch (e) { console.error('Erro ao editar:', e) }
|
||||
},
|
||||
})
|
||||
|
||||
// Entra em modo edição: preenche o input com o texto atual da mensagem.
|
||||
const handleEnterEdit = useCallback((msg: any) => {
|
||||
setEditingMessage(msg)
|
||||
setReplyingTo(null)
|
||||
setNewMessage(msg?.text ?? msg?.body ?? '')
|
||||
}, [setEditingMessage, setReplyingTo, setNewMessage])
|
||||
|
||||
// ── Painel de Protocolo ───────────────────────────────────────────────────
|
||||
const [isProtocolOpen, setIsProtocolOpen] = useState(false)
|
||||
const protocolPanel = useProtocolPanel(selectedChat ? String(selectedChat.id) : null)
|
||||
@@ -121,6 +154,10 @@ export function InboxView({ onNavigate }: { onNavigate?: (view: string) => void
|
||||
// ── Estado local de UI ────────────────────────────────────────────────────
|
||||
const [activeTab, setActiveTab] = useState<TabType>('chats')
|
||||
const [isInstanceDropdownOpen, setIsInstanceDropdownOpen] = useState(false)
|
||||
const [isRoleInfoOpen, setIsRoleInfoOpen] = useState(false)
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const roleInfoRef = useRef<HTMLDivElement>(null)
|
||||
const [isOptionsMenuOpen, setIsOptionsMenuOpen] = useState(false)
|
||||
const [drafts] = useState<Record<string, string>>({})
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
@@ -128,9 +165,12 @@ export function InboxView({ onNavigate }: { onNavigate?: (view: string) => void
|
||||
|
||||
// Permissão de apagar conversa/mensagem (dono ou liberado). Fail-open enquanto carrega.
|
||||
const [canDeleteMsg, setCanDeleteMsg] = useState(true)
|
||||
// "Limpar conversa" é só do DONO do workspace. Fail-closed: só aparece se confirmado dono.
|
||||
const [isWaOwner, setIsWaOwner] = useState(false)
|
||||
const [labelModalOpen, setLabelModalOpen] = useState(false)
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
HybridBackend.getWaAccess().then(a => { if (alive) setCanDeleteMsg(a.isOwner || a.delete_msg) }).catch(() => {})
|
||||
HybridBackend.getWaAccess().then(a => { if (alive) { setCanDeleteMsg(a.isOwner || a.delete_msg); setIsWaOwner(!!a.isOwner); useChatStore.getState().setOperatorSignature(a.signature ?? null) } }).catch(() => {})
|
||||
return () => { alive = false }
|
||||
}, [])
|
||||
|
||||
@@ -171,6 +211,7 @@ export function InboxView({ onNavigate }: { onNavigate?: (view: string) => void
|
||||
resize()
|
||||
const click = (e: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) setIsInstanceDropdownOpen(false)
|
||||
if (roleInfoRef.current && !roleInfoRef.current.contains(e.target as Node)) setIsRoleInfoOpen(false)
|
||||
if (optionsRef.current && !optionsRef.current.contains(e.target as Node)) setIsOptionsMenuOpen(false)
|
||||
}
|
||||
document.addEventListener('mousedown', click)
|
||||
@@ -284,10 +325,12 @@ export function InboxView({ onNavigate }: { onNavigate?: (view: string) => void
|
||||
{isInstanceDropdownOpen && (
|
||||
<motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }}
|
||||
className="absolute left-0 top-full bg-white shadow-2xl rounded-xl border border-gray-100 w-72 z-50 py-2">
|
||||
{instances.map(i => (
|
||||
{instances.map(i => {
|
||||
const rm = roleMeta(i.role)
|
||||
return (
|
||||
<button key={i.instance} onClick={() => { selectInstance(i); setIsInstanceDropdownOpen(false) }}
|
||||
className="w-full p-3 hover:bg-gray-50 flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-gray-100 overflow-hidden">
|
||||
<div className="w-8 h-8 rounded-full bg-gray-100 overflow-hidden shrink-0">
|
||||
<img
|
||||
src={getAvatarProxyUrl({
|
||||
instance_name: i.instance,
|
||||
@@ -297,12 +340,71 @@ export function InboxView({ onNavigate }: { onNavigate?: (view: string) => void
|
||||
onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = 'none' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<span className="text-sm font-medium block">{i.nome}</span>
|
||||
<span className={`text-xs ${i.status === 'connected' ? 'text-green-500' : 'text-gray-400'}`}>{i.status}</span>
|
||||
<div className="text-left min-w-0 flex-1">
|
||||
<span className="text-sm font-medium block truncate">{i.nome}</span>
|
||||
<span className="text-[11px] flex items-center gap-1 truncate" style={{ color: rm.color }}>
|
||||
<span className="w-1.5 h-1.5 rounded-full inline-block shrink-0" style={{ background: rm.color }} />
|
||||
{rm.label}{!i.own && i.ownerNome ? ` · ${i.ownerNome}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
{!i.own && (
|
||||
<span className="text-[9px] font-bold uppercase tracking-wide text-brand-600 bg-brand-500/10 px-1.5 py-0.5 rounded shrink-0">clínica</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Secretária IA — aba dentro do WhatsApp (saiu do menu lateral). */}
|
||||
<button
|
||||
onClick={() => onNavigate?.('wa-secretaria')}
|
||||
className="p-1.5 rounded-full text-[#8696a0] hover:bg-black/5 hover:text-[#2563eb] transition-colors"
|
||||
title="Secretária IA"
|
||||
>
|
||||
<Bot className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* Ícone (i): legenda dos papéis de cada número acessível */}
|
||||
<div className="relative" ref={roleInfoRef}>
|
||||
<button
|
||||
onClick={() => setIsRoleInfoOpen(o => !o)}
|
||||
className={`p-1.5 rounded-full transition-colors ${isRoleInfoOpen ? 'bg-black/10 text-[#111b21]' : 'text-[#8696a0] hover:bg-black/5 hover:text-[#54656f]'}`}
|
||||
title="O que é cada número?"
|
||||
>
|
||||
<Info className="w-4 h-4" />
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{isRoleInfoOpen && (
|
||||
<motion.div initial={{ opacity: 0, y: -8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -8 }}
|
||||
className="absolute left-0 top-full mt-1 bg-white shadow-2xl rounded-xl border border-gray-100 w-80 z-50 p-3">
|
||||
<p className="text-[11px] font-bold uppercase tracking-wide text-gray-400 px-1 pb-2">Seus números</p>
|
||||
<div className="space-y-2 max-h-[60vh] overflow-y-auto">
|
||||
{instances.length === 0 && (
|
||||
<p className="text-xs text-gray-400 px-1 py-2">Nenhum número disponível.</p>
|
||||
)}
|
||||
{instances.map(i => {
|
||||
const rm = roleMeta(i.role)
|
||||
const desc = [i.area, i.notes].filter(Boolean).join(' · ')
|
||||
return (
|
||||
<div key={i.instance} className="flex items-start gap-2.5 px-1">
|
||||
<span className="w-2 h-2 rounded-full mt-1.5 shrink-0" style={{ background: rm.color }} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[13px] font-semibold text-[#111b21] truncate">{i.nome}</span>
|
||||
{!i.own && <span className="text-[9px] font-bold uppercase text-brand-600 bg-brand-500/10 px-1 py-0.5 rounded shrink-0">clínica</span>}
|
||||
</div>
|
||||
<p className="text-[11px] font-medium" style={{ color: rm.color }}>{rm.label}</p>
|
||||
{i.phone && <p className="text-[11px] text-gray-500">{i.phone}</p>}
|
||||
{desc && <p className="text-[11px] text-gray-400 leading-snug">{desc}</p>}
|
||||
{!i.own && i.ownerNome && <p className="text-[10px] text-gray-400">caixa de {i.ownerNome}</p>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
@@ -393,10 +495,22 @@ export function InboxView({ onNavigate }: { onNavigate?: (view: string) => void
|
||||
isSyncingAvatar={isSyncingAvatar}
|
||||
isProtocolOpen={isProtocolOpen}
|
||||
hasActiveProtocol={protocolPanel.hasActive}
|
||||
onSearchClick={() => {}}
|
||||
isOwner={isWaOwner}
|
||||
onClearConversation={handleClearConversation}
|
||||
onManageLabels={() => setLabelModalOpen(true)}
|
||||
onSearchClick={() => setIsSearchOpen(true)}
|
||||
onToggleMenu={() => setIsChatMenuOpen(!isChatMenuOpen)}
|
||||
onProtocolClick={() => { setIsProtocolOpen(!isProtocolOpen); setIsProfileOpen(false) }}
|
||||
/>
|
||||
{isSearchOpen && (
|
||||
<ConversationSearch
|
||||
messages={messages as any}
|
||||
onClose={() => setIsSearchOpen(false)}
|
||||
onQueryChange={setSearchQuery}
|
||||
hasOlder={hasMoreHistory}
|
||||
loadOlder={async () => { const r = await handleLoadMoreHistory(); return !!r?.hasMore; }}
|
||||
/>
|
||||
)}
|
||||
<MessageArea
|
||||
messages={messages as any}
|
||||
selectedChat={selectedChat}
|
||||
@@ -405,14 +519,15 @@ export function InboxView({ onNavigate }: { onNavigate?: (view: string) => void
|
||||
onScroll={handleScroll}
|
||||
historyLoadingMore={loadingMore}
|
||||
hasMoreHistory={hasMoreHistory}
|
||||
highlightTerm={isSearchOpen ? searchQuery : ''}
|
||||
reactionPickerFor={reactionPickerFor}
|
||||
onReply={setReplyingTo}
|
||||
onToggleReactionPicker={toggleReactionPicker}
|
||||
onReact={handleReactToMessage}
|
||||
onForward={() => {}}
|
||||
onDelete={() => {}}
|
||||
onForward={(msg: any) => setForwardingMessage(msg)}
|
||||
onDelete={(msg: any) => { if (selectedChat && msg?.id) handleDeleteMessage(String(selectedChat.id), String(msg.id)); }}
|
||||
canDelete={canDeleteMsg}
|
||||
onEdit={() => {}}
|
||||
onEdit={handleEnterEdit}
|
||||
quickReactions={['👍', '❤️', '😂', '😮', '😢', '🙏']}
|
||||
getGroupSenderLabel={(msg: any) => {
|
||||
if (msg.direction === 'out') return 'Você'
|
||||
@@ -432,6 +547,8 @@ export function InboxView({ onNavigate }: { onNavigate?: (view: string) => void
|
||||
onPresence={handleTyping}
|
||||
onCancelReply={() => setReplyingTo(null)}
|
||||
replyingTo={replyingTo}
|
||||
editingMessage={editingMessage}
|
||||
onCancelEdit={() => { setEditingMessage(null); setNewMessage('') }}
|
||||
selectedChat={selectedChat}
|
||||
isMobile={isMobile}
|
||||
onSendMedia={handleSendMedia}
|
||||
@@ -479,6 +596,26 @@ export function InboxView({ onNavigate }: { onNavigate?: (view: string) => void
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<LabelManagerModal
|
||||
isOpen={labelModalOpen}
|
||||
onClose={() => setLabelModalOpen(false)}
|
||||
chat={selectedChat as any}
|
||||
onChanged={() => { if (activeInstance?.instance) useChatStore.getState().loadChats(activeInstance.instance) }}
|
||||
/>
|
||||
|
||||
<ForwardModal
|
||||
isOpen={!!forwardingMessage}
|
||||
onClose={() => setForwardingMessage(null)}
|
||||
chats={chatsByScope as any}
|
||||
onForward={async (toChatIds: string[]) => {
|
||||
if (!forwardingMessage || !selectedChat?.id) return
|
||||
try {
|
||||
await chatApi.forward(String(selectedChat.id), String((forwardingMessage as any).id), toChatIds)
|
||||
} catch (e) { console.error('Erro ao encaminhar:', e) }
|
||||
setForwardingMessage(null)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1722,7 +1722,7 @@ export function SecretariaView(props: { onNavigate?: (view: string) => void } =
|
||||
<div className="flex h-full w-full bg-gray-50 overflow-hidden font-sans">
|
||||
|
||||
{/* Thin Nav */}
|
||||
<ThinNav active={activeTab} onChange={handleTabChange} onHelp={() => setShowHelp(true)} onBack={() => props.onNavigate?.('dashboard')}
|
||||
<ThinNav active={activeTab} onChange={handleTabChange} onHelp={() => setShowHelp(true)} onBack={() => props.onNavigate?.('wa-inbox')}
|
||||
onPendencias={ehDono ? () => setShowPendencias(true) : undefined} pendenciasBadge={pendCount}
|
||||
secOn={secOn} onTogglePower={openPowerModal} />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user