257 lines
11 KiB
TypeScript
257 lines
11 KiB
TypeScript
import { Link, router } from '@inertiajs/react';
|
|
import { useState } from 'react';
|
|
import { Management, Pagination, button, control, states } from './shared';
|
|
import type { Paginated } from './shared';
|
|
import CountrySelect, { type Country } from '@/components/country-select';
|
|
|
|
type Interest = {
|
|
id: number;
|
|
reference: string;
|
|
full_name: string;
|
|
email: string;
|
|
sector_interest: string;
|
|
existing_business_id: string | null;
|
|
modality: string | null;
|
|
assignee: { id: number; name: string } | null;
|
|
next_action: string | null;
|
|
next_action_due_at: string | null;
|
|
status: string;
|
|
countryName: string | null;
|
|
declaredOrigin: { code: string | null; name: string };
|
|
};
|
|
export default function Interests({
|
|
requests,
|
|
filters,
|
|
countries,
|
|
originStatistics,
|
|
}: {
|
|
requests: Paginated<Interest>;
|
|
filters: {
|
|
q?: string;
|
|
status?: string;
|
|
follow_up?: string;
|
|
country?: string;
|
|
origin?: string;
|
|
};
|
|
countries: Country[];
|
|
originStatistics: Record<
|
|
string,
|
|
{ code: string | null; name: string; count: number }[]
|
|
>;
|
|
}) {
|
|
const [q, setQ] = useState(filters.q ?? '');
|
|
const [status, setStatus] = useState(filters.status ?? '');
|
|
const [followUp, setFollowUp] = useState(filters.follow_up ?? '');
|
|
const [country, setCountry] = useState(filters.country ?? '');
|
|
const [origin, setOrigin] = useState(filters.origin ?? '');
|
|
return (
|
|
<Management title="Manifestações de interesse">
|
|
<form
|
|
className="flex flex-wrap gap-3"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
router.get('/management/interests', {
|
|
q,
|
|
status,
|
|
follow_up: followUp,
|
|
country,
|
|
origin,
|
|
});
|
|
}}
|
|
>
|
|
<input
|
|
aria-label="Pesquisar pedidos"
|
|
className={`${control} max-w-sm`}
|
|
placeholder="Referência, nome ou email"
|
|
value={q}
|
|
onChange={(e) => setQ(e.target.value)}
|
|
/>
|
|
<select
|
|
aria-label="Estado"
|
|
className={`${control} max-w-xs`}
|
|
value={status}
|
|
onChange={(e) => setStatus(e.target.value)}
|
|
>
|
|
<option value="">Todos os estados</option>
|
|
{Object.keys(states)
|
|
.filter((key) =>
|
|
[
|
|
'received',
|
|
'submitted',
|
|
'in_review',
|
|
'reviewed',
|
|
'qualification',
|
|
'qualified',
|
|
'contacted',
|
|
'contact_accepted',
|
|
'referred',
|
|
'due_diligence',
|
|
'negotiation',
|
|
'pending',
|
|
'refused',
|
|
'converted',
|
|
'cancelled',
|
|
'closed',
|
|
].includes(key),
|
|
)
|
|
.map((key) => (
|
|
<option key={key} value={key}>
|
|
{states[key]}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<select
|
|
aria-label="Acompanhamento"
|
|
className={`${control} max-w-xs`}
|
|
value={followUp}
|
|
onChange={(e) => setFollowUp(e.target.value)}
|
|
>
|
|
<option value="">Todos os acompanhamentos</option>
|
|
<option value="overdue">Acções atrasadas</option>
|
|
<option value="upcoming">Acções por vencer</option>
|
|
<option value="unassigned">Sem responsável</option>
|
|
</select>
|
|
<CountrySelect
|
|
countries={countries}
|
|
value={country}
|
|
onChange={setCountry}
|
|
label="Filtrar país do requerente"
|
|
/>
|
|
<label>
|
|
Origem declarada
|
|
<select
|
|
aria-label="Filtrar origem declarada"
|
|
className={control}
|
|
value={origin}
|
|
onChange={(e) => setOrigin(e.target.value)}
|
|
>
|
|
<option value="">Todas as origens</option>
|
|
<option value="unknown">Não informada</option>
|
|
{countries.map((c) => (
|
|
<option key={c.code} value={c.code}>
|
|
{c.name} ({c.code})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<button className={button}>Pesquisar</button>
|
|
</form>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-left text-sm">
|
|
<thead>
|
|
<tr>
|
|
{[
|
|
'Referência',
|
|
'Nome',
|
|
'Email',
|
|
'País do requerente',
|
|
'Origem declarada',
|
|
'Sector',
|
|
'Modalidade',
|
|
'Responsável',
|
|
'Próxima acção',
|
|
'Prazo',
|
|
'Estado',
|
|
].map((label) => (
|
|
<th key={label} className="border-b p-3">
|
|
{label}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{requests.data.map((item) => (
|
|
<tr key={item.id}>
|
|
<td className="border-b p-3">
|
|
<Link
|
|
className="underline"
|
|
href={`/management/interests/${item.id}`}
|
|
>
|
|
{item.reference}
|
|
</Link>
|
|
</td>
|
|
<td className="border-b p-3">
|
|
{item.full_name}
|
|
</td>
|
|
<td className="border-b p-3">{item.email}</td>
|
|
<td className="border-b p-3">
|
|
{item.countryName ?? 'Não informado'}
|
|
</td>
|
|
<td className="border-b p-3">
|
|
{item.declaredOrigin.name}
|
|
</td>
|
|
<td className="border-b p-3">
|
|
{item.sector_interest}
|
|
</td>
|
|
<td className="border-b p-3">
|
|
{item.modality ??
|
|
(item.existing_business_id
|
|
? 'Negócio'
|
|
: '—')}
|
|
</td>
|
|
<td className="border-b p-3">
|
|
{item.assignee?.name ?? '—'}
|
|
</td>
|
|
<td className="border-b p-3">
|
|
{item.next_action ?? '—'}
|
|
</td>
|
|
<td
|
|
className={`border-b p-3 ${item.next_action_due_at && new Date(item.next_action_due_at) < new Date() ? 'font-semibold text-red-700' : ''}`}
|
|
>
|
|
{item.next_action_due_at
|
|
? `${new Date(item.next_action_due_at) < new Date() ? 'Atrasada · ' : ''}${new Intl.DateTimeFormat('pt-MZ', { dateStyle: 'medium', timeZone: 'Africa/Maputo' }).format(new Date(item.next_action_due_at))}`
|
|
: '—'}
|
|
</td>
|
|
<td className="border-b p-3">
|
|
{states[item.status] ?? item.status}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{!requests.data.length && <p>Nenhum pedido encontrado.</p>}
|
|
<Pagination page={requests} />
|
|
<details>
|
|
<summary>
|
|
Estatísticas de solicitações por origem declarada
|
|
</summary>
|
|
<p>
|
|
Contagens por conjunto e filtro de origem; o filtro do país
|
|
do requerente aplica-se à lista. Rascunhos de negócios e do
|
|
Balcão são excluídos. Apoio fundiário recebido integra
|
|
manifestações; não se soma como outro conjunto nem se contam
|
|
investidores únicos.
|
|
</p>
|
|
{Object.entries(originStatistics).map(([group, counts]) => (
|
|
<section key={group}>
|
|
<h2>
|
|
{
|
|
(
|
|
{
|
|
interests:
|
|
'Manifestações recebidas, incluindo apoio fundiário',
|
|
desk: 'Pedidos do Balcão',
|
|
businesses: 'Negócios submetidos',
|
|
} as Record<string, string>
|
|
)[group]
|
|
}
|
|
</h2>
|
|
{counts.length ? (
|
|
<ul>
|
|
{counts.map((row) => (
|
|
<li key={row.code ?? 'unknown'}>
|
|
{row.name}: {row.count}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
) : (
|
|
<p>Sem solicitações neste conjunto.</p>
|
|
)}
|
|
</section>
|
|
))}
|
|
</details>
|
|
</Management>
|
|
);
|
|
}
|