Files
butcem/src/shared/schemas/stock.ts
T

161 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<typeof stockCategoryFormSchema>;
export type StockCategoryForm = z.output<typeof stockCategoryFormSchema>;
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<typeof productTypeFormSchema>;
export type ProductTypeForm = z.output<typeof productTypeFormSchema>;
/** 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<typeof stockMovementFormSchema>;
export type StockMovementForm = z.output<typeof stockMovementFormSchema>;
/** 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<typeof stockCountFormSchema>;
export type StockCountForm = z.output<typeof stockCountFormSchema>;
/**
* 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<typeof stockCountSheetSchema>;
export type StockCountSheet = z.output<typeof stockCountSheetSchema>;