Replace hardcoded user with live profile data

This commit is contained in:
EdilbertoC
2026-04-28 12:46:39 -03:00
parent d496494b3e
commit 000abb39ac
15 changed files with 993 additions and 358 deletions

View File

@@ -1,5 +1,6 @@
import { useMemo, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { profileRepository } from '../repositories/profileRepository.js'
import { BrandLogo } from './Brand.jsx'
import { FeatureLegend } from './FeatureState.jsx'
@@ -7,16 +8,16 @@ const navItems = [
{ href: '/inicio', label: 'Painel', icon: 'pulse', activePaths: ['/inicio', '/home', '/dashboard'] },
{ href: '/agenda', label: 'Agenda', icon: 'calendar' },
{ href: '/pacientes', label: 'Pacientes', icon: 'users', exact: true },
{ href: '/prontuario', label: 'Prontuário', icon: 'file' },
{ href: '/prontuario', label: 'Prontuario', icon: 'file' },
{ href: '/laudos', label: 'Laudos', icon: 'clipboard' },
{
href: '/camunicacao',
label: 'Comunicação',
label: 'Comunicacao',
icon: 'message',
activePaths: ['/camunicacao', '/comunicacao', '/mensagens'],
},
{ href: '/relatorios', label: 'Relatórios', icon: 'chart' },
{ href: '/configuracoes', label: 'Configurações', icon: 'settings', activePaths: ['/configuracoes', '/config'] },
{ href: '/relatorios', label: 'Relatorios', icon: 'chart' },
{ href: '/configuracoes', label: 'Configuracoes', icon: 'settings', activePaths: ['/configuracoes', '/config'] },
]
const titles = {
@@ -27,20 +28,21 @@ const titles = {
'/consultas': 'Consultas',
'/laudos': 'Laudos',
'/pacientes': 'Pacientes',
'/prontuario': 'Prontuário',
'/camunicacao': 'Comunicação',
'/comunicacao': 'Comunicação',
'/mensagens': 'Comunicação',
'/relatorios': 'Relatórios',
'/prontuario': 'Prontuario',
'/camunicacao': 'Comunicacao',
'/comunicacao': 'Comunicacao',
'/mensagens': 'Comunicacao',
'/relatorios': 'Relatorios',
'/profissionais': 'Profissionais',
'/perfil': 'Perfil',
'/configuracoes': 'Configurações',
'/config': 'Configurações',
'/configuracoes': 'Configuracoes',
'/config': 'Configuracoes',
}
export function AppShell({ children, currentPath, navigate, routeTitle }) {
const [menuOpen, setMenuOpen] = useState(false)
const [quickSearch, setQuickSearch] = useState('')
const [viewerProfile, setViewerProfile] = useState({ name: 'Usuario', role: 'Usuario do Sistema' })
const pageTitle = useMemo(() => {
if (currentPath.startsWith('/pacientes/') && routeTitle) {
@@ -50,6 +52,25 @@ export function AppShell({ children, currentPath, navigate, routeTitle }) {
return routeTitle || titles[currentPath] || 'MediConnect'
}, [currentPath, routeTitle])
useEffect(() => {
let active = true
profileRepository.getCurrentUserProfile()
.then((profile) => {
if (!active || !profile) return
setViewerProfile({
name: profile.name || 'Usuario',
role: profile.role || 'Usuario do Sistema',
})
})
.catch(() => {})
return () => {
active = false
}
}, [])
function goTo(path) {
setMenuOpen(false)
navigate(path)
@@ -96,8 +117,8 @@ export function AppShell({ children, currentPath, navigate, routeTitle }) {
onClick={() => goTo('/perfil')}
type="button"
>
<p className="truncate text-xs font-semibold text-[#e5e5e5]">Dr. Henrique Cardoso</p>
<p className="mt-0.5 truncate text-[11px] leading-4 text-[#a3a3a3]">Médico Clínico Geral</p>
<p className="truncate text-xs font-semibold text-[#e5e5e5]">{viewerProfile.name}</p>
<p className="mt-0.5 truncate text-[11px] leading-4 text-[#a3a3a3]">{viewerProfile.role}</p>
</button>
</div>
</aside>
@@ -129,7 +150,7 @@ export function AppShell({ children, currentPath, navigate, routeTitle }) {
aria-label="Busca rapida"
className="h-[38px] w-full rounded-sm border border-[#404040] bg-[#303030] py-2 pl-10 pr-4 text-sm text-[#e5e5e5] outline-none transition placeholder:text-[#a3a3a3] focus:border-[#3b82f6] focus:ring-2 focus:ring-[#3b82f6]/20"
onChange={(event) => setQuickSearch(event.target.value)}
placeholder="Buscar paciente, prontuário..."
placeholder="Buscar paciente, prontuario..."
value={quickSearch}
/>
</div>
@@ -155,14 +176,14 @@ export function AppShell({ children, currentPath, navigate, routeTitle }) {
type="button"
>
<span className="grid size-8 shrink-0 place-items-center rounded-full border border-[#3b82f6]/30 bg-[#3b82f6]/15 text-xs font-bold text-[#3b82f6]">
HC
{getInitials(viewerProfile.name)}
</span>
<span className="hidden min-w-0 sm:block">
<span className="block truncate text-sm font-semibold leading-4 text-[#e5e5e5]">
Dr. Henrique Cardoso
{viewerProfile.name}
</span>
<span className="mt-0.5 block truncate text-[11px] font-medium leading-4 text-[#51a2ff]">
Médico(a)
{viewerProfile.role}
</span>
</span>
<ChevronDownIcon className="hidden size-4 text-[#a3a3a3] sm:block" />
@@ -335,3 +356,13 @@ function SearchIcon({ className = 'size-4' }) {
</svg>
)
}
function getInitials(name) {
return String(name || 'US')
.split(' ')
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0])
.join('')
.toUpperCase()
}

View File

@@ -0,0 +1,100 @@
import React from 'react'
import { format, isToday } from 'date-fns'
import { ptBR } from 'date-fns/locale'
import { sortAppointmentsByTime } from '../../utils/agendaDate.js'
export function AgendaDailyView({ baseDate, appointments, onAppointmentClick }) {
const dailyAppointments = sortAppointmentsByTime(appointments)
return (
<div className="rounded-2xl border border-[#404040] bg-[#262626] p-5">
<div className="flex flex-col gap-3 border-b border-[#404040] pb-4 md:flex-row md:items-end md:justify-between">
<div>
<span className="text-xs font-semibold uppercase tracking-[0.16em] text-[#737373]">
Vista ampliada do dia
</span>
<h3 className="mt-2 text-xl font-bold text-[#e5e5e5]">
{format(baseDate, "EEEE, dd 'de' MMMM", { locale: ptBR })}
</h3>
</div>
<div className="flex flex-wrap gap-2">
<span className="rounded-full border border-[#404040] bg-[#1f1f1f] px-3 py-1 text-xs font-semibold text-[#a3a3a3]">
{dailyAppointments.length} {dailyAppointments.length === 1 ? 'agendamento' : 'agendamentos'}
</span>
{isToday(baseDate) && (
<span className="rounded-full border border-[#3b82f6]/30 bg-[#3b82f6]/10 px-3 py-1 text-xs font-semibold text-[#93c5fd]">
Hoje
</span>
)}
</div>
</div>
{dailyAppointments.length === 0 ? (
<div className="mt-4 rounded-xl border border-dashed border-[#404040] bg-[#1f1f1f] p-8 text-center">
<h3 className="text-base font-bold text-[#e5e5e5]">Nenhum horário encontrado</h3>
<p className="mt-2 text-sm leading-6 text-[#a3a3a3]">
Ajuste o filtro ou altere o período no calendário.
</p>
</div>
) : (
<div className="mt-4 grid gap-3">
{dailyAppointments.map((appointment) => (
<article
key={appointment.id}
className={`grid gap-4 rounded-xl border p-4 md:grid-cols-[96px_1fr_auto] ${getStatusColors(appointment.status)}`}
>
<div>
<p className="text-2xl font-bold leading-none">{appointment.time || '--:--'}</p>
<p className="mt-2 text-[11px] font-semibold uppercase tracking-[0.14em] opacity-80">
{appointment.mode}
</p>
</div>
<div>
<button
className="text-left text-base font-bold transition hover:opacity-85"
onClick={() => onAppointmentClick && onAppointmentClick(appointment)}
type="button"
>
{appointment.patient}
</button>
<p className="mt-1 text-sm opacity-90">
{appointment.type} com {appointment.professional}
</p>
<div className="mt-3 flex flex-wrap gap-2 text-xs font-medium opacity-80">
<span className="rounded-full bg-black/15 px-2.5 py-1">{appointment.room}</span>
<span className="rounded-full bg-black/15 px-2.5 py-1">{appointment.type}</span>
</div>
</div>
<div className="flex items-start justify-start md:justify-end">
<span className="rounded-full border border-current/20 bg-black/10 px-3 py-1 text-xs font-bold">
{appointment.status}
</span>
</div>
</article>
))}
</div>
)}
</div>
)
}
function getStatusColors(status) {
switch (status) {
case 'Confirmada':
return 'border-[#14532d] bg-[#052e1a] text-[#a7f3d0]'
case 'Em triagem':
return 'border-[#78350f] bg-[#2d1e05] text-[#fde68a]'
case 'Concluida':
case 'Concluída':
return 'border-[#1e3a8a] bg-[#172554] text-[#bfdbfe]'
case 'Cancelada':
return 'border-[#7f1d1d] bg-[#450a0a] text-[#fecaca]'
case 'Aguardando':
default:
return 'border-[#404040] bg-[#1f1f1f] text-[#e5e5e5]'
}
}

View File

@@ -0,0 +1,107 @@
import React from 'react'
import {
startOfMonth,
endOfMonth,
startOfWeek,
endOfWeek,
eachDayOfInterval,
format,
isSameMonth,
isSameDay,
isToday,
} from 'date-fns'
import { parseLocalDate, sortAppointmentsByTime } from '../../utils/agendaDate.js'
export function AgendaMonthlyView({ baseDate, appointments, onDayClick }) {
const monthStart = startOfMonth(baseDate)
const monthEnd = endOfMonth(monthStart)
const startDate = startOfWeek(monthStart, { weekStartsOn: 0 })
const endDate = endOfWeek(monthEnd, { weekStartsOn: 0 })
const days = eachDayOfInterval({ start: startDate, end: endDate })
const weekDays = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb']
return (
<div className="rounded-2xl border border-[#404040] bg-[#262626] p-5">
<div className="grid grid-cols-7 gap-px border-b border-[#404040] pb-4">
{weekDays.map((day) => (
<div key={day} className="text-center text-xs font-semibold uppercase tracking-widest text-[#a3a3a3]">
{day}
</div>
))}
</div>
<div className="mt-4 grid grid-cols-7 gap-2">
{days.map((day) => {
const isCurrentMonth = isSameMonth(day, monthStart)
const dayAppointments = sortAppointmentsByTime(
appointments.filter((appointment) => {
if (!appointment.date) return false
const appointmentDate = parseLocalDate(appointment.date)
return appointmentDate && isSameDay(appointmentDate, day)
}),
)
return (
<button
key={day.toISOString()}
onClick={() => onDayClick && onDayClick(day)}
className={`flex min-h-[100px] flex-col rounded-xl border p-2 text-left transition hover:border-[#525252] ${
isCurrentMonth
? 'border-[#404040] bg-[#1f1f1f]'
: 'border-transparent bg-transparent opacity-40 hover:opacity-80'
}`}
>
<span
className={`text-sm font-bold ${
isToday(day)
? 'flex h-6 w-6 items-center justify-center rounded-full bg-[#3b82f6] text-white'
: 'text-[#e5e5e5]'
}`}
>
{format(day, 'd')}
</span>
<div className="mt-2 flex w-full flex-col gap-1">
{dayAppointments.slice(0, 3).map((appointment) => (
<div
key={appointment.id}
className="flex items-center gap-1.5 truncate rounded bg-[#303030] px-1.5 py-1 text-[10px] font-semibold text-[#a3a3a3]"
>
<span className={`h-1.5 w-1.5 shrink-0 rounded-full ${getDotColor(appointment.status)}`} />
<span className="truncate">
{appointment.time} - {appointment.patient}
</span>
</div>
))}
{dayAppointments.length > 3 && (
<span className="text-[10px] font-semibold text-[#3b82f6]">
+ {dayAppointments.length - 3} mais
</span>
)}
</div>
</button>
)
})}
</div>
</div>
)
}
function getDotColor(status) {
switch (status) {
case 'Confirmada':
return 'bg-[#10b981]'
case 'Em triagem':
return 'bg-[#f59e0b]'
case 'Aguardando':
return 'bg-[#a3a3a3]'
case 'Bloqueado':
return 'bg-[#737373]'
default:
return 'bg-[#3b82f6]'
}
}

View File

@@ -0,0 +1,122 @@
import React from 'react'
import {
startOfWeek,
endOfWeek,
eachDayOfInterval,
format,
isSameDay,
isToday,
} from 'date-fns'
import { ptBR } from 'date-fns/locale'
import { parseLocalDate, sortAppointmentsByTime } from '../../utils/agendaDate.js'
export function AgendaWeeklyView({ baseDate, appointments, onAppointmentClick }) {
const start = startOfWeek(baseDate, { weekStartsOn: 0 })
const end = endOfWeek(baseDate, { weekStartsOn: 0 })
const days = eachDayOfInterval({ start, end })
const weeklyAppointments = sortAppointmentsByTime(
appointments.filter((appointment) => {
if (!appointment.date) return false
const appointmentDate = parseLocalDate(appointment.date)
return appointmentDate && appointmentDate >= start && appointmentDate <= end
}),
)
return (
<div className="rounded-2xl border border-[#404040] bg-[#262626] p-5">
<div className="grid grid-cols-7 gap-4 border-b border-[#404040] pb-4">
{days.map((day) => {
const isWeekend = day.getDay() === 0
return (
<div key={day.toISOString()} className="text-center">
<span
className={`block text-xs font-semibold uppercase tracking-[0.16em] ${
isWeekend ? 'text-[#93c5fd]' : 'text-[#a3a3a3]'
}`}
>
{format(day, 'EEE', { locale: ptBR })}
</span>
<span className={`mt-1 block text-2xl font-bold ${isToday(day) ? 'text-[#3b82f6]' : 'text-[#e5e5e5]'}`}>
{format(day, 'dd')}
</span>
</div>
)
})}
</div>
<div className="mt-4 grid min-h-[400px] grid-cols-7 gap-4">
{days.map((day) => {
const dayAppointments = weeklyAppointments.filter((appointment) => {
if (!appointment.date) return false
const appointmentDate = parseLocalDate(appointment.date)
return appointmentDate && isSameDay(appointmentDate, day)
})
return (
<div
key={day.toISOString()}
className="flex h-full flex-col gap-2 rounded-lg border border-[#404040]/50 bg-[#1f1f1f] p-2"
>
{dayAppointments.length === 0 ? (
<div className="flex h-full items-center justify-center p-4">
<span className="text-center text-xs text-[#737373]">Livre</span>
</div>
) : (
dayAppointments.map((appointment) => (
<button
key={appointment.id}
onClick={() => onAppointmentClick && onAppointmentClick(appointment)}
className={`flex w-full flex-col items-start rounded-md border p-2 text-left shadow-sm transition hover:brightness-110 ${getStatusColors(appointment.status)}`}
>
<div className="mb-1 flex items-center gap-2">
<span className="rounded bg-black/20 px-1.5 py-0.5 text-xs font-bold leading-none">
{appointment.time}
</span>
<span className="truncate text-[10px] font-semibold uppercase tracking-wider opacity-80">
{appointment.mode}
</span>
</div>
<span className="w-full truncate text-xs font-bold leading-tight" title={appointment.patient}>
{appointment.patient}
</span>
<span
className="mt-0.5 w-full truncate text-[10px] font-medium opacity-80"
title={appointment.professional}
>
Dr(a). {appointment.professional?.split(' ')[0]}
</span>
</button>
))
)}
</div>
)
})}
</div>
</div>
)
}
function getStatusColors(status) {
switch (status) {
case 'Confirmada':
return 'border-[#14532d] bg-[#052e1a] text-[#10b981]'
case 'Em triagem':
return 'border-[#78350f] bg-[#2d1e05] text-[#f59e0b]'
case 'Concluida':
case 'Concluída':
return 'border-[#1e3a8a] bg-[#172554] text-[#60a5fa]'
case 'Aguardando':
return 'border-[#404040] bg-[#303030] text-[#e5e5e5]'
case 'Cancelada':
return 'border-[#7f1d1d] bg-[#450a0a] text-[#f87171] opacity-75'
case 'Bloqueado':
return 'border-[#404040] bg-[#1f1f1f] text-[#737373]'
default:
return 'border-[#404040] bg-[#303030] text-[#e5e5e5]'
}
}