Files
Investidor-V2/resources/js/components/investment-profile-editor.tsx
T

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}
/>
)}
</>
);
}