150 lines
5.8 KiB
TypeScript
150 lines
5.8 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>
|
|
);
|
|
}
|