2026-05-26 09:08:08 -03:00
|
|
|
import { useEffect, useMemo, useState } from 'react';
|
2026-06-01 17:13:46 -03:00
|
|
|
import { apiRequest } from '../../../shared/services/apiClient';
|
2026-05-26 09:08:08 -03:00
|
|
|
import { getCurrentUser } from '../../auth/services/sessionService';
|
|
|
|
|
import { listContactProfiles } from '../../chat/services/contactProfileService';
|
|
|
|
|
import { DataPanel } from './DataPanel';
|
|
|
|
|
import { listTemplates } from '../services/templateService';
|
|
|
|
|
|
|
|
|
|
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 normalizePhoneToChatId(value) {
|
|
|
|
|
const digits = String(value || '').replace(/\D/g, '');
|
|
|
|
|
if (!digits) return '';
|
|
|
|
|
return `${digits}@c.us`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getPhoneFromChatId(chatId) {
|
|
|
|
|
return String(chatId || '').split('@')[0].replace(/\D/g, '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeContact(contact) {
|
|
|
|
|
const phone = String(contact.phone || getPhoneFromChatId(contact.chat_id)).replace(/\D/g, '');
|
|
|
|
|
return {
|
|
|
|
|
id: contact.chat_id || normalizePhoneToChatId(phone),
|
|
|
|
|
name: contact.name || phone || 'Contato sem nome',
|
|
|
|
|
company: contact.company || '',
|
|
|
|
|
phone,
|
|
|
|
|
chatId: contact.chat_id || normalizePhoneToChatId(phone),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderPreview(content, variables) {
|
|
|
|
|
const name = variables?.nome || 'colaborador';
|
|
|
|
|
return String(content || '')
|
|
|
|
|
.replace(/\{nome\}/gi, name || 'colaborador')
|
|
|
|
|
.replace(/\{cliente\}/gi, name || 'colaborador')
|
|
|
|
|
.replace(/\{data\}/gi, variables?.data || '{data}')
|
|
|
|
|
.replace(/\{link\}/gi, variables?.link || '{link}')
|
|
|
|
|
.replace(/\{variavel\}/gi, variables?.variavel || '{variavel}')
|
|
|
|
|
.replace(/\{variável\}/gi, variables?.variavel || '{variável}');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function startAttendance(payload) {
|
2026-06-01 17:13:46 -03:00
|
|
|
return apiRequest('/whatsapp/start-attendance', {
|
2026-05-26 09:08:08 -03:00
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify(payload),
|
2026-06-01 17:13:46 -03:00
|
|
|
fallbackMessage: 'Falha ao enviar disparo.',
|
2026-05-26 09:08:08 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function MassMessagePanel({
|
|
|
|
|
areas,
|
|
|
|
|
managedAreaNames = [],
|
|
|
|
|
mode = 'admin',
|
|
|
|
|
isMobile = false,
|
|
|
|
|
}) {
|
|
|
|
|
const currentUserId = getUserId(getCurrentUser());
|
|
|
|
|
const isAdmin = mode === 'admin';
|
|
|
|
|
const visibleAreaNames = isAdmin ? [] : managedAreaNames;
|
|
|
|
|
const [templates, setTemplates] = useState([]);
|
|
|
|
|
const [contacts, setContacts] = useState([]);
|
|
|
|
|
const [selectedAreaId, setSelectedAreaId] = useState('');
|
|
|
|
|
const [selectedTemplateId, setSelectedTemplateId] = useState('');
|
|
|
|
|
const [defaultName, setDefaultName] = useState('colaborador');
|
|
|
|
|
const [templateDate, setTemplateDate] = useState('');
|
|
|
|
|
const [templateLink, setTemplateLink] = useState('');
|
|
|
|
|
const [templateCustomVariable, setTemplateCustomVariable] = useState('');
|
|
|
|
|
const [numbersText, setNumbersText] = useState('');
|
|
|
|
|
const [contactSearch, setContactSearch] = useState('');
|
|
|
|
|
const [selectedContactIds, setSelectedContactIds] = useState([]);
|
|
|
|
|
const [isSending, setIsSending] = useState(false);
|
|
|
|
|
const [results, setResults] = useState([]);
|
|
|
|
|
const [status, setStatus] = useState('');
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let isMounted = true;
|
|
|
|
|
|
|
|
|
|
listTemplates()
|
|
|
|
|
.then((data) => {
|
|
|
|
|
if (!isMounted) return;
|
|
|
|
|
const approved = Array.isArray(data)
|
|
|
|
|
? data.filter((template) => {
|
|
|
|
|
const isApproved = template.status === 'approved';
|
|
|
|
|
const isManaged = !visibleAreaNames.length || visibleAreaNames.includes(template.area_nome);
|
|
|
|
|
return isApproved && isManaged;
|
|
|
|
|
})
|
|
|
|
|
: [];
|
|
|
|
|
setTemplates(approved);
|
|
|
|
|
setSelectedTemplateId((current) => current || (approved[0]?.id ? String(approved[0].id) : ''));
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
if (isMounted) setStatus(error.message);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
isMounted = false;
|
|
|
|
|
};
|
|
|
|
|
}, [visibleAreaNames.join('|')]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let isMounted = true;
|
|
|
|
|
|
|
|
|
|
listContactProfiles()
|
|
|
|
|
.then((data) => {
|
|
|
|
|
if (!isMounted) return;
|
|
|
|
|
setContacts(Array.isArray(data) ? data.map(normalizeContact).filter((contact) => contact.phone) : []);
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
if (isMounted) setStatus(error.message);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
isMounted = false;
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const selectedTemplate = templates.find((template) => String(template.id) === String(selectedTemplateId));
|
|
|
|
|
const filteredTemplates = useMemo(() => {
|
|
|
|
|
if (!selectedAreaId) return templates;
|
|
|
|
|
return templates.filter((template) => String(template.area_id || '') === String(selectedAreaId));
|
|
|
|
|
}, [templates, selectedAreaId]);
|
|
|
|
|
|
|
|
|
|
const numbers = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
numbersText
|
|
|
|
|
.split(/\r?\n|,|;/)
|
|
|
|
|
.map((item) => item.trim())
|
|
|
|
|
.filter(Boolean),
|
|
|
|
|
[numbersText],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const selectedContacts = useMemo(
|
|
|
|
|
() => contacts.filter((contact) => selectedContactIds.includes(contact.id)),
|
|
|
|
|
[contacts, selectedContactIds],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const filteredContacts = useMemo(() => {
|
|
|
|
|
const search = contactSearch.trim().toLowerCase();
|
|
|
|
|
if (!search) return contacts;
|
|
|
|
|
return contacts.filter((contact) =>
|
|
|
|
|
`${contact.name} ${contact.company} ${contact.phone}`.toLowerCase().includes(search),
|
|
|
|
|
);
|
|
|
|
|
}, [contacts, contactSearch]);
|
|
|
|
|
|
|
|
|
|
const recipients = useMemo(() => {
|
|
|
|
|
const items = [];
|
|
|
|
|
const seen = new Set();
|
|
|
|
|
|
|
|
|
|
numbers.forEach((number) => {
|
|
|
|
|
const chatId = normalizePhoneToChatId(number);
|
|
|
|
|
if (!chatId || seen.has(chatId)) return;
|
|
|
|
|
seen.add(chatId);
|
|
|
|
|
items.push({
|
|
|
|
|
id: chatId,
|
|
|
|
|
number,
|
|
|
|
|
chatId,
|
|
|
|
|
name: defaultName,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return items;
|
|
|
|
|
}, [defaultName, numbers]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (selectedTemplate && filteredTemplates.some((template) => String(template.id) === String(selectedTemplateId))) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setSelectedTemplateId(filteredTemplates[0]?.id ? String(filteredTemplates[0].id) : '');
|
|
|
|
|
}, [filteredTemplates, selectedTemplate, selectedTemplateId]);
|
|
|
|
|
|
|
|
|
|
function toggleContact(contact) {
|
|
|
|
|
const phone = String(contact.phone || '').replace(/\D/g, '');
|
|
|
|
|
if (!phone) return;
|
|
|
|
|
|
|
|
|
|
setSelectedContactIds((current) => {
|
|
|
|
|
const isSelected = current.includes(contact.id);
|
|
|
|
|
return isSelected ? current.filter((id) => id !== contact.id) : [...current, contact.id];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setNumbersText((current) => {
|
|
|
|
|
const currentNumbers = current
|
|
|
|
|
.split(/\r?\n|,|;/)
|
|
|
|
|
.map((item) => item.trim())
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
const exists = currentNumbers.some((item) => String(item).replace(/\D/g, '') === phone);
|
|
|
|
|
const nextNumbers = exists
|
|
|
|
|
? currentNumbers.filter((item) => String(item).replace(/\D/g, '') !== phone)
|
|
|
|
|
: [...currentNumbers, phone];
|
|
|
|
|
return nextNumbers.join('\n');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearSelectedContacts() {
|
|
|
|
|
const selectedPhones = new Set(selectedContacts.map((contact) => contact.phone));
|
|
|
|
|
setSelectedContactIds([]);
|
|
|
|
|
setNumbersText((current) =>
|
|
|
|
|
current
|
|
|
|
|
.split(/\r?\n|,|;/)
|
|
|
|
|
.map((item) => item.trim())
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.filter((item) => !selectedPhones.has(String(item).replace(/\D/g, '')))
|
|
|
|
|
.join('\n'),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function sendSelectedRecipients() {
|
|
|
|
|
if (!currentUserId) {
|
|
|
|
|
setStatus('Não foi possível identificar o usuário logado.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!selectedTemplateId) {
|
|
|
|
|
setStatus('Selecione um template aprovado.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!recipients.length) {
|
|
|
|
|
setStatus('Informe ao menos um número ou selecione contatos da agenda.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setIsSending(true);
|
|
|
|
|
setResults([]);
|
|
|
|
|
setStatus('');
|
|
|
|
|
|
|
|
|
|
const nextResults = [];
|
|
|
|
|
for (const recipient of recipients) {
|
|
|
|
|
if (!recipient.chatId) {
|
|
|
|
|
nextResults.push({ number: recipient.number, status: 'erro', detail: 'Número inválido' });
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await startAttendance({
|
|
|
|
|
to: recipient.chatId,
|
|
|
|
|
templateId: Number(selectedTemplateId),
|
|
|
|
|
userId: currentUserId,
|
|
|
|
|
areaId: selectedTemplate?.area_id || null,
|
|
|
|
|
variables: {
|
|
|
|
|
nome: defaultName,
|
|
|
|
|
cliente: defaultName,
|
|
|
|
|
data: templateDate,
|
|
|
|
|
link: templateLink,
|
|
|
|
|
variavel: templateCustomVariable,
|
|
|
|
|
'variável': templateCustomVariable,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
nextResults.push({
|
|
|
|
|
number: recipient.number,
|
|
|
|
|
status: 'enviado',
|
|
|
|
|
detail: `${recipient.name || 'Contato'} - template enviado e atendimento iniciado`,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
nextResults.push({ number: recipient.number, status: 'erro', detail: error.message });
|
|
|
|
|
}
|
|
|
|
|
setResults([...nextResults]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setStatus(`Disparo finalizado: ${nextResults.filter((item) => item.status === 'enviado').length} enviados.`);
|
|
|
|
|
setIsSending(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<DataPanel
|
|
|
|
|
title="Disparo em massa"
|
|
|
|
|
description="Envie templates aprovados para uma lista de colaboradores. Após o envio, a conversa aguarda resposta do cliente."
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: 'grid',
|
|
|
|
|
gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) minmax(320px, 0.85fr)',
|
|
|
|
|
gap: '1rem',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div style={{ display: 'grid', gap: '0.85rem' }}>
|
|
|
|
|
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
|
|
|
|
<span style={{ fontWeight: 700 }}>Especialidade</span>
|
|
|
|
|
<select value={selectedAreaId} onChange={(event) => setSelectedAreaId(event.target.value)} style={inputStyle}>
|
|
|
|
|
<option value="">Todas as especialidades</option>
|
|
|
|
|
{areas
|
|
|
|
|
.filter((area) => isAdmin || !managedAreaNames.length || managedAreaNames.includes(area.nome))
|
|
|
|
|
.map((area) => (
|
|
|
|
|
<option key={area.id} value={area.id}>
|
|
|
|
|
{area.nome}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
|
|
|
|
<span style={{ fontWeight: 700 }}>Template aprovado</span>
|
|
|
|
|
<select value={selectedTemplateId} onChange={(event) => setSelectedTemplateId(event.target.value)} style={inputStyle}>
|
|
|
|
|
<option value="">Selecione</option>
|
|
|
|
|
{filteredTemplates.map((template) => (
|
|
|
|
|
<option key={template.id} value={template.id}>
|
|
|
|
|
{template.name}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
|
|
|
|
<span style={{ fontWeight: 700 }}>Nome usado no preview</span>
|
|
|
|
|
<input value={defaultName} onChange={(event) => setDefaultName(event.target.value)} style={inputStyle} />
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, minmax(0, 1fr))', gap: '0.85rem' }}>
|
|
|
|
|
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
|
|
|
|
<span style={{ fontWeight: 700 }}>Data</span>
|
|
|
|
|
<input value={templateDate} onChange={(event) => setTemplateDate(event.target.value)} placeholder="Ex: 26/05/2026" style={inputStyle} />
|
|
|
|
|
</label>
|
|
|
|
|
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
|
|
|
|
<span style={{ fontWeight: 700 }}>Link</span>
|
|
|
|
|
<input value={templateLink} onChange={(event) => setTemplateLink(event.target.value)} placeholder="https://..." style={inputStyle} />
|
|
|
|
|
</label>
|
|
|
|
|
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
|
|
|
|
<span style={{ fontWeight: 700 }}>Variável</span>
|
|
|
|
|
<input value={templateCustomVariable} onChange={(event) => setTemplateCustomVariable(event.target.value)} placeholder="Valor livre" style={inputStyle} />
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
|
|
|
|
<span style={{ fontWeight: 700 }}>Números manuais</span>
|
|
|
|
|
<textarea
|
|
|
|
|
rows={8}
|
|
|
|
|
value={numbersText}
|
|
|
|
|
onChange={(event) => setNumbersText(event.target.value)}
|
|
|
|
|
placeholder="5511999999999 5511888888888"
|
|
|
|
|
style={{ ...inputStyle, resize: 'vertical', lineHeight: 1.5 }}
|
|
|
|
|
/>
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={sendSelectedRecipients}
|
|
|
|
|
disabled={isSending}
|
|
|
|
|
style={{
|
|
|
|
|
border: 'none',
|
|
|
|
|
borderRadius: 16,
|
|
|
|
|
padding: '0.95rem 1rem',
|
|
|
|
|
background: 'linear-gradient(135deg, var(--color-primary), #0b5a86)',
|
|
|
|
|
color: '#fff',
|
|
|
|
|
fontWeight: 800,
|
|
|
|
|
opacity: isSending ? 0.7 : 1,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{isSending ? 'Enviando...' : `Enviar para ${recipients.length || 0} contato(s)`}
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{status ? <span style={{ color: 'var(--color-primary)', fontWeight: 800 }}>{status}</span> : null}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<aside style={{ display: 'grid', gap: '0.85rem', alignContent: 'start' }}>
|
|
|
|
|
<article
|
|
|
|
|
style={{
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
borderRadius: 18,
|
|
|
|
|
padding: '1rem',
|
|
|
|
|
background: '#fff',
|
|
|
|
|
display: 'grid',
|
|
|
|
|
gap: '0.75rem',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div>
|
|
|
|
|
<strong style={{ display: 'block' }}>Agenda de contatos</strong>
|
|
|
|
|
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.9rem' }}>
|
|
|
|
|
Selecione contatos salvos para incluir no disparo.
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<input
|
|
|
|
|
value={contactSearch}
|
|
|
|
|
onChange={(event) => setContactSearch(event.target.value)}
|
|
|
|
|
placeholder="Buscar por nome, empresa ou telefone"
|
|
|
|
|
style={inputStyle}
|
|
|
|
|
/>
|
|
|
|
|
<div style={{ display: 'grid', gap: '0.45rem', maxHeight: 260, overflowY: 'auto', paddingRight: '0.2rem' }}>
|
|
|
|
|
{filteredContacts.map((contact) => {
|
|
|
|
|
const isSelected = selectedContactIds.includes(contact.id);
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={contact.id}
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => toggleContact(contact)}
|
|
|
|
|
style={{
|
|
|
|
|
border: '1px solid',
|
|
|
|
|
borderColor: isSelected ? 'rgba(0, 164, 183, 0.36)' : 'var(--color-border)',
|
|
|
|
|
borderRadius: 14,
|
|
|
|
|
padding: '0.7rem',
|
|
|
|
|
background: isSelected ? 'rgba(0, 164, 183, 0.08)' : '#fff',
|
|
|
|
|
textAlign: 'left',
|
|
|
|
|
display: 'grid',
|
|
|
|
|
gap: '0.2rem',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<strong>{contact.name}</strong>
|
|
|
|
|
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.88rem' }}>
|
|
|
|
|
+{contact.phone}{contact.company ? ` · ${contact.company}` : ''}
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
{!filteredContacts.length ? (
|
|
|
|
|
<span style={{ color: 'var(--color-text-soft)', fontWeight: 700 }}>
|
|
|
|
|
Nenhum contato encontrado na agenda.
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
{selectedContacts.length ? (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={clearSelectedContacts}
|
|
|
|
|
style={{
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
borderRadius: 14,
|
|
|
|
|
padding: '0.75rem',
|
|
|
|
|
background: '#fff',
|
|
|
|
|
color: 'var(--color-primary)',
|
|
|
|
|
fontWeight: 800,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Limpar seleção ({selectedContacts.length})
|
|
|
|
|
</button>
|
|
|
|
|
) : null}
|
|
|
|
|
</article>
|
|
|
|
|
|
|
|
|
|
<article
|
|
|
|
|
style={{
|
|
|
|
|
border: '1px solid rgba(0, 164, 183, 0.24)',
|
|
|
|
|
borderRadius: 18,
|
|
|
|
|
padding: '1rem',
|
|
|
|
|
background: 'rgba(0, 164, 183, 0.06)',
|
|
|
|
|
lineHeight: 1.5,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<strong style={{ display: 'block', color: 'var(--color-primary)', marginBottom: '0.45rem' }}>
|
|
|
|
|
Preview
|
|
|
|
|
</strong>
|
|
|
|
|
{selectedTemplate
|
|
|
|
|
? renderPreview(selectedTemplate.content, { nome: defaultName, data: templateDate, link: templateLink, variavel: templateCustomVariable })
|
|
|
|
|
: 'Selecione um template aprovado.'}
|
|
|
|
|
</article>
|
|
|
|
|
|
|
|
|
|
<article
|
|
|
|
|
style={{
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
borderRadius: 18,
|
|
|
|
|
padding: '1rem',
|
|
|
|
|
background: '#fff',
|
|
|
|
|
color: 'var(--color-text-soft)',
|
|
|
|
|
fontWeight: 700,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Destinatários no campo: {numbers.length}.
|
|
|
|
|
</article>
|
|
|
|
|
|
|
|
|
|
<div style={{ display: 'grid', gap: '0.55rem', maxHeight: 320, overflowY: 'auto' }}>
|
|
|
|
|
{results.map((result) => (
|
|
|
|
|
<div
|
|
|
|
|
key={`${result.number}-${result.status}`}
|
|
|
|
|
style={{
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
borderRadius: 14,
|
|
|
|
|
padding: '0.75rem',
|
|
|
|
|
background: result.status === 'enviado' ? 'rgba(16,185,129,0.08)' : 'rgba(181,31,31,0.08)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<strong style={{ display: 'block' }}>{result.number}</strong>
|
|
|
|
|
<span style={{ color: 'var(--color-text-soft)' }}>{result.detail}</span>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
</div>
|
|
|
|
|
</DataPanel>
|
|
|
|
|
);
|
|
|
|
|
}
|