category
This commit is contained in:
+1
-1
@@ -15,7 +15,7 @@ import { AdminModule } from './modules/admin/admin.module';
|
||||
// import { CacheService } from './modules/utils/cache.service';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { RestaurantsModule } from './modules/restaurants/restaurants.module';
|
||||
import { FoodsModule } from './modules/foods/foods.module';
|
||||
import { FoodsModule } from './modules/foods/food.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
||||
@@ -67,7 +67,15 @@ export const enum AuthMessage {
|
||||
OLD_PASSWORD_REQUIRED = 'رمز عبور قبلی الزامی است',
|
||||
PASSWORD_CHANGE_FAILED = 'تغییر رمز عبور با مشکل روبرد شد',
|
||||
}
|
||||
|
||||
export const enum FoodMessage {
|
||||
NOT_FOUND = 'غذایی با این مشخصات یافت نشد',
|
||||
}
|
||||
export const enum CategoryMessage {
|
||||
NOT_FOUND = 'دستهبندی مورد نظر یافت نشد',
|
||||
NOT_CREATED = 'ایجاد دستهبندی با خطا مواجه شد',
|
||||
NOT_UPDATED = 'بهروزرسانی دستهبندی با خطا مواجه شد',
|
||||
NOT_DELETED = 'حذف دستهبندی با خطا مواجه شد',
|
||||
}
|
||||
export const enum UserMessage {
|
||||
USER_NOT_FOUND = 'کاربری با این مشخصات یافت نشد',
|
||||
Rest_NOT_FOUND = 'رستورانی با این مشخصات یافت نشد',
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';
|
||||
import { FoodService } from '../providers/food.service';
|
||||
import { CreateFoodDto } from '../dto/create-food.dto';
|
||||
import { UpdateFoodDto } from '../dto/update-food.dto';
|
||||
import { FindFoodsDto } from '../dto/find-foods.dto';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiCreatedResponse,
|
||||
ApiOkResponse,
|
||||
ApiNotFoundResponse,
|
||||
ApiQuery,
|
||||
ApiBody,
|
||||
ApiParam,
|
||||
} from '@nestjs/swagger';
|
||||
|
||||
@ApiTags('foods')
|
||||
@Controller('foods')
|
||||
export class CategoryController {
|
||||
constructor(private readonly foodService: FoodService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create a new food' })
|
||||
@ApiCreatedResponse({ description: 'The food has been successfully created.', type: CreateFoodDto })
|
||||
@ApiBody({ type: CreateFoodDto })
|
||||
create(@Body() createFoodDto: CreateFoodDto) {
|
||||
return this.foodService.create(createFoodDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get a paginated list of foods' })
|
||||
@ApiOkResponse({ description: 'Paginated list of foods' })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@ApiQuery({ name: 'search', required: false, type: String })
|
||||
@ApiQuery({ name: 'orderBy', required: false, type: String })
|
||||
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
|
||||
@ApiQuery({ name: 'categoryId', required: false, type: String })
|
||||
@ApiQuery({ name: 'isActive', required: false, type: Boolean })
|
||||
findAll(@Query() dto: FindFoodsDto) {
|
||||
return this.foodService.findAll(dto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get a food by id' })
|
||||
@ApiParam({ name: 'id', required: true })
|
||||
@ApiOkResponse({ description: 'The food', type: CreateFoodDto })
|
||||
@ApiNotFoundResponse({ description: 'Food not found' })
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.foodService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({ summary: 'Update a food' })
|
||||
@ApiParam({ name: 'id', required: true })
|
||||
@ApiBody({ type: UpdateFoodDto })
|
||||
@ApiOkResponse({ description: 'The updated food', type: UpdateFoodDto })
|
||||
update(@Param('id') id: string, @Body() updateFoodDto: UpdateFoodDto) {
|
||||
return this.foodService.update(id, updateFoodDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete (soft) a food' })
|
||||
@ApiParam({ name: 'id', required: true })
|
||||
@ApiResponse({ status: 200, description: 'Food deleted' })
|
||||
remove(@Param('id') id: string) {
|
||||
return this.foodService.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';
|
||||
import { CategoryService } from '../providers/category.service';
|
||||
import { CreateCategoryDto } from '../dto/create-category.dto';
|
||||
import { UpdateCategoryDto } from '../dto/update-category.dto';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiCreatedResponse,
|
||||
ApiOkResponse,
|
||||
ApiNotFoundResponse,
|
||||
ApiParam,
|
||||
ApiBody,
|
||||
ApiQuery,
|
||||
} from '@nestjs/swagger';
|
||||
|
||||
@ApiTags('categories')
|
||||
@Controller('categories')
|
||||
export class CategorysController {
|
||||
constructor(private readonly categoryService: CategoryService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create category' })
|
||||
@ApiCreatedResponse({ description: 'Category created', type: CreateCategoryDto })
|
||||
@ApiBody({ type: CreateCategoryDto })
|
||||
create(@Body() dto: CreateCategoryDto) {
|
||||
return this.categoryService.create(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'List categories' })
|
||||
@ApiOkResponse({ description: 'List of categories' })
|
||||
@ApiQuery({ name: 'restId', required: false, type: String })
|
||||
@ApiQuery({ name: 'isActive', required: false, type: Boolean })
|
||||
findAll(@Query('restId') restId?: string, @Query('isActive') isActive?: string) {
|
||||
// convert isActive from query string to boolean when provided
|
||||
const isActiveBool: boolean | undefined = typeof isActive !== 'undefined' ? isActive === 'true' : undefined;
|
||||
return this.categoryService.findAll({ restId, isActive: isActiveBool });
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get category' })
|
||||
@ApiParam({ name: 'id' })
|
||||
@ApiOkResponse({ description: 'Category detail', type: CreateCategoryDto })
|
||||
@ApiNotFoundResponse({ description: 'Category not found' })
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.categoryService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({ summary: 'Update category' })
|
||||
@ApiParam({ name: 'id' })
|
||||
@ApiBody({ type: UpdateCategoryDto })
|
||||
@ApiOkResponse({ description: 'Updated category', type: UpdateCategoryDto })
|
||||
update(@Param('id') id: string, @Body() dto: UpdateCategoryDto) {
|
||||
return this.categoryService.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete category' })
|
||||
@ApiParam({ name: 'id' })
|
||||
@ApiOkResponse({ description: 'Deleted' })
|
||||
remove(@Param('id') id: string) {
|
||||
return this.categoryService.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';
|
||||
import { FoodService } from '../providers/food.service';
|
||||
import { CreateFoodDto } from '../dto/create-food.dto';
|
||||
import { UpdateFoodDto } from '../dto/update-food.dto';
|
||||
import { FindFoodsDto } from '../dto/find-foods.dto';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiCreatedResponse,
|
||||
ApiOkResponse,
|
||||
ApiNotFoundResponse,
|
||||
ApiQuery,
|
||||
ApiBody,
|
||||
ApiParam,
|
||||
} from '@nestjs/swagger';
|
||||
|
||||
@ApiTags('foods')
|
||||
@Controller('foods')
|
||||
export class FoodController {
|
||||
constructor(private readonly foodsService: FoodService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create a new food' })
|
||||
@ApiCreatedResponse({ description: 'The food has been successfully created.', type: CreateFoodDto })
|
||||
@ApiBody({ type: CreateFoodDto })
|
||||
create(@Body() createFoodDto: CreateFoodDto) {
|
||||
return this.foodsService.create(createFoodDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get a paginated list of foods' })
|
||||
@ApiOkResponse({ description: 'Paginated list of foods' })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@ApiQuery({ name: 'search', required: false, type: String })
|
||||
@ApiQuery({ name: 'orderBy', required: false, type: String })
|
||||
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
|
||||
@ApiQuery({ name: 'categoryId', required: false, type: String })
|
||||
@ApiQuery({ name: 'isActive', required: false, type: Boolean })
|
||||
findAll(@Query() dto: FindFoodsDto) {
|
||||
return this.foodsService.findAll(dto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get a food by id' })
|
||||
@ApiParam({ name: 'id', required: true })
|
||||
@ApiOkResponse({ description: 'The food', type: CreateFoodDto })
|
||||
@ApiNotFoundResponse({ description: 'Food not found' })
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.foodsService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({ summary: 'Update a food' })
|
||||
@ApiParam({ name: 'id', required: true })
|
||||
@ApiBody({ type: UpdateFoodDto })
|
||||
@ApiOkResponse({ description: 'The updated food', type: UpdateFoodDto })
|
||||
update(@Param('id') id: string, @Body() updateFoodDto: UpdateFoodDto) {
|
||||
return this.foodsService.update(id, updateFoodDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete (soft) a food' })
|
||||
@ApiParam({ name: 'id', required: true })
|
||||
@ApiResponse({ status: 200, description: 'Food deleted' })
|
||||
remove(@Param('id') id: string) {
|
||||
return this.foodsService.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { IsString, IsOptional, IsBoolean } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class CreateCategoryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'Beverages' })
|
||||
title?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Type(() => Boolean)
|
||||
@ApiPropertyOptional({ example: true })
|
||||
isActive?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'restaurant-ulid' })
|
||||
restId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'https://cdn.example.com/avatar.png' })
|
||||
avatarUrl?: string;
|
||||
}
|
||||
@@ -1,87 +1,132 @@
|
||||
import { IsBoolean, IsOptional, IsString, IsNumber, IsArray } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class CreateFoodDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@Type(() => Boolean)
|
||||
breakfast?: boolean;
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@Type(() => Boolean)
|
||||
tue?: boolean;
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@Type(() => Boolean)
|
||||
wed?: boolean;
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@Type(() => Boolean)
|
||||
thu?: boolean;
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@Type(() => Boolean)
|
||||
fri?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
titleFa?: string;
|
||||
@ApiPropertyOptional({ example: 'قرمه سبزی' })
|
||||
title?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
titleEn?: string;
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contentFa?: string;
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contentEn?: string;
|
||||
@ApiPropertyOptional({ example: 'توضیحات غذا' })
|
||||
content?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 120000 })
|
||||
price?: number;
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 10 })
|
||||
points?: number;
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 15 })
|
||||
prepareTime?: number;
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: true })
|
||||
@Type(() => Boolean)
|
||||
sat?: boolean;
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: true })
|
||||
@Type(() => Boolean)
|
||||
sun?: boolean;
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: true })
|
||||
@Type(() => Boolean)
|
||||
mon?: boolean;
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@Type(() => Boolean)
|
||||
noon?: boolean;
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: true })
|
||||
@Type(() => Boolean)
|
||||
dinner?: boolean;
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@Type(() => Boolean)
|
||||
isPickup?: boolean;
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 10 })
|
||||
stock?: number;
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 10 })
|
||||
stockDefault?: number;
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: true })
|
||||
@Type(() => Boolean)
|
||||
isActive?: boolean;
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiPropertyOptional({ type: [String] })
|
||||
images?: string[];
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@Type(() => Boolean)
|
||||
inPlaceServe?: boolean;
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@Type(() => Boolean)
|
||||
pickupServe?: boolean;
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 4.5 })
|
||||
rate?: number;
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 0 })
|
||||
discount?: number;
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiPropertyOptional({ type: [String] })
|
||||
categoryIds?: string[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { IsOptional, IsNumber, IsString, IsIn, IsBoolean } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class FindFoodsDto {
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 1 })
|
||||
page?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 10 })
|
||||
limit?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'cheese' })
|
||||
search?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'createdAt' })
|
||||
orderBy?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['asc', 'desc'])
|
||||
@ApiPropertyOptional({ example: 'desc' })
|
||||
order?: 'asc' | 'desc';
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'category-ulid' })
|
||||
categoryId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Type(() => Boolean)
|
||||
@ApiPropertyOptional({ example: true })
|
||||
isActive?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateCategoryDto } from './create-category.dto';
|
||||
|
||||
export class UpdateCategoryDto extends PartialType(CreateCategoryDto) {}
|
||||
@@ -9,4 +9,13 @@ export class Category extends BaseEntity {
|
||||
|
||||
@ManyToMany(() => Foods, food => food.categories)
|
||||
foods = new Collection<Foods>(this);
|
||||
|
||||
@Property({ default: true })
|
||||
isActive: boolean = true;
|
||||
|
||||
@Property({ nullable: true })
|
||||
restId?: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
avatarUrl?: string;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Collection, Entity, ManyToMany, Property } from '@mikro-orm/core';
|
||||
import { Category } from './category';
|
||||
import { Category } from './category.entity';
|
||||
import { BaseEntity } from 'src/common/entities/base.entity';
|
||||
|
||||
@Entity({ tableName: 'foods' })
|
||||
@@ -8,16 +8,10 @@ export class Foods extends BaseEntity {
|
||||
categories = new Collection<Category>(this);
|
||||
|
||||
@Property({ nullable: true })
|
||||
titleFa?: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
titleEn?: string;
|
||||
title?: string;
|
||||
|
||||
@Property({ type: 'text', nullable: true })
|
||||
contentFa?: string;
|
||||
|
||||
@Property({ type: 'text', nullable: true })
|
||||
contentEn?: string;
|
||||
content?: string;
|
||||
|
||||
@Property({ type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||
price?: number;
|
||||
@@ -25,6 +19,9 @@ export class Foods extends BaseEntity {
|
||||
@Property({ type: 'int', nullable: true })
|
||||
points?: number;
|
||||
|
||||
@Property({ type: 'int', nullable: true })
|
||||
order?: number;
|
||||
|
||||
@Property({ type: 'int', nullable: true })
|
||||
prepareTime?: number; // in minutes
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FoodService } from './providers/food.service';
|
||||
import { FoodController } from './controllers/food.controller';
|
||||
import { CategorysController } from './controllers/categorys.controller';
|
||||
import { CategoryService } from './providers/category.service';
|
||||
|
||||
@Module({
|
||||
controllers: [FoodController, CategorysController],
|
||||
providers: [FoodService, CategoryService],
|
||||
})
|
||||
export class FoodModule {}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
|
||||
import { FoodsService } from './providers/foods.service';
|
||||
import { CreateFoodDto } from './dto/create-food.dto';
|
||||
import { UpdateFoodDto } from './dto/update-food.dto';
|
||||
|
||||
@Controller('foods')
|
||||
export class FoodsController {
|
||||
constructor(private readonly foodsService: FoodsService) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() createFoodDto: CreateFoodDto) {
|
||||
return this.foodsService.create(createFoodDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.foodsService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.foodsService.findOne(+id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() updateFoodDto: UpdateFoodDto) {
|
||||
return this.foodsService.update(+id, updateFoodDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.foodsService.remove(+id);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FoodsService } from './providers/foods.service';
|
||||
import { FoodsController } from './foods.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [FoodsController],
|
||||
providers: [FoodsService],
|
||||
})
|
||||
export class FoodsModule {}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { CreateCategoryDto } from '../dto/create-category.dto';
|
||||
import { UpdateCategoryDto } from '../dto/update-category.dto';
|
||||
import { CategoryRepository } from '../repositories/category.repository';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { RequiredEntityData, FilterQuery } from '@mikro-orm/core';
|
||||
import { Category } from '../entities/category.entity';
|
||||
import { CategoryMessage } from 'src/common/enums/message.enum';
|
||||
|
||||
@Injectable()
|
||||
export class CategoryService {
|
||||
constructor(
|
||||
private readonly categoryRepository: CategoryRepository,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
async create(dto: CreateCategoryDto): Promise<Category> {
|
||||
const data = {
|
||||
title: dto.title,
|
||||
isActive: dto.isActive ?? true,
|
||||
restId: dto.restId,
|
||||
avatarUrl: dto.avatarUrl,
|
||||
} as RequiredEntityData<Category>;
|
||||
|
||||
const category = this.categoryRepository.create(data);
|
||||
await this.em.persistAndFlush(category);
|
||||
return category;
|
||||
}
|
||||
|
||||
async findAll(opts?: { restId?: string; isActive?: boolean }): Promise<Category[]> {
|
||||
const where: FilterQuery<Category> = {};
|
||||
if (opts?.restId) where.restId = opts.restId;
|
||||
if (opts && typeof opts.isActive === 'boolean') where.isActive = opts.isActive;
|
||||
return this.categoryRepository.find(where, { populate: ['foods'] });
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<Category> {
|
||||
const cat = await this.categoryRepository.findOne({ id }, { populate: ['foods'] });
|
||||
if (!cat) throw new NotFoundException(CategoryMessage.NOT_FOUND);
|
||||
return cat;
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateCategoryDto): Promise<Category> {
|
||||
const cat = await this.categoryRepository.findOne({ id });
|
||||
if (!cat) throw new NotFoundException(CategoryMessage.NOT_FOUND);
|
||||
this.em.assign(cat, dto as Partial<Category>);
|
||||
await this.em.persistAndFlush(cat);
|
||||
return cat;
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const cat = await this.categoryRepository.findOne({ id });
|
||||
if (!cat) throw new NotFoundException(CategoryMessage.NOT_FOUND);
|
||||
cat.deletedAt = new Date();
|
||||
await this.em.persistAndFlush(cat);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { CreateFoodDto } from '../dto/create-food.dto';
|
||||
import { FindFoodsDto } from '../dto/find-foods.dto';
|
||||
import { FoodRepository } from '../repositories/food.repository';
|
||||
import { CategoryRepository } from '../repositories/category.repository';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { RequiredEntityData } from '@mikro-orm/core';
|
||||
import { Foods } from '../entities/food.entity';
|
||||
import { FoodMessage } from 'src/common/enums/message.enum';
|
||||
|
||||
@Injectable()
|
||||
export class FoodService {
|
||||
constructor(
|
||||
private readonly foodRepository: FoodRepository,
|
||||
private readonly categoryRepository: CategoryRepository,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
async create(createFoodDto: CreateFoodDto): Promise<Foods> {
|
||||
const { categoryIds, ...rest } = createFoodDto;
|
||||
|
||||
// prepare data with defaults for required fields so repository.create typing is satisfied
|
||||
const data: RequiredEntityData<Foods> = {
|
||||
// boolean/day flags
|
||||
sat: rest.sat ?? false,
|
||||
sun: rest.sun ?? false,
|
||||
mon: rest.mon ?? false,
|
||||
breakfast: rest.breakfast ?? false,
|
||||
noon: rest.noon ?? false,
|
||||
dinner: rest.dinner ?? false,
|
||||
// pickup/stock
|
||||
isPickup: rest.isPickup ?? false,
|
||||
stock: rest.stock ?? 0,
|
||||
stockDefault: rest.stockDefault ?? 0,
|
||||
isActive: rest.isActive ?? true,
|
||||
inPlaceServe: rest.inPlaceServe ?? false,
|
||||
pickupServe: rest.pickupServe ?? false,
|
||||
rate: rest.rate ?? 0,
|
||||
discount: rest.discount ?? 0,
|
||||
// map single-title/content DTO to localized fields
|
||||
titleFa: rest.title,
|
||||
contentFa: rest.content,
|
||||
// numeric/array fields
|
||||
price: rest.price ?? 0,
|
||||
points: rest.points ?? 0,
|
||||
prepareTime: rest.prepareTime ?? 0,
|
||||
images: rest.images ?? undefined,
|
||||
} as RequiredEntityData<Foods>;
|
||||
|
||||
const food = this.foodRepository.create(data);
|
||||
|
||||
// attach categories if provided
|
||||
if (categoryIds && categoryIds.length) {
|
||||
const categories = await this.categoryRepository.find({ id: { $in: categoryIds } });
|
||||
categories.forEach(cat => food.categories.add(cat));
|
||||
}
|
||||
|
||||
await this.em.persistAndFlush(food);
|
||||
return food;
|
||||
}
|
||||
|
||||
findAll(dto: FindFoodsDto) {
|
||||
return this.foodRepository.findAllPaginated(dto);
|
||||
}
|
||||
|
||||
findOne(id: string) {
|
||||
return this.foodRepository.findOne({ id }, { populate: ['categories'] });
|
||||
}
|
||||
|
||||
async update(id: string, dto: Partial<CreateFoodDto>): Promise<Foods> {
|
||||
const food = await this.foodRepository.findOne({ id });
|
||||
|
||||
if (!food) {
|
||||
throw new NotFoundException(FoodMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
const { categoryIds, ...rest } = dto;
|
||||
|
||||
// assign scalar fields
|
||||
this.em.assign(food, rest as Partial<Foods>);
|
||||
|
||||
// attach new categories if provided (adds, does not attempt to preserve/remove existing ones)
|
||||
if (Array.isArray(categoryIds) && categoryIds.length) {
|
||||
const categories = await this.categoryRepository.find({ id: { $in: categoryIds } });
|
||||
categories.forEach(cat => {
|
||||
// Collection.add will ignore duplicates
|
||||
food.categories.add(cat);
|
||||
});
|
||||
}
|
||||
|
||||
await this.em.persistAndFlush(food);
|
||||
return food;
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
const food = await this.foodRepository.findOne({ id });
|
||||
if (!food) {
|
||||
throw new NotFoundException(FoodMessage.NOT_FOUND);
|
||||
}
|
||||
food.deletedAt = new Date();
|
||||
await this.em.persistAndFlush(food);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CreateFoodDto } from '../dto/create-food.dto';
|
||||
import { FoodRepository } from '../repositories/food.repository';
|
||||
import { CategoryRepository } from '../repositories/category.repository';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { Foods } from '../entities/food.entity';
|
||||
|
||||
@Injectable()
|
||||
export class FoodsService {
|
||||
constructor(
|
||||
private readonly foodRepository: FoodRepository,
|
||||
private readonly categoryRepository: CategoryRepository,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
async create(createFoodDto: CreateFoodDto): Promise<Foods> {
|
||||
const { categoryIds, ...rest } = createFoodDto;
|
||||
|
||||
// create an empty food entity and assign dto properties to avoid strict typing issues
|
||||
const food = new Foods();
|
||||
this.em.assign(food, rest as Partial<Foods>);
|
||||
|
||||
// attach categories if provided
|
||||
if (categoryIds && categoryIds.length) {
|
||||
const categories = await this.categoryRepository.find({ id: { $in: categoryIds } });
|
||||
categories.forEach(cat => food.categories.add(cat));
|
||||
}
|
||||
|
||||
await this.em.persistAndFlush(food);
|
||||
return food;
|
||||
}
|
||||
|
||||
findAll() {
|
||||
return `This action returns all foods`;
|
||||
}
|
||||
|
||||
findOne(id: number) {
|
||||
return `This action returns a #${id} food`;
|
||||
}
|
||||
|
||||
update(id: number) {
|
||||
return `This action updates a #${id} food`;
|
||||
}
|
||||
|
||||
remove(id: number) {
|
||||
return `This action removes a #${id} food`;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { Category } from '../entities/category';
|
||||
import { Category } from '../entities/category.entity';
|
||||
|
||||
@Injectable()
|
||||
export class CategoryRepository extends EntityRepository<Category> {
|
||||
|
||||
@@ -1,10 +1,66 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { FilterQuery } from '@mikro-orm/core';
|
||||
import { Foods } from '../entities/food.entity';
|
||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||
|
||||
type FindFoodsOpts = {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
orderBy?: string;
|
||||
order?: 'asc' | 'desc';
|
||||
categoryId?: string;
|
||||
isActive?: boolean;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class FoodRepository extends EntityRepository<Foods> {
|
||||
constructor(readonly em: EntityManager) {
|
||||
super(em, Foods);
|
||||
}
|
||||
/**
|
||||
* Find foods with pagination and optional filters.
|
||||
* Supports: search (title/content), categoryId, isActive, ordering.
|
||||
*/
|
||||
async findAllPaginated(opts: FindFoodsOpts = {}): Promise<PaginatedResult<Foods>> {
|
||||
const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc', categoryId, isActive } = opts;
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const where: FilterQuery<Foods> = {};
|
||||
|
||||
if (typeof isActive === 'boolean') {
|
||||
where.isActive = isActive;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
const pattern = `%${search}%`;
|
||||
where.$or = [{ title: { $ilike: pattern } }, { content: { $ilike: pattern } }];
|
||||
}
|
||||
|
||||
if (categoryId) {
|
||||
// filter by related categories (typed via FilterQuery)
|
||||
Object.assign(where, { categories: { id: categoryId } } as unknown as FilterQuery<Foods>);
|
||||
}
|
||||
|
||||
const [data, total] = await this.findAndCount(where, {
|
||||
limit,
|
||||
offset,
|
||||
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
|
||||
populate: ['categories'],
|
||||
});
|
||||
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,55 @@
|
||||
import { IsString, IsNotEmpty, IsBoolean, IsNumber, MinLength } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional, IsBoolean, IsNumber } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* DTO for updating user profile.
|
||||
* Mirrors fields defined on the User entity and keeps everything optional
|
||||
* to allow partial updates.
|
||||
*/
|
||||
export class UpdateUserDto {
|
||||
@ApiProperty({ example: ' ', description: "User's first name" })
|
||||
@IsNotEmpty()
|
||||
@ApiPropertyOptional({ description: "User's first name", example: 'John' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
firstName: string;
|
||||
firstName?: string;
|
||||
|
||||
@ApiProperty({ example: ' ', description: "User's last name" })
|
||||
@IsNotEmpty()
|
||||
@ApiPropertyOptional({ description: "User's last name", example: 'Doe' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
lastName: string;
|
||||
lastName?: string;
|
||||
|
||||
@ApiProperty({ example: ' ', description: "User's father's name" })
|
||||
@IsNotEmpty()
|
||||
@ApiPropertyOptional({ description: "User's birth date (ISO)", example: '1990-01-01' })
|
||||
@IsOptional()
|
||||
// keep as string here, caller should send ISO date; service will assign to Date
|
||||
@IsString()
|
||||
fatherName: string;
|
||||
birthDate?: string;
|
||||
|
||||
// --- Personal Details ---
|
||||
@ApiPropertyOptional({ description: "User's marriage date (ISO)", example: '2015-06-01' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
marriageDate?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: true, description: 'Gender: true for male, false for female' })
|
||||
@IsNotEmpty()
|
||||
@ApiPropertyOptional({ description: "Referrer's identifier (optional)", example: 'abc123' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
referrer?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Is the user active?', example: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isMale: boolean;
|
||||
isActive?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ example: '1234567890', description: 'Unique national code' })
|
||||
@IsNotEmpty()
|
||||
@MinLength(10)
|
||||
nationalCode: string;
|
||||
@ApiPropertyOptional({ description: 'Gender flag (boolean)', example: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
gender?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ example: '1001', description: 'Employee personal code' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
personalCode: string;
|
||||
|
||||
// --- Employment Details ---
|
||||
|
||||
@ApiPropertyOptional({ example: 'قراردادی', description: 'Type of hire (e.g., permanent, temporary)' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
hireType: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'اراک', description: 'Primary work location' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
workLocation: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 5, description: 'Years of work experience' })
|
||||
@IsNotEmpty()
|
||||
@ApiPropertyOptional({ description: 'Wallet balance (integer)', example: 0 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
workExperienceYear: number;
|
||||
wallet?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Reward points (integer)', example: 0 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
points?: number;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
|
||||
@Entity({ tableName: 'users' })
|
||||
export class User extends BaseEntity {
|
||||
@Property({ nullable: true })
|
||||
firstName?: string;
|
||||
@Property()
|
||||
firstName!: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
lastName?: string;
|
||||
@@ -13,8 +13,26 @@ export class User extends BaseEntity {
|
||||
@Property({ unique: true })
|
||||
phone!: string;
|
||||
|
||||
@Property({ default: null, type: 'date' })
|
||||
birthDate!: Date;
|
||||
|
||||
@Property({ default: null, type: 'date' })
|
||||
marriageDate!: Date;
|
||||
|
||||
@Property({ nullable: true })
|
||||
referrer?: string;
|
||||
|
||||
@Property({ default: true })
|
||||
isActive?: boolean;
|
||||
isActive?: boolean = true;
|
||||
|
||||
@Property({ default: true })
|
||||
gender?: boolean;
|
||||
|
||||
@Property({ default: 0, type: 'int' })
|
||||
wallet: number = 0;
|
||||
|
||||
@Property({ default: 0, type: 'int' })
|
||||
points: number = 0;
|
||||
|
||||
@OneToMany(() => RefreshToken, token => token.user)
|
||||
refreshTokens = new Collection<RefreshToken>(this);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Controller, Get, Req, UseGuards } from '@nestjs/common';
|
||||
import { Controller, Get, Req, UseGuards, Patch, Body, ValidationPipe } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
||||
import { AuthGuard, type AuthRequest } from 'src/modules/auth/guards/auth.guard';
|
||||
import { UserService } from './user.service';
|
||||
// import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
|
||||
@ApiTags('User')
|
||||
@Controller('user')
|
||||
@@ -24,22 +24,23 @@ export class UserController {
|
||||
restId,
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Update User Specification (PATCH /user/spec)
|
||||
// @UseGuards(AuthGuard)
|
||||
// @ApiBearerAuth()
|
||||
// @ApiOperation({ summary: 'Update the user specification' })
|
||||
// @ApiBody({ type: UpdateUserDto })
|
||||
// @Patch('/')
|
||||
// async updateUserSpec(@Req() req: AuthRequest, @Body() dto: UpdateUserDto) {
|
||||
// const userId = req.user.sub;
|
||||
// const user = await this.userService.updateUser(userId, dto);
|
||||
// return {
|
||||
// message: `PATCH request: Updating specification for user ${userId} `,
|
||||
// userId,
|
||||
// user,
|
||||
// };
|
||||
// }
|
||||
// 2. Update User Specification (PATCH /user)
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Update the authenticated user profile' })
|
||||
@Patch('/')
|
||||
async updateUser(
|
||||
@Req() req: AuthRequest,
|
||||
@Body(new ValidationPipe({ transform: true, whitelist: true })) dto: UpdateUserDto,
|
||||
) {
|
||||
const userId = req.userId;
|
||||
const user = await this.userService.updateUser(userId, dto);
|
||||
return {
|
||||
message: `PATCH request: Updating profile for user ${userId}`,
|
||||
userId,
|
||||
user,
|
||||
};
|
||||
}
|
||||
|
||||
// @UseGuards(AdminAuthGuard)
|
||||
// @ApiBearerAuth()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { FilterQuery } from '@mikro-orm/core';
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { FilterQuery, RequiredEntityData } from '@mikro-orm/core';
|
||||
import { User } from './entities/user.entity';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
// import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { FindUsersDto } from './dto/find-user.dto';
|
||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||
import { UserRepository } from './repositories/user.repository';
|
||||
@@ -18,9 +18,20 @@ export class UserService {
|
||||
let user = await this.userRepository.findOne({ phone });
|
||||
|
||||
if (!user) {
|
||||
user = this.userRepository.create({
|
||||
// firstName is required on the entity; use phone as a sensible default
|
||||
const _raw = {
|
||||
phone,
|
||||
});
|
||||
firstName: phone,
|
||||
// keep dates undefined (no value) — cast through unknown to satisfy TS typing
|
||||
birthDate: undefined,
|
||||
marriageDate: undefined,
|
||||
wallet: 0,
|
||||
points: 0,
|
||||
};
|
||||
|
||||
const createData = _raw as unknown as RequiredEntityData<User>;
|
||||
|
||||
user = this.userRepository.create(createData);
|
||||
await this.em.persistAndFlush(user);
|
||||
}
|
||||
|
||||
@@ -48,11 +59,43 @@ export class UserService {
|
||||
}
|
||||
|
||||
async create(phone: string): Promise<User> {
|
||||
const user = this.userRepository.create({ phone });
|
||||
const _raw = {
|
||||
phone,
|
||||
firstName: phone,
|
||||
birthDate: undefined,
|
||||
marriageDate: undefined,
|
||||
wallet: 0,
|
||||
points: 0,
|
||||
};
|
||||
|
||||
const createData = _raw as unknown as RequiredEntityData<User>;
|
||||
|
||||
const user = this.userRepository.create(createData);
|
||||
await this.em.persistAndFlush(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
async updateUser(userId: string, dto: UpdateUserDto): Promise<User> {
|
||||
const user = await this.userRepository.findOne({ id: userId });
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with ID ${userId} not found.`);
|
||||
}
|
||||
|
||||
// Normalize date strings into Date objects if provided
|
||||
const assignData: Partial<User> = { ...dto } as Partial<User>;
|
||||
if (dto.birthDate) {
|
||||
assignData.birthDate = new Date(dto.birthDate);
|
||||
}
|
||||
if (dto.marriageDate) {
|
||||
assignData.marriageDate = new Date(dto.marriageDate);
|
||||
}
|
||||
|
||||
this.em.assign(user, assignData);
|
||||
await this.em.flush();
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async findAll(dto: FindUsersDto): Promise<PaginatedResult<User>> {
|
||||
const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc' } = dto;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user