diff --git a/src/admin/hooks/useDocumentDraft.ts b/src/admin/hooks/useDocumentDraft.ts deleted file mode 100644 index 0ddd793..0000000 --- a/src/admin/hooks/useDocumentDraft.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { useEffect, useCallback } from "react"; -import useDebounce from "./useDebounce"; - -/** - * Record-scoped localStorage draft envelope. - * - * - `recordId === null` → the draft belongs to the "new document" form. - * - `recordId === N` → the draft belongs to editing record N. - * - * Scoping the draft to its record kills cross-record leakage: a create-form - * draft can never be restored over an existing record (and vice-versa) because - * the consumer compares `recordId` before restoring. - */ -export interface DraftEnvelope { - recordId: number | null; - savedAt: number; - data: T; -} - -/** - * Read (and validate) a draft envelope from localStorage. Returns `null` when - * absent, unparseable, or the wrong shape. Availability-safe (guarded). The - * caller is responsible for record-scoping (compare `recordId` itself) so the - * list/banner can decide whether the stored draft applies to the "new" form. - */ -export function readDraft(key: string): DraftEnvelope | null { - try { - if (typeof localStorage === "undefined") return null; - const raw = localStorage.getItem(key); - if (!raw) return null; - const parsed = JSON.parse(raw) as Partial>; - if ( - parsed && - typeof parsed === "object" && - "data" in parsed && - "recordId" in parsed - ) { - return { - recordId: parsed.recordId ?? null, - savedAt: typeof parsed.savedAt === "number" ? parsed.savedAt : 0, - data: parsed.data as T, - }; - } - } catch (e) { - console.error(`Failed to read draft "${key}":`, e); - } - return null; -} - -/** Remove a draft envelope from localStorage. Availability-safe. */ -export function clearDraft(key: string): void { - try { - if (typeof localStorage === "undefined") return; - localStorage.removeItem(key); - } catch (e) { - console.error(`Failed to clear draft "${key}":`, e); - } -} - -interface UseDocumentDraftOptions { - /** `null` = the "new" form; a number = editing that record. */ - recordId: number | null; - /** Only autosave while ready/dirty-eligible (e.g. create mode, data loaded). */ - enabled: boolean; - /** Current form data to persist (debounced before writing). */ - data: T; - /** Debounce window in ms before a write. Defaults to Offers' proven 1500ms. */ - debounceMs?: number; -} - -/** - * Generalizes the proven Offers autosave: debounced write of `data` to - * `localStorage[key]` (wrapped in a {@link DraftEnvelope} stamped with - * `recordId` + `savedAt`) while `enabled`. The form supplies the write side; - * the list/banner uses {@link readDraft} for the read side. - */ -export default function useDocumentDraft( - key: string, - { recordId, enabled, data, debounceMs = 1500 }: UseDocumentDraftOptions, -): { clear: () => void } { - const payload = JSON.stringify(data); - const debouncedPayload = useDebounce(payload, debounceMs); - - useEffect(() => { - if (!enabled) return; - try { - if (typeof localStorage === "undefined") return; - const envelope: DraftEnvelope = { - recordId, - savedAt: Date.now(), - data: JSON.parse(debouncedPayload) as T, - }; - localStorage.setItem(key, JSON.stringify(envelope)); - } catch (e) { - console.error(`Failed to save draft "${key}":`, e); - } - // `recordId` is stable for a given mount; intentionally tracking the - // debounced payload + enabled flag only (mirrors Offers' original deps). - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [debouncedPayload, enabled]); - - const clear = useCallback(() => clearDraft(key), [key]); - - return { clear }; -} diff --git a/src/admin/lib/documentStatus.ts b/src/admin/lib/documentStatus.ts index 6168d24..c45b384 100644 --- a/src/admin/lib/documentStatus.ts +++ b/src/admin/lib/documentStatus.ts @@ -31,6 +31,7 @@ export interface StatusMeta { * stored value rendering consistently (warning, matching the expired row wash). */ export const OFFER_STATUS: Record = { + draft: { label: "Koncept", color: "default" }, active: { label: "Aktivní", color: "info" }, ordered: { label: "Objednaná", color: "success" }, invalidated: { label: "Zneplatněná", color: "error" }, @@ -62,6 +63,7 @@ export const ISSUED_ORDER_STATUS: Record = { * = warning, NOT info) — this fixes the list↔detail color drift. */ export const INVOICE_STATUS: Record = { + draft: { label: "Koncept", color: "default" }, issued: { label: "Vystavena", color: "warning" }, overdue: { label: "Po splatnosti", color: "error" }, paid: { label: "Zaplacena", color: "success" }, diff --git a/src/admin/lib/draftKeys.ts b/src/admin/lib/draftKeys.ts deleted file mode 100644 index 9b8c24e..0000000 --- a/src/admin/lib/draftKeys.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Single source of truth for the localStorage keys used by the document-draft - * autosave feature. Both the form (write side) and the list/banner (read side) - * MUST import the key from here — duplicating the string literal in two files - * means changing one silently breaks the other (the banner stops matching). - */ -export const DRAFT_KEYS = { - offer: "boha_offer_draft", - invoice: "boha_invoice_draft", -} as const; - -export type DraftKey = (typeof DRAFT_KEYS)[keyof typeof DRAFT_KEYS]; diff --git a/src/admin/pages/InvoiceDetail.tsx b/src/admin/pages/InvoiceDetail.tsx index e30c5b8..5dfe713 100644 --- a/src/admin/pages/InvoiceDetail.tsx +++ b/src/admin/pages/InvoiceDetail.tsx @@ -46,8 +46,6 @@ import { } from "@dnd-kit/modifiers"; import { CSS } from "@dnd-kit/utilities"; import apiFetch from "../utils/api"; -import useDocumentDraft, { readDraft } from "../hooks/useDocumentDraft"; -import { DRAFT_KEYS } from "../lib/draftKeys"; import { companySettingsOptions } from "../lib/queries/settings"; import { invoiceDetailOptions } from "../lib/queries/invoices"; import { offerCustomersOptions } from "../lib/queries/offers"; @@ -199,27 +197,6 @@ interface InvoiceForm { bank_account: string; } -interface InvoiceDraftData { - form?: Record; - items?: 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 invoice" draft (recordId: null), so a - * create draft can never leak into editing an existing invoice. Mirrors - * OfferDetail.loadOfferDraft + the Invoices-list banner's validity check - * (recordId === null, data.form present, Array.isArray(data.items)). - */ -function loadInvoiceDraft(): InvoiceDraftData | null { - const env = readDraft(DRAFT_KEYS.invoice); - if (!env || env.recordId !== null) return null; - const data = env.data; - if (!data || !data.form || !Array.isArray(data.items)) return null; - return data; -} - // Sortable row for create mode function SortableInvoiceRow({ item, @@ -539,68 +516,41 @@ export default function InvoiceDetail() { const fromOrderId = !isEdit && rawOrderId && /^\d+$/.test(rawOrderId) ? rawOrderId : null; - // Restore the unsaved "new invoice" draft (create mode only). Read once, - // lazily, so the autosave effect can't clobber it before it's seeded — and so - // editing an existing invoice never reads the create draft (isEdit short- - // circuits). `restoredFromDraft` lets the create-hydration effect below skip - // the default-bank / fromOrder overrides so the user's draft wins. - const restoredFromDraft = useRef(false); - - const [form, setForm] = useState(() => { - const defaults: InvoiceForm = { - customer_id: null, - customer_name: "", - order_id: fromOrderId ? Number(fromOrderId) : null, - issue_date: todayLocalStr(), - due_date: addDaysLocalStr(14), - tax_date: todayLocalStr(), - currency: "CZK", - apply_vat: 1, - vat_rate: 21, - payment_method: "Příkazem", - constant_symbol: "0308", - issued_by: user?.fullName || "", - billing_text: "", - notes: "", - language: "cs", - bank_account_id: "", - bank_name: "", - bank_swift: "", - bank_iban: "", - bank_account: "", - }; - const draft = isEdit ? null : loadInvoiceDraft(); - if (draft?.form) { - restoredFromDraft.current = true; - return { ...defaults, ...(draft.form as Partial) }; - } - return defaults; - }); + const [form, setForm] = useState(() => ({ + customer_id: null, + customer_name: "", + order_id: fromOrderId ? Number(fromOrderId) : null, + issue_date: todayLocalStr(), + due_date: addDaysLocalStr(14), + tax_date: todayLocalStr(), + currency: "CZK", + apply_vat: 1, + vat_rate: 21, + payment_method: "Příkazem", + constant_symbol: "0308", + issued_by: user?.fullName || "", + billing_text: "", + notes: "", + language: "cs", + bank_account_id: "", + bank_name: "", + bank_swift: "", + bank_iban: "", + bank_account: "", + })); const [dueDays, setDueDays] = useState(14); - const [items, setItems] = useState(() => { - const draft = isEdit ? null : loadInvoiceDraft(); - if (Array.isArray(draft?.items) && draft.items.length > 0) { - // Re-key restored items: their persisted _key values are stale and the - // key counter restarts each load, so reusing them would collide with - // newly-added rows (duplicate React key / dnd-kit id). - return (draft.items as InvoiceItem[]).map((it) => ({ - ...it, - _key: `inv-${++keyCounterRef.current}`, - })); - } - return [ - { - _key: "inv-1", - description: "", - item_description: "", - quantity: 1, - unit: "ks", - unit_price: 0, - vat_rate: 21, - }, - ]; - }); + const [items, setItems] = useState(() => [ + { + _key: "inv-1", + description: "", + item_description: "", + quantity: 1, + unit: "ks", + unit_price: 0, + vat_rate: 21, + }, + ]); const [errors, setErrors] = useState>({}); const [saving, setSaving] = useState(false); const [dataReady, setDataReady] = useState(false); @@ -632,16 +582,6 @@ export default function InvoiceDetail() { label: `${v}%`, })); - // Autosave the create-form draft (record-scoped to recordId: null) so the - // Invoices list can surface a "you have a draft" banner. Edit mode never - // autosaves (enabled: !isEdit), matching the Offers behavior. - const draftData = useMemo(() => ({ form, items }), [form, items]); - const { clear: clearDraft } = useDocumentDraft(DRAFT_KEYS.invoice, { - recordId: null, - enabled: !isEdit, - data: draftData, - }); - // ─── TanStack Query ─── const customersQuery = useQuery(offerCustomersOptions()); @@ -797,61 +737,54 @@ export default function InvoiceDetail() { return; if (fromOrderId && orderDataQuery.isLoading) return; - // Set invoice number (NOT part of the draft — always set from the server's - // next-number, even when a draft was restored). + // Set invoice number from the server's next-number. if (nextNumberQuery.data) { setInvoiceNumber(nextNumberQuery.data); } - // When a draft was restored, the user's saved form/items WIN: skip the - // default-bank seed and the fromOrder prefill so we don't clobber the - // restored values (mirrors OfferDetail, which never re-applies create - // defaults over a restored draft). The no-draft path is unchanged. - if (!restoredFromDraft.current) { - // Set default bank account - const defaultAcc = bankAccounts.find((a) => a.is_default); - if (defaultAcc) { - setForm((prev) => ({ - ...prev, - bank_account_id: defaultAcc.id, - bank_name: defaultAcc.bank_name || "", - bank_swift: defaultAcc.bic || "", - bank_iban: defaultAcc.iban || "", - bank_account: defaultAcc.account_number || "", - })); - } + // Set default bank account + const defaultAcc = bankAccounts.find((a) => a.is_default); + if (defaultAcc) { + setForm((prev) => ({ + ...prev, + bank_account_id: defaultAcc.id, + bank_name: defaultAcc.bank_name || "", + bank_swift: defaultAcc.bic || "", + bank_iban: defaultAcc.iban || "", + bank_account: defaultAcc.account_number || "", + })); + } - // Pre-fill from order - if (fromOrderId && orderDataQuery.data) { - const order = orderDataQuery.data; - const vatRate = - Number(order.vat_rate) || (companySettings?.default_vat_rate ?? 21); - setForm((prev) => ({ - ...prev, - customer_id: order.customer_id as number, - customer_name: (order.customer_name as string) || "", - order_id: order.id as number, - currency: - (order.currency as string) || - companySettings?.default_currency || - "CZK", - apply_vat: Number(order.apply_vat) || 0, - vat_rate: vatRate, - })); - const orderItems = order.items as Record[] | undefined; - if (orderItems && orderItems.length > 0) { - setItems( - orderItems.map((item) => ({ - _key: `inv-${++keyCounterRef.current}`, - description: (item.description as string) || "", - item_description: (item.item_description as string) || "", - quantity: Number(item.quantity) || 1, - unit: (item.unit as string) || "", - unit_price: Number(item.unit_price) || 0, - vat_rate: vatRate, - })), - ); - } + // Pre-fill from order + if (fromOrderId && orderDataQuery.data) { + const order = orderDataQuery.data; + const vatRate = + Number(order.vat_rate) || (companySettings?.default_vat_rate ?? 21); + setForm((prev) => ({ + ...prev, + customer_id: order.customer_id as number, + customer_name: (order.customer_name as string) || "", + order_id: order.id as number, + currency: + (order.currency as string) || + companySettings?.default_currency || + "CZK", + apply_vat: Number(order.apply_vat) || 0, + vat_rate: vatRate, + })); + const orderItems = order.items as Record[] | undefined; + if (orderItems && orderItems.length > 0) { + setItems( + orderItems.map((item) => ({ + _key: `inv-${++keyCounterRef.current}`, + description: (item.description as string) || "", + item_description: (item.item_description as string) || "", + quantity: Number(item.quantity) || 1, + unit: (item.unit as string) || "", + unit_price: Number(item.unit_price) || 0, + vat_rate: vatRate, + })), + ); } } @@ -1044,7 +977,6 @@ export default function InvoiceDetail() { if (isEdit) payload.invoice_number = invoiceNumber; const data = await saveMutation.mutateAsync(payload); - if (!isEdit) clearDraft(); alert.success(isEdit ? "Faktura byla uložena" : "Faktura byla vytvořena"); initialSnapshotRef.current = JSON.stringify({ form, items }); if (!isEdit) { diff --git a/src/admin/pages/Invoices.tsx b/src/admin/pages/Invoices.tsx index 323d4cf..5226644 100644 --- a/src/admin/pages/Invoices.tsx +++ b/src/admin/pages/Invoices.tsx @@ -25,12 +25,6 @@ import { type Invoice, type CurrencyAmount, } from "../lib/queries/invoices"; -import { - readDraft, - clearDraft, - type DraftEnvelope, -} from "../hooks/useDocumentDraft"; -import { DRAFT_KEYS } from "../lib/draftKeys"; import { Button, Card, @@ -40,7 +34,6 @@ import { TextField, StatusChip, StatCard, - Alert, EmptyState, LoadingState, FilterBar, @@ -55,6 +48,7 @@ import { INVOICE_STATUS, statusLabel, statusColor, + statusOptions, } from "../lib/documentStatus"; const ReceivedInvoices = lazy(() => import("./ReceivedInvoices")); @@ -96,17 +90,12 @@ function formatCzkWithDetail( return { value: formatMultiCurrency(amounts), detail: null }; } -const STATUS_FILTERS = [ - { value: "", label: "Vše" }, - { value: "issued", label: "Vystavené" }, - { value: "paid", label: "Zaplacené" }, - { value: "overdue", label: "Po splatnosti" }, -]; - -interface DraftData { - form: Record; - items: Record[]; -} +// Filter tabs derive from INVOICE_STATUS so `draft` ("Koncept") flows in +// automatically; the leading "Všechny" tab is the all-statuses filter. +const STATUS_FILTERS = statusOptions(INVOICE_STATUS, { + value: "", + label: "Všechny", +}); const PlusIcon = ( ({ show: false, invoice: null }); const [deleting, setDeleting] = useState(false); const [pdfLoading, setPdfLoading] = useState(null); - // Only the "new invoice" 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 | null>(() => { - const env = readDraft(DRAFT_KEYS.invoice); - if ( - env && - env.recordId === null && - env.data?.form && - Array.isArray(env.data.items) - ) - return env; - return null; - }); - - const discardDraft = () => { - clearDraft(DRAFT_KEYS.invoice); - setDraft(null); - }; const queryClient = useQueryClient(); const { @@ -763,77 +734,6 @@ export default function Invoices() { - {draft && !search && !statusFilter && ( - - - - - - Koncept - - {draft.savedAt && ( - - ·{" "} - {new Date(draft.savedAt).toLocaleTimeString("cs-CZ", { - hour: "2-digit", - minute: "2-digit", - })} - - )} - - {(draft.data.form.customer_name as string) || "—"} ·{" "} - {draft.data.form.issue_date - ? formatDate(draft.data.form.issue_date as string) - : "—"} - {" — "} - {draft.data.form.due_date - ? formatDate(draft.data.form.due_date as string) - : "—"} - - - - - - - - - - )} - columns={columns} diff --git a/src/admin/pages/OfferDetail.tsx b/src/admin/pages/OfferDetail.tsx index 3c44df0..9746033 100644 --- a/src/admin/pages/OfferDetail.tsx +++ b/src/admin/pages/OfferDetail.tsx @@ -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; - 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(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>({}); - const [form, setForm] = useState(() => { - // 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(() => { - 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(() => { - const draft = isEdit ? null : loadOfferDraft(); - if (Array.isArray(draft?.sections) && draft.sections.length > 0) { - return draft.sections as ScopeSection[]; - } - return []; - }); + const [form, setForm] = useState(emptyForm); + const [items, setItems] = useState(() => [emptyItem()]); + const [sections, setSections] = useState([]); const [orderInfo, setOrderInfo] = useState(null); const [offerStatus, setOfferStatus] = useState(""); @@ -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( - () => ({ - 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( - 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"] }); diff --git a/src/admin/pages/Offers.tsx b/src/admin/pages/Offers.tsx index 98f9247..3c5d417 100644 --- a/src/admin/pages/Offers.tsx +++ b/src/admin/pages/Offers.tsx @@ -20,12 +20,6 @@ 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, @@ -38,7 +32,6 @@ import { Select, StatusChip, FileUpload, - Alert, EmptyState, FilterBar, Tabs, @@ -48,19 +41,23 @@ import { type DataColumn, type TabDef, } from "../ui"; -import { OFFER_STATUS, statusLabel, statusColor } from "../lib/documentStatus"; +import { + OFFER_STATUS, + statusLabel, + statusColor, + statusOptions, +} from "../lib/documentStatus"; const API_BASE = "/api/admin"; -// 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). -const STATUS_FILTERS = [ - { value: "", label: "Vše" }, - ...(["active", "ordered", "invalidated"] as const).map((value) => ({ - value, - label: statusLabel(OFFER_STATUS, value), - })), -]; +// Filter tabs derive from OFFER_STATUS so `draft` ("Koncept") flows in +// automatically; the leading "Všechny" tab is the all-statuses filter. The +// `expired` entry in OFFER_STATUS is a front-end-derived state (not a stored +// value), so it is excluded from the filter tabs. +const STATUS_FILTERS = statusOptions(OFFER_STATUS, { + value: "", + label: "Všechny", +}).filter((o) => o.value !== "expired"); interface Quotation { id: number; @@ -76,17 +73,6 @@ interface Quotation { order_status?: string; } -interface DraftData { - form: { - project_code: string; - customer_name: string; - created_at: string; - valid_until: string; - currency: string; - }; - items: unknown[]; -} - const TemplatesIcon = ( ({ show: false, quotation: null }); const [customerOrderNumber, setCustomerOrderNumber] = useState(""); const [orderAttachment, setOrderAttachment] = useState(null); - // 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 | null>(() => { - const env = readDraft(DRAFT_KEYS.offer); - if ( - env && - env.recordId === null && - env.data?.form && - Array.isArray(env.data.items) - ) - return env; - return null; - }); const queryClient = useQueryClient(); const { @@ -440,11 +413,6 @@ export default function Offers() { }, }); - const discardDraft = () => { - clearDraft(DRAFT_KEYS.offer); - setDraft(null); - }; - if (!hasPermission("offers.view")) return ; const handleDuplicate = async (quotation: Quotation) => { @@ -873,81 +841,6 @@ export default function Offers() { - {draft && !debouncedSearch && ( - - - - - - Rozpracovaný koncept - - {draft.savedAt && ( - - ·{" "} - {new Date(draft.savedAt).toLocaleTimeString("cs-CZ", { - hour: "2-digit", - minute: "2-digit", - })} - - )} - - {draft.data.form.project_code || "—"} ·{" "} - {draft.data.form.customer_name || "—"} ·{" "} - {draft.data.form.created_at - ? formatDate(draft.data.form.created_at) - : "—"} - {" — "} - {draft.data.form.valid_until - ? formatDate(draft.data.form.valid_until) - : "—"} - {draft.data.form.currency - ? ` · ${draft.data.form.currency}` - : ""} - - - - - - - - - - )} - columns={columns}