Files
hwinkel b22e5baa5c
Build and Push Docker Image / build (push) Successful in 1m23s
feat: add client-side validation utilities and debugging tools
- Implemented client-side validation functions for tax ID, VAT ID, IBAN, BIC, and website URL.
- Added debug logging functionality to assist in development.
- Created a comprehensive validation function for company form data.

feat: initialize database with Prisma migrations

- Added a server-side script to run Prisma migrations and check database health.
- Ensured safe initialization of the database to prevent concurrent migrations.

feat: comprehensive server-side error logging

- Developed an error logging system that captures detailed error context, including request details and stack traces.
- Implemented logging functions for different error types (route, action, database, API, startup).

fix: validate user ID existence in audit logs

- Updated the logging function to validate that the user ID exists in the database before logging actions.

fix: update schemas for optional fields and validation

- Modified schemas to allow for nullable fields and refined validation logic for tax ID, VAT ID, IBAN, and BIC.

feat: enhance error boundary for better debugging

- Improved error boundary to log detailed error information in development mode.
- Added a debug panel to the main application layout for real-time error tracking.

feat: implement company deletion functionality in admin routes

- Added a new API route for deleting companies with appropriate logging.
- Integrated delete confirmation in the admin interface for better user experience.

fix: handle API errors gracefully

- Wrapped API actions in try-catch blocks to log errors and return appropriate responses.

feat: generate and save invoice PDFs

- Implemented functionality to generate and save invoice PDFs upon status updates.
- Added a new column in the database for storing the URL of the generated PDF.

chore: update Docker image reference

- Changed the Docker image reference to point to the new Git repository.

chore: update package dependencies

- Added @radix-ui/react-tooltip for enhanced UI components.
- Updated package-lock.json to reflect new dependencies.
2026-05-03 08:46:58 +02:00

240 lines
6.2 KiB
TypeScript

import { z } from "zod";
import { InvoiceStatus } from "@prisma/client";
// ===== Reusable validators =====
/**
* Validates that a decimal string has at most 2 decimal places
* (required for currency/money fields in MySQL DECIMAL(10,2))
*/
export const currencySchema = z
.number()
.nonnegative("Geldbeträge dürfen nicht negativ sein")
.refine(
(n) => {
const decimal = n.toString().split(".")[1];
return !decimal || decimal.length <= 2;
},
"Geldbeträge dürfen maximal 2 Dezimalstellen haben"
);
/**
* Tax rate must be one of the valid German VAT rates
*/
export const taxRateSchema = z
.number()
.int("Steuersatz muss eine ganze Zahl sein")
.refine(
(r) => [0, 7, 19].includes(r),
"Steuersatz muss 0 (steuerfrei), 7 (ermäßigt) oder 19 (Standard) sein"
);
/**
* IBAN validation: 15-34 characters, starts with 2 letters + 2 digits
*/
export const ibanSchema = z
.string()
.refine(
(iban) => iban === "" || /^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$/.test(iban),
"Ungültige IBAN"
);
/**
* German tax ID (Steuernummer): 10 digits
*/
export const taxIdSchema = z
.string()
.refine(
(val) => val === "" || /^\d{10}$/.test(val),
"Steuernummer muss 10 Ziffern haben"
);
/**
* VAT ID (Umsatzsteuer-IdNr): DE + 9 digits
*/
export const vatIdSchema = z
.string()
.refine(
(val) => val === "" || /^DE\d{9}$/.test(val),
"USt-IdNr. muss im Format DE + 9 Ziffern sein"
);
// ===== Invoice Schemas =====
export const invoiceItemSchema = z.object({
position: z
.number()
.int("Position muss eine ganze Zahl sein")
.positive("Position muss größer als 0 sein"),
description: z
.string()
.min(1, "Beschreibung erforderlich")
.max(500, "Beschreibung darf maximal 500 Zeichen sein"),
quantity: z
.number()
.positive("Menge muss größer als 0 sein")
.refine(
(q) => {
const decimal = q.toString().split(".")[1];
return !decimal || decimal.length <= 3;
},
"Menge darf maximal 3 Dezimalstellen haben"
),
unit: z
.string()
.max(50, "Einheit darf maximal 50 Zeichen sein")
.optional(),
unitPrice: currencySchema,
taxRate: taxRateSchema,
netAmount: currencySchema,
taxAmount: currencySchema,
grossAmount: currencySchema,
});
export const invoiceSchema = z.object({
companyId: z.string().min(1, "Mandant erforderlich"),
customerId: z.string().min(1, "Kunde erforderlich"),
issueDate: z
.string()
.refine(
(d) => !isNaN(Date.parse(d)),
"Ungültiges Datum"
),
deliveryDate: z
.string()
.refine((d) => !isNaN(Date.parse(d)), "Ungültiges Datum")
.optional(),
dueDate: z
.string()
.refine((d) => !isNaN(Date.parse(d)), "Ungültiges Datum"),
notes: z
.string()
.max(5000, "Notizen darf maximal 5000 Zeichen sein")
.optional(),
kleinunternehmer: z.boolean().optional().default(false),
items: z
.array(invoiceItemSchema)
.min(1, "Mindestens ein Rechnungsposition erforderlich"),
netTotal: currencySchema,
taxTotal: currencySchema,
grossTotal: currencySchema,
});
export const invoiceUpdateSchema = invoiceSchema.omit({ companyId: true });
export const invoiceStatusSchema = z.object({
status: z.nativeEnum(InvoiceStatus),
});
// ===== Company Schemas =====
export const companySchema = z.object({
name: z
.string()
.min(1, "Firmenname erforderlich")
.max(255, "Firmenname darf maximal 255 Zeichen sein"),
legalForm: z
.string()
.max(100, "Rechtsform darf maximal 100 Zeichen sein")
.optional(),
taxId: taxIdSchema.nullable(),
vatId: vatIdSchema.nullable(),
address: z
.string()
.min(1, "Adresse erforderlich")
.max(500, "Adresse darf maximal 500 Zeichen sein"),
zip: z
.string()
.min(1, "PLZ erforderlich")
.max(20, "PLZ darf maximal 20 Zeichen sein")
.regex(/^[\d\s-]*$/, "PLZ darf nur Zahlen, Bindestriche und Leerzeichen enthalten"),
city: z
.string()
.min(1, "Stadt erforderlich")
.max(100, "Stadt darf maximal 100 Zeichen sein"),
country: z
.string()
.max(2, "Ländercode darf maximal 2 Zeichen sein")
.optional()
.default("DE"),
email: z
.string()
.email("Ungültige E-Mail-Adresse")
.optional()
.or(z.literal("")),
phone: z
.string()
.max(20, "Telefonnummer darf maximal 20 Zeichen sein")
.optional(),
website: z
.string()
.refine(
(val) => val === "" || /^https?:\/\//.test(val),
"Website muss mit http:// oder https:// beginnen"
)
.max(255, "Website darf maximal 255 Zeichen sein")
.optional()
.or(z.literal("")),
bankIban: ibanSchema.nullable(),
bankBic: z
.string()
.refine(
(val) => val === "" || /^[A-Z0-9]{8,11}$/.test(val),
"Ungültiger BIC"
)
.optional()
.or(z.literal("")),
bankName: z
.string()
.max(255, "Bankname darf maximal 255 Zeichen sein")
.optional(),
invoicePrefix: z
.string()
.max(10, "Rechnungsprefix darf maximal 10 Zeichen sein")
.optional()
.default("RE"),
kleinunternehmer: z.boolean().optional().default(false),
});
export const companyUpdateSchema = companySchema;
// ===== Customer Schemas =====
export const customerSchema = z.object({
companyId: z.string().min(1, "Mandant erforderlich"),
name: z
.string()
.min(1, "Kundenname erforderlich")
.max(255, "Kundenname darf maximal 255 Zeichen sein"),
taxId: taxIdSchema.optional(),
address: z
.string()
.min(1, "Adresse erforderlich")
.max(500, "Adresse darf maximal 500 Zeichen sein"),
zip: z
.string()
.min(1, "PLZ erforderlich")
.max(20, "PLZ darf maximal 20 Zeichen sein")
.regex(/^[\d\s-]*$/, "PLZ darf nur Zahlen, Bindestriche und Leerzeichen enthalten"),
city: z
.string()
.min(1, "Stadt erforderlich")
.max(100, "Stadt darf maximal 100 Zeichen sein"),
country: z
.string()
.max(2, "Ländercode darf maximal 2 Zeichen sein")
.optional()
.default("DE"),
email: z
.string()
.email("Ungültige E-Mail-Adresse")
.optional()
.or(z.literal("")),
phone: z
.string()
.max(20, "Telefonnummer darf maximal 20 Zeichen sein")
.optional(),
});
export const customerUpdateSchema = customerSchema.omit({ companyId: true });