From 7a7179bb5da00c20ec8cf051dfa2e428ccffc413 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Tue, 26 May 2026 11:35:07 -0300 Subject: [PATCH] FEAT: Adicionado agenda de contatos e disparo em massa --- .../home/components/MessagesWorkspace.jsx | 21 +- .../home/pages/AgentMassMessagePage.jsx | 174 +++++++ src/modules/home/pages/ContactsPage.jsx | 439 ++++++++++++++++++ src/modules/home/pages/HomePage.jsx | 37 +- src/modules/home/services/homeMocks.js | 19 +- src/routes/router.jsx | 10 + 6 files changed, 684 insertions(+), 16 deletions(-) create mode 100644 src/modules/home/pages/AgentMassMessagePage.jsx create mode 100644 src/modules/home/pages/ContactsPage.jsx 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 ( +
+
+
+
+
+ +
+ +
+ +
+
+
+

Disparo em massa

+

+ Envie templates aprovados para contatos da agenda ou numeros informados manualmente. +

+
+ +
+
+ {userDisplay.name} + + Atendimento omnichannel + +
+ +
+
+ + +
+
+
+
+ ); +} 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 + +
+ +
+
+ +
+ + +
+
+ + {selectedChatId ? 'Editar contato' : 'Novo contato'} + + + O WhatsApp é usado para vincular o contato à conversa. + +
+ +
+ + + + + + + + + +
+ +