v1.7.0: fix offer created_at override, invoice PDF translations, remove has_order filter

- Add created_at to offer create/update schemas so user-selected date is stored instead of always defaulting to now()
- Translate payment method in invoice PDF based on language (Příkazem → Bank transfer, etc.)
- Add order_date and cnb_rate translation keys, replace hardcoded inline strings
- Remove duplicate has_order filter from offers backend route

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-05-28 13:35:36 +02:00
parent 1f5885de84
commit 2254a13ad0
6 changed files with 40 additions and 22 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "app-ts", "name": "app-ts",
"version": "1.6.9", "version": "1.7.0",
"description": "", "description": "",
"main": "dist/server.js", "main": "dist/server.js",
"scripts": { "scripts": {

View File

@@ -73,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],
@@ -87,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}` : ""}`);
}, },

View File

@@ -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>`

View File

@@ -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(

View File

@@ -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(),

View File

@@ -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