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:
@@ -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`;
|
||||
@@ -378,10 +378,7 @@ model quotation_items {
|
||||
unit String? @db.VarChar(20)
|
||||
unit_price Decimal? @default(0.00) @db.Decimal(12, 2)
|
||||
is_included_in_total Boolean? @default(true)
|
||||
uuid String? @db.VarChar(36)
|
||||
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")
|
||||
|
||||
@@index([quotation_id], map: "quotation_id")
|
||||
@@ -404,9 +401,7 @@ model quotations {
|
||||
scope_description String? @db.Text
|
||||
locked_by Int?
|
||||
locked_at DateTime? @db.DateTime(0)
|
||||
uuid String? @db.VarChar(36)
|
||||
modified_at DateTime? @db.DateTime(0)
|
||||
sync_version Int? @default(0)
|
||||
orders orders[]
|
||||
projects projects[]
|
||||
quotation_items quotation_items[]
|
||||
@@ -491,11 +486,7 @@ model scope_sections {
|
||||
title String? @db.VarChar(500)
|
||||
title_cz String? @db.VarChar(500)
|
||||
content String? @db.Text
|
||||
content_editor_height Int?
|
||||
uuid String? @db.VarChar(36)
|
||||
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")
|
||||
|
||||
@@index([quotation_id], map: "quotation_id")
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface ScopeSection {
|
||||
export interface ScopeTemplate {
|
||||
id: number;
|
||||
name: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
scope_template_sections?: ScopeSection[];
|
||||
}
|
||||
@@ -134,8 +135,6 @@ export interface OfferDetailData {
|
||||
language: string;
|
||||
vat_rate: number;
|
||||
apply_vat: boolean;
|
||||
scope_title: string;
|
||||
scope_description: string;
|
||||
items?: OfferItemData[];
|
||||
sections?: OfferSectionData[];
|
||||
status: string;
|
||||
|
||||
@@ -46,33 +46,6 @@
|
||||
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 */
|
||||
.offers-lang-badge {
|
||||
display: inline-flex;
|
||||
@@ -617,17 +590,6 @@
|
||||
}
|
||||
|
||||
/* 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 */
|
||||
.admin-filters {
|
||||
display: flex;
|
||||
|
||||
@@ -16,12 +16,15 @@ import {
|
||||
offerCustomersOptions,
|
||||
scopeTemplatesOptions,
|
||||
offerNextNumberOptions,
|
||||
itemTemplatesOptions,
|
||||
type ItemTemplate,
|
||||
type OfferDetailData,
|
||||
type OfferItemData,
|
||||
type OfferSectionData,
|
||||
type OfferLockInfo,
|
||||
type OfferOrderInfo,
|
||||
type ScopeTemplate,
|
||||
type Customer,
|
||||
} from "../lib/queries/offers";
|
||||
|
||||
import {
|
||||
@@ -90,14 +93,6 @@ interface OfferForm {
|
||||
language: string;
|
||||
vat_rate: number;
|
||||
apply_vat: boolean;
|
||||
scope_title: string;
|
||||
scope_description: string;
|
||||
}
|
||||
|
||||
interface Customer {
|
||||
id: number;
|
||||
name: string;
|
||||
city?: string;
|
||||
}
|
||||
|
||||
const emptyForm: OfferForm = {
|
||||
@@ -111,8 +106,6 @@ const emptyForm: OfferForm = {
|
||||
language: "EN",
|
||||
vat_rate: 21,
|
||||
apply_vat: false,
|
||||
scope_title: "",
|
||||
scope_description: "",
|
||||
};
|
||||
|
||||
const emptyScopeSection = (): ScopeSection => ({
|
||||
@@ -319,6 +312,7 @@ export default function OfferDetail() {
|
||||
enabled: !isEdit,
|
||||
});
|
||||
const { data: templatesData } = useQuery(scopeTemplatesOptions());
|
||||
const { data: itemTemplates } = useQuery(itemTemplatesOptions());
|
||||
const { data: nextNumberData } = useQuery({
|
||||
...offerNextNumberOptions(),
|
||||
enabled: !isEdit,
|
||||
@@ -350,11 +344,6 @@ export default function OfferDetail() {
|
||||
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,
|
||||
scope_title:
|
||||
(draft.form.scope_title as string) || emptyForm.scope_title,
|
||||
scope_description:
|
||||
(draft.form.scope_description as string) ||
|
||||
emptyForm.scope_description,
|
||||
};
|
||||
}
|
||||
return emptyForm;
|
||||
@@ -445,8 +434,6 @@ export default function OfferDetail() {
|
||||
language: d.language || "EN",
|
||||
vat_rate: d.vat_rate ?? companySettings?.default_vat_rate ?? 21,
|
||||
apply_vat: !!d.apply_vat,
|
||||
scope_title: d.scope_title || "",
|
||||
scope_description: d.scope_description || "",
|
||||
};
|
||||
setForm(formData);
|
||||
const mappedItems =
|
||||
@@ -1252,12 +1239,59 @@ export default function OfferDetail() {
|
||||
<div className="admin-card-header flex-between">
|
||||
<h3 className="admin-card-title">Položky</h3>
|
||||
{!readOnly && (
|
||||
<button
|
||||
onClick={addItem}
|
||||
className="admin-btn admin-btn-secondary admin-btn-sm"
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "0.5rem",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
+ Přidat položku
|
||||
</button>
|
||||
{itemTemplates && itemTemplates.length > 0 && (
|
||||
<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>
|
||||
{errors.items && (
|
||||
@@ -1408,13 +1442,6 @@ export default function OfferDetail() {
|
||||
content: s.content || "",
|
||||
}));
|
||||
setSections((prev) => [...prev, ...newSections]);
|
||||
if (template.description) {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
scope_description:
|
||||
template.description || prev.scope_description,
|
||||
}));
|
||||
}
|
||||
alert.success(`Načtena šablona "${template.name}"`);
|
||||
}
|
||||
e.target.value = "";
|
||||
|
||||
@@ -186,7 +186,6 @@ function buildAddressLines(
|
||||
|
||||
const TRANSLATIONS: Record<string, Record<string, string>> = {
|
||||
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" },
|
||||
customer: { EN: "Customer", CZ: "Z\u00E1kazn\u00EDk" },
|
||||
supplier: { EN: "Supplier", CZ: "Dodavatel" },
|
||||
@@ -571,12 +570,6 @@ ${indentCSS}
|
||||
border-bottom: 2.5pt solid #de3a3a;
|
||||
padding-bottom: 1mm;
|
||||
}
|
||||
.totals .exchange-rate {
|
||||
text-align: right;
|
||||
font-size: 7.5pt;
|
||||
color: #969696;
|
||||
margin-top: 3mm;
|
||||
}
|
||||
|
||||
/* ---- Scope sections ---- */
|
||||
.scope-page {
|
||||
|
||||
Reference in New Issue
Block a user