commit
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-28 12:06:05 +02:00
parent 90a6848721
commit 4d42f083ce
82 changed files with 7538 additions and 76 deletions
+1 -5
View File
@@ -1,10 +1,6 @@
import { usePage } from '@inertiajs/react';
import AppLogoIcon from '@/components/app-logo-icon';
export default function AppLogo() {
const { name } = usePage().props;
return (
<>
<div className="flex aspect-square size-8 items-center justify-center rounded-md bg-sidebar-primary text-sidebar-primary-foreground">
@@ -12,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">
{name}
ProcuraFlow
</span>
</div>
</>
+39 -19
View File
@@ -1,7 +1,14 @@
import { Link } from '@inertiajs/react';
import { BookOpen, FolderGit2, LayoutGrid } from 'lucide-react';
import { Link, usePage } from '@inertiajs/react';
import {
Building2,
ClipboardList,
LayoutGrid,
Users,
Scale,
ShieldAlert,
FileCheck2,
} from 'lucide-react';
import AppLogo from '@/components/app-logo';
import { NavFooter } from '@/components/nav-footer';
import { NavMain } from '@/components/nav-main';
import { NavUser } from '@/components/nav-user';
import {
@@ -18,26 +25,40 @@ import type { NavItem } from '@/types';
const mainNavItems: NavItem[] = [
{
title: 'Dashboard',
title: 'Visão geral',
href: dashboard(),
icon: LayoutGrid,
},
];
const footerNavItems: NavItem[] = [
{
title: 'Repository',
href: 'https://github.com/laravel/react-starter-kit',
icon: FolderGit2,
},
{
title: 'Documentation',
href: 'https://laravel.com/docs/starter-kits#react',
icon: BookOpen,
},
{ title: 'Requisições', href: '/requisicoes', icon: ClipboardList },
{ title: 'Conflitos de interesses', href: '/conflitos', icon: ShieldAlert },
];
export function AppSidebar() {
const { auth } = usePage().props;
const purchasingItems = [
'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 =
auth.user.purchasing_role === 'admin'
? [
...purchasingItems,
{
title: 'Empresa e equipa',
href: '/empresa',
icon: Building2,
},
]
: purchasingItems;
return (
<Sidebar collapsible="icon" variant="inset">
<SidebarHeader>
@@ -53,11 +74,10 @@ export function AppSidebar() {
</SidebarHeader>
<SidebarContent>
<NavMain items={mainNavItems} />
<NavMain items={items} />
</SidebarContent>
<SidebarFooter>
<NavFooter items={footerNavItems} className="mt-auto" />
<NavUser />
</SidebarFooter>
</Sidebar>
+161
View File
@@ -0,0 +1,161 @@
import { Link } from '@inertiajs/react';
import { ArrowUpRight, ClipboardList } from 'lucide-react';
import type { ReactNode } from 'react';
import { Badge } from '@/components/ui/badge';
import { date, money, statusLabels } from '@/types/procurement';
import type { Requisition } from '@/types/procurement';
export const controlClass =
'h-10 w-full rounded-lg border border-input bg-background px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring';
export function PageTitle({
eyebrow,
title,
description,
action,
}: {
eyebrow: string;
title: string;
description: string;
action?: ReactNode;
}) {
return (
<div className="flex flex-wrap items-end justify-between gap-4">
<div>
<p className="mb-2 text-xs font-semibold tracking-widest text-teal-700 uppercase dark:text-teal-400">
{eyebrow}
</p>
<h1 className="text-3xl font-semibold tracking-tight">
{title}
</h1>
<p className="mt-2 max-w-2xl text-sm text-muted-foreground">
{description}
</p>
</div>
{action}
</div>
);
}
export function Panel({
title,
description,
children,
}: {
title: string;
description?: string;
children: ReactNode;
}) {
return (
<section className="rounded-xl border bg-card p-5 shadow-xs">
<h2 className="font-semibold">{title}</h2>
{description && (
<p className="mt-1 text-sm text-muted-foreground">
{description}
</p>
)}
<div className="mt-5">{children}</div>
</section>
);
}
export function Status({ value }: { value: string }) {
return (
<Badge
variant="outline"
className={
value === 'approved'
? 'border-teal-200 bg-teal-50 text-teal-800 dark:bg-teal-950 dark:text-teal-200'
: value === 'pending'
? 'border-amber-200 bg-amber-50 text-amber-800 dark:bg-amber-950 dark:text-amber-200'
: value === 'rejected'
? 'border-rose-200 bg-rose-50 text-rose-800 dark:bg-rose-950 dark:text-rose-200'
: ''
}
>
{statusLabels[value] ?? value}
</Badge>
);
}
export function RequisitionTable({ rows }: { rows: Requisition[] }) {
if (!rows.length)
return (
<div className="py-12 text-center">
<ClipboardList className="mx-auto mb-3 size-9 text-muted-foreground/50" />
<p className="font-medium">Sem requisições para apresentar</p>
<p className="mt-1 text-sm text-muted-foreground">
Os pedidos de compra aparecem aqui depois de criados.
</p>
</div>
);
return (
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead className="border-b text-xs text-muted-foreground">
<tr>
<th className="pb-3 font-medium">Requisição</th>
<th className="pb-3 font-medium">Departamento</th>
<th className="pb-3 font-medium">Data</th>
<th className="pb-3 font-medium">Estado</th>
<th className="pb-3 text-right font-medium">
Total com IVA
</th>
<th>
<span className="sr-only">Abrir</span>
</th>
</tr>
</thead>
<tbody>
{rows.map((r) => (
<tr key={r.id} className="border-b last:border-0">
<td className="py-4 pr-4">
<Link
className="font-semibold hover:text-teal-700"
href={`/requisicoes/${r.id}`}
>
{r.number}
</Link>
<div className="mt-1 text-xs text-muted-foreground">
{r.requester?.name}
</div>
</td>
<td className="pr-4">
{r.department?.name ?? '—'}
</td>
<td className="pr-4 whitespace-nowrap">
{date(r.created_at)}
</td>
<td className="pr-4">
<Status value={r.status} />
</td>
<td className="text-right font-medium whitespace-nowrap">
{money(Number(r.total_minor))}
</td>
<td className="pl-3">
<Link
href={`/requisicoes/${r.id}`}
aria-label={`Abrir ${r.number}`}
>
<ArrowUpRight className="size-4" />
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
export function FormErrors({ errors }: { errors: Record<string, string> }) {
if (!Object.keys(errors).length) return null;
return (
<div
role="alert"
className="rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive"
>
<p className="mb-1 font-medium">Verifique os dados:</p>
<ul className="list-inside list-disc">
{Object.entries(errors).map(([key, value]) => (
<li key={key}>{value}</li>
))}
</ul>
</div>
);
}