206 lines
5.8 KiB
TypeScript
206 lines
5.8 KiB
TypeScript
"use client";
|
|
|
|
import type { Ajuda, Anexo, Pergunta, Resposta } from "@prisma/client";
|
|
import { useEffect, useRef, useState, useTransition } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { FormProvider, useForm } from "react-hook-form";
|
|
import { PerguntaField } from "@/components/form/PerguntaField";
|
|
import { Button } from "@/components/ui/button";
|
|
import { salvarRespostas } from "./actions";
|
|
|
|
type PerguntaComDados = Pergunta & {
|
|
ajuda: Ajuda | null;
|
|
anexos?: Anexo[];
|
|
respostas: Resposta[];
|
|
};
|
|
|
|
type Props = {
|
|
briefingId: string;
|
|
secaoId: string;
|
|
perguntas: PerguntaComDados[];
|
|
readOnly?: boolean;
|
|
};
|
|
|
|
type FormValues = {
|
|
perguntas: Record<
|
|
string,
|
|
{
|
|
texto?: string;
|
|
selecionadas?: string[];
|
|
complemento?: string;
|
|
}
|
|
>;
|
|
};
|
|
|
|
function getDefaultValues(perguntas: PerguntaComDados[]): FormValues {
|
|
return {
|
|
perguntas: perguntas.reduce<FormValues["perguntas"]>((acc, pergunta) => {
|
|
const resposta = pergunta.respostas[0];
|
|
const selecionadas = Array.isArray(resposta?.selecionadas)
|
|
? resposta.selecionadas.filter((item): item is string => typeof item === "string")
|
|
: [];
|
|
|
|
acc[pergunta.id] = {
|
|
texto: resposta?.texto ?? "",
|
|
selecionadas,
|
|
complemento: resposta?.complemento ?? ""
|
|
};
|
|
|
|
return acc;
|
|
}, {})
|
|
};
|
|
}
|
|
|
|
type SaveStatus = "idle" | "dirty" | "saving" | "saved" | "error";
|
|
|
|
function formatTime(value: string) {
|
|
return new Intl.DateTimeFormat("pt-BR", {
|
|
hour: "2-digit",
|
|
minute: "2-digit"
|
|
}).format(new Date(value));
|
|
}
|
|
|
|
export function SecaoForm({ briefingId, secaoId, perguntas, readOnly = false }: Props) {
|
|
const [status, setStatus] = useState<SaveStatus>("idle");
|
|
const [savedAt, setSavedAt] = useState<string | null>(null);
|
|
const [message, setMessage] = useState<string | null>(null);
|
|
const [, startTransition] = useTransition();
|
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
const router = useRouter();
|
|
const form = useForm<FormValues>({
|
|
defaultValues: getDefaultValues(perguntas)
|
|
});
|
|
const watchedValues = form.watch();
|
|
|
|
async function saveNow(values: FormValues) {
|
|
if (readOnly) {
|
|
return true;
|
|
}
|
|
|
|
setStatus("saving");
|
|
setMessage(null);
|
|
|
|
const result = await salvarRespostas(briefingId, secaoId, values);
|
|
|
|
startTransition(() => {
|
|
if (!result.ok) {
|
|
setStatus("error");
|
|
setMessage(result.message ?? "Erro ao salvar. Tente novamente.");
|
|
return;
|
|
}
|
|
|
|
form.reset(values);
|
|
setStatus("saved");
|
|
setSavedAt(result.savedAt ?? new Date().toISOString());
|
|
});
|
|
|
|
return result.ok;
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (readOnly || !form.formState.isDirty) {
|
|
return;
|
|
}
|
|
|
|
setStatus("dirty");
|
|
|
|
if (debounceRef.current) {
|
|
clearTimeout(debounceRef.current);
|
|
}
|
|
|
|
debounceRef.current = setTimeout(() => {
|
|
void saveNow(form.getValues());
|
|
}, 800);
|
|
|
|
return () => {
|
|
if (debounceRef.current) {
|
|
clearTimeout(debounceRef.current);
|
|
}
|
|
};
|
|
}, [watchedValues, form, readOnly]);
|
|
|
|
useEffect(() => {
|
|
function handleBeforeUnload(event: BeforeUnloadEvent) {
|
|
if (form.formState.isDirty && status !== "saved") {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
}, [form.formState.isDirty, status]);
|
|
|
|
useEffect(() => {
|
|
async function handleClick(event: MouseEvent) {
|
|
const link = (event.target as HTMLElement).closest("a");
|
|
|
|
if (!link || readOnly || !form.formState.isDirty) {
|
|
return;
|
|
}
|
|
|
|
const href = link.getAttribute("href");
|
|
|
|
if (!href || !href.startsWith(`/briefings/${briefingId}`)) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
if (debounceRef.current) {
|
|
clearTimeout(debounceRef.current);
|
|
}
|
|
|
|
const saved = await saveNow(form.getValues());
|
|
|
|
if (saved) {
|
|
router.push(href);
|
|
}
|
|
}
|
|
|
|
document.addEventListener("click", handleClick);
|
|
return () => document.removeEventListener("click", handleClick);
|
|
}, [briefingId, form, readOnly, router]);
|
|
|
|
return (
|
|
<FormProvider {...form}>
|
|
<form className="space-y-4">
|
|
<div className="sticky top-0 z-20 rounded-lg border bg-card p-3 text-sm shadow-sm">
|
|
{readOnly ? (
|
|
<span className="text-muted-foreground">Briefing enviado. Respostas em modo leitura.</span>
|
|
) : status === "saving" ? (
|
|
<span className="text-muted-foreground">Salvando...</span>
|
|
) : status === "saved" && savedAt ? (
|
|
<span className="text-muted-foreground">Salvo as {formatTime(savedAt)}</span>
|
|
) : status === "error" ? (
|
|
<span className="text-destructive">{message ?? "Erro ao salvar — tentar de novo"}</span>
|
|
) : status === "dirty" ? (
|
|
<span className="text-muted-foreground">Alteracoes pendentes...</span>
|
|
) : (
|
|
<span className="text-muted-foreground">Rascunho pronto para edicao.</span>
|
|
)}
|
|
</div>
|
|
{perguntas.map((pergunta, index) => (
|
|
<fieldset key={pergunta.id} disabled={readOnly} className="disabled:opacity-75">
|
|
<PerguntaField
|
|
briefingId={briefingId}
|
|
pergunta={pergunta}
|
|
index={index}
|
|
readOnly={readOnly}
|
|
/>
|
|
</fieldset>
|
|
))}
|
|
{!readOnly ? (
|
|
<div className="flex flex-col gap-3 sm:flex-row">
|
|
<Button type="button" variant="outline" onClick={() => form.reset()}>
|
|
Descartar alteracoes locais
|
|
</Button>
|
|
<Button type="button" onClick={() => saveNow(form.getValues())}>
|
|
Salvar agora
|
|
</Button>
|
|
</div>
|
|
) : null}
|
|
</form>
|
|
</FormProvider>
|
|
);
|
|
}
|