370 lines
14 KiB
TypeScript
370 lines
14 KiB
TypeScript
import { Link, useForm } from '@inertiajs/react';
|
|
import { Management, Field, button, control, states } from './shared';
|
|
|
|
type Interest = {
|
|
id: number;
|
|
reference: string;
|
|
full_name: string;
|
|
email: string;
|
|
phone: string | null;
|
|
investor_type: string;
|
|
country: string;
|
|
sector_interest: string;
|
|
opportunity_reference: string | null;
|
|
investment_range: string | null;
|
|
message: string;
|
|
status: string;
|
|
existing_business_id: string | null;
|
|
modality: string | null;
|
|
assigned_to: number | null;
|
|
next_action: string | null;
|
|
next_action_due_at: string | null;
|
|
consented_at: string | null;
|
|
reviewed_at: string | null;
|
|
reviewed_by: number | null;
|
|
created_at: string;
|
|
source_page: string | null;
|
|
metadata?: {
|
|
submission_id?: string;
|
|
journey_id?: string;
|
|
dossier_id?: string;
|
|
role?: string;
|
|
horizon?: string;
|
|
};
|
|
};
|
|
function dateTime(value: string | null) {
|
|
if (!value) return '—';
|
|
return new Intl.DateTimeFormat('pt-MZ', {
|
|
dateStyle: 'medium',
|
|
timeStyle: 'short',
|
|
timeZone: 'Africa/Maputo',
|
|
}).format(new Date(value));
|
|
}
|
|
type Transition = {
|
|
from_status: string | null;
|
|
to_status: string;
|
|
actor_id: number | null;
|
|
note: string | null;
|
|
created_at: string;
|
|
};
|
|
type Assignee = { id: number; name: string };
|
|
type Consent = {
|
|
purpose: string;
|
|
policy_version: string | null;
|
|
status: string;
|
|
granted_at: string | null;
|
|
withdrawn_at: string | null;
|
|
};
|
|
|
|
export default function InterestDetail({
|
|
interest,
|
|
history = [],
|
|
assignees = [],
|
|
consents = [],
|
|
declaredOrigin,
|
|
}: {
|
|
interest: Interest;
|
|
history?: Transition[];
|
|
assignees?: Assignee[];
|
|
consents?: Consent[];
|
|
declaredOrigin: { code: string | null; name: string };
|
|
}) {
|
|
const form = useForm({
|
|
status: interest.status,
|
|
note: '',
|
|
next_action: interest.next_action ?? '',
|
|
next_action_due_at: interest.next_action_due_at?.slice(0, 10) ?? '',
|
|
assigned_to: interest.assigned_to?.toString() ?? '',
|
|
});
|
|
const contactConsentForm = useForm({ reason: '' });
|
|
const sharingConsentForm = useForm({ reason: '' });
|
|
const rows: [string, string | number | null][] = [
|
|
['Nome', interest.full_name],
|
|
['Email', interest.email],
|
|
['Telefone', interest.phone],
|
|
['Tipo de investidor', interest.investor_type],
|
|
['País', interest.country],
|
|
['Página de entrada registada', interest.source_page],
|
|
[
|
|
'Origem declarada da solicitação',
|
|
declaredOrigin.code
|
|
? `${declaredOrigin.name} (${declaredOrigin.code})`
|
|
: declaredOrigin.name,
|
|
],
|
|
['Sector', interest.sector_interest],
|
|
['Oportunidade', interest.opportunity_reference],
|
|
['Negócio publicado', interest.existing_business_id],
|
|
['Modalidade', interest.modality],
|
|
['Faixa de investimento', interest.investment_range],
|
|
['Recebido em (Maputo)', dateTime(interest.created_at)],
|
|
['Consentimento em (Maputo)', dateTime(interest.consented_at)],
|
|
['Revisto em (Maputo)', dateTime(interest.reviewed_at)],
|
|
['Revisto pelo utilizador', interest.reviewed_by],
|
|
];
|
|
return (
|
|
<Management title={interest.reference}>
|
|
{interest.metadata?.submission_id && (
|
|
<Link
|
|
className="underline"
|
|
href={`/investidor/manifestacoes/${interest.metadata.submission_id}`}
|
|
>
|
|
Consultar manifestação completa, necessidades e anexos
|
|
privados
|
|
</Link>
|
|
)}
|
|
{interest.metadata?.dossier_id && (
|
|
<section className="space-y-2 rounded border p-4">
|
|
<h2 className="font-semibold">Contexto de investimento</h2>
|
|
<Link
|
|
className="underline"
|
|
href={`/management/portfolio/${interest.metadata.dossier_id}`}
|
|
>
|
|
Consultar corredor, cluster ou projecto de origem
|
|
</Link>
|
|
<p>Jornada: {interest.metadata.journey_id}</p>
|
|
<p>
|
|
Papel: {interest.metadata.role} · Horizonte:{' '}
|
|
{interest.metadata.horizon}
|
|
</p>
|
|
</section>
|
|
)}
|
|
<Link
|
|
className={button}
|
|
href={`/management/crm/convert/${interest.id}`}
|
|
>
|
|
Encaminhar para o CRM
|
|
</Link>
|
|
<dl className="grid gap-4 sm:grid-cols-2">
|
|
{rows.map(([label, value]) => (
|
|
<div key={label}>
|
|
<dt className="text-muted-foreground text-sm">
|
|
{label}
|
|
</dt>
|
|
<dd className="break-words">{value ?? '—'}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
<section>
|
|
<h2 className="font-semibold">Mensagem</h2>
|
|
<p className="mt-2 break-words whitespace-pre-wrap">
|
|
{interest.message}
|
|
</p>
|
|
</section>
|
|
<section className="space-y-3">
|
|
<h2 className="font-semibold">Consentimentos e versões</h2>
|
|
{consents.map((consent, index) => (
|
|
<article
|
|
key={`${consent.purpose}-${index}`}
|
|
className="rounded border p-3"
|
|
>
|
|
<p>
|
|
{consent.purpose}: {consent.status}
|
|
</p>
|
|
<p className="text-sm">
|
|
Política {consent.policy_version ?? '—'} · concedido{' '}
|
|
{dateTime(consent.granted_at)}
|
|
</p>
|
|
{consent.withdrawn_at && (
|
|
<p className="text-sm">
|
|
Revogado {dateTime(consent.withdrawn_at)}
|
|
</p>
|
|
)}
|
|
</article>
|
|
))}
|
|
{consents.some(
|
|
(consent) =>
|
|
consent.purpose === 'respond_to_interest' &&
|
|
consent.status === 'granted',
|
|
) && (
|
|
<form
|
|
className="max-w-sm space-y-3"
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
contactConsentForm.patch(
|
|
`/management/interests/${interest.id}/consent/contact`,
|
|
);
|
|
}}
|
|
>
|
|
<Field
|
|
label="Motivo do pedido de revogação"
|
|
error={contactConsentForm.errors.reason}
|
|
>
|
|
<textarea
|
|
className={control}
|
|
minLength={10}
|
|
maxLength={2000}
|
|
required
|
|
rows={3}
|
|
value={contactConsentForm.data.reason}
|
|
onChange={(event) =>
|
|
contactConsentForm.setData(
|
|
'reason',
|
|
event.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</Field>
|
|
<button
|
|
className={button}
|
|
disabled={contactConsentForm.processing}
|
|
>
|
|
Revogar autorização de contacto
|
|
</button>
|
|
</form>
|
|
)}
|
|
{consents.some(
|
|
(consent) =>
|
|
consent.purpose === 'share_contact_with_proponent' &&
|
|
consent.status === 'granted',
|
|
) && (
|
|
<form
|
|
className="max-w-sm space-y-3"
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
sharingConsentForm.patch(
|
|
`/management/interests/${interest.id}/consent/proponent`,
|
|
);
|
|
}}
|
|
>
|
|
<Field
|
|
label="Motivo do pedido de revogação"
|
|
error={sharingConsentForm.errors.reason}
|
|
>
|
|
<textarea
|
|
className={control}
|
|
minLength={10}
|
|
maxLength={2000}
|
|
required
|
|
rows={3}
|
|
value={sharingConsentForm.data.reason}
|
|
onChange={(event) =>
|
|
sharingConsentForm.setData(
|
|
'reason',
|
|
event.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</Field>
|
|
<button
|
|
className={button}
|
|
disabled={sharingConsentForm.processing}
|
|
>
|
|
Revogar autorização de partilha
|
|
</button>
|
|
</form>
|
|
)}
|
|
</section>
|
|
<form
|
|
className="max-w-sm space-y-4"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
form.patch(`/management/interests/${interest.id}`);
|
|
}}
|
|
>
|
|
<Field label="Estado do pedido" error={form.errors.status}>
|
|
<select
|
|
className={control}
|
|
value={form.data.status}
|
|
onChange={(e) => form.setData('status', e.target.value)}
|
|
>
|
|
{[
|
|
'received',
|
|
'submitted',
|
|
'in_review',
|
|
'reviewed',
|
|
'qualification',
|
|
'qualified',
|
|
'contacted',
|
|
'contact_accepted',
|
|
'referred',
|
|
'due_diligence',
|
|
'negotiation',
|
|
'pending',
|
|
'refused',
|
|
'converted',
|
|
'cancelled',
|
|
'closed',
|
|
].map((key) => (
|
|
<option key={key} value={key}>
|
|
{states[key]}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
<Field label="Nota de acompanhamento" error={form.errors.note}>
|
|
<textarea
|
|
className={control}
|
|
rows={3}
|
|
value={form.data.note}
|
|
onChange={(e) => form.setData('note', e.target.value)}
|
|
/>
|
|
</Field>
|
|
<Field label="Próxima acção" error={form.errors.next_action}>
|
|
<input
|
|
className={control}
|
|
value={form.data.next_action}
|
|
onChange={(e) =>
|
|
form.setData('next_action', e.target.value)
|
|
}
|
|
/>
|
|
</Field>
|
|
<Field
|
|
label="Prazo da próxima acção"
|
|
error={form.errors.next_action_due_at}
|
|
>
|
|
<input
|
|
type="date"
|
|
className={control}
|
|
value={form.data.next_action_due_at}
|
|
onChange={(e) =>
|
|
form.setData('next_action_due_at', e.target.value)
|
|
}
|
|
/>
|
|
</Field>
|
|
<Field label="Responsável" error={form.errors.assigned_to}>
|
|
<select
|
|
className={control}
|
|
value={form.data.assigned_to}
|
|
onChange={(e) =>
|
|
form.setData('assigned_to', e.target.value)
|
|
}
|
|
>
|
|
<option value="">Sem responsável</option>
|
|
{assignees.map((assignee) => (
|
|
<option key={assignee.id} value={assignee.id}>
|
|
{assignee.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
<button className={button} disabled={form.processing}>
|
|
{form.processing ? 'A guardar…' : 'Guardar estado'}
|
|
</button>
|
|
</form>
|
|
<section className="space-y-3">
|
|
<h2 className="font-semibold">Histórico do fluxo</h2>
|
|
{!history.length && <p>Sem alterações de estado registadas.</p>}
|
|
{history.map((entry, index) => (
|
|
<article
|
|
key={`${entry.created_at}-${index}`}
|
|
className="rounded border p-3"
|
|
>
|
|
<p>
|
|
{entry.from_status
|
|
? `${states[entry.from_status] ?? entry.from_status} → `
|
|
: ''}
|
|
{states[entry.to_status] ?? entry.to_status} ·{' '}
|
|
{dateTime(entry.created_at)}
|
|
</p>
|
|
{entry.note && (
|
|
<p className="mt-1 whitespace-pre-wrap">
|
|
{entry.note}
|
|
</p>
|
|
)}
|
|
</article>
|
|
))}
|
|
</section>
|
|
</Management>
|
|
);
|
|
}
|