feat: add client-side validation utilities and debugging tools
Build and Push Docker Image / build (push) Successful in 1m23s

- 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.
This commit is contained in:
hwinkel
2026-05-03 08:46:58 +02:00
parent c3e7a97c8a
commit b22e5baa5c
26 changed files with 1573 additions and 134 deletions
+244
View File
@@ -0,0 +1,244 @@
/**
* Client-side validation and debugging utilities
* Mirrors server-side validation for real-time user feedback
*/
// Debug mode - enable via localStorage: localStorage.setItem('DEBUG_MODE', 'true')
export function isDebugMode(): boolean {
if (typeof window === "undefined") return false;
return localStorage.getItem("DEBUG_MODE") === "true";
}
export function setDebugMode(enabled: boolean): void {
if (typeof window === "undefined") return;
if (enabled) {
localStorage.setItem("DEBUG_MODE", "true");
console.log("✅ DEBUG MODE ENABLED");
} else {
localStorage.removeItem("DEBUG_MODE");
console.log("❌ DEBUG MODE DISABLED");
}
}
/**
* Log error to browser console (only in debug mode)
*/
export function debugLog(type: string, message: string, data?: unknown): void {
if (!isDebugMode()) return;
const style = {
error: "color: #ef4444; font-weight: bold;",
warning: "color: #f97316; font-weight: bold;",
info: "color: #3b82f6; font-weight: bold;",
success: "color: #22c55e; font-weight: bold;",
};
const typeStyle = style[type as keyof typeof style] || style.info;
console.log(`%c[${type.toUpperCase()}] ${message}`, typeStyle, data);
}
/**
* Validation error type
*/
export interface ValidationError {
field: string;
message: string;
}
/**
* Validate tax ID (Steuernummer): 10 digits
*/
export function validateTaxId(val: string | null | undefined): ValidationError | null {
if (!val || val === "") return null;
if (!/^\d{10}$/.test(val)) {
return { field: "taxId", message: "Steuernummer muss 10 Ziffern haben" };
}
return null;
}
/**
* Validate VAT ID (USt-IdNr): DE + 9 digits
*/
export function validateVatId(val: string | null | undefined): ValidationError | null {
if (!val || val === "") return null;
if (!/^DE\d{9}$/.test(val)) {
return { field: "vatId", message: "USt-IdNr. muss im Format DE + 9 Ziffern sein" };
}
return null;
}
/**
* Validate IBAN
*/
export function validateIban(val: string | null | undefined): ValidationError | null {
if (!val || val === "") return null;
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$/.test(val)) {
return { field: "bankIban", message: "Ungültige IBAN" };
}
return null;
}
/**
* Validate BIC
*/
export function validateBic(val: string | null | undefined): ValidationError | null {
if (!val || val === "") return null;
if (!/^[A-Z0-9]{8,11}$/.test(val)) {
return { field: "bankBic", message: "Ungültiger BIC" };
}
return null;
}
/**
* Validate website URL
*/
export function validateWebsite(val: string | null | undefined): ValidationError | null {
if (!val || val === "") return null;
if (!/^https?:\/\//.test(val)) {
return { field: "website", message: "Website muss mit http:// oder https:// beginnen" };
}
if (val.length > 255) {
return { field: "website", message: "Website darf maximal 255 Zeichen sein" };
}
return null;
}
/**
* Validate company form data
*/
export interface CompanyFormData {
name: string;
address: string;
zip: string;
city: string;
taxId?: string;
vatId?: string;
website?: string;
bankIban?: string;
bankBic?: string;
email?: string;
phone?: string;
legalForm?: string;
country?: string;
bankName?: string;
}
export function validateCompanyForm(
data: CompanyFormData
): ValidationError[] {
const errors: ValidationError[] = [];
// Required fields
if (!data.name || data.name.trim() === "") {
errors.push({ field: "name", message: "Firmenname erforderlich" });
} else if (data.name.length > 255) {
errors.push({ field: "name", message: "Firmenname darf maximal 255 Zeichen sein" });
}
if (!data.address || data.address.trim() === "") {
errors.push({ field: "address", message: "Adresse erforderlich" });
} else if (data.address.length > 500) {
errors.push({ field: "address", message: "Adresse darf maximal 500 Zeichen sein" });
}
if (!data.zip || data.zip.trim() === "") {
errors.push({ field: "zip", message: "PLZ erforderlich" });
} else if (!/^[\d\s-]*$/.test(data.zip)) {
errors.push({ field: "zip", message: "PLZ darf nur Zahlen, Bindestriche und Leerzeichen enthalten" });
} else if (data.zip.length > 20) {
errors.push({ field: "zip", message: "PLZ darf maximal 20 Zeichen sein" });
}
if (!data.city || data.city.trim() === "") {
errors.push({ field: "city", message: "Stadt erforderlich" });
} else if (data.city.length > 100) {
errors.push({ field: "city", message: "Stadt darf maximal 100 Zeichen sein" });
}
// Optional fields with validation
if (data.taxId) {
const taxError = validateTaxId(data.taxId);
if (taxError) errors.push(taxError);
}
if (data.vatId) {
const vatError = validateVatId(data.vatId);
if (vatError) errors.push(vatError);
}
if (data.website) {
const webError = validateWebsite(data.website);
if (webError) errors.push(webError);
}
if (data.bankIban) {
const ibanError = validateIban(data.bankIban);
if (ibanError) errors.push(ibanError);
}
if (data.bankBic) {
const bicError = validateBic(data.bankBic);
if (bicError) errors.push(bicError);
}
if (data.email && data.email.trim()) {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
errors.push({ field: "email", message: "Ungültige E-Mail-Adresse" });
}
}
if (data.phone && data.phone.length > 20) {
errors.push({ field: "phone", message: "Telefonnummer darf maximal 20 Zeichen sein" });
}
if (data.legalForm && data.legalForm.length > 100) {
errors.push({ field: "legalForm", message: "Rechtsform darf maximal 100 Zeichen sein" });
}
if (data.country && data.country.length > 2) {
errors.push({ field: "country", message: "Ländercode darf maximal 2 Zeichen sein" });
}
if (data.bankName && data.bankName.length > 255) {
errors.push({ field: "bankName", message: "Bankname darf maximal 255 Zeichen sein" });
}
// Debug logging
if (errors.length > 0) {
debugLog("warning", `Validation failed: ${errors.length} error(s)`, errors);
} else {
debugLog("success", "Validation passed", data);
}
return errors;
}
/**
* Validate API response errors
*/
export function handleApiError(error: unknown, endpoint: string): void {
debugLog("error", `API Error from ${endpoint}`, error);
if (error instanceof Response) {
debugLog("error", `HTTP ${error.status}: ${error.statusText}`, error);
} else if (error instanceof Error) {
debugLog("error", error.message, error.stack);
} else {
debugLog("error", "Unknown error", error);
}
}
/**
* Get error message for a specific field
*/
export function getFieldError(field: string, errors: ValidationError[]): string | null {
const error = errors.find((e) => e.field === field);
return error?.message || null;
}
/**
* Check if field has error
*/
export function hasFieldError(field: string, errors: ValidationError[]): boolean {
return errors.some((e) => e.field === field);
}