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

@@ -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 = (
<svg
width="20"
@@ -365,19 +351,6 @@ export default function Offers() {
}>({ show: false, quotation: null });
const [customerOrderNumber, setCustomerOrderNumber] = useState("");
const [orderAttachment, setOrderAttachment] = useState<File | null>(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<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;
});
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 <Forbidden />;
const handleDuplicate = async (quotation: Quotation) => {
@@ -873,81 +841,6 @@ export default function Offers() {
</Box>
</FilterBar>
{draft && !debouncedSearch && (
<Box sx={{ mb: 2 }}>
<Alert severity="info" onClose={discardDraft}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
flexWrap: "wrap",
gap: 1.5,
}}
>
<Box>
<Typography component="span" sx={{ fontWeight: 600 }}>
Rozpracovaný koncept
</Typography>
{draft.savedAt && (
<Typography
component="span"
sx={{ ml: 1, opacity: 0.8, fontSize: "0.875rem" }}
>
·{" "}
{new Date(draft.savedAt).toLocaleTimeString("cs-CZ", {
hour: "2-digit",
minute: "2-digit",
})}
</Typography>
)}
<Typography
variant="body2"
sx={{ color: "text.secondary", mt: 0.25 }}
>
{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}`
: ""}
</Typography>
</Box>
<Box
sx={{
display: "flex",
gap: 1,
flexWrap: "wrap",
}}
>
<Button
component={RouterLink}
to="/offers/new"
size="small"
startIcon={EditIcon}
>
Pokračovat v konceptu
</Button>
<Button
size="small"
variant="outlined"
color="inherit"
onClick={discardDraft}
>
Zahodit koncept
</Button>
</Box>
</Box>
</Alert>
</Box>
)}
<Card sx={{ opacity: isFetching ? 0.6 : 1, transition: "opacity .2s" }}>
<DataTable<Quotation>
columns={columns}