feat(shopping): mağazalar, zahmet skoru ve ürünler (marka/paket, dönüşüm, görseller)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-06 11:48:04 +03:00
parent 8e7e374778
commit f0b570284e
23 changed files with 1893 additions and 1 deletions
+47
View File
@@ -0,0 +1,47 @@
import { z } from "zod";
import { tr } from "../../i18n/tr";
import { MAX_ATTACHMENTS_PER_RECORD } from "../attachments";
import { BARCODE_PATTERN } from "../products";
import { parseQuantity } from "../quantity";
const v = tr.validation;
const s = tr.products.validation;
const quantity = (message: string) =>
z
.string()
.trim()
.transform((x, ctx) => {
const q = parseQuantity(x);
if (q === null || q <= 0) {
ctx.addIssue({ code: "custom", message });
return z.NEVER;
}
return q;
});
/**
* Ürün (marka/varyant). Dönüşümün zorunlu olup olmadığı ürün tipinin birimine bağlı olduğundan
* serviste denetlenir; burada yalnızca biçim.
*/
export const productFormSchema = z.object({
productTypeId: z.string().regex(/^[a-f0-9]{24}$/i, s.productType),
brand: z.string().trim().min(1, tr.common.required).max(60, v.nameMax),
name: z.string().trim().max(120).default(""),
barcode: z
.string()
.trim()
.transform((x) => x.replace(/\s/g, ""))
.refine((x) => x === "" || BARCODE_PATTERN.test(x), s.barcode)
.transform((x) => (x === "" ? null : x)),
packageQuantity: quantity(s.packageQuantity),
packageUnit: z.string().trim().min(1, s.packageUnit).max(20, s.packageUnit),
typeQuantity: z.preprocess(
(x) => (x === undefined || x === null || String(x).trim() === "" ? null : x),
quantity(s.typeQuantity).nullable(),
),
note: z.string().trim().max(300).default(""),
imageIds: z.array(z.string().regex(/^[a-f0-9]{24}$/i)).max(MAX_ATTACHMENTS_PER_RECORD),
});
export type ProductFormInput = z.input<typeof productFormSchema>;
export type ProductForm = z.output<typeof productFormSchema>;
+72
View File
@@ -0,0 +1,72 @@
import { z } from "zod";
import { tr } from "../../i18n/tr";
import { MEAL_CARD_PROVIDERS } from "../accounts";
import { parseMoney } from "../money";
import { parseQuantity } from "../quantity";
import { STORE_KINDS } from "../stores";
const v = tr.validation;
const s = tr.stores.validation;
/** Boş → null; dolu → kuruş (≥ 0). */
const optionalMoney = z
.string()
.trim()
.transform((x, ctx) => {
if (x === "") return null;
const minor = parseMoney(x);
if (minor === null || minor < 0) {
ctx.addIssue({ code: "custom", message: v.money });
return z.NEVER;
}
return minor;
});
/** "2,5" km → 2500 m (3 ondalık = metre). Boş → null. */
const distance = z
.string()
.trim()
.transform((x, ctx) => {
if (x === "") return null;
const m = parseQuantity(x);
if (m === null || m > 1_000_000) {
ctx.addIssue({ code: "custom", message: s.distance });
return z.NEVER;
}
return m;
});
export const storeFormSchema = z
.object({
name: z.string().trim().min(1, tr.common.required).max(80, v.nameMax),
kind: z.enum(STORE_KINDS),
branch: z.string().trim().max(80).default(""),
distanceKm: distance,
carryDifficulty: z.coerce.number().int().min(1, s.carry).max(5, s.carry),
deliveryFee: optionalMoney,
freeDeliveryMin: optionalMoney,
mealCardAccepted: z.boolean(),
mealCardProviders: z.array(z.enum(MEAL_CARD_PROVIDERS)).max(MEAL_CARD_PROVIDERS.length),
note: z.string().trim().max(300).default(""),
})
.superRefine((x, ctx) => {
if (x.mealCardAccepted && x.mealCardProviders.length === 0)
ctx.addIssue({ code: "custom", path: ["mealCardProviders"], message: s.providers });
})
.transform((x) => {
const online = x.kind === "online";
return {
name: x.name,
kind: x.kind,
branch: x.branch,
distanceM: online ? null : x.distanceKm,
carryDifficulty: x.carryDifficulty,
deliveryFeeMinor: online ? x.deliveryFee : null,
freeDeliveryMinMinor: online ? x.freeDeliveryMin : null,
mealCardAccepted: x.mealCardAccepted,
mealCardProviders: x.mealCardAccepted ? [...new Set(x.mealCardProviders)] : [],
note: x.note,
};
});
export type StoreFormInput = z.input<typeof storeFormSchema>;
export type StoreForm = z.output<typeof storeFormSchema>;