Consolida recepcoes, stock, equipamentos e identidade SGC

This commit is contained in:
2026-10-28 13:54:53 +02:00
parent eb1c8b8074
commit 75ffdb395c
76 changed files with 6933 additions and 152 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ export default function AppLogo() {
</div>
<div className="ml-1 grid flex-1 text-left text-sm">
<span className="mb-0.5 truncate leading-tight font-semibold">
ProcuraFlow
SGC
</span>
</div>
</>
+80 -15
View File
@@ -7,6 +7,8 @@ import {
Scale,
ShieldAlert,
FileCheck2,
PackageCheck,
Warehouse,
} from 'lucide-react';
import AppLogo from '@/components/app-logo';
import { NavMain } from '@/components/nav-main';
@@ -23,42 +25,89 @@ import {
import { dashboard } from '@/routes';
import type { NavItem } from '@/types';
const mainNavItems: NavItem[] = [
const generalNavItems: NavItem[] = [
{
title: 'Visão geral',
href: dashboard(),
icon: LayoutGrid,
},
{ title: 'Requisições', href: '/requisicoes', icon: ClipboardList },
{ title: 'Conflitos de interesses', href: '/conflitos', icon: ShieldAlert },
];
export function AppSidebar() {
const { auth } = usePage().props;
const purchasingItems = [
const canReadPurchases = [
'admin',
'procurement',
'finance',
'department_head',
].includes(auth.user.purchasing_role ?? '')
? [
...mainNavItems,
{ title: 'Fornecedores', href: '/fornecedores', icon: Users },
{ title: 'Cotações', href: '/cotacoes', icon: Scale },
{ title: 'Ordens de compra', href: '/ordens', icon: FileCheck2 },
]
: mainNavItems;
const items =
].includes(auth.user.purchasing_role ?? '');
const purchasingItems: NavItem[] = [
{ title: 'Requisições', href: '/requisicoes', icon: ClipboardList },
...(canReadPurchases
? [
{ title: 'Fornecedores', href: '/fornecedores', icon: Users },
{ title: 'Cotações', href: '/cotacoes', icon: Scale },
{
title: 'Ordens de compra',
href: '/ordens',
icon: FileCheck2,
},
]
: []),
];
const inventoryItems = [
...([
'admin',
'procurement',
'finance',
'warehouse',
'department_head',
].includes(auth.user.purchasing_role ?? '')
? [{ title: 'Recepção', href: '/recepcoes', icon: PackageCheck }]
: []),
...([
'admin',
'procurement',
'finance',
'warehouse',
'department_head',
].includes(auth.user.purchasing_role ?? '')
? [
{
title: 'Equipamentos e peças',
href: '/equipamentos',
icon: Warehouse,
},
]
: []),
...(['admin', 'procurement', 'finance', 'warehouse'].includes(
auth.user.purchasing_role ?? '',
)
? [
{
title: 'Stock e movimentos',
href: '/stock',
icon: Warehouse,
},
{
title: 'Saídas e inventários',
href: '/stock/operacoes',
icon: ClipboardList,
},
]
: []),
];
const administrationItems =
auth.user.purchasing_role === 'admin'
? [
...purchasingItems,
{
title: 'Empresa e equipa',
href: '/empresa',
icon: Building2,
},
]
: purchasingItems;
: [];
return (
<Sidebar collapsible="icon" variant="inset">
<SidebarHeader>
@@ -74,10 +123,26 @@ export function AppSidebar() {
</SidebarHeader>
<SidebarContent>
<NavMain items={items} />
<NavMain title="Geral" items={generalNavItems} />
<NavMain title="Compras" items={purchasingItems} />
{inventoryItems.length > 0 && (
<NavMain title="Armazém" items={inventoryItems} />
)}
{administrationItems.length > 0 && (
<NavMain
title="Administração"
items={administrationItems}
/>
)}
</SidebarContent>
<SidebarFooter>
<Link
href="/"
className="px-3 py-2 text-xs text-muted-foreground hover:text-foreground"
>
Página inicial
</Link>
<NavUser />
</SidebarFooter>
</Sidebar>
+85
View File
@@ -0,0 +1,85 @@
import { Link } from '@inertiajs/react';
import type { Page, ReceiptProgress } from '@/types/inventory';
import { quantity, receiptStates } from '@/types/inventory';
export function InventoryNotice() {
return (
<div className="rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-100">
Ambiente de desenvolvimento. Não autoriza compras reais.{' '}
<strong>O orçamento ainda não é validado.</strong>
</div>
);
}
export function ReceiptProgressTable({
progress,
}: {
progress: ReceiptProgress;
}) {
return (
<>
<p className="mb-4 text-sm font-semibold">
{receiptStates[progress.state]}
{progress.has_divergences
? ' · Existem divergências de qualidade'
: ''}
</p>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead className="border-b text-muted-foreground">
<tr>
<th className="pr-4 pb-3">Artigo</th>
<th className="pr-4">Autorizado</th>
<th className="pr-4">Recebido</th>
<th className="pr-4">Aceite</th>
<th className="pr-4">Rejeitado</th>
<th className="pr-4">Por entregar</th>
<th>Por aceitar</th>
</tr>
</thead>
<tbody>
{progress.lines.map((line) => (
<tr
key={line.id}
className="border-b last:border-0"
>
<td className="py-4 pr-4">
<strong>{line.description}</strong>
<p className="mt-1 text-xs text-muted-foreground">
{line.unit}
</p>
</td>
{[
line.ordered_milli,
line.received_milli,
line.accepted_milli,
line.rejected_milli,
line.delivery_pending_milli,
line.acceptance_pending_milli,
].map((value, index) => (
<td
key={index}
className="pr-4 whitespace-nowrap"
>
{quantity(value)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</>
);
}
export function InventoryPagination<T>({ page }: { page: Page<T> }) {
return (
<div className="mt-4 flex gap-4 text-sm text-teal-700 dark:text-teal-400">
{page.prev_page_url && (
<Link href={page.prev_page_url}>Anterior</Link>
)}
{page.next_page_url && (
<Link href={page.next_page_url}>Seguinte</Link>
)}
</div>
);
}
+20 -4
View File
@@ -7,20 +7,36 @@ import {
SidebarMenuItem,
} from '@/components/ui/sidebar';
import { useCurrentUrl } from '@/hooks/use-current-url';
import { toUrl } from '@/lib/utils';
import type { NavItem } from '@/types';
export function NavMain({ items }: { items: NavItem[] }) {
const { isCurrentUrl } = useCurrentUrl();
export function NavMain({
items,
title = 'Navegação',
}: {
items: NavItem[];
title?: string;
}) {
const { isCurrentOrParentUrl, currentUrl } = useCurrentUrl();
const receivingOrder = /^\/ordens\/\d+\/recepcoes/.test(currentUrl);
return (
<SidebarGroup className="px-2 py-0">
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarGroupLabel>{title}</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
asChild
isActive={isCurrentUrl(item.href)}
isActive={
(toUrl(item.href) === '/recepcoes' &&
receivingOrder) ||
(isCurrentOrParentUrl(item.href) &&
!(
toUrl(item.href) === '/ordens' &&
receivingOrder
))
}
tooltip={{ children: item.title }}
>
<Link href={item.href} prefetch>