370 lines
17 KiB
TypeScript
370 lines
17 KiB
TypeScript
import { Link, usePage } from '@inertiajs/react';
|
|
import {
|
|
ChevronDown,
|
|
LayoutDashboard,
|
|
Menu,
|
|
Plus,
|
|
Search,
|
|
X,
|
|
} from 'lucide-react';
|
|
import { useEffect, useState } from 'react';
|
|
import type { ReactNode } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetDescription,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
} from '@/components/ui/sheet';
|
|
import { UserMenuContent } from '@/components/user-menu-content';
|
|
import {
|
|
activeProcurementEntry,
|
|
procurementNavigation,
|
|
} from '@/lib/procurement-navigation';
|
|
import { roleLabels } from '@/types/procurement';
|
|
|
|
export function SgcWorkspaceShell({ children }: { children: ReactNode }) {
|
|
const page = usePage();
|
|
const user = page.props.auth?.user;
|
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
const [search, setSearch] = useState('');
|
|
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
|
const groups = procurementNavigation(
|
|
user?.purchasing_role ?? '',
|
|
page.props.payablesAvailable === true,
|
|
);
|
|
const activeId = activeProcurementEntry(groups, page.url);
|
|
const activeGroup = groups.find((group) =>
|
|
group.entries.some((entry) => entry.id === activeId),
|
|
);
|
|
const activeGroupId = activeGroup?.id;
|
|
useEffect(() => {
|
|
if (activeGroupId)
|
|
setExpanded((previous) =>
|
|
previous[activeGroupId]
|
|
? previous
|
|
: { ...previous, [activeGroupId]: true },
|
|
);
|
|
}, [activeId, activeGroupId]);
|
|
const normalize = (text: string) =>
|
|
text
|
|
.normalize('NFD')
|
|
.replace(/[\u0300-\u036f]/g, '')
|
|
.toLocaleLowerCase('pt');
|
|
const query = normalize(search.trim());
|
|
const visibleGroups = groups
|
|
.map((group) => ({
|
|
...group,
|
|
entries: group.entries.filter(
|
|
(entry) =>
|
|
!query ||
|
|
normalize(group.label + ' ' + entry.label).includes(query),
|
|
),
|
|
}))
|
|
.filter((group) => group.entries.length);
|
|
const navigate = () => {
|
|
setMobileOpen(false);
|
|
setSearch('');
|
|
};
|
|
const brand = (
|
|
<Link
|
|
href="/dashboard"
|
|
onClick={navigate}
|
|
className="flex items-center gap-3 px-5 py-6 focus-visible:outline-2 focus-visible:outline-offset-[-4px] focus-visible:outline-white"
|
|
>
|
|
<span className="grid size-10 shrink-0 place-items-center rounded-xl bg-[#ed1c24] text-sm font-bold text-white">
|
|
SGC
|
|
</span>
|
|
<span>
|
|
<span className="block text-lg font-semibold tracking-tight text-white">
|
|
Compras
|
|
</span>
|
|
<span className="block text-xs text-slate-400">
|
|
Sistema de Gestão de Compras
|
|
</span>
|
|
</span>
|
|
</Link>
|
|
);
|
|
const searchField = (mobile = false) => (
|
|
<div
|
|
className={`relative ${mobile ? 'mx-4 mb-4' : 'hidden w-full max-w-sm lg:block'}`}
|
|
>
|
|
<Search
|
|
className="pointer-events-none absolute top-3 left-3 size-4 text-slate-400"
|
|
aria-hidden="true"
|
|
/>
|
|
<input
|
|
type="search"
|
|
aria-label="Procurar no menu"
|
|
placeholder="Procurar no menu…"
|
|
value={search}
|
|
onChange={(event) => setSearch(event.target.value)}
|
|
className={`h-10 w-full rounded-lg border pr-9 pl-9 text-sm outline-none focus:border-red-400 focus:ring-2 focus:ring-red-400/20 ${mobile ? 'border-white/15 bg-white/5 text-white placeholder:text-slate-400' : 'border-border bg-background text-foreground'}`}
|
|
/>
|
|
{search && (
|
|
<button
|
|
type="button"
|
|
aria-label="Limpar pesquisa do menu"
|
|
onClick={() => setSearch('')}
|
|
className="absolute top-2 right-2 rounded p-1 text-slate-400 hover:text-red-400"
|
|
>
|
|
<X className="size-4" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
const navigation = (
|
|
<nav
|
|
aria-label="Navegação principal"
|
|
className="min-h-0 flex-1 overflow-y-auto px-3 pb-5"
|
|
>
|
|
<p className="px-3 pt-2 pb-3 text-[10px] font-semibold tracking-[0.18em] text-slate-500">
|
|
ESPAÇO DE TRABALHO
|
|
</p>
|
|
{(!query ||
|
|
normalize('Painel inicial Visão geral').includes(query)) && (
|
|
<Link
|
|
href="/dashboard"
|
|
onClick={navigate}
|
|
aria-current={
|
|
page.url.split('?')[0] === '/dashboard'
|
|
? 'page'
|
|
: undefined
|
|
}
|
|
className={`mb-3 flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors focus-visible:outline-2 focus-visible:outline-white ${page.url.split('?')[0] === '/dashboard' ? 'bg-white/10 text-white' : 'text-slate-300 hover:bg-white/5 hover:text-white'}`}
|
|
>
|
|
<LayoutDashboard
|
|
className="size-4 shrink-0"
|
|
aria-hidden="true"
|
|
/>
|
|
Painel inicial
|
|
</Link>
|
|
)}
|
|
{visibleGroups.map((group) => (
|
|
<details
|
|
key={group.id}
|
|
open={
|
|
query
|
|
? true
|
|
: (expanded[group.id] ??
|
|
activeGroup?.id === group.id)
|
|
}
|
|
onToggle={(event) => {
|
|
if (!query) {
|
|
const open = event.currentTarget.open;
|
|
setExpanded((previous) =>
|
|
previous[group.id] === open
|
|
? previous
|
|
: { ...previous, [group.id]: open },
|
|
);
|
|
}
|
|
}}
|
|
className="group/menu mb-1"
|
|
>
|
|
<summary
|
|
className={`flex cursor-pointer list-none items-center gap-3 rounded-lg px-3 py-2.5 text-[13px] font-medium transition-colors hover:bg-white/5 focus-visible:outline-2 focus-visible:outline-white [&::-webkit-details-marker]:hidden ${activeGroup?.id === group.id ? 'text-white' : 'text-slate-300'}`}
|
|
>
|
|
<group.icon
|
|
className={`size-4 shrink-0 ${activeGroup?.id === group.id ? 'text-red-400' : 'text-slate-500'}`}
|
|
aria-hidden="true"
|
|
/>
|
|
<span className="flex-1">{group.label}</span>
|
|
<ChevronDown
|
|
className="size-3.5 shrink-0 text-slate-500 transition-transform group-open/menu:rotate-180"
|
|
aria-hidden="true"
|
|
/>
|
|
</summary>
|
|
<ul className="mt-1 mb-3 ml-5 space-y-1 border-l border-white/10 pl-3">
|
|
{group.entries.map((entry) => {
|
|
const className = `block rounded-md px-3 py-2 text-[13px] leading-5 transition-colors focus-visible:outline-2 focus-visible:outline-white ${activeId === entry.id ? 'bg-red-500/15 font-medium text-red-300' : 'text-slate-400 hover:bg-white/5 hover:text-white'}`;
|
|
return (
|
|
<li key={entry.id}>
|
|
{entry.fullPage ? (
|
|
<a
|
|
href={entry.path}
|
|
onClick={navigate}
|
|
className={className}
|
|
aria-current={
|
|
activeId === entry.id
|
|
? 'page'
|
|
: undefined
|
|
}
|
|
>
|
|
{entry.label}
|
|
</a>
|
|
) : (
|
|
<Link
|
|
href={entry.path}
|
|
onClick={navigate}
|
|
className={className}
|
|
aria-current={
|
|
activeId === entry.id
|
|
? 'page'
|
|
: undefined
|
|
}
|
|
>
|
|
{entry.label}
|
|
</Link>
|
|
)}
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</details>
|
|
))}
|
|
{query &&
|
|
visibleGroups.length === 0 &&
|
|
!normalize('Painel inicial Visão geral').includes(query) && (
|
|
<p
|
|
role="status"
|
|
className="px-3 py-5 text-sm text-slate-400"
|
|
>
|
|
Nenhuma opção encontrada. Tenta outra palavra.
|
|
</p>
|
|
)}
|
|
</nav>
|
|
);
|
|
const sidebarFooter = (
|
|
<div className="border-t border-white/10 px-5 py-4">
|
|
<p className="text-[11px] text-slate-500">
|
|
SGC · Espaço de trabalho
|
|
</p>
|
|
<Link
|
|
href="/settings/appearance"
|
|
onClick={navigate}
|
|
className="mt-1 inline-block text-xs text-slate-400 hover:text-white"
|
|
>
|
|
Aparência
|
|
</Link>
|
|
<span className="mx-2 text-slate-700">·</span>
|
|
<Link
|
|
href="/"
|
|
onClick={navigate}
|
|
className="text-xs text-slate-400 hover:text-white"
|
|
>
|
|
Página inicial
|
|
</Link>
|
|
</div>
|
|
);
|
|
return (
|
|
<div className="min-h-screen bg-slate-50 text-foreground lg:grid lg:grid-cols-[17rem_minmax(0,1fr)] dark:bg-background print:block">
|
|
<a
|
|
href="#sgc-main"
|
|
className="sr-only z-50 rounded bg-white p-3 text-black focus:not-sr-only focus:fixed focus:top-2 focus:left-2"
|
|
>
|
|
Saltar para o conteúdo
|
|
</a>
|
|
<aside className="sticky top-0 hidden h-screen flex-col border-r border-white/5 bg-[#101715] text-white lg:flex print:hidden">
|
|
{brand}
|
|
{navigation}
|
|
{sidebarFooter}
|
|
</aside>
|
|
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
|
|
<SheetContent
|
|
side="left"
|
|
className="flex w-[min(90vw,20rem)] max-w-none flex-col gap-0 border-white/10 bg-[#101715] p-0 text-white [&>button]:text-white"
|
|
>
|
|
<SheetHeader className="sr-only">
|
|
<SheetTitle>Menu de Compras</SheetTitle>
|
|
<SheetDescription>
|
|
Escolhe uma área da aplicação ou procura pelo nome.
|
|
</SheetDescription>
|
|
</SheetHeader>
|
|
{brand}
|
|
{searchField(true)}
|
|
{navigation}
|
|
{sidebarFooter}
|
|
</SheetContent>
|
|
</Sheet>
|
|
<div className="flex min-w-0 flex-col">
|
|
<header className="sticky top-0 z-20 flex min-h-18 items-center justify-between gap-4 border-b border-border bg-background/95 px-5 py-3 backdrop-blur lg:px-8 print:hidden">
|
|
<div className="flex min-w-0 items-center gap-3">
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
aria-label="Abrir menu de Compras"
|
|
onClick={() => setMobileOpen(true)}
|
|
className="shrink-0 lg:hidden"
|
|
>
|
|
<Menu className="size-4" />
|
|
</Button>
|
|
<div className="min-w-0">
|
|
<p className="text-[10px] font-semibold tracking-[0.15em] text-muted-foreground">
|
|
COMPRAS
|
|
</p>
|
|
<p className="truncate text-sm font-medium">
|
|
{page.url.split('?')[0] === '/dashboard'
|
|
? 'Painel inicial'
|
|
: (activeGroup?.label ??
|
|
'Espaço de trabalho')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{searchField()}
|
|
<div className="flex shrink-0 items-center gap-2">
|
|
<Button
|
|
asChild
|
|
size="sm"
|
|
className="hidden bg-[#ed1c24] text-white hover:bg-[#d01720] sm:inline-flex"
|
|
>
|
|
<Link href="/requisicoes/criar">
|
|
<Plus className="size-4" />
|
|
Nova requisição
|
|
</Link>
|
|
</Button>
|
|
{user && (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
aria-label={`Conta de ${user.name}`}
|
|
className="h-auto max-w-52 gap-2 px-2 py-1.5"
|
|
>
|
|
<span
|
|
className="grid size-8 shrink-0 place-items-center rounded-full bg-muted text-xs font-semibold"
|
|
aria-hidden="true"
|
|
>
|
|
{user.name
|
|
.trim()
|
|
.split(/\s+/)
|
|
.slice(0, 2)
|
|
.map((part) => part[0])
|
|
.join('')}
|
|
</span>
|
|
<span className="hidden min-w-0 text-left xl:block">
|
|
<span className="block truncate text-xs font-medium">
|
|
{user.name}
|
|
</span>
|
|
<span className="block text-[10px] text-muted-foreground">
|
|
{roleLabels[
|
|
user.purchasing_role ?? ''
|
|
] ?? 'Utilizador'}
|
|
</span>
|
|
</span>
|
|
<ChevronDown className="size-3 text-muted-foreground" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<UserMenuContent user={user} />
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)}
|
|
</div>
|
|
</header>
|
|
<main id="sgc-main" className="min-w-0 flex-1" tabIndex={-1}>
|
|
{children}
|
|
</main>
|
|
<footer className="mt-auto border-t border-border px-5 py-4 text-xs text-muted-foreground lg:px-8 print:hidden">
|
|
SGC · Sistema de Gestão de Compras
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|