modified: src/App.jsx

modified:   src/components/AppShell.jsx
modified:   src/config/api.js
modified:   src/config/permissions.js
modified:   src/data/mockData.js
modified:   src/hooks/useAgenda.js
modified:   src/hooks/useAuth.js
modified:   src/mappers/appointmentMapper.js
modified:   src/pages/AgendaPage.jsx
modified:   src/pages/AuthPages.jsx
modified:   src/pages/HomePage.jsx
modified:   src/pages/MedicalRecordsPage.jsx
modified:   src/pages/MessagesPage.jsx
modified:   src/pages/NotFoundPage.jsx
modified:   src/pages/PatientsPage.jsx
modified:   src/pages/ReportsPage.jsx
modified:   src/pages/TeamPage.jsx
modified:   src/pages/UsersPage.jsx
modified:   src/pages/VisitsPage.jsx
modified:   src/repositories/authRepository.js
new file:   src/repositories/availabilityRepository.js
modified:   src/repositories/communicationRepository.js
modified:   src/repositories/patientRepository.js
modified:   src/repositories/professionalRepository.js
modified:   src/repositories/profileRepository.js
modified:   src/repositories/reportRepository.js
modified:   src/repositories/repositoryUtils.js
modified:   src/repositories/settingsRepository.js
modified:   src/repositories/userRepository.js
modified:   src/repositories/visitRepository.js
This commit is contained in:
2026-05-06 01:09:36 -03:00
parent bb5200664a
commit 666b3b5c0e
30 changed files with 1038 additions and 376 deletions

View File

@@ -1,5 +1,6 @@
import { 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'
@@ -40,7 +41,13 @@ 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() {
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())
@@ -51,10 +58,15 @@ export function MessagesPage() {
const [templateEditorOpen, setTemplateEditorOpen] = useState(false)
const [composer, setComposer] = useState(emptyMessage)
const [templateDraft, setTemplateDraft] = useState(emptyTemplate)
const availableTemplates = useMemo(
() => templates.filter((template) => allowedChannelKeys.includes(template.channel)),
[allowedChannelKeys, templates],
)
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 =
@@ -64,22 +76,24 @@ export function MessagesPage() {
.toLowerCase()
.includes(query)
return matchesChannel && matchesSearch
return isAllowedChannel && matchesChannel && matchesSearch
}),
[channelFilter, messages, search],
[allowedChannelKeys, channelFilter, messages, search],
)
const stats = useMemo(
() => ({
total: messages.length,
delivered: messages.filter((message) => message.status === 'entregue' || message.status === 'lida').length,
read: messages.filter((message) => message.status === 'lida').length,
failed: messages.filter((message) => message.status === 'falha').length,
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,
}),
[messages],
[allowedChannelKeys, messages],
)
function openTemplate(template) {
if (!allowedChannelKeys.includes(template.channel)) return
setComposer({
patient: '',
phone: '',
@@ -97,6 +111,11 @@ export function MessagesPage() {
return
}
if (!allowedChannelKeys.includes(composer.channel)) {
alert('Canal indisponivel para o seu perfil.')
return
}
let smsSent = false
if (composer.channel === 'sms') {
@@ -158,26 +177,28 @@ export function MessagesPage() {
return (
<div className="mx-auto max-w-7xl space-y-6">
<FeatureCallout
description="Envio de SMS usa API. Histórico, templates e campanhas ainda são dados locais de demonstração."
description={isSecretary ? 'Perfil Secretária limitado a comunicação básica por WhatsApp e SMS.' : 'Envio de SMS usa API. Histórico, templates e campanhas ainda são dados locais de demonstração.'}
status="partial"
title="Mensageria híbrida"
title={isSecretary ? 'Comunicação basica' : 'Mensageria hibrida'}
/>
<div className="flex flex-col items-start justify-between gap-4 md:flex-row md:items-center">
<div>
<h1 className="text-2xl font-bold tracking-tight text-[#f5f5f5]">Comunicação</h1>
<p className="mt-1 text-sm text-[#b8b8b8]">WhatsApp, E-mail e SMS - histórico e campanhas</p>
<p className="mt-1 text-sm text-[#b8b8b8]">{isSecretary ? 'WhatsApp e SMS para contato operacional com pacientes' : 'WhatsApp, E-mail e SMS - historico e campanhas'}</p>
</div>
<div className="flex flex-wrap gap-3">
<button
className="inline-flex h-12 items-center gap-2 rounded-sm border border-[#404040] bg-[#262626] px-4 text-sm font-semibold text-[#e5e5e5] transition hover:bg-[#303030]"
onClick={() => setActiveTab('campanha')}
type="button"
>
<CommIcon className="size-4" name="send" />
Envio em Massa
</button>
{!isSecretary ? (
<button
className="inline-flex h-12 items-center gap-2 rounded-sm border border-[#404040] bg-[#262626] px-4 text-sm font-semibold text-[#e5e5e5] transition hover:bg-[#303030]"
onClick={() => setActiveTab('campanha')}
type="button"
>
<CommIcon className="size-4" name="send" />
Envio em Massa
</button>
) : null}
<button
className="inline-flex h-12 items-center gap-2 rounded-sm bg-[#3b82f6] px-4 text-sm font-semibold text-white transition hover:bg-[#2563eb]"
onClick={() => setComposerOpen(true)}
@@ -199,8 +220,7 @@ export function MessagesPage() {
<div className="flex gap-4 border-b border-[#404040]">
{[
['historico', 'Histórico'],
['templates', 'Templates'],
['campanha', 'Campanhas'],
...(!isSecretary ? [['templates', 'Templates'], ['campanha', 'Campanhas']] : []),
].map(([key, label]) => (
<button
className={`border-b-2 px-2 pb-3 text-sm font-semibold transition ${
@@ -238,9 +258,7 @@ export function MessagesPage() {
<div className="flex flex-wrap gap-2">
{[
['todos', 'Todos'],
['whatsapp', 'Whatsapp'],
['email', 'E-mail'],
['sms', 'Sms'],
...allowedChannelKeys.map((key) => [key, channels[key].label]),
].map(([key, label]) => (
<button
className={`h-12 rounded-sm border px-4 text-xs font-semibold transition ${
@@ -300,7 +318,7 @@ export function MessagesPage() {
</div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{templates.map((template) => (
{availableTemplates.map((template) => (
<TemplateCard key={template.id} onUse={openTemplate} template={template} />
))}
</div>
@@ -363,6 +381,7 @@ export function MessagesPage() {
{composerOpen ? (
<MessageComposer
allowedChannelKeys={allowedChannelKeys}
draft={composer}
onChange={setComposer}
onClose={() => {
@@ -370,12 +389,13 @@ export function MessagesPage() {
setComposer(emptyMessage)
}}
onSubmit={submitMessage}
templates={templates}
templates={availableTemplates}
/>
) : null}
{templateEditorOpen ? (
<TemplateEditor
allowedChannelKeys={allowedChannelKeys}
draft={templateDraft}
onChange={setTemplateDraft}
onClose={() => {
@@ -459,7 +479,7 @@ function TemplateCard({ onUse, template }) {
)
}
function MessageComposer({ draft, onChange, onClose, onSubmit, templates }) {
function MessageComposer({ allowedChannelKeys, draft, onChange, onClose, onSubmit, templates }) {
function update(field, value) {
onChange((current) => ({ ...current, [field]: value }))
}
@@ -494,9 +514,9 @@ function MessageComposer({ draft, onChange, onClose, onSubmit, templates }) {
</DarkField>
<DarkField label="Canal">
<select className={inputClass} onChange={(event) => update('channel', event.target.value)} value={draft.channel}>
<option value="whatsapp">WhatsApp</option>
<option value="email">E-mail</option>
<option value="sms">SMS</option>
{allowedChannelKeys.map((key) => (
<option key={key} value={key}>{channels[key].label}</option>
))}
</select>
</DarkField>
</div>
@@ -549,7 +569,7 @@ function MessageComposer({ draft, onChange, onClose, onSubmit, templates }) {
)
}
function TemplateEditor({ draft, onChange, onClose, onSubmit }) {
function TemplateEditor({ allowedChannelKeys, draft, onChange, onClose, onSubmit }) {
function update(field, value) {
onChange((current) => ({ ...current, [field]: value }))
}
@@ -563,9 +583,9 @@ function TemplateEditor({ draft, onChange, onClose, onSubmit }) {
</DarkField>
<DarkField label="Canal">
<select className={inputClass} onChange={(event) => update('channel', event.target.value)} value={draft.channel}>
<option value="whatsapp">WhatsApp</option>
<option value="email">E-mail</option>
<option value="sms">SMS</option>
{allowedChannelKeys.map((key) => (
<option key={key} value={key}>{channels[key].label}</option>
))}
</select>
</DarkField>
</div>