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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user