+
diff --git a/src/modules/chat/hooks/useChat.js b/src/modules/chat/hooks/useChat.js
index f6fe6fb..f86a570 100644
--- a/src/modules/chat/hooks/useChat.js
+++ b/src/modules/chat/hooks/useChat.js
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { useWhatsappSocket } from '../../../shared/hooks/useWhatsappSocket';
import { API_BASE_URL } from '../../../shared/services/apiConfig';
import { getAccessOptions, getAccessUsers } from '../../management/services/adminAccessService';
-import { getCurrentUser } from '../../auth/services/sessionService';
+import { getCurrentUser, getCurrentUserProfile } from '../../auth/services/sessionService';
import { chatContacts, transferAreas as fallbackTransferAreas } from '../services/chatMocks';
function buildInitialMessages() {
@@ -212,8 +212,10 @@ function getUserDisplayName(user) {
export function useChat() {
const currentUser = getCurrentUser();
+ const currentUserProfile = getCurrentUserProfile();
const currentUserId = getUserId(currentUser);
const currentUserAreas = getUserAreas(currentUser);
+ const isAdminUser = currentUserProfile === 'admin';
const { status: whatsappStatus, incomingMessage, clearIncomingMessage } = useWhatsappSocket();
const [contacts, setContacts] = useState(buildFallbackContacts);
const [activeContactId, setActiveContactId] = useState(chatContacts[0].id);
@@ -261,7 +263,7 @@ export function useChat() {
const isWaitingCustomerReply = Boolean(activeAssignment?.awaiting_customer_reply);
const isQueuedForUserArea = Boolean(
activeAssignment?.status === 'queued' &&
- (!activeAssignment.area_nome || currentUserAreas.includes(activeAssignment.area_nome)),
+ (isAdminUser || !activeAssignment.area_nome || currentUserAreas.includes(activeAssignment.area_nome)),
);
const canAssumeChat = Boolean(activeContact?.id?.includes('@') && currentUserId && isQueuedForUserArea);
const canReply = Boolean(isAssignedToCurrentUser && !isWaitingCustomerReply);
@@ -314,6 +316,9 @@ export function useChat() {
}, []);
function canSeeContact(contact) {
+ if (isAdminUser) {
+ return Boolean(contact.assignment && contact.assignment.status !== 'bot_triage');
+ }
if (!currentUserAreas.length) return false;
if (!contact.assignment) return false;
if (contact.assignment.status === 'bot_triage') return false;
@@ -369,7 +374,7 @@ export function useChat() {
isMounted = false;
window.clearInterval(intervalId);
};
- }, [currentUserId, currentUserAreas.join('|'), whatsappStatus]);
+ }, [currentUserId, currentUserAreas.join('|'), isAdminUser, whatsappStatus]);
useEffect(() => {
if (!activeContactId) return;
@@ -724,6 +729,7 @@ export function useChat() {
}
return {
+ currentUserId,
contacts,
activeContact,
activeContactId,
diff --git a/src/modules/chat/pages/ChatPage.jsx b/src/modules/chat/pages/ChatPage.jsx
index ed883a0..4e60ee7 100644
--- a/src/modules/chat/pages/ChatPage.jsx
+++ b/src/modules/chat/pages/ChatPage.jsx
@@ -13,6 +13,7 @@ export function ChatPage() {
const [searchParams, setSearchParams] = useSearchParams();
const { isWideDesktop, isDesktop, isTablet, isMobile } = useViewport();
const {
+ currentUserId,
contacts,
activeContact,
activeContactId,
@@ -144,6 +145,7 @@ export function ChatPage() {
setIsTransferOpen(false);
setIsContactPanelOpen(true);
}}
+ currentUserId={currentUserId}
isMobile={isMobile}
/>
diff --git a/src/modules/management/components/ManagementLayout.jsx b/src/modules/management/components/ManagementLayout.jsx
index 76e2b65..6d54664 100644
--- a/src/modules/management/components/ManagementLayout.jsx
+++ b/src/modules/management/components/ManagementLayout.jsx
@@ -4,11 +4,15 @@ import { clearSession } from '../../auth/services/sessionService';
const navigationBySection = {
supervisor: [
- { id: 'dashboard', label: 'Dashboard', count: null },
- { id: 'queues', label: 'Filas em tempo real', count: 42 },
- { id: 'areas', label: 'Especialidades supervisionadas', count: 3 },
- { id: 'agents', label: 'Agentes online', count: 18 },
- { id: 'reports', label: 'Relatórios', count: null },
+ { id: 'dashboard', label: 'Home' },
+ { id: 'templates', label: 'Templates' },
+ { id: 'knowledge', label: 'Base de conhecimento IA' },
+ { id: 'audit', label: 'Auditoria' },
+ { type: 'separator' },
+ { id: 'attendance', label: 'Atendimento' },
+ { id: 'new-attendance', label: 'Abrir Atendimento', path: '/new-attendance' },
+ { id: 'mass-message', label: 'Disparo em Massa' },
+ { id: 'contacts', label: 'Contatos' },
],
admin: [
{ id: 'home', label: 'Home' },
@@ -31,7 +35,7 @@ const navigationBySection = {
const actionLabelBySection = {
supervisor: '+ Redistribuir atendimento',
- admin: '+ Nova configuração',
+ admin: 'Home',
};
export function ManagementLayout({
@@ -48,7 +52,7 @@ export function ManagementLayout({
}) {
const navigate = useNavigate();
const navItems = navigationBySection[activeSection] || navigationBySection.supervisor;
- const actionLabel = actionLabelBySection[activeSection] || '+ Nova ação';
+ const actionLabel = actionLabelBySection[activeSection] || 'Home';
function handleLogout() {
clearSession();
diff --git a/src/modules/management/components/TemplateManagementPanel.jsx b/src/modules/management/components/TemplateManagementPanel.jsx
new file mode 100644
index 0000000..8b77cb0
--- /dev/null
+++ b/src/modules/management/components/TemplateManagementPanel.jsx
@@ -0,0 +1,347 @@
+import { useEffect, useMemo, useState } from 'react';
+import { DataPanel } from './DataPanel';
+import {
+ approveTemplateByAdmin,
+ deleteTemplate,
+ listTemplates,
+ rejectTemplateByAdmin,
+ saveTemplate,
+} from '../services/templateService';
+
+const fieldStyle = {
+ width: '100%',
+ border: '1px solid var(--color-border)',
+ borderRadius: '14px',
+ padding: '0.75rem 0.85rem',
+ background: '#fff',
+ color: 'var(--color-text)',
+ fontWeight: 600,
+};
+
+const statusMeta = {
+ approved: {
+ label: 'Aprovado pela Meta',
+ background: 'rgba(34, 197, 94, 0.12)',
+ color: '#15803d',
+ },
+ meta_review: {
+ label: 'Em análise pela Meta',
+ background: 'rgba(229, 162, 42, 0.16)',
+ color: '#8a5a00',
+ },
+ admin_review: {
+ label: 'Aguardando aprovação do admin',
+ background: 'rgba(0, 49, 80, 0.1)',
+ color: 'var(--color-primary)',
+ },
+ rejected: {
+ label: 'Reprovado pelo admin',
+ background: 'rgba(181, 31, 31, 0.1)',
+ color: 'var(--color-secondary)',
+ },
+};
+
+function getTemplateStatus(template) {
+ return statusMeta[template.status] || statusMeta.approved;
+}
+
+function getRemainingMetaText(template) {
+ if (template.status !== 'meta_review' || !template.meta_submitted_at) return '';
+ const submittedAt = new Date(template.meta_submitted_at).getTime();
+ const approvedAt = submittedAt + 15 * 60 * 1000;
+ const remainingMs = approvedAt - Date.now();
+ if (remainingMs <= 0) return 'Aprovação fake disponível ao atualizar.';
+ const minutes = Math.ceil(remainingMs / 60000);
+ return `Aprovação fake em aproximadamente ${minutes} min.`;
+}
+
+export function TemplateManagementPanel({
+ areas = [],
+ mode = 'admin',
+ managedAreaNames = [],
+ isMobile = false,
+}) {
+ const [templates, setTemplates] = useState([]);
+ const [selectedArea, setSelectedArea] = useState('all');
+ const [form, setForm] = useState({ name: '', content: '', areaId: '' });
+ const [statusMessage, setStatusMessage] = useState('');
+ const [isLoading, setIsLoading] = useState(true);
+
+ const isAdmin = mode === 'admin';
+ const visibleAreas = isAdmin
+ ? areas
+ : areas.filter((area) => managedAreaNames.includes(area.nome));
+
+ const filteredTemplates = useMemo(() => {
+ return templates.filter((template) => {
+ const areaMatches = selectedArea === 'all' || String(template.area_id || '') === selectedArea;
+ const supervisorAreaMatches =
+ isAdmin ||
+ !template.area_nome ||
+ managedAreaNames.includes(template.area_nome);
+ return areaMatches && supervisorAreaMatches;
+ });
+ }, [templates, selectedArea, isAdmin, managedAreaNames]);
+
+ async function loadTemplates() {
+ setIsLoading(true);
+ try {
+ const data = await listTemplates();
+ setTemplates(Array.isArray(data) ? data : []);
+ setStatusMessage('');
+ } catch (error) {
+ setStatusMessage(error.message);
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ useEffect(() => {
+ loadTemplates();
+ }, []);
+
+ async function submitTemplate(event) {
+ event.preventDefault();
+ const name = form.name.trim();
+ const content = form.content.trim();
+ if (!name || !content) return;
+
+ try {
+ await saveTemplate({
+ name,
+ content,
+ areaId: Number(form.areaId) || null,
+ requestedByRole: isAdmin ? 'admin' : 'supervisor',
+ });
+ setForm({ name: '', content: '', areaId: '' });
+ setStatusMessage(
+ isAdmin
+ ? 'Template enviado para aprovação.'
+ : 'Template enviado para aprovação do admin.',
+ );
+ await loadTemplates();
+ } catch (error) {
+ setStatusMessage(error.message);
+ }
+ }
+
+ async function approveTemplate(templateId) {
+ try {
+ await approveTemplateByAdmin(templateId);
+ setStatusMessage('Template aprovado pelo admin e enviado para análise fake da Meta.');
+ await loadTemplates();
+ } catch (error) {
+ setStatusMessage(error.message);
+ }
+ }
+
+ async function rejectTemplate(templateId) {
+ try {
+ await rejectTemplateByAdmin(templateId);
+ setStatusMessage('Template reprovado pelo admin.');
+ await loadTemplates();
+ } catch (error) {
+ setStatusMessage(error.message);
+ }
+ }
+
+ async function removeTemplate(templateId) {
+ try {
+ await deleteTemplate(templateId);
+ setStatusMessage('Template excluído.');
+ await loadTemplates();
+ } catch (error) {
+ setStatusMessage(error.message);
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {statusMessage ? (
+
+ {statusMessage}
+
+ ) : null}
+
+
+
+
+ {filteredTemplates.map((template) => {
+ const status = getTemplateStatus(template);
+ const remainingMetaText = getRemainingMetaText(template);
+
+ return (
+
+
+
+ {template.name}
+
+ {template.area_nome || 'Sem especialidade'}
+
+
+
+ {status.label}
+
+
+
+
+ {template.content}
+
+
+ {remainingMetaText ? (
+ {remainingMetaText}
+ ) : null}
+
+ {isAdmin ? (
+
+ {template.status === 'admin_review' ? (
+ <>
+
+
+ >
+ ) : null}
+
+
+ ) : null}
+
+ );
+ })}
+
+ {!filteredTemplates.length ? (
+
+ Nenhum template encontrado para o filtro atual.
+
+ ) : null}
+
+
+
+ );
+}
diff --git a/src/modules/management/pages/AdminPage.jsx b/src/modules/management/pages/AdminPage.jsx
index 1317900..c792897 100644
--- a/src/modules/management/pages/AdminPage.jsx
+++ b/src/modules/management/pages/AdminPage.jsx
@@ -4,6 +4,7 @@ import { ManagementLayout } from '../components/ManagementLayout';
import { ManagementTable } from '../components/ManagementTable';
import { MetricGrid } from '../components/MetricGrid';
import { OperationalDashboard } from '../components/OperationalDashboard';
+import { TemplateManagementPanel } from '../components/TemplateManagementPanel';
import { aiContentRows, areaRows, userRows } from '../services/managementMocks';
import { AttendantOpsPanel } from '../../home/components/AttendantOpsPanel';
import { MessagesWorkspace } from '../../home/components/MessagesWorkspace';
@@ -111,7 +112,7 @@ function toHomeConversation(contact, messages = []) {
};
}
-function AdminAttendanceWorkspace({ isWideDesktop, isDesktop, isTablet, isMobile }) {
+export function AdminAttendanceWorkspace({ isWideDesktop, isDesktop, isTablet, isMobile }) {
const {
contacts,
activeContactId,
@@ -865,7 +866,7 @@ export function AdminPage() {
home: renderMonthlyHome(),
today:
,
'users-access': renderUsersAccess(),
- templates: renderPlaceholder('Templates', 'Gestão de templates aprovados pela Meta.'),
+ templates:
,
knowledge: (
row.id} isMobile={isMobile} />
diff --git a/src/modules/management/pages/SupervisorPage.jsx b/src/modules/management/pages/SupervisorPage.jsx
index 5a8f155..6e6def0 100644
--- a/src/modules/management/pages/SupervisorPage.jsx
+++ b/src/modules/management/pages/SupervisorPage.jsx
@@ -1,11 +1,99 @@
+import { useEffect, useState } from 'react';
import { ManagementLayout } from '../components/ManagementLayout';
import { OperationalDashboard } from '../components/OperationalDashboard';
+import { TemplateManagementPanel } from '../components/TemplateManagementPanel';
+import { DataPanel } from '../components/DataPanel';
+import { ManagementTable } from '../components/ManagementTable';
+import { aiContentRows } from '../services/managementMocks';
+import { getAccessOptions } from '../services/adminAccessService';
import { useViewport } from '../../../shared/hooks/useViewport';
-import { getCurrentUserDisplay } from '../../auth/services/sessionService';
+import { getCurrentUser, getCurrentUserDisplay } from '../../auth/services/sessionService';
+import { AdminAttendanceWorkspace } from './AdminPage';
+
+function getUserSpecialties(user) {
+ const normalize = (area) => {
+ if (!area) return null;
+ if (typeof area === 'string') return area;
+ return area.nome || area.name || null;
+ };
+
+ const areas = Array.isArray(user?.areas) ? user.areas.map(normalize).filter(Boolean) : [];
+ const primary = normalize(user?.areaPrincipal);
+ return primary && !areas.includes(primary) ? [primary, ...areas] : areas;
+}
export function SupervisorPage() {
- const { isDesktop, isMobile } = useViewport();
+ const { isWideDesktop, isDesktop, isTablet, isMobile } = useViewport();
const userDisplay = getCurrentUserDisplay();
+ const currentUser = getCurrentUser();
+ const managedSpecialties = getUserSpecialties(currentUser);
+ const [activeSection, setActiveSection] = useState('dashboard');
+ const [areas, setAreas] = useState([]);
+
+ useEffect(() => {
+ let isMounted = true;
+
+ getAccessOptions()
+ .then((options) => {
+ if (isMounted) setAreas(options.areas || []);
+ })
+ .catch(() => {
+ if (isMounted) setAreas([]);
+ });
+
+ return () => {
+ isMounted = false;
+ };
+ }, []);
+
+ function renderPlaceholder(title, description) {
+ return (
+
+
+ Seção em preparação.
+
+
+ );
+ }
+
+ const contentColumns = [
+ { key: 'title', label: 'Conteúdo' },
+ { key: 'area', label: 'Especialidade' },
+ { key: 'status', label: 'Status' },
+ { key: 'updatedAt', label: 'Atualizado' },
+ ];
+
+ const filteredKnowledgeRows = managedSpecialties.length
+ ? aiContentRows.filter((row) => managedSpecialties.includes(row.area))
+ : aiContentRows;
+
+ const sectionContent = {
+ dashboard: ,
+ templates: (
+
+ ),
+ knowledge: (
+
+ row.id} isMobile={isMobile} />
+
+ ),
+ audit: renderPlaceholder('Auditoria', 'Eventos do time supervisionado serão consolidados aqui.'),
+ attendance: (
+
+ ),
+ 'mass-message': renderPlaceholder('Disparo em massa', 'Fluxo de disparos por templates aprovados.'),
+ contacts: renderPlaceholder('Contatos', 'Agenda geral de contatos.'),
+ };
return (
-
+ {sectionContent[activeSection] || sectionContent.dashboard}
);
}
diff --git a/src/modules/management/services/templateService.js b/src/modules/management/services/templateService.js
new file mode 100644
index 0000000..c72290b
--- /dev/null
+++ b/src/modules/management/services/templateService.js
@@ -0,0 +1,53 @@
+import { API_BASE_URL } from '../../../shared/services/apiConfig';
+
+async function request(path, options = {}) {
+ const response = await fetch(`${API_BASE_URL}${path}`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options.headers,
+ },
+ ...options,
+ });
+
+ if (!response.ok) {
+ throw new Error('Falha ao consultar templates.');
+ }
+
+ return response.json();
+}
+
+export function listTemplates() {
+ return request('/whatsapp/templates');
+}
+
+export function saveTemplate(payload) {
+ return request('/whatsapp/templates', {
+ method: 'POST',
+ body: JSON.stringify(payload),
+ });
+}
+
+export function updateTemplate(id, payload) {
+ return request(`/whatsapp/templates/update/${id}`, {
+ method: 'POST',
+ body: JSON.stringify(payload),
+ });
+}
+
+export function approveTemplateByAdmin(id) {
+ return request(`/whatsapp/templates/approve-admin/${id}`, {
+ method: 'POST',
+ });
+}
+
+export function rejectTemplateByAdmin(id) {
+ return request(`/whatsapp/templates/reject-admin/${id}`, {
+ method: 'POST',
+ });
+}
+
+export function deleteTemplate(id) {
+ return request(`/whatsapp/templates/${id}`, {
+ method: 'DELETE',
+ });
+}