diff --git a/package.json b/package.json index 89090d7..57919d5 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "db:up": "docker compose up -d", "db:down": "docker compose down", "create-super-admin": "ts-node -r tsconfig-paths/register scripts/create-super-admin.ts", - "send-sms": "ts-node -r tsconfig-paths/register scripts/send-sms.ts" + "send-sms": "ts-node -r tsconfig-paths/register scripts/send-sms.ts", + "ensure:dock-categories": "ts-node --transpile-only scripts/ensure-dock-categories.ts" }, "dependencies": { "@aws-sdk/client-s3": "^3.1101.0", diff --git a/prisma/migrations/20260923120000_product_preparation_hours/migration.sql b/prisma/migrations/20260923120000_product_preparation_hours/migration.sql new file mode 100644 index 0000000..4b4bb8a --- /dev/null +++ b/prisma/migrations/20260923120000_product_preparation_hours/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Product" ADD COLUMN "preparationHours" INTEGER; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e8c7928..915e8eb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -156,23 +156,24 @@ model CategoryOptionEntry { } model Product { - id String @id @default(cuid()) - nameFa String - nameEn String - price Int @default(0) - sellUnit SellUnit @default(unit) - categoryId String - category Category @relation(fields: [categoryId], references: [id], onDelete: Restrict) - intro String @default("") - description String @default("") - mainImageUrl String? - mainImageKey String? - tags String[] @default([]) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - gallery ProductGalleryImage[] - options ProductOption[] - orderItems OrderItem[] + id String @id @default(cuid()) + nameFa String + nameEn String + price Int @default(0) + sellUnit SellUnit @default(unit) + categoryId String + category Category @relation(fields: [categoryId], references: [id], onDelete: Restrict) + intro String @default("") + description String @default("") + preparationHours Int? + mainImageUrl String? + mainImageKey String? + tags String[] @default([]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + gallery ProductGalleryImage[] + options ProductOption[] + orderItems OrderItem[] @@index([categoryId]) @@index([price]) diff --git a/scripts/ensure-dock-categories.ts b/scripts/ensure-dock-categories.ts new file mode 100644 index 0000000..75727be --- /dev/null +++ b/scripts/ensure-dock-categories.ts @@ -0,0 +1,38 @@ +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +const cats = [ + { nameFa: 'شکلات', nameEn: 'Chocolate', slug: 'chocolate', sortOrder: 10 }, + { nameFa: 'باقلوا', nameEn: 'Baklava', slug: 'baklava', sortOrder: 11 }, +] + +async function main() { + for (const cat of cats) { + await prisma.category.upsert({ + where: { slug: cat.slug }, + create: cat, + update: { + nameFa: cat.nameFa, + nameEn: cat.nameEn, + sortOrder: cat.sortOrder, + }, + }) + } + + const roots = await prisma.category.findMany({ + where: { parentId: null }, + select: { slug: true, nameFa: true, nameEn: true }, + orderBy: { sortOrder: 'asc' }, + }) + console.log(JSON.stringify(roots, null, 2)) +} + +main() + .catch((err) => { + console.error(err) + process.exitCode = 1 + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/src/products/dto/create-product.dto.ts b/src/products/dto/create-product.dto.ts index 6193d40..af01f8a 100644 --- a/src/products/dto/create-product.dto.ts +++ b/src/products/dto/create-product.dto.ts @@ -8,6 +8,7 @@ import { IsString, Min, MinLength, + ValidateIf, ValidateNested, } from 'class-validator'; @@ -61,6 +62,14 @@ export class CreateProductDto { @IsString() description?: string; + /** null = ready immediately (no prep time) */ + @IsOptional() + @ValidateIf((_, value) => value !== null && value !== undefined) + @Type(() => Number) + @IsInt() + @Min(1) + preparationHours?: number | null; + @IsOptional() @IsArray() @IsString({ each: true }) diff --git a/src/products/dto/list-products-query.dto.ts b/src/products/dto/list-products-query.dto.ts index 173b031..b422fb6 100644 --- a/src/products/dto/list-products-query.dto.ts +++ b/src/products/dto/list-products-query.dto.ts @@ -14,6 +14,10 @@ export class ListProductsQueryDto { @IsString() categorySlug?: string; + @IsOptional() + @IsString() + tag?: string; + @IsOptional() @Type(() => Number) @IsInt() diff --git a/src/products/dto/update-product.dto.ts b/src/products/dto/update-product.dto.ts index 50b35c7..fb74a35 100644 --- a/src/products/dto/update-product.dto.ts +++ b/src/products/dto/update-product.dto.ts @@ -8,6 +8,7 @@ import { IsString, Min, MinLength, + ValidateIf, ValidateNested, } from 'class-validator'; import { @@ -47,6 +48,14 @@ export class UpdateProductDto { @IsString() description?: string; + /** null = ready immediately (no prep time) */ + @IsOptional() + @ValidateIf((_, value) => value !== null && value !== undefined) + @Type(() => Number) + @IsInt() + @Min(1) + preparationHours?: number | null; + @IsOptional() @IsArray() @IsString({ each: true }) diff --git a/src/products/products.service.ts b/src/products/products.service.ts index 931d87d..851568a 100644 --- a/src/products/products.service.ts +++ b/src/products/products.service.ts @@ -35,6 +35,9 @@ export class ProductsService { const where = { ...(categoryIds ? { categoryId: { in: categoryIds } } : {}), + ...(query.tag?.trim() + ? { tags: { has: query.tag.trim() } } + : {}), ...(query.minPrice !== undefined || query.maxPrice !== undefined ? { price: { @@ -100,6 +103,7 @@ export class ProductsService { sellUnit: dto.sellUnit, intro: dto.intro ?? '', description: dto.description ?? '', + preparationHours: dto.preparationHours ?? null, tags: dto.tags ?? [], mainImageUrl: dto.mainImageUrl, mainImageKey: dto.mainImageKey, @@ -178,6 +182,9 @@ export class ProductsService { ...(dto.sellUnit !== undefined ? { sellUnit: dto.sellUnit } : {}), ...(dto.intro !== undefined ? { intro: dto.intro } : {}), ...(dto.description !== undefined ? { description: dto.description } : {}), + ...(dto.preparationHours !== undefined + ? { preparationHours: dto.preparationHours } + : {}), ...(dto.tags !== undefined ? { tags: dto.tags } : {}), ...(dto.mainImageUrl !== undefined ? { mainImageUrl: dto.mainImageUrl }