326 lines
11 KiB
TypeScript
326 lines
11 KiB
TypeScript
import { router } from '@inertiajs/react';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { createOperationKey } from './operation-key';
|
|
import { portalRequest, PortalApiError } from './portal-api';
|
|
import type {
|
|
useInvestmentProfileEditor,
|
|
OrganizationRecord,
|
|
} from './investment-profile-editor';
|
|
|
|
export type CountryReusePreview = {
|
|
organizationId: string;
|
|
organizationName: string;
|
|
organizationVersion: number;
|
|
country: string;
|
|
reference: string;
|
|
companyVersion: number;
|
|
companyCountry: string;
|
|
};
|
|
type Attempt = {
|
|
version: number;
|
|
organization_id: string;
|
|
organization_version: number;
|
|
operation_key: string;
|
|
country: string;
|
|
};
|
|
type Editor = ReturnType<typeof useInvestmentProfileEditor>;
|
|
const object = (value: unknown): value is Record<string, unknown> =>
|
|
!!value && typeof value === 'object' && !Array.isArray(value);
|
|
const version = (value: unknown): value is number =>
|
|
typeof value === 'number' && Number.isSafeInteger(value) && value >= 1;
|
|
|
|
export function useOrganizationCountryReuse(
|
|
companyId: string,
|
|
actorId: number,
|
|
editor: Editor,
|
|
reportDirty: (id: string, dirty: boolean) => void,
|
|
organizations: OrganizationRecord[],
|
|
) {
|
|
const [preview, setPreview] = useState<CountryReusePreview | null>(null),
|
|
[busy, setBusy] = useState(false),
|
|
[reading, setReading] = useState(false),
|
|
[uncertain, setUncertain] = useState(false),
|
|
[blocked, setBlocked] = useState(false),
|
|
[error, setError] = useState(''),
|
|
[notice, setNotice] = useState('');
|
|
const pending = useRef<Attempt | null>(null),
|
|
active = useRef(false),
|
|
alive = useRef(true),
|
|
liveEditor = useRef(editor),
|
|
sources = useRef(organizations),
|
|
origin = useRef({ companyId, actorId }),
|
|
current = useRef({ companyId, actorId });
|
|
current.current = { companyId, actorId };
|
|
liveEditor.current = editor;
|
|
sources.current = organizations;
|
|
const valid = () =>
|
|
alive.current &&
|
|
Number.isSafeInteger(actorId) &&
|
|
actorId > 0 &&
|
|
origin.current.actorId === current.current.actorId &&
|
|
origin.current.companyId === current.current.companyId;
|
|
const changedSource = () => {
|
|
if (!preview || pending.current) return false;
|
|
const source = sources.current.find(
|
|
(item) => item.id === preview.organizationId,
|
|
);
|
|
return (
|
|
!source ||
|
|
source.version > preview.organizationVersion ||
|
|
(source.version === preview.organizationVersion &&
|
|
source.country !== preview.country)
|
|
);
|
|
};
|
|
useEffect(() => {
|
|
alive.current = true;
|
|
return () => {
|
|
alive.current = false;
|
|
};
|
|
}, []);
|
|
useEffect(() => {
|
|
reportDirty(
|
|
'country-reuse-' + companyId,
|
|
!!preview || busy || uncertain,
|
|
);
|
|
}, [preview, busy, uncertain, companyId, reportDirty]);
|
|
useEffect(
|
|
() => () => reportDirty('country-reuse-' + companyId, false),
|
|
[companyId, reportDirty],
|
|
);
|
|
|
|
async function inspect(organizationId: string) {
|
|
if (active.current || !valid()) return;
|
|
if (
|
|
!pending.current &&
|
|
(liveEditor.current.dirty ||
|
|
liveEditor.current.blocked ||
|
|
liveEditor.current.stale)
|
|
) {
|
|
setError(
|
|
'Guarde ou confirme a preparação desta declaração antes de reutilizar o país.',
|
|
);
|
|
return;
|
|
}
|
|
active.current = true;
|
|
setReading(true);
|
|
setError('');
|
|
setNotice('');
|
|
try {
|
|
const data = await portalRequest<unknown>(
|
|
'/api/v1/investment/profile',
|
|
);
|
|
if (!valid()) return;
|
|
if (
|
|
!object(data) ||
|
|
data.actorId !== actorId ||
|
|
!Array.isArray(data.companies) ||
|
|
!Array.isArray(data.organizations)
|
|
)
|
|
throw new Error('Invalid context');
|
|
const company = data.companies.find(
|
|
(item) => object(item) && item.id === companyId,
|
|
),
|
|
source = data.organizations.find(
|
|
(item) => object(item) && item.id === organizationId,
|
|
);
|
|
if (
|
|
!object(company) ||
|
|
!version(company.version) ||
|
|
!object(company.payload) ||
|
|
!object(source) ||
|
|
!version(source.version) ||
|
|
typeof source.name !== 'string'
|
|
) {
|
|
setBlocked(true);
|
|
throw new Error('Source unavailable');
|
|
}
|
|
if (pending.current) {
|
|
setBlocked(false);
|
|
setNotice(
|
|
'Estado consultado: declaração na versão ' +
|
|
company.version +
|
|
'. Confirme o pedido sem resposta antes de preparar outra reutilização.',
|
|
);
|
|
return;
|
|
}
|
|
if (
|
|
company.version !== liveEditor.current.base.version ||
|
|
liveEditor.current.dirty
|
|
) {
|
|
setPreview(null);
|
|
setError(
|
|
'A declaração ou a preparação mudou. Consulte e compare o registo no formulário antes de reutilizar.',
|
|
);
|
|
router.reload({ only: ['companies', 'organizations'] });
|
|
return;
|
|
}
|
|
if (
|
|
typeof source.country !== 'string' ||
|
|
!/^[A-Z]{2}$/.test(source.country)
|
|
) {
|
|
setPreview(null);
|
|
setError(
|
|
'O país de constituição ainda não está disponível nesta organização.',
|
|
);
|
|
return;
|
|
}
|
|
const reference =
|
|
object(source.provenance) &&
|
|
typeof source.provenance.reference === 'string'
|
|
? source.provenance.reference
|
|
: '';
|
|
setPreview({
|
|
organizationId,
|
|
organizationName: source.name,
|
|
organizationVersion: source.version,
|
|
country: source.country,
|
|
reference,
|
|
companyVersion: company.version,
|
|
companyCountry:
|
|
typeof company.payload.incorporation_country_code ===
|
|
'string'
|
|
? company.payload.incorporation_country_code
|
|
: '',
|
|
});
|
|
setBlocked(false);
|
|
} catch {
|
|
if (valid())
|
|
setError(
|
|
'Não foi possível consultar a declaração e a organização nesta sessão. A preparação foi conservada.',
|
|
);
|
|
} finally {
|
|
if (valid()) {
|
|
active.current = false;
|
|
setReading(false);
|
|
}
|
|
}
|
|
}
|
|
async function confirm() {
|
|
if (active.current || blocked || !valid()) return;
|
|
if (!pending.current) {
|
|
if (changedSource()) {
|
|
setError(
|
|
'A organização mudou. Consulte novamente o país antes de confirmar.',
|
|
);
|
|
return;
|
|
}
|
|
if (
|
|
!preview ||
|
|
preview.companyVersion !== liveEditor.current.base.version
|
|
) {
|
|
setError('Volte a consultar os valores antes de confirmar.');
|
|
return;
|
|
}
|
|
let key: string;
|
|
try {
|
|
key = createOperationKey();
|
|
} catch (failure) {
|
|
setError(
|
|
failure instanceof Error
|
|
? failure.message
|
|
: 'Não foi possível preparar o pedido.',
|
|
);
|
|
return;
|
|
}
|
|
if (!liveEditor.current.beginCountryReuse()) {
|
|
setError(
|
|
'Guarde ou confirme a preparação desta declaração antes de reutilizar o país.',
|
|
);
|
|
return;
|
|
}
|
|
pending.current = {
|
|
version: preview.companyVersion,
|
|
organization_id: preview.organizationId,
|
|
organization_version: preview.organizationVersion,
|
|
operation_key: key,
|
|
country: preview.country,
|
|
};
|
|
}
|
|
const attempt = pending.current;
|
|
active.current = true;
|
|
setBusy(true);
|
|
setError('');
|
|
setNotice('');
|
|
try {
|
|
const { country: expectedCountry, ...command } = attempt;
|
|
const result = await portalRequest<unknown>(
|
|
'/api/v1/investment/companies/' +
|
|
companyId +
|
|
'/organization-country',
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(command),
|
|
},
|
|
);
|
|
if (!valid() || pending.current !== attempt) return;
|
|
if (
|
|
!object(result) ||
|
|
!version(result.version) ||
|
|
result.version !== attempt.version + 1 ||
|
|
result.country !== expectedCountry
|
|
)
|
|
throw new Error('Unconfirmed result');
|
|
liveEditor.current.endCountryReuse({
|
|
version: result.version,
|
|
country: expectedCountry,
|
|
});
|
|
pending.current = null;
|
|
setPreview(null);
|
|
setUncertain(false);
|
|
setBlocked(false);
|
|
setNotice('País reutilizado e gravação confirmada.');
|
|
router.reload({ only: ['companies', 'organizations'] });
|
|
} catch (failure) {
|
|
if (!valid() || pending.current !== attempt) return;
|
|
if (
|
|
failure instanceof PortalApiError &&
|
|
[409, 422].includes(failure.status)
|
|
) {
|
|
pending.current = null;
|
|
liveEditor.current.endCountryReuse();
|
|
setUncertain(false);
|
|
setPreview(null);
|
|
setError(
|
|
'A declaração ou a fonte já não corresponde ao pedido. Consulte os valores guardados antes de preparar outra reutilização.',
|
|
);
|
|
router.reload({ only: ['companies', 'organizations'] });
|
|
} else {
|
|
setUncertain(true);
|
|
setBlocked(
|
|
failure instanceof PortalApiError &&
|
|
[401, 403, 404, 419].includes(failure.status),
|
|
);
|
|
setError(
|
|
'Não foi possível confirmar a reutilização. O pedido original foi conservado. Consulte o estado ou repita o mesmo pedido antes de editar.',
|
|
);
|
|
}
|
|
} finally {
|
|
if (valid()) {
|
|
active.current = false;
|
|
setBusy(false);
|
|
}
|
|
}
|
|
}
|
|
function cancel() {
|
|
if (pending.current || active.current || !valid()) return;
|
|
setPreview(null);
|
|
setError('');
|
|
setNotice('');
|
|
}
|
|
return {
|
|
preview,
|
|
busy,
|
|
reading,
|
|
uncertain,
|
|
blocked,
|
|
error,
|
|
notice,
|
|
inspect,
|
|
confirm,
|
|
cancel,
|
|
sourceChanged: changedSource(),
|
|
pendingOrganizationId: pending.current?.organization_id,
|
|
};
|
|
}
|