FEAT: Adicionado a lista de contatos a partir do modulo de novo atendimento
This commit is contained in:
parent
b605a4c507
commit
eeab43d2a4
@ -2,7 +2,6 @@ export function RecentContactsList({
|
||||
contacts,
|
||||
activeContactId,
|
||||
onSelectContact,
|
||||
selectedChannel,
|
||||
}) {
|
||||
return (
|
||||
<aside
|
||||
@ -25,11 +24,6 @@ export function RecentContactsList({
|
||||
<div style={{ display: 'grid', gap: '0.75rem' }}>
|
||||
{contacts.map((contact) => {
|
||||
const isActive = contact.id === activeContactId;
|
||||
const isPreferred = selectedChannel === 'call'
|
||||
? contact.channel === 'Ligacao'
|
||||
: selectedChannel === 'sms'
|
||||
? contact.channel === 'SMS'
|
||||
: contact.channel === 'WhatsApp';
|
||||
|
||||
return (
|
||||
<button
|
||||
@ -49,24 +43,22 @@ export function RecentContactsList({
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '1rem' }}>
|
||||
<strong>{contact.name}</strong>
|
||||
{isPreferred ? (
|
||||
<span
|
||||
style={{
|
||||
padding: '0.2rem 0.5rem',
|
||||
borderRadius: 999,
|
||||
background: 'rgba(0, 164, 183, 0.12)',
|
||||
color: 'var(--color-primary)',
|
||||
fontSize: '0.76rem',
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
Sugerido
|
||||
</span>
|
||||
) : null}
|
||||
<span
|
||||
style={{
|
||||
padding: '0.2rem 0.5rem',
|
||||
borderRadius: 999,
|
||||
background: 'rgba(43, 183, 65, 0.12)',
|
||||
color: '#25883a',
|
||||
fontSize: '0.76rem',
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
Agenda
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ color: 'var(--color-text-soft)' }}>{contact.phone}</span>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '1rem' }}>
|
||||
<span style={{ color: 'var(--color-primary)', fontWeight: 700 }}>{contact.channel}</span>
|
||||
<span style={{ color: 'var(--color-primary)', fontWeight: 700 }}>WhatsApp</span>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.85rem' }}>
|
||||
{contact.lastContact}
|
||||
</span>
|
||||
@ -74,6 +66,22 @@ export function RecentContactsList({
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
{contacts.length === 0 ? (
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '18px',
|
||||
padding: '1rem',
|
||||
background: 'rgba(0, 49, 80, 0.04)',
|
||||
color: 'var(--color-text-soft)',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.45,
|
||||
}}
|
||||
>
|
||||
Nenhum contato encontrado na agenda.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@ -1,42 +1,262 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { BrandMark } from '../../../shared/components/BrandMark';
|
||||
import { useViewport } from '../../../shared/hooks/useViewport';
|
||||
import { API_BASE_URL } from '../../../shared/services/apiConfig';
|
||||
import { getCurrentUser } from '../../auth/services/sessionService';
|
||||
import { listContactProfiles, saveContactProfile } from '../../chat/services/contactProfileService';
|
||||
import { getAccessOptions } from '../../management/services/adminAccessService';
|
||||
import { RecentContactsList } from '../components/RecentContactsList';
|
||||
import {
|
||||
attendanceAreas,
|
||||
attendanceChannels,
|
||||
recentContacts,
|
||||
} from '../services/attendanceMocks';
|
||||
import { attendanceChannels } from '../services/attendanceMocks';
|
||||
|
||||
const countryOptions = [
|
||||
{ id: 'br', label: 'Brasil', dialCode: '55', placeholder: '(11) 99999-9999' },
|
||||
{ id: 'us', label: 'EUA', dialCode: '1', placeholder: '(212) 555-0199' },
|
||||
{ id: 'ar', label: 'Argentina', dialCode: '54', placeholder: '11 1234-5678' },
|
||||
{ id: 'cl', label: 'Chile', dialCode: '56', placeholder: '9 1234 5678' },
|
||||
{ id: 'mx', label: 'Mexico', dialCode: '52', placeholder: '55 1234 5678' },
|
||||
];
|
||||
|
||||
function getUserId(user) {
|
||||
const value = user?.databaseId || user?.id;
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? numeric : null;
|
||||
}
|
||||
|
||||
function normalizePhone(phone) {
|
||||
return String(phone || '').replace(/\D/g, '');
|
||||
}
|
||||
|
||||
function getCountryById(countryId) {
|
||||
return countryOptions.find((country) => country.id === countryId) || countryOptions[0];
|
||||
}
|
||||
|
||||
function inferCountryId(phone) {
|
||||
const digits = normalizePhone(phone);
|
||||
const matchedCountry = countryOptions.find((country) => digits.startsWith(country.dialCode));
|
||||
return matchedCountry?.id || 'br';
|
||||
}
|
||||
|
||||
function stripCountryCode(phone, countryId) {
|
||||
const digits = normalizePhone(phone);
|
||||
const country = getCountryById(countryId);
|
||||
return digits.startsWith(country.dialCode) ? digits.slice(country.dialCode.length) : digits;
|
||||
}
|
||||
|
||||
function buildInternationalPhone(phone, countryId) {
|
||||
const country = getCountryById(countryId);
|
||||
const nationalDigits = stripCountryCode(phone, country.id);
|
||||
return nationalDigits ? `${country.dialCode}${nationalDigits}` : '';
|
||||
}
|
||||
|
||||
function formatPhone(phone, countryId = inferCountryId(phone)) {
|
||||
const country = getCountryById(countryId);
|
||||
const digits = stripCountryCode(phone, country.id);
|
||||
if (!digits) return '';
|
||||
|
||||
if (country.id === 'br') {
|
||||
if (digits.length >= 11) {
|
||||
return `(${digits.slice(0, 2)}) ${digits.slice(2, 7)}-${digits.slice(7, 11)}`;
|
||||
}
|
||||
if (digits.length >= 10) {
|
||||
return `(${digits.slice(0, 2)}) ${digits.slice(2, 6)}-${digits.slice(6, 10)}`;
|
||||
}
|
||||
if (digits.length > 2) {
|
||||
return `(${digits.slice(0, 2)}) ${digits.slice(2)}`;
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
|
||||
if (country.id === 'us') {
|
||||
if (digits.length >= 10) {
|
||||
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6, 10)}`;
|
||||
}
|
||||
if (digits.length > 3) {
|
||||
return `(${digits.slice(0, 3)}) ${digits.slice(3)}`;
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
|
||||
if (country.id === 'ar') {
|
||||
if (digits.length >= 10) {
|
||||
return `${digits.slice(0, 2)} ${digits.slice(2, 6)}-${digits.slice(6, 10)}`;
|
||||
}
|
||||
if (digits.length > 2) {
|
||||
return `${digits.slice(0, 2)} ${digits.slice(2)}`;
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
|
||||
if (country.id === 'cl') {
|
||||
if (digits.length >= 9) {
|
||||
return `${digits.slice(0, 1)} ${digits.slice(1, 5)} ${digits.slice(5, 9)}`;
|
||||
}
|
||||
if (digits.length > 1) {
|
||||
return `${digits.slice(0, 1)} ${digits.slice(1)}`;
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
|
||||
if (country.id === 'mx') {
|
||||
if (digits.length >= 10) {
|
||||
return `${digits.slice(0, 2)} ${digits.slice(2, 6)} ${digits.slice(6, 10)}`;
|
||||
}
|
||||
if (digits.length > 2) {
|
||||
return `${digits.slice(0, 2)} ${digits.slice(2)}`;
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
|
||||
return digits;
|
||||
}
|
||||
|
||||
function applyPhoneMask(value, countryId) {
|
||||
const digits = stripCountryCode(value, countryId);
|
||||
if (!digits) return '';
|
||||
return formatPhone(digits, countryId);
|
||||
}
|
||||
|
||||
function requiresUnsupportedTemplateFields(template) {
|
||||
const allowedFields = new Set(['nome', 'cliente']);
|
||||
const placeholders = String(template?.content || '').matchAll(/\{([a-zA-Z0-9_]+)\}/g);
|
||||
return Array.from(placeholders).some((match) => !allowedFields.has(String(match[1]).toLowerCase()));
|
||||
}
|
||||
|
||||
function renderTemplatePreview(content, form) {
|
||||
return String(content || '')
|
||||
.replace(/\{nome\}/gi, form.name.trim() || 'cliente')
|
||||
.replace(/\{cliente\}/gi, form.name.trim() || 'cliente');
|
||||
}
|
||||
|
||||
function formatLastContact(value) {
|
||||
if (!value) return 'Sem data';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return 'Sem data';
|
||||
return date.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
|
||||
}
|
||||
|
||||
function buildChatId(phone, countryId) {
|
||||
const digits = buildInternationalPhone(phone, countryId);
|
||||
if (!digits) return '';
|
||||
return `${digits}@c.us`;
|
||||
}
|
||||
|
||||
function normalizeAgendaContact(contact) {
|
||||
const phone = contact.phone || '';
|
||||
return {
|
||||
id: contact.chat_id,
|
||||
chatId: contact.chat_id,
|
||||
name: contact.name || phone || 'Contato sem nome',
|
||||
phone: formatPhone(phone),
|
||||
rawPhone: phone,
|
||||
countryId: inferCountryId(phone),
|
||||
company: contact.company || '',
|
||||
note: contact.note || '',
|
||||
lastContact: formatLastContact(contact.updated_at || contact.created_at),
|
||||
};
|
||||
}
|
||||
|
||||
function getUserAreas(user) {
|
||||
const normalizeArea = (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(normalizeArea).filter(Boolean);
|
||||
const primaryArea = normalizeArea(user?.areaPrincipal);
|
||||
if (primaryArea && !areas.includes(primaryArea)) {
|
||||
return [primaryArea, ...areas];
|
||||
}
|
||||
return areas;
|
||||
}
|
||||
|
||||
async function listWhatsappTemplates() {
|
||||
const response = await fetch(`${API_BASE_URL}/whatsapp/templates`);
|
||||
if (!response.ok) throw new Error('Falha ao carregar templates do WhatsApp.');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function startWhatsappAttendance(payload) {
|
||||
const response = await fetch(`${API_BASE_URL}/whatsapp/start-attendance`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) throw new Error('Falha ao iniciar atendimento pelo WhatsApp.');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export function NewAttendancePage() {
|
||||
const navigate = useNavigate();
|
||||
const { isWideDesktop, isDesktop, isTablet, isMobile } = useViewport();
|
||||
const currentUser = getCurrentUser();
|
||||
const currentUserId = getUserId(currentUser);
|
||||
const currentUserAreas = getUserAreas(currentUser);
|
||||
const [contacts, setContacts] = useState([]);
|
||||
const [templates, setTemplates] = useState([]);
|
||||
const [areaOptions, setAreaOptions] = useState([]);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [selectedChannelId, setSelectedChannelId] = useState('whatsapp');
|
||||
const [selectedArea, setSelectedArea] = useState('');
|
||||
const [selectedContactId, setSelectedContactId] = useState(recentContacts[0].id);
|
||||
const [customNumber, setCustomNumber] = useState('');
|
||||
const [selectedContactId, setSelectedContactId] = useState('');
|
||||
const [selectedTemplateId, setSelectedTemplateId] = useState('');
|
||||
const [selectedCountryId, setSelectedCountryId] = useState('br');
|
||||
const [form, setForm] = useState({ phone: '', name: '', company: '', note: '' });
|
||||
const [isLoadingContacts, setIsLoadingContacts] = useState(false);
|
||||
const [isStarting, setIsStarting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
async function loadContacts() {
|
||||
setIsLoadingContacts(true);
|
||||
try {
|
||||
const [contactsData, templatesData, accessOptions] = await Promise.all([
|
||||
listContactProfiles(),
|
||||
listWhatsappTemplates(),
|
||||
getAccessOptions(),
|
||||
]);
|
||||
if (!isMounted) return;
|
||||
setContacts(Array.isArray(contactsData) ? contactsData.map(normalizeAgendaContact) : []);
|
||||
const supportedTemplates = Array.isArray(templatesData)
|
||||
? templatesData.filter((template) => !requiresUnsupportedTemplateFields(template))
|
||||
: [];
|
||||
setTemplates(supportedTemplates);
|
||||
setSelectedTemplateId((current) => current || (supportedTemplates?.[0]?.id ? String(supportedTemplates[0].id) : ''));
|
||||
setAreaOptions(accessOptions.areas || []);
|
||||
setError('');
|
||||
} catch (err) {
|
||||
if (isMounted) setError(err.message);
|
||||
} finally {
|
||||
if (isMounted) setIsLoadingContacts(false);
|
||||
}
|
||||
}
|
||||
|
||||
loadContacts();
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const search = searchValue.trim().toLowerCase();
|
||||
const filteredContacts = useMemo(() => {
|
||||
if (!search) {
|
||||
return recentContacts;
|
||||
return contacts;
|
||||
}
|
||||
|
||||
return recentContacts.filter((contact) => {
|
||||
const haystack = `${contact.name} ${contact.phone} ${contact.channel}`.toLowerCase();
|
||||
return contacts.filter((contact) => {
|
||||
const haystack = `${contact.name} ${contact.phone} ${contact.rawPhone}`.toLowerCase();
|
||||
return haystack.includes(search);
|
||||
});
|
||||
}, [search]);
|
||||
|
||||
const selectedContact =
|
||||
filteredContacts.find((contact) => contact.id === selectedContactId) ||
|
||||
recentContacts.find((contact) => contact.id === selectedContactId) ||
|
||||
recentContacts[0];
|
||||
}, [contacts, search]);
|
||||
|
||||
const selectedChannel =
|
||||
attendanceChannels.find((channel) => channel.id === selectedChannelId) || attendanceChannels[0];
|
||||
const selectedCountry = getCountryById(selectedCountryId);
|
||||
const selectedTemplate = templates.find((template) => String(template.id) === String(selectedTemplateId));
|
||||
const primaryArea = areaOptions.find((area) => currentUserAreas.includes(area.nome));
|
||||
const isWhatsapp = selectedChannel.id === 'whatsapp';
|
||||
const canStartAttendance = isWhatsapp && Boolean(buildInternationalPhone(form.phone, selectedCountryId)) && Boolean(selectedTemplateId);
|
||||
|
||||
const gridTemplateColumns = isMobile
|
||||
? '1fr'
|
||||
@ -46,8 +266,71 @@ export function NewAttendancePage() {
|
||||
? 'minmax(280px, 340px) minmax(0, 1fr)'
|
||||
: '1fr';
|
||||
|
||||
function handleStartAttendance() {
|
||||
navigate(selectedChannel.route);
|
||||
function selectContact(contactId) {
|
||||
const contact = contacts.find((item) => item.id === contactId);
|
||||
if (!contact) return;
|
||||
|
||||
const contactCountryId = contact.countryId || inferCountryId(contact.rawPhone);
|
||||
setSelectedContactId(contactId);
|
||||
setSelectedCountryId(contactCountryId);
|
||||
setForm({
|
||||
phone: applyPhoneMask(contact.rawPhone || contact.phone || '', contactCountryId),
|
||||
name: contact.name || '',
|
||||
company: contact.company || '',
|
||||
note: contact.note || '',
|
||||
});
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
setSelectedContactId('');
|
||||
setSelectedCountryId('br');
|
||||
setForm({ phone: '', name: '', company: '', note: '' });
|
||||
}
|
||||
|
||||
async function handleStartAttendance() {
|
||||
if (!canStartAttendance) {
|
||||
setError('Informe um numero de WhatsApp para iniciar o atendimento.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedTemplateId) {
|
||||
setError('Selecione uma mensagem pre-aprovada para iniciar o atendimento.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentUserId) {
|
||||
setError('Nao foi possivel identificar o usuario logado.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsStarting(true);
|
||||
try {
|
||||
const fullPhone = buildInternationalPhone(form.phone, selectedCountryId);
|
||||
const chatId = selectedContactId || buildChatId(form.phone, selectedCountryId);
|
||||
const saved = await saveContactProfile(chatId, {
|
||||
phone: fullPhone,
|
||||
name: form.name,
|
||||
company: form.company,
|
||||
note: form.note,
|
||||
userId: currentUserId,
|
||||
});
|
||||
await startWhatsappAttendance({
|
||||
to: saved.chat_id || chatId,
|
||||
templateId: Number(selectedTemplateId),
|
||||
userId: currentUserId,
|
||||
areaId: primaryArea?.id || null,
|
||||
variables: {
|
||||
nome: form.name,
|
||||
cliente: form.name,
|
||||
},
|
||||
});
|
||||
setError('');
|
||||
navigate(`/chat?chatId=${encodeURIComponent(saved.chat_id || chatId)}`);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setIsStarting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@ -112,9 +395,8 @@ export function NewAttendancePage() {
|
||||
>
|
||||
<RecentContactsList
|
||||
contacts={filteredContacts}
|
||||
activeContactId={selectedContact.id}
|
||||
onSelectContact={setSelectedContactId}
|
||||
selectedChannel={selectedChannelId}
|
||||
activeContactId={selectedContactId}
|
||||
onSelectContact={selectContact}
|
||||
/>
|
||||
|
||||
<section
|
||||
@ -130,46 +412,24 @@ export function NewAttendancePage() {
|
||||
<div>
|
||||
<strong style={{ display: 'block', fontSize: '1.18rem' }}>Novo atendimento</strong>
|
||||
<p style={{ margin: '0.45rem 0 0', color: 'var(--color-text-soft)', lineHeight: 1.6 }}>
|
||||
Escolha o contato, o canal e a area opcional antes de iniciar. O fluxo e mockado
|
||||
e leva voce direto para chat ou ligacao.
|
||||
Informe um contato de WhatsApp ou selecione alguem da agenda para iniciar o atendimento.
|
||||
Para conversas novas, o primeiro envio usa uma mensagem pre-aprovada da Meta.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
<input
|
||||
type="search"
|
||||
value={searchValue}
|
||||
onChange={(event) => setSearchValue(event.target.value)}
|
||||
placeholder="Buscar contato salvo por nome ou numero"
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) auto',
|
||||
gap: '0.85rem',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '18px',
|
||||
padding: '0.95rem 1rem',
|
||||
background: '#fff',
|
||||
outline: 'none',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="search"
|
||||
value={searchValue}
|
||||
onChange={(event) => setSearchValue(event.target.value)}
|
||||
placeholder="Buscar contato por nome ou numero"
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '18px',
|
||||
padding: '0.95rem 1rem',
|
||||
background: '#fff',
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCustomNumber(selectedContact.phone)}
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '18px',
|
||||
padding: '0.95rem 1rem',
|
||||
background: '#fff',
|
||||
color: 'var(--color-primary)',
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
Novo numero
|
||||
</button>
|
||||
</div>
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
@ -180,12 +440,16 @@ export function NewAttendancePage() {
|
||||
>
|
||||
{attendanceChannels.map((channel) => {
|
||||
const isActive = channel.id === selectedChannelId;
|
||||
const isDisabled = Boolean(channel.disabled);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={channel.id}
|
||||
type="button"
|
||||
onClick={() => setSelectedChannelId(channel.id)}
|
||||
onClick={() => {
|
||||
if (!isDisabled) setSelectedChannelId(channel.id);
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
style={{
|
||||
border: '1px solid',
|
||||
borderColor: isActive ? `${channel.accent}44` : 'var(--color-border)',
|
||||
@ -195,15 +459,15 @@ export function NewAttendancePage() {
|
||||
textAlign: 'left',
|
||||
display: 'grid',
|
||||
gap: '0.45rem',
|
||||
opacity: isDisabled ? 0.58 : 1,
|
||||
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
<strong style={{ color: isActive ? channel.accent : 'var(--color-text)' }}>
|
||||
{channel.label}
|
||||
</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)' }}>
|
||||
{channel.id === 'call'
|
||||
? 'Inicia uma ligacao mock em tela cheia.'
|
||||
: 'Abre o fluxo de conversa em tempo real.'}
|
||||
{isDisabled ? 'Canal em construcao.' : 'Inicia uma conversa pelo WhatsApp.'}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
@ -213,15 +477,22 @@ export function NewAttendancePage() {
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : 'repeat(2, minmax(0, 1fr))',
|
||||
gridTemplateColumns: isMobile ? '1fr' : 'minmax(170px, 0.45fr) minmax(0, 1fr) minmax(0, 1fr)',
|
||||
gap: '1rem',
|
||||
}}
|
||||
>
|
||||
<label style={{ display: 'grid', gap: '0.45rem' }}>
|
||||
<span style={{ fontWeight: 600 }}>Area (opcional)</span>
|
||||
<span style={{ fontWeight: 600 }}>Pais</span>
|
||||
<select
|
||||
value={selectedArea}
|
||||
onChange={(event) => setSelectedArea(event.target.value)}
|
||||
value={selectedCountryId}
|
||||
onChange={(event) => {
|
||||
const nextCountryId = event.target.value;
|
||||
setSelectedCountryId(nextCountryId);
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
phone: applyPhoneMask(current.phone, nextCountryId),
|
||||
}));
|
||||
}}
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '18px',
|
||||
@ -230,22 +501,38 @@ export function NewAttendancePage() {
|
||||
outline: 'none',
|
||||
}}
|
||||
>
|
||||
<option value="">Selecionar depois</option>
|
||||
{attendanceAreas.map((area) => (
|
||||
<option key={area} value={area}>
|
||||
{area}
|
||||
{countryOptions.map((country) => (
|
||||
<option key={country.id} value={country.id}>
|
||||
{country.label} +{country.dialCode}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.45rem' }}>
|
||||
<span style={{ fontWeight: 600 }}>Numero selecionado</span>
|
||||
<span style={{ fontWeight: 600 }}>Numero do WhatsApp</span>
|
||||
<input
|
||||
type="text"
|
||||
value={customNumber || selectedContact.phone}
|
||||
onChange={(event) => setCustomNumber(event.target.value)}
|
||||
placeholder="+55 11 99999-9999"
|
||||
value={form.phone}
|
||||
onChange={(event) => setForm((current) => ({ ...current, phone: applyPhoneMask(event.target.value, selectedCountryId) }))}
|
||||
placeholder={selectedCountry.placeholder}
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '18px',
|
||||
padding: '0.95rem 1rem',
|
||||
background: '#fff',
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.45rem' }}>
|
||||
<span style={{ fontWeight: 600 }}>Nome do cliente</span>
|
||||
<input
|
||||
type="text"
|
||||
value={form.name}
|
||||
onChange={(event) => setForm((current) => ({ ...current, name: event.target.value }))}
|
||||
placeholder="Nome para salvar na agenda"
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '18px',
|
||||
@ -257,6 +544,92 @@ export function NewAttendancePage() {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : 'repeat(2, minmax(0, 1fr))',
|
||||
gap: '1rem',
|
||||
}}
|
||||
>
|
||||
<label style={{ display: 'grid', gap: '0.45rem' }}>
|
||||
<span style={{ fontWeight: 600 }}>Empresa</span>
|
||||
<input
|
||||
type="text"
|
||||
value={form.company}
|
||||
onChange={(event) => setForm((current) => ({ ...current, company: event.target.value }))}
|
||||
placeholder="Empresa ou conta vinculada"
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '18px',
|
||||
padding: '0.95rem 1rem',
|
||||
background: '#fff',
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.45rem' }}>
|
||||
<span style={{ fontWeight: 600 }}>Mensagem pre-aprovada</span>
|
||||
<select
|
||||
value={selectedTemplateId}
|
||||
onChange={(event) => setSelectedTemplateId(event.target.value)}
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '18px',
|
||||
padding: '0.95rem 1rem',
|
||||
background: '#fff',
|
||||
outline: 'none',
|
||||
}}
|
||||
>
|
||||
<option value="">Selecione um template</option>
|
||||
{templates.map((template) => (
|
||||
<option key={template.id} value={template.id}>
|
||||
{template.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{selectedTemplate ? (
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid rgba(0, 164, 183, 0.24)',
|
||||
borderRadius: '18px',
|
||||
padding: '1rem',
|
||||
background: 'rgba(0, 164, 183, 0.06)',
|
||||
color: 'var(--color-text)',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
<strong style={{ display: 'block', color: 'var(--color-primary)', marginBottom: '0.35rem' }}>
|
||||
Preview do template
|
||||
</strong>
|
||||
{renderTemplatePreview(selectedTemplate.content, form)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.45rem' }}>
|
||||
<span style={{ fontWeight: 600 }}>Observacao</span>
|
||||
<textarea
|
||||
rows={5}
|
||||
value={form.note}
|
||||
onChange={(event) => setForm((current) => ({ ...current, note: event.target.value }))}
|
||||
placeholder="Contexto inicial deste atendimento."
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '18px',
|
||||
padding: '0.95rem 1rem',
|
||||
background: '#fff',
|
||||
outline: 'none',
|
||||
resize: 'vertical',
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{error ? <span style={{ color: '#b42318', fontWeight: 700 }}>{error}</span> : null}
|
||||
{isLoadingContacts ? <span style={{ color: 'var(--color-text-soft)' }}>Carregando agenda...</span> : null}
|
||||
|
||||
<section
|
||||
style={{
|
||||
display: 'grid',
|
||||
@ -285,17 +658,20 @@ export function NewAttendancePage() {
|
||||
fontSize: '0.84rem',
|
||||
}}
|
||||
>
|
||||
Resumo do fluxo
|
||||
Resumo
|
||||
</span>
|
||||
<strong style={{ fontSize: '1.25rem' }}>{selectedContact.name}</strong>
|
||||
<strong style={{ fontSize: '1.25rem' }}>{form.name || 'Cliente sem nome'}</strong>
|
||||
<span style={{ color: 'rgba(255, 255, 255, 0.74)' }}>
|
||||
Canal escolhido: {selectedChannel.label}
|
||||
Canal: {selectedChannel.label}
|
||||
</span>
|
||||
<span style={{ color: 'rgba(255, 255, 255, 0.74)' }}>
|
||||
Numero: {customNumber || selectedContact.phone}
|
||||
Numero: {buildInternationalPhone(form.phone, selectedCountryId) ? `+${buildInternationalPhone(form.phone, selectedCountryId)}` : 'Nao informado'}
|
||||
</span>
|
||||
<span style={{ color: 'rgba(255, 255, 255, 0.74)' }}>
|
||||
Area: {selectedArea || 'Definir depois'}
|
||||
Empresa: {form.company || 'Nao informada'}
|
||||
</span>
|
||||
<span style={{ color: 'rgba(255, 255, 255, 0.74)' }}>
|
||||
Origem: {selectedContactId ? 'Agenda' : 'Novo contato'}
|
||||
</span>
|
||||
</article>
|
||||
|
||||
@ -311,25 +687,42 @@ export function NewAttendancePage() {
|
||||
>
|
||||
<strong>Proxima rota</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)' }}>
|
||||
{selectedChannel.route === '/call'
|
||||
? 'Ao iniciar, voce vai para a tela de ligacao.'
|
||||
: 'Ao iniciar, voce vai para a tela de chat.'}
|
||||
O contato sera salvo, o template sera enviado e a conversa abrira atribuida a voce no chat.
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleStartAttendance}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: '18px',
|
||||
padding: '1rem 1.1rem',
|
||||
background: 'linear-gradient(135deg, var(--color-primary), #0b5a86)',
|
||||
color: '#fff',
|
||||
fontWeight: 800,
|
||||
marginTop: '0.4rem',
|
||||
}}
|
||||
>
|
||||
Iniciar atendimento
|
||||
</button>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap', marginTop: '0.4rem' }}>
|
||||
{selectedContactId ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearSelection}
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '18px',
|
||||
padding: '1rem 1.1rem',
|
||||
background: '#fff',
|
||||
color: 'var(--color-primary)',
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
Limpar selecao
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleStartAttendance}
|
||||
disabled={!canStartAttendance || isStarting}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: '18px',
|
||||
padding: '1rem 1.1rem',
|
||||
background: 'linear-gradient(135deg, var(--color-primary), #0b5a86)',
|
||||
color: '#fff',
|
||||
fontWeight: 800,
|
||||
opacity: canStartAttendance && !isStarting ? 1 : 0.55,
|
||||
}}
|
||||
>
|
||||
{isStarting ? 'Iniciando...' : 'Iniciar atendimento'}
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@ -1,38 +1,5 @@
|
||||
export const attendanceChannels = [
|
||||
{ id: 'whatsapp', label: 'WhatsApp', route: '/chat', accent: '#2bb741' },
|
||||
{ id: 'sms', label: 'SMS', route: '/chat', accent: '#00a4b7' },
|
||||
{ id: 'call', label: 'Ligacao', route: '/call', accent: '#e5a22a' },
|
||||
];
|
||||
|
||||
export const attendanceAreas = ['Suporte', 'Financeiro', 'Comercial'];
|
||||
|
||||
export const recentContacts = [
|
||||
{
|
||||
id: 'maria-souza',
|
||||
name: 'Maria Souza',
|
||||
phone: '+55 11 99888-7766',
|
||||
channel: 'WhatsApp',
|
||||
lastContact: 'Hoje, 09:42',
|
||||
},
|
||||
{
|
||||
id: 'empresa-alpha',
|
||||
name: 'Empresa Alpha',
|
||||
phone: '+55 11 4002-2020',
|
||||
channel: 'Email',
|
||||
lastContact: 'Ontem, 16:18',
|
||||
},
|
||||
{
|
||||
id: 'joao-pedro',
|
||||
name: 'João Pedro',
|
||||
phone: '+55 31 98877-1102',
|
||||
channel: 'SMS',
|
||||
lastContact: 'Hoje, 08:15',
|
||||
},
|
||||
{
|
||||
id: 'beatriz-lima',
|
||||
name: 'Beatriz Lima',
|
||||
phone: '+55 21 99701-4455',
|
||||
channel: 'Ligação',
|
||||
lastContact: 'Hoje, 07:51',
|
||||
},
|
||||
{ id: 'sms', label: 'SMS', route: '/chat', accent: '#00a4b7', disabled: true },
|
||||
{ id: 'email', label: 'E-mail', route: '/chat', accent: '#e5a22a', disabled: true },
|
||||
];
|
||||
|
||||
@ -88,6 +88,8 @@ function SavedContactLabel({ contact }) {
|
||||
);
|
||||
}
|
||||
|
||||
const CHAT_LIST_HEIGHT = 'min(760px, calc(100vh - 160px))';
|
||||
|
||||
export function ChatConversationList({
|
||||
contacts,
|
||||
activeContactId,
|
||||
@ -105,9 +107,9 @@ export function ChatConversationList({
|
||||
display: 'grid',
|
||||
gridTemplateRows: 'auto minmax(0, 1fr)',
|
||||
gap: '0.85rem',
|
||||
height: isMobile ? 'auto' : '100%',
|
||||
maxHeight: isMobile ? 'none' : '100%',
|
||||
alignSelf: isMobile ? 'start' : 'stretch',
|
||||
height: isMobile ? 'auto' : CHAT_LIST_HEIGHT,
|
||||
maxHeight: isMobile ? 'none' : CHAT_LIST_HEIGHT,
|
||||
alignSelf: 'start',
|
||||
minHeight: 0,
|
||||
}}
|
||||
>
|
||||
|
||||
@ -274,7 +274,7 @@ export function ChatWindow({
|
||||
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
behavior: 'auto',
|
||||
});
|
||||
}, [messages, isReplying]);
|
||||
|
||||
@ -615,6 +615,8 @@ export function ChatWindow({
|
||||
? 'Aguardando conversa entrar em uma fila'
|
||||
: canReply
|
||||
? 'Escreva sua mensagem...'
|
||||
: assignmentLabel?.includes('Aguardando resposta')
|
||||
? 'Aguardando resposta do cliente'
|
||||
: canAssumeChat
|
||||
? 'Assuma o atendimento para responder'
|
||||
: 'Atendimento bloqueado para resposta'
|
||||
|
||||
@ -154,6 +154,38 @@ function buildFallbackContacts() {
|
||||
}));
|
||||
}
|
||||
|
||||
function normalizeComparableContact(contact) {
|
||||
return {
|
||||
id: contact.id,
|
||||
name: contact.name,
|
||||
preview: contact.preview,
|
||||
time: contact.time,
|
||||
unread: contact.unread,
|
||||
area: contact.area,
|
||||
areaId: contact.areaId,
|
||||
lastSeen: contact.lastSeen,
|
||||
lastMessageFromMe: contact.lastMessageFromMe,
|
||||
assignmentStatus: contact.assignment?.status || null,
|
||||
assignmentUserId: contact.assignment?.user_id || null,
|
||||
assignmentAreaId: contact.assignment?.area_id || null,
|
||||
transferNote: contact.assignment?.transfer_note || null,
|
||||
awaitingCustomerReply: contact.assignment?.awaiting_customer_reply || false,
|
||||
contactName: contact.contactProfile?.name || null,
|
||||
contactCompany: contact.contactProfile?.company || null,
|
||||
contactNote: contact.contactProfile?.note || null,
|
||||
contactPhone: contact.contactProfile?.phone || null,
|
||||
};
|
||||
}
|
||||
|
||||
function areContactListsEqual(currentContacts, nextContacts) {
|
||||
if (currentContacts.length !== nextContacts.length) return false;
|
||||
return currentContacts.every((contact, index) => {
|
||||
const currentComparable = normalizeComparableContact(contact);
|
||||
const nextComparable = normalizeComparableContact(nextContacts[index]);
|
||||
return JSON.stringify(currentComparable) === JSON.stringify(nextComparable);
|
||||
});
|
||||
}
|
||||
|
||||
function getUserId(user) {
|
||||
const value = user?.databaseId || user?.id;
|
||||
const numeric = Number(value);
|
||||
@ -200,6 +232,7 @@ export function useChat() {
|
||||
const [isLoadingMessages, setIsLoadingMessages] = useState(false);
|
||||
const [apiError, setApiError] = useState(null);
|
||||
const activeContactRef = useRef(activeContactId);
|
||||
const contactsRef = useRef(contacts);
|
||||
|
||||
const activeContact = useMemo(
|
||||
() => {
|
||||
@ -225,14 +258,17 @@ export function useChat() {
|
||||
const isAssignedToCurrentUser = Boolean(
|
||||
activeAssignment?.user_id && currentUserId && Number(activeAssignment.user_id) === currentUserId,
|
||||
);
|
||||
const isWaitingCustomerReply = Boolean(activeAssignment?.awaiting_customer_reply);
|
||||
const isQueuedForUserArea = Boolean(
|
||||
activeAssignment?.status === 'queued' &&
|
||||
(!activeAssignment.area_nome || currentUserAreas.includes(activeAssignment.area_nome)),
|
||||
);
|
||||
const canAssumeChat = Boolean(activeContact?.id?.includes('@') && currentUserId && isQueuedForUserArea);
|
||||
const canReply = Boolean(isAssignedToCurrentUser);
|
||||
const canReply = Boolean(isAssignedToCurrentUser && !isWaitingCustomerReply);
|
||||
const assignmentLabel = activeAssignment?.user_id
|
||||
? `Atendimento com ${activeAssignment.user_nome || 'outro atendente'}`
|
||||
? isWaitingCustomerReply
|
||||
? 'Aguardando resposta do cliente para liberar novas mensagens'
|
||||
: `Atendimento com ${activeAssignment.user_nome || 'outro atendente'}`
|
||||
: activeAssignment?.area_nome
|
||||
? `Na fila de ${activeAssignment.area_nome}`
|
||||
: 'Sem fila definida';
|
||||
@ -250,6 +286,10 @@ export function useChat() {
|
||||
activeContactRef.current = activeContactId;
|
||||
}, [activeContactId]);
|
||||
|
||||
useEffect(() => {
|
||||
contactsRef.current = contacts;
|
||||
}, [contacts]);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
@ -282,10 +322,10 @@ export function useChat() {
|
||||
return currentUserAreas.includes(contact.assignment.area_nome);
|
||||
}
|
||||
|
||||
async function loadChats() {
|
||||
async function loadChats({ showLoading = false } = {}) {
|
||||
if (whatsappStatus !== 'CONNECTED') {
|
||||
const fallbackContacts = buildFallbackContacts();
|
||||
setContacts(fallbackContacts);
|
||||
setContacts((current) => (areContactListsEqual(current, fallbackContacts) ? current : fallbackContacts));
|
||||
setActiveContactId((current) =>
|
||||
fallbackContacts.some((contact) => contact.id === current) ? current : fallbackContacts[0]?.id,
|
||||
);
|
||||
@ -293,7 +333,9 @@ export function useChat() {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoadingChats(true);
|
||||
if (showLoading) {
|
||||
setIsLoadingChats(true);
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/whatsapp/chats`);
|
||||
if (!response.ok) throw new Error('Falha ao carregar chats do WhatsApp.');
|
||||
@ -301,7 +343,7 @@ export function useChat() {
|
||||
if (!Array.isArray(data)) return;
|
||||
|
||||
const nextContacts = data.map(normalizeChat).filter(canSeeContact);
|
||||
setContacts(nextContacts);
|
||||
setContacts((current) => (areContactListsEqual(current, nextContacts) ? current : nextContacts));
|
||||
setActiveContactId((current) =>
|
||||
nextContacts.some((contact) => contact.id === current) ? current : nextContacts[0]?.id || '',
|
||||
);
|
||||
@ -318,7 +360,7 @@ export function useChat() {
|
||||
|
||||
async function guardedLoadChats() {
|
||||
if (!isMounted) return;
|
||||
await loadChats();
|
||||
await loadChats({ showLoading: contactsRef.current.length === 0 });
|
||||
}
|
||||
|
||||
guardedLoadChats();
|
||||
@ -426,7 +468,7 @@ export function useChat() {
|
||||
});
|
||||
|
||||
clearIncomingMessage();
|
||||
window.setTimeout(loadChats, 1200);
|
||||
window.setTimeout(() => loadChats({ showLoading: false }), 1200);
|
||||
}, [incomingMessage, clearIncomingMessage]);
|
||||
|
||||
function updateContact(contactId, updater) {
|
||||
@ -558,11 +600,16 @@ export function useChat() {
|
||||
const targetIsAssignedToCurrentUser = Boolean(
|
||||
targetAssignment?.user_id && currentUserId && Number(targetAssignment.user_id) === currentUserId,
|
||||
);
|
||||
const targetIsWaitingCustomerReply = Boolean(targetAssignment?.awaiting_customer_reply);
|
||||
try {
|
||||
if (!targetIsAssignedToCurrentUser) {
|
||||
setApiError('Assuma o atendimento antes de responder.');
|
||||
return;
|
||||
}
|
||||
if (targetIsWaitingCustomerReply) {
|
||||
setApiError('Aguarde o cliente responder antes de enviar novas mensagens.');
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
setApiError(error.message);
|
||||
return;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { BrandMark } from '../../../shared/components/BrandMark';
|
||||
import { useViewport } from '../../../shared/hooks/useViewport';
|
||||
import { ChatConversationList } from '../components/ChatConversationList';
|
||||
@ -10,7 +10,7 @@ import { useChat } from '../hooks/useChat';
|
||||
import { quickReplies } from '../services/chatMocks';
|
||||
|
||||
export function ChatPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { isWideDesktop, isDesktop, isTablet, isMobile } = useViewport();
|
||||
const {
|
||||
contacts,
|
||||
@ -49,13 +49,24 @@ export function ChatPage() {
|
||||
submitTransfer,
|
||||
} = useChat();
|
||||
const requestedChatId = searchParams.get('chatId');
|
||||
const handledRequestedChatIdRef = useRef('');
|
||||
const [isContactPanelOpen, setIsContactPanelOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!requestedChatId) return;
|
||||
if (handledRequestedChatIdRef.current === requestedChatId) return;
|
||||
if (!contacts.some((contact) => contact.id === requestedChatId)) return;
|
||||
handledRequestedChatIdRef.current = requestedChatId;
|
||||
setActiveContactId(requestedChatId);
|
||||
}, [requestedChatId, contacts, setActiveContactId]);
|
||||
setSearchParams({}, { replace: true });
|
||||
}, [requestedChatId, contacts, setActiveContactId, setSearchParams]);
|
||||
|
||||
function selectContact(contactId) {
|
||||
setActiveContactId(contactId);
|
||||
if (requestedChatId) {
|
||||
setSearchParams({}, { replace: true });
|
||||
}
|
||||
}
|
||||
|
||||
const gridTemplateColumns = isMobile
|
||||
? '1fr'
|
||||
@ -128,7 +139,7 @@ export function ChatPage() {
|
||||
<ChatConversationList
|
||||
contacts={contacts}
|
||||
activeContactId={activeContactId}
|
||||
onSelectContact={setActiveContactId}
|
||||
onSelectContact={selectContact}
|
||||
onOpenContact={() => {
|
||||
setIsTransferOpen(false);
|
||||
setIsContactPanelOpen(true);
|
||||
|
||||
@ -5,7 +5,7 @@ export const chatContacts = [
|
||||
channel: 'WhatsApp',
|
||||
status: 'away',
|
||||
area: 'Suporte',
|
||||
lastSeen: 'Ultima atividade as 09:42',
|
||||
lastSeen: 'Última atividade as 09:42',
|
||||
preview: 'Preciso atualizar o cadastro do meu pedido.',
|
||||
time: '09:42',
|
||||
unread: 2,
|
||||
@ -22,7 +22,7 @@ export const chatContacts = [
|
||||
channel: 'SMS',
|
||||
status: 'offline',
|
||||
area: 'Financeiro',
|
||||
lastSeen: 'Ultima atividade as 08:15',
|
||||
lastSeen: 'Última atividade as 08:15',
|
||||
preview: 'Pode me ligar em 10 minutos?',
|
||||
time: '08:15',
|
||||
unread: 1,
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
import { API_BASE_URL } from '../../../shared/services/apiConfig';
|
||||
|
||||
export async function listContactProfiles() {
|
||||
const response = await fetch(`${API_BASE_URL}/contacts`);
|
||||
if (!response.ok) throw new Error('Falha ao carregar agenda.');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getContactProfile(chatId) {
|
||||
const response = await fetch(`${API_BASE_URL}/contacts/${encodeURIComponent(chatId)}`);
|
||||
if (!response.ok) throw new Error('Falha ao carregar contato.');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user