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

@@ -170,6 +170,23 @@ describe("createIssuedOrder", () => {
const res = await createIssuedOrder({ supplier_id: 99999999 });
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", () => {
@@ -507,6 +524,29 @@ describe("renderIssuedOrderHtml", () => {
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", () => {
const html = renderIssuedOrderHtml(order, items, null, null, "cs", issuer);
expect(html).toContain("%DPH");

View File

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

View File

@@ -171,6 +171,7 @@ interface OrderForm {
delivery_terms: string;
payment_terms: string;
issued_by: string;
order_text: string;
notes: string;
internal_notes: string;
status: string;
@@ -520,6 +521,7 @@ export default function IssuedOrderDetail() {
delivery_terms: "",
payment_terms: "",
issued_by: user?.fullName || "",
order_text: "",
notes: "",
internal_notes: "",
status: "draft",
@@ -621,6 +623,7 @@ export default function IssuedOrderDetail() {
delivery_terms: d.delivery_terms || "",
payment_terms: d.payment_terms || "",
issued_by: d.issued_by || "",
order_text: d.order_text || "",
notes: d.notes || "",
internal_notes: d.internal_notes || "",
status: d.status,
@@ -779,6 +782,7 @@ export default function IssuedOrderDetail() {
delivery_terms: form.delivery_terms,
payment_terms: form.payment_terms,
issued_by: form.issued_by,
order_text: form.order_text || null,
notes: form.notes,
internal_notes: form.internal_notes,
items: items
@@ -1419,6 +1423,16 @@ export default function IssuedOrderDetail() {
{/* Notes & terms */}
<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">
<TextField
value={form.delivery_terms}

View File

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

View File

@@ -40,6 +40,9 @@ export const CreateIssuedOrderSchema = z.object({
delivery_terms: z.string().max(500).nullish(),
payment_terms: z.string().max(500).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(),
internal_notes: z.string().nullish(),
items: z.array(IssuedOrderItemSchema).optional(),

View File

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