/* 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' && Figurinha 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, };