omnichannel-frontend/src/modules/home/pages/ContactsPage.jsx

452 lines
14 KiB
JavaScript

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 ContactsPanel({ embedded = false }) {
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);
}
}
const content = (
<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 }}>Etiqueta de identificação</span>
<input
value={draft.tag}
onChange={(event) => setDraft((current) => ({ ...current, tag: event.target.value }))}
placeholder="Ex: Departamento, vaga ou conta vinculada"
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>
);
if (embedded) {
return content;
}
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, etiqueta 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>
{content}
</div>
</div>
</section>
</main>
);
}
export function ContactsPage() {
return <ContactsPanel />;
}