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:
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user