563 lines
23 KiB
TypeScript
563 lines
23 KiB
TypeScript
import { Link, useForm, router, usePage } from '@inertiajs/react';
|
||
import { useState } from 'react';
|
||
import { catalogueReturnHref } from '@/lib/catalogue-context';
|
||
import {
|
||
searchTerritoryHref,
|
||
portfolioPreparationHref,
|
||
} from '@/lib/search-territory';
|
||
import InvestmentMapPanel from '@/components/investment-map-panel';
|
||
import StructureGeographyPanel, {
|
||
type StructureGeography,
|
||
} from '@/components/structure-geography';
|
||
import PartnershipSourceLinks from '@/components/partnership-source-links';
|
||
import { Management } from '../management/shared';
|
||
import { Layout, Field, control, button, labels, kind } from './shared';
|
||
import type { Dossier } from './shared';
|
||
import InvestmentComplements, {
|
||
type ComplementOption,
|
||
} from '@/components/investment-complements';
|
||
import ClusterCompetitivenessPublic, {
|
||
type ApprovedCompetitiveness,
|
||
} from '@/components/cluster-competitiveness-public';
|
||
export default function Show(props: Props) {
|
||
return (
|
||
<Editor
|
||
key={`${props.record.id}:${props.record.version_no ?? 0}`}
|
||
{...props}
|
||
/>
|
||
);
|
||
}
|
||
|
||
type Props = {
|
||
competitiveness?: ApprovedCompetitiveness | null;
|
||
complements?: ComplementOption[];
|
||
geography: StructureGeography;
|
||
existingSubmissions?: { id: string; version: number; updated_at: string }[];
|
||
anchorProject?: {
|
||
code: string;
|
||
name: string;
|
||
status: string;
|
||
budget_amount: string | null;
|
||
budget_currency: string | null;
|
||
start_date: string | null;
|
||
end_date: string | null;
|
||
} | null;
|
||
zones?: {
|
||
id: string;
|
||
name: string;
|
||
category: string;
|
||
association_type: string;
|
||
source_reference: string;
|
||
}[];
|
||
record: Dossier;
|
||
related: Dossier[];
|
||
management: boolean;
|
||
origin: { id: string; title: string } | null;
|
||
opportunities?: {
|
||
id: number;
|
||
title: string;
|
||
slug: string;
|
||
summary: string;
|
||
}[];
|
||
chains?: { id: string; name: string; stage: string }[];
|
||
};
|
||
|
||
function Editor({
|
||
competitiveness,
|
||
complements = [],
|
||
record,
|
||
related,
|
||
management,
|
||
origin,
|
||
opportunities = [],
|
||
chains = [],
|
||
zones = [],
|
||
anchorProject = null,
|
||
existingSubmissions = [],
|
||
geography,
|
||
}: Props) {
|
||
const { auth } = usePage<{ auth: { user: unknown } }>().props;
|
||
const [creatingManifestation, setCreatingManifestation] = useState(false);
|
||
const form = useForm({
|
||
title: record.title,
|
||
source: record.source,
|
||
content: record.content,
|
||
version_no: record.version_no,
|
||
});
|
||
const approval = useForm({
|
||
approval_evidence: '',
|
||
version_no: record.version_no,
|
||
});
|
||
const save = useForm({ origin_id: origin?.id ?? null });
|
||
const Frame = management ? Management : Layout;
|
||
const base = management ? '/management/portfolio' : '/carteira';
|
||
const pageUrl = usePage().url;
|
||
return (
|
||
<Frame title={record.title}>
|
||
<Link
|
||
className="underline"
|
||
href={catalogueReturnHref(base, pageUrl)}
|
||
>
|
||
Voltar à pesquisa
|
||
</Link>
|
||
<p>
|
||
{kind(record)} · {record.content.code}
|
||
</p>
|
||
<p className="text-sm">Fonte: {record.source}</p>
|
||
{competitiveness && (
|
||
<ClusterCompetitivenessPublic analysis={competitiveness} />
|
||
)}
|
||
{!management && (
|
||
<div className="space-y-2 rounded-xl border p-4">
|
||
<Link
|
||
className="underline"
|
||
href={portfolioPreparationHref(record.id, pageUrl)}
|
||
>
|
||
Preparar investimento a partir desta ficha
|
||
</Link>
|
||
<p>
|
||
A ficha acompanha o dossier como origem documental. A
|
||
localização do investimento e as ofertas associadas são
|
||
escolhas próprias.
|
||
</p>
|
||
</div>
|
||
)}
|
||
{(record.project_id || record.cluster_id) && (
|
||
<PartnershipSourceLinks
|
||
type={record.project_id ? 'project' : 'cluster'}
|
||
id={record.project_id ?? record.cluster_id!}
|
||
/>
|
||
)}
|
||
{(record.project_id || record.cluster_id) && (
|
||
<StructureGeographyPanel
|
||
geography={geography}
|
||
cluster={Boolean(record.cluster_id)}
|
||
dossierId={record.id}
|
||
/>
|
||
)}
|
||
{anchorProject && (
|
||
<section
|
||
className="space-y-2 rounded-xl border p-5"
|
||
aria-labelledby="anchor-project-heading"
|
||
>
|
||
<h2
|
||
id="anchor-project-heading"
|
||
className="text-xl font-bold"
|
||
>
|
||
Referência de projecto âncora
|
||
</h2>
|
||
<p>
|
||
{anchorProject.code} · {anchorProject.name}
|
||
</p>
|
||
<p>
|
||
Estado registado:{' '}
|
||
{{
|
||
draft: 'rascunho',
|
||
planned: 'previsto',
|
||
active: 'em execução',
|
||
suspended: 'suspenso',
|
||
completed: 'concluído',
|
||
cancelled: 'cancelado',
|
||
}[anchorProject.status] ?? 'por confirmar'}
|
||
.
|
||
</p>
|
||
{anchorProject.budget_amount && (
|
||
<p>
|
||
Investimento estimado documentado:{' '}
|
||
{anchorProject.budget_amount}{' '}
|
||
{anchorProject.budget_currency}
|
||
</p>
|
||
)}
|
||
{(anchorProject.start_date || anchorProject.end_date) && (
|
||
<p>
|
||
Calendário registado:{' '}
|
||
{anchorProject.start_date ?? 'início não indicado'}{' '}
|
||
– {anchorProject.end_date ?? 'fim não indicado'}
|
||
</p>
|
||
)}
|
||
<p>
|
||
Esta é uma referência de planeamento. Não significa
|
||
concurso aberto, disponibilidade imediata ou convite à
|
||
apresentação de propostas.
|
||
</p>
|
||
</section>
|
||
)}
|
||
{record.project_id && !anchorProject && (
|
||
<section
|
||
className="space-y-2 rounded-xl border p-5"
|
||
aria-labelledby="anchor-status-pending-heading"
|
||
>
|
||
<h2
|
||
id="anchor-status-pending-heading"
|
||
className="text-xl font-bold"
|
||
>
|
||
Referência de projecto âncora
|
||
</h2>
|
||
<p>
|
||
O estado de execução e o investimento não estão
|
||
confirmados num registo publicável. A referência do
|
||
dossier não representa concurso aberto nem
|
||
disponibilidade imediata.
|
||
</p>
|
||
</section>
|
||
)}
|
||
{!management &&
|
||
(record.cluster_id ||
|
||
record.corridor_id ||
|
||
record.project_id) && (
|
||
<InvestmentMapPanel
|
||
filters={
|
||
record.cluster_id
|
||
? { cluster_id: record.cluster_id }
|
||
: record.corridor_id
|
||
? { corridor_id: record.corridor_id }
|
||
: { project_id: record.project_id! }
|
||
}
|
||
/>
|
||
)}
|
||
{zones.length > 0 && (
|
||
<section>
|
||
<h2 className="text-xl font-bold">
|
||
ZEE, ZFI e parques relacionados
|
||
</h2>
|
||
{zones.map((z) => (
|
||
<p key={z.id}>
|
||
<Link
|
||
className="underline"
|
||
href={searchTerritoryHref(
|
||
`/zonas/${z.id}`,
|
||
pageUrl,
|
||
)}
|
||
>
|
||
{z.name}
|
||
</Link>{' '}
|
||
· {z.association_type} · Fonte: {z.source_reference}
|
||
</p>
|
||
))}
|
||
</section>
|
||
)}
|
||
{origin && <p>Exploração iniciada em: {origin.title}</p>}
|
||
{!management && <InvestmentComplements items={complements} />}
|
||
{!management && auth.user && (
|
||
<div className="space-y-2 rounded-xl border p-4">
|
||
<p>
|
||
As manifestações ficam privadas até uma submissão
|
||
separada. A referência não copia nem publica o dossier
|
||
público no seu nome.
|
||
</p>
|
||
{existingSubmissions.length > 0 && (
|
||
<section
|
||
className="space-y-2"
|
||
aria-label="Manifestações em rascunho para esta ficha"
|
||
>
|
||
<h2 className="font-semibold">
|
||
Continuar um rascunho existente
|
||
</h2>
|
||
{existingSubmissions.map((submission) => (
|
||
<Link
|
||
key={submission.id}
|
||
className="block underline"
|
||
href={`/investidor/manifestacoes/${submission.id}`}
|
||
>
|
||
Continuar rascunho actualizado em{' '}
|
||
{new Date(
|
||
submission.updated_at,
|
||
).toLocaleDateString('pt-MZ')}
|
||
</Link>
|
||
))}
|
||
<p>
|
||
Ou crie outra manifestação privada. Os rascunhos
|
||
não serão fundidos.
|
||
</p>
|
||
</section>
|
||
)}
|
||
<button
|
||
type="button"
|
||
className={button}
|
||
disabled={creatingManifestation}
|
||
onClick={() => {
|
||
if (
|
||
!window.confirm(
|
||
`Criar ${existingSubmissions.length ? 'outra' : 'uma'} manifestação privada associada a “${record.title}”? Pode continuar a editá-la antes de submeter. Os rascunhos existentes não serão alterados.`,
|
||
)
|
||
)
|
||
return;
|
||
setCreatingManifestation(true);
|
||
router.post(
|
||
'/investidor/manifestacoes',
|
||
{ dossier_id: record.id },
|
||
{
|
||
onFinish: () =>
|
||
setCreatingManifestation(false),
|
||
},
|
||
);
|
||
}}
|
||
>
|
||
{creatingManifestation
|
||
? 'A preparar…'
|
||
: 'Preparar manifestação privada'}
|
||
</button>
|
||
</div>
|
||
)}
|
||
{!management && !auth.user && (
|
||
<p className="rounded-xl border p-4">
|
||
Para associar esta referência a uma manifestação privada,
|
||
entre na sua conta. Esta ficha continuará pública e não será
|
||
copiada nem publicada no seu nome.{' '}
|
||
<Link
|
||
className="underline"
|
||
href={
|
||
'/carteira/' +
|
||
record.id +
|
||
'/interesse' +
|
||
(() => {
|
||
const query = new URL(
|
||
pageUrl,
|
||
'https://portal.invalid',
|
||
).searchParams.get('catalogue_query');
|
||
return query
|
||
? '?catalogue_query=' +
|
||
encodeURIComponent(query)
|
||
: '';
|
||
})()
|
||
}
|
||
>
|
||
Entrar
|
||
</Link>
|
||
</p>
|
||
)}
|
||
{chains.length > 0 && (
|
||
<section>
|
||
<h2 className="text-xl font-bold">Cadeia de valor</h2>
|
||
{chains.map((chain) => (
|
||
<p key={chain.id}>
|
||
{chain.name} —{' '}
|
||
{(
|
||
{
|
||
input: 'Insumos',
|
||
production: 'Produção',
|
||
processing: 'Transformação',
|
||
logistics: 'Logística',
|
||
market: 'Mercado',
|
||
} as Record<string, string>
|
||
)[chain.stage] ?? chain.stage}
|
||
</p>
|
||
))}
|
||
</section>
|
||
)}
|
||
<section className="space-y-3">
|
||
<h2 className="text-xl font-bold">
|
||
Oportunidades relacionadas
|
||
</h2>
|
||
{opportunities.map((opportunity) => (
|
||
<article key={opportunity.id}>
|
||
<Link
|
||
className="font-semibold underline"
|
||
href={searchTerritoryHref(
|
||
`/oportunidades/${opportunity.slug}?from_dossier=${record.id}`,
|
||
pageUrl,
|
||
)}
|
||
>
|
||
{opportunity.title}
|
||
</Link>
|
||
<p>{opportunity.summary}</p>
|
||
</article>
|
||
))}
|
||
{!opportunities.length && (
|
||
<p>Sem oportunidades publicadas associadas a esta ficha.</p>
|
||
)}
|
||
</section>
|
||
{management ? (
|
||
<>
|
||
<p>
|
||
Versão editorial: {record.version_no}. Guarde as
|
||
alterações antes de publicar.
|
||
</p>
|
||
<form
|
||
className="space-y-4"
|
||
onSubmit={(e) => {
|
||
e.preventDefault();
|
||
form.put(`${base}/${record.id}`);
|
||
}}
|
||
>
|
||
<Field label="Título">
|
||
<input
|
||
className={control}
|
||
value={form.data.title}
|
||
onChange={(e) =>
|
||
form.setData('title', e.target.value)
|
||
}
|
||
/>
|
||
</Field>
|
||
<Field label="Fonte e versão">
|
||
<input
|
||
className={control}
|
||
value={form.data.source}
|
||
onChange={(e) =>
|
||
form.setData('source', e.target.value)
|
||
}
|
||
/>
|
||
</Field>
|
||
{Object.entries(form.data.content).map(
|
||
([key, value]) => (
|
||
<Field key={key} label={labels[key] ?? key}>
|
||
<textarea
|
||
className={control}
|
||
value={value ?? ''}
|
||
onChange={(e) =>
|
||
form.setData('content', {
|
||
...form.data.content,
|
||
[key]: e.target.value,
|
||
})
|
||
}
|
||
/>
|
||
</Field>
|
||
),
|
||
)}
|
||
{Object.entries(form.errors).map(([k, v]) => (
|
||
<p role="alert" key={k}>
|
||
{v}
|
||
</p>
|
||
))}
|
||
<button disabled={form.processing} className={button}>
|
||
Guardar rascunho
|
||
</button>
|
||
</form>
|
||
<form
|
||
className="space-y-3 rounded border p-4"
|
||
onSubmit={(e) => {
|
||
e.preventDefault();
|
||
approval.post(`${base}/${record.id}/publish`);
|
||
}}
|
||
>
|
||
<p>
|
||
O gestor autorizado pode aprovar e publicar esta
|
||
ficha. Registe a referência da sua decisão.
|
||
</p>
|
||
<Field label="Referência da autorização e revisão">
|
||
<textarea
|
||
className={control}
|
||
required
|
||
minLength={15}
|
||
value={approval.data.approval_evidence}
|
||
onChange={(e) =>
|
||
approval.setData(
|
||
'approval_evidence',
|
||
e.target.value,
|
||
)
|
||
}
|
||
/>
|
||
</Field>
|
||
{approval.errors.approval_evidence && (
|
||
<p role="alert">
|
||
{approval.errors.approval_evidence}
|
||
</p>
|
||
)}
|
||
<button
|
||
className={button}
|
||
disabled={approval.processing || form.isDirty}
|
||
>
|
||
Validar e publicar
|
||
</button>
|
||
</form>
|
||
</>
|
||
) : (
|
||
<>
|
||
<dl className="space-y-5">
|
||
{Object.entries(record.content)
|
||
.filter(([k]) => k !== 'name' && k !== 'code')
|
||
.map(([key, value]) => (
|
||
<div key={key}>
|
||
<dt className="font-semibold">
|
||
{labels[key] ?? key}
|
||
</dt>
|
||
<dd className="mt-1 whitespace-pre-line">
|
||
{value}
|
||
</dd>
|
||
</div>
|
||
))}
|
||
</dl>
|
||
<form
|
||
onSubmit={(e) => {
|
||
e.preventDefault();
|
||
save.post(`/investidor/guardar/${record.id}`);
|
||
}}
|
||
>
|
||
<button className={button} disabled={save.processing}>
|
||
Guardar no painel e preparar interesse
|
||
</button>
|
||
<p className="mt-2 text-sm">
|
||
Acesso com conta e email verificado.
|
||
</p>
|
||
</form>
|
||
</>
|
||
)}
|
||
<section className="space-y-3">
|
||
<h2 className="text-xl font-semibold">Percurso de execução</h2>
|
||
<ol className="list-decimal space-y-2 pl-6">
|
||
<li>
|
||
Explore as relações entre corredor, cluster e projecto
|
||
âncora.
|
||
</li>
|
||
<li>
|
||
Consulte as componentes, parceiros e oportunidades de
|
||
participação da ficha.
|
||
</li>
|
||
<li>
|
||
Guarde a ficha no painel do investidor e manifeste
|
||
interesse com o papel e prazo pretendidos.
|
||
</li>
|
||
<li>
|
||
A equipa acompanha o pedido no CRM, com responsável e
|
||
próxima acção.
|
||
</li>
|
||
</ol>
|
||
{management && (
|
||
<div className="flex flex-wrap gap-4 underline">
|
||
{record.project_id && (
|
||
<Link
|
||
href={`/management/projects/${record.project_id}/edit`}
|
||
>
|
||
Gerir projecto
|
||
</Link>
|
||
)}
|
||
{record.cluster_id && (
|
||
<Link
|
||
href={`/management/clusters/${record.cluster_id}/edit`}
|
||
>
|
||
Gerir cadeias e lacunas do cluster
|
||
</Link>
|
||
)}
|
||
<Link href="/management/interests">
|
||
Acompanhar pedidos
|
||
</Link>
|
||
{record.publication_status === 'published' && (
|
||
<Link href={`/carteira/${record.id}`}>
|
||
Abrir ficha pública
|
||
</Link>
|
||
)}
|
||
</div>
|
||
)}
|
||
<h2 className="text-xl font-semibold">Relações na carteira</h2>
|
||
{related.map((r) => (
|
||
<p key={r.id}>
|
||
{kind(r)}:{' '}
|
||
<Link
|
||
className="underline"
|
||
href={searchTerritoryHref(
|
||
`${base}/${r.id}${management ? '' : `?from=${record.id}`}`,
|
||
pageUrl,
|
||
)}
|
||
>
|
||
{r.title}
|
||
</Link>
|
||
</p>
|
||
))}
|
||
{!related.length && (
|
||
<p>Sem relações disponíveis para consulta.</p>
|
||
)}
|
||
</section>
|
||
</Frame>
|
||
);
|
||
}
|