Files
compras/resources/js/pages/preview/index.tsx
T

789 lines
41 KiB
TypeScript

import { Head, Link, router } from '@inertiajs/react';
import { useState } from 'react';
import {
ArrowRight,
Building2,
ClipboardList,
Eye,
FileText,
LayoutGrid,
LockKeyhole,
Menu,
Palette,
Scale,
Search,
ShieldCheck,
Users,
PackageCheck,
Warehouse,
} from 'lucide-react';
import { PageTitle, Panel, Status } from '@/components/procurement';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { money } from '@/types/procurement';
import { previewPaths } from '@/lib/preview-navigation';
import InventoryDemo from './inventory-demo';
import { OperationsDemo } from '../stock/operations';
import { EquipmentDemo } from '../equipment';
// Static examples only: this page never loads or submits business data.
const requests = [
{
number: 'DEMO-001',
title: 'Material de escritório',
department: 'Administração',
status: 'approved',
amount: 1856000,
author: 'Utilizador de exemplo A',
},
{
number: 'DEMO-002',
title: 'Peças para manutenção',
department: 'Produção',
status: 'pending',
amount: 6264000,
author: 'Utilizador de exemplo B',
},
{
number: 'DEMO-003',
title: 'Equipamento informático',
department: 'Tecnologia',
status: 'draft',
amount: 13920000,
author: 'Utilizador de exemplo C',
},
];
const suppliers = [
{
name: 'Fornecedor de demonstração A',
category: 'Material de escritório',
status: 'Activo',
review: '2027-01-15',
contact: 'contacto-a@example.invalid',
},
{
name: 'Fornecedor de demonstração B',
category: 'Equipamentos e peças',
status: 'Pendente de validação',
review: '2027-02-20',
contact: 'contacto-b@example.invalid',
},
{
name: 'Fornecedor de demonstração C',
category: 'Tecnologia',
status: 'Activo',
review: '2027-03-10',
contact: 'contacto-c@example.invalid',
},
];
const quotes = [
{
supplier: 'Fornecedor de demonstração A',
amount: 1600000,
days: 7,
score: '8,65',
evaluations: 2,
},
{
supplier: 'Fornecedor de demonstração B',
amount: 1750000,
days: 10,
score: 'Pendente',
evaluations: 1,
},
{
supplier: 'Fornecedor de demonstração C',
amount: 1680000,
days: 8,
score: '8,12',
evaluations: 2,
},
];
const navigation = [
{ id: 'overview', title: 'Visão geral', icon: LayoutGrid },
{ id: 'requests', title: 'Requisições', icon: ClipboardList },
{ id: 'suppliers', title: 'Fornecedores e anexos', icon: Users },
{ id: 'quotes', title: 'Cotações', icon: Scale },
{ id: 'conflicts', title: 'Conflitos de interesses', icon: ShieldCheck },
{ id: 'orders', title: 'Ordens de compra', icon: FileText },
{ id: 'receipts', title: 'Recepção e qualidade', icon: PackageCheck },
{ id: 'stock', title: 'Stock e movimentos', icon: Warehouse },
{ id: 'operations', title: 'Saídas e inventários', icon: Warehouse },
{ id: 'equipment', title: 'Equipamentos e peças', icon: Warehouse },
{ id: 'appearance', title: 'Cores e componentes', icon: Palette },
];
type Detail = { title: string; subtitle: string; rows: [string, string][] };
const navigationGroups = [
{ title: 'Geral', ids: ['overview'] },
{ title: 'Compras', ids: ['requests', 'suppliers', 'quotes', 'orders'] },
{ title: 'Armazém', ids: ['receipts', 'stock', 'operations', 'equipment'] },
{ title: 'Controlo', ids: ['conflicts'] },
{ title: 'Apresentação', ids: ['appearance'] },
];
export default function Preview({
initialScreen = 'overview',
}: {
initialScreen?: string;
}) {
const screen = initialScreen;
const [search, setSearch] = useState('');
const [menu, setMenu] = useState(false);
const [detail, setDetail] = useState<Detail | null>(null);
const title = navigation.find((n) => n.id === screen)?.title ?? 'Consulta';
function navigate(id: string) {
setSearch('');
setMenu(false);
router.get(previewPaths[id] ?? '/consulta');
}
const filteredRequests = requests.filter((r) =>
`${r.number} ${r.title} ${r.department}`
.toLowerCase()
.includes(search.toLowerCase()),
);
const requestTable = (
<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="pr-4 pb-3">Requisição</th>
<th className="pr-4">Departamento</th>
<th className="pr-4">Estado</th>
<th className="pr-4 text-right">Total com IVA</th>
<th>
<span className="sr-only">Consultar</span>
</th>
</tr>
</thead>
<tbody>
{filteredRequests.map((r) => (
<tr key={r.number} className="border-b last:border-0">
<td className="py-5 pr-4">
<strong>{r.number}</strong>
<p className="mt-1 text-xs text-muted-foreground">
{r.title}
</p>
</td>
<td className="pr-4">{r.department}</td>
<td className="pr-4">
<Status value={r.status} />
</td>
<td className="pr-4 text-right whitespace-nowrap">
{money(r.amount)}
</td>
<td>
<Button
variant="ghost"
size="sm"
onClick={() =>
setDetail({
title: r.number,
subtitle: r.title,
rows: [
['Solicitante', r.author],
['Departamento', r.department],
[
'Total com IVA',
money(r.amount),
],
[
'Finalidade',
'Exemplo fictício para consulta visual.',
],
],
})
}
>
<Eye className="size-4" />
<span className="sr-only">
Ver {r.number}
</span>
</Button>
</td>
</tr>
))}
</tbody>
</table>
{!filteredRequests.length && (
<p className="py-8 text-center text-muted-foreground">
Nenhum exemplo corresponde à pesquisa.
</p>
)}
</div>
);
return (
<>
<Head title="Consulta livre" />
<div className="min-h-screen bg-muted/25 text-foreground">
<aside
className={`${menu ? 'flex' : 'hidden'} fixed inset-y-0 left-0 z-30 w-64 flex-col border-r bg-background p-5 lg:flex`}
>
<Link
href="/"
className="mb-8 flex items-center gap-3 text-xl font-semibold"
>
<span className="grid size-9 place-items-center rounded-xl bg-teal-700 text-sm text-white">
SGC
</span>
SGC
</Link>
<div className="mb-7 rounded-xl border bg-muted/30 p-4">
<p className="flex items-center gap-2 text-sm font-semibold">
<Building2 className="size-4" />
Empresa de demonstração
</p>
<p className="mt-2 text-xs text-muted-foreground">
Consulta livre · sem autenticação
</p>
</div>
<nav
aria-label="Áreas da demonstração"
className="space-y-4"
>
{navigationGroups.map((group) => (
<div key={group.title}>
<p className="mb-1 px-3 text-[10px] font-semibold tracking-widest text-muted-foreground uppercase">
{group.title}
</p>
{navigation
.filter((n) => group.ids.includes(n.id))
.map((n) => (
<Link
href={previewPaths[n.id]}
key={n.id}
onClick={() => setMenu(false)}
aria-current={
screen === n.id
? 'page'
: undefined
}
className={`flex w-full items-center gap-3 rounded-lg px-3 py-3 text-left text-sm transition-colors ${screen === n.id ? 'bg-teal-50 font-medium text-teal-800 dark:bg-teal-950 dark:text-teal-200' : 'text-muted-foreground hover:bg-muted'}`}
>
<n.icon className="size-4" />
{n.title}
</Link>
))}
</div>
))}
</nav>
<p className="mt-auto pt-8 text-xs leading-5 text-muted-foreground">
Dados fictícios. A navegação e a pesquisa permitem
avaliar a apresentação; não executam compras.
</p>
</aside>
{menu && (
<button
className="fixed inset-0 z-20 bg-black/25 lg:hidden"
aria-label="Fechar menu"
onClick={() => setMenu(false)}
/>
)}
<div className="lg:ml-64">
<header className="flex min-h-20 items-center justify-between gap-3 border-b bg-background px-5 lg:px-8">
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="icon"
className="lg:hidden"
aria-label="Abrir menu"
onClick={() => setMenu(!menu)}
>
<Menu className="size-5" />
</Button>
<span className="text-sm text-muted-foreground">
Consulta visual <span className="mx-2">/</span>
<strong className="font-medium text-foreground">
{title}
</strong>
</span>
</div>
<Button asChild variant="outline">
<Link href="/login">
<LockKeyhole className="size-4" />
Entrar na empresa
</Link>
</Button>
</header>
<main className="mx-auto flex max-w-7xl flex-col gap-6 p-5 lg:p-8">
<div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-teal-200 bg-teal-50 px-4 py-3 text-sm text-teal-900 dark:border-teal-900 dark:bg-teal-950 dark:text-teal-100">
<span className="flex items-center gap-2">
<Eye className="size-4" />
<strong>Entrada livre para consulta</strong>
</span>
<span>
Exemplos fictícios · sem gravação de dados
</span>
</div>
<PageTitle
eyebrow="SGC · Demonstração"
title={title}
description="Explore os exemplos para verificar a organização, os ecrãs, as cores e a navegação. Os dados apresentados não pertencem à sua empresa."
/>
{screen === 'overview' && (
<>
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
{[
[
'Requisições de exemplo',
'3',
'Uma aprovada, uma pendente e um rascunho',
],
[
'A aguardar decisão',
'1',
'Exemplo de aprovação por departamento',
],
[
'Fornecedores de exemplo',
'3',
'Com estados de validação diferentes',
],
[
'Valor aprovado de exemplo',
money(1856000),
'Estimativa em MZN, com IVA',
],
].map(([label, value, note]) => (
<div
key={label}
className="rounded-xl border bg-card p-5 shadow-xs"
>
<p className="text-sm text-muted-foreground">
{label}
</p>
<strong className="mt-5 block text-2xl tracking-tight">
{value}
</strong>
<p className="mt-2 text-xs text-muted-foreground">
{note}
</p>
</div>
))}
</div>
<Panel
title="Requisições recentes"
description="Clique no ícone de consulta para abrir os detalhes do exemplo."
>
{requestTable}
</Panel>
<div className="grid gap-4 md:grid-cols-3">
{[
[
'suppliers',
'Ficha e documentos',
'Consultar identificação, revisão e anexos.',
],
[
'quotes',
'Comparação de propostas',
'Comparar preços, prazos e avaliações.',
],
[
'conflicts',
'Transparência nas decisões',
'Consultar exemplos de declarações pessoais.',
],
].map(([id, label, note]) => (
<button
key={id}
onClick={() => navigate(id)}
className="rounded-xl border bg-card p-5 text-left shadow-xs hover:border-teal-500"
>
<strong className="flex items-center justify-between text-sm">
{label}
<ArrowRight className="size-4 text-teal-700" />
</strong>
<p className="mt-3 text-sm text-muted-foreground">
{note}
</p>
</button>
))}
</div>
</>
)}
{screen === 'requests' && (
<Panel
title="Pedidos de compra"
description="Pesquisa e detalhe disponíveis nesta demonstração."
>
<div className="relative mb-5">
<Search className="absolute top-3 left-3 size-4 text-muted-foreground" />
<Input
className="pl-10"
aria-label="Pesquisar exemplos"
placeholder="Pesquisar referência, descrição ou departamento"
value={search}
onChange={(e) =>
setSearch(e.target.value)
}
/>
</div>
{requestTable}
</Panel>
)}
{screen === 'suppliers' && (
<div className="grid gap-5 lg:grid-cols-3">
{suppliers.map((s) => (
<Panel
key={s.name}
title={s.name}
description={s.category}
>
<div className="space-y-4 text-sm">
<span
className={`inline-block rounded-full px-3 py-1 text-xs ${s.status === 'Activo' ? 'bg-teal-50 text-teal-800' : 'bg-amber-50 text-amber-900'}`}
>
{s.status}
</span>
<p>
<span className="block text-xs text-muted-foreground">
Próxima revisão
</span>
{s.review}
</p>
<p className="break-all">
<span className="block text-xs text-muted-foreground">
Contacto fictício
</span>
{s.contact}
</p>
<Button
variant="outline"
className="w-full"
onClick={() =>
setDetail({
title: s.name,
subtitle:
'Ficha de demonstração · dados fictícios',
rows: [
[
'Categoria',
s.category,
],
[
'Estado',
s.status,
],
[
'Próxima revisão',
s.review,
],
[
'Documentos de exemplo',
'Alvará.pdf · Declaração.pdf (sem ficheiros reais)',
],
[
'Controlo',
'Alterações e novos anexos exigem nova validação por Finanças.',
],
],
})
}
>
Consultar ficha
</Button>
</div>
</Panel>
))}
</div>
)}
{screen === 'quotes' && (
<Panel
title="Matriz de avaliação · DEMO-001"
description="Valores ilustrativos. Preço 40% · qualidade 20% · prazo 15% · histórico 15% · pagamento 10%."
>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead className="border-b text-xs text-muted-foreground">
<tr>
{[
'Fornecedor',
'Preço sem IVA',
'Prazo',
'Avaliações',
'Pontuação',
].map((h) => (
<th
key={h}
className="pr-4 pb-3"
>
{h}
</th>
))}
</tr>
</thead>
<tbody>
{quotes.map((q) => (
<tr
className="border-b last:border-0"
key={q.supplier}
>
<td className="py-5 pr-4">
{q.supplier}
</td>
<td className="pr-4 whitespace-nowrap">
{money(q.amount)}
</td>
<td className="pr-4">
{q.days} dias
</td>
<td className="pr-4">
{q.evaluations} / 2
</td>
<td className="pr-4 font-semibold">
{q.score}
</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mt-5 rounded-lg bg-amber-50 p-4 text-sm text-amber-900">
Exemplo de comparação incompleta: uma das
propostas ainda não tem duas avaliações. A
consulta não permite adjudicar.
</p>
</Panel>
)}
{screen === 'conflicts' && (
<Panel
title="Declarações pessoais"
description="Exemplo fictício de um conflito por fornecedor, com histórico preservado."
>
<div className="rounded-lg border p-5">
<div className="flex flex-wrap items-center justify-between gap-3">
<strong>
Fornecedor de demonstração B
</strong>
<span className="rounded-full bg-rose-50 px-3 py-1 text-xs text-rose-800">
Conflito declarado
</span>
</div>
<dl className="mt-5 grid gap-5 text-sm sm:grid-cols-2">
<div>
<dt className="text-muted-foreground">
Autor
</dt>
<dd>Utilizador de exemplo A</dd>
</div>
<div>
<dt className="text-muted-foreground">
Âmbito
</dt>
<dd>
Fornecedor em todos os processos
</dd>
</div>
<div>
<dt className="text-muted-foreground">
Fundamento fictício
</dt>
<dd>
Relação pessoal com o
fornecedor.
</dd>
</div>
<div>
<dt className="text-muted-foreground">
Efeito
</dt>
<dd>
Participação impedida nos
processos afectados.
</dd>
</div>
</dl>
<p className="mt-5 border-t pt-4 text-xs text-muted-foreground">
Uma declaração de ausência não anula um
conflito registado. A política de
levantamento continua por definir.
</p>
</div>
</Panel>
)}
{screen === 'orders' && (
<Panel
title="Adjudicação e ordens de compra"
description="Etapa em desenvolvimento no projecto. Este ecrã é apenas uma ilustração, não uma ordem emitida."
>
<div className="rounded-lg border p-6">
<div className="flex flex-wrap justify-between gap-4">
<div>
<p className="text-xs tracking-widest text-teal-700 uppercase">
Exemplo visual
</p>
<h2 className="mt-2 text-xl font-semibold">
Ordem DEMO-OC-001
</h2>
</div>
<span className="text-sm text-muted-foreground">
Sem validade comercial
</span>
</div>
<p className="mt-6 text-sm">
Fornecedor de demonstração A · Material
de escritório
</p>
<div className="mt-5 grid gap-4 border-y py-5 text-sm sm:grid-cols-3">
<p>
Sem IVA
<br />
<strong>{money(1600000)}</strong>
</p>
<p>
IVA ilustrativo
<br />
<strong>{money(256000)}</strong>
</p>
<p>
Total
<br />
<strong>{money(1856000)}</strong>
</p>
</div>
<p className="mt-5 text-sm text-muted-foreground">
A emissão real depende das cotações,
avaliações, aprovações e verificações de
conflito no servidor.
</p>
</div>
</Panel>
)}
{(screen === 'receipts' || screen === 'stock') && (
<InventoryDemo key={screen} screen={screen} />
)}
{screen === 'operations' && <OperationsDemo />}
{screen === 'equipment' && <EquipmentDemo />}
{screen === 'appearance' && (
<>
<Panel
title="Paleta e estados"
description="Componentes e estilos partilhados com a aplicação para apoiar a revisão visual."
>
<div className="grid gap-4 sm:grid-cols-4">
{[
['Principal', 'bg-teal-700'],
['Aprovação', 'bg-teal-100'],
['Atenção', 'bg-amber-100'],
['Impedimento', 'bg-rose-100'],
].map(([label, color]) => (
<div key={label}>
<div
className={`h-20 rounded-lg border ${color}`}
/>
<p className="mt-2 text-sm">
{label}
</p>
</div>
))}
</div>
<div className="mt-6 flex flex-wrap gap-3">
<Status value="draft" />
<Status value="pending" />
<Status value="approved" />
<Status value="rejected" />
</div>
</Panel>
<Panel
title="Campos e botões"
description="Os campos abaixo são apenas locais e não são enviados ou guardados."
>
<div className="grid gap-5 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="sample-title">
Descrição de exemplo
</Label>
<Input
id="sample-title"
placeholder="Escreva para experimentar o campo"
/>
</div>
<div className="space-y-2">
<Label htmlFor="sample-disabled">
Campo apenas de consulta
</Label>
<Input
id="sample-disabled"
disabled
value="Valor de demonstração"
/>
</div>
</div>
<div className="mt-6 flex flex-wrap gap-3">
<Button
onClick={() =>
setDetail({
title: 'Exemplo de diálogo',
subtitle:
'Sem alteração de dados',
rows: [
[
'Acção',
'Este botão apenas demonstra a apresentação de uma confirmação.',
],
],
})
}
>
Experimentar diálogo
</Button>
<Button
variant="outline"
onClick={() => navigate('overview')}
>
Voltar à visão geral
</Button>
<Button disabled>
Acção indisponível
</Button>
</div>
</Panel>
</>
)}
<footer className="py-4 text-xs text-muted-foreground">
SGC · Consulta pública de exemplos · Os processos
reais permanecem no acesso autenticado.
</footer>
</main>
</div>
</div>
<Dialog
open={detail !== null}
onOpenChange={(open) => {
if (!open) setDetail(null);
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>{detail?.title}</DialogTitle>
<DialogDescription>
{detail?.subtitle}
</DialogDescription>
</DialogHeader>
<dl className="space-y-4">
{detail?.rows.map(([label, value]) => (
<div key={label}>
<dt className="text-xs text-muted-foreground">
{label}
</dt>
<dd className="mt-1 text-sm">{value}</dd>
</div>
))}
</dl>
<p className="border-t pt-4 text-xs text-muted-foreground">
Consulta livre · dados fictícios · nenhuma operação de
compra é efectuada.
</p>
</DialogContent>
</Dialog>
</>
);
}