Files
compras/resources/js/pages/purchases/process.tsx
T

305 lines
14 KiB
TypeScript

import { Head, Link } from '@inertiajs/react';
import { ArrowRight, FileText, Link2 } from 'lucide-react';
import { PageTitle, Panel, Status } from '@/components/procurement';
import { Button } from '@/components/ui/button';
import { date, roleLabels } from '@/types/procurement';
type Stage = {
label: string;
state: string;
owner: string;
detail: string;
href: string | null;
};
type Document = { label: string; kind: string; href: string };
type Props = {
process: {
number: string;
status: string;
requester: string;
department: string;
needed_by: string;
href: string;
};
stages: Stage[];
documents: Document[];
decisions: {
area: string;
role: string;
decision: string;
actor: string | null;
date: string | null;
}[];
history: { id: number; action: string; created_at: string }[];
related: { label: string; href: string }[];
cancellation: { status: string; href: string } | null;
future: string[];
};
const labels: Record<string, string> = {
draft: 'Rascunho',
pending: 'Em aprovação',
approved: 'Aprovada',
rejected: 'Rejeitada',
cancelled: 'Cancelada',
not_started: 'Sem registos',
not_required: 'Não aplicável aos artigos actuais',
restricted: 'Acesso reservado',
recorded: 'Com registos',
issued: 'Emitida',
};
const events: Record<string, string> = {
'qualification.defined': 'Versão dos requisitos documentais registada',
'qualification.assessed': 'Versão da análise documental registada',
'admission.adopted': 'Política de admissão adoptada expressamente',
'admission.proposed': 'Tentativa de decisão de admissão registada',
'admission.reviewed': 'Conferência independente de admissão registada',
'recommendation.adopted': 'Política de recomendação adoptada expressamente',
'recommendation.proposed': 'Tentativa de recomendação registada',
'recommendation.reviewed':
'Conferência independente da recomendação registada',
'opening.recorded': 'Versão da acta de abertura registada',
'requisition.created': 'Requisição criada',
'requisition.updated': 'Requisição actualizada',
'requisition.submitted': 'Requisição submetida',
'requisition.revision_created': 'Revisão criada em novo processo',
'approval.approved': 'Etapa da estimativa aprovada',
'approval.rejected': 'Estimativa rejeitada',
'equipment.technical_approved': 'Validação técnica aprovada',
'equipment.technical_rejected': 'Validação técnica rejeitada',
'award.created': 'Adjudicação proposta',
'award.approved': 'Etapa da adjudicação aprovada',
'award.rejected': 'Adjudicação rejeitada',
'purchase_order.issued': 'Ordem emitida',
'receipt.confirmed': 'Recepção registada',
'invoice.created': 'Factura registada',
'invoice.validated': 'Factura validada',
'payment.registered': 'Pagamento externo registado',
'payment.confirmed': 'Pagamento externo confirmado',
'payment.rejected': 'Pagamento externo rejeitado',
'budget.reserve': 'Reserva orçamental registada',
'budget.award': 'Reserva ajustada à adjudicação',
'budget.commit': 'Compromisso registado',
'budget.release': 'Reserva libertada',
'cancellation.requested': 'Cancelamento solicitado',
'cancellation.approved': 'Cancelamento aprovado',
'cancellation.rejected': 'Cancelamento rejeitado',
};
export default function PurchaseProcess({
process,
stages,
documents,
decisions,
history,
related,
cancellation,
future,
}: Props) {
return (
<>
<Head title={`Processo · ${process.number}`} />
<div className="mx-auto flex max-w-7xl flex-col gap-6 p-5 lg:p-8">
<PageTitle
eyebrow="Processo de compra"
title={process.number}
description={`${process.requester} · ${process.department} · Necessário em ${date(process.needed_by)}`}
action={
<Button asChild variant="outline">
<Link href={process.href}>
Abrir requisição
<ArrowRight className="size-4" />
</Link>
</Button>
}
/>
<div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border bg-card p-4">
<p className="text-sm text-muted-foreground">
Estado da requisição. As restantes etapas têm estados
próprios.
</p>
<Status value={process.status} />
</div>
{cancellation && (
<div className="rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-950">
<p>
Cancelamento da adjudicação:{' '}
<strong>{labels[cancellation.status]}</strong>.{' '}
{cancellation.status === 'pending' &&
'Novas decisões finais e emissão estão suspensas.'}
</p>
<Link
className="mt-2 inline-flex underline"
href={cancellation.href}
>
Consultar cancelamento e revisão
</Link>
</div>
)}
<section
aria-label="Etapas do processo"
className="grid gap-4 md:grid-cols-2 xl:grid-cols-3"
>
{stages.map((stage, index) => (
<article
key={stage.label}
className="flex flex-col rounded-xl border bg-card p-5"
>
<div className="flex items-center gap-3">
<span className="grid size-8 shrink-0 place-items-center rounded-full bg-muted text-xs font-semibold">
{String(index + 1).padStart(2, '0')}
</span>
<h2 className="text-sm font-semibold">
{stage.label}
</h2>
</div>
<p
className={`mt-4 text-sm font-medium ${stage.state === 'restricted' ? 'text-muted-foreground' : stage.state === 'rejected' || stage.state === 'cancelled' ? 'text-red-700 dark:text-red-400' : 'text-foreground'}`}
>
{labels[stage.state] ?? stage.state}
</p>
<p className="mt-2 text-xs font-medium text-muted-foreground">
Responsável: {stage.owner}
</p>
<p className="mt-3 flex-1 text-sm leading-6 text-muted-foreground">
{stage.detail}
</p>
{stage.href && (
<Link
href={stage.href}
className="mt-4 inline-flex items-center gap-2 text-sm font-medium underline underline-offset-4"
>
Consultar etapa
<ArrowRight className="size-3.5" />
</Link>
)}
</article>
))}
</section>
<div className="grid items-start gap-6 lg:grid-cols-2">
<Panel
title="Documentos e registos"
description="Atalhos para os registos disponíveis ao seu perfil; não inclui anexos privados nem dados bancários."
>
<ul className="divide-y">
{documents.map((document, index) => (
<li key={`${document.kind}-${index}`}>
<Link
href={document.href}
className="flex items-center gap-3 py-3 text-sm"
>
<FileText className="size-4 shrink-0 text-muted-foreground" />
<span className="flex-1">
<span className="block font-medium">
{document.label}
</span>
<span className="text-xs text-muted-foreground">
{document.kind}
</span>
</span>
<ArrowRight className="size-4" />
</Link>
</li>
))}
</ul>
</Panel>
<Panel
title="Decisões registadas"
description="Histórico por etapa. Uma aprovação anterior não substitui as verificações actuais."
>
{!decisions.length && (
<p className="text-sm text-muted-foreground">
Ainda não existem etapas de decisão registadas.
</p>
)}
<ol className="divide-y">
{decisions.map((decision, index) => (
<li key={index} className="py-3 text-sm">
<div className="flex flex-wrap justify-between gap-2">
<strong className="font-medium">
{decision.area} ·{' '}
{roleLabels[decision.role] ??
decision.role}
</strong>
<span>
{labels[decision.decision] ??
decision.decision}
</span>
</div>
<p className="mt-1 text-xs text-muted-foreground">
{decision.actor ??
'Por designar na decisão'}
{decision.date
? ` · ${date(decision.date)}`
: ''}
</p>
</li>
))}
</ol>
</Panel>
</div>
{related.length > 0 && (
<Panel title="Processos ligados">
<div className="flex flex-wrap gap-4">
{related.map((link) => (
<Link
key={link.href}
href={link.href}
className="inline-flex items-center gap-2 text-sm underline"
>
<Link2 className="size-4" />
{link.label}
</Link>
))}
</div>
</Panel>
)}
<Panel
title="Histórico do processo"
description="Eventos disponíveis ao seu perfil, sem expor o conteúdo privado dos registos."
>
{history.length ? (
<ol className="grid gap-3 sm:grid-cols-2">
{history.map((event) => (
<li
key={event.id}
className="border-l-2 border-[#ed1c24] pl-3 text-sm"
>
<p>
{events[event.action] ??
'Evento do processo'}
</p>
<p className="mt-1 text-xs text-muted-foreground">
{date(event.created_at)}
</p>
</li>
))}
</ol>
) : (
<p className="text-sm text-muted-foreground">
Sem eventos disponíveis.
</p>
)}
</Panel>
{future.length > 0 && (
<Panel
title="Etapas previstas no roteiro"
description="Ainda não implementadas. Não são tarefas obrigatórias deste pedido e não indicam validação concluída."
>
<ul className="grid gap-2 text-sm text-muted-foreground sm:grid-cols-2">
{future.map((stage) => (
<li key={stage}>{stage}</li>
))}
</ul>
</Panel>
)}
</div>
</>
);
}
PurchaseProcess.layout = {
breadcrumbs: [
{ title: 'Requisições', href: '/requisicoes' },
{ title: 'Processo de compra', href: '#' },
],
};