Files
Investidor-V2/resources/js/pages/portfolio/show.tsx
T

563 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}