feat(secretaria): agente/cérebro por número na aba Números

Adiciona o seletor "Agente / Cérebro deste número" no NumbersPanel: cada
número de WhatsApp passa a apontar para um agente (sec_numbers.agent_id),
tornando a Secretária, os nós do cérebro e a agenda separados POR SESSÃO.
Mostra o cérebro vinculado em cada card; tipos SecNumber.agent_id e
CalendarSlot.instance_id + param instance_id no calendar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-07-05 19:48:29 +02:00
parent 09edeb9558
commit 03711e6280
2 changed files with 36 additions and 5 deletions
+32 -4
View File
@@ -722,9 +722,10 @@ const ROLE_OPTIONS: { value: NumberRole; label: string }[] = [
interface BaileysInstance { id: string; name: string; status: string } interface BaileysInstance { id: string; name: string; status: string }
function NumbersPanel({ function NumbersPanel({
numbers, loading, onCreate, onUpdate, onDelete, numbers, agents, loading, onCreate, onUpdate, onDelete,
}: { }: {
numbers: SecNumber[] numbers: SecNumber[]
agents: SecAgent[]
loading: boolean loading: boolean
onCreate: (d: Partial<SecNumber>) => void onCreate: (d: Partial<SecNumber>) => void
onUpdate: (id: string, d: Partial<SecNumber>) => void onUpdate: (id: string, d: Partial<SecNumber>) => void
@@ -734,7 +735,8 @@ function NumbersPanel({
const [loadingInst, setLoadingInst] = useState(false) const [loadingInst, setLoadingInst] = useState(false)
const [rolePickerFor, setRolePickerFor] = useState<string | null>(null) // instance_id const [rolePickerFor, setRolePickerFor] = useState<string | null>(null) // instance_id
const [editingId, setEditingId] = useState<string | null>(null) // sec_number.id const [editingId, setEditingId] = useState<string | null>(null) // sec_number.id
const [editForm, setEditForm] = useState<{ area: string; priority: number; notes: string }>({ area: '', priority: 10, notes: '' }) const [editForm, setEditForm] = useState<{ area: string; priority: number; notes: string; agent_id: string | null }>({ area: '', priority: 10, notes: '', agent_id: null })
const agentNameOf = (id: string | null | undefined) => agents.find((a) => a.id === id)?.name ?? null
useEffect(() => { useEffect(() => {
setLoadingInst(true) setLoadingInst(true)
@@ -825,6 +827,14 @@ function NumbersPanel({
)} )}
{num?.area && <span className="text-[10px] text-gray-400">· {num.area}</span>} {num?.area && <span className="text-[10px] text-gray-400">· {num.area}</span>}
</div> </div>
{num && (
<div className="flex items-center gap-1 mt-0.5">
<Brain size={9} className="text-gray-400 shrink-0" />
<span className={`text-[10px] truncate ${num.agent_id ? 'text-brand-600 font-semibold' : 'text-gray-400 italic'}`}>
{agentNameOf(num.agent_id) ?? 'Cérebro padrão (1º agente ativo)'}
</span>
</div>
)}
</div> </div>
{/* Actions */} {/* Actions */}
@@ -843,6 +853,8 @@ function NumbersPanel({
{/* Role picker dropdown */} {/* Role picker dropdown */}
<AnimatePresence> <AnimatePresence>
{rolePickerFor === inst.id && ( {rolePickerFor === inst.id && (
<>
<div className="fixed inset-0 z-40" onClick={() => setRolePickerFor(null)} />
<motion.div <motion.div
initial={{ opacity: 0, y: -4 }} initial={{ opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
@@ -877,6 +889,7 @@ function NumbersPanel({
) )
})} })}
</motion.div> </motion.div>
</>
)} )}
</AnimatePresence> </AnimatePresence>
@@ -891,7 +904,7 @@ function NumbersPanel({
{/* Editar detalhes (área, prioridade) */} {/* Editar detalhes (área, prioridade) */}
{num && !isEditing && ( {num && !isEditing && (
<button onClick={() => { setEditingId(num.id); setEditForm({ area: num.area ?? '', priority: num.priority, notes: num.notes ?? '' }) }} <button onClick={() => { setEditingId(num.id); setEditForm({ area: num.area ?? '', priority: num.priority, notes: num.notes ?? '', agent_id: num.agent_id ?? null }) }}
className="w-5 h-5 flex items-center justify-center text-gray-400 hover:text-gray-600" className="w-5 h-5 flex items-center justify-center text-gray-400 hover:text-gray-600"
> >
<Pencil size={11} /> <Pencil size={11} />
@@ -914,9 +927,23 @@ function NumbersPanel({
</div> </div>
</div> </div>
{/* Inline edit: área + prioridade */} {/* Inline edit: agente/cérebro + área + prioridade */}
{isEditing && ( {isEditing && (
<div className="px-3 pb-3 pt-1 border-t border-gray-100 grid grid-cols-2 gap-2"> <div className="px-3 pb-3 pt-1 border-t border-gray-100 grid grid-cols-2 gap-2">
<div className="col-span-2">
<p className="text-[10px] text-gray-500 mb-0.5">Agente / Cérebro deste número</p>
<select
value={editForm.agent_id ?? ''}
onChange={(e) => setEditForm((f) => ({ ...f, agent_id: e.target.value || null }))}
className="w-full text-xs bg-gray-50 text-gray-800 border border-gray-200 rounded-lg px-2 py-1.5 focus:outline-none focus:border-brand-500/40"
>
<option value="">Cérebro padrão (1º agente ativo)</option>
{agents.map((a) => (
<option key={a.id} value={a.id}>{a.name}{a.active ? '' : ' (inativo)'}</option>
))}
</select>
<p className="text-[9px] text-gray-400 mt-0.5 leading-tight">Cada número responde com o cérebro (persona, regras, conhecimento, agenda) do agente escolhido aqui.</p>
</div>
<div> <div>
<p className="text-[10px] text-gray-500 mb-0.5">Área / Especialidade</p> <p className="text-[10px] text-gray-500 mb-0.5">Área / Especialidade</p>
<input <input
@@ -1688,6 +1715,7 @@ export function SecretariaView(props: { onNavigate?: (view: string) => void } =
{activeTab === 'numbers' && ( {activeTab === 'numbers' && (
<NumbersPanel <NumbersPanel
numbers={store.numbers} numbers={store.numbers}
agents={store.agents}
loading={store.loadingNumbers} loading={store.loadingNumbers}
onCreate={(d) => store.createNumber(d)} onCreate={(d) => store.createNumber(d)}
onUpdate={(id, d) => store.updateNumber(id, d)} onUpdate={(id, d) => store.updateNumber(id, d)}
@@ -75,6 +75,7 @@ export interface CalendarSlot {
date: string date: string
time_start: string time_start: string
time_end: string time_end: string
instance_id?: string | null // número/sessão dona do slot (null = global)
attendee_name: string | null attendee_name: string | null
attendee_phone: string | null attendee_phone: string | null
status: 'available' | 'booked' | 'cancelled' status: 'available' | 'booked' | 'cancelled'
@@ -132,6 +133,8 @@ export type NumberRole =
export interface SecNumber { export interface SecNumber {
id: string id: string
instance_id: string instance_id: string
clinica_id?: string | null
agent_id: string | null // agente/cérebro deste número (separação por sessão)
label: string label: string
role: NumberRole role: NumberRole
area: string | null area: string | null
@@ -153,7 +156,7 @@ export const secNumberApi = {
// ─── Calendar ───────────────────────────────────────────────────────────────── // ─── Calendar ─────────────────────────────────────────────────────────────────
export const secCalApi = { export const secCalApi = {
list: (params?: { from?: string; to?: string; status?: string }) => list: (params?: { from?: string; to?: string; status?: string; instance_id?: string }) =>
api.get<CalendarSlot[]>(`${BASE}/calendar`, { params }).then((r) => r.data), api.get<CalendarSlot[]>(`${BASE}/calendar`, { params }).then((r) => r.data),
create: (d: Partial<CalendarSlot>) => api.post<CalendarSlot>(`${BASE}/calendar`, d).then((r) => r.data), create: (d: Partial<CalendarSlot>) => api.post<CalendarSlot>(`${BASE}/calendar`, d).then((r) => r.data),
update: (id: string, d: Partial<CalendarSlot>) => api.put<CalendarSlot>(`${BASE}/calendar/${id}`, d).then((r) => r.data), update: (id: string, d: Partial<CalendarSlot>) => api.put<CalendarSlot>(`${BASE}/calendar/${id}`, d).then((r) => r.data),