feat: explore the public opportunity catalogue by corridor
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-23 12:08:33 +02:00
parent 165c2f6171
commit 203492997e
9 changed files with 201 additions and 7 deletions
@@ -0,0 +1,51 @@
import { Link } from '@inertiajs/react';
import { useState } from 'react';
import { button, card, input } from '@/pages/investment/shared';
type Item = {id:number;title:string;slug:string;summary:string|null;province:string|null;district:string|null;chains:string[];stages:string[];maturity_label:string|null};
export type CorridorGroup = {id:string;name:string;count:number;locations:string[];opportunities:Item[]};
type Props = {groups:CorridorGroup[];selected:number[];onSelect:(id:number,checked:boolean)=>void;onCompare:(ids:number[])=>void;onClear:()=>void};
function Group({group,selected,onSelect}: {group:CorridorGroup} & Pick<Props,'selected'|'onSelect'>) {
const [province,setProvince]=useState('');
const [district,setDistrict]=useState('');
const [chain,setChain]=useState('');
const provinces=[...new Set(group.opportunities.map(r=>r.province).filter((s):s is string=>!!s))].sort();
const districts=[...new Set(group.opportunities.filter(r=>!province||r.province===province).map(r=>r.district).filter((s):s is string=>!!s))].sort();
const chains=[...new Set(group.opportunities.flatMap(r=>r.chains))].sort();
const rows=group.opportunities.filter(r=>(!province||r.province===province)&&(!district||r.district===district)&&(!chain||r.chains.includes(chain)));
return <section className={card+' space-y-4'} aria-label={group.name}>
<header><h2 className="text-2xl font-bold">{group.name}</h2><p>{group.count} oportunidades publicadas</p>
<p>{group.locations.length ? 'Localizações identificadas: '+group.locations.join(' · ') : 'Abrangência territorial por indicar.'}</p></header>
<div className="grid gap-3 md:grid-cols-3">
<label>Província<select className={input} value={province} onChange={e=>{setProvince(e.target.value);setDistrict('');}}><option value="">Todas as províncias</option>{provinces.map(p=><option key={p}>{p}</option>)}</select></label>
<label>Distrito<select className={input} value={district} onChange={e=>setDistrict(e.target.value)}><option value="">Todos os distritos</option>{districts.map(d=><option key={d}>{d}</option>)}</select></label>
<label>Cadeia de valor<select className={input} value={chain} onChange={e=>setChain(e.target.value)}><option value="">Todas as cadeias</option>{chains.map(c=><option key={c}>{c}</option>)}</select></label>
</div>
<p role="status">{rows.length} oportunidades com estes filtros</p>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">{rows.map(r=><article key={r.id} className="space-y-3 rounded-lg border p-4">
<h3 className="text-lg font-semibold"><Link href={'/oportunidades/'+r.slug}>{r.title}</Link></h3>
<p>{[r.province,r.district].filter(Boolean).join(' / ')||'Localização por escolher'}</p>
<p>Cadeia de valor: {r.chains.join(' · ')||'Por indicar'}</p>
{r.summary && !r.stages.includes(r.summary) && <p>{r.summary}</p>}
<p>Etapas e actividades: {r.stages.join(' · ')||'Por indicar'}</p><p>{r.maturity_label}</p>
<label className="flex gap-2"><input type="checkbox" checked={selected.includes(r.id)} onChange={e=>onSelect(r.id,e.target.checked)}/>Escolher para preparar investimento</label>
<Link className="underline" href={'/oportunidades/'+r.slug}>Ver ficha</Link>
</article>)}</div>
</section>;
}
export default function OpportunityCorridorGroups({groups,selected,onSelect,onCompare,onClear}:Props) {
return <div className="space-y-6">
<section className={card+' space-y-3'} aria-label="Escolhas entre corredores">
<h2 className="text-xl font-bold">Explore por corredor e escolha onde investir</h2>
<p>Pode escolher oportunidades de vários corredores e confirmar a localização de cada uma durante a preparação.</p>
<p>{selected.length} oportunidades escolhidas</p>
{!!selected.length&&<div className="flex flex-wrap gap-4"><Link className={button} href={'/investimentos/iniciar?'+selected.map(id=>'opportunities[]='+id).join('&')}>Preparar investimento com as oportunidades escolhidas</Link>
<button className={button} disabled={selected.length>4} onClick={()=>onCompare(selected)}>Comparar as escolhidas</button>
<button className="underline" onClick={onClear}>Limpar escolha</button>
{selected.length>4&&<p>A comparação apresenta até quatro fichas de cada vez. Pode preparar o investimento com todas as escolhidas.</p>}</div>}
</section>
{groups.map(group=><Group key={group.id} group={group} selected={selected} onSelect={onSelect}/>)}
</div>;
}