/* Componentes de apresentação compartilhados pela caixa de entrada do CRM. */
function crmInboxDate(value, includeDate = false) {
if (!value) return '';
const date = new Date(String(value).replace(' ', 'T'));
if (Number.isNaN(date.getTime())) return String(value);
const today = new Date();
const sameDay = date.toDateString() === today.toDateString();
if (!includeDate && sameDay) {
return date.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
}
return date.toLocaleString('pt-BR', includeDate
? { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }
: { day: '2-digit', month: '2-digit' });
}
function crmInboxAppointmentDate(value) {
if (!value) return '';
const date = new Date(`${value}T12:00:00`);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleDateString('pt-BR', {
weekday: 'short',
day: '2-digit',
month: 'short',
});
}
function crmInboxPhone(value) {
const digits = String(value || '').replace(/\D/g, '');
if (digits.length === 13 && digits.startsWith('55')) {
return `+55 (${digits.slice(2, 4)}) ${digits.slice(4, 9)}-${digits.slice(9)}`;
}
if (digits.length === 12 && digits.startsWith('55')) {
return `+55 (${digits.slice(2, 4)}) ${digits.slice(4, 8)}-${digits.slice(8)}`;
}
return digits ? `+${digits}` : '';
}
function crmInboxContactName(conversation) {
if (!conversation) return 'Contato';
const name = String(conversation.displayName || '').trim();
const nameDigits = name.replace(/\D/g, '');
const phoneDigits = String(conversation.counterpartPhone || '').replace(/\D/g, '');
return name && nameDigits !== phoneDigits ? name : crmInboxPhone(phoneDigits);
}
function crmInboxPhoneInput(value) {
const digits = String(value || '').replace(/\D/g, '').slice(0, 13);
if (!digits) return '';
if (digits.startsWith('55')) return crmInboxPhone(digits);
if (digits.length <= 11) {
const area = digits.slice(0, 2);
const number = digits.slice(2);
const split = number.length > 8 ? 5 : 4;
return `${area ? `(${area}${area.length === 2 ? ') ' : ''}` : ''}${number.slice(0, split)}${number.length > split ? `-${number.slice(split)}` : ''}`;
}
return `+${digits}`;
}
function crmInboxInitials(value) {
const parts = String(value || '').trim().split(/\s+/).filter(Boolean);
if (!parts.length) return 'C';
return `${parts[0][0] || ''}${parts.length > 1 ? parts[parts.length - 1][0] : ''}`.toUpperCase();
}
const crmInboxMessageLabels = {
pending: 'Pendente',
sent: 'Enviada',
delivered: 'Entregue',
read: 'Lida',
failed: 'Falhou',
received: 'Recebida',
};
const crmInboxStatusLabels = {
open: 'Em atendimento',
pending: 'Pendente',
resolved: 'Resolvida',
};
const crmInboxRoleLabels = {
admin: 'Administrador',
manager: 'Gerente',
agent: 'Atendente',
};
function crmInboxMessagePreview(value) {
const raw = String(value || '');
if (/^\[Mensagem (unsupported|não compatível)\]$/i.test(raw)) return 'Mensagem não compatível';
const attributed = raw.match(/^\*([^*\r\n]{1,80}):\*\r?\n?([\s\S]*)$/);
if (!attributed) return raw;
return attributed[2].trim() ? `${attributed[1]}: ${attributed[2].trim()}` : attributed[1];
}
function crmInboxReplyPreview(message) {
if (!message) return 'Mensagem original não carregada';
const type = String(message.messageType || 'text');
const labels = {
image: 'Imagem',
video: 'Vídeo',
audio: 'Áudio',
document: message.mediaFilename || 'Documento',
sticker: 'Figurinha',
location: 'Localização',
contacts: 'Contato',
unsupported: 'Mensagem não compatível',
};
if (labels[type]) return labels[type];
const preview = crmInboxMessagePreview(message.body).trim();
return preview || 'Mensagem';
}
function CrmConversationItem({ conversation, active, onClick }) {
const client = conversation.client || {};
const contactName = crmInboxContactName(conversation);
const hasAttributedPreview = /^\*([^*\r\n]{1,80}):\*/.test(String(conversation.lastMessagePreview || ''));
const messagePreview = crmInboxMessagePreview(conversation.lastMessagePreview);
return (
);
}
function CrmKanbanCard({ conversation, busy, onOpen, onMove, onDragStart, onDragEnd }) {
const client = conversation.client || {};
const contactName = crmInboxContactName(conversation);
const preview = crmInboxMessagePreview(conversation.lastMessagePreview) || 'Conversa iniciada';
return (
onDragStart(event, conversation)}
onDragEnd={onDragEnd}
>
);
}
function crmInboxArrangeMessages(messages) {
const regular = [];
const byProviderId = new Map();
const reactions = [];
(messages || []).forEach((message) => {
if (message.messageType === 'reaction') {
reactions.push(message);
return;
}
const item = { message, reactions: [] };
regular.push(item);
if (message.providerMessageId) byProviderId.set(message.providerMessageId, item);
});
const unlinked = [];
reactions.forEach((reaction) => {
const content = reaction.content || {};
const target = content.messageId ? byProviderId.get(content.messageId) : null;
if (!target) {
unlinked.push({ message: reaction, reactions: [] });
return;
}
const direction = reaction.direction || 'inbound';
target.reactions = target.reactions.filter((item) => item.direction !== direction);
if (content.emoji) target.reactions.push({ id: reaction.id, emoji: content.emoji, direction });
});
return regular.concat(unlinked).sort((a, b) => Number(a.message.id) - Number(b.message.id));
}
function CrmMessageBubble({ message, reactions = [], replyToMessage = null, contactName = 'Cliente', onReply }) {
const outbound = message.direction === 'outbound';
const type = String(message.messageType || 'text');
const mediaUrl = String(message.mediaUrl || '');
const [mediaFailed, setMediaFailed] = React.useState(false);
const [imageViewerOpen, setImageViewerOpen] = React.useState(false);
React.useEffect(() => setMediaFailed(false), [mediaUrl]);
const visibleMediaUrl = mediaFailed ? '' : mediaUrl;
const mediaUnavailable = !visibleMediaUrl && ['image', 'video', 'audio', 'document', 'sticker'].includes(type);
const content = message.content || {};
const rawBody = String(message.body || '');
const attributedBody = outbound ? rawBody.match(/^\*([^*\r\n]{1,80}):\*\r?\n?([\s\S]*)$/) : null;
const attributedSender = attributedBody ? attributedBody[1].trim() : '';
const visibleBody = attributedBody ? attributedBody[2] : rawBody;
const textParts = visibleBody.split(/(https?:\/\/[^\s<]+)/gi);
const bodyIsFallback = /^\[(Imagem|Vídeo|Áudio|Documento|Figurinha|Localização|Contato|Reação|Mensagem enviada pelo canal oficial)\]$/i.test(visibleBody);
const isLegacyOfficialPlaceholder = /^\[Mensagem enviada pelo canal oficial\]$/i.test(rawBody);
const isUnsupported = type === 'unsupported' || /^\[Mensagem (unsupported|não compatível)\]$/i.test(rawBody);
const replyToProviderMessageId = String(content.replyToMessageId || '');
const replyAuthor = replyToMessage
? (replyToMessage.direction === 'outbound'
? ((String(replyToMessage.body || '').match(/^\*([^*\r\n]{1,80}):\*/) || [])[1] || 'Equipe')
: contactName)
: 'Mensagem original';
if (isLegacyOfficialPlaceholder && !mediaUrl) {
return (
Mensagem enviada pelo celular
);
}
if (type === 'reaction') {
return (
{content.emoji || '♡'}
{content.emoji ? 'Reação a uma mensagem anterior' : 'Reação não identificada'}
);
}
if (isUnsupported) {
return (
Mensagem não compatível
Este formato não foi disponibilizado pela Meta. Veja a mensagem pelo WhatsApp.
);
}
return (
{typeof onReply === 'function' && message.providerMessageId && (
)}
{replyToProviderMessageId && (
{replyAuthor}
{crmInboxReplyPreview(replyToMessage)}
)}
{visibleMediaUrl && type === 'image' && (
)}
{visibleMediaUrl && type === 'sticker' &&
setMediaFailed(true)} />}
{visibleMediaUrl && type === 'video' &&
);
}
function CrmNewConversationModal({
open,
contacts,
contactsLoading,
contactQuery,
setContactQuery,
phone,
setPhone,
templates,
templatesLoading,
templateKey,
setTemplateKey,
templateParameters,
setTemplateParameters,
templateMedia,
setTemplateMedia,
error,
busy,
onClose,
onSubmit,
}) {
if (!open) return null;
const selectedTemplate = templates.find((item) => `${item.name}|${item.language}` === templateKey) || null;
const requiredParameters = selectedTemplate ? (selectedTemplate.parameters || []) : [];
const hasMissingParameters = requiredParameters.some((parameter) => !String(templateParameters[parameter.key] || '').trim());
const hasMissingMedia = !!(selectedTemplate && selectedTemplate.mediaHeader && !templateMedia);
const previewBody = selectedTemplate
? String(selectedTemplate.body || selectedTemplate.name)
.replace(/\{\{\s*(\d+)\s*\}\}/g, (match, index) => (
templateParameters[`body:${Number(index)}`] || match
))
.replace(/\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g, (match, name) => (
templateParameters[`body:${name}`] || match
))
: '';
return (
Cancelar
{busy ? 'Enviando…' : 'Enviar mensagem'}
>
)}
>
{error &&
{error}
}
setContactQuery(event.target.value)}
autoFocus
/>
{contactsLoading ? (
Buscando clientes…
) : contacts.length === 0 ? (
Nenhum cliente encontrado.
) : contacts.map((contact) => (
))}
setPhone(crmInboxPhoneInput(event.target.value))}
/>
setTemplateKey(event.target.value)}
>
{templates.map((template) => (
))}
{!templatesLoading && templates.length === 0 && (
Crie e aprove um modelo no Gerenciador do WhatsApp da conta conectada. Depois, feche e abra esta janela para atualizar a lista.
)}
{selectedTemplate && requiredParameters.map((parameter) => (
setTemplateParameters((current) => ({
...current,
[parameter.key]: event.target.value,
}))}
/>
))}
{selectedTemplate && selectedTemplate.mediaHeader && (
setTemplateMedia((event.target.files && event.target.files[0]) || null)}
/>
A imagem usada na aprovação é apenas uma amostra da Meta. A Cloud API exige uma mídia em cada envio. Máximo de 16 MB.
)}
{templateKey && (() => {
const selected = selectedTemplate;
return selected ? (
{previewBody}
{!!(selected.buttons && selected.buttons.length) && (
{selected.buttons.map((button) => (
{button.text}
))}
)}
) : null;
})()}
Texto livre é permitido quando o cliente iniciou a conversa nas últimas 24 horas.
Fora desse período, utilize uma mensagem previamente aprovada.
);
}
window.CrmInboxParts = {
crmInboxDate,
crmInboxAppointmentDate,
crmInboxPhone,
crmInboxContactName,
crmInboxPhoneInput,
crmInboxInitials,
crmInboxStatusLabels,
crmInboxRoleLabels,
crmInboxReplyPreview,
CrmConversationItem,
CrmKanbanCard,
crmInboxArrangeMessages,
CrmMessageBubble,
CrmNewConversationModal,
};