Files
Investidor-V2/resources/js/components/offer-confidential-access.tsx
T

280 lines
12 KiB
TypeScript

import { useEffect, useState } from 'react';
import { profileJson } from '@/lib/profile-json';
type Acceptance = { id: string; actor_id: number; version_hash: string };
type Grant = { interested_user_id: number; version_hash: string };
type Overview = {
seller: boolean;
nda_pending: boolean;
can_accept: boolean;
can_authorize: boolean;
version_hash: string | null;
nda: { sha256: string; version: number; fingerprint: string } | null;
requests: Acceptance[];
grants: Grant[];
acceptance: Acceptance | null;
etag: string | null;
history: { id: string; action: string; at: string; version_hash: string }[];
};
export default function OfferConfidentialAccess({
id,
onChanged,
}: {
id: number;
onChanged?: (etag: string) => void;
}) {
const [overview, setOverview] = useState<Overview | null>(null);
const [confirm, setConfirm] = useState(false);
const [reason, setReason] = useState('');
const [message, setMessage] = useState('');
const [busy, setBusy] = useState(false);
const [business, setBusiness] = useState<Record<
string,
string | number | null
> | null>(null);
const base = `/minhas-oportunidades/ofertas/${id}`;
useEffect(() => {
let active = true;
profileJson(`${base}/confidencialidade`, {}, 'GET')
.then((result) => {
if (active) setOverview(result);
})
.catch((error) => {
if (active) setMessage(error.message);
});
return () => {
active = false;
};
}, [base]);
async function act(action: string, target?: Acceptance | Grant) {
if (!overview || !confirm || busy) return;
setBusy(true);
setBusiness(null);
try {
await profileJson(`${base}/confidencialidade`, {
action,
confirm,
reason,
version_hash: target?.version_hash ?? overview.version_hash,
nda_sha256: overview.nda?.sha256,
nda_fingerprint: overview.nda?.fingerprint,
interested_user_id:
target &&
('actor_id' in target
? target.actor_id
: target.interested_user_id),
acceptance_id: target && 'id' in target ? target.id : undefined,
});
const next = await profileJson(
`${base}/confidencialidade`,
{},
'GET',
);
setOverview(next);
if (typeof next.etag === 'string') onChanged?.(next.etag);
setConfirm(false);
setMessage('Decisão registada para a versão indicada.');
} catch (error) {
setMessage(
error instanceof Error
? error.message
: 'Não foi possível registar.',
);
} finally {
setBusy(false);
}
}
async function read() {
setBusy(true);
setBusiness(null);
try {
const result = await profileJson(`${base}/confidencial`, {}, 'GET');
setBusiness(result.business);
setMessage('Acesso autorizado e registado para esta versão.');
} catch (error) {
setMessage(
error instanceof Error ? error.message : 'Acesso indisponível.',
);
} finally {
setBusy(false);
}
}
const labels: Record<string, string> = {
sector: 'Sector principal',
secondary_sector: 'Sector secundário',
revenue: 'Receita',
cash_flow: 'Lucro / fluxo de caixa',
currency: 'Moeda',
reason: 'Motivo da venda',
availability: 'Disponibilidade declarada',
};
return (
<section className="space-y-3 rounded-lg border p-4">
<h2 className="font-semibold">Acesso confidencial à empresa</h2>
<p>
O acesso depende da aceitação do NDA e da autorização expressa
do vendedor para a mesma versão.
</p>
{overview?.nda_pending && (
<p>
O exemplar aprovado do NDA ainda não foi disponibilizado. O
acesso confidencial mantém-se fechado.
</p>
)}
{overview?.nda && (
<a className="underline" href={`${base}/nda`}>
Consultar NDA — versão {overview.nda.version}
</a>
)}
{overview && (
<fieldset disabled={busy} className="space-y-3">
<label className="block">
{overview.seller
? 'Fundamento da decisão'
: 'Observação (opcional)'}
<textarea
className="block w-full rounded border p-2"
minLength={overview.seller ? 20 : undefined}
maxLength={1000}
value={reason}
onChange={(e) => setReason(e.target.value)}
/>
</label>
<label className="flex gap-2">
<input
type="checkbox"
checked={confirm}
onChange={(e) => setConfirm(e.target.checked)}
/>
{overview.seller
? 'Confirmo a decisão expressa para o interessado e versão indicados.'
: 'Confirmo a operação para o exemplar e a versão indicados.'}
</label>
{!overview.seller && (
<div className="flex flex-wrap gap-3">
<button
type="button"
className="rounded border p-2"
disabled={!confirm || !overview.can_accept}
onClick={() => void act('accept')}
>
Li e aceito — registar aceitação
</button>
{overview.acceptance && (
<button
type="button"
className="underline"
disabled={!confirm}
onClick={() =>
void act(
'withdraw',
overview.acceptance!,
)
}
>
Retirar aceitação
</button>
)}
<button
type="button"
className="underline"
disabled={overview.nda_pending}
onClick={() => void read()}
>
Consultar dados autorizados
</button>
</div>
)}
{overview.seller && (
<>
<ul className="space-y-2">
{overview.requests.map((item) => (
<li
key={item.id}
className="flex flex-wrap gap-3"
>
Interessado #{item.actor_id} · versão{' '}
{item.version_hash.slice(0, 12)}
<button
type="button"
className="underline"
disabled={
!confirm ||
reason.trim().length < 20 ||
!overview.can_authorize ||
overview.version_hash !==
item.version_hash
}
onClick={() =>
void act('authorize', item)
}
>
Autorizar esta aceitação
</button>
</li>
))}
</ul>
<ul>
{overview.grants.map((item) => (
<li key={item.interested_user_id}>
Autorização ao interessado #
{item.interested_user_id}
<button
type="button"
className="ml-3 underline"
disabled={
!confirm ||
reason.trim().length < 20
}
onClick={() =>
void act('revoke', item)
}
>
Revogar
</button>
</li>
))}
</ul>
</>
)}
</fieldset>
)}
{overview && overview.history.length > 0 && (
<details>
<summary>Histórico de acesso e decisões</summary>
<ul className="mt-3 space-y-2">
{overview.history.map((event) => (
<li key={event.id}>
{(
{
accept: 'Aceitação do NDA',
authorize: 'Autorização do vendedor',
revoke: 'Autorização revogada',
withdraw: 'Aceitação retirada',
} as Record<string, string>
)[event.action] ?? 'Decisão registada'}{' '}
· {new Date(event.at).toLocaleString('pt-MZ')} ·
versão {event.version_hash.slice(0, 12)}
</li>
))}
</ul>
</details>
)}
{business && (
<dl className="grid gap-2 sm:grid-cols-2">
{Object.entries(business).map(([key, value]) => (
<div key={key}>
<dt className="font-semibold">
{labels[key] ?? key}
</dt>
<dd className="break-words">
{value ?? 'Não indicado'}
</dd>
</div>
))}
</dl>
)}
<p role="status">{message}</p>
</section>
);
}