Files
2026-11-02 08:08:05 +02:00

171 lines
6.7 KiB
TypeScript

import { router, usePage } from '@inertiajs/react';
import { useEffect, useRef, useState } from 'react';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
} from '@/components/ui/dialog';
export function WorkContextChange() {
const page = usePage();
const dirty = useRef(new Set<HTMLFormElement>());
const pending = useRef(false);
const savingForm = useRef<HTMLFormElement | null>(null);
const [open, setOpen] = useState(false);
const [message, setMessage] = useState('');
useEffect(() => {
dirty.current.clear();
}, [page.url]);
useEffect(() => {
const formChanged = (event: Event) => {
if (
!(event instanceof CustomEvent) ||
!(event.detail.form instanceof HTMLFormElement)
)
return;
if (event.detail.dirty) dirty.current.add(event.detail.form);
else dirty.current.delete(event.detail.form);
};
const changed = (event: Event) => {
const input = event.target;
if (
input instanceof HTMLInputElement ||
input instanceof HTMLTextAreaElement ||
input instanceof HTMLSelectElement
) {
if (input.form) dirty.current.add(input.form);
}
};
const click = (event: MouseEvent) => {
if (!(event.target instanceof Element)) return;
const link = event.target.closest('a');
if (
!link ||
new URL(link.href).pathname !== '/dashboard' ||
!dirty.current.size
)
return;
event.preventDefault();
event.stopPropagation();
setMessage('');
setOpen(true);
};
const unload = (event: BeforeUnloadEvent) => {
if (dirty.current.size) {
event.preventDefault();
event.returnValue = '';
}
};
const saved = (event: Event) => {
if (!(event instanceof CustomEvent)) return;
if (event.detail.success) dirty.current.delete(event.detail.form);
if (!pending.current || event.detail.form !== savingForm.current)
return;
if (event.detail.success) {
pending.current = false;
dirty.current.clear();
setOpen(false);
router.visit('/dashboard');
} else {
pending.current = false;
setOpen(true);
setMessage(
'A gravação falhou. O contexto actual foi mantido. Corrija os campos antes de mudar.',
);
}
};
const failure = () => {
if (pending.current) {
pending.current = false;
setOpen(true);
setMessage(
'Não foi possível concluir a gravação. O contexto actual foi mantido.',
);
}
};
const offException = router.on('httpException', failure);
const offNetwork = router.on('networkError', failure);
document.addEventListener('input', changed);
document.addEventListener('change', changed);
document.addEventListener('click', click, true);
window.addEventListener('beforeunload', unload);
window.addEventListener('sgc:context-save-result', saved);
window.addEventListener('sgc:context-dirty', formChanged);
return () => {
document.removeEventListener('input', changed);
document.removeEventListener('change', changed);
document.removeEventListener('click', click, true);
window.removeEventListener('beforeunload', unload);
window.removeEventListener('sgc:context-save-result', saved);
window.removeEventListener('sgc:context-dirty', formChanged);
offException();
offNetwork();
};
}, []);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogTitle>Alterações por guardar</DialogTitle>
<DialogDescription>
Guarde as alterações antes de escolher outro contexto, ou
descarte-as.
</DialogDescription>
{message && <p role="alert">{message}</p>}
<div className="flex flex-wrap gap-3">
<Button
onClick={() => {
const forms = [...dirty.current].filter(
(form) => form.isConnected,
);
if (forms.length !== 1) {
setMessage(
'Existem alterações em vários formulários. Cancele a mudança e guarde cada formulário antes de continuar.',
);
return;
}
if (!forms[0].hasAttribute('data-context-save')) {
setMessage(
'Cancele a mudança e conclua este formulário pela sua acção própria. A mudança de contexto não executa aprovações ou outras decisões.',
);
return;
}
if (!forms[0].reportValidity()) {
setOpen(false);
return;
}
pending.current = true;
savingForm.current = forms[0];
setOpen(false);
forms[0].requestSubmit();
}}
>
Guardar
</Button>
<Button
variant="outline"
onClick={() => {
dirty.current.clear();
pending.current = false;
setOpen(false);
router.visit('/dashboard');
}}
>
Descartar
</Button>
<Button
variant="outline"
onClick={() => {
pending.current = false;
setOpen(false);
}}
>
Cancelar
</Button>
</div>
</DialogContent>
</Dialog>
);
}