3042ddca38
- /wa-inbox, /wa-sessions e /wa-secretaria rodam sem a sidebar do scoreodonto (isStandaloneView); cada tela tem seu "Voltar" (Secretária ganhou botão na ThinNav). - SessionsView: banner "Você já possui acesso ao WhatsApp" quando há sessão ativa. - avatares: usa a URL do CDN (contactAvatarUrl/instance.avatar) direto; Avatar exibe sem depender de version; self-heal no onError re-busca a foto atual. - deps: framer-motion, immer. Dev override com HMR (docker-compose.dev.yml). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
174 lines
6.2 KiB
TypeScript
174 lines
6.2 KiB
TypeScript
import { useState, useCallback, useEffect } from 'react'
|
|
import { protocolApi, sectorApi, type Sector, type Team } from '../services/chatApiService'
|
|
|
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
|
|
export type ProtocolStatus =
|
|
| 'OPEN'
|
|
| 'IN_PROGRESS'
|
|
| 'WAITING_CLIENT'
|
|
| 'WAITING_AGENT'
|
|
| 'RESOLVED'
|
|
| 'CANCELLED'
|
|
|
|
export interface Protocol {
|
|
id: string
|
|
number: string
|
|
chatId: string
|
|
contactId: string
|
|
sectorId: string | null
|
|
teamId: string | null
|
|
agentId: string | null
|
|
status: ProtocolStatus
|
|
summary: string | null
|
|
deadline: string | null
|
|
resolvedAt: string | null
|
|
createdAt: string
|
|
updatedAt: string
|
|
sector?: { name: string } | null
|
|
team?: { name: string } | null
|
|
}
|
|
|
|
// ─── Hook ─────────────────────────────────────────────────────────────────────
|
|
|
|
export function useProtocolPanel(chatId: string | null) {
|
|
const [protocols, setProtocols] = useState<Protocol[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
// Sectors & teams for the "open protocol" form
|
|
const [sectors, setSectors] = useState<Sector[]>([])
|
|
const [teams, setTeams] = useState<Team[]>([])
|
|
const [selectedSectorId, setSelectedSectorId] = useState<string>('')
|
|
const [selectedTeamId, setSelectedTeamId] = useState<string>('')
|
|
const [loadingSectors, setLoadingSectors] = useState(false)
|
|
const [loadingTeams, setLoadingTeams] = useState(false)
|
|
|
|
// Opening a new protocol
|
|
const [isOpening, setIsOpening] = useState(false)
|
|
const [showOpenForm, setShowOpenForm] = useState(false)
|
|
|
|
// Updating status
|
|
const [updatingId, setUpdatingId] = useState<string | null>(null)
|
|
|
|
// Toast
|
|
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null)
|
|
|
|
const showToast = useCallback((message: string, type: 'success' | 'error') => {
|
|
setToast({ message, type })
|
|
setTimeout(() => setToast(null), 3000)
|
|
}, [])
|
|
|
|
// ── Load protocols ────────────────────────────────────────────────────────
|
|
const load = useCallback(async () => {
|
|
if (!chatId) { setProtocols([]); return }
|
|
setLoading(true)
|
|
try {
|
|
const data = await protocolApi.list(chatId)
|
|
setProtocols(data as Protocol[])
|
|
} catch {
|
|
// silently ignore
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [chatId])
|
|
|
|
useEffect(() => { load() }, [chatId])
|
|
|
|
// ── Load sectors (lazy, once) ─────────────────────────────────────────────
|
|
const loadSectors = useCallback(async () => {
|
|
if (sectors.length > 0) return
|
|
setLoadingSectors(true)
|
|
try {
|
|
const data = await sectorApi.list()
|
|
setSectors(data.filter((s) => s.isActive))
|
|
} catch {
|
|
// ignore
|
|
} finally {
|
|
setLoadingSectors(false)
|
|
}
|
|
}, [sectors.length])
|
|
|
|
// ── Load teams when sector changes ────────────────────────────────────────
|
|
useEffect(() => {
|
|
if (!selectedSectorId) { setTeams([]); setSelectedTeamId(''); return }
|
|
setLoadingTeams(true)
|
|
sectorApi.listTeams(selectedSectorId)
|
|
.then((data) => { setTeams(data.filter((t) => t.isActive)); setSelectedTeamId('') })
|
|
.catch(() => {})
|
|
.finally(() => setLoadingTeams(false))
|
|
}, [selectedSectorId])
|
|
|
|
const openForm = useCallback(() => {
|
|
setShowOpenForm(true)
|
|
setSelectedSectorId('')
|
|
setSelectedTeamId('')
|
|
loadSectors()
|
|
}, [loadSectors])
|
|
|
|
// ── Open new protocol ─────────────────────────────────────────────────────
|
|
const openProtocol = useCallback(async () => {
|
|
if (!chatId) return
|
|
setIsOpening(true)
|
|
try {
|
|
const protocol = await protocolApi.open(chatId, {
|
|
sectorId: selectedSectorId || undefined,
|
|
teamId: selectedTeamId || undefined,
|
|
})
|
|
setProtocols((prev) => [protocol as Protocol, ...prev])
|
|
setShowOpenForm(false)
|
|
showToast('Protocolo aberto.', 'success')
|
|
} catch (err: any) {
|
|
showToast(err?.response?.data?.error ?? 'Erro ao abrir protocolo.', 'error')
|
|
} finally {
|
|
setIsOpening(false)
|
|
}
|
|
}, [chatId, selectedSectorId, selectedTeamId, showToast])
|
|
|
|
// ── Update status ─────────────────────────────────────────────────────────
|
|
const updateStatus = useCallback(async (protocol: Protocol, status: ProtocolStatus) => {
|
|
if (!chatId) return
|
|
setUpdatingId(protocol.id)
|
|
try {
|
|
await protocolApi.update(chatId, protocol.id, { status })
|
|
setProtocols((prev) =>
|
|
prev.map((p) =>
|
|
p.id === protocol.id
|
|
? { ...p, status, resolvedAt: status === 'RESOLVED' ? new Date().toISOString() : p.resolvedAt }
|
|
: p
|
|
)
|
|
)
|
|
showToast('Status atualizado.', 'success')
|
|
} catch {
|
|
showToast('Erro ao atualizar status.', 'error')
|
|
} finally {
|
|
setUpdatingId(null)
|
|
}
|
|
}, [chatId, showToast])
|
|
|
|
// ── Derived ───────────────────────────────────────────────────────────────
|
|
const activeProtocol = protocols.find(
|
|
(p) => p.status !== 'RESOLVED' && p.status !== 'CANCELLED'
|
|
) ?? null
|
|
|
|
const hasActive = activeProtocol !== null
|
|
|
|
return {
|
|
protocols, loading, load,
|
|
activeProtocol, hasActive,
|
|
|
|
// open form
|
|
showOpenForm, setShowOpenForm,
|
|
openForm,
|
|
sectors, teams,
|
|
selectedSectorId, setSelectedSectorId,
|
|
selectedTeamId, setSelectedTeamId,
|
|
loadingSectors, loadingTeams,
|
|
isOpening, openProtocol,
|
|
|
|
// update
|
|
updatingId, updateStatus,
|
|
|
|
toast,
|
|
}
|
|
}
|