280 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|