From 3bd0d055d9d4c0897cd9de5334c16a821ad9a532 Mon Sep 17 00:00:00 2001
From: BOHA
Date: Wed, 29 Apr 2026 15:23:42 +0200
Subject: [PATCH] v1.6.0: fix offer items mobile layout and localStorage draft
save/restore
- Fix items table description column width on mobile (was ~82px, now ~260px)
- Activate unused offers-items-table CSS class in OfferDetail form
- Save all form fields to localStorage draft (language, VAT, exchange rate were missing)
- Use DRAFT_KEY constant in loadOfferDraft, add error logging
Co-Authored-By: Claude Opus 4.7
---
package.json | 2 +-
src/admin/offers.css | 59 +++++++++++++++++++++
src/admin/pages/OfferDetail.tsx | 91 ++++++++++++++++++++++++---------
3 files changed, 126 insertions(+), 26 deletions(-)
diff --git a/package.json b/package.json
index 819dd6e..28ab37a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "app-ts",
- "version": "1.5.9",
+ "version": "1.6.0",
"description": "",
"main": "dist/server.js",
"scripts": {
diff --git a/src/admin/offers.css b/src/admin/offers.css
index f409554..563175d 100644
--- a/src/admin/offers.css
+++ b/src/admin/offers.css
@@ -491,10 +491,69 @@
display: none;
}
+@media (max-width: 768px) {
+ .offers-items-table .admin-table td .admin-form-input {
+ font-size: 16px;
+ min-height: 44px;
+ padding: 9px 10px;
+ }
+
+ /* Give the description column more room by shrinking numeric columns */
+ .offers-col-qty {
+ width: 4rem !important;
+ }
+ .offers-col-unit {
+ width: 4rem !important;
+ }
+ .offers-col-price {
+ width: 5.5rem !important;
+ }
+ .offers-col-included {
+ width: 3.5rem !important;
+ }
+ .offers-col-total {
+ width: 5.5rem !important;
+ }
+ .offers-col-del {
+ width: 2.5rem !important;
+ }
+}
+
@media (max-width: 640px) {
.offers-items-table {
margin: 0 -1rem;
width: calc(100% + 2rem);
+ border-radius: 0;
+ border-left: none;
+ border-right: none;
+ }
+
+ .offers-items-table .admin-table {
+ min-width: 700px;
+ }
+
+ .offers-items-table .admin-table td {
+ padding: 6px;
+ font-size: 12px;
+ }
+
+ .offers-items-table .admin-table th {
+ font-size: 10px;
+ padding: 6px;
+ }
+
+ /* Further reduce numeric columns on very small screens */
+ .offers-col-qty {
+ width: 3.5rem !important;
+ }
+ .offers-col-unit {
+ width: 3.5rem !important;
+ }
+ .offers-col-price {
+ width: 5rem !important;
+ }
+ .offers-col-total {
+ width: 5rem !important;
}
}
diff --git a/src/admin/pages/OfferDetail.tsx b/src/admin/pages/OfferDetail.tsx
index f1122c0..ca9fe5f 100644
--- a/src/admin/pages/OfferDetail.tsx
+++ b/src/admin/pages/OfferDetail.tsx
@@ -274,9 +274,10 @@ function loadOfferDraft(): {
sections?: unknown[];
} | null {
try {
- const raw = localStorage.getItem("boha_offer_draft");
+ const raw = localStorage.getItem(DRAFT_KEY);
return raw ? JSON.parse(raw) : null;
- } catch {
+ } catch (e) {
+ console.error("Failed to load offer draft:", e);
return null;
}
}
@@ -353,17 +354,28 @@ export default function OfferDetail() {
const draft = loadOfferDraft();
if (draft?.form) {
return {
- ...emptyForm,
+ 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,
- customer_id:
- (draft.form.customer_id as number | null) ?? emptyForm.customer_id,
+ 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,
+ exchange_rate:
+ (draft.form.exchange_rate as string) || emptyForm.exchange_rate,
+ scope_title:
+ (draft.form.scope_title as string) || emptyForm.scope_title,
+ scope_description:
+ (draft.form.scope_description as string) ||
+ emptyForm.scope_description,
};
}
return emptyForm;
@@ -590,22 +602,27 @@ export default function OfferDetail() {
useEffect(() => {
if (isEdit) return;
try {
+ const data = JSON.parse(debouncedDraft);
const draft = {
form: {
- project_code: form.project_code,
- customer_id: form.customer_id,
- customer_name: form.customer_name,
- created_at: form.created_at,
- valid_until: form.valid_until,
- currency: form.currency,
+ project_code: data.form.project_code ?? "",
+ customer_id: data.form.customer_id ?? null,
+ customer_name: data.form.customer_name ?? "",
+ created_at: data.form.created_at ?? "",
+ valid_until: data.form.valid_until ?? "",
+ currency: data.form.currency ?? "CZK",
+ language: data.form.language ?? "EN",
+ vat_rate: data.form.vat_rate ?? 21,
+ apply_vat: data.form.apply_vat ?? false,
+ exchange_rate: data.form.exchange_rate ?? "",
},
- items,
- sections,
+ items: data.items,
+ sections: data.sections,
savedAt: new Date().toISOString(),
};
localStorage.setItem(DRAFT_KEY, JSON.stringify(draft));
- } catch {
- /* localStorage full or unavailable */
+ } catch (e) {
+ console.error("Failed to save offer draft:", e);
}
}, [debouncedDraft]); // eslint-disable-line react-hooks/exhaustive-deps
@@ -697,8 +714,8 @@ export default function OfferDetail() {
if (!isEdit) {
try {
localStorage.removeItem(DRAFT_KEY);
- } catch {
- /* ignore */
+ } catch (e) {
+ console.error("Failed to remove offer draft:", e);
}
}
if (!isEdit && result.data?.id) {
@@ -1283,7 +1300,7 @@ export default function OfferDetail() {
)}
-
+
#
- | Popis |
- Množství |
- Jednotka |
- Cena/ks |
-
+ | Popis |
+
+ Množství
+ |
+
+ Jednotka
+ |
+
+ Cena/ks
+ |
+
V ceně
|
-
+ |
Celkem
|
{!isInvalidated && !isLockedByOther && (
- |
+ |
)}