"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { AdminDialog } from "@/components/ui/admin-dialog"; interface Props { /** Arm the guard — true once the form has unsaved changes. */ enabled: boolean; /** Persist the in-progress PO as a draft. Should navigate away on success. */ onSaveDraft: () => void; /** True while the draft save is in flight (drives the button label/disable). */ saving: boolean; } // Warns the user before they leave a PO form with unsaved changes (issue #18). // Two paths are covered: // • Hard navigations (refresh, tab close, external links) → the browser's own // "Leave site?" prompt (browsers can't render custom buttons here, so the // save-as-draft option isn't offered on this path). // • In-app navigations (sidebar / header / any internal ) → intercepted and // replaced with our own modal offering Save as draft / Discard / Stay. export function UnsavedChangesGuard({ enabled, onSaveDraft, saving }: Props) { const router = useRouter(); const [pendingHref, setPendingHref] = useState(null); // Listeners are attached once; read `enabled` through a ref so they always see // the latest value without re-binding on every keystroke. const enabledRef = useRef(enabled); enabledRef.current = enabled; useEffect(() => { function onBeforeUnload(e: BeforeUnloadEvent) { if (!enabledRef.current) return; e.preventDefault(); e.returnValue = ""; } window.addEventListener("beforeunload", onBeforeUnload); return () => window.removeEventListener("beforeunload", onBeforeUnload); }, []); useEffect(() => { function onClick(e: MouseEvent) { if (!enabledRef.current || e.defaultPrevented) return; // Ignore non-primary clicks and modifier-clicks (new tab / download etc.). if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; const anchor = (e.target as HTMLElement | null)?.closest("a"); const href = anchor?.getAttribute("href"); if (!anchor || !href || href.startsWith("#")) return; if (anchor.hasAttribute("download")) return; if (anchor.target && anchor.target !== "_self") return; const url = new URL(href, window.location.href); // External origin → let the browser's beforeunload prompt handle it. if (url.origin !== window.location.origin) return; // Same page (e.g. a no-op link) → nothing to guard. if (url.pathname === window.location.pathname && url.search === window.location.search) return; e.preventDefault(); e.stopPropagation(); setPendingHref(url.pathname + url.search + url.hash); } // Capture phase so we run before Next's click handler. document.addEventListener("click", onClick, true); return () => document.removeEventListener("click", onClick, true); }, []); const discard = useCallback(() => { const href = pendingHref; setPendingHref(null); enabledRef.current = false; // let this navigation through if (href) router.push(href); }, [pendingHref, router]); const stay = useCallback(() => { if (saving) return; setPendingHref(null); }, [saving]); function saveDraft() { // Close the prompt so any inline save error is visible; the save action // navigates to the PO on success. setPendingHref(null); onSaveDraft(); } return (

You have unsaved changes on this purchase order. Save it as a draft before leaving, or discard your changes?

); }