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,6 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { ROLE_LABELS, ROLE_NAV_ITEMS } from '../config/permissions.js'
|
||||
import { authRepository } from '../repositories/authRepository.js'
|
||||
import { profileRepository } from '../repositories/profileRepository.js'
|
||||
import { BrandLogo } from './Brand.jsx'
|
||||
|
||||
@@ -10,15 +11,14 @@ const ALL_NAV_ITEMS = [
|
||||
{ href: '/agenda', label: 'Agenda', icon: 'calendar' },
|
||||
{ href: '/pacientes', label: 'Pacientes', icon: 'users', exact: true },
|
||||
{ href: '/prontuario', label: 'Prontuário', icon: 'file' },
|
||||
{ href: '/laudos', label: 'Relatórios médicos', icon: 'clipboard' },
|
||||
{ href: '/laudos', label: 'Relatórios', icon: 'clipboard' },
|
||||
{
|
||||
href: '/comunicacao',
|
||||
label: 'Comunicação',
|
||||
icon: 'message',
|
||||
activePaths: ['/comunicacao', '/mensagens'],
|
||||
},
|
||||
{ href: '/relatorios', label: 'Relatórios', icon: 'chart' },
|
||||
{ href: '/profissionais', label: 'Profissionais', icon: 'users' },
|
||||
{ href: '/relatorios', label: 'Analytics', icon: 'chart' },
|
||||
{ href: '/usuarios', label: 'Usuários', icon: 'shield' },
|
||||
{ href: '/configuracoes', label: 'Configurações', icon: 'settings', activePaths: ['/configuracoes', '/config'] },
|
||||
]
|
||||
@@ -29,13 +29,12 @@ const titles = {
|
||||
'/dashboard': 'Painel',
|
||||
'/agenda': 'Agenda',
|
||||
'/consultas': 'Consultas',
|
||||
'/laudos': 'Relatórios médicos',
|
||||
'/laudos': 'Relatórios',
|
||||
'/pacientes': 'Pacientes',
|
||||
'/prontuario': 'Prontuário',
|
||||
'/comunicacao': 'Comunicação',
|
||||
'/mensagens': 'Comunicação',
|
||||
'/relatorios': 'Relatórios',
|
||||
'/profissionais': 'Profissionais',
|
||||
'/relatorios': 'Analytics',
|
||||
'/perfil': 'Perfil',
|
||||
'/configuracoes': 'Configurações',
|
||||
'/config': 'Configurações',
|
||||
@@ -44,7 +43,8 @@ const titles = {
|
||||
|
||||
export function AppShell({ children, currentPath, navigate, role, routeTitle }) {
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const [quickSearch, setQuickSearch] = useState('')
|
||||
const [profileMenuOpen, setProfileMenuOpen] = useState(false)
|
||||
const [notificationsOpen, setNotificationsOpen] = useState(false)
|
||||
const [viewerProfile, setViewerProfile] = useState({ name: 'Usuário', role: 'Usuário do Sistema' })
|
||||
|
||||
const pageTitle = useMemo(() => {
|
||||
@@ -68,6 +68,22 @@ export function AppShell({ children, currentPath, navigate, role, routeTitle })
|
||||
)
|
||||
}, [role])
|
||||
|
||||
const canOpenSettings = useMemo(
|
||||
() =>
|
||||
(ROLE_NAV_ITEMS[role] ?? []).some(
|
||||
(item) => item.path === '/configuracoes' || item.path === '/config',
|
||||
),
|
||||
[role],
|
||||
)
|
||||
const mockNotifications = useMemo(
|
||||
() => [
|
||||
{ id: 'mock-1', title: 'Retorno agendado', detail: 'Paciente Ana Souza às 14:30', time: 'Agora' },
|
||||
{ id: 'mock-2', title: 'Laudo pendente', detail: 'Hemograma aguardando revisão', time: '12 min' },
|
||||
{ id: 'mock-3', title: 'Mensagem recebida', detail: 'Resposta via WhatsApp registrada', time: '35 min' },
|
||||
],
|
||||
[],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
|
||||
@@ -96,11 +112,33 @@ export function AppShell({ children, currentPath, navigate, role, routeTitle })
|
||||
}
|
||||
}, [role])
|
||||
|
||||
useEffect(() => {
|
||||
if (!profileMenuOpen && !notificationsOpen) return undefined
|
||||
|
||||
function closeOnEscape(event) {
|
||||
if (event.key === 'Escape') {
|
||||
setProfileMenuOpen(false)
|
||||
setNotificationsOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', closeOnEscape)
|
||||
return () => window.removeEventListener('keydown', closeOnEscape)
|
||||
}, [notificationsOpen, profileMenuOpen])
|
||||
|
||||
function goTo(path) {
|
||||
setMenuOpen(false)
|
||||
setProfileMenuOpen(false)
|
||||
setNotificationsOpen(false)
|
||||
navigate(path)
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
setProfileMenuOpen(false)
|
||||
await authRepository.logout()
|
||||
navigate('/login', { replace: true })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#171717] text-[#e5e5e5]">
|
||||
<a
|
||||
@@ -169,57 +207,132 @@ export function AppShell({ children, currentPath, navigate, role, routeTitle })
|
||||
>
|
||||
Menu
|
||||
</button>
|
||||
<div className="relative w-full max-w-sm lg:w-96">
|
||||
<SearchIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-[#a3a3a3]" />
|
||||
<input
|
||||
aria-label="Busca rápida"
|
||||
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..."
|
||||
value={quickSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
<button
|
||||
aria-label="Notificações"
|
||||
className="relative grid size-8 place-items-center text-[#a3a3a3] transition hover:text-[#e5e5e5]"
|
||||
type="button"
|
||||
>
|
||||
<BellIcon className="size-5" />
|
||||
<span className="absolute right-0 top-0 grid size-4 place-items-center rounded-full bg-[#ef4444] text-[10px] font-bold leading-none text-white">
|
||||
3
|
||||
</span>
|
||||
</button>
|
||||
<div className="relative z-30">
|
||||
<button
|
||||
aria-expanded={notificationsOpen}
|
||||
aria-haspopup="menu"
|
||||
aria-label="Notificações"
|
||||
className="relative grid size-8 place-items-center text-[#a3a3a3] transition hover:text-[#e5e5e5]"
|
||||
onClick={() => {
|
||||
setNotificationsOpen((open) => !open)
|
||||
setProfileMenuOpen(false)
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<BellIcon className="size-5" />
|
||||
<span className="absolute right-0 top-0 grid size-4 place-items-center rounded-full bg-[#ef4444] text-[10px] font-bold leading-none text-white">
|
||||
{mockNotifications.length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{notificationsOpen ? (
|
||||
<div
|
||||
aria-label="Notificações mock"
|
||||
className="absolute right-0 top-12 z-30 w-80 rounded-md border border-[#404040] bg-[#262626] p-2 shadow-2xl shadow-black/30"
|
||||
role="menu"
|
||||
>
|
||||
<div className="flex items-center justify-between px-2 py-2">
|
||||
<p className="text-sm font-semibold text-[#e5e5e5]">Notificações</p>
|
||||
<span className="rounded bg-amber-500/15 px-2 py-0.5 text-[10px] font-bold uppercase tracking-[0.08em] text-amber-300">
|
||||
Mock
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{mockNotifications.map((notification) => (
|
||||
<button
|
||||
className="w-full rounded-sm border border-transparent px-2 py-2 text-left transition hover:border-[#404040] hover:bg-[#303030]"
|
||||
key={notification.id}
|
||||
role="menuitem"
|
||||
type="button"
|
||||
>
|
||||
<span className="flex items-start justify-between gap-3">
|
||||
<span className="min-w-0">
|
||||
<span className="block text-sm font-semibold text-[#e5e5e5]">{notification.title}</span>
|
||||
<span className="mt-0.5 block text-xs leading-5 text-[#a3a3a3]">{notification.detail}</span>
|
||||
</span>
|
||||
<span className="shrink-0 text-[10px] font-semibold text-[#51a2ff]">{notification.time}</span>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<span className="hidden h-6 w-px bg-[#404040] sm:block" aria-hidden="true" />
|
||||
|
||||
<button
|
||||
className="flex min-w-0 items-center gap-3 text-left"
|
||||
onClick={() => goTo('/perfil')}
|
||||
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]">
|
||||
{getInitials(viewerProfile.name)}
|
||||
</span>
|
||||
<span className="hidden min-w-0 sm:block">
|
||||
<span className="block truncate text-sm font-semibold leading-4 text-[#e5e5e5]">
|
||||
{viewerProfile.name}
|
||||
<div className="relative z-30">
|
||||
<button
|
||||
aria-expanded={profileMenuOpen}
|
||||
aria-haspopup="menu"
|
||||
className="flex min-w-0 items-center gap-3 rounded-sm px-1.5 py-1 text-left transition hover:bg-[#303030] focus:outline-none focus:ring-2 focus:ring-[#3b82f6]/40"
|
||||
onClick={() => {
|
||||
setProfileMenuOpen((open) => !open)
|
||||
setNotificationsOpen(false)
|
||||
}}
|
||||
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]">
|
||||
{getInitials(viewerProfile.name)}
|
||||
</span>
|
||||
<span className="mt-0.5 block truncate text-[11px] font-medium leading-4 text-[#51a2ff]">
|
||||
{viewerProfile.role}
|
||||
<span className="hidden min-w-0 sm:block">
|
||||
<span className="block max-w-40 truncate text-sm font-semibold leading-4 text-[#e5e5e5]">
|
||||
{viewerProfile.name}
|
||||
</span>
|
||||
<span className="mt-0.5 block max-w-40 truncate text-[11px] font-medium leading-4 text-[#51a2ff]">
|
||||
{viewerProfile.role}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<ChevronDownIcon className="hidden size-4 text-[#a3a3a3] sm:block" />
|
||||
</button>
|
||||
<ChevronDownIcon className="hidden size-4 text-[#a3a3a3] sm:block" />
|
||||
</button>
|
||||
|
||||
{profileMenuOpen ? (
|
||||
<div
|
||||
aria-label="Menu do usuário"
|
||||
className="absolute right-0 top-12 z-30 w-56 rounded-md border border-[#404040] bg-[#262626] p-1 shadow-2xl shadow-black/30"
|
||||
role="menu"
|
||||
>
|
||||
<button
|
||||
className="flex w-full items-center gap-2 rounded-sm px-3 py-2 text-left text-sm font-medium text-[#e5e5e5] transition hover:bg-[#303030]"
|
||||
onClick={() => goTo('/perfil')}
|
||||
role="menuitem"
|
||||
type="button"
|
||||
>
|
||||
<UserIcon className="size-4 text-[#a3a3a3]" />
|
||||
Ver perfil
|
||||
</button>
|
||||
|
||||
{canOpenSettings ? (
|
||||
<button
|
||||
className="flex w-full items-center gap-2 rounded-sm px-3 py-2 text-left text-sm font-medium text-[#e5e5e5] transition hover:bg-[#303030]"
|
||||
onClick={() => goTo('/configuracoes')}
|
||||
role="menuitem"
|
||||
type="button"
|
||||
>
|
||||
<AppIcon className="size-4 text-[#a3a3a3]" name="settings" />
|
||||
Configurações
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
<div className="my-1 h-px bg-[#404040]" />
|
||||
|
||||
<button
|
||||
className="flex w-full items-center gap-2 rounded-sm px-3 py-2 text-left text-sm font-medium text-[#f87171] transition hover:bg-[#303030]"
|
||||
onClick={handleLogout}
|
||||
role="menuitem"
|
||||
type="button"
|
||||
>
|
||||
<LogoutIcon className="size-4" />
|
||||
Sair
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{quickSearch ? (
|
||||
<div className="mt-3 rounded-md border border-[#404040] bg-[#303030] px-4 py-3 text-sm text-[#a3a3a3] lg:absolute lg:left-8 lg:top-[52px] lg:w-96">
|
||||
Busca local ativa por <strong className="text-[#e5e5e5]">{quickSearch}</strong>.
|
||||
</div>
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
<main className="w-full px-4 py-6 md:px-8 md:py-8" id="app-content">
|
||||
@@ -362,19 +475,29 @@ function BellIcon({ className = 'size-5' }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ChevronDownIcon({ className = 'size-4' }) {
|
||||
function UserIcon({ className = 'size-4' }) {
|
||||
return (
|
||||
<svg className={className} fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" viewBox="0 0 24 24">
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
<path d="M20 21a8 8 0 0 0-16 0" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function SearchIcon({ className = 'size-4' }) {
|
||||
function LogoutIcon({ className = 'size-4' }) {
|
||||
return (
|
||||
<svg className={className} fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" viewBox="0 0 24 24">
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
<circle cx="11" cy="11" r="7" />
|
||||
<path d="M10 17 15 12l-5-5" />
|
||||
<path d="M15 12H3" />
|
||||
<path d="M21 3v18" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function ChevronDownIcon({ className = 'size-4' }) {
|
||||
return (
|
||||
<svg className={className} fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" viewBox="0 0 24 24">
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user