feat(drafts): remove localStorage draft banner/hook; add Koncept status + filter (drafts are DB rows)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-09 18:25:24 +02:00
parent 473f7c4a8c
commit ce636215dc
7 changed files with 102 additions and 580 deletions

View File

@@ -57,8 +57,6 @@ import {
import { CSS } from "@dnd-kit/utilities";
import Forbidden from "../components/Forbidden";
import RichEditor from "../components/RichEditor";
import useDocumentDraft, { readDraft } from "../hooks/useDocumentDraft";
import { DRAFT_KEYS } from "../lib/draftKeys";
import apiFetch from "../utils/api";
import { formatCurrency, todayLocalStr } from "../utils/formatters";
import { companySettingsOptions } from "../lib/queries/settings";
@@ -497,24 +495,6 @@ function SortableItemRow({
);
}
interface OfferDraftData {
form?: Record<string, unknown>;
items?: unknown[];
sections?: unknown[];
}
/**
* Read the create-form draft (recordId === null). Returns its `data` payload or
* null. Record-scoped: a draft stored for a specific record id is ignored here,
* and we only ever write/read the "new offer" draft (mirrors the original
* create-only semantic), so a create draft can't leak into editing a record.
*/
function loadOfferDraft(): OfferDraftData | null {
const env = readDraft<OfferDraftData>(DRAFT_KEYS.offer);
if (!env || env.recordId !== null) return null;
return env.data ?? null;
}
export default function OfferDetail() {
const { id } = useParams();
const isEdit = Boolean(id);
@@ -566,51 +546,9 @@ export default function OfferDetail() {
const loading = isEdit && offerQuery.isLoading;
const [saving, setSaving] = useState(false);
const [errors, setErrors] = useState<Record<string, string | undefined>>({});
const [form, setForm] = useState<OfferForm>(() => {
// The draft is the unsaved-NEW-offer buffer — never restore it over an
// existing offer in edit mode (it would also leak its stale item keys).
const draft = isEdit ? null : loadOfferDraft();
if (draft?.form) {
return {
quotation_number:
(draft.form.quotation_number as string) || emptyForm.quotation_number,
project_code:
(draft.form.project_code as string) || emptyForm.project_code,
customer_id:
(draft.form.customer_id as number | null) ?? emptyForm.customer_id,
customer_name:
(draft.form.customer_name as string) || emptyForm.customer_name,
created_at: (draft.form.created_at as string) || emptyForm.created_at,
valid_until:
(draft.form.valid_until as string) || emptyForm.valid_until,
currency: (draft.form.currency as string) || emptyForm.currency,
language: (draft.form.language as string) || emptyForm.language,
vat_rate: (draft.form.vat_rate as number) ?? emptyForm.vat_rate,
apply_vat: (draft.form.apply_vat as boolean) ?? emptyForm.apply_vat,
};
}
return emptyForm;
});
const [items, setItems] = useState<OfferItem[]>(() => {
const draft = isEdit ? null : loadOfferDraft();
if (Array.isArray(draft?.items) && draft.items.length > 0) {
// Re-key restored items: their persisted _key values are stale and the
// counter restarts at 0 each load, so reusing them would collide with
// newly-added rows (duplicate React key / dnd-kit id → the warning).
return (draft.items as OfferItem[]).map((it) => ({
...it,
_key: `item-${++itemKeyCounter.current}`,
}));
}
return [emptyItem()];
});
const [sections, setSections] = useState<ScopeSection[]>(() => {
const draft = isEdit ? null : loadOfferDraft();
if (Array.isArray(draft?.sections) && draft.sections.length > 0) {
return draft.sections as ScopeSection[];
}
return [];
});
const [form, setForm] = useState<OfferForm>(emptyForm);
const [items, setItems] = useState<OfferItem[]>(() => [emptyItem()]);
const [sections, setSections] = useState<ScopeSection[]>([]);
const [orderInfo, setOrderInfo] = useState<OfferOrderInfo | null>(null);
const [offerStatus, setOfferStatus] = useState<string>("");
@@ -808,31 +746,6 @@ export default function OfferDetail() {
return () => window.removeEventListener("beforeunload", handler);
}, [isDirty]);
// Auto-save draft to localStorage (create mode only), record-scoped to the
// "new offer" form (recordId: null) via the shared useDocumentDraft hook.
const draftData = useMemo<OfferDraftData>(
() => ({
form: {
project_code: form.project_code ?? "",
customer_id: form.customer_id ?? null,
customer_name: form.customer_name ?? "",
created_at: form.created_at ?? "",
valid_until: form.valid_until ?? "",
currency: form.currency ?? "CZK",
language: form.language ?? "EN",
vat_rate: form.vat_rate ?? 21,
apply_vat: form.apply_vat ?? false,
},
items,
sections,
}),
[form, items, sections],
);
const { clear: clearOfferDraft } = useDocumentDraft<OfferDraftData>(
DRAFT_KEYS.offer,
{ recordId: null, enabled: !isEdit, data: draftData },
);
const updateForm = (field: keyof OfferForm, value: unknown) => {
setForm((prev) => ({ ...prev, [field]: value }));
setErrors((prev) => ({ ...prev, [field]: undefined }));
@@ -918,7 +831,6 @@ export default function OfferDetail() {
result.message ||
(isEdit ? "Nabídka byla aktualizována" : "Nabídka byla vytvořena"),
);
if (!isEdit) clearOfferDraft();
if (!isEdit && result.data?.id) {
queryClient.invalidateQueries({ queryKey: ["offers"] });
queryClient.invalidateQueries({ queryKey: ["orders"] });