diff --git a/src/modules/home/components/MessagesWorkspace.jsx b/src/modules/home/components/MessagesWorkspace.jsx
index c18faa1..95bc225 100644
--- a/src/modules/home/components/MessagesWorkspace.jsx
+++ b/src/modules/home/components/MessagesWorkspace.jsx
@@ -376,10 +376,13 @@ export function MessagesWorkspace({
textAlign: 'left',
display: 'grid',
gap: '0.6rem',
+ minWidth: 0,
}}
>
-
-
{conversation.name}
+
+
+ {conversation.name}
+
{conversation.time}
@@ -388,7 +391,19 @@ export function MessagesWorkspace({
-
{conversation.lastMessage}
+
+ {conversation.lastMessage}
+
);
})}
diff --git a/src/modules/home/pages/AgentMassMessagePage.jsx b/src/modules/home/pages/AgentMassMessagePage.jsx
new file mode 100644
index 0000000..624323d
--- /dev/null
+++ b/src/modules/home/pages/AgentMassMessagePage.jsx
@@ -0,0 +1,174 @@
+import { useEffect, useState } from 'react';
+import { BrandMark } from '../../../shared/components/BrandMark';
+import { useViewport } from '../../../shared/hooks/useViewport';
+import { getCurrentUser, getCurrentUserDisplay } from '../../auth/services/sessionService';
+import { listContactProfiles } from '../../chat/services/contactProfileService';
+import { MassMessagePanel } from '../../management/components/MassMessagePanel';
+import { getAccessOptions } from '../../management/services/adminAccessService';
+import { HomeSidebar } from '../components/HomeSidebar';
+import { sidebarItems } from '../services/homeMocks';
+
+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 AgentMassMessagePage() {
+ const { isDesktop, isMobile } = useViewport();
+ const userDisplay = getCurrentUserDisplay();
+ const currentUser = getCurrentUser();
+ const specialties = getUserSpecialties(currentUser);
+ const [areas, setAreas] = useState([]);
+ const [contactCount, setContactCount] = useState(0);
+
+ useEffect(() => {
+ let isMounted = true;
+
+ getAccessOptions()
+ .then((options) => {
+ if (isMounted) setAreas(options.areas || []);
+ })
+ .catch(() => {
+ if (isMounted) setAreas([]);
+ });
+
+ return () => {
+ isMounted = false;
+ };
+ }, []);
+
+ useEffect(() => {
+ let isMounted = true;
+
+ listContactProfiles()
+ .then((items) => {
+ if (isMounted) setContactCount(Array.isArray(items) ? items.length : 0);
+ })
+ .catch(() => {
+ if (isMounted) setContactCount(0);
+ });
+
+ return () => {
+ isMounted = false;
+ };
+ }, []);
+
+ const sidebarWithContactCount = sidebarItems.map((item) =>
+ item.id === 'contacts' ? { ...item, count: contactCount } : item,
+ );
+
+ return (
+
+
+
+ );
+}
diff --git a/src/modules/home/pages/ContactsPage.jsx b/src/modules/home/pages/ContactsPage.jsx
new file mode 100644
index 0000000..0c19d2c
--- /dev/null
+++ b/src/modules/home/pages/ContactsPage.jsx
@@ -0,0 +1,439 @@
+import { useEffect, useMemo, useState } from 'react';
+import { BrandMark } from '../../../shared/components/BrandMark';
+import { useViewport } from '../../../shared/hooks/useViewport';
+import { getCurrentUser, getCurrentUserDisplay } from '../../auth/services/sessionService';
+import { listContactProfiles, saveContactProfile } from '../../chat/services/contactProfileService';
+import { HomeSidebar } from '../components/HomeSidebar';
+import { sidebarItems } from '../services/homeMocks';
+
+const inputStyle = {
+ width: '100%',
+ border: '1px solid var(--color-border)',
+ borderRadius: 14,
+ padding: '0.85rem 0.9rem',
+ background: '#fff',
+ color: 'var(--color-text)',
+ fontWeight: 600,
+};
+
+function getUserId(user) {
+ const value = user?.databaseId || user?.id;
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function onlyDigits(value) {
+ return String(value || '').replace(/\D/g, '');
+}
+
+function buildChatId(phone) {
+ const digits = onlyDigits(phone);
+ return digits ? `${digits}@c.us` : '';
+}
+
+function normalizeContact(contact) {
+ return {
+ chatId: contact.chat_id || buildChatId(contact.phone),
+ name: contact.name || contact.phone || 'Contato sem nome',
+ whatsappPhone: contact.phone || '',
+ callSmsPhone: contact.call_sms_phone || contact.callSmsPhone || '',
+ email: contact.email || '',
+ tag: contact.company || '',
+ note: contact.note || '',
+ updatedAt: contact.updated_at || contact.created_at || null,
+ };
+}
+
+function emptyDraft() {
+ return {
+ chatId: '',
+ name: '',
+ whatsappPhone: '',
+ callSmsPhone: '',
+ email: '',
+ tag: '',
+ note: '',
+ };
+}
+
+export function ContactsPage() {
+ const { isDesktop, isMobile } = useViewport();
+ const currentUser = getCurrentUser();
+ const currentUserId = getUserId(currentUser);
+ const userDisplay = getCurrentUserDisplay();
+ const [contacts, setContacts] = useState([]);
+ const [search, setSearch] = useState('');
+ const [draft, setDraft] = useState(emptyDraft());
+ const [selectedChatId, setSelectedChatId] = useState('');
+ const [status, setStatus] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [isSaving, setIsSaving] = useState(false);
+
+ async function loadContacts() {
+ setIsLoading(true);
+ try {
+ const data = await listContactProfiles();
+ setContacts(Array.isArray(data) ? data.map(normalizeContact) : []);
+ setStatus('');
+ } catch (error) {
+ setStatus(error.message);
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ useEffect(() => {
+ loadContacts();
+ }, []);
+
+ const sidebarWithCount = useMemo(
+ () => sidebarItems.map((item) => (item.id === 'contacts' ? { ...item, count: contacts.length } : item)),
+ [contacts.length],
+ );
+
+ const filteredContacts = useMemo(() => {
+ const value = search.trim().toLowerCase();
+ if (!value) return contacts;
+ return contacts.filter((contact) =>
+ `${contact.name} ${contact.whatsappPhone} ${contact.callSmsPhone} ${contact.email} ${contact.tag} ${contact.note}`
+ .toLowerCase()
+ .includes(value),
+ );
+ }, [contacts, search]);
+
+ function selectContact(contact) {
+ setSelectedChatId(contact.chatId);
+ setDraft({ ...contact });
+ setStatus('');
+ }
+
+ function startNewContact() {
+ setSelectedChatId('');
+ setDraft(emptyDraft());
+ setStatus('');
+ }
+
+ async function handleSave(event) {
+ event.preventDefault();
+ const whatsappPhone = onlyDigits(draft.whatsappPhone);
+ const chatId = selectedChatId || draft.chatId || buildChatId(whatsappPhone);
+
+ if (!chatId || !whatsappPhone) {
+ setStatus('Informe o número de WhatsApp para salvar o contato.');
+ return;
+ }
+
+ setIsSaving(true);
+ try {
+ await saveContactProfile(chatId, {
+ phone: whatsappPhone,
+ whatsappPhone,
+ callSmsPhone: onlyDigits(draft.callSmsPhone),
+ email: draft.email,
+ name: draft.name,
+ company: draft.tag,
+ note: draft.note,
+ userId: currentUserId,
+ });
+ setStatus('Contato salvo com sucesso.');
+ await loadContacts();
+ setSelectedChatId(chatId);
+ setDraft((current) => ({ ...current, chatId, whatsappPhone }));
+ } catch (error) {
+ setStatus(error.message);
+ } finally {
+ setIsSaving(false);
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
Contatos
+
+ Agenda geral com WhatsApp, telefone para ligação/SMS, e-mail, tag e observação.
+
+
+
+
+
+ {userDisplay.name}
+
+ Atendimento omnichannel
+
+
+
+ {userDisplay.initials}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/modules/home/pages/HomePage.jsx b/src/modules/home/pages/HomePage.jsx
index 692a636..6bf74e9 100644
--- a/src/modules/home/pages/HomePage.jsx
+++ b/src/modules/home/pages/HomePage.jsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { BrandMark } from '../../../shared/components/BrandMark';
import { HomeSidebar } from '../components/HomeSidebar';
import { HomeTopbar } from '../components/HomeTopbar';
@@ -8,14 +8,23 @@ import { AttendantOpsPanel } from '../components/AttendantOpsPanel';
import { recentCalls, sidebarItems } from '../services/homeMocks';
import { useViewport } from '../../../shared/hooks/useViewport';
import { useChat } from '../../chat/hooks/useChat';
+import { listContactProfiles } from '../../chat/services/contactProfileService';
+
+function truncatePreview(value, limit = 96) {
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
+ if (text.length <= limit) return text;
+ return `${text.slice(0, limit).trim()}...`;
+}
function toHomeConversation(contact, messages = []) {
+ const lastMessage = contact.preview || messages[messages.length - 1]?.text || '';
+
return {
id: contact.id,
name: contact.name,
channel: contact.channel || 'WhatsApp',
status: contact.status || 'online',
- lastMessage: contact.preview || messages[messages.length - 1]?.text || '',
+ lastMessage: truncatePreview(lastMessage),
unread: contact.unread || 0,
time: contact.time || 'Agora',
lastSeen: contact.lastSeen,
@@ -45,6 +54,28 @@ export function HomePage() {
} = useChat();
const [activeTab, setActiveTab] = useState('messages');
const [searchValue, setSearchValue] = useState('');
+ const [contactCount, setContactCount] = useState(0);
+
+ useEffect(() => {
+ let isMounted = true;
+
+ listContactProfiles()
+ .then((items) => {
+ if (isMounted) setContactCount(Array.isArray(items) ? items.length : 0);
+ })
+ .catch(() => {
+ if (isMounted) setContactCount(0);
+ });
+
+ return () => {
+ isMounted = false;
+ };
+ }, []);
+
+ const sidebarWithContactCount = useMemo(
+ () => sidebarItems.map((item) => (item.id === 'contacts' ? { ...item, count: contactCount } : item)),
+ [contactCount],
+ );
const conversations = contacts.map((contact) =>
toHomeConversation(contact, contact.id === activeContactId ? messages : []),
@@ -106,7 +137,7 @@ export function HomePage() {
>
-
diff --git a/src/modules/home/services/homeMocks.js b/src/modules/home/services/homeMocks.js
index 72ee9ee..4456e97 100644
--- a/src/modules/home/services/homeMocks.js
+++ b/src/modules/home/services/homeMocks.js
@@ -1,10 +1,9 @@
-export const sidebarItems = [
- { id: 'scripts', label: 'Scripts e respostas prontas' },
- { id: 'personal-reports', label: 'Relatórios pessoais' },
- { id: 'mass-message', label: 'Disparo em massa' },
+export const sidebarItems = [
+ { id: 'new-attendance', label: 'Abrir atendimento', route: '/new-attendance' },
+ { id: 'mass-message', label: 'Disparo em massa', route: '/mass-message' },
{ id: 'knowledge-base', label: 'Base de conhecimento' },
- { id: 'completed', label: 'Finalizados', count: 24 },
- { id: 'contacts', label: 'Contatos', count: 128 },
+ { id: 'scripts', label: 'Scripts e respostas prontas' },
+ { id: 'contacts', label: 'Contatos', route: '/contacts' },
];
export const conversations = [
@@ -31,21 +30,21 @@ export const conversations = [
unread: 0,
time: 'Ontem',
messages: [
- { id: 1, from: 'customer', text: 'Precisamos rever os valores da última proposta.' },
+ { id: 1, from: 'customer', text: 'Precisamos rever os valores da última proposta.' },
{ id: 2, from: 'agent', text: 'Perfeito, vou encaminhar para o time comercial.' },
],
},
{
id: 'joao-pedro',
- name: 'João Pedro',
+ name: 'João Pedro',
channel: 'SMS',
status: 'online',
lastMessage: 'Pode me ligar em 10 minutos?',
unread: 1,
time: '08:15',
messages: [
- { id: 1, from: 'customer', text: 'Recebi a cobrança em duplicidade.' },
- { id: 2, from: 'agent', text: 'Vou analisar isso agora para você.' },
+ { id: 1, from: 'customer', text: 'Recebi a cobrança em duplicidade.' },
+ { id: 2, from: 'agent', text: 'Vou analisar isso agora para você.' },
{ id: 3, from: 'customer', text: 'Pode me ligar em 10 minutos?' },
],
},
diff --git a/src/routes/router.jsx b/src/routes/router.jsx
index fdb0d34..1b3f6c0 100644
--- a/src/routes/router.jsx
+++ b/src/routes/router.jsx
@@ -1,6 +1,8 @@
import { createBrowserRouter, Navigate } from 'react-router-dom';
import { LoginPage } from '../modules/auth/pages/LoginPage';
import { ProfileHomePage } from '../modules/home/pages/ProfileHomePage';
+import { AgentMassMessagePage } from '../modules/home/pages/AgentMassMessagePage';
+import { ContactsPage } from '../modules/home/pages/ContactsPage';
import { ChatPage } from '../modules/chat/pages/ChatPage';
import { CallPage } from '../modules/call/pages/CallPage';
import { NewAttendancePage } from '../modules/attendance/pages/NewAttendancePage';
@@ -31,6 +33,14 @@ export const router = createBrowserRouter([
path: '/new-attendance',
element:
,
},
+ {
+ path: '/mass-message',
+ element:
,
+ },
+ {
+ path: '/contacts',
+ element:
,
+ },
{
path: '/admin/whatsapp',
element:
,