This commit is contained in:
@@ -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>;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import EducationalGuides from '@/components/educational-guides';
|
||||
import OpportunityCorridorGroups, { type CorridorGroup } from '@/components/opportunity-corridor-groups';
|
||||
import { searchTerritoryHref } from '@/lib/search-territory';
|
||||
import type { EducationalArticle } from '@/components/educational-article-card';
|
||||
import OpportunitySearchFilters from '@/components/opportunity-search-filters';
|
||||
@@ -25,8 +26,10 @@ export default function Catalogue({
|
||||
territorial,
|
||||
territorySelection,
|
||||
catalogueUrl,
|
||||
corridorGroups = [],
|
||||
}: {
|
||||
guides?: EducationalArticle[];
|
||||
corridorGroups?: CorridorGroup[];
|
||||
filters: Record<string, string | string[] | boolean>;
|
||||
references: References;
|
||||
results: {
|
||||
@@ -123,6 +126,10 @@ export default function Catalogue({
|
||||
const [saved, setSaved] = useState(favourites);
|
||||
const [selected, setSelected] = useState<number[]>([]);
|
||||
const [comparison, setComparison] = useState<{ record: Opportunity }[]>([]);
|
||||
const comparisonRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (view === 'corridors' && comparison.length) comparisonRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}, [comparison, view]);
|
||||
const [points, setPoints] = useState<Feature[] | null>(null);
|
||||
const [areas, setAreas] = useState<InvestmentMapArea[]>([]);
|
||||
const [unlocated, setUnlocated] = useState<
|
||||
@@ -418,7 +425,7 @@ export default function Catalogue({
|
||||
Alargar pesquisa (remover localização)
|
||||
</button>
|
||||
)}
|
||||
{['grid', 'list', 'map', 'combined'].map((value, i) => (
|
||||
{['grid', 'list', 'map', 'combined', 'corridors'].map((value, i) => (
|
||||
<button
|
||||
key={value}
|
||||
disabled={searchLoading || !!searchError}
|
||||
@@ -434,7 +441,7 @@ export default function Catalogue({
|
||||
);
|
||||
}}
|
||||
>
|
||||
{['Grelha', 'Lista', 'Mapa', 'Lista e mapa'][i]}
|
||||
{['Grelha', 'Lista', 'Mapa', 'Lista e mapa', 'Por corredor'][i]}
|
||||
</button>
|
||||
))}
|
||||
<a
|
||||
@@ -568,7 +575,8 @@ export default function Catalogue({
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
{!searchLoading &&
|
||||
{view === 'corridors' && !searchLoading && !searchError && <OpportunityCorridorGroups groups={corridorGroups} selected={investmentSelection} onSelect={selectInvestment} onClear={()=>{setInvestmentSelection([]);sessionStorage.removeItem('investment-opportunity-selection');}} onCompare={ids=>void api('/api/v1/investment/compare?'+ids.map(id=>'ids[]='+id).join('&')).then(data=>setComparison(data.records)).catch(e=>setError(e.message))}/>}
|
||||
{view !== 'corridors' && !searchLoading &&
|
||||
!searchError &&
|
||||
(view !== 'map' || error || points?.length === 0) && (
|
||||
<div
|
||||
@@ -647,7 +655,7 @@ export default function Catalogue({
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
<nav aria-label="Paginação" className="flex gap-6">
|
||||
<nav aria-label="Paginação" className={view === 'corridors' ? 'hidden' : 'flex gap-6'}>
|
||||
{results.prev_page_url && (
|
||||
<Link href={results.prev_page_url}>Anterior</Link>
|
||||
)}
|
||||
@@ -670,9 +678,9 @@ export default function Catalogue({
|
||||
Comparar {selected.length} oportunidades
|
||||
</button>
|
||||
)}
|
||||
{!!investmentSelection.length && <section className={card}><p>{investmentSelection.length} oportunidades escolhidas, conservadas ao mudar de página.</p><Link className={button} href={'/investimentos/iniciar?' + investmentSelection.map(id => 'opportunities[]='+id).join('&')}>Preparar investimento com as oportunidades escolhidas</Link><button className="ml-4 underline" onClick={() => {setInvestmentSelection([]);sessionStorage.removeItem('investment-opportunity-selection');}}>Limpar escolha</button></section>}
|
||||
{view !== 'corridors' && !!investmentSelection.length && <section className={card}><p>{investmentSelection.length} oportunidades escolhidas, conservadas ao mudar de página.</p><Link className={button} href={'/investimentos/iniciar?' + investmentSelection.map(id => 'opportunities[]='+id).join('&')}>Preparar investimento com as oportunidades escolhidas</Link><button className="ml-4 underline" onClick={() => {setInvestmentSelection([]);sessionStorage.removeItem('investment-opportunity-selection');}}>Limpar escolha</button></section>}
|
||||
{comparison.length > 0 && (
|
||||
<div className="overflow-x-auto">
|
||||
<div ref={comparisonRef} className="overflow-x-auto">
|
||||
<table className="w-full border-collapse text-left">
|
||||
<caption className="p-3 text-left font-bold">
|
||||
Comparação — montantes nas moedas de origem
|
||||
|
||||
@@ -105,6 +105,7 @@ export default function LegacySector({
|
||||
? article.group.heading
|
||||
: article.title}
|
||||
</h1>
|
||||
{article.sector?.slug === 'agronegocio' && <Link className="inline-flex rounded-lg bg-emerald-800 px-5 py-3 font-semibold text-white" href="/oportunidades?sector=agronegocio&view=corridors">Explorar oportunidades por corredor</Link>}
|
||||
<p>
|
||||
Explore o setor e prepare a sua ideia com o contexto
|
||||
guardado no dossier.
|
||||
|
||||
Reference in New Issue
Block a user