254 lines
9.9 KiB
TypeScript
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>
|
|
);
|
|
}
|