294 lines
14 KiB
TypeScript
294 lines
14 KiB
TypeScript
import { Head, Link } from '@inertiajs/react';
|
|
import { Printer } from 'lucide-react';
|
|
import { PageTitle, Panel } from '@/components/procurement';
|
|
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[];
|
|
}) {
|
|
const snapshot = order.snapshot;
|
|
return (
|
|
<>
|
|
<Head title={order.number} />
|
|
<Link
|
|
href={`/ordens/${order.id}/recepcoes`}
|
|
className="mx-auto mt-4 w-full max-w-6xl px-5 text-sm font-medium text-teal-700 dark:text-teal-400"
|
|
>
|
|
Recepções, qualidade e quantidades pendentes
|
|
</Link>
|
|
<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">
|
|
{snapshot.investment_rules && (
|
|
<Panel title="Regra de investimento congelada">
|
|
<p>
|
|
Versão {snapshot.investment_rules.revision} ·
|
|
Tecto{' '}
|
|
{money(snapshot.investment_rules.ceiling_minor)}{' '}
|
|
·{' '}
|
|
{snapshot.investment_rules.includes_tax
|
|
? 'com IVA'
|
|
: 'sem IVA'}
|
|
</p>
|
|
<p>
|
|
Vigência: {snapshot.investment_rules.valid_from}{' '}
|
|
a {snapshot.investment_rules.valid_until}
|
|
</p>
|
|
{snapshot.equipment_requests?.map((line) => (
|
|
<p key={line.id} className="mt-2">
|
|
{line.description} · Área:{' '}
|
|
{line.production_area}
|
|
</p>
|
|
))}
|
|
</Panel>
|
|
)}
|
|
<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: '#' },
|
|
],
|
|
};
|