feat(issued-orders): editable PDF heading 'Text objednavky (na PDF)' (order_text)

New nullable issued_orders.order_text column (migration applied to dev+test). The heading above the PDF items table is now editable per order, mirroring invoices' billing_text: empty falls back to the default, which is now 'Objednavame si u Vas:' per user wording. Field sits above Dodaci podminky in the detail form; persisted via create + update strFields (both schemas - the invoices Update-schema gap is not repeated). Tests: persistence round-trip incl. null-clears, PDF custom heading + default fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-10 12:16:40 +02:00
parent 638264fc7c
commit ca1f07671f
8 changed files with 69 additions and 2 deletions

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE `issued_orders` ADD COLUMN `order_text` VARCHAR(500) NULL;

View File

@@ -381,6 +381,7 @@ model issued_orders {
delivery_terms String? @db.VarChar(500) delivery_terms String? @db.VarChar(500)
payment_terms String? @db.VarChar(500) payment_terms String? @db.VarChar(500)
issued_by String? @db.VarChar(255) issued_by String? @db.VarChar(255)
order_text String? @db.VarChar(500)
notes String? @db.Text notes String? @db.Text
internal_notes String? @db.Text internal_notes String? @db.Text
created_at DateTime? @default(now()) @db.DateTime(0) created_at DateTime? @default(now()) @db.DateTime(0)

View File

@@ -170,6 +170,23 @@ describe("createIssuedOrder", () => {
const res = await createIssuedOrder({ supplier_id: 99999999 }); const res = await createIssuedOrder({ supplier_id: 99999999 });
expect("error" in res && res.error).toBe("supplier_not_found"); expect("error" in res && res.error).toBe("supplier_not_found");
}); });
it("persists order_text on create, updates and clears it on update", async () => {
const order = await mkIssued({ order_text: "Objednáváme dle smlouvy:" });
let row = await prisma.issued_orders.findUnique({
where: { id: order.id },
});
expect(row!.order_text).toBe("Objednáváme dle smlouvy:");
await updateIssuedOrder(order.id, { order_text: "Jiný text:" });
row = await prisma.issued_orders.findUnique({ where: { id: order.id } });
expect(row!.order_text).toBe("Jiný text:");
// null clears back to the PDF default.
await updateIssuedOrder(order.id, { order_text: null });
row = await prisma.issued_orders.findUnique({ where: { id: order.id } });
expect(row!.order_text).toBeNull();
});
}); });
describe("updateIssuedOrder status transitions", () => { describe("updateIssuedOrder status transitions", () => {
@@ -507,6 +524,29 @@ describe("renderIssuedOrderHtml", () => {
expect(html).not.toContain("alert(1)"); expect(html).not.toContain("alert(1)");
}); });
it("renders the custom order_text heading when set, default when not", () => {
const custom = renderIssuedOrderHtml(
{ ...order, order_text: "Objednáváme dle nabídky č. 123:" },
items,
null,
null,
"cs",
issuer,
);
expect(custom).toContain("Objednáváme dle nabídky č. 123:");
expect(custom).not.toContain("Objednáváme si u Vás:");
const fallback = renderIssuedOrderHtml(
order,
items,
null,
null,
"cs",
issuer,
);
expect(fallback).toContain("Objednáváme si u Vás:");
});
it("with VAT: keeps the VAT columns and the DPH cell holds ONLY the line VAT", () => { it("with VAT: keeps the VAT columns and the DPH cell holds ONLY the line VAT", () => {
const html = renderIssuedOrderHtml(order, items, null, null, "cs", issuer); const html = renderIssuedOrderHtml(order, items, null, null, "cs", issuer);
expect(html).toContain("%DPH"); expect(html).toContain("%DPH");

View File

@@ -45,6 +45,7 @@ export interface IssuedOrderDetail extends IssuedOrder {
delivery_terms: string | null; delivery_terms: string | null;
payment_terms: string | null; payment_terms: string | null;
issued_by: string | null; issued_by: string | null;
order_text: string | null;
notes: string | null; notes: string | null;
internal_notes: string | null; internal_notes: string | null;
items: IssuedOrderItem[]; items: IssuedOrderItem[];

View File

@@ -171,6 +171,7 @@ interface OrderForm {
delivery_terms: string; delivery_terms: string;
payment_terms: string; payment_terms: string;
issued_by: string; issued_by: string;
order_text: string;
notes: string; notes: string;
internal_notes: string; internal_notes: string;
status: string; status: string;
@@ -520,6 +521,7 @@ export default function IssuedOrderDetail() {
delivery_terms: "", delivery_terms: "",
payment_terms: "", payment_terms: "",
issued_by: user?.fullName || "", issued_by: user?.fullName || "",
order_text: "",
notes: "", notes: "",
internal_notes: "", internal_notes: "",
status: "draft", status: "draft",
@@ -621,6 +623,7 @@ export default function IssuedOrderDetail() {
delivery_terms: d.delivery_terms || "", delivery_terms: d.delivery_terms || "",
payment_terms: d.payment_terms || "", payment_terms: d.payment_terms || "",
issued_by: d.issued_by || "", issued_by: d.issued_by || "",
order_text: d.order_text || "",
notes: d.notes || "", notes: d.notes || "",
internal_notes: d.internal_notes || "", internal_notes: d.internal_notes || "",
status: d.status, status: d.status,
@@ -779,6 +782,7 @@ export default function IssuedOrderDetail() {
delivery_terms: form.delivery_terms, delivery_terms: form.delivery_terms,
payment_terms: form.payment_terms, payment_terms: form.payment_terms,
issued_by: form.issued_by, issued_by: form.issued_by,
order_text: form.order_text || null,
notes: form.notes, notes: form.notes,
internal_notes: form.internal_notes, internal_notes: form.internal_notes,
items: items items: items
@@ -1419,6 +1423,16 @@ export default function IssuedOrderDetail() {
{/* Notes & terms */} {/* Notes & terms */}
<Card sx={{ mb: 3 }}> <Card sx={{ mb: 3 }}>
<Field label="Text objednávky (na PDF)">
<TextField
value={form.order_text}
disabled={!editable}
onChange={(e) =>
setForm((prev) => ({ ...prev, order_text: e.target.value }))
}
placeholder="Objednáváme si u Vás: (ponechte prázdné pro výchozí)"
/>
</Field>
<Field label="Dodací podmínky"> <Field label="Dodací podmínky">
<TextField <TextField
value={form.delivery_terms} value={form.delivery_terms}

View File

@@ -195,7 +195,7 @@ const translations: Record<Lang, Record<string, string>> = {
buyer: "Odběratel", buyer: "Odběratel",
issue_date: "Datum vystavení:", issue_date: "Datum vystavení:",
delivery_date: "Požadované dodání:", delivery_date: "Požadované dodání:",
billing: "Objednáváme u Vás:", billing: "Objednáváme si u Vás:",
col_no: "Č.", col_no: "Č.",
col_desc: "Popis", col_desc: "Popis",
col_qty: "Množství", col_qty: "Množství",
@@ -257,6 +257,8 @@ interface IssuedOrderPdfData {
delivery_terms: string | null; delivery_terms: string | null;
payment_terms: string | null; payment_terms: string | null;
issued_by: string | null; issued_by: string | null;
// Editable heading above the items table; null → t.billing default.
order_text?: string | null;
} }
interface IssuedOrderPdfItem { interface IssuedOrderPdfItem {
@@ -771,7 +773,7 @@ ${indentCSS}
</table> </table>
<!-- Polozky --> <!-- Polozky -->
<div class="billing-label">${escapeHtml(t.billing)}</div> <div class="billing-label">${escapeHtml(order.order_text || t.billing)}</div>
<table class="items"> <table class="items">
<thead> <thead>
<tr> <tr>

View File

@@ -40,6 +40,9 @@ export const CreateIssuedOrderSchema = z.object({
delivery_terms: z.string().max(500).nullish(), delivery_terms: z.string().max(500).nullish(),
payment_terms: z.string().max(500).nullish(), payment_terms: z.string().max(500).nullish(),
issued_by: z.string().max(255).nullish(), issued_by: z.string().max(255).nullish(),
// Editable heading above the PDF items table; empty/null falls back to the
// default "Objednáváme si u Vás:" (issued-orders-pdf t.billing).
order_text: z.string().max(500).nullish(),
notes: z.string().nullish(), notes: z.string().nullish(),
internal_notes: z.string().nullish(), internal_notes: z.string().nullish(),
items: z.array(IssuedOrderItemSchema).optional(), items: z.array(IssuedOrderItemSchema).optional(),

View File

@@ -32,6 +32,7 @@ export interface IssuedOrderInput {
delivery_terms?: string | null; delivery_terms?: string | null;
payment_terms?: string | null; payment_terms?: string | null;
issued_by?: string | null; issued_by?: string | null;
order_text?: string | null;
notes?: string | null; notes?: string | null;
internal_notes?: string | null; internal_notes?: string | null;
items?: IssuedOrderItemInput[]; items?: IssuedOrderItemInput[];
@@ -304,6 +305,7 @@ export async function createIssuedOrder(body: IssuedOrderInput) {
: null, : null,
payment_terms: body.payment_terms ? String(body.payment_terms) : null, payment_terms: body.payment_terms ? String(body.payment_terms) : null,
issued_by: body.issued_by ? String(body.issued_by) : null, issued_by: body.issued_by ? String(body.issued_by) : null,
order_text: body.order_text ? String(body.order_text) : null,
notes: body.notes ? String(body.notes) : null, notes: body.notes ? String(body.notes) : null,
internal_notes: body.internal_notes internal_notes: body.internal_notes
? String(body.internal_notes) ? String(body.internal_notes)
@@ -354,6 +356,7 @@ export async function updateIssuedOrder(id: number, body: IssuedOrderInput) {
"delivery_terms", "delivery_terms",
"payment_terms", "payment_terms",
"issued_by", "issued_by",
"order_text",
]; ];
for (const f of strFields) { for (const f of strFields) {
if (body[f] !== undefined) data[f] = body[f] ? String(body[f]) : null; if (body[f] !== undefined) data[f] = body[f] ? String(body[f]) : null;