import { z } from "zod"; import { tr } from "../../i18n/tr"; import { parseQuantity } from "../quantity"; import { WASTE_REASONS } from "../stock"; const v = tr.validation; const s = tr.stock.validation; const ymd = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, v.date); const optionalId = z .string() .regex(/^([a-f0-9]{24})?$/i) .transform((x) => (x === "" ? null : x)); const requiredId = z.string().regex(/^[a-f0-9]{24}$/i, s.selectCategory); /** Miktar metni → binde bir tamsayı. `allowZero` sayım ve başlangıç için. */ const quantity = (allowZero = false) => z .string() .trim() .transform((x, ctx) => { const q = parseQuantity(x); if (q === null || q < 0 || (!allowZero && q === 0)) { ctx.addIssue({ code: "custom", message: s.quantity }); return z.NEVER; } return q; }); const optionalQuantity = z.preprocess( (x) => (x === undefined || x === null || String(x).trim() === "" ? null : x), quantity(true).nullable(), ); /** Boş → null; dolu → 1..365 gün. */ const countEveryDays = z.preprocess( (x) => (x === undefined || x === null || String(x).trim() === "" ? null : x), z .string() .trim() .transform((x, ctx) => { const n = Number(x); if (!/^\d+$/.test(x) || n < 1 || n > 365) { ctx.addIssue({ code: "custom", message: s.countDays }); return z.NEVER; } return n; }) .nullable(), ); export const stockCategoryFormSchema = z.object({ name: z.string().trim().min(1, tr.common.required).max(80, v.nameMax), parentId: optionalId, expenseCategoryId: optionalId, countEveryDays, }); export type StockCategoryFormInput = z.input; export type StockCategoryForm = z.output; export const productTypeFormSchema = z.object({ name: z.string().trim().min(1, tr.common.required).max(80, v.nameMax), categoryId: requiredId, unit: z.string().trim().min(1, s.unit).max(20, s.unit), targetQuantity: quantity(), thresholdPct: z .string() .trim() .transform((x, ctx) => { const n = Number(x); if (!/^\d+$/.test(x) || n > 100) { ctx.addIssue({ code: "custom", message: s.thresholdPct }); return z.NEVER; } return n; }), location: z.string().trim().max(80).default(""), step: quantity(), countEveryDays, note: z.string().trim().max(300).default(""), /** Yalnızca yeni ürün tipinde: evdeki başlangıç miktarı. */ initialQuantity: optionalQuantity.default(null), }); export type ProductTypeFormInput = z.input; export type ProductTypeForm = z.output; /** Alım, tüketim veya fire. Sayım düzeltmesi ayrı formdan (gerçek miktar). */ export const stockMovementFormSchema = z .object({ kind: z.enum(["purchase", "consume", "waste"]), date: ymd, quantity: quantity(), wasteReason: z.preprocess( (x) => (x === "" || x === undefined ? null : x), z.enum(WASTE_REASONS).nullable(), ), expiryDate: z.preprocess((x) => (x === "" || x === undefined ? null : x), ymd.nullable()), note: z.string().trim().max(300).default(""), }) .superRefine((x, ctx) => { if (x.kind === "waste" && !x.wasteReason) ctx.addIssue({ code: "custom", path: ["wasteReason"], message: s.wasteReason }); if (x.expiryDate && x.expiryDate < x.date) ctx.addIssue({ code: "custom", path: ["expiryDate"], message: s.expiryBeforeDate }); }) .transform((x) => ({ ...x, wasteReason: x.kind === "waste" ? x.wasteReason : null, expiryDate: x.kind === "purchase" ? x.expiryDate : null, })); export type StockMovementFormInput = z.input; export type StockMovementForm = z.output; /** Tek ürün sayımı: gerçek miktar → fark sayım düzeltmesi olur. */ export const stockCountFormSchema = z.object({ date: ymd, actualQuantity: quantity(true), note: z.string().trim().max(300).default(""), }); export type StockCountFormInput = z.input; export type StockCountForm = z.output; /** * Toplu sayım satırı: sayılan miktar boşsa ürün sayılmamış sayılır (atlanır). Eksiğin bir kısmı * fire olarak işaretlenebilir (neden zorunlu). */ const stockCountRowSchema = z .object({ productTypeId: z.string().regex(/^[a-f0-9]{24}$/i), actualQuantity: optionalQuantity, wasteQuantity: optionalQuantity.default(null), wasteReason: z.preprocess( (x) => (x === "" || x === undefined ? null : x), z.enum(WASTE_REASONS).nullable(), ), }) .superRefine((x, ctx) => { if (x.wasteQuantity && !x.wasteReason) ctx.addIssue({ code: "custom", path: ["wasteReason"], message: s.wasteReason }); if (x.wasteQuantity && x.actualQuantity === null) ctx.addIssue({ code: "custom", path: ["actualQuantity"], message: s.quantity }); }) .transform((x) => ({ ...x, wasteQuantity: x.wasteQuantity || null, wasteReason: x.wasteQuantity ? x.wasteReason : null, })); export const stockCountSheetSchema = z .object({ date: ymd, categoryId: optionalId, note: z.string().trim().max(300).default(""), rows: z.array(stockCountRowSchema).max(500), }) .superRefine((x, ctx) => { if (!x.rows.some((r) => r.actualQuantity !== null)) ctx.addIssue({ code: "custom", path: ["rows"], message: s.nothingCounted }); }); export type StockCountSheetInput = z.input; export type StockCountSheet = z.output;