- NAS storage for created invoices (PDF via puppeteer), received invoices, and offers with auto-save on create/edit - Deterministic file paths derived from DB fields (no file_path column needed) - Separate NAS mount points: NAS_FINANCIALS_PATH, NAS_OFFERS_PATH - Invoice language field (cs/en) stored per invoice, replaces lang modal - Invoices list filtered by month/year matching KPI card selection - Centralized date helpers (src/utils/date.ts) replacing all .toISOString() calls that returned UTC instead of local time - Attendance project switching uses exact time (not rounded) - Comment cleanup: removed ~100 unnecessary/Czech comments - Removed as-any casts in orders and attendance - Prisma migrations: add invoice language, drop received_invoices BLOB columns Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
98 lines
2.8 KiB
TypeScript
98 lines
2.8 KiB
TypeScript
import { z } from "zod";
|
|
|
|
const QuotationItemSchema = z.object({
|
|
description: z.string().nullish(),
|
|
item_description: z.string().nullish(),
|
|
quantity: z
|
|
.union([z.number(), z.string()])
|
|
.transform((v) => Number(v) || 1)
|
|
.optional()
|
|
.default(1),
|
|
unit: z.string().nullish(),
|
|
unit_price: z
|
|
.union([z.number(), z.string()])
|
|
.transform((v) => Number(v) || 0)
|
|
.optional()
|
|
.default(0),
|
|
is_included_in_total: z
|
|
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
|
|
.optional()
|
|
.default(true),
|
|
position: z
|
|
.union([z.number(), z.string()])
|
|
.transform((v) => Number(v))
|
|
.optional(),
|
|
});
|
|
|
|
const ScopeSectionSchema = z.object({
|
|
title: z.string().nullish(),
|
|
title_cz: z.string().nullish(),
|
|
content: z.string().nullish(),
|
|
position: z
|
|
.union([z.number(), z.string()])
|
|
.transform((v) => Number(v))
|
|
.optional(),
|
|
});
|
|
|
|
export const CreateQuotationSchema = z.object({
|
|
quotation_number: z.string().nullish(),
|
|
project_code: z.string().nullish(),
|
|
customer_id: z
|
|
.union([z.number(), z.string()])
|
|
.transform((v) => Number(v))
|
|
.nullish(),
|
|
valid_until: z.string().nullish(),
|
|
currency: z.string().optional().default("CZK"),
|
|
language: z.string().optional().default("cs"),
|
|
vat_rate: z
|
|
.union([z.number(), z.string()])
|
|
.transform((v) => Number(v))
|
|
.optional()
|
|
.default(21.0),
|
|
apply_vat: z
|
|
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
|
|
.optional()
|
|
.default(true),
|
|
exchange_rate: z
|
|
.union([z.number(), z.string()])
|
|
.transform((v) => Number(v))
|
|
.optional()
|
|
.default(1.0),
|
|
status: z.string().optional().default("active"),
|
|
scope_title: z.string().nullish(),
|
|
scope_description: z.string().nullish(),
|
|
items: z.array(QuotationItemSchema).optional(),
|
|
sections: z.array(ScopeSectionSchema).optional(),
|
|
});
|
|
|
|
export const UpdateQuotationSchema = z.object({
|
|
quotation_number: z.string().optional(),
|
|
project_code: z.string().nullish(),
|
|
customer_id: z
|
|
.union([z.number(), z.string()])
|
|
.transform((v) => Number(v))
|
|
.optional(),
|
|
valid_until: z.union([z.string(), z.null()]).optional(),
|
|
currency: z.string().optional(),
|
|
language: z.string().optional(),
|
|
vat_rate: z
|
|
.union([z.number(), z.string()])
|
|
.transform((v) => Number(v))
|
|
.optional(),
|
|
apply_vat: z
|
|
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
|
|
.optional(),
|
|
exchange_rate: z
|
|
.union([z.number(), z.string()])
|
|
.transform((v) => Number(v))
|
|
.optional(),
|
|
status: z.string().optional(),
|
|
scope_title: z.string().nullish(),
|
|
scope_description: z.string().nullish(),
|
|
items: z.array(QuotationItemSchema).optional(),
|
|
sections: z.array(ScopeSectionSchema).optional(),
|
|
});
|
|
|
|
export type CreateQuotationInput = z.infer<typeof CreateQuotationSchema>;
|
|
export type UpdateQuotationInput = z.infer<typeof UpdateQuotationSchema>;
|