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

@@ -51,6 +51,9 @@ export function AgendaPage({ navigate }) {
updateForm,
handleCreate,
visibleAppointments,
availableSlots,
slotsLoading,
slotsError,
} = useAgenda()
if (loading) {
@@ -131,10 +134,10 @@ export function AgendaPage({ navigate }) {
{error ? (
<section className="rounded-2xl border border-[#404040] bg-[#262626] p-5 shadow-[0_1px_3px_rgba(0,0,0,0.2)]">
<div className="rounded-xl border border-dashed border-[#7f1d1d] bg-[#2a1111] p-6">
<h2 className="text-base font-bold text-[#fecaca]">Nao foi possivel liberar a agenda</h2>
<h2 className="text-base font-bold text-[#fecaca]">Não foi possível liberar a agenda</h2>
<p className="mt-2 text-sm leading-6 text-[#fca5a5]">{error}</p>
<p className="mt-3 text-sm leading-6 text-[#a3a3a3]">
Enquanto esse vinculo nao existir na API, a tela fica bloqueada para evitar exibir consultas de outro medico.
Enquanto esse vínculo não existir na API, a tela fica bloqueada para evitar exibir consultas de outro médico.
</p>
</div>
</section>
@@ -244,12 +247,32 @@ export function AgendaPage({ navigate }) {
<div className="grid gap-4 sm:grid-cols-2">
<DarkField label="Horário">
<input
className="h-11 rounded-md border border-[#404040] bg-[#303030] px-3 text-sm text-[#e5e5e5] outline-none focus:border-[#3b82f6]"
onChange={(event) => updateForm('time', event.target.value)}
type="time"
value={form.time}
/>
{availableSlots.length ? (
<select
className="h-11 rounded-md border border-[#404040] bg-[#303030] px-3 text-sm text-[#e5e5e5] outline-none focus:border-[#3b82f6]"
onChange={(event) => updateForm('time', event.target.value)}
value={form.time}
>
{availableSlots.map((slot) => (
<option key={slot.time} value={slot.time}>
{slot.time}
</option>
))}
</select>
) : (
<input
className="h-11 rounded-md border border-[#404040] bg-[#303030] px-3 text-sm text-[#e5e5e5] outline-none focus:border-[#3b82f6]"
onChange={(event) => updateForm('time', event.target.value)}
type="time"
value={form.time}
/>
)}
{slotsLoading ? (
<span className="text-xs font-normal text-[#a3a3a3]">Calculando horários...</span>
) : null}
{slotsError ? (
<span className="text-xs font-normal text-amber-400">{slotsError}</span>
) : null}
</DarkField>
<DarkField label="Formato">
<select

View File

@@ -176,7 +176,7 @@ export function LoginPage({ navigate }) {
}
export function RegisterPage({ navigate }) {
const [role, setRole] = useState('Clinica')
const [role, setRole] = useState('Clínica')
return (
<AuthLayout
@@ -204,7 +204,7 @@ export function RegisterPage({ navigate }) {
</AuthField>
<AuthField label="Tipo de conta">
<div className="grid grid-cols-2 gap-2">
{['Clinica', 'Profissional'].map((option) => (
{['Clínica', 'Profissional'].map((option) => (
<button
className={`h-11 rounded-[6px] border px-3 text-sm font-semibold transition ${
role === option

View File

@@ -209,7 +209,7 @@ function ReportAction({ card, navigate }) {
function LineChart() {
return (
<svg aria-label="Grafico mockado de absenteismo" className="h-full w-full" role="img" viewBox="0 0 732 260">
<svg aria-label="Gráfico mockado de absenteísmo" className="h-full w-full" role="img" viewBox="0 0 732 260">
<defs>
<linearGradient id="home-chart-fill" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stopColor="#3b82f6" stopOpacity="0.24" />

View File

@@ -197,7 +197,7 @@ function RecordEditorModal({ onClose, onSave, recordTypes }) {
date: formData.date ? formatDate(formData.date) : '07/04/2026',
doctor: 'Dr. Henrique Cardoso',
type: formData.type,
cid: formData.cid || 'CID nao informado',
cid: formData.cid || 'CID não informado',
status,
summary: formData.conduct || formData.anamnesis || 'Registro criado localmente para simulação.',
})

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>

View File

@@ -4,9 +4,9 @@ export function NotFoundPage({ navigate }) {
return (
<div className="grid gap-6">
<PageHeader
description="A rota acessada nao faz parte do shell navegavel deste prototipo."
description="A rota acessada não faz parte do shell navegável deste protótipo."
eyebrow="404"
title="Tela nao encontrada"
title="Tela não encontrada"
/>
<Card className="p-6">
<p className="max-w-2xl text-sm leading-6 text-slate-600">

View File

@@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from 'react'
import { hasCapability } from '../config/permissions.js'
import { patientRepository } from '../repositories/patientRepository.js'
const ITEMS_PER_PAGE = 25
@@ -14,7 +15,7 @@ const patientTabs = [
{ label: 'Documentos', value: 'documentos' },
]
export function PatientsPage({ navigate }) {
export function PatientsPage({ navigate, role }) {
const [rows, setRows] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
@@ -45,6 +46,8 @@ export function PatientsPage({ navigate }) {
const insuranceOptions = useMemo(() => [...new Set(rows.map((patient) => patient.insurance).filter(Boolean))], [rows])
const stateOptions = useMemo(() => [...new Set(rows.map((patient) => patient.state).filter(Boolean))], [rows])
const hasAdvancedFilters = city || state || ageMin || ageMax || lastVisitSince
const canEditPatients = hasCapability(role, 'canEditPatients')
const canHardDeletePatients = hasCapability(role, 'hardDeletePatients')
const filteredPatients = useMemo(() => {
return rows.filter((patient) => {
@@ -65,7 +68,7 @@ export function PatientsPage({ navigate }) {
return false
}
if (vip === 'Nao' && patient.vip) {
if (vip === 'Não' && patient.vip) {
return false
}
@@ -117,12 +120,18 @@ export function PatientsPage({ navigate }) {
}
function openForm(patientId = null) {
if (!canEditPatients) return
setEditingId(patientId)
setOpenMenuId(null)
setView('form')
}
async function savePatient(patient) {
if (!canEditPatients) {
window.alert('Você não tem permissão para salvar pacientes.')
return
}
const isNew = !rows.some((item) => item.id === patient.id)
setSaving(true)
@@ -156,6 +165,11 @@ export function PatientsPage({ navigate }) {
}
async function deletePatient(patientId) {
if (!canHardDeletePatients) {
window.alert('Você não tem permissão para excluir pacientes.')
return
}
if (window.confirm('Tem certeza que deseja excluir este paciente?')) {
try {
await patientRepository.remove(patientId)
@@ -206,16 +220,18 @@ async function deletePatient(patientId) {
<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-[#e5e5e5]">Pacientes</h1>
<p className="mt-1 text-sm text-[#a3a3a3]">Gerencie as informacoes de seus pacientes</p>
<p className="mt-1 text-sm text-[#a3a3a3]">Gerencie as informações de seus pacientes</p>
</div>
<button
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded-lg bg-[#3b82f6] px-4 text-sm font-medium text-white shadow-sm transition hover:bg-[#2563eb] md:w-auto"
onClick={() => openForm()}
type="button"
>
<PatientIcon name="user-plus" />
Adicionar
</button>
{canEditPatients ? (
<button
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded-lg bg-[#3b82f6] px-4 text-sm font-medium text-white shadow-sm transition hover:bg-[#2563eb] md:w-auto"
onClick={() => openForm()}
type="button"
>
<PatientIcon name="user-plus" />
Adicionar
</button>
) : null}
</div>
<section className="rounded-2xl border border-[#404040] bg-[#262626] px-6 py-8 shadow-sm xl:py-14">
@@ -253,7 +269,7 @@ async function deletePatient(patientId) {
setVip(value)
setPage(1)
}}
options={['Sim', 'Nao']}
options={['Sim', 'Não']}
value={vip}
/>
@@ -310,7 +326,7 @@ async function deletePatient(patientId) {
<th className="w-[8%] px-6 py-4">Estado</th>
<th className="w-[16%] px-6 py-4">Ultimo atendimento</th>
<th className="w-[18%] px-6 py-4">Proximo atendimento</th>
<th className="sticky right-0 w-[8.5rem] bg-[#171717] px-6 py-4 text-right">Acoes</th>
<th className="sticky right-0 w-[8.5rem] bg-[#171717] px-6 py-4 text-right">Ações</th>
</tr>
</thead>
<tbody className="divide-y divide-[#404040] bg-[#262626]">
@@ -335,11 +351,11 @@ async function deletePatient(patientId) {
<td className="px-6 py-4 align-top whitespace-normal break-words text-[#a3a3a3]">{patient.phone}</td>
<td className="px-6 py-4 align-top whitespace-normal break-words text-[#a3a3a3]">{patient.city}</td>
<td className="px-6 py-4 align-top text-[#a3a3a3]">{patient.state}</td>
<td className="px-6 py-4 align-top whitespace-normal break-words text-[#a3a3a3]">{patient.lastVisit || 'Ainda nao houve atendimento'}</td>
<td className="px-6 py-4 align-top whitespace-normal break-words text-[#a3a3a3]">{patient.lastVisit || 'Ainda não houve atendimento'}</td>
<td className="px-6 py-4 align-top whitespace-normal break-words text-[#a3a3a3]">{patient.nextVisit || 'Nenhum atendimento agendado'}</td>
<td className="relative sticky right-0 bg-[#262626] px-6 py-4 text-right shadow-[-10px_0_12px_-12px_rgba(0,0,0,0.75)]">
<button
aria-label={`Acoes de ${patient.name}`}
aria-label={`Ações de ${patient.name}`}
className="rounded p-1 text-[#a3a3a3] transition hover:bg-[#333333] hover:text-[#e5e5e5]"
onClick={() => setOpenMenuId(openMenuId === patient.id ? null : patient.id)}
type="button"
@@ -356,7 +372,7 @@ async function deletePatient(patientId) {
/>
<div className="absolute right-8 top-10 z-20 w-48 rounded-lg border border-[#404040] bg-[#303030] py-1 text-left shadow-lg">
<ActionItem icon="file" label="Ver detalhes" onClick={() => openDetail(patient)} />
<ActionItem icon="edit" label="Editar" onClick={() => openForm(patient.id)} />
{canEditPatients ? <ActionItem icon="edit" label="Editar" onClick={() => openForm(patient.id)} /> : null}
<ActionItem
icon="calendar"
label="Marcar consulta"
@@ -365,7 +381,9 @@ async function deletePatient(patientId) {
navigate('/agenda')
}}
/>
<ActionItem danger icon="trash" label="Excluir" onClick={() => deletePatient(patient.id)} />
{canHardDeletePatients ? (
<ActionItem danger icon="trash" label="Excluir" onClick={() => deletePatient(patient.id)} />
) : null}
</div>
</>
) : null}
@@ -488,12 +506,12 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
id: formData.id || uniqueSlug(formData.name, existingIds),
age: Number(formData.age) || 0,
birthday: formData.birthday || '07/04',
city: formData.city || 'Cidade nao informada',
document: formData.cpf ? `CPF ${formData.cpf}` : 'CPF nao informado',
city: formData.city || 'Cidade não informada',
document: formData.cpf ? `CPF ${formData.cpf}` : 'CPF não informado',
insurance: formData.insurance || 'Particular',
lastVisit: formData.lastVisit || 'Ainda nao houve atendimento',
lastVisit: formData.lastVisit || 'Ainda não houve atendimento',
nextVisit: formData.nextVisit || null,
phone: formData.phone || 'Telefone nao informado',
phone: formData.phone || 'Telefone não informado',
plan: formData.insurance || formData.plan || 'Particular',
state: formData.state || 'UF',
})
@@ -512,7 +530,7 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
</button>
<div>
<h1 className="text-2xl font-bold tracking-tight text-[#e5e5e5]">Paciente</h1>
<p className="mt-1 text-sm text-[#a3a3a3]">Gerencie as informacoes de seus pacientes</p>
<p className="mt-1 text-sm text-[#a3a3a3]">Gerencie as informações de seus pacientes</p>
</div>
</div>
</div>
@@ -551,8 +569,8 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
<DarkField className="md:col-span-3" label="Etnia">
<select className={darkInput} defaultValue="">
<option value="">Selecione</option>
<option>Indigena</option>
<option>Nao Indigena</option>
<option>Indígena</option>
<option>Não Indígena</option>
</select>
</DarkField>
<DarkField className="md:col-span-3" label="Estado civil">
@@ -605,12 +623,12 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
</section>
<section className={darkCard}>
<h2 className="mb-6 text-lg font-semibold text-[#e5e5e5]">Endereco</h2>
<h2 className="mb-6 text-lg font-semibold text-[#e5e5e5]">Endereço</h2>
<div className="grid grid-cols-1 gap-x-6 gap-y-6 md:grid-cols-12">
<DarkField className="md:col-span-3" label="CEP">
<input className={darkInput} maxLength={9} onChange={maskCEPInput} placeholder="_____-___" />
</DarkField>
<DarkField className="md:col-span-5" label="Endereco">
<DarkField className="md:col-span-5" label="Endereço">
<input className={darkInput} />
</DarkField>
<DarkField className="md:col-span-4" label="Cidade">
@@ -621,7 +639,7 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
<option value="">Selecione</option>
<option value="PE">Pernambuco</option>
<option value="SE">Sergipe</option>
<option value="SP">Sao Paulo</option>
<option value="SP">São Paulo</option>
<option value="RJ">Rio de Janeiro</option>
</select>
</DarkField>
@@ -629,7 +647,7 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
</section>
<section className={darkCard}>
<h2 className="mb-6 text-lg font-semibold text-[#e5e5e5]">Informacoes de convenio</h2>
<h2 className="mb-6 text-lg font-semibold text-[#e5e5e5]">Informações de convenio</h2>
<div className="grid grid-cols-1 gap-x-6 gap-y-6 md:grid-cols-12">
<DarkField className="md:col-span-6" label="Convenio">
<select className={darkInput} name="insurance" onChange={handleChange} value={formData.insurance}>

View File

@@ -15,8 +15,8 @@ const statusConfig = {
}
const orderOptions = [
{ label: 'Criacao mais recente', value: 'created_at.desc' },
{ label: 'Criacao mais antiga', value: 'created_at.asc' },
{ label: 'Criação mais recente', value: 'created_at.desc' },
{ label: 'Criação mais antiga', value: 'created_at.asc' },
{ label: 'Prazo mais proximo', value: 'due_at.asc' },
{ label: 'Prazo mais distante', value: 'due_at.desc' },
]
@@ -80,7 +80,7 @@ export function ReportsPage() {
return {
id: String(professional.id || ''),
createdByValue,
name: professional.name || 'Medico(a)',
name: professional.name || 'Médico(a)',
}
})
.filter((professional) => {
@@ -107,7 +107,7 @@ export function ReportsPage() {
() =>
reports.map((report) => ({
...report,
patientName: patientNameById[String(report.patientId || '')] || 'Paciente nao encontrado',
patientName: patientNameById[String(report.patientId || '')] || 'Paciente não encontrado',
createdByName: professionalNameByCreatedBy[String(report.createdBy || '')] || report.createdBy || 'Sistema',
})),
[patientNameById, professionalNameByCreatedBy, reports],
@@ -146,7 +146,7 @@ export function ReportsPage() {
setPage(1)
} catch (loadError) {
console.error(loadError)
setError(loadError.message || 'Erro ao carregar relatorios medicos.')
setError(loadError.message || 'Erro ao carregar relatórios médicos.')
setReports([])
setPage(1)
} finally {
@@ -238,7 +238,7 @@ export function ReportsPage() {
setEditorOpen(false)
await loadReports()
} catch (saveError) {
alert(saveError.message || 'Erro ao salvar relatorio medico.')
alert(saveError.message || 'Erro ao salvar relatório médico.')
} finally {
setSaving(false)
}
@@ -248,8 +248,8 @@ export function ReportsPage() {
<div className="mx-auto max-w-7xl space-y-6 text-[#e5e5e5]">
<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-[#e5e5e5]">Relatorios medicos</h1>
<p className="mt-1 text-sm text-[#a3a3a3]">Consulta, criacao e edicao de relatorios medicos.</p>
<h1 className="text-2xl font-bold tracking-tight text-[#e5e5e5]">Relatórios médicos</h1>
<p className="mt-1 text-sm text-[#a3a3a3]">Consulta, criação e edição de relatórios médicos.</p>
</div>
<button
className="inline-flex h-10 items-center justify-center gap-2 rounded-lg bg-[#3b82f6] px-4 text-sm font-medium text-white transition hover:bg-[#2563eb]"
@@ -257,7 +257,7 @@ export function ReportsPage() {
type="button"
>
<ReportIcon name="plus" />
Novo relatorio
Novo relatório
</button>
</div>
@@ -324,7 +324,7 @@ export function ReportsPage() {
</select>
</FilterField>
<FilterField label="Ordenacao">
<FilterField label="Ordenação">
<select
className={inputClass}
onChange={(event) => {
@@ -358,14 +358,14 @@ export function ReportsPage() {
<th className="w-[18%] px-4 py-3">Solicitante</th>
<th className="w-[14%] px-4 py-3">Criado em</th>
<th className="w-[10%] px-4 py-3">Status</th>
<th className="sticky right-0 w-[8.5rem] bg-[#171717] px-4 py-3 text-right">Acoes</th>
<th className="sticky right-0 w-[8.5rem] bg-[#171717] px-4 py-3 text-right">Ações</th>
</tr>
</thead>
<tbody className="divide-y divide-[#404040] bg-[#262626]">
{loading ? (
<tr>
<td className="px-4 py-8 text-center text-sm text-[#a3a3a3]" colSpan={7}>
Carregando relatorios medicos...
Carregando relatórios médicos...
</td>
</tr>
) : paginatedReports.length ? (
@@ -380,7 +380,7 @@ export function ReportsPage() {
) : (
<tr>
<td className="px-4 py-8 text-center text-sm text-[#a3a3a3]" colSpan={7}>
Nenhum relatorio encontrado com os filtros atuais.
Nenhum relatório encontrado com os filtros atuais.
</td>
</tr>
)}
@@ -391,7 +391,7 @@ export function ReportsPage() {
<div className="mt-4 flex flex-col gap-4 border-t border-[#404040] pt-4 sm:flex-row sm:items-center sm:justify-between">
<p className="text-xs text-[#a3a3a3]">
Mostrando {enrichedReports.length ? startIndex + 1 : 0}-{Math.min(startIndex + ITEMS_PER_PAGE, enrichedReports.length)} de{' '}
{enrichedReports.length} relatorios
{enrichedReports.length} relatórios
</p>
<div className="flex items-center gap-2">
<PageButton disabled={currentPage === 1} onClick={() => setPage(currentPage - 1)}>
@@ -480,7 +480,7 @@ function ReportEditorModal({ editor, onChange, onClose, onSave, patientOptions,
>
<div className="flex items-center justify-between border-b border-[#404040] px-6 py-4">
<h2 className="text-lg font-bold text-[#e5e5e5]">
{editor.id ? 'Editar relatorio medico' : 'Novo relatorio medico'}
{editor.id ? 'Editar relatório médico' : 'Novo relatório médico'}
</h2>
<button className="rounded-lg p-1.5 transition hover:bg-[#2a2a2a]" onClick={onClose} type="button">
<ReportIcon className="size-4 text-[#a3a3a3]" name="x" />
@@ -556,29 +556,29 @@ function ReportEditorModal({ editor, onChange, onClose, onSave, patientOptions,
</DarkField>
</div>
<DarkField label="Diagnostico">
<DarkField label="Diagnóstico">
<textarea
className={textareaClass}
onChange={(event) => updateField('diagnosis', event.target.value)}
placeholder="Diagnostico do relatorio"
placeholder="Diagnóstico do relatório"
value={editor.diagnosis}
/>
</DarkField>
<DarkField label="Conclusao">
<DarkField label="Conclusão">
<textarea
className={textareaClass}
onChange={(event) => updateField('conclusion', event.target.value)}
placeholder="Conclusao do relatorio"
placeholder="Conclusão do relatório"
value={editor.conclusion}
/>
</DarkField>
<DarkField label="Conteudo HTML">
<DarkField label="Conteúdo HTML">
<textarea
className={`${textareaClass} min-h-72`}
onChange={(event) => updateField('contentHtml', event.target.value)}
placeholder="<p>Conteudo do relatorio</p>"
placeholder="<p>Conteúdo do relatório</p>"
value={editor.contentHtml}
/>
</DarkField>
@@ -622,7 +622,7 @@ function ReportEditorModal({ editor, onChange, onClose, onSave, patientOptions,
type="button"
>
<ReportIcon className="size-3.5" name="save" />
{saving ? 'Salvando...' : 'Salvar relatorio'}
{saving ? 'Salvando...' : 'Salvar relatório'}
</button>
</div>
</div>
@@ -639,8 +639,8 @@ function ReportViewModal({ onClose, report }) {
>
<div className="flex items-center justify-between border-b border-[#404040] px-6 py-4">
<div>
<h2 className="text-lg font-bold text-[#e5e5e5]">Relatorio medico</h2>
<p className="mt-1 text-xs text-[#a3a3a3]">{report.orderNumber || 'Sem numero'} </p>
<h2 className="text-lg font-bold text-[#e5e5e5]">Relatório médico</h2>
<p className="mt-1 text-xs text-[#a3a3a3]">{report.orderNumber || 'Sem número'} </p>
</div>
<button className="rounded-lg p-1.5 transition hover:bg-[#2a2a2a]" onClick={onClose} type="button">
<ReportIcon className="size-4 text-[#a3a3a3]" name="x" />
@@ -663,8 +663,8 @@ function ReportViewModal({ onClose, report }) {
</div>
<div className="mt-4 grid gap-4 md:grid-cols-2">
<DetailBlock label="Diagnostico" value={report.diagnosis || '-'} />
<DetailBlock label="Conclusao" value={report.conclusion || '-'} />
<DetailBlock label="Diagnóstico" value={report.diagnosis || '-'} />
<DetailBlock label="Conclusão" value={report.conclusion || '-'} />
</div>
<div className="mt-4 flex flex-wrap gap-3 text-xs text-[#a3a3a3]">
@@ -677,14 +677,14 @@ function ReportViewModal({ onClose, report }) {
</div>
<div className="mt-6 rounded-xl border border-[#404040] bg-[#1a1a1a] p-5">
<p className="mb-3 text-xs font-semibold uppercase tracking-wide text-[#a3a3a3]">Conteudo HTML</p>
<p className="mb-3 text-xs font-semibold uppercase tracking-wide text-[#a3a3a3]">Conteúdo HTML</p>
{report.contentHtml ? (
<div
className="prose prose-invert max-w-none text-sm text-[#e5e5e5]"
dangerouslySetInnerHTML={{ __html: report.contentHtml }}
/>
) : (
<p className="text-sm text-[#a3a3a3]">Nenhum conteudo HTML informado.</p>
<p className="text-sm text-[#a3a3a3]">Nenhum conteúdo HTML informado.</p>
)}
</div>
</div>

View File

@@ -1,25 +1,66 @@
import { useState, useEffect } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { FeatureBadge, FeatureCallout } from '../components/FeatureState.jsx'
import { featurePanelClass } from '../components/featureStateStyles.js'
import { availabilityRepository } from '../repositories/availabilityRepository.js'
import { professionalRepository } from '../repositories/professionalRepository.js'
const cardClass = 'rounded-2xl border border-[#404040] bg-[#262626] shadow-sm'
const weekdays = [
{ label: 'Seg', value: 1 },
{ label: 'Ter', value: 2 },
{ label: 'Qua', value: 3 },
{ label: 'Qui', value: 4 },
{ label: 'Sex', value: 5 },
]
export function TeamPage({ navigate }) {
const [professionals, setProfessionals] = useState([])
const { slots, weekdays } = professionalRepository.getCoverageMap()
const [availability, setAvailability] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
useEffect(() => {
professionalRepository.getAll().then(setProfessionals).catch(console.error)
let active = true
async function loadTeam() {
try {
setError('')
const [professionalsData, availabilityData] = await Promise.all([
professionalRepository.getAll(),
availabilityRepository.getAll({ active: true }),
])
if (!active) return
setProfessionals(professionalsData)
setAvailability(availabilityData)
} catch (loadError) {
if (!active) return
setError(loadError.message || 'Erro ao carregar profissionais e disponibilidade.')
} finally {
if (active) setLoading(false)
}
}
loadTeam()
return () => {
active = false
}
}, [])
const availabilityByDoctor = useMemo(() => groupAvailabilityByDoctor(availability), [availability])
return (
<div className="mx-auto max-w-7xl space-y-6">
<FeatureCallout
description="A listagem de profissionais usa API, mas o mapa de cobertura e parte da disponibilidade ainda são simulados."
status="partial"
title="Tela híbrida: parte real, parte mockada"
/>
{error ? (
<FeatureCallout
description={error}
status="wip"
title="Não foi possível carregar disponibilidade"
/>
) : null}
<header className="flex flex-col items-start justify-between gap-4 md:flex-row md:items-center">
<div>
@@ -35,7 +76,11 @@ export function TeamPage({ navigate }) {
</button>
</header>
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4" aria-label="Equipe médica">
{loading ? (
<p className="py-10 text-center text-sm text-[#a3a3a3]">Carregando profissionais...</p>
) : null}
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4" aria-label="Equipe medica">
{professionals.map((professional) => (
<article className={`${cardClass} ${featurePanelClass('live')} p-5`} key={professional.id}>
<div className="flex items-start justify-between gap-3">
@@ -51,22 +96,22 @@ export function TeamPage({ navigate }) {
<dl className="mt-5 grid gap-3 text-sm">
<Info label="Agenda" value={professional.schedule} />
<Info label="Próximo horário" value={professional.nextSlot} />
<Info label="Proximo horario" value={professional.nextSlot} />
<Info label="Pacientes ativos" value={professional.patients} />
</dl>
</article>
))}
</section>
<section className={`${cardClass} ${featurePanelClass('mock')} p-5`}>
<section className={`${cardClass} ${featurePanelClass('live')} p-5`}>
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<div className="flex flex-wrap items-center gap-2">
<h2 className="text-xl font-bold text-[#f5f5f5]">Mapa de cobertura</h2>
<FeatureBadge status="mock" />
<FeatureBadge status="live" />
</div>
<p className="mt-1 text-sm text-[#a3a3a3]">
Matriz simples para preparar o fluxo de agenda, plantão e disponibilidade.
Disponibilidades ativas cadastradas em /rest/v1/doctor_availability.
</p>
</div>
<button
@@ -80,18 +125,18 @@ export function TeamPage({ navigate }) {
<div className="mt-5 overflow-x-auto rounded-sm border border-[#404040]">
<div className="grid min-w-[720px] grid-cols-[1.2fr_repeat(5,1fr)] bg-[#171717] text-xs font-bold uppercase tracking-[0.16em] text-[#a3a3a3]">
{['Profissional', ...weekdays].map((label) => (
{['Profissional', ...weekdays.map((weekday) => weekday.label)].map((label) => (
<div className="border-b border-[#404040] px-4 py-3" key={label}>
{label}
</div>
))}
</div>
{professionals.map((professional, rowIndex) => (
{professionals.map((professional) => (
<div className="grid min-w-[720px] grid-cols-[1.2fr_repeat(5,1fr)] text-sm" key={professional.id}>
<div className="border-b border-[#404040] px-4 py-3 font-semibold text-[#f5f5f5]">{professional.name}</div>
{slots.map((slot, index) => (
<div className="border-b border-[#404040] px-4 py-3 text-[#b8b8b8]" key={`${professional.id}-${slot}`}>
{shiftSlot(slot, rowIndex + index)}
{weekdays.map((weekday) => (
<div className="border-b border-[#404040] px-4 py-3 text-[#b8b8b8]" key={`${professional.id}-${weekday.value}`}>
{formatCoverage(availabilityByDoctor.get(String(professional.id))?.[weekday.value])}
</div>
))}
</div>
@@ -123,7 +168,7 @@ function StatusPill({ status }) {
}
function initials(name) {
return name
return String(name || '')
.replace(/^(Dr\.|Dra\.|Nutri\.|Enf\.)\s+/i, '')
.split(' ')
.slice(0, 2)
@@ -132,10 +177,28 @@ function initials(name) {
.toUpperCase()
}
function shiftSlot(slot, index) {
if (index % 4 === 0) {
return 'Bloqueado'
function groupAvailabilityByDoctor(items) {
const grouped = new Map()
for (const item of items) {
const doctorId = String(item.doctorId)
const current = grouped.get(doctorId) || {}
current[item.weekday] = [...(current[item.weekday] || []), item]
grouped.set(doctorId, current)
}
return slot
return grouped
}
function formatCoverage(items = []) {
const activeItems = items.filter((item) => item.active !== false)
if (!activeItems.length) return 'Sem regra'
return activeItems
.map((item) => `${formatTime(item.startTime)}-${formatTime(item.endTime)}`)
.join(', ')
}
function formatTime(value) {
return String(value || '').slice(0, 5)
}

View File

@@ -1,11 +1,34 @@
import { useEffect, useState } from 'react'
import { ADMIN_CREATABLE_ROLES, GESTOR_CREATABLE_ROLES, ROLE_LABELS } from '../config/permissions.js'
import { ADMIN_CREATABLE_ROLES, GESTOR_CREATABLE_ROLES, hasCapability, normalizeRole, ROLE_LABELS } from '../config/permissions.js'
import { userRepository } from '../repositories/userRepository.js'
const darkInput =
'h-10 w-full rounded-lg border border-[#404040] bg-[#1a1a1a] px-3 text-sm text-[#e5e5e5] outline-none transition placeholder:text-[#737373] focus:border-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6]'
const darkLabel = 'mb-1.5 block text-xs font-medium text-[#e5e5e5]'
const authMethodOptions = [
{
value: 'magic_link',
label: 'Magic Link',
description: 'Enviar link de acesso por email',
},
{
value: 'password',
label: 'Email e senha',
description: 'Definir senha inicial agora',
},
]
const initialUserForm = {
email: '',
full_name: '',
phone: '',
cpf: '',
role: '',
auth_method: 'magic_link',
password: '',
confirm_password: '',
create_patient_record: false,
}
export function UsersPage({ role: currentRole }) {
const [users, setUsers] = useState([])
@@ -14,16 +37,12 @@ export function UsersPage({ role: currentRole }) {
const [modalOpen, setModalOpen] = useState(false)
const [saving, setSaving] = useState(false)
const [deletingId, setDeletingId] = useState(null)
const [form, setForm] = useState({
email: '',
full_name: '',
role: '',
create_patient_record: false,
cpf: '',
phone_mobile: '',
})
const [form, setForm] = useState(initialUserForm)
const creatableRoles = currentRole === 'admin' ? ADMIN_CREATABLE_ROLES : GESTOR_CREATABLE_ROLES
const normalizedRole = normalizeRole(currentRole)
const canManageUsers = hasCapability(normalizedRole, 'manageUsers')
const creatableRoles = normalizedRole === 'admin' ? ADMIN_CREATABLE_ROLES : GESTOR_CREATABLE_ROLES
const isPasswordCreation = form.auth_method === 'password'
useEffect(() => {
loadUsers()
@@ -47,29 +66,60 @@ export function UsersPage({ role: currentRole }) {
setForm((current) => ({ ...current, [name]: type === 'checkbox' ? checked : value }))
}
async function handleCreate(event) {
event.preventDefault()
if (!form.email || !form.full_name || !form.role) {
window.alert('Preencha email, nome completo e perfil.')
return
async function handleCreate(event) {
event.preventDefault()
if (!canManageUsers) {
window.alert('Você não tem permissão para criar usuários.')
return
}
if (!form.email || !form.full_name || !form.phone || !form.cpf || !form.role) {
window.alert('Preencha email, nome completo, celular, CPF e perfil.')
return
}
if (isPasswordCreation) {
if (!form.password || !form.confirm_password) {
window.alert('Preencha a senha e a confirmação de senha.')
return
}
if (form.password.length < 8) {
window.alert('A senha deve ter pelo menos 8 caracteres.')
return
}
if (form.password !== form.confirm_password) {
window.alert('A confirmação de senha não confere.')
return
}
}
setSaving(true)
try {
if (isPasswordCreation) {
await userRepository.createWithPassword(form)
window.alert(`Usuário criado com email e senha para ${form.email}.`)
} else {
await userRepository.create(form)
window.alert(`Usuário criado! Magic Link enviado para ${form.email}.`)
}
setModalOpen(false)
setForm(initialUserForm)
loadUsers()
} catch (err) {
window.alert(`Erro ao criar usuário: ${err.message}`)
} finally {
setSaving(false)
}
}
setSaving(true)
try {
const result = await userRepository.create(form)
console.log('Usuário criado:', result)
window.alert(`Usuário criado! Magic Link enviado para ${form.email}.`)
setModalOpen(false)
setForm({ email: '', full_name: '', role: '', create_patient_record: false, cpf: '', phone_mobile: '' })
loadUsers()
} catch (err) {
console.error('Erro completo:', err)
window.alert(`Erro ao criar usuário: ${err.message}`)
} finally {
setSaving(false)
}
}
async function handleDelete(user) {
if (!canManageUsers) {
window.alert('Você não tem permissão para deletar usuários.')
return
}
const confirmed = window.confirm(
`⚠️ ATENÇÃO: Esta operação é IRREVERSÍVEL!\n\nO usuário "${user.full_name || user.email}" e TODOS os dados relacionados (perfil, agendamentos, registros) serão deletados permanentemente.\n\nDeseja continuar?`
)
@@ -86,6 +136,15 @@ async function handleCreate(event) {
}
}
if (!canManageUsers) {
return (
<div className="mx-auto max-w-3xl rounded-2xl border border-[#404040] bg-[#262626] p-8 text-center text-[#e5e5e5]">
<h1 className="text-xl font-bold">Acesso não permitido</h1>
<p className="mt-2 text-sm text-[#a3a3a3]">Somente Administrador e Gestão/Coordenação podem gerenciar usuários.</p>
</div>
)
}
return (
<div className="mx-auto max-w-7xl space-y-6 text-[#e5e5e5]">
<div className="flex flex-col items-start justify-between gap-4 md:flex-row md:items-center">
@@ -175,13 +234,15 @@ async function handleCreate(event) {
{modalOpen ? (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4" onClick={() => setModalOpen(false)}>
<div
className="w-full max-w-lg rounded-2xl border border-[#404040] bg-[#262626] p-6 shadow-xl"
className="max-h-[90vh] w-full max-w-lg overflow-y-auto rounded-2xl border border-[#404040] bg-[#262626] p-6 shadow-xl"
onClick={(e) => e.stopPropagation()}
>
<div className="mb-6 flex items-center justify-between">
<div>
<h2 className="text-lg font-bold text-[#e5e5e5]">Novo Usuário</h2>
<p className="mt-1 text-xs text-[#a3a3a3]">Um Magic Link será enviado para o email cadastrado.</p>
<p className="mt-1 text-xs text-[#a3a3a3]">
{isPasswordCreation ? 'Crie o acesso inicial com email e senha.' : 'Um Magic Link sera enviado para o email cadastrado.'}
</p>
</div>
<button
className="rounded p-1 text-[#a3a3a3] transition hover:bg-[#333333]"
@@ -193,6 +254,40 @@ async function handleCreate(event) {
</div>
<form className="space-y-4" onSubmit={handleCreate}>
<div>
<span className={darkLabel}>Criar usuário usando *</span>
<div className="grid gap-3 sm:grid-cols-2">
{authMethodOptions.map((option) => {
const selected = form.auth_method === option.value
return (
<label
className={`cursor-pointer rounded-lg border p-3 transition ${
selected
? 'border-[#3b82f6] bg-[#3b82f6]/15 text-[#e5e5e5]'
: 'border-[#404040] bg-[#1a1a1a] text-[#a3a3a3] hover:border-[#525252] hover:text-[#e5e5e5]'
}`}
key={option.value}
>
<span className="flex items-start gap-3">
<input
checked={selected}
className="mt-1 size-4 accent-[#3b82f6]"
name="auth_method"
onChange={handleFormChange}
type="radio"
value={option.value}
/>
<span>
<span className="block text-sm font-semibold">{option.label}</span>
<span className="mt-1 block text-xs text-[#a3a3a3]">{option.description}</span>
</span>
</span>
</label>
)
})}
</div>
</div>
<div>
<label className={darkLabel}>Nome completo *</label>
<input
@@ -218,6 +313,65 @@ async function handleCreate(event) {
/>
</div>
<div>
<label className={darkLabel}>Celular *</label>
<input
className={darkInput}
maxLength={15}
name="phone"
onChange={handleFormChange}
placeholder="(00) 00000-0000"
required
value={form.phone}
/>
</div>
<div>
<label className={darkLabel}>CPF *</label>
<input
className={darkInput}
maxLength={14}
name="cpf"
onChange={handleFormChange}
placeholder="000.000.000-00"
required
value={form.cpf}
/>
</div>
{isPasswordCreation ? (
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className={darkLabel}>Senha *</label>
<input
autoComplete="new-password"
className={darkInput}
minLength={8}
name="password"
onChange={handleFormChange}
placeholder="Mínimo 8 caracteres"
required={isPasswordCreation}
type="password"
value={form.password}
/>
</div>
<div>
<label className={darkLabel}>Confirmar senha *</label>
<input
autoComplete="new-password"
className={darkInput}
minLength={8}
name="confirm_password"
onChange={handleFormChange}
placeholder="Repita a senha"
required={isPasswordCreation}
type="password"
value={form.confirm_password}
/>
</div>
</div>
) : null}
<div>
<label className={darkLabel}>Perfil de acesso *</label>
<select
@@ -245,35 +399,6 @@ async function handleCreate(event) {
Criar também um registro de paciente
</label>
{form.create_patient_record ? (
<div className="grid gap-4 rounded-lg border border-[#404040] bg-[#1a1a1a] p-4 sm:grid-cols-2">
<div>
<label className={darkLabel}>CPF *</label>
<input
className={darkInput}
maxLength={14}
name="cpf"
onChange={handleFormChange}
placeholder="000.000.000-00"
required={form.create_patient_record}
value={form.cpf}
/>
</div>
<div>
<label className={darkLabel}>Celular *</label>
<input
className={darkInput}
maxLength={15}
name="phone_mobile"
onChange={handleFormChange}
placeholder="(00) 00000-0000"
required={form.create_patient_record}
value={form.phone_mobile}
/>
</div>
</div>
) : null}
<div className="flex justify-end gap-3 pt-2">
<button
className="rounded-lg border border-[#404040] bg-[#262626] px-4 py-2 text-sm font-medium text-[#e5e5e5] transition hover:bg-[#333333]"
@@ -288,7 +413,7 @@ async function handleCreate(event) {
disabled={saving}
type="submit"
>
{saving ? 'Criando...' : 'Criar e enviar Magic Link'}
{saving ? 'Criando...' : isPasswordCreation ? 'Criar com senha' : 'Criar e enviar Magic Link'}
</button>
</div>
</form>

View File

@@ -22,7 +22,7 @@ export function VisitsPage({ navigate }) {
}
if (activeTab === 'atendimento') {
return careQueue.filter((item) => item.status !== 'Finalizada' && item.status !== 'Aguardando medico')
return careQueue.filter((item) => item.status !== 'Finalizada' && item.status !== 'Aguardando médico')
}
return careQueue.filter((item) => item.status !== 'Finalizada')