modified: index.html

modified:   src/App.jsx
modified:   src/components/AppShell.jsx
modified:   src/components/featureStateStyles.js
modified:   src/config/permissions.js
modified:   src/hooks/useAgenda.js
modified:   src/mappers/reportMapper.js
modified:   src/pages/AgendaPage.jsx
modified:   src/pages/AnalyticsPage.jsx
modified:   src/pages/AuthPages.jsx
modified:   src/pages/HomePage.jsx
modified:   src/pages/MedicalRecordsPage.jsx
modified:   src/pages/MessagesPage.jsx
modified:   src/pages/PatientsPage.jsx
modified:   src/pages/ReportsPage.jsx
modified:   src/pages/SettingsPage.jsx
deleted:    src/pages/TeamPage.jsx
modified:   src/pages/UsersPage.jsx
modified:   src/repositories/availabilityRepository.js
modified:   src/repositories/patientRepository.js
modified:   src/repositories/professionalRepository.js
modified:   src/repositories/reportRepository.js
modified:   src/repositories/settingsRepository.js
This commit is contained in:
2026-05-07 01:11:10 -03:00
parent 9335e974eb
commit efb942d5aa
23 changed files with 1461 additions and 591 deletions

View File

@@ -1,9 +1,10 @@
import { useMemo, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { normalizeRole } from '../config/permissions.js'
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' },
@@ -20,6 +21,7 @@ const statusConfig = {
const emptyMessage = {
patientId: '',
patient: '',
phone: '',
channel: 'whatsapp',
@@ -51,17 +53,47 @@ export function MessagesPage({ role }) {
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(
() =>
@@ -95,6 +127,7 @@ export function MessagesPage({ role }) {
if (!allowedChannelKeys.includes(template.channel)) return
setComposer({
patientId: '',
patient: '',
phone: '',
channel: template.channel,
@@ -104,6 +137,26 @@ export function MessagesPage({ role }) {
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()
@@ -160,16 +213,20 @@ export function MessagesPage({ role }) {
return
}
setTemplates((current) => [
{
id: `template-${Date.now()}`,
name: templateDraft.name.trim(),
channel: templateDraft.channel,
content: templateDraft.content.trim(),
category: templateDraft.category.trim() || 'Personalizado',
},
...current,
])
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)
}
@@ -309,7 +366,7 @@ export function MessagesPage({ role }) {
<div className="flex justify-end">
<button
className="inline-flex h-10 items-center gap-2 rounded-sm bg-[#3b82f6] px-4 text-sm font-semibold text-white transition hover:bg-[#2563eb]"
onClick={() => setTemplateEditorOpen(true)}
onClick={() => openTemplateEditor()}
type="button"
>
<CommIcon className="size-4" name="plus" />
@@ -319,7 +376,7 @@ export function MessagesPage({ role }) {
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{availableTemplates.map((template) => (
<TemplateCard key={template.id} onUse={openTemplate} template={template} />
<TemplateCard key={template.id} onEdit={openTemplateEditor} onUse={openTemplate} template={template} />
))}
</div>
</section>
@@ -347,6 +404,7 @@ export function MessagesPage({ role }) {
className="mt-3 h-8 w-full rounded-sm bg-[#3b82f6] text-xs font-semibold text-white transition hover:bg-[#2563eb]"
onClick={() => {
setComposer({
patientId: '',
patient: campaign.count,
phone: '',
channel: 'whatsapp',
@@ -389,6 +447,7 @@ export function MessagesPage({ role }) {
setComposer(emptyMessage)
}}
onSubmit={submitMessage}
patients={patientOptions}
templates={availableTemplates}
/>
) : null}
@@ -401,8 +460,10 @@ export function MessagesPage({ role }) {
onClose={() => {
setTemplateEditorOpen(false)
setTemplateDraft(emptyTemplate)
setEditingTemplateId(null)
}}
onSubmit={submitTemplate}
title={editingTemplateId ? 'Editar Template' : 'Novo Template'}
/>
) : null}
</div>
@@ -444,7 +505,7 @@ function MessageRow({ message }) {
)
}
function TemplateCard({ onUse, template }) {
function TemplateCard({ onEdit, onUse, template }) {
const channel = channels[template.channel]
return (
@@ -463,6 +524,7 @@ function TemplateCard({ onUse, template }) {
<div className="mt-4 flex gap-2">
<button
className="h-9 flex-1 rounded-sm border border-[#404040] bg-[#171717] text-xs font-semibold text-[#e5e5e5] transition hover:bg-[#303030]"
onClick={() => onEdit(template)}
type="button"
>
Editar
@@ -479,11 +541,35 @@ function TemplateCard({ onUse, template }) {
)
}
function MessageComposer({ allowedChannelKeys, draft, onChange, onClose, onSubmit, templates }) {
function MessageComposer({ allowedChannelKeys, draft, onChange, onClose, onSubmit, patients, templates }) {
const [patientSearch, setPatientSearch] = useState('')
const filteredPatients = useMemo(() => {
const query = patientSearch.trim().toLowerCase()
if (!query) return patients
return patients.filter((patient) =>
[patient.name, patient.phone, patient.document]
.join(' ')
.toLowerCase()
.includes(query),
)
}, [patientSearch, patients])
function update(field, value) {
onChange((current) => ({ ...current, [field]: value }))
}
function selectPatient(patientId) {
const patient = patients.find((item) => item.id === patientId)
onChange((current) => ({
...current,
patientId,
patient: patient?.name || '',
phone: patient?.phone || current.phone,
}))
}
function applyTemplate(templateName) {
const template = templates.find((item) => item.name === templateName)
@@ -505,10 +591,36 @@ function MessageComposer({ allowedChannelKeys, draft, onChange, onClose, onSubmi
<form className="space-y-4" onSubmit={onSubmit}>
<div className="grid gap-4 md:grid-cols-2">
<DarkField label="Paciente">
<input
className={inputClass}
onChange={(event) => setPatientSearch(event.target.value)}
placeholder="Digite nome, CPF ou telefone"
type="search"
value={patientSearch}
/>
</DarkField>
<DarkField label="Selecionar paciente">
<select
className={inputClass}
onChange={(event) => selectPatient(event.target.value)}
value={draft.patientId}
>
<option value="">Selecione um paciente</option>
{filteredPatients.map((patient) => (
<option key={patient.id} value={patient.id}>
{patient.name}
</option>
))}
</select>
</DarkField>
</div>
<div className="grid gap-4 md:grid-cols-2">
<DarkField label="Paciente selecionado">
<input
className={inputClass}
onChange={(event) => update('patient', event.target.value)}
placeholder="Nome do paciente"
readOnly
value={draft.patient}
/>
</DarkField>
@@ -547,7 +659,7 @@ function MessageComposer({ allowedChannelKeys, draft, onChange, onClose, onSubmi
<textarea
className={textareaClass}
onChange={(event) => update('content', event.target.value)}
placeholder="Escreva a mensagem mockada..."
placeholder="Escreva a mensagem"
value={draft.content}
/>
</DarkField>
@@ -569,13 +681,13 @@ function MessageComposer({ allowedChannelKeys, draft, onChange, onClose, onSubmi
)
}
function TemplateEditor({ allowedChannelKeys, draft, onChange, onClose, onSubmit }) {
function TemplateEditor({ allowedChannelKeys, draft, onChange, onClose, onSubmit, title }) {
function update(field, value) {
onChange((current) => ({ ...current, [field]: value }))
}
return (
<ModalFrame onClose={onClose} title="Novo Template">
<ModalFrame onClose={onClose} title={title}>
<form className="space-y-4" onSubmit={onSubmit}>
<div className="grid gap-4 md:grid-cols-2">
<DarkField label="Nome">