Files
compras/resources/js/components/sgc-shell.tsx
T
nala 413c201d97
tests / ci (push) Canceled after 0s
commit5
2026-10-29 22:03:09 +02:00

338 lines
13 KiB
TypeScript

import { Link, usePage } from '@inertiajs/react';
import { ChevronDown, LayoutDashboard } from 'lucide-react';
import type { ReactNode } from 'react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { UserMenuContent } from '@/components/user-menu-content';
import { SgcWorkspaceShell } from '@/components/sgc-workspace-shell';
type Entry = { label: string; path: string; demo: string; roles?: string[] };
const readers = ['admin', 'procurement', 'finance', 'department_head'];
const inventory = [...readers, 'warehouse'];
const groups: { label: string; entries: Entry[] }[] = [
{
label: 'Compras',
entries: [
{ label: 'Planeamento', path: '/planeamento', demo: '' },
{
label: 'Requisições',
path: '/requisicoes',
demo: '/consulta/requisicoes',
},
{
label: 'Fornecedores',
path: '/fornecedores',
demo: '/consulta/fornecedores',
roles: readers,
},
{
label: 'Cotações',
path: '/cotacoes',
demo: '/consulta/cotacoes',
roles: readers,
},
{
label: 'Ordens de compra',
path: '/ordens',
demo: '/consulta/ordens',
roles: readers,
},
{
label: 'Recomendação (proposta)',
path: '',
demo: '/proposta/recomendacao',
roles: [],
},
],
},
{
label: 'Finanças',
entries: [
{
label: 'Orçamentos',
path: '/orcamentos',
demo: '/consulta?ecra=reinforcements',
roles: ['admin', 'finance'],
},
{
label: 'Facturas',
path: '/facturas',
demo: '/consulta/facturas',
roles: ['admin', 'finance'],
},
{
label: 'Reforços orçamentais',
path: '/orcamentos',
demo: '/consulta/reforcos',
roles: ['admin', 'finance'],
},
{
label: 'Dados bancários',
path: '/fornecedores',
demo: '/consulta/dados-bancarios',
roles: ['admin', 'finance'],
},
{
label: 'Pagamentos externos',
path: '/pagamentos',
demo: '/consulta/pagamentos',
roles: ['admin', 'finance'],
},
{
label: 'Regras de investimento',
path: '/regras-investimento',
demo: '/consulta/investimento',
roles: ['admin', 'finance', 'procurement'],
},
{
label: 'Política de admissão',
path: '/politica-admissao',
demo: '/proposta/admissao',
roles: ['admin'],
},
{
label: 'Política de recomendação',
path: '/politica-recomendacao',
demo: '',
roles: ['admin'],
},
],
},
{
label: 'Armazém',
entries: [
{
label: 'Recepções',
path: '/recepcoes',
demo: '/consulta/recepcoes',
roles: inventory,
},
{
label: 'Equipamentos e peças',
path: '/equipamentos',
demo: '/consulta/equipamentos',
roles: inventory,
},
{
label: 'Stock e movimentos',
path: '/stock',
demo: '/consulta/stock',
roles: ['admin', 'procurement', 'finance', 'warehouse'],
},
{
label: 'Saídas e inventários',
path: '/stock/operacoes',
demo: '/consulta/operacoes-stock',
roles: ['admin', 'procurement', 'finance', 'warehouse'],
},
],
},
{
label: 'Gestão',
entries: [
{
label: 'Conflitos de interesses',
path: '/conflitos',
demo: '/consulta/conflitos',
},
{
label: 'Cancelamento e revisão',
path: '/revisao-compras',
demo: '',
},
{
label: 'Empresa e equipa',
path: '/empresa',
demo: '',
roles: ['admin'],
},
{
label: 'Aparência',
path: '/settings/appearance',
demo: '/consulta/aparencia',
},
],
},
];
function SgcPreviewShell({
children,
demo = false,
}: {
children: ReactNode;
demo?: boolean;
}) {
const page = usePage();
const user = page.props.auth?.user;
const role = user?.purchasing_role ?? '';
const overview = demo ? '/proposta/principal' : '/dashboard';
const active = (entry: Entry) =>
demo
? page.url.split('?')[0] === entry.demo.split('?')[0] &&
page.url === entry.demo
: page.url === entry.path || page.url.startsWith(`${entry.path}/`);
return (
<div className="flex min-h-screen flex-col bg-background text-foreground">
<a
href="#sgc-main"
className="sr-only z-50 rounded bg-white p-3 text-black focus:not-sr-only focus:absolute"
>
Saltar para o conteúdo
</a>
{demo && (
<div className="bg-[#1f1f1f] px-4 py-2 text-center text-xs text-slate-200">
Demonstração · Dados fictícios · Sem operações reais
</div>
)}
<header className="border-b border-white/10 bg-[#090909] text-white print:hidden">
<div className="mx-auto flex max-w-7xl flex-wrap items-center justify-between gap-4 px-5 py-5 lg:px-8">
<Link href={overview} className="flex items-center gap-3">
<span className="grid size-11 place-items-center rounded-xl bg-[#ed1c24] font-bold text-white">
SGC
</span>
<span>
<span className="block text-sm font-semibold">
Sistema de Gestão de Compras
</span>
<span className="mt-1 block text-xs text-slate-400">
Compras com controlo e transparência
</span>
</span>
</Link>
{user && !demo ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="text-white hover:bg-white/10 hover:text-white"
>
{user.name}
<ChevronDown className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<UserMenuContent user={user} />
</DropdownMenuContent>
</DropdownMenu>
) : (
<Button
asChild
variant="outline"
className="border-white/20 bg-transparent text-white hover:bg-white/10 hover:text-white"
>
<Link href="/login">Entrar na minha empresa</Link>
</Button>
)}
</div>
<nav
aria-label="Navegação principal"
className="mx-auto flex max-w-7xl flex-wrap gap-1 px-5 pb-3 lg:px-8"
>
<Link
href={overview}
aria-current={
page.url === overview ? 'page' : undefined
}
className={`inline-flex min-h-11 items-center gap-2 rounded-lg border-b-2 px-4 py-2 text-sm font-medium ${page.url === overview ? 'border-[#ed1c24] bg-white/5' : 'border-transparent text-slate-200 hover:bg-white/10'}`}
>
<LayoutDashboard className="size-4" />
Visão geral
</Link>
{groups.map((group) => {
const entries = group.entries.filter((entry) =>
demo
? entry.demo !== ''
: !entry.roles || entry.roles.includes(role),
);
if (!entries.length) return null;
return (
<DropdownMenu key={group.label}>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className={`min-h-11 border-b-2 text-slate-200 hover:bg-white/10 hover:text-white data-[state=open]:bg-white/10 ${entries.some(active) ? 'border-[#ed1c24] text-white' : 'border-transparent'}`}
>
{group.label}
<ChevronDown className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="min-w-60 rounded-xl border-slate-200 bg-white p-2 text-slate-900 shadow-xl"
>
{entries.map((entry) => (
<DropdownMenuItem
key={entry.label}
asChild
className="min-h-11 rounded-md focus:bg-red-50 focus:text-red-700"
>
<Link
href={
demo
? entry.demo
: entry.path
}
aria-current={
active(entry)
? 'page'
: undefined
}
>
{entry.label}
</Link>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
})}
</nav>
</header>
<main id="sgc-main" className="min-w-0 flex-1" tabIndex={-1}>
{children}
</main>
<footer className="mt-auto bg-[#090909] text-slate-300 print:hidden">
<div className="mx-auto flex max-w-7xl flex-wrap items-center justify-between gap-3 px-5 py-6 text-xs lg:px-8">
<span>
<strong className="font-semibold text-white">
SGC
</strong>{' '}
· Sistema de Gestão de Compras
</span>
<span>
{demo
? 'Ambiente de demonstração'
: 'Espaço de trabalho da sua empresa'}
<span className="mx-3 text-[#ed1c24]">•</span>
<Link
href={demo ? '/' : '/settings/profile'}
className="hover:text-white"
>
{demo ? 'Página inicial' : 'A minha conta'}
</Link>
</span>
</div>
</footer>
</div>
);
}
export function SgcShell({
children,
demo = false,
}: {
children: ReactNode;
demo?: boolean;
}) {
return demo ? (
<SgcPreviewShell demo>{children}</SgcPreviewShell>
) : (
<SgcWorkspaceShell>{children}</SgcWorkspaceShell>
);
}