FEAT: Adicionado agenda de contatos e disparo em massa
This commit is contained in:
parent
760fca5875
commit
7a7179bb5d
@ -376,10 +376,13 @@ export function MessagesWorkspace({
|
||||
textAlign: 'left',
|
||||
display: 'grid',
|
||||
gap: '0.6rem',
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '1rem' }}>
|
||||
<strong>{conversation.name}</strong>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '1rem', minWidth: 0 }}>
|
||||
<strong style={{ minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{conversation.name}
|
||||
</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.86rem' }}>
|
||||
{conversation.time}
|
||||
</span>
|
||||
@ -388,7 +391,19 @@ export function MessagesWorkspace({
|
||||
<ChannelBadge channel={conversation.channel} />
|
||||
<UnreadBadge count={conversation.unread} />
|
||||
</div>
|
||||
<span style={{ color: 'var(--color-text-soft)' }}>{conversation.lastMessage}</span>
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--color-text-soft)',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
overflowWrap: 'anywhere',
|
||||
lineHeight: 1.35,
|
||||
}}
|
||||
>
|
||||
{conversation.lastMessage}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
174
src/modules/home/pages/AgentMassMessagePage.jsx
Normal file
174
src/modules/home/pages/AgentMassMessagePage.jsx
Normal file
@ -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 (
|
||||
<main style={{ minHeight: '100vh', padding: '1.5rem' }}>
|
||||
<section
|
||||
style={{
|
||||
width: 'min(1680px, calc(100vw - 3rem))',
|
||||
margin: '0 auto',
|
||||
background: 'var(--color-surface-strong)',
|
||||
borderRadius: '32px',
|
||||
boxShadow: 'var(--shadow-lg)',
|
||||
padding: '1.5rem',
|
||||
display: 'grid',
|
||||
gap: '1.5rem',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isDesktop ? 'minmax(340px, 380px) minmax(0, 1fr)' : '1fr',
|
||||
gap: '1.5rem',
|
||||
alignItems: 'start',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'grid', gap: '1.25rem' }}>
|
||||
<div
|
||||
style={{
|
||||
background: '#fff',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '28px',
|
||||
padding: '1.5rem',
|
||||
}}
|
||||
>
|
||||
<BrandMark size="lg" />
|
||||
</div>
|
||||
<HomeSidebar items={sidebarWithContactCount} activeItem="mass-message" isMobile={!isDesktop} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: '1.25rem', minWidth: 0 }}>
|
||||
<header
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) auto',
|
||||
gap: '1rem',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: '1.1rem 1.25rem',
|
||||
borderRadius: '22px',
|
||||
background: '#fff',
|
||||
border: '1px solid var(--color-border)',
|
||||
}}
|
||||
>
|
||||
<h1 style={{ margin: 0, fontSize: '1.65rem' }}>Disparo em massa</h1>
|
||||
<p style={{ margin: '0.45rem 0 0', color: 'var(--color-text-soft)' }}>
|
||||
Envie templates aprovados para contatos da agenda ou numeros informados manualmente.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.9rem',
|
||||
justifySelf: isMobile ? 'stretch' : 'end',
|
||||
justifyContent: isMobile ? 'space-between' : 'flex-end',
|
||||
padding: '0.85rem 1rem',
|
||||
borderRadius: '22px',
|
||||
background: '#fff',
|
||||
border: '1px solid var(--color-border)',
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<strong style={{ display: 'block' }}>{userDisplay.name}</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.92rem' }}>
|
||||
Atendimento omnichannel
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: '16px',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
background: 'linear-gradient(135deg, var(--color-accent), var(--color-primary))',
|
||||
color: '#fff',
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
{userDisplay.initials}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<MassMessagePanel
|
||||
areas={areas}
|
||||
mode="agent"
|
||||
managedAreaNames={specialties}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
439
src/modules/home/pages/ContactsPage.jsx
Normal file
439
src/modules/home/pages/ContactsPage.jsx
Normal file
@ -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 (
|
||||
<main style={{ minHeight: '100vh', padding: '1.5rem' }}>
|
||||
<section
|
||||
style={{
|
||||
width: 'min(1680px, calc(100vw - 3rem))',
|
||||
margin: '0 auto',
|
||||
background: 'var(--color-surface-strong)',
|
||||
borderRadius: '32px',
|
||||
boxShadow: 'var(--shadow-lg)',
|
||||
padding: '1.5rem',
|
||||
display: 'grid',
|
||||
gap: '1.5rem',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isDesktop ? 'minmax(340px, 380px) minmax(0, 1fr)' : '1fr',
|
||||
gap: '1.5rem',
|
||||
alignItems: 'start',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'grid', gap: '1.25rem' }}>
|
||||
<div
|
||||
style={{
|
||||
background: '#fff',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '28px',
|
||||
padding: '1.5rem',
|
||||
}}
|
||||
>
|
||||
<BrandMark size="lg" />
|
||||
</div>
|
||||
<HomeSidebar items={sidebarWithCount} activeItem="contacts" isMobile={!isDesktop} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: '1.25rem', minWidth: 0 }}>
|
||||
<header
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) auto',
|
||||
gap: '1rem',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: '1.1rem 1.25rem',
|
||||
borderRadius: '22px',
|
||||
background: '#fff',
|
||||
border: '1px solid var(--color-border)',
|
||||
}}
|
||||
>
|
||||
<h1 style={{ margin: 0, fontSize: '1.65rem' }}>Contatos</h1>
|
||||
<p style={{ margin: '0.45rem 0 0', color: 'var(--color-text-soft)' }}>
|
||||
Agenda geral com WhatsApp, telefone para ligação/SMS, e-mail, tag e observação.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.9rem',
|
||||
justifySelf: isMobile ? 'stretch' : 'end',
|
||||
justifyContent: isMobile ? 'space-between' : 'flex-end',
|
||||
padding: '0.85rem 1rem',
|
||||
borderRadius: '22px',
|
||||
background: '#fff',
|
||||
border: '1px solid var(--color-border)',
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<strong style={{ display: 'block' }}>{userDisplay.name}</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.92rem' }}>
|
||||
Atendimento omnichannel
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: '16px',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
background: 'linear-gradient(135deg, var(--color-accent), var(--color-primary))',
|
||||
color: '#fff',
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
{userDisplay.initials}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : 'minmax(320px, 0.85fr) minmax(0, 1fr)',
|
||||
gap: '1rem',
|
||||
alignItems: 'start',
|
||||
}}
|
||||
>
|
||||
<aside
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 24,
|
||||
padding: '1rem',
|
||||
background: '#fff',
|
||||
display: 'grid',
|
||||
gap: '0.8rem',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.75rem', alignItems: 'center' }}>
|
||||
<div>
|
||||
<strong style={{ display: 'block' }}>Agenda</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.9rem' }}>
|
||||
{contacts.length} contato(s)
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={startNewContact}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: 14,
|
||||
padding: '0.7rem 0.85rem',
|
||||
background: 'var(--color-highlight)',
|
||||
color: '#132534',
|
||||
fontWeight: 900,
|
||||
}}
|
||||
>
|
||||
Novo
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
placeholder="Buscar contato"
|
||||
style={inputStyle}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'grid', gap: '0.45rem', maxHeight: 560, overflowY: 'auto', paddingRight: '0.2rem' }}>
|
||||
{filteredContacts.map((contact) => {
|
||||
const isSelected = selectedChatId === contact.chatId;
|
||||
return (
|
||||
<button
|
||||
key={contact.chatId}
|
||||
type="button"
|
||||
onClick={() => selectContact(contact)}
|
||||
style={{
|
||||
border: '1px solid',
|
||||
borderColor: isSelected ? 'rgba(0, 164, 183, 0.36)' : 'var(--color-border)',
|
||||
borderRadius: 16,
|
||||
padding: '0.8rem',
|
||||
background: isSelected ? 'rgba(0, 164, 183, 0.08)' : '#fff',
|
||||
textAlign: 'left',
|
||||
display: 'grid',
|
||||
gap: '0.25rem',
|
||||
}}
|
||||
>
|
||||
<strong>{contact.name}</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.88rem' }}>
|
||||
WhatsApp: +{contact.whatsappPhone}
|
||||
</span>
|
||||
{contact.email ? (
|
||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.84rem' }}>
|
||||
{contact.email}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{!filteredContacts.length ? (
|
||||
<span style={{ color: 'var(--color-text-soft)', fontWeight: 700 }}>
|
||||
Nenhum contato encontrado.
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<form
|
||||
onSubmit={handleSave}
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 24,
|
||||
padding: '1.2rem',
|
||||
background: '#fff',
|
||||
display: 'grid',
|
||||
gap: '0.9rem',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<strong style={{ display: 'block', fontSize: '1.08rem' }}>
|
||||
{selectedChatId ? 'Editar contato' : 'Novo contato'}
|
||||
</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)' }}>
|
||||
O WhatsApp é usado para vincular o contato à conversa.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(2, minmax(0, 1fr))', gap: '0.85rem' }}>
|
||||
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<span style={{ fontWeight: 700 }}>Nome</span>
|
||||
<input
|
||||
value={draft.name}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))}
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<span style={{ fontWeight: 700 }}>Tag</span>
|
||||
<input
|
||||
value={draft.tag}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, tag: event.target.value }))}
|
||||
placeholder="Ex: Analista de TI III"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<span style={{ fontWeight: 700 }}>Número WhatsApp</span>
|
||||
<input
|
||||
value={draft.whatsappPhone}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, whatsappPhone: event.target.value }))}
|
||||
placeholder="5511988267544"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<span style={{ fontWeight: 700 }}>Ligação/SMS</span>
|
||||
<input
|
||||
value={draft.callSmsPhone}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, callSmsPhone: event.target.value }))}
|
||||
placeholder="5511988267544"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<span style={{ fontWeight: 700 }}>E-mail</span>
|
||||
<input
|
||||
type="email"
|
||||
value={draft.email}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, email: event.target.value }))}
|
||||
placeholder="nome@empresa.com"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<span style={{ fontWeight: 700 }}>Observação</span>
|
||||
<textarea
|
||||
rows={5}
|
||||
value={draft.note}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, note: event.target.value }))}
|
||||
style={{ ...inputStyle, resize: 'vertical', lineHeight: 1.5 }}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{status ? <span style={{ color: status.includes('sucesso') ? 'var(--color-primary)' : '#b42318', fontWeight: 800 }}>{status}</span> : null}
|
||||
{isLoading ? <span style={{ color: 'var(--color-text-soft)', fontWeight: 700 }}>Carregando agenda...</span> : null}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSaving}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: 16,
|
||||
padding: '0.95rem 1rem',
|
||||
background: 'linear-gradient(135deg, var(--color-primary), #0b5a86)',
|
||||
color: '#fff',
|
||||
fontWeight: 900,
|
||||
opacity: isSaving ? 0.7 : 1,
|
||||
}}
|
||||
>
|
||||
{isSaving ? 'Salvando...' : 'Salvar contato'}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@ -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() {
|
||||
>
|
||||
<BrandMark size="lg" />
|
||||
</div>
|
||||
<HomeSidebar items={sidebarItems} activeItem="dashboard" isMobile={!isDesktop} />
|
||||
<HomeSidebar items={sidebarWithContactCount} activeItem="dashboard" isMobile={!isDesktop} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: '1.25rem', minWidth: 0 }}>
|
||||
|
||||
@ -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?' },
|
||||
],
|
||||
},
|
||||
|
||||
@ -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: <NewAttendancePage />,
|
||||
},
|
||||
{
|
||||
path: '/mass-message',
|
||||
element: <AgentMassMessagePage />,
|
||||
},
|
||||
{
|
||||
path: '/contacts',
|
||||
element: <ContactsPage />,
|
||||
},
|
||||
{
|
||||
path: '/admin/whatsapp',
|
||||
element: <WhatsappAdminPage />,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user