52 lines
3.6 KiB
TypeScript
52 lines
3.6 KiB
TypeScript
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
import { opportunityErrorGroup, opportunityMissingGroups, type FillGroup } from '@/lib/opportunity-fill-flow';
|
|
|
|
export default function OpportunityFillFlow({groups,errors,renderField,onOpenResources,missingPaths=[]}:{
|
|
groups:FillGroup[];errors:Record<string,string>;renderField:(field:string)=>ReactNode;onOpenResources?:(group:string)=>void;
|
|
missingPaths?:string[];
|
|
}) {
|
|
const [active,setActive]=useState(groups[0]?.id??'');
|
|
const groupIds=groups.map(group=>group.id).join(',');
|
|
useEffect(()=>{
|
|
const saved=window.location.hash.replace('#fill-','');
|
|
if(groupIds.split(',').includes(saved))setActive(saved);
|
|
},[groupIds]);
|
|
const heading=useRef<HTMLHeadingElement|null>(null);
|
|
const errorGroup=opportunityErrorGroup(groups,errors);
|
|
const missing=opportunityMissingGroups(groups,missingPaths);
|
|
const errorKey=JSON.stringify(errors);
|
|
useEffect(()=>{if(errorGroup)setActive(errorGroup);},[errorGroup,errorKey]);
|
|
const index=Math.max(0,groups.findIndex(group=>group.id===active));
|
|
const current=groups[index];
|
|
if(!current)return null;
|
|
const go=(id:string)=>{
|
|
setActive(id);
|
|
window.history.replaceState(window.history.state,'',`#fill-${id}`);
|
|
requestAnimationFrame(()=>heading.current?.focus());
|
|
};
|
|
return <div className="space-y-4">
|
|
<p>Pode escolher qualquer grupo, guardar incompleto e retomar. As informações já preenchidas mantêm-se ao avançar e regressar.</p>
|
|
<nav aria-label="Preencher a oportunidade" className="flex flex-wrap gap-2">
|
|
{groups.map(group=><a key={group.id} href={`#fill-${group.id}`} aria-current={current.id===group.id?'step':undefined}
|
|
className={`rounded border px-3 py-2 text-left ${current.id===group.id?'bg-primary text-primary-foreground':'bg-background'}`}
|
|
onClick={event=>{event.preventDefault();go(group.id);}}>{group.title}{opportunityErrorGroup([group],errors)?' — verificar':missing.some(item=>item.id===group.id)?' — dados em falta':''}</a>)}
|
|
</nav>
|
|
{missing.length>0&&<p>Pendências da última gravação: {missing.map(group=>group.title).join(' · ')}. Guarde para actualizar esta indicação.</p>}
|
|
<h2 ref={heading} tabIndex={-1} className="text-xl font-semibold">{current.title}</h2>
|
|
<p>{current.hint}</p>
|
|
{groups.map(group=><section key={group.id} id={`fill-${group.id}`} hidden={current.id!==group.id}
|
|
aria-label={group.title} className="space-y-4">
|
|
{group.fields.map(field=><div key={field}>{renderField(field)}</div>)}
|
|
{onOpenResources&&group.fields.some(field=>['locations','documents','images'].includes(field))&&
|
|
<button type="button" className="rounded border px-4 py-2 font-medium" onClick={()=>onOpenResources(group.id)}>Guardar e abrir documentos, imagens e localizações</button>}
|
|
</section>)}
|
|
<div className="flex flex-wrap items-center justify-between gap-3 border-t pt-3">
|
|
<span className="text-sm">Grupo {index+1} de {groups.length} · navegação livre</span>
|
|
<div className="flex gap-3">
|
|
{index>0&&<a href={`#fill-${groups[index-1].id}`} className="rounded border px-4 py-2" onClick={event=>{event.preventDefault();go(groups[index-1].id);}}>Anterior</a>}
|
|
{index<groups.length-1&&<a href={`#fill-${groups[index+1].id}`} className="rounded border px-4 py-2" onClick={event=>{event.preventDefault();go(groups[index+1].id);}}>Continuar: {groups[index+1].title}</a>}
|
|
</div>
|
|
</div>
|
|
</div>;
|
|
}
|