feat(ui): shared record-scoped useDocumentDraft hook; fix half-wired invoice draft banner; dedupe draft keys
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,12 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
||||
import { offerListOptions, offerCustomersOptions } from "../lib/queries/offers";
|
||||
import { useApiMutation } from "../lib/queries/mutations";
|
||||
import {
|
||||
readDraft,
|
||||
clearDraft,
|
||||
type DraftEnvelope,
|
||||
} from "../hooks/useDocumentDraft";
|
||||
import { DRAFT_KEYS } from "../lib/draftKeys";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -41,7 +47,6 @@ import {
|
||||
import { OFFER_STATUS, statusLabel, statusColor } from "../lib/documentStatus";
|
||||
|
||||
const API_BASE = "/api/admin";
|
||||
const DRAFT_KEY = "boha_offer_draft";
|
||||
|
||||
// Filter tabs cover only the real stored offer statuses (the `expired` entry
|
||||
// in OFFER_STATUS is a front-end-derived state, not a stored value).
|
||||
@@ -67,7 +72,7 @@ interface Quotation {
|
||||
order_status?: string;
|
||||
}
|
||||
|
||||
interface Draft {
|
||||
interface DraftData {
|
||||
form: {
|
||||
project_code: string;
|
||||
customer_name: string;
|
||||
@@ -76,7 +81,6 @@ interface Draft {
|
||||
currency: string;
|
||||
};
|
||||
items: unknown[];
|
||||
savedAt?: string;
|
||||
}
|
||||
|
||||
const TemplatesIcon = (
|
||||
@@ -268,15 +272,17 @@ export default function Offers() {
|
||||
}>({ show: false, quotation: null });
|
||||
const [customerOrderNumber, setCustomerOrderNumber] = useState("");
|
||||
const [orderAttachment, setOrderAttachment] = useState<File | null>(null);
|
||||
const [draft, setDraft] = useState<Draft | null>(() => {
|
||||
try {
|
||||
const raw = localStorage.getItem(DRAFT_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && parsed.form && Array.isArray(parsed.items)) return parsed;
|
||||
} catch {
|
||||
/* ignore corrupt data */
|
||||
}
|
||||
// Only the "new offer" draft (recordId === null) is surfaced here; a draft
|
||||
// scoped to a specific record id is never shown on the list (no leakage).
|
||||
const [draft, setDraft] = useState<DraftEnvelope<DraftData> | null>(() => {
|
||||
const env = readDraft<DraftData>(DRAFT_KEYS.offer);
|
||||
if (
|
||||
env &&
|
||||
env.recordId === null &&
|
||||
env.data?.form &&
|
||||
Array.isArray(env.data.items)
|
||||
)
|
||||
return env;
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -336,11 +342,7 @@ export default function Offers() {
|
||||
});
|
||||
|
||||
const discardDraft = () => {
|
||||
try {
|
||||
localStorage.removeItem(DRAFT_KEY);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
clearDraft(DRAFT_KEYS.offer);
|
||||
setDraft(null);
|
||||
};
|
||||
|
||||
@@ -812,16 +814,18 @@ export default function Offers() {
|
||||
variant="body2"
|
||||
sx={{ color: "text.secondary", mt: 0.25 }}
|
||||
>
|
||||
{draft.form.project_code || "—"} ·{" "}
|
||||
{draft.form.customer_name || "—"} ·{" "}
|
||||
{draft.form.created_at
|
||||
? formatDate(draft.form.created_at)
|
||||
{draft.data.form.project_code || "—"} ·{" "}
|
||||
{draft.data.form.customer_name || "—"} ·{" "}
|
||||
{draft.data.form.created_at
|
||||
? formatDate(draft.data.form.created_at)
|
||||
: "—"}
|
||||
{" — "}
|
||||
{draft.form.valid_until
|
||||
? formatDate(draft.form.valid_until)
|
||||
{draft.data.form.valid_until
|
||||
? formatDate(draft.data.form.valid_until)
|
||||
: "—"}
|
||||
{draft.form.currency ? ` · ${draft.form.currency}` : ""}
|
||||
{draft.data.form.currency
|
||||
? ` · ${draft.data.form.currency}`
|
||||
: ""}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
|
||||
Reference in New Issue
Block a user