Files
Investidor-V2/resources/js/pages/procurement/shared.tsx
T

254 lines
9.9 KiB
TypeScript

import type { ReactNode } from 'react';
export type Lot = { code: string; description: string; quantities: string };
export type Attachment = {
resource_id: number;
public: boolean;
purpose: string;
title?: string;
available?: boolean;
};
export type Draft = {
[key: string]: string | number | boolean | null | Lot[] | Attachment[];
};
export type Payload = Draft & {
opportunity?: { title: string; url: string } | null;
};
export type Publication = {
id: string;
version: number;
act: string;
payload: Payload;
published_at: string;
reason: string;
};
export const states: Record<string, string> = {
open: 'Aberto',
extended: 'Prorrogado',
deadline_ended: 'Prazo de propostas terminado',
cancelled: 'Cancelado',
results: 'Resultados publicados',
};
export const acts: Record<string, string> = {
initial: 'Anúncio inicial',
clarification: 'Esclarecimento',
correction: 'Rectificação',
extension: 'Prorrogação',
award: 'Resultado / adjudicação',
cancellation: 'Cancelamento',
other: 'Outro acto',
};
export const labels: Record<string, string> = {
reference: 'Referência',
object: 'Objecto',
responsible_unit: 'Unidade responsável / UGEA quando aplicável',
modality: 'Modalidade',
object_kind: 'Natureza do objecto',
quantities: 'Quantidades essenciais',
financier: 'Financiador',
instrument: 'Instrumento aplicável',
edition: 'Edição aplicável',
rule_source: 'Fonte das regras',
financing_agreement: 'Acordo de financiamento',
eligibility: 'Elegibilidade',
requirements: 'Requisitos de participação',
proposal_validity: 'Validade da proposta',
guarantee_status: 'Aplicabilidade de garantias',
guarantees: 'Garantias documentadas',
delivery_channel: 'Canal de entrega',
delivery_address: 'Endereço / instruções para apresentar proposta',
delivery_url: 'Plataforma de entrega',
public_contact: 'Contacto público autorizado',
dissemination: 'Meios de divulgação exigidos / adoptados e comprovativos',
origin_url: 'Anúncio de origem',
origin_published_at: 'Publicação de origem — referência do prazo',
clarifications_at: 'Limite para esclarecimentos',
deadline: 'Entrega de propostas',
opening_at: 'Abertura (quando aplicável)',
timezone: 'Fuso horário do procedimento',
minimum_days: 'Prazo mínimo documentado (dias)',
minimum_source: 'Fonte / citação do prazo mínimo',
minimum_edition: 'Edição da regra de prazo',
minimum_modality: 'Modalidade da regra de prazo',
minimum_day_basis: 'Base de contagem do prazo',
minimum_calendar_source: 'Fonte do calendário de dias úteis',
minimum_working_days: 'Dias de trabalho (1=segunda, 7=domingo)',
minimum_holidays: 'Feriados e exclusões documentados',
result: 'Acto ou resultado documentado',
};
export const control =
'w-full rounded border border-slate-400 bg-background p-2 text-foreground';
export function Field({
label,
children,
}: {
label: string;
children: ReactNode;
}) {
return (
<label className="block space-y-1">
<span className="font-medium">{label}</span>
{children}
</label>
);
}
export function NoticeContent({ p }: { p: Payload }) {
const links = ['rule_source', 'origin_url', 'delivery_url'];
const names: Record<string, string> = {
works: 'Obras',
goods: 'Bens',
services: 'Serviços',
mixed: 'Misto',
other: 'Outro',
required: 'Exigidas',
not_required: 'Não exigidas',
unknown: 'Por determinar',
calendar: 'Dias corridos',
business: 'Dias úteis',
};
return (
<div className="space-y-5">
<h2 className="text-2xl font-semibold">{String(p.title ?? '')}</h2>
<p>
<strong>Entidade:</strong>{' '}
{String(p.organization_name ?? 'Por indicar')}
</p>
<p>
Regime:{' '}
{(
{
state: 'Contratação pública do Estado',
external: 'Financiamento externo',
private: 'Concurso privado',
other: 'Outro regime documentado',
} as Record<string, string>
)[String(p.regime)] ?? 'Por indicar'}
</p>
<section>
<h3 className="text-lg font-semibold">
Objecto e como participar
</h3>
<p className="whitespace-pre-wrap">{String(p.object ?? '')}</p>
<p className="my-2 rounded border p-3">
<strong>Entrega:</strong>{' '}
{String(p.deadline ?? 'Por indicar').replace('T', ' ')} ·{' '}
{String(p.timezone ?? '')}
<br />
{String(p.delivery_channel ?? '')} ·{' '}
{String(p.delivery_address ?? '')}
</p>
<p>
Sector: {String(p.sector_name ?? 'Por indicar')} ·
Localização: {String(p.territory_name ?? 'Por indicar')}
</p>
{p.project_name && (
<p>
Projecto relacionado: {String(p.project_name)}. Esta
relação não confirma execução ou contratação.
</p>
)}
{p.opportunity && (
<a className="underline" href={p.opportunity.url}>
Oportunidade relacionada: {p.opportunity.title}
</a>
)}
</section>
{p.deadline_previous && (
<p className="rounded border p-3">
<strong>Prorrogação:</strong> prazo anterior{' '}
{String(p.deadline_previous).replace('T', ' ')} (
{String(p.timezone_previous)}) → novo prazo{' '}
{String(p.deadline).replace('T', ' ')} ({String(p.timezone)}
).
</p>
)}
<dl className="grid gap-3 md:grid-cols-2">
{Object.entries(labels)
.filter(
([key]) =>
p[key] !== null &&
p[key] !== undefined &&
p[key] !== '',
)
.map(([key, label]) => (
<div
key={key}
className="rounded border p-3 break-words"
>
<dt className="font-semibold">{label}</dt>
<dd className="whitespace-pre-wrap">
{links.includes(key) ? (
<a
className="underline"
href={String(p[key])}
rel="noopener noreferrer"
>
{String(p[key])}
</a>
) : (
(
names[String(p[key])] ?? String(p[key])
).replace(/^(\d{4}-\d{2}-\d{2})T/, '$1 ')
)}
</dd>
</div>
))}
</dl>
<section>
<h3 className="text-lg font-semibold">Lotes</h3>
{((p.lots ?? []) as Lot[]).length ? (
<ul>
{(p.lots as Lot[]).map((lot, i) => (
<li key={i}>
{lot.code} — {lot.description} ·{' '}
{lot.quantities}
</li>
))}
</ul>
) : (
<p>Sem lotes indicados.</p>
)}
</section>
<section>
<h3 className="text-lg font-semibold">
Documentos do concurso
</h3>
<ul>
{((p.attachments ?? []) as Attachment[]).map((item, i) => (
<li key={i}>
{item.available === false ? (
<span>
{item.title} — documento actualmente
indisponível
</span>
) : (
<a
className="underline"
href={`/documentos/${item.resource_id}/download`}
>
{item.title ??
`Documento ${item.resource_id}`}{' '}
— {item.purpose}
</a>
)}
</li>
))}
</ul>
{!((p.attachments as Attachment[]) ?? []).length && (
<p>
Sem anexos públicos neste acto. Consulte o canal e o
anúncio de origem.
</p>
)}
</section>
<p className="rounded border p-3">
O InvesteMoz divulga este acto e não recebe propostas nesta
secção. Apresente a proposta no canal indicado nos documentos.
Esta divulgação não substitui os meios oficiais exigidos,
não comprova envio à UFSA ou ao financiador e não equivale à
execução do investimento.
</p>
</div>
);
}