import { prisma } from "@/lib/prisma"; import { asStringArray } from "@/lib/json-array"; import { matchesSearchQuery } from "@/lib/search-match"; import type { Product, Harvest, Batch, ProductCategory, Prisma } from "@/generated/prisma/client"; // tasteTags/images sind im Schema als Json modelliert (MySQL-Kompatibilitaet, // siehe schema.prisma) — hier auf das tatsaechliche Laufzeitformat // (String-Array) zurueckgeschmalert, damit der Rest der App wie zuvor damit // arbeiten kann. normalizeProduct() unten stellt sicher, dass das an jeder // Rueckgabestelle in diesem Modul auch wirklich so ankommt. export type ProductWithRelations = Omit & { tasteTags: string[]; images: string[]; harvest: Harvest | null; batch: Batch | null; inventory: { quantity: number } | null; }; function normalizeProduct( product: T ): Omit & { tasteTags: string[]; images: string[] } { return { ...product, tasteTags: asStringArray(product.tasteTags), images: asStringArray(product.images) }; } /** Dasselbe Prinzip wie normalizeProduct oben, nur fuer das images-Feld * alleine — wiederverwendbar fuer Harvest UND fuer die (verschachtelten) * Product-Eintraege in den Harvest-/Batch-Funktionen unten, die keine * tasteTags-Normalisierung brauchen. */ function withImages(item: T): Omit & { images: string[] } { return { ...item, images: asStringArray(item.images) }; } export type HarvestWithImages = Omit & { images: string[] }; export type ProductSort = "preis-auf" | "preis-ab" | "verfuegbarkeit"; export interface GetActiveProductsOptions { query?: string; category?: ProductCategory; sort?: ProductSort; /** Nur Produkte mit Lagerbestand > 0 anzeigen. */ onlyAvailable?: boolean; } export async function getActiveProducts( options: GetActiveProductsOptions = {} ): Promise { const trimmed = options.query?.trim(); let orderBy: Prisma.ProductOrderByWithRelationInput = { createdAt: "asc" }; if (options.sort === "preis-auf") orderBy = { priceCents: "asc" }; else if (options.sort === "preis-ab") orderBy = { priceCents: "desc" }; else if (options.sort === "verfuegbarkeit") orderBy = { inventory: { quantity: "desc" } }; const products = ( await prisma.product.findMany({ where: { isActive: true, ...(options.category ? { category: options.category } : {}), ...(options.onlyAvailable ? { inventory: { quantity: { gt: 0 } } } : {}), }, include: { harvest: true, batch: true, inventory: { select: { quantity: true } } }, orderBy, }) ).map(normalizeProduct); if (!trimmed) return products; // Teilstring-Suche auf einzelnen tasteTags-Elementen unterstuetzt Prisma // fuer Postgres-Arrays nicht (nur exakte Gleichheit via "has") — bei der // ueberschaubaren Katalogsgroesse wird deshalb hier statt in der DB-Query // gefiltert. Das behebt zugleich, dass mehrwortige, gemischt geschriebene // Tags wie "reines Bienenwachs" bei case-insensitiver Suche nie trafen. // matchesSearchQuery gleicht zusaetzlich Umlaute/ASCII-Transliteration ab // (z.B. "Bluete" <-> "Blüte"), da Produktnamen/-tags durchgaengig echte // Umlaute verwenden, aber nicht jede Kundin/jeder Kunde sie eintippt. return products.filter((product) => { const haystack = [product.name, product.description, ...product.tasteTags].join(" "); return matchesSearchQuery(haystack, trimmed); }); } /** Bis zu `limit` weitere aktive Produkte derselben Kategorie fuer "Das koennte dir auch gefallen". */ export async function getRelatedProducts( productId: string, category: ProductCategory, limit = 3 ): Promise { const products = await prisma.product.findMany({ where: { isActive: true, category, id: { not: productId } }, include: { harvest: true, batch: true, inventory: { select: { quantity: true } } }, orderBy: { createdAt: "desc" }, take: limit, }); return products.map(normalizeProduct); } export async function getProductBySlug( slug: string ): Promise { const product = await prisma.product.findUnique({ where: { slug }, include: { harvest: true, batch: true, inventory: { select: { quantity: true } } }, }); return product ? normalizeProduct(product) : null; } export async function getProductsByIds( ids: string[] ): Promise { if (ids.length === 0) return []; const products = await prisma.product.findMany({ where: { id: { in: ids } }, include: { harvest: true, batch: true, inventory: { select: { quantity: true } } }, }); return products.map(normalizeProduct); } /** Nur oeffentlich sichtbare Ernten (siehe Harvest.isPublished) — fuer Shop-/Ernten-Seiten. */ export async function getHarvests(): Promise<(HarvestWithImages & { batches: Batch[] })[]> { const harvests = await prisma.harvest.findMany({ where: { isPublished: true }, include: { batches: true }, orderBy: [{ year: "desc" }, { harvestDate: "desc" }], }); return harvests.map(withImages); } /** * Nur oeffentlich sichtbare Ernten (siehe Harvest.isPublished) — gibt null * zurueck, wenn die Ernte nicht existiert ODER noch nicht veroeffentlicht * ist, damit unveroeffentlichte (z.B. frisch aus ApiCore synchronisierte) * Ernten ueber ihre Shop-URL nicht erratbar sind. */ export async function getHarvestByYearType( year: number, type: string ): Promise< | (HarvestWithImages & { batches: Batch[]; products: (Omit & { images: string[] })[]; }) | null > { const harvest = await prisma.harvest.findUnique({ where: { year_type: { year, type } }, include: { batches: true, products: true }, }); if (!harvest || !harvest.isPublished) return null; return { ...withImages(harvest), products: harvest.products.map(withImages) }; } export async function getBatchByCode( code: string ): Promise< | (Omit & { images: string[]; harvest: HarvestWithImages; products: (Omit & { images: string[] })[]; }) | null > { const batch = await prisma.batch.findUnique({ where: { code }, include: { harvest: true, products: true }, }); if (!batch) return null; return { ...withImages(batch), harvest: withImages(batch.harvest), products: batch.products.map(withImages), }; }