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

872 lines
46 KiB
TypeScript

import { Head, router } from '@inertiajs/react';
import { useState } from 'react';
import {
ArrowRight,
ClipboardList,
Eye,
FileText,
LayoutGrid,
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: 'invoices', title: 'Facturas e conferência', icon: FileText },
{ id: 'bank', title: 'Dados bancários', icon: ShieldCheck },
{ id: 'payments', title: 'Pagamentos externos', icon: FileText },
{ id: 'reinforcements', title: 'Reforços orçamentais', icon: FileText },
{ id: 'investment', title: 'Regras de investimento', icon: ShieldCheck },
{ id: 'appearance', title: 'Cores e componentes', icon: Palette },
];
type Detail = { title: string; subtitle: string; rows: [string, string][] };
export default function Preview({
initialScreen = 'overview',
}: {
initialScreen?: string;
}) {
const screen = initialScreen;
const [search, setSearch] = useState('');
const [detail, setDetail] = useState<Detail | null>(null);
const title = navigation.find((n) => n.id === screen)?.title ?? 'Consulta';
function navigate(id: string) {
setSearch('');
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">
<div>
<div className="mx-auto max-w-7xl px-5 pt-5 text-sm text-muted-foreground">
Consulta / {title}
</div>
<div 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 === 'investment' && (
<Panel
title="Regra de investimento · exemplo de circuito"
description="Sem limites predefinidos, consultas ou gravações de negócio."
>
<div className="space-y-3 text-sm">
<p>
A empresa escolhe o tecto por processo,
a base com/sem IVA e as datas. Dois
administradores distintos do proponente
aprovam a versão.
</p>
<p>
Equipamento exige também validação
técnica, Finanças para qualquer valor e
reserva em dotação de investimento. O
tecto aplica-se ao total do processo que
contenha equipamento.
</p>
<p>
Sem regra vigente, ou acima do tecto, a
adjudicação fica bloqueada. A ordem
conserva a versão aplicada; alterações
posteriores não reescrevem documentos
emitidos.
</p>
<p>
Aprovação adicional, agregação por
projecto e detecção automática de
fraccionamento não fazem parte desta
etapa.
</p>
</div>
</Panel>
)}
{screen === 'reinforcements' && (
<Panel
title="Reforço orçamental · exemplo fictício"
description="Valores ilustrativos; cada empresa define as suas dotações. Sem consultas ou gravações de negócio."
>
<div className="space-y-3 text-sm">
<p>
Dotação inicial fictícia de 1.000,00
MZN, com 300,00 MZN utilizados:
disponível de 700,00 MZN.
</p>
<p>
Um reforço proposto de 400,00 MZN fica
pendente. Só depois da aprovação por
outra pessoa a dotação acumulada passa a
1.400,00 MZN e o disponível a 1.100,00
MZN.
</p>
<p>
O valor inicial, a base de IVA, o centro
de custo e o exercício são preservados.
Repetir uma aprovação não duplica a
verba. Rejeições ficam no histórico sem
alterar o saldo.
</p>
</div>
</Panel>
)}
{screen === 'payments' && (
<Panel
title="Pagamento externo · exemplo fictício"
description="Sem registos reais, contas bancárias ou execução de transferências."
>
<div className="space-y-3 text-sm">
<p>
Factura fictícia de 1.000,00 MZN. Um
pagamento externo de 600,00 MZN é
registado com referência e evidência.
</p>
<p>
Enquanto pendente de conferência, o
saldo continua em 1.000,00 MZN. Após
confirmação por outra pessoa de
Finanças, fica em 400,00 MZN.
</p>
<p>
A versão bancária e as duas validações
são novamente verificadas. Repetir a
confirmação não duplica a liquidação; o
orçamento não é consumido outra vez.
</p>
<p>
Os valores são apenas exemplos. Cada
empresa tem as suas facturas e
montantes; o SGC não efectua a operação
no banco.
</p>
</div>
</Panel>
)}
{screen === 'bank' && (
<Panel
title="Dupla validação bancária · exemplo fictício"
description="Sem dados de contas, consultas ou gravações de negócio."
>
<div className="space-y-3 text-sm">
<p>
Procurement propõe uma versão com
evidência. Finanças A confere e aprova;
Finanças B confere de forma
independente.
</p>
<p>
Uma aprovação: pendente. Duas pessoas
distintas: duplamente validado. Repetir
a decisão da mesma pessoa não conta duas
vezes.
</p>
<p>
Nova versão: disponibilidade anterior
substituída e duas novas validações
obrigatórias. Rejeição, conflito ou
perda de elegibilidade bloqueiam o
estado.
</p>
<p>
Cada empresa mantém os seus fornecedores
e dados. Não autoriza transferências nem
confirma titularidade junto do banco.
</p>
</div>
</Panel>
)}
{screen === 'invoices' && (
<Panel
title="Factura parcial · exemplo fictício"
description="Sem consultas ou gravações de negócio. Os valores são apenas exemplos; cada empresa define as suas dotações e base de IVA."
>
<p className="mb-4 text-sm">
FT-DEMO-01 · Fornecedor fictício ·
OC-DEMO-01 · Validada por uma segunda pessoa
de Finanças.
</p>
<dl className="grid gap-3 text-sm sm:grid-cols-2">
<div>
<dt>Quantidade autorizada e aceite</dt>
<dd>10 unidades</dd>
</div>
<div>
<dt>Quantidade facturada</dt>
<dd>6 unidades</dd>
</div>
<div>
<dt>Valor desta factura</dt>
<dd>600,00 MZN (exemplo sem IVA)</dd>
</div>
<div>
<dt>
Compromisso antes da conferência
</dt>
<dd>1.000,00 MZN</dd>
</div>
<div>
<dt>Consumo após validação</dt>
<dd>600,00 MZN</dd>
</div>
<div>
<dt>Compromisso remanescente</dt>
<dd>400,00 MZN</dd>
</div>
</dl>
<p className="mt-4 text-sm">
O disponível não aumenta: o compromisso
converte-se em consumo. Preço, unidade, IVA
e quantidades são novamente conferidos no
servidor. Sem compromisso aprovado, a
factura permanece pendente. Não autoriza
pagamentos.
</p>
</Panel>
)}
{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>
</>
)}
</div>
</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>
</>
);
}