@@ -13,6 +13,7 @@ void createInertiaApp({
|
||||
layout: (name) => {
|
||||
switch (true) {
|
||||
case name === 'welcome':
|
||||
case name.startsWith('preview/'):
|
||||
return null;
|
||||
case name.startsWith('auth/'):
|
||||
return AuthLayout;
|
||||
|
||||
@@ -21,6 +21,12 @@ export default function Login({ status, canResetPassword }: Props) {
|
||||
<>
|
||||
<Head title="Entrar" />
|
||||
|
||||
<Button asChild variant="outline" className="mb-5 w-full">
|
||||
<TextLink href="/consulta">
|
||||
Consultar demonstração sem entrar
|
||||
</TextLink>
|
||||
</Button>
|
||||
|
||||
<Form
|
||||
{...store.form()}
|
||||
resetOnSuccess={['password']}
|
||||
|
||||
@@ -0,0 +1,748 @@
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
ArrowRight,
|
||||
Building2,
|
||||
ClipboardList,
|
||||
Eye,
|
||||
FileText,
|
||||
LayoutGrid,
|
||||
LockKeyhole,
|
||||
Menu,
|
||||
Palette,
|
||||
Scale,
|
||||
Search,
|
||||
ShieldCheck,
|
||||
Users,
|
||||
} 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';
|
||||
|
||||
// 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: 'appearance', title: 'Cores e componentes', icon: Palette },
|
||||
];
|
||||
type Detail = { title: string; subtitle: string; rows: [string, string][] };
|
||||
export default function Preview() {
|
||||
const [screen, setScreen] = useState('overview');
|
||||
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) {
|
||||
setScreen(id);
|
||||
setSearch('');
|
||||
setMenu(false);
|
||||
}
|
||||
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="/consulta"
|
||||
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">
|
||||
PF
|
||||
</span>
|
||||
ProcuraFlow
|
||||
</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 className="space-y-2">
|
||||
{navigation.map((n) => (
|
||||
<button
|
||||
type="button"
|
||||
key={n.id}
|
||||
onClick={() => navigate(n.id)}
|
||||
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}
|
||||
</button>
|
||||
))}
|
||||
</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="ProcuraFlow · 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 === '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">
|
||||
ProcuraFlow · 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Head, Link, useForm } from '@inertiajs/react';
|
||||
import { FormErrors, PageTitle, Panel, controlClass } from '@/components/procurement';
|
||||
import {
|
||||
FormErrors,
|
||||
PageTitle,
|
||||
Panel,
|
||||
controlClass,
|
||||
} from '@/components/procurement';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -7,20 +12,384 @@ import { money, roleLabels, date } from '@/types/procurement';
|
||||
import type { MatrixRow, PurchaseAward } from '@/types/purchase';
|
||||
|
||||
type Props = {
|
||||
requisition: { id: number; number: string }; comparison: { rows: MatrixRow[]; ready: boolean; top_ids: number[] };
|
||||
award: PurchaseAward | null; canSelect: boolean; canDecide: boolean; canIssue: boolean; order: { id: number; number: string } | null;
|
||||
requisition: { id: number; number: string };
|
||||
comparison: { rows: MatrixRow[]; ready: boolean; top_ids: number[] };
|
||||
award: PurchaseAward | null;
|
||||
canSelect: boolean;
|
||||
canDecide: boolean;
|
||||
canIssue: boolean;
|
||||
order: { id: number; number: string } | null;
|
||||
};
|
||||
export default function Award({ requisition, comparison, award, canSelect, canDecide, canIssue, order }: Props) {
|
||||
const selection = useForm({ quotation_id: '', justification: '', delivery_date: '', delivery_location: '' });
|
||||
export default function Award({
|
||||
requisition,
|
||||
comparison,
|
||||
award,
|
||||
canSelect,
|
||||
canDecide,
|
||||
canIssue,
|
||||
order,
|
||||
}: Props) {
|
||||
const selection = useForm({
|
||||
quotation_id: '',
|
||||
justification: '',
|
||||
delivery_date: '',
|
||||
delivery_location: '',
|
||||
});
|
||||
const decision = useForm({ decision: 'approved', comment: '' });
|
||||
const issue = useForm({});
|
||||
return <><Head title={`Adjudicação · ${requisition.number}`} /><div className="mx-auto flex w-full max-w-6xl flex-col gap-7 p-5 lg:p-8"><PageTitle eyebrow={requisition.number} title="Adjudicação e emissão" description="Escolha fundamentada da proposta de maior pontuação, seguida de aprovação explícita do valor final." action={<Button asChild variant="outline"><Link href={`/requisicoes/${requisition.id}/cotacoes`}>Consultar cotações</Link></Button>} />
|
||||
<div className="rounded-xl border border-amber-200 bg-amber-50 p-5 text-sm text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-100">Ambiente de desenvolvimento: os documentos não autorizam compras reais. O orçamento ainda não é verificado. Excepções às três propostas ou à maior pontuação permanecem bloqueadas enquanto não existir política aprovada.</div>
|
||||
<Panel title="Comparação actual" description={comparison.ready ? 'Matriz completa para análise. O servidor volta a verificar tudo em cada operação.' : 'A matriz ainda não reúne três propostas elegíveis, cada uma com duas avaliações elegíveis.'}><div className="overflow-x-auto"><table className="w-full text-left text-sm"><thead className="border-b text-muted-foreground"><tr><th className="pb-3 pr-4">Fornecedor</th><th className="pr-4">Total com IVA</th><th className="pr-4">Avaliações elegíveis</th><th>Pontuação</th></tr></thead><tbody>{comparison.rows.map(q => <tr key={q.id} className="border-b last:border-0"><td className="py-4 pr-4"><p className="font-medium">{q.supplier?.name} · {q.reference}</p>{q.ineligibility_reasons.map(reason => <p className="mt-1 text-xs text-destructive" key={reason}>{reason}</p>)}</td><td className="pr-4 whitespace-nowrap">{money(q.total_minor)}</td><td className="pr-4">{q.evaluation_count} / 2</td><td>{q.score === null ? 'Pendente' : `${q.score.toFixed(2)} / 10`}{comparison.top_ids.includes(q.id) && <p className="text-xs font-medium text-teal-700 dark:text-teal-400">Maior pontuação</p>}</td></tr>)}</tbody></table></div></Panel>
|
||||
{canSelect && <Panel title="Seleccionar proposta" description="Empates permitem escolha fundamentada entre as propostas no topo. A selecção fecha novas propostas e avaliações."><form className="space-y-5" onSubmit={e => { e.preventDefault(); selection.post(`/requisicoes/${requisition.id}/adjudicacao`); }}><FormErrors errors={selection.errors} /><div className="space-y-2"><Label htmlFor="award-quote">Proposta</Label><select id="award-quote" className={controlClass} required value={selection.data.quotation_id} onChange={e => selection.setData('quotation_id', e.target.value)}><option value="">Seleccione uma proposta no topo</option>{comparison.rows.filter(q => comparison.top_ids.includes(q.id)).map(q => <option key={q.id} value={q.id}>{q.supplier.name} · {money(q.total_minor)} · {q.score?.toFixed(2)} / 10</option>)}</select></div><div className="space-y-2"><Label htmlFor="award-justification">Justificação da adjudicação</Label><textarea id="award-justification" className={`${controlClass} h-24 py-3`} required minLength={10} maxLength={5000} value={selection.data.justification} onChange={e => selection.setData('justification', e.target.value)} /></div><div className="grid gap-4 sm:grid-cols-2"><div className="space-y-2"><Label htmlFor="award-date">Data acordada de entrega</Label><Input id="award-date" type="date" required value={selection.data.delivery_date} onChange={e => selection.setData('delivery_date', e.target.value)} /></div><div className="space-y-2"><Label htmlFor="award-location">Local de entrega</Label><Input id="award-location" required maxLength={500} value={selection.data.delivery_location} onChange={e => selection.setData('delivery_location', e.target.value)} /></div></div><p className="text-sm text-muted-foreground">Será aberto um novo ciclo de aprovações pelo valor adjudicado. As decisões da estimativa ficam preservadas e não são reutilizadas. Solicitante e autor da adjudicação não aprovam este ciclo.</p><Button disabled={selection.processing || !comparison.ready}>Registar adjudicação para aprovação</Button></form></Panel>}
|
||||
{award && <Panel title={`Adjudicação #${award.id}`} description={`Estado: ${award.status === 'pending' ? 'Em aprovação' : award.status === 'approved' ? 'Aprovada para emissão' : 'Rejeitada'}`}><p className="text-sm whitespace-pre-wrap">{award.justification}</p><dl className="mt-5 grid gap-4 text-sm sm:grid-cols-3"><div><dt className="text-muted-foreground">Total final com IVA</dt><dd className="mt-1 font-semibold">{money(award.total_minor)}</dd></div><div><dt className="text-muted-foreground">Base dos escalões guardados</dt><dd className="mt-1">{award.approval_snapshot.approval_includes_tax ? 'Com IVA' : 'Sem IVA'}{award.approval_snapshot.rules_provisional ? ' · Provisórios' : ''}</dd></div><div><dt className="text-muted-foreground">Entrega</dt><dd className="mt-1">{date(award.delivery_date)} · {award.delivery_location}</dd></div></dl><ol className="mt-6 space-y-3 border-t pt-5">{award.approvals.map(step => <li key={step.id} className="text-sm"><strong>{step.position}. {roleLabels[step.role]}</strong> · {step.decision === 'pending' ? 'Aguarda decisão' : step.decision === 'approved' ? 'Aprovada' : 'Rejeitada'}{step.approver_id ? ` · Utilizador #${step.approver_id}` : ''}{step.comment && <p className="mt-1 text-muted-foreground">{step.comment}</p>}</li>)}</ol></Panel>}
|
||||
{canDecide && award && <Panel title="Decidir sobre o valor final"><form className="space-y-4" onSubmit={e => { e.preventDefault(); decision.post(`/adjudicacoes/${award.id}/decidir`); }}><FormErrors errors={decision.errors} /><Label htmlFor="award-decision">Decisão</Label><select id="award-decision" className={controlClass} value={decision.data.decision} onChange={e => decision.setData('decision', e.target.value)}><option value="approved">Aprovar etapa</option><option value="rejected">Rejeitar adjudicação</option></select><Label htmlFor="award-comment">Fundamento (obrigatório se rejeitar)</Label><textarea id="award-comment" className={`${controlClass} h-24 py-3`} required={decision.data.decision === 'rejected'} maxLength={2000} value={decision.data.comment} onChange={e => decision.setData('comment', e.target.value)} /><Button disabled={decision.processing}>Registar decisão</Button></form></Panel>}
|
||||
{order ? <Button asChild><Link href={`/ordens/${order.id}`}>Abrir ordem {order.number}</Link></Button> : canIssue && award && <Panel title="Emitir ordem para revisão" description="O servidor revalida a matriz, os conflitos e as aprovações. A ordem emitida mantém um retrato dos dados e não pode ser reescrita."><FormErrors errors={issue.errors} /><Button className="mt-4" disabled={issue.processing} onClick={() => issue.post(`/adjudicacoes/${award.id}/emitir`)}>Emitir documento de desenvolvimento</Button></Panel>}
|
||||
</div></>;
|
||||
return (
|
||||
<>
|
||||
<Head title={`Adjudicação · ${requisition.number}`} />
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col gap-7 p-5 lg:p-8">
|
||||
<PageTitle
|
||||
eyebrow={requisition.number}
|
||||
title="Adjudicação e emissão"
|
||||
description="Escolha fundamentada da proposta de maior pontuação, seguida de aprovação explícita do valor final."
|
||||
action={
|
||||
<Button asChild variant="outline">
|
||||
<Link
|
||||
href={`/requisicoes/${requisition.id}/cotacoes`}
|
||||
>
|
||||
Consultar cotações
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<div className="rounded-xl border border-amber-200 bg-amber-50 p-5 text-sm text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-100">
|
||||
Ambiente de desenvolvimento: os documentos não autorizam
|
||||
compras reais. O orçamento ainda não é verificado. Excepções
|
||||
às três propostas ou à maior pontuação permanecem bloqueadas
|
||||
enquanto não existir política aprovada.
|
||||
</div>
|
||||
<Panel
|
||||
title="Comparação actual"
|
||||
description={
|
||||
comparison.ready
|
||||
? 'Matriz completa para análise. O servidor volta a verificar tudo em cada operação.'
|
||||
: 'A matriz ainda não reúne três propostas elegíveis, cada uma com duas avaliações elegíveis.'
|
||||
}
|
||||
>
|
||||
<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">Fornecedor</th>
|
||||
<th className="pr-4">Total com IVA</th>
|
||||
<th className="pr-4">
|
||||
Avaliações elegíveis
|
||||
</th>
|
||||
<th>Pontuação</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{comparison.rows.map((q) => (
|
||||
<tr
|
||||
key={q.id}
|
||||
className="border-b last:border-0"
|
||||
>
|
||||
<td className="py-4 pr-4">
|
||||
<p className="font-medium">
|
||||
{q.supplier?.name} ·{' '}
|
||||
{q.reference}
|
||||
</p>
|
||||
{q.ineligibility_reasons.map(
|
||||
(reason) => (
|
||||
<p
|
||||
className="mt-1 text-xs text-destructive"
|
||||
key={reason}
|
||||
>
|
||||
{reason}
|
||||
</p>
|
||||
),
|
||||
)}
|
||||
</td>
|
||||
<td className="pr-4 whitespace-nowrap">
|
||||
{money(q.total_minor)}
|
||||
</td>
|
||||
<td className="pr-4">
|
||||
{q.evaluation_count} / 2
|
||||
</td>
|
||||
<td>
|
||||
{q.score === null
|
||||
? 'Pendente'
|
||||
: `${q.score.toFixed(2)} / 10`}
|
||||
{comparison.top_ids.includes(
|
||||
q.id,
|
||||
) && (
|
||||
<p className="text-xs font-medium text-teal-700 dark:text-teal-400">
|
||||
Maior pontuação
|
||||
</p>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Panel>
|
||||
{canSelect && (
|
||||
<Panel
|
||||
title="Seleccionar proposta"
|
||||
description="Empates permitem escolha fundamentada entre as propostas no topo. A selecção fecha novas propostas e avaliações."
|
||||
>
|
||||
<form
|
||||
className="space-y-5"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
selection.post(
|
||||
`/requisicoes/${requisition.id}/adjudicacao`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FormErrors errors={selection.errors} />
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="award-quote">Proposta</Label>
|
||||
<select
|
||||
id="award-quote"
|
||||
className={controlClass}
|
||||
required
|
||||
value={selection.data.quotation_id}
|
||||
onChange={(e) =>
|
||||
selection.setData(
|
||||
'quotation_id',
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="">
|
||||
Seleccione uma proposta no topo
|
||||
</option>
|
||||
{comparison.rows
|
||||
.filter((q) =>
|
||||
comparison.top_ids.includes(q.id),
|
||||
)
|
||||
.map((q) => (
|
||||
<option key={q.id} value={q.id}>
|
||||
{q.supplier.name} ·{' '}
|
||||
{money(q.total_minor)} ·{' '}
|
||||
{q.score?.toFixed(2)} / 10
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="award-justification">
|
||||
Justificação da adjudicação
|
||||
</Label>
|
||||
<textarea
|
||||
id="award-justification"
|
||||
className={`${controlClass} h-24 py-3`}
|
||||
required
|
||||
minLength={10}
|
||||
maxLength={5000}
|
||||
value={selection.data.justification}
|
||||
onChange={(e) =>
|
||||
selection.setData(
|
||||
'justification',
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="award-date">
|
||||
Data acordada de entrega
|
||||
</Label>
|
||||
<Input
|
||||
id="award-date"
|
||||
type="date"
|
||||
required
|
||||
value={selection.data.delivery_date}
|
||||
onChange={(e) =>
|
||||
selection.setData(
|
||||
'delivery_date',
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="award-location">
|
||||
Local de entrega
|
||||
</Label>
|
||||
<Input
|
||||
id="award-location"
|
||||
required
|
||||
maxLength={500}
|
||||
value={selection.data.delivery_location}
|
||||
onChange={(e) =>
|
||||
selection.setData(
|
||||
'delivery_location',
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Será aberto um novo ciclo de aprovações pelo
|
||||
valor adjudicado. As decisões da estimativa
|
||||
ficam preservadas e não são reutilizadas.
|
||||
Solicitante e autor da adjudicação não aprovam
|
||||
este ciclo.
|
||||
</p>
|
||||
<Button
|
||||
disabled={
|
||||
selection.processing || !comparison.ready
|
||||
}
|
||||
>
|
||||
Registar adjudicação para aprovação
|
||||
</Button>
|
||||
</form>
|
||||
</Panel>
|
||||
)}
|
||||
{award && (
|
||||
<Panel
|
||||
title={`Adjudicação #${award.id}`}
|
||||
description={`Estado: ${award.status === 'pending' ? 'Em aprovação' : award.status === 'approved' ? 'Aprovada para emissão' : 'Rejeitada'}`}
|
||||
>
|
||||
<p className="text-sm whitespace-pre-wrap">
|
||||
{award.justification}
|
||||
</p>
|
||||
<dl className="mt-5 grid gap-4 text-sm sm:grid-cols-3">
|
||||
<div>
|
||||
<dt className="text-muted-foreground">
|
||||
Total final com IVA
|
||||
</dt>
|
||||
<dd className="mt-1 font-semibold">
|
||||
{money(award.total_minor)}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground">
|
||||
Base dos escalões guardados
|
||||
</dt>
|
||||
<dd className="mt-1">
|
||||
{award.approval_snapshot
|
||||
.approval_includes_tax
|
||||
? 'Com IVA'
|
||||
: 'Sem IVA'}
|
||||
{award.approval_snapshot.rules_provisional
|
||||
? ' · Provisórios'
|
||||
: ''}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground">
|
||||
Entrega
|
||||
</dt>
|
||||
<dd className="mt-1">
|
||||
{date(award.delivery_date)} ·{' '}
|
||||
{award.delivery_location}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<ol className="mt-6 space-y-3 border-t pt-5">
|
||||
{award.approvals.map((step) => (
|
||||
<li key={step.id} className="text-sm">
|
||||
<strong>
|
||||
{step.position}. {roleLabels[step.role]}
|
||||
</strong>{' '}
|
||||
·{' '}
|
||||
{step.decision === 'pending'
|
||||
? 'Aguarda decisão'
|
||||
: step.decision === 'approved'
|
||||
? 'Aprovada'
|
||||
: 'Rejeitada'}
|
||||
{step.approver_id
|
||||
? ` · Utilizador #${step.approver_id}`
|
||||
: ''}
|
||||
{step.comment && (
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
{step.comment}
|
||||
</p>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</Panel>
|
||||
)}
|
||||
{canDecide && award && (
|
||||
<Panel title="Decidir sobre o valor final">
|
||||
<form
|
||||
className="space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
decision.post(
|
||||
`/adjudicacoes/${award.id}/decidir`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FormErrors errors={decision.errors} />
|
||||
<Label htmlFor="award-decision">Decisão</Label>
|
||||
<select
|
||||
id="award-decision"
|
||||
className={controlClass}
|
||||
value={decision.data.decision}
|
||||
onChange={(e) =>
|
||||
decision.setData('decision', e.target.value)
|
||||
}
|
||||
>
|
||||
<option value="approved">Aprovar etapa</option>
|
||||
<option value="rejected">
|
||||
Rejeitar adjudicação
|
||||
</option>
|
||||
</select>
|
||||
<Label htmlFor="award-comment">
|
||||
Fundamento (obrigatório se rejeitar)
|
||||
</Label>
|
||||
<textarea
|
||||
id="award-comment"
|
||||
className={`${controlClass} h-24 py-3`}
|
||||
required={decision.data.decision === 'rejected'}
|
||||
maxLength={2000}
|
||||
value={decision.data.comment}
|
||||
onChange={(e) =>
|
||||
decision.setData('comment', e.target.value)
|
||||
}
|
||||
/>
|
||||
<Button disabled={decision.processing}>
|
||||
Registar decisão
|
||||
</Button>
|
||||
</form>
|
||||
</Panel>
|
||||
)}
|
||||
{order ? (
|
||||
<Button asChild>
|
||||
<Link href={`/ordens/${order.id}`}>
|
||||
Abrir ordem {order.number}
|
||||
</Link>
|
||||
</Button>
|
||||
) : (
|
||||
canIssue &&
|
||||
award && (
|
||||
<Panel
|
||||
title="Emitir ordem para revisão"
|
||||
description="O servidor revalida a matriz, os conflitos e as aprovações. A ordem emitida mantém um retrato dos dados e não pode ser reescrita."
|
||||
>
|
||||
<FormErrors errors={issue.errors} />
|
||||
<Button
|
||||
className="mt-4"
|
||||
disabled={issue.processing}
|
||||
onClick={() =>
|
||||
issue.post(
|
||||
`/adjudicacoes/${award.id}/emitir`,
|
||||
)
|
||||
}
|
||||
>
|
||||
Emitir documento de desenvolvimento
|
||||
</Button>
|
||||
</Panel>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Award.layout = { breadcrumbs: [{ title: 'Ordens de compra', href: '/ordens' }, { title: 'Adjudicação', href: '#' }] };
|
||||
Award.layout = {
|
||||
breadcrumbs: [
|
||||
{ title: 'Ordens de compra', href: '/ordens' },
|
||||
{ title: 'Adjudicação', href: '#' },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -3,8 +3,125 @@ import { PageTitle, Panel } from '@/components/procurement';
|
||||
import { money, date } from '@/types/procurement';
|
||||
import type { PurchaseAward, PurchaseOrder } from '@/types/purchase';
|
||||
|
||||
type Page<T> = { data: T[]; prev_page_url: string | null; next_page_url: string | null };
|
||||
export default function Orders({ orders, awards }: { orders: Page<PurchaseOrder>; awards: Page<PurchaseAward> }) {
|
||||
return <><Head title="Ordens de compra" /><div className="mx-auto flex w-full max-w-7xl flex-col gap-7 p-5 lg:p-8"><PageTitle eyebrow="Compras" title="Adjudicações e ordens" description="Acompanhe as aprovações dos valores finais e os documentos emitidos para revisão." /><Panel title="Ordens emitidas"><div className="overflow-x-auto"><table className="w-full text-left text-sm"><thead className="border-b text-muted-foreground"><tr><th className="pb-3 pr-4">Ordem</th><th className="pr-4">Fornecedor</th><th className="pr-4">Emissão</th><th>Total com IVA</th></tr></thead><tbody>{orders.data.map(order => <tr key={order.id} className="border-b last:border-0"><td className="py-4 pr-4"><Link href={`/ordens/${order.id}`} className="font-medium text-teal-700 dark:text-teal-400">{order.number}</Link><p className="mt-1 text-xs text-muted-foreground">{order.snapshot.requisition.number}</p></td><td className="pr-4">{order.snapshot.supplier.name}</td><td className="pr-4">{date(order.issued_at)}</td><td>{money(order.total_minor)}</td></tr>)}</tbody></table></div>{orders.data.length === 0 && <p className="py-6 text-sm text-muted-foreground">Ainda não foram emitidas ordens.</p>}<div className="mt-4 flex gap-4 text-sm">{orders.prev_page_url && <Link href={orders.prev_page_url}>Anterior</Link>}{orders.next_page_url && <Link href={orders.next_page_url}>Seguinte</Link>}</div></Panel><Panel title="Adjudicações"><div className="divide-y">{awards.data.map(award => <Link href={`/requisicoes/${award.requisition_id}/adjudicacao`} key={award.id} className="flex items-center justify-between gap-4 py-4 text-sm"><div><strong>Adjudicação #{award.id}</strong><p className="mt-1 text-muted-foreground">{award.status === 'pending' ? 'Em aprovação' : award.status === 'approved' ? 'Aprovada para emissão' : 'Rejeitada'}</p></div><span>{money(award.total_minor)}</span></Link>)}{awards.data.length === 0 && <p className="py-6 text-sm text-muted-foreground">Inicie a adjudicação a partir da comparação de cotações.</p>}</div><div className="mt-4 flex gap-4 text-sm">{awards.prev_page_url && <Link href={awards.prev_page_url}>Anterior</Link>}{awards.next_page_url && <Link href={awards.next_page_url}>Seguinte</Link>}</div></Panel></div></>;
|
||||
type Page<T> = {
|
||||
data: T[];
|
||||
prev_page_url: string | null;
|
||||
next_page_url: string | null;
|
||||
};
|
||||
export default function Orders({
|
||||
orders,
|
||||
awards,
|
||||
}: {
|
||||
orders: Page<PurchaseOrder>;
|
||||
awards: Page<PurchaseAward>;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Head title="Ordens de compra" />
|
||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-7 p-5 lg:p-8">
|
||||
<PageTitle
|
||||
eyebrow="Compras"
|
||||
title="Adjudicações e ordens"
|
||||
description="Acompanhe as aprovações dos valores finais e os documentos emitidos para revisão."
|
||||
/>
|
||||
<Panel title="Ordens emitidas">
|
||||
<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">Ordem</th>
|
||||
<th className="pr-4">Fornecedor</th>
|
||||
<th className="pr-4">Emissão</th>
|
||||
<th>Total com IVA</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{orders.data.map((order) => (
|
||||
<tr
|
||||
key={order.id}
|
||||
className="border-b last:border-0"
|
||||
>
|
||||
<td className="py-4 pr-4">
|
||||
<Link
|
||||
href={`/ordens/${order.id}`}
|
||||
className="font-medium text-teal-700 dark:text-teal-400"
|
||||
>
|
||||
{order.number}
|
||||
</Link>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{
|
||||
order.snapshot.requisition
|
||||
.number
|
||||
}
|
||||
</p>
|
||||
</td>
|
||||
<td className="pr-4">
|
||||
{order.snapshot.supplier.name}
|
||||
</td>
|
||||
<td className="pr-4">
|
||||
{date(order.issued_at)}
|
||||
</td>
|
||||
<td>{money(order.total_minor)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{orders.data.length === 0 && (
|
||||
<p className="py-6 text-sm text-muted-foreground">
|
||||
Ainda não foram emitidas ordens.
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-4 flex gap-4 text-sm">
|
||||
{orders.prev_page_url && (
|
||||
<Link href={orders.prev_page_url}>Anterior</Link>
|
||||
)}
|
||||
{orders.next_page_url && (
|
||||
<Link href={orders.next_page_url}>Seguinte</Link>
|
||||
)}
|
||||
</div>
|
||||
</Panel>
|
||||
<Panel title="Adjudicações">
|
||||
<div className="divide-y">
|
||||
{awards.data.map((award) => (
|
||||
<Link
|
||||
href={`/requisicoes/${award.requisition_id}/adjudicacao`}
|
||||
key={award.id}
|
||||
className="flex items-center justify-between gap-4 py-4 text-sm"
|
||||
>
|
||||
<div>
|
||||
<strong>Adjudicação #{award.id}</strong>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
{award.status === 'pending'
|
||||
? 'Em aprovação'
|
||||
: award.status === 'approved'
|
||||
? 'Aprovada para emissão'
|
||||
: 'Rejeitada'}
|
||||
</p>
|
||||
</div>
|
||||
<span>{money(award.total_minor)}</span>
|
||||
</Link>
|
||||
))}
|
||||
{awards.data.length === 0 && (
|
||||
<p className="py-6 text-sm text-muted-foreground">
|
||||
Inicie a adjudicação a partir da comparação de
|
||||
cotações.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 flex gap-4 text-sm">
|
||||
{awards.prev_page_url && (
|
||||
<Link href={awards.prev_page_url}>Anterior</Link>
|
||||
)}
|
||||
{awards.next_page_url && (
|
||||
<Link href={awards.next_page_url}>Seguinte</Link>
|
||||
)}
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Orders.layout = { breadcrumbs: [{ title: 'Ordens de compra', href: '/ordens' }] };
|
||||
Orders.layout = {
|
||||
breadcrumbs: [{ title: 'Ordens de compra', href: '/ordens' }],
|
||||
};
|
||||
|
||||
@@ -5,15 +5,260 @@ import { Button } from '@/components/ui/button';
|
||||
import { date, money, roleLabels } from '@/types/procurement';
|
||||
import type { PurchaseOrder } from '@/types/purchase';
|
||||
|
||||
type History = { id: number; action: string; user_id: number; created_at: string };
|
||||
const actions: Record<string, string> = { 'award.created': 'Adjudicação registada', 'award.approved': 'Etapa do valor final aprovada', 'award.rejected': 'Adjudicação rejeitada', 'purchase_order.issued': 'Ordem emitida para revisão' };
|
||||
export default function OrderShow({ order, history }: { order: PurchaseOrder; history: History[] }) {
|
||||
type History = {
|
||||
id: number;
|
||||
action: string;
|
||||
user_id: number;
|
||||
created_at: string;
|
||||
};
|
||||
const actions: Record<string, string> = {
|
||||
'award.created': 'Adjudicação registada',
|
||||
'award.approved': 'Etapa do valor final aprovada',
|
||||
'award.rejected': 'Adjudicação rejeitada',
|
||||
'purchase_order.issued': 'Ordem emitida para revisão',
|
||||
};
|
||||
export default function OrderShow({
|
||||
order,
|
||||
history,
|
||||
}: {
|
||||
order: PurchaseOrder;
|
||||
history: History[];
|
||||
}) {
|
||||
const snapshot = order.snapshot;
|
||||
return <><Head title={order.number} /><div className="mx-auto flex w-full max-w-6xl flex-col gap-7 p-5 lg:p-8"><PageTitle eyebrow={snapshot.company.name} title={order.number} description={`${snapshot.requisition.number} · Emitida em ${date(order.issued_at)} por ${snapshot.issuer.name}`} action={<Button asChild><a href={`/ordens/${order.id}/imprimir`} target="_blank" rel="noopener noreferrer"><Printer className="size-4" />Imprimir / guardar PDF</a></Button>} /><div className="rounded-xl border border-amber-200 bg-amber-50 p-5 text-sm text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-100"><strong>Documento de desenvolvimento. Não autoriza compras reais.</strong> Orçamento não verificado. Os dados apresentados foram congelados na emissão.{snapshot.approval_rules.rules_provisional && ' Regras de aprovação provisórias.'}</div>
|
||||
<div className="grid gap-6 md:grid-cols-2"><Panel title="Fornecedor"><dl className="space-y-3 text-sm"><div><dt className="text-muted-foreground">Nome e identificação fiscal</dt><dd>{snapshot.supplier.name} · NUIT {snapshot.supplier.nuit}</dd></div><div><dt className="text-muted-foreground">Contacto</dt><dd>{snapshot.supplier.contact} · {snapshot.supplier.phone}<br />{snapshot.supplier.email}</dd></div><div><dt className="text-muted-foreground">Morada</dt><dd className="whitespace-pre-wrap">{snapshot.supplier.address}</dd></div></dl></Panel><Panel title="Condições acordadas"><dl className="space-y-3 text-sm"><div><dt className="text-muted-foreground">Entrega</dt><dd>{date(snapshot.award.delivery_date)} · {snapshot.award.delivery_location}</dd></div><div><dt className="text-muted-foreground">Pagamento</dt><dd>{snapshot.quotation.payment_terms}</dd></div><div><dt className="text-muted-foreground">Departamento e centro de custo</dt><dd>{snapshot.department.name} · {snapshot.cost_center.code} · {snapshot.cost_center.name}</dd></div><div><dt className="text-muted-foreground">Proposta</dt><dd>{snapshot.quotation.reference} · {order.currency}</dd></div></dl></Panel></div>
|
||||
<Panel title="Artigos e valores congelados"><div className="overflow-x-auto"><table className="w-full text-left text-sm"><thead className="border-b text-muted-foreground"><tr><th className="pb-3 pr-4">Descrição</th><th className="pr-4">Quantidade</th><th className="pr-4">Preço sem IVA</th><th className="pr-4">IVA</th><th>Total com IVA</th></tr></thead><tbody>{order.items.map(item => <tr key={item.id} className="border-b"><td className="py-4 pr-4">{item.description}</td><td className="pr-4 whitespace-nowrap">{Number(item.quantity)} {item.unit}</td><td className="pr-4 whitespace-nowrap">{money(item.unit_price_minor)}</td><td className="pr-4">{Number(item.tax_rate)}%</td><td className="whitespace-nowrap">{money(item.total_minor)}</td></tr>)}</tbody></table></div><dl className="mt-5 ml-auto max-w-sm space-y-3 text-sm"><div className="flex justify-between gap-4"><dt>Subtotal sem IVA</dt><dd>{money(order.subtotal_minor)}</dd></div><div className="flex justify-between gap-4"><dt>IVA</dt><dd>{money(order.tax_minor)}</dd></div><div className="flex justify-between gap-4 border-t pt-3 font-semibold"><dt>Total com IVA</dt><dd>{money(order.total_minor)}</dd></div></dl></Panel>
|
||||
<Panel title="Fundamento e matriz congelada"><p className="text-sm whitespace-pre-wrap">{snapshot.award.justification}</p><div className="mt-5 space-y-4">{snapshot.decision_matrix.map(row => <div key={row.id} className="rounded-lg border p-4 text-sm"><p className="font-semibold">{row.supplier.name} · {row.reference} · {row.score === null ? 'Não elegível / incompleta' : `${row.score.toFixed(2)} / 10`}</p><p className="mt-1 text-muted-foreground">{money(row.total_minor)} com IVA · {row.evaluation_count} avaliações elegíveis</p>{row.evaluations.map(e => <p key={e.evaluator_id} className="mt-2 text-xs">Avaliador #{e.evaluator_id}: qualidade {e.quality}, histórico {e.history}, pagamento {e.payment}. {e.comment}</p>)}</div>)}</div></Panel>
|
||||
<Panel title="Aprovações do valor final"><ol className="space-y-3">{snapshot.final_approvals.map(step => <li key={step.id} className="text-sm">{roleLabels[step.role]} · Utilizador #{step.approver_id} · Aprovada{step.comment && <p className="mt-1 text-muted-foreground">{step.comment}</p>}</li>)}</ol><p className="mt-4 text-xs text-muted-foreground">Base: {snapshot.approval_rules.approval_includes_tax ? 'com' : 'sem'} IVA. As aprovações da estimativa foram preservadas separadamente.</p></Panel><Panel title="Histórico do processo"><ol className="space-y-3">{history.map(event => <li key={event.id} className="text-sm">{actions[event.action]} · {date(event.created_at)} · Utilizador #{event.user_id}</li>)}</ol></Panel><Link href="/ordens" className="text-sm text-teal-700 dark:text-teal-400">Voltar às ordens</Link>
|
||||
</div></>;
|
||||
return (
|
||||
<>
|
||||
<Head title={order.number} />
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col gap-7 p-5 lg:p-8">
|
||||
<PageTitle
|
||||
eyebrow={snapshot.company.name}
|
||||
title={order.number}
|
||||
description={`${snapshot.requisition.number} · Emitida em ${date(order.issued_at)} por ${snapshot.issuer.name}`}
|
||||
action={
|
||||
<Button asChild>
|
||||
<a
|
||||
href={`/ordens/${order.id}/imprimir`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Printer className="size-4" />
|
||||
Imprimir / guardar PDF
|
||||
</a>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<div className="rounded-xl border border-amber-200 bg-amber-50 p-5 text-sm text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-100">
|
||||
<strong>
|
||||
Documento de desenvolvimento. Não autoriza compras
|
||||
reais.
|
||||
</strong>{' '}
|
||||
Orçamento não verificado. Os dados apresentados foram
|
||||
congelados na emissão.
|
||||
{snapshot.approval_rules.rules_provisional &&
|
||||
' Regras de aprovação provisórias.'}
|
||||
</div>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<Panel title="Fornecedor">
|
||||
<dl className="space-y-3 text-sm">
|
||||
<div>
|
||||
<dt className="text-muted-foreground">
|
||||
Nome e identificação fiscal
|
||||
</dt>
|
||||
<dd>
|
||||
{snapshot.supplier.name} · NUIT{' '}
|
||||
{snapshot.supplier.nuit}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground">
|
||||
Contacto
|
||||
</dt>
|
||||
<dd>
|
||||
{snapshot.supplier.contact} ·{' '}
|
||||
{snapshot.supplier.phone}
|
||||
<br />
|
||||
{snapshot.supplier.email}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground">
|
||||
Morada
|
||||
</dt>
|
||||
<dd className="whitespace-pre-wrap">
|
||||
{snapshot.supplier.address}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Panel>
|
||||
<Panel title="Condições acordadas">
|
||||
<dl className="space-y-3 text-sm">
|
||||
<div>
|
||||
<dt className="text-muted-foreground">
|
||||
Entrega
|
||||
</dt>
|
||||
<dd>
|
||||
{date(snapshot.award.delivery_date)} ·{' '}
|
||||
{snapshot.award.delivery_location}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground">
|
||||
Pagamento
|
||||
</dt>
|
||||
<dd>{snapshot.quotation.payment_terms}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground">
|
||||
Departamento e centro de custo
|
||||
</dt>
|
||||
<dd>
|
||||
{snapshot.department.name} ·{' '}
|
||||
{snapshot.cost_center.code} ·{' '}
|
||||
{snapshot.cost_center.name}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground">
|
||||
Proposta
|
||||
</dt>
|
||||
<dd>
|
||||
{snapshot.quotation.reference} ·{' '}
|
||||
{order.currency}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Panel>
|
||||
</div>
|
||||
<Panel title="Artigos e valores congelados">
|
||||
<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">Descrição</th>
|
||||
<th className="pr-4">Quantidade</th>
|
||||
<th className="pr-4">Preço sem IVA</th>
|
||||
<th className="pr-4">IVA</th>
|
||||
<th>Total com IVA</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{order.items.map((item) => (
|
||||
<tr key={item.id} className="border-b">
|
||||
<td className="py-4 pr-4">
|
||||
{item.description}
|
||||
</td>
|
||||
<td className="pr-4 whitespace-nowrap">
|
||||
{Number(item.quantity)} {item.unit}
|
||||
</td>
|
||||
<td className="pr-4 whitespace-nowrap">
|
||||
{money(item.unit_price_minor)}
|
||||
</td>
|
||||
<td className="pr-4">
|
||||
{Number(item.tax_rate)}%
|
||||
</td>
|
||||
<td className="whitespace-nowrap">
|
||||
{money(item.total_minor)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<dl className="mt-5 ml-auto max-w-sm space-y-3 text-sm">
|
||||
<div className="flex justify-between gap-4">
|
||||
<dt>Subtotal sem IVA</dt>
|
||||
<dd>{money(order.subtotal_minor)}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between gap-4">
|
||||
<dt>IVA</dt>
|
||||
<dd>{money(order.tax_minor)}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between gap-4 border-t pt-3 font-semibold">
|
||||
<dt>Total com IVA</dt>
|
||||
<dd>{money(order.total_minor)}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Panel>
|
||||
<Panel title="Fundamento e matriz congelada">
|
||||
<p className="text-sm whitespace-pre-wrap">
|
||||
{snapshot.award.justification}
|
||||
</p>
|
||||
<div className="mt-5 space-y-4">
|
||||
{snapshot.decision_matrix.map((row) => (
|
||||
<div
|
||||
key={row.id}
|
||||
className="rounded-lg border p-4 text-sm"
|
||||
>
|
||||
<p className="font-semibold">
|
||||
{row.supplier.name} · {row.reference} ·{' '}
|
||||
{row.score === null
|
||||
? 'Não elegível / incompleta'
|
||||
: `${row.score.toFixed(2)} / 10`}
|
||||
</p>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
{money(row.total_minor)} com IVA ·{' '}
|
||||
{row.evaluation_count} avaliações elegíveis
|
||||
</p>
|
||||
{row.evaluations.map((e) => (
|
||||
<p
|
||||
key={e.evaluator_id}
|
||||
className="mt-2 text-xs"
|
||||
>
|
||||
Avaliador #{e.evaluator_id}: qualidade{' '}
|
||||
{e.quality}, histórico {e.history},
|
||||
pagamento {e.payment}. {e.comment}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Panel>
|
||||
<Panel title="Aprovações do valor final">
|
||||
<ol className="space-y-3">
|
||||
{snapshot.final_approvals.map((step) => (
|
||||
<li key={step.id} className="text-sm">
|
||||
{roleLabels[step.role]} · Utilizador #
|
||||
{step.approver_id} · Aprovada
|
||||
{step.comment && (
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
{step.comment}
|
||||
</p>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<p className="mt-4 text-xs text-muted-foreground">
|
||||
Base:{' '}
|
||||
{snapshot.approval_rules.approval_includes_tax
|
||||
? 'com'
|
||||
: 'sem'}{' '}
|
||||
IVA. As aprovações da estimativa foram preservadas
|
||||
separadamente.
|
||||
</p>
|
||||
</Panel>
|
||||
<Panel title="Histórico do processo">
|
||||
<ol className="space-y-3">
|
||||
{history.map((event) => (
|
||||
<li key={event.id} className="text-sm">
|
||||
{actions[event.action]} ·{' '}
|
||||
{date(event.created_at)} · Utilizador #
|
||||
{event.user_id}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</Panel>
|
||||
<Link
|
||||
href="/ordens"
|
||||
className="text-sm text-teal-700 dark:text-teal-400"
|
||||
>
|
||||
Voltar às ordens
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
OrderShow.layout = { breadcrumbs: [{ title: 'Ordens de compra', href: '/ordens' }, { title: 'Documento emitido', href: '#' }] };
|
||||
OrderShow.layout = {
|
||||
breadcrumbs: [
|
||||
{ title: 'Ordens de compra', href: '/ordens' },
|
||||
{ title: 'Documento emitido', href: '#' },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -38,6 +38,7 @@ type Quote = {
|
||||
valid_until: string;
|
||||
payment_terms: string;
|
||||
evaluations: Evaluation[];
|
||||
evaluation_count: number;
|
||||
eligible: boolean;
|
||||
conflict_blocked: boolean;
|
||||
score: number | null;
|
||||
@@ -103,7 +104,12 @@ export default function Comparison({
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
<Link className="text-sm font-medium text-teal-700 dark:text-teal-400" href={`/requisicoes/${requisition.id}/adjudicacao`}>Adjudicação, aprovações do valor final e ordem de compra</Link>
|
||||
<Link
|
||||
className="text-sm font-medium text-teal-700 dark:text-teal-400"
|
||||
href={`/requisicoes/${requisition.id}/adjudicacao`}
|
||||
>
|
||||
Adjudicação, aprovações do valor final e ordem de compra
|
||||
</Link>
|
||||
<Link
|
||||
className="text-sm text-teal-700"
|
||||
href={`/requisicoes/${requisition.id}`}
|
||||
|
||||
@@ -1,25 +1,96 @@
|
||||
export type MatrixRow = {
|
||||
id: number; reference: string; supplier: { name: string }; supplier_id: number;
|
||||
eligible: boolean; ineligibility_reasons: string[]; evaluation_count: number;
|
||||
score: number | null; score_minor: number | null; subtotal_minor: number; tax_minor: number; total_minor: number;
|
||||
evaluations: { evaluator_id: number; quality: string; history: string; payment: string; comment: string }[];
|
||||
id: number;
|
||||
reference: string;
|
||||
supplier: { name: string };
|
||||
supplier_id: number;
|
||||
eligible: boolean;
|
||||
ineligibility_reasons: string[];
|
||||
evaluation_count: number;
|
||||
score: number | null;
|
||||
score_minor: number | null;
|
||||
subtotal_minor: number;
|
||||
tax_minor: number;
|
||||
total_minor: number;
|
||||
evaluations: {
|
||||
evaluator_id: number;
|
||||
quality: string;
|
||||
history: string;
|
||||
payment: string;
|
||||
comment: string;
|
||||
}[];
|
||||
};
|
||||
export type AwardApproval = {
|
||||
id: number;
|
||||
position: number;
|
||||
role: string;
|
||||
decision: string;
|
||||
approver_id: number | null;
|
||||
comment: string | null;
|
||||
};
|
||||
export type AwardApproval = { id: number; position: number; role: string; decision: string; approver_id: number | null; comment: string | null };
|
||||
export type PurchaseAward = {
|
||||
id: number; requisition_id: number; quotation_id: number; status: string; justification: string;
|
||||
subtotal_minor: number; tax_minor: number; total_minor: number; delivery_date: string; delivery_location: string;
|
||||
approval_snapshot: { approval_includes_tax: boolean; rules_provisional: boolean }; approvals: AwardApproval[];
|
||||
id: number;
|
||||
requisition_id: number;
|
||||
quotation_id: number;
|
||||
status: string;
|
||||
justification: string;
|
||||
subtotal_minor: number;
|
||||
tax_minor: number;
|
||||
total_minor: number;
|
||||
delivery_date: string;
|
||||
delivery_location: string;
|
||||
approval_snapshot: {
|
||||
approval_includes_tax: boolean;
|
||||
rules_provisional: boolean;
|
||||
};
|
||||
approvals: AwardApproval[];
|
||||
};
|
||||
export type OrderItem = {
|
||||
id: number;
|
||||
description: string;
|
||||
quantity: string;
|
||||
unit: string;
|
||||
unit_price_minor: number;
|
||||
tax_rate: string;
|
||||
subtotal_minor: number;
|
||||
tax_minor: number;
|
||||
total_minor: number;
|
||||
};
|
||||
export type OrderItem = { id: number; description: string; quantity: string; unit: string; unit_price_minor: number; tax_rate: string; subtotal_minor: number; tax_minor: number; total_minor: number };
|
||||
export type PurchaseOrder = {
|
||||
id: number; number: string; requisition_id: number; issued_at: string; currency: string;
|
||||
subtotal_minor: number; tax_minor: number; total_minor: number; items: OrderItem[];
|
||||
id: number;
|
||||
number: string;
|
||||
requisition_id: number;
|
||||
issued_at: string;
|
||||
currency: string;
|
||||
subtotal_minor: number;
|
||||
tax_minor: number;
|
||||
total_minor: number;
|
||||
items: OrderItem[];
|
||||
snapshot: {
|
||||
company: { name: string }; department: { name: string }; cost_center: { code: string; name: string };
|
||||
requisition: { number: string }; issuer: { name: string }; requester: { name: string };
|
||||
supplier: { name: string; nuit: string; contact: string; phone: string; email: string; address: string };
|
||||
quotation: { reference: string; payment_terms: string }; award: { justification: string; delivery_date: string; delivery_location: string };
|
||||
approval_rules: { approval_includes_tax: boolean; rules_provisional: boolean };
|
||||
decision_matrix: MatrixRow[]; final_approvals: AwardApproval[];
|
||||
company: { name: string };
|
||||
department: { name: string };
|
||||
cost_center: { code: string; name: string };
|
||||
requisition: { number: string };
|
||||
issuer: { name: string };
|
||||
requester: { name: string };
|
||||
supplier: {
|
||||
name: string;
|
||||
nuit: string;
|
||||
contact: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
address: string;
|
||||
};
|
||||
quotation: { reference: string; payment_terms: string };
|
||||
award: {
|
||||
justification: string;
|
||||
delivery_date: string;
|
||||
delivery_location: string;
|
||||
};
|
||||
approval_rules: {
|
||||
approval_includes_tax: boolean;
|
||||
rules_provisional: boolean;
|
||||
};
|
||||
decision_matrix: MatrixRow[];
|
||||
final_approvals: AwardApproval[];
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user