{template.name}
{template.content}
import { useEffect, useMemo, useState } from 'react' import { normalizeRole } from '../config/permissions.js' import { StethoscopeIcon } from '../components/Brand.jsx' import { FeatureCallout } from '../components/FeatureState.jsx' import { featurePanelClass } from '../components/featureStateStyles.js' import { communicationRepository } from '../repositories/communicationRepository.js' import { patientRepository } from '../repositories/patientRepository.js' const channels = { whatsapp: { label: 'WhatsApp', className: 'bg-emerald-500/20 text-emerald-400', icon: 'message' }, email: { label: 'E-mail', className: 'bg-blue-500/20 text-blue-400', icon: 'mail' }, sms: { label: 'SMS', className: 'bg-purple-500/20 text-purple-400', icon: 'phone' }, } const statusConfig = { entregue: { label: 'Entregue', className: 'text-emerald-400', icon: 'check' }, lida: { label: 'Lida', className: 'text-blue-400', icon: 'eye' }, falha: { label: 'Falha', className: 'text-red-400', icon: 'x-circle' }, pendente: { label: 'Pendente', className: 'text-amber-400', icon: 'clock' }, } const emptyMessage = { patientId: '', patient: '', phone: '', channel: 'whatsapp', template: 'Lembrete 48h', content: '', } const emptyTemplate = { name: '', channel: 'whatsapp', category: 'Lembrete', content: '', } const cardClass = 'rounded-2xl border border-[#404040] bg-[#262626] shadow-sm' const inputClass = 'h-10 w-full rounded-sm border border-[#404040] bg-[#171717] px-3 text-sm text-[#e5e5e5] outline-none transition placeholder:text-[#a3a3a3] focus:border-[#3b82f6] focus:ring-2 focus:ring-[#3b82f6]/20' const textareaClass = 'min-h-28 w-full resize-y rounded-sm border border-[#404040] bg-[#171717] px-3 py-2 text-sm leading-6 text-[#e5e5e5] outline-none transition placeholder:text-[#a3a3a3] focus:border-[#3b82f6] focus:ring-2 focus:ring-[#3b82f6]/20' const labelClass = 'text-xs font-semibold uppercase tracking-[0.08em] text-[#a3a3a3]' export function MessagesPage({ role }) { const normalizedRole = normalizeRole(role) const isSecretary = normalizedRole === 'secretaria' const allowedChannelKeys = useMemo( () => (isSecretary ? ['whatsapp', 'sms'] : Object.keys(channels)), [isSecretary], ) const campaigns = communicationRepository.getCampaigns() const [messages, setMessages] = useState(() => communicationRepository.getInitialMessages()) const [templates, setTemplates] = useState(() => communicationRepository.getInitialTemplates()) const [patients, setPatients] = useState([]) const [activeTab, setActiveTab] = useState('historico') const [channelFilter, setChannelFilter] = useState('todos') const [search, setSearch] = useState('') const [composerOpen, setComposerOpen] = useState(false) const [templateEditorOpen, setTemplateEditorOpen] = useState(false) const [editingTemplateId, setEditingTemplateId] = useState(null) const [composer, setComposer] = useState(emptyMessage) const [templateDraft, setTemplateDraft] = useState(emptyTemplate) const availableTemplates = useMemo( () => templates.filter((template) => allowedChannelKeys.includes(template.channel)), [allowedChannelKeys, templates], ) const patientOptions = useMemo( () => patients.map((patient) => ({ id: String(patient.detailId || patient.id || ''), name: patient.name || patient.full_name || patient.nome || 'Paciente', phone: patient.phone || patient.phone_mobile || patient.telefone || '', document: patient.cpf || patient.document || '', })), [patients], ) useEffect(() => { let active = true patientRepository .getDirectoryRows() .then((data) => { if (active) setPatients(data || []) }) .catch((loadError) => { console.error(loadError) if (active) setPatients([]) }) return () => { active = false } }, []) const filteredMessages = useMemo( () => messages.filter((message) => { const isAllowedChannel = allowedChannelKeys.includes(message.channel) const matchesChannel = channelFilter === 'todos' || message.channel === channelFilter const query = search.trim().toLowerCase() const matchesSearch = !query || [message.patient, message.template, channels[message.channel].label] .join(' ') .toLowerCase() .includes(query) return isAllowedChannel && matchesChannel && matchesSearch }), [allowedChannelKeys, channelFilter, messages, search], ) const stats = useMemo( () => ({ total: messages.filter((message) => allowedChannelKeys.includes(message.channel)).length, delivered: messages.filter((message) => allowedChannelKeys.includes(message.channel) && (message.status === 'entregue' || message.status === 'lida')).length, read: messages.filter((message) => allowedChannelKeys.includes(message.channel) && message.status === 'lida').length, failed: messages.filter((message) => allowedChannelKeys.includes(message.channel) && message.status === 'falha').length, }), [allowedChannelKeys, messages], ) function openTemplate(template) { if (!allowedChannelKeys.includes(template.channel)) return setComposer({ patientId: '', patient: '', phone: '', channel: template.channel, template: template.name, content: template.content, }) setComposerOpen(true) } function openTemplateEditor(template = null) { if (template && !allowedChannelKeys.includes(template.channel)) return setEditingTemplateId(template?.id || null) setTemplateDraft( template ? { name: template.name || '', channel: template.channel || allowedChannelKeys[0], category: template.category || 'Personalizado', content: template.content || '', } : { ...emptyTemplate, channel: allowedChannelKeys[0] || 'whatsapp', }, ) setTemplateEditorOpen(true) } async function submitMessage(event) { event.preventDefault() if (!composer.patient.trim()) { return } if (!allowedChannelKeys.includes(composer.channel)) { alert('Canal indisponivel para o seu perfil.') return } let smsSent = false if (composer.channel === 'sms') { if (!composer.phone.trim()) { alert('Informe o telefone para enviar SMS.') return } try { await communicationRepository.sendSms({ patientName: composer.patient.trim(), phone: composer.phone.trim(), content: composer.content, }) smsSent = true } catch (e) { alert('Falha ao disparar SMS: ' + e.message) } } setMessages((current) => [ { id: `local-${Date.now()}`, patient: composer.patient.trim(), channel: composer.channel, template: composer.template.trim() || 'Mensagem avulsa', sentAt: 'Agora', status: composer.channel === 'sms' ? (smsSent ? 'entregue' : 'falha') : 'pendente', response: '', }, ...current, ]) setComposer(emptyMessage) setComposerOpen(false) setActiveTab('historico') } function submitTemplate(event) { event.preventDefault() if (!templateDraft.name.trim() || !templateDraft.content.trim()) { return } const nextTemplate = { id: editingTemplateId || `template-${Date.now()}`, name: templateDraft.name.trim(), channel: templateDraft.channel, content: templateDraft.content.trim(), category: templateDraft.category.trim() || 'Personalizado', } setTemplates((current) => editingTemplateId ? current.map((template) => (template.id === editingTemplateId ? nextTemplate : template)) : [nextTemplate, ...current], ) setEditingTemplateId(null) setTemplateDraft(emptyTemplate) setTemplateEditorOpen(false) } return (
{isSecretary ? 'WhatsApp e SMS para contato operacional com pacientes' : 'WhatsApp, E-mail e SMS - historico e campanhas'}
| Paciente | Canal | Template | Enviado em | Status | Resposta |
|---|
Crie campanhas segmentadas por perfil comportamental. A IA sugere os melhores horários e canais para cada paciente.
{campaign.desc}
{campaign.count}
Todas as comunicações respeitam as preferências de Opt-in/Opt-out dos pacientes. Os pacientes podem cancelar o recebimento de mensagens a qualquer momento, conforme exigido pela LGPD.
{label}
{value}
{template.content}