@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user