forked from RiseUP/riseup_squad_03
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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user