pelagia-portal/App/components/po/unsaved-changes-dialog.tsx
Claude (auto-fix) 61345dd7b7 feat(po): prompt to save as draft when leaving with unsaved changes
Both the PO create and edit screens now guard against accidental loss of
in-progress data. While a form has unsaved changes:

- in-app link clicks and the edit screen's Cancel button are intercepted,
  showing a dialog offering Save as Draft / Discard changes / Stay on page
- hard unloads (reload, tab close, external navigation) trigger the browser's
  native warning via beforeunload

"Save as Draft" reuses the existing draft-save action; validation errors are
surfaced in the dialog so the user can recover or discard. Dirty state is
tracked from any field edit, line-item change, or attachment change.

Adds useUnsavedChanges hook + UnsavedChangesDialog component and unit tests.

Fixes #18

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 05:17:32 +05:30

82 lines
2.6 KiB
TypeScript

"use client";
import { useState } from "react";
import { AdminDialog } from "@/components/ui/admin-dialog";
interface Props {
open: boolean;
/** Saves the PO as a draft. Resolves `{ ok: true }` on success. */
onSaveDraft: () => Promise<{ ok: boolean; error?: string }>;
/** Leave the page without saving. */
onDiscard: () => void;
/** Cancel and stay on the page. */
onStay: () => void;
saveLabel?: string;
}
export function UnsavedChangesDialog({
open,
onSaveDraft,
onDiscard,
onStay,
saveLabel = "Save as Draft",
}: Props) {
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
async function handleSave() {
setSaving(true);
setError("");
const result = await onSaveDraft();
setSaving(false);
// On success the form navigates away; on failure keep the prompt open so
// the user can fix the problem (or discard).
if (!result.ok) setError(result.error ?? "Could not save draft.");
}
function handleStay() {
if (saving) return;
setError("");
onStay();
}
return (
<AdminDialog title="Unsaved changes" open={open} onClose={handleStay}>
<div className="space-y-4">
<p className="text-sm text-neutral-600">
You have unsaved changes to this purchase order. Would you like to
save it as a draft before leaving?
</p>
{error && (
<p className="text-sm text-danger-700 bg-danger-50 rounded-lg px-3 py-2">{error}</p>
)}
<div className="flex flex-col-reverse gap-2 pt-1 sm:flex-row sm:justify-end sm:gap-3">
<button
type="button"
onClick={handleStay}
disabled={saving}
className="rounded-lg border border-neutral-300 px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50 disabled:opacity-60"
>
Stay on page
</button>
<button
type="button"
onClick={() => { if (!saving) onDiscard(); }}
disabled={saving}
className="rounded-lg border border-danger-200 px-4 py-2 text-sm font-medium text-danger-700 hover:bg-danger-50 disabled:opacity-60"
>
Discard changes
</button>
<button
type="button"
onClick={handleSave}
disabled={saving}
className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-700 disabled:opacity-60 transition-colors"
>
{saving ? "Saving…" : saveLabel}
</button>
</div>
</div>
</AdminDialog>
);
}