v1.6.9: cleanup offer dead columns, add item template picker to offer form

- Remove dead DB columns: uuid, sync_version from quotations/quotation_items/scope_sections; is_deleted from quotation_items/scope_sections; content_editor_height from scope_sections
- Remove unused scope_title/scope_description from offer form state
- Remove dead CSS: offers-template-menu, offers-draft-indicator, exchange-rate
- Remove dead scope_title translation key from offers PDF
- Fix duplicate Customer interface in OfferDetail.tsx (use shared type)
- Add missing title field to ScopeTemplate interface
- Add item template picker dropdown next to "Přidat položku" button

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-05-28 13:20:22 +02:00
parent 0330453ad6
commit 705f58e3d1
6 changed files with 71 additions and 85 deletions

View File

@@ -0,0 +1,14 @@
-- AlterTable: quotation_items
ALTER TABLE `quotation_items` DROP COLUMN `uuid`;
ALTER TABLE `quotation_items` DROP COLUMN `is_deleted`;
ALTER TABLE `quotation_items` DROP COLUMN `sync_version`;
-- AlterTable: quotations
ALTER TABLE `quotations` DROP COLUMN `uuid`;
ALTER TABLE `quotations` DROP COLUMN `sync_version`;
-- AlterTable: scope_sections
ALTER TABLE `scope_sections` DROP COLUMN `content_editor_height`;
ALTER TABLE `scope_sections` DROP COLUMN `uuid`;
ALTER TABLE `scope_sections` DROP COLUMN `is_deleted`;
ALTER TABLE `scope_sections` DROP COLUMN `sync_version`;

View File

@@ -378,10 +378,7 @@ model quotation_items {
unit String? @db.VarChar(20) unit String? @db.VarChar(20)
unit_price Decimal? @default(0.00) @db.Decimal(12, 2) unit_price Decimal? @default(0.00) @db.Decimal(12, 2)
is_included_in_total Boolean? @default(true) is_included_in_total Boolean? @default(true)
uuid String? @db.VarChar(36)
modified_at DateTime? @db.DateTime(0) modified_at DateTime? @db.DateTime(0)
is_deleted Boolean? @default(false)
sync_version Int? @default(0)
quotations quotations @relation(fields: [quotation_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "quotation_items_ibfk_1") quotations quotations @relation(fields: [quotation_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "quotation_items_ibfk_1")
@@index([quotation_id], map: "quotation_id") @@index([quotation_id], map: "quotation_id")
@@ -404,9 +401,7 @@ model quotations {
scope_description String? @db.Text scope_description String? @db.Text
locked_by Int? locked_by Int?
locked_at DateTime? @db.DateTime(0) locked_at DateTime? @db.DateTime(0)
uuid String? @db.VarChar(36)
modified_at DateTime? @db.DateTime(0) modified_at DateTime? @db.DateTime(0)
sync_version Int? @default(0)
orders orders[] orders orders[]
projects projects[] projects projects[]
quotation_items quotation_items[] quotation_items quotation_items[]
@@ -491,11 +486,7 @@ model scope_sections {
title String? @db.VarChar(500) title String? @db.VarChar(500)
title_cz String? @db.VarChar(500) title_cz String? @db.VarChar(500)
content String? @db.Text content String? @db.Text
content_editor_height Int?
uuid String? @db.VarChar(36)
modified_at DateTime? @db.DateTime(0) modified_at DateTime? @db.DateTime(0)
is_deleted Boolean? @default(false)
sync_version Int? @default(0)
quotations quotations @relation(fields: [quotation_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "scope_sections_ibfk_1") quotations quotations @relation(fields: [quotation_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "scope_sections_ibfk_1")
@@index([quotation_id], map: "quotation_id") @@index([quotation_id], map: "quotation_id")

View File

@@ -19,6 +19,7 @@ export interface ScopeSection {
export interface ScopeTemplate { export interface ScopeTemplate {
id: number; id: number;
name: string; name: string;
title?: string;
description?: string; description?: string;
scope_template_sections?: ScopeSection[]; scope_template_sections?: ScopeSection[];
} }
@@ -134,8 +135,6 @@ export interface OfferDetailData {
language: string; language: string;
vat_rate: number; vat_rate: number;
apply_vat: boolean; apply_vat: boolean;
scope_title: string;
scope_description: string;
items?: OfferItemData[]; items?: OfferItemData[];
sections?: OfferSectionData[]; sections?: OfferSectionData[];
status: string; status: string;

View File

@@ -46,33 +46,6 @@
min-height: 32px; min-height: 32px;
} }
/* Template dropdown menu */
.offers-template-menu {
position: absolute;
top: 100%;
right: 0;
z-index: 100;
min-width: 200px;
max-height: 250px;
overflow-y: auto;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 0.5rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
margin-top: 0.25rem;
}
.offers-template-menu-item {
padding: 0.5rem 0.75rem;
cursor: pointer;
font-size: 0.875rem;
transition: background var(--transition);
}
.offers-template-menu-item:hover {
background: var(--bg-secondary);
}
/* Language badges */ /* Language badges */
.offers-lang-badge { .offers-lang-badge {
display: inline-flex; display: inline-flex;
@@ -617,17 +590,6 @@
} }
/* Offer draft indicator */ /* Offer draft indicator */
.offers-draft-indicator {
display: flex;
align-items: center;
gap: 0.3rem;
font-size: 0.72rem;
font-weight: 500;
color: var(--text-tertiary);
margin-top: 0.2rem;
opacity: 0.8;
}
/* Filters */ /* Filters */
.admin-filters { .admin-filters {
display: flex; display: flex;

View File

@@ -16,12 +16,15 @@ import {
offerCustomersOptions, offerCustomersOptions,
scopeTemplatesOptions, scopeTemplatesOptions,
offerNextNumberOptions, offerNextNumberOptions,
itemTemplatesOptions,
type ItemTemplate,
type OfferDetailData, type OfferDetailData,
type OfferItemData, type OfferItemData,
type OfferSectionData, type OfferSectionData,
type OfferLockInfo, type OfferLockInfo,
type OfferOrderInfo, type OfferOrderInfo,
type ScopeTemplate, type ScopeTemplate,
type Customer,
} from "../lib/queries/offers"; } from "../lib/queries/offers";
import { import {
@@ -90,14 +93,6 @@ interface OfferForm {
language: string; language: string;
vat_rate: number; vat_rate: number;
apply_vat: boolean; apply_vat: boolean;
scope_title: string;
scope_description: string;
}
interface Customer {
id: number;
name: string;
city?: string;
} }
const emptyForm: OfferForm = { const emptyForm: OfferForm = {
@@ -111,8 +106,6 @@ const emptyForm: OfferForm = {
language: "EN", language: "EN",
vat_rate: 21, vat_rate: 21,
apply_vat: false, apply_vat: false,
scope_title: "",
scope_description: "",
}; };
const emptyScopeSection = (): ScopeSection => ({ const emptyScopeSection = (): ScopeSection => ({
@@ -319,6 +312,7 @@ export default function OfferDetail() {
enabled: !isEdit, enabled: !isEdit,
}); });
const { data: templatesData } = useQuery(scopeTemplatesOptions()); const { data: templatesData } = useQuery(scopeTemplatesOptions());
const { data: itemTemplates } = useQuery(itemTemplatesOptions());
const { data: nextNumberData } = useQuery({ const { data: nextNumberData } = useQuery({
...offerNextNumberOptions(), ...offerNextNumberOptions(),
enabled: !isEdit, enabled: !isEdit,
@@ -350,11 +344,6 @@ export default function OfferDetail() {
language: (draft.form.language as string) || emptyForm.language, language: (draft.form.language as string) || emptyForm.language,
vat_rate: (draft.form.vat_rate as number) ?? emptyForm.vat_rate, vat_rate: (draft.form.vat_rate as number) ?? emptyForm.vat_rate,
apply_vat: (draft.form.apply_vat as boolean) ?? emptyForm.apply_vat, apply_vat: (draft.form.apply_vat as boolean) ?? emptyForm.apply_vat,
scope_title:
(draft.form.scope_title as string) || emptyForm.scope_title,
scope_description:
(draft.form.scope_description as string) ||
emptyForm.scope_description,
}; };
} }
return emptyForm; return emptyForm;
@@ -445,8 +434,6 @@ export default function OfferDetail() {
language: d.language || "EN", language: d.language || "EN",
vat_rate: d.vat_rate ?? companySettings?.default_vat_rate ?? 21, vat_rate: d.vat_rate ?? companySettings?.default_vat_rate ?? 21,
apply_vat: !!d.apply_vat, apply_vat: !!d.apply_vat,
scope_title: d.scope_title || "",
scope_description: d.scope_description || "",
}; };
setForm(formData); setForm(formData);
const mappedItems = const mappedItems =
@@ -1252,12 +1239,59 @@ export default function OfferDetail() {
<div className="admin-card-header flex-between"> <div className="admin-card-header flex-between">
<h3 className="admin-card-title">Položky</h3> <h3 className="admin-card-title">Položky</h3>
{!readOnly && ( {!readOnly && (
<button <div
onClick={addItem} style={{
className="admin-btn admin-btn-secondary admin-btn-sm" display: "flex",
gap: "0.5rem",
alignItems: "center",
}}
> >
+ Přidat položku {itemTemplates && itemTemplates.length > 0 && (
</button> <select
className="admin-form-select"
style={{ width: "auto", minWidth: "160px" }}
defaultValue=""
onChange={(e) => {
const templateId = Number(e.target.value);
if (!templateId) return;
const template = itemTemplates.find(
(t: ItemTemplate) => t.id === templateId,
);
if (template) {
setItems((prev) => [
...prev,
{
_key: `item-${++itemKeyCounter.current}`,
description: template.name || "",
item_description: template.description || "",
quantity: 1,
unit: "ks",
unit_price: template.default_price || 0,
is_included_in_total: true,
},
]);
alert.success(
`Načtena šablona položky "${template.name}"`,
);
}
e.target.value = "";
}}
>
<option value="">Ze šablony...</option>
{itemTemplates.map((t: ItemTemplate) => (
<option key={t.id} value={t.id}>
{t.name}
</option>
))}
</select>
)}
<button
onClick={addItem}
className="admin-btn admin-btn-secondary admin-btn-sm"
>
+ Přidat položku
</button>
</div>
)} )}
</div> </div>
{errors.items && ( {errors.items && (
@@ -1408,13 +1442,6 @@ export default function OfferDetail() {
content: s.content || "", content: s.content || "",
})); }));
setSections((prev) => [...prev, ...newSections]); setSections((prev) => [...prev, ...newSections]);
if (template.description) {
setForm((prev) => ({
...prev,
scope_description:
template.description || prev.scope_description,
}));
}
alert.success(`Načtena šablona "${template.name}"`); alert.success(`Načtena šablona "${template.name}"`);
} }
e.target.value = ""; e.target.value = "";

View File

@@ -186,7 +186,6 @@ function buildAddressLines(
const TRANSLATIONS: Record<string, Record<string, string>> = { const TRANSLATIONS: Record<string, Record<string, string>> = {
title: { EN: "PRICE QUOTATION", CZ: "CENOV\u00C1 NAB\u00CDDKA" }, title: { EN: "PRICE QUOTATION", CZ: "CENOV\u00C1 NAB\u00CDDKA" },
scope_title: { EN: "SCOPE OF THE PROJECT", CZ: "ROZSAH PROJEKTU" },
valid_until: { EN: "Valid until", CZ: "Platnost do" }, valid_until: { EN: "Valid until", CZ: "Platnost do" },
customer: { EN: "Customer", CZ: "Z\u00E1kazn\u00EDk" }, customer: { EN: "Customer", CZ: "Z\u00E1kazn\u00EDk" },
supplier: { EN: "Supplier", CZ: "Dodavatel" }, supplier: { EN: "Supplier", CZ: "Dodavatel" },
@@ -571,12 +570,6 @@ ${indentCSS}
border-bottom: 2.5pt solid #de3a3a; border-bottom: 2.5pt solid #de3a3a;
padding-bottom: 1mm; padding-bottom: 1mm;
} }
.totals .exchange-rate {
text-align: right;
font-size: 7.5pt;
color: #969696;
margin-top: 3mm;
}
/* ---- Scope sections ---- */ /* ---- Scope sections ---- */
.scope-page { .scope-page {