8ab2244755
- shoppingLists koleksiyonu (ürün tipi / marka / serbest metin kalemleri), migration 015 - planShopping saf fonksiyonu: paket yuvarlama, zahmet ve hacimli cezası, teslimat, yemek kartı önceliği, ek mağaza eşiği - eşik önerileri onay/red (red stok yenilenene kadar), ürün tipine hacimli işareti - fiyat servisinde ortak loadTypeOffers; seed listeleri ve testler Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
163 lines
5.6 KiB
TypeScript
163 lines
5.6 KiB
TypeScript
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(""),
|
||
/** Ağır/hacimli: akıllı mağaza önerisinde taşıma cezası. */
|
||
bulky: z.boolean().default(false),
|
||
/** 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>;
|