233 lines
7.7 KiB
TypeScript
233 lines
7.7 KiB
TypeScript
import { Head, Link, usePage } from '@inertiajs/react';
|
|
import type { ReactNode } from 'react';
|
|
import { PortalMenu } from '@/components/portal-menu';
|
|
import { cn } from '@/lib/utils';
|
|
import type { TerritorialSourceEvidence } from '@/components/territorial-source-evidence';
|
|
import type {
|
|
StructureGeography,
|
|
DocumentedLocation,
|
|
} from '@/components/structure-geography';
|
|
|
|
export type Option = {
|
|
id: number;
|
|
group: string;
|
|
code: string;
|
|
label: string;
|
|
active: boolean;
|
|
requires_description: boolean;
|
|
sort_order: number;
|
|
};
|
|
export type Entity = {
|
|
id: string;
|
|
name: string;
|
|
url?: string | null;
|
|
dossiers?: { id: string; title: string; source: string }[];
|
|
geography?: StructureGeography | null;
|
|
};
|
|
export type References = {
|
|
preparationOptions?: { value: string; label: string; group: string }[];
|
|
classification?: import('@/components/opportunity-classification').ClassificationOptions;
|
|
authorities?: { id: string; label_pt_mz: string }[];
|
|
categories?: string[];
|
|
maturityCatalogue?: {
|
|
version: string;
|
|
stages: Record<string, string>;
|
|
legacyVersion: string;
|
|
legacyLabels: Record<number, string>;
|
|
};
|
|
zones?: { id: string; name: string; category: string }[];
|
|
options: Option[];
|
|
clusters: Entity[];
|
|
corridors: Entity[];
|
|
projects: Entity[];
|
|
locations: Entity[];
|
|
sectors: { id: number; name: string; slug: string }[];
|
|
};
|
|
export type Opportunity = {
|
|
proactive?: import('@/components/proactive-investment').ProactivePublic;
|
|
id: number;
|
|
title: string;
|
|
slug: string;
|
|
reference: string;
|
|
summary: string | null;
|
|
description: string | null;
|
|
province: string | null;
|
|
district: string | null;
|
|
territorial_summary?: string;
|
|
sector?: { name: string; slug: string };
|
|
estimated_investment_amount: string | null;
|
|
estimated_investment_currency: string;
|
|
maturity_label: string | null;
|
|
maturity_catalogue_version?: string | null;
|
|
promoter_name: string | null;
|
|
expected_impact: string | null;
|
|
financing_modality: string | null;
|
|
status: string;
|
|
last_validated_at?: string | null;
|
|
};
|
|
export type Context = {
|
|
classification?: import('@/components/opportunity-classification').PublicClassification[];
|
|
source_evidence?: TerritorialSourceEvidence[];
|
|
opportunity_locations?: DocumentedLocation[];
|
|
primary_territory_path?: {
|
|
id: string;
|
|
name: string;
|
|
location_type: string;
|
|
}[];
|
|
zones?: {
|
|
id: string;
|
|
name: string;
|
|
category: string;
|
|
association_type: string;
|
|
source_reference: string;
|
|
}[];
|
|
profile: {
|
|
version: number;
|
|
is_education: boolean;
|
|
business_model: string | null;
|
|
requirements: string | null;
|
|
risks: string | null;
|
|
target_date: string | null;
|
|
source_reference: string | null;
|
|
} | null;
|
|
facets: { id: number; group: string; label: string }[];
|
|
clusters: Entity[];
|
|
corridors: Entity[];
|
|
projects: Entity[];
|
|
locations: Entity[];
|
|
};
|
|
export const input =
|
|
'w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-slate-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700';
|
|
export const button =
|
|
'inline-flex items-center justify-center rounded-lg bg-emerald-800 px-4 py-2 font-semibold text-white disabled:opacity-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700';
|
|
export const card = 'rounded-xl border border-slate-200 bg-white p-5 shadow-sm';
|
|
export function Frame({
|
|
title,
|
|
children,
|
|
mainClassName,
|
|
}: {
|
|
title: string;
|
|
children: ReactNode;
|
|
mainClassName?: string;
|
|
}) {
|
|
const { flash } = usePage<{
|
|
flash?: { success?: string; warning?: string };
|
|
}>().props;
|
|
return (
|
|
<div className="min-h-screen bg-slate-50 text-slate-900">
|
|
<Head title={title} />
|
|
<PortalMenu />
|
|
<main
|
|
className={cn(
|
|
'mx-auto max-w-7xl space-y-6 px-5 py-9',
|
|
mainClassName,
|
|
)}
|
|
>
|
|
<h1 className="text-3xl font-bold">{title}</h1>
|
|
{flash?.warning && (
|
|
<p
|
|
role="alert"
|
|
className="rounded border border-amber-600 bg-amber-50 p-4 text-amber-950"
|
|
>
|
|
{flash.warning}
|
|
</p>
|
|
)}
|
|
{flash?.success && (
|
|
<p
|
|
role="status"
|
|
className="rounded border border-emerald-700 bg-emerald-50 p-4"
|
|
>
|
|
{flash.success}
|
|
</p>
|
|
)}
|
|
{children}
|
|
</main>
|
|
<footer className="border-t p-6 text-center">
|
|
<Link href="/apoio" className="underline">
|
|
Apoio ao Investidor
|
|
</Link>{' '}
|
|
·{' '}
|
|
<Link href="/documentos" className="underline">
|
|
Documentos
|
|
</Link>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|
|
export function Field({
|
|
label,
|
|
children,
|
|
}: {
|
|
label: string;
|
|
children: ReactNode;
|
|
}) {
|
|
return (
|
|
<label className="block space-y-1 text-sm font-medium">
|
|
<span>{label}</span>
|
|
{children}
|
|
</label>
|
|
);
|
|
}
|
|
export function money(r: Opportunity) {
|
|
return r.estimated_investment_amount === null
|
|
? 'Montante não indicado'
|
|
: `${new Intl.NumberFormat('pt-MZ', { maximumFractionDigits: 2 }).format(Number(r.estimated_investment_amount))} ${r.estimated_investment_currency}`;
|
|
}
|
|
export async function api(url: string, body?: unknown) {
|
|
const encodedToken =
|
|
body === undefined || typeof document === 'undefined'
|
|
? ''
|
|
: (document.cookie
|
|
.split('; ')
|
|
.find((c) => c.startsWith('XSRF-TOKEN='))
|
|
?.slice(11) ?? '');
|
|
let token = '';
|
|
try {
|
|
token = decodeURIComponent(encodedToken);
|
|
} catch {
|
|
throw new Error(
|
|
'A sessão não pôde ser validada. Actualize a página e tente novamente.',
|
|
);
|
|
}
|
|
const formData =
|
|
typeof FormData !== 'undefined' && body instanceof FormData;
|
|
const response = await fetch(url, {
|
|
method: body === undefined ? 'GET' : 'POST',
|
|
credentials: 'same-origin',
|
|
headers: {
|
|
Accept: 'application/json',
|
|
...(formData ? {} : { 'Content-Type': 'application/json' }),
|
|
'X-XSRF-TOKEN': token,
|
|
},
|
|
...(body === undefined
|
|
? {}
|
|
: { body: formData ? body : JSON.stringify(body) }),
|
|
});
|
|
const data = await response.json().catch(() => null);
|
|
if (!response.ok || (data === null && response.status !== 204))
|
|
throw new Error(
|
|
typeof data?.message === 'string'
|
|
? data.message
|
|
: response.status === 419
|
|
? 'A sessão expirou. Actualize a página e tente novamente.'
|
|
: 'Não foi possível concluir a operação. Tente novamente.',
|
|
);
|
|
return data;
|
|
}
|
|
export const groupLabels: Record<string, string> = {
|
|
education: 'Educação',
|
|
infrastructure: 'Infraestruturas',
|
|
labour: 'Mão de obra',
|
|
qualification: 'Qualificações',
|
|
support: 'Apoios',
|
|
impact: 'Impacto',
|
|
partnership: 'Modelo de parceria',
|
|
subsector: 'Subsector',
|
|
need_category: 'Categoria da necessidade',
|
|
priority: 'Prioridade',
|
|
unit: 'Unidade',
|
|
participation: 'Participação',
|
|
investor_profile: 'Perfil do investidor',
|
|
need_status: 'Estado da necessidade',
|
|
};
|