Files
app/src/schemas/invoices.schema.ts
BOHA 40a859f5e1 feat(invoices)!: Obsah sections, internal-only notes, unified per-page PDF header+footer
Invoices now mirror the issued-orders document model:

- New invoice_sections table (CZ/EN rich-text "Obsah") edited via the
  shared SectionsEditor, printed inline right after the items on the
  PDF. Full-replace on update, same transaction as items.
- Printed notes dropped: the notes column is removed (migration merges
  existing content into internal_notes first); the form field is now
  "Interni poznamky", never printed. Legacy payloads sending notes are
  silently stripped.
- Form cleanup: Cislo faktury and Vystavil fields removed (number lives
  in the header, issued_by auto-fills); page header title restyled to
  the orders/offers pattern (number span + status chip).
- Unified per-page PDF header for the red-accent family: shared
  buildPdfHeaderTemplate in pdf-shared (22mm logo, red heading, red
  rule) rendered by a Puppeteer headerTemplate on EVERY page of both
  invoices and issued orders (incl. the /file fallback render); body
  headers are print-hidden. htmlToPdf gained the headerTemplate option.
- Footer parity: invoices get the per-page "Vystavil + Strana X z Y"
  footer; the invoice bottom block (notice + QR/VAT recap + Prevzal)
  is break-inside: avoid so a page break can never split it.
- @page margins now match the template space (32mm top, 18mm bottom) -
  Chromium lays out by CSS @page margins, which also fixes issued
  orders' content running into the 18mm footer zone on full pages.

BREAKING CHANGE: invoices.notes column dropped (data merged into
internal_notes); deploy must run prisma migrate deploy + generate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 19:02:37 +02:00

78 lines
3.3 KiB
TypeScript

import { z } from "zod";
import {
numberFromForm,
numberInRange,
nonNegativeNumberFromForm,
positiveNumberFromForm,
nullableIntIdFromForm,
booleanFromForm,
DocumentSectionSchema,
} from "./common";
const InvoiceItemSchema = z.object({
description: z.string().max(8000).nullish(),
item_description: z.string().max(8000).nullish(),
quantity: positiveNumberFromForm.default(1),
unit: z.string().max(255).nullish(),
unit_price: numberFromForm.default(0),
vat_rate: numberInRange(0, 100).optional().default(21.0),
position: nonNegativeNumberFromForm.optional(),
});
export const CreateInvoiceSchema = z.object({
invoice_number: z.string().max(255).nullish(),
order_id: nullableIntIdFromForm.nullish(),
customer_id: nullableIntIdFromForm.nullish(),
status: z.string().max(20).optional().default("draft"),
currency: z.string().max(20).optional().default("CZK"),
vat_rate: numberInRange(0, 100).optional().default(21.0),
apply_vat: booleanFromForm.optional().default(true),
payment_method: z.string().max(255).nullish(),
constant_symbol: z.string().max(255).nullish(),
bank_name: z.string().max(255).nullish(),
bank_swift: z.string().max(255).nullish(),
bank_iban: z.string().max(255).nullish(),
bank_account: z.string().max(255).nullish(),
issue_date: z.string().max(255).nullish(),
due_date: z.string().max(255).nullish(),
tax_date: z.string().max(255).nullish(),
issued_by: z.string().max(255).nullish(),
billing_text: z.string().max(8000).nullish(),
language: z.string().max(20).optional().default("cs"),
// `notes` was dropped (printed-notes block removed — free-form PDF content
// lives in the sections); legacy clients still sending it are silently
// stripped by z.object. internal_notes is never printed.
internal_notes: z.string().max(8000).nullish(),
items: z.array(InvoiceItemSchema).optional(),
// Rich-text CZ/EN "Obsah" sections rendered after the items on the PDF
// (mirrors issued orders — shared DocumentSectionSchema, DB-aligned limits).
sections: z.array(DocumentSectionSchema).optional(),
});
export const UpdateInvoiceSchema = z.object({
status: z.string().max(20).optional(),
currency: z.string().max(20).optional(),
language: z.string().max(20).optional(),
payment_method: z.string().max(255).nullish(),
constant_symbol: z.string().max(255).nullish(),
bank_name: z.string().max(255).nullish(),
bank_swift: z.string().max(255).nullish(),
bank_iban: z.string().max(255).nullish(),
bank_account: z.string().max(255).nullish(),
issued_by: z.string().max(255).nullish(),
billing_text: z.string().max(8000).nullish(),
customer_id: nullableIntIdFromForm.optional(),
vat_rate: numberInRange(0, 100).optional(),
apply_vat: booleanFromForm.optional(),
issue_date: z.union([z.string().max(255), z.null()]).optional(),
due_date: z.union([z.string().max(255), z.null()]).optional(),
tax_date: z.union([z.string().max(255), z.null()]).optional(),
internal_notes: z.string().max(8000).nullish(),
paid_date: z.union([z.string().max(255), z.null()]).optional(),
items: z.array(InvoiceItemSchema).optional(),
sections: z.array(DocumentSectionSchema).optional(),
});
export type CreateInvoiceInput = z.infer<typeof CreateInvoiceSchema>;
export type UpdateInvoiceInput = z.infer<typeof UpdateInvoiceSchema>;