Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2254a13ad0 | ||
|
|
1f5885de84 | ||
|
|
705f58e3d1 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "app-ts",
|
"name": "app-ts",
|
||||||
"version": "1.6.8",
|
"version": "1.7.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/server.js",
|
"main": "dist/server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -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 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")
|
||||||
|
|||||||
@@ -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[];
|
||||||
}
|
}
|
||||||
@@ -72,7 +73,6 @@ export const offerListOptions = (filters: {
|
|||||||
perPage?: number;
|
perPage?: number;
|
||||||
status?: string;
|
status?: string;
|
||||||
customer_id?: number;
|
customer_id?: number;
|
||||||
has_order?: string;
|
|
||||||
}) =>
|
}) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ["offers", "list", filters],
|
queryKey: ["offers", "list", filters],
|
||||||
@@ -86,7 +86,6 @@ export const offerListOptions = (filters: {
|
|||||||
if (filters.status) params.set("status", filters.status);
|
if (filters.status) params.set("status", filters.status);
|
||||||
if (filters.customer_id)
|
if (filters.customer_id)
|
||||||
params.set("customer_id", String(filters.customer_id));
|
params.set("customer_id", String(filters.customer_id));
|
||||||
if (filters.has_order) params.set("has_order", filters.has_order);
|
|
||||||
const qs = params.toString();
|
const qs = params.toString();
|
||||||
return paginatedJsonQuery(`/api/admin/offers${qs ? `?${qs}` : ""}`);
|
return paginatedJsonQuery(`/api/admin/offers${qs ? `?${qs}` : ""}`);
|
||||||
},
|
},
|
||||||
@@ -134,8 +133,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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "0.5rem",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{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
|
<button
|
||||||
onClick={addItem}
|
onClick={addItem}
|
||||||
className="admin-btn admin-btn-secondary admin-btn-sm"
|
className="admin-btn admin-btn-secondary admin-btn-sm"
|
||||||
>
|
>
|
||||||
+ Přidat položku
|
+ Přidat položku
|
||||||
</button>
|
</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 = "";
|
||||||
|
|||||||
@@ -168,6 +168,24 @@ function buildAddressLines(
|
|||||||
|
|
||||||
/* ── Translations ────────────────────────────────────────────────── */
|
/* ── Translations ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
const paymentMethodMap: Record<string, Record<string, string>> = {
|
||||||
|
cs: {
|
||||||
|
"Bank transfer": "Příkazem",
|
||||||
|
Cash: "Hotově",
|
||||||
|
"Cash on delivery": "Dobírka",
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
Příkazem: "Bank transfer",
|
||||||
|
Hotově: "Cash",
|
||||||
|
Dobírka: "Cash on delivery",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function translatePaymentMethod(value: string | null, lang: string): string {
|
||||||
|
if (!value) return "";
|
||||||
|
return paymentMethodMap[lang]?.[value] || value;
|
||||||
|
}
|
||||||
|
|
||||||
const translations: Record<string, Record<string, string>> = {
|
const translations: Record<string, Record<string, string>> = {
|
||||||
cs: {
|
cs: {
|
||||||
title: "Faktura",
|
title: "Faktura",
|
||||||
@@ -181,6 +199,7 @@ const translations: Record<string, Record<string, string>> = {
|
|||||||
var_symbol: "Variabilní s.:",
|
var_symbol: "Variabilní s.:",
|
||||||
const_symbol: "Konstantní s.:",
|
const_symbol: "Konstantní s.:",
|
||||||
order_no: "Objednávka č.:",
|
order_no: "Objednávka č.:",
|
||||||
|
order_date: "Objednávka ze dne:",
|
||||||
issue_date: "Datum vystavení:",
|
issue_date: "Datum vystavení:",
|
||||||
due_date: "Datum splatnosti:",
|
due_date: "Datum splatnosti:",
|
||||||
tax_date: "Datum uskutečnění plnění:",
|
tax_date: "Datum uskutečnění plnění:",
|
||||||
@@ -213,6 +232,7 @@ const translations: Record<string, Record<string, string>> = {
|
|||||||
stamp: "Razítko:",
|
stamp: "Razítko:",
|
||||||
ico: "IČ: ",
|
ico: "IČ: ",
|
||||||
dic: "DIČ: ",
|
dic: "DIČ: ",
|
||||||
|
cnb_rate: "Přepočet kurzem ČNB ke dni",
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
title: "Invoice",
|
title: "Invoice",
|
||||||
@@ -226,6 +246,7 @@ const translations: Record<string, Record<string, string>> = {
|
|||||||
var_symbol: "Variable symbol:",
|
var_symbol: "Variable symbol:",
|
||||||
const_symbol: "Constant symbol:",
|
const_symbol: "Constant symbol:",
|
||||||
order_no: "Order No.:",
|
order_no: "Order No.:",
|
||||||
|
order_date: "Order date:",
|
||||||
issue_date: "Issue date:",
|
issue_date: "Issue date:",
|
||||||
due_date: "Due date:",
|
due_date: "Due date:",
|
||||||
tax_date: "Tax point date:",
|
tax_date: "Tax point date:",
|
||||||
@@ -257,6 +278,7 @@ const translations: Record<string, Record<string, string>> = {
|
|||||||
stamp: "Stamp:",
|
stamp: "Stamp:",
|
||||||
ico: "Reg. No.: ",
|
ico: "Reg. No.: ",
|
||||||
dic: "Tax ID: ",
|
dic: "Tax ID: ",
|
||||||
|
cnb_rate: "CNB exchange rate as of",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -923,9 +945,9 @@ ${indentCSS}
|
|||||||
<div class="info-row"><span class="lbl">${escapeHtml(t.issue_date)}</span> <span class="val">${escapeHtml(formatDate(invoice.issue_date))}</span></div>
|
<div class="info-row"><span class="lbl">${escapeHtml(t.issue_date)}</span> <span class="val">${escapeHtml(formatDate(invoice.issue_date))}</span></div>
|
||||||
<div class="info-row"><span class="lbl">${escapeHtml(t.due_date)}</span> <span class="val">${escapeHtml(formatDate(invoice.due_date))}</span></div>
|
<div class="info-row"><span class="lbl">${escapeHtml(t.due_date)}</span> <span class="val">${escapeHtml(formatDate(invoice.due_date))}</span></div>
|
||||||
<div class="info-row"><span class="lbl">${escapeHtml(t.tax_date)}</span> <span class="val">${escapeHtml(formatDate(invoice.tax_date))}</span></div>
|
<div class="info-row"><span class="lbl">${escapeHtml(t.tax_date)}</span> <span class="val">${escapeHtml(formatDate(invoice.tax_date))}</span></div>
|
||||||
<div class="info-row"><span class="lbl">${escapeHtml(t.payment_method)}</span> <span class="val">${escapeHtml(invoice.payment_method)}</span></div>
|
<div class="info-row"><span class="lbl">${escapeHtml(t.payment_method)}</span> <span class="val">${escapeHtml(translatePaymentMethod(invoice.payment_method, lang))}</span></div>
|
||||||
${orderNumber ? `<div class="info-row"><span class="lbl">${lang === "cs" ? "Objednávka č.:" : "Order no.:"}</span> <span class="val">${orderNumber}</span></div>` : ""}
|
${orderNumber ? `<div class="info-row"><span class="lbl">${escapeHtml(t.order_no)}</span> <span class="val">${orderNumber}</span></div>` : ""}
|
||||||
${orderDate ? `<div class="info-row"><span class="lbl">${lang === "cs" ? "Objednávka ze dne:" : "Order date:"}</span> <span class="val">${escapeHtml(orderDate)}</span></div>` : ""}
|
${orderDate ? `<div class="info-row"><span class="lbl">${escapeHtml(t.order_date)}</span> <span class="val">${escapeHtml(orderDate)}</span></div>` : ""}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -1008,7 +1030,7 @@ ${indentCSS}
|
|||||||
? `<tfoot>
|
? `<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" style="font-size:0.7em; color:#666; padding-top:6px; text-align:left;">
|
<td colspan="4" style="font-size:0.7em; color:#666; padding-top:6px; text-align:left;">
|
||||||
Přepočet kurzem ČNB ke dni ${formatDate(invoice.issue_date)}: 1 ${escapeHtml(currency)} = ${cnbRate.toFixed(3).replace(".", ",")} CZK
|
${escapeHtml(t.cnb_rate)} ${formatDate(invoice.issue_date)}: 1 ${escapeHtml(currency)} = ${cnbRate.toFixed(3).replace(".", ",")} CZK
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>`
|
</tfoot>`
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ export default async function quotationsRoutes(
|
|||||||
search,
|
search,
|
||||||
status: query.status ? String(query.status) : undefined,
|
status: query.status ? String(query.status) : undefined,
|
||||||
customer_id: query.customer_id ? Number(query.customer_id) : undefined,
|
customer_id: query.customer_id ? Number(query.customer_id) : undefined,
|
||||||
has_order: query.has_order ? String(query.has_order) : undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return paginated(
|
return paginated(
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export const CreateQuotationSchema = z.object({
|
|||||||
.transform((v) => Number(v))
|
.transform((v) => Number(v))
|
||||||
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
|
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
|
||||||
.nullish(),
|
.nullish(),
|
||||||
|
created_at: z.string().nullish(),
|
||||||
valid_until: z.string().nullish(),
|
valid_until: z.string().nullish(),
|
||||||
currency: z.string().optional().default("CZK"),
|
currency: z.string().optional().default("CZK"),
|
||||||
language: z.string().optional().default("cs"),
|
language: z.string().optional().default("cs"),
|
||||||
@@ -76,6 +77,7 @@ export const UpdateQuotationSchema = z.object({
|
|||||||
.transform((v) => Number(v))
|
.transform((v) => Number(v))
|
||||||
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
|
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
|
||||||
.optional(),
|
.optional(),
|
||||||
|
created_at: z.union([z.string(), z.null()]).optional(),
|
||||||
valid_until: z.union([z.string(), z.null()]).optional(),
|
valid_until: z.union([z.string(), z.null()]).optional(),
|
||||||
currency: z.string().optional(),
|
currency: z.string().optional(),
|
||||||
language: z.string().optional(),
|
language: z.string().optional(),
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ interface ListOffersParams {
|
|||||||
search: string;
|
search: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
customer_id?: number;
|
customer_id?: number;
|
||||||
has_order?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function enrichQuotation(q: any) {
|
function enrichQuotation(q: any) {
|
||||||
@@ -73,24 +72,13 @@ function enrichQuotation(q: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function listOffers(params: ListOffersParams) {
|
export async function listOffers(params: ListOffersParams) {
|
||||||
const {
|
const { page, limit, skip, sort, order, search, status, customer_id } =
|
||||||
page,
|
params;
|
||||||
limit,
|
|
||||||
skip,
|
|
||||||
sort,
|
|
||||||
order,
|
|
||||||
search,
|
|
||||||
status,
|
|
||||||
customer_id,
|
|
||||||
has_order,
|
|
||||||
} = params;
|
|
||||||
const sortField = ALLOWED_SORT_FIELDS.includes(sort) ? sort : "id";
|
const sortField = ALLOWED_SORT_FIELDS.includes(sort) ? sort : "id";
|
||||||
|
|
||||||
const where: Record<string, unknown> = {};
|
const where: Record<string, unknown> = {};
|
||||||
if (status) where.status = status;
|
if (status) where.status = status;
|
||||||
if (customer_id) where.customer_id = customer_id;
|
if (customer_id) where.customer_id = customer_id;
|
||||||
if (has_order === "yes") where.order_id = { not: null };
|
|
||||||
if (has_order === "no") where.order_id = null;
|
|
||||||
if (search) {
|
if (search) {
|
||||||
where.OR = [
|
where.OR = [
|
||||||
{ quotation_number: { contains: search } },
|
{ quotation_number: { contains: search } },
|
||||||
@@ -189,6 +177,9 @@ export async function createOffer(body: Record<string, any>) {
|
|||||||
quotation_number: quotationNumber,
|
quotation_number: quotationNumber,
|
||||||
project_code: body.project_code ? String(body.project_code) : null,
|
project_code: body.project_code ? String(body.project_code) : null,
|
||||||
customer_id: body.customer_id ? Number(body.customer_id) : null,
|
customer_id: body.customer_id ? Number(body.customer_id) : null,
|
||||||
|
created_at: body.created_at
|
||||||
|
? new Date(String(body.created_at))
|
||||||
|
: undefined,
|
||||||
valid_until: body.valid_until
|
valid_until: body.valid_until
|
||||||
? new Date(String(body.valid_until))
|
? new Date(String(body.valid_until))
|
||||||
: null,
|
: null,
|
||||||
@@ -251,6 +242,12 @@ export async function updateOffer(id: number, body: Record<string, any>) {
|
|||||||
const data = {
|
const data = {
|
||||||
customer_id:
|
customer_id:
|
||||||
body.customer_id !== undefined ? Number(body.customer_id) : undefined,
|
body.customer_id !== undefined ? Number(body.customer_id) : undefined,
|
||||||
|
created_at:
|
||||||
|
body.created_at !== undefined
|
||||||
|
? body.created_at
|
||||||
|
? new Date(String(body.created_at))
|
||||||
|
: null
|
||||||
|
: undefined,
|
||||||
valid_until:
|
valid_until:
|
||||||
body.valid_until !== undefined
|
body.valid_until !== undefined
|
||||||
? body.valid_until
|
? body.valid_until
|
||||||
|
|||||||
Reference in New Issue
Block a user