314 lines
14 KiB
TypeScript
314 lines
14 KiB
TypeScript
import { useEffect, useId } from 'react';
|
|
import CountrySelect, { type Country } from './country-select';
|
|
import OrganizationCountryReuse from './organization-country-reuse';
|
|
import { Field, input, button } from '@/pages/investment/shared';
|
|
import {
|
|
useInvestmentProfileEditor,
|
|
type ProfileRecord,
|
|
type OrganizationRecord,
|
|
} from '@/lib/investment-profile-editor';
|
|
|
|
export default function InvestmentProfileEditor({
|
|
record,
|
|
fields,
|
|
countries,
|
|
company = false,
|
|
actorId,
|
|
editorId,
|
|
reportDirty,
|
|
labels,
|
|
organizations = [],
|
|
}: {
|
|
record: ProfileRecord;
|
|
fields: string[];
|
|
countries: Country[];
|
|
company?: boolean;
|
|
actorId: number;
|
|
editorId: string;
|
|
reportDirty: (id: string, dirty: boolean) => void;
|
|
labels: Record<string, string>;
|
|
organizations?: OrganizationRecord[];
|
|
}) {
|
|
const isCompany = company || !!record.id;
|
|
const editor = useInvestmentProfileEditor(
|
|
record,
|
|
fields,
|
|
isCompany,
|
|
actorId,
|
|
editorId,
|
|
reportDirty,
|
|
);
|
|
const id = useId(),
|
|
disabled =
|
|
editor.busy ||
|
|
editor.uncertain ||
|
|
editor.blocked ||
|
|
editor.externalBusy;
|
|
useEffect(() => {
|
|
if (Object.keys(editor.errors).length)
|
|
document
|
|
.getElementById(id)
|
|
?.querySelector<HTMLElement>('[aria-invalid="true"]')
|
|
?.focus();
|
|
}, [editor.errors, id]);
|
|
const fieldErrorKeys = [
|
|
...fields.map((key) => 'payload.' + key),
|
|
...(isCompany ? ['payload.country'] : []),
|
|
];
|
|
return (
|
|
<>
|
|
<form
|
|
id={id}
|
|
aria-label={
|
|
isCompany
|
|
? record.id
|
|
? 'Declaração de empresa'
|
|
: 'Nova declaração de empresa'
|
|
: 'Perfil de investimento'
|
|
}
|
|
className="mt-4 space-y-4"
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
void editor.save();
|
|
}}
|
|
>
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
{record.payload.country && (
|
|
<p>
|
|
País anteriormente registado:{' '}
|
|
{record.payload.country}. Conservado sem atribuir
|
|
automaticamente residência, constituição ou origem
|
|
da solicitação.
|
|
</p>
|
|
)}
|
|
{fields.map((key) => {
|
|
const fieldId = id + '-' + key,
|
|
error =
|
|
editor.errors['payload.' + key] ??
|
|
(key === 'incorporation_country_code'
|
|
? editor.errors['payload.country']
|
|
: undefined),
|
|
long = [
|
|
'description',
|
|
'experience',
|
|
'investment_capacity',
|
|
].includes(key);
|
|
return (
|
|
<Field key={key} label={labels[key]}>
|
|
{key.endsWith('_country_code') ? (
|
|
<CountrySelect
|
|
id={fieldId}
|
|
countries={countries}
|
|
label={labels[key]}
|
|
error={error}
|
|
value={editor.draft[key]}
|
|
disabled={disabled}
|
|
required={
|
|
isCompany &&
|
|
key ===
|
|
'incorporation_country_code' &&
|
|
!editor.draft.country
|
|
}
|
|
onChange={(value) =>
|
|
editor.change(key, value)
|
|
}
|
|
/>
|
|
) : long ? (
|
|
<textarea
|
|
id={fieldId}
|
|
aria-label={labels[key]}
|
|
className={input}
|
|
value={editor.draft[key]}
|
|
disabled={disabled}
|
|
rows={3}
|
|
maxLength={
|
|
key === 'investment_capacity'
|
|
? 1000
|
|
: 2000
|
|
}
|
|
aria-invalid={!!error}
|
|
aria-describedby={
|
|
error
|
|
? fieldId + '-error'
|
|
: undefined
|
|
}
|
|
onChange={(event) =>
|
|
editor.change(
|
|
key,
|
|
event.target.value,
|
|
)
|
|
}
|
|
/>
|
|
) : (
|
|
<input
|
|
id={fieldId}
|
|
aria-label={labels[key]}
|
|
className={input}
|
|
value={editor.draft[key]}
|
|
disabled={disabled}
|
|
required={
|
|
isCompany &&
|
|
['name', 'role'].includes(key)
|
|
}
|
|
maxLength={
|
|
key === 'phone'
|
|
? 30
|
|
: key === 'registration'
|
|
? 100
|
|
: ['role', 'sector'].includes(
|
|
key,
|
|
)
|
|
? 150
|
|
: 200
|
|
}
|
|
aria-invalid={!!error}
|
|
aria-describedby={
|
|
error
|
|
? fieldId + '-error'
|
|
: undefined
|
|
}
|
|
onChange={(event) =>
|
|
editor.change(
|
|
key,
|
|
event.target.value,
|
|
)
|
|
}
|
|
/>
|
|
)}
|
|
{!key.endsWith('_country_code') && error && (
|
|
<p
|
|
id={fieldId + '-error'}
|
|
role="alert"
|
|
className="text-red-700"
|
|
>
|
|
{error}
|
|
</p>
|
|
)}
|
|
</Field>
|
|
);
|
|
})}
|
|
</div>
|
|
{editor.error && <p role="alert">{editor.error}</p>}
|
|
{Object.entries(editor.errors)
|
|
.filter(([key]) => !fieldErrorKeys.includes(key))
|
|
.map(([key, value]) => (
|
|
<p role="alert" key={key}>
|
|
{value}
|
|
</p>
|
|
))}
|
|
{editor.notice && <p role="status">{editor.notice}</p>}
|
|
{editor.stale && (
|
|
<p role="alert">
|
|
Existe uma versão mais recente deste registo. A sua
|
|
preparação foi conservada; consulte e compare antes de
|
|
guardar.
|
|
</p>
|
|
)}
|
|
<div className="flex flex-wrap gap-3">
|
|
<button
|
|
className={button}
|
|
disabled={
|
|
editor.busy ||
|
|
editor.externalBusy ||
|
|
editor.reading ||
|
|
editor.blocked ||
|
|
(editor.stale && !editor.uncertain)
|
|
}
|
|
>
|
|
{editor.uncertain
|
|
? 'Repetir o mesmo pedido'
|
|
: isCompany
|
|
? 'Guardar declaração da empresa'
|
|
: 'Guardar perfil de investimento'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={button}
|
|
disabled={
|
|
editor.busy || editor.externalBusy || editor.reading
|
|
}
|
|
onClick={() => void editor.refresh()}
|
|
>
|
|
Consultar registo guardado
|
|
</button>
|
|
</div>
|
|
{editor.remote && (
|
|
<section
|
|
aria-label="Comparação do registo guardado"
|
|
className="space-y-3 rounded border p-3"
|
|
>
|
|
<p>
|
|
Versão guardada: {editor.remote.version}; versão da
|
|
preparação: {editor.base.version}.
|
|
</p>
|
|
<table className="w-full text-left text-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Campo</th>
|
|
<th>Preparação nesta página</th>
|
|
<th>Registo guardado</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{fields
|
|
.filter(
|
|
(key) =>
|
|
editor.draft[key] !==
|
|
editor.remote?.payload[key],
|
|
)
|
|
.map((key) => (
|
|
<tr key={key}>
|
|
<th>{labels[key]}</th>
|
|
<td className="break-words">
|
|
{editor.draft[key] ||
|
|
'Não informado'}
|
|
</td>
|
|
<td className="break-words">
|
|
{editor.remote?.payload[key] ||
|
|
'Não informado'}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
{!editor.uncertain && (
|
|
<div className="flex flex-wrap gap-3">
|
|
<button
|
|
type="button"
|
|
className={button}
|
|
onClick={() => editor.adopt(true)}
|
|
>
|
|
Usar versão actual e manter a preparação
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={button}
|
|
onClick={() => {
|
|
if (
|
|
window.confirm(
|
|
'Substituir a preparação desta página pelos valores guardados?',
|
|
)
|
|
)
|
|
editor.adopt(false);
|
|
}}
|
|
>
|
|
Carregar os valores guardados
|
|
</button>
|
|
</div>
|
|
)}
|
|
</section>
|
|
)}
|
|
</form>
|
|
{isCompany && record.id && (
|
|
<OrganizationCountryReuse
|
|
companyId={record.id}
|
|
actorId={actorId}
|
|
organizations={organizations}
|
|
editor={editor}
|
|
reportDirty={reportDirty}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|