diff --git a/apps/api/src/database/index.ts b/apps/api/src/database/index.ts index e6715c9..302c6ee 100644 --- a/apps/api/src/database/index.ts +++ b/apps/api/src/database/index.ts @@ -3,3 +3,4 @@ export * from './schema/activity_logs'; export * from './schema/auth'; export * from './schema/general'; export * from './schema/surveys' +export * from './schema/inventory' diff --git a/apps/api/src/database/schema/inventory.ts b/apps/api/src/database/schema/inventory.ts new file mode 100644 index 0000000..9cd06e6 --- /dev/null +++ b/apps/api/src/database/schema/inventory.ts @@ -0,0 +1,17 @@ +import * as t from 'drizzle-orm/pg-core'; +import { timestamps } from '../timestamps'; +import { users } from './auth'; + +export const products = t.pgTable( + 'products', + { + id: t.serial('id').primaryKey(), + title: t.text('title').notNull(), + description: t.text('description').notNull(), + price: t.numeric('price').notNull(), + stock: t.integer('stock').notNull(), + urlImg: t.text('url_img').notNull(), + userId: t.integer('user_id').references(() => users.id, { onDelete: 'cascade' }), + ...timestamps, + } +); \ No newline at end of file diff --git a/apps/api/src/database/seeds/index.ts b/apps/api/src/database/seeds/index.ts index c90d616..a8e65e7 100644 --- a/apps/api/src/database/seeds/index.ts +++ b/apps/api/src/database/seeds/index.ts @@ -8,6 +8,7 @@ import { seedMunicipalites } from './municipalities'; import { seedParishes } from './parishes'; import { seedStates } from './states'; import { seedUserAdmin } from './user-admin.seed'; +import {seedProducts} from './inventory.seed' async function main() { const pool = new Pool({ @@ -25,6 +26,7 @@ async function main() { await seedLocalities(db); await seedAdminRole(db); await seedUserAdmin(db); + await seedProducts(db); console.log('All seeds completed successfully'); } catch (error) { diff --git a/apps/api/src/database/seeds/inventory.seed.ts b/apps/api/src/database/seeds/inventory.seed.ts new file mode 100644 index 0000000..b0c7745 --- /dev/null +++ b/apps/api/src/database/seeds/inventory.seed.ts @@ -0,0 +1,28 @@ +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import * as schema from '../index'; +import { products } from '../schema/inventory'; + + +export async function seedProducts(db: NodePgDatabase) { + console.log('Seeding example product...'); + + // Insert inventory + const array = [{title:'manzana',description:'fruta roja',price:'100',urlImg:'apple.avif',userId:1,stock:0}]; + + for (const item of array) { + try { + await db.insert(products).values({ + title: item.title, + description: item.description, + price: item.price, + stock: item.stock, + urlImg: item.urlImg, + userId: item.userId + }).onConflictDoNothing(); + } catch (error) { + console.error(`Error creating products '${item.title}':`, error); + } + } + + console.log('All products seeded successfully'); +} diff --git a/apps/api/src/features/inventory/dto/create-product.dto.ts b/apps/api/src/features/inventory/dto/create-product.dto.ts new file mode 100644 index 0000000..7a4728b --- /dev/null +++ b/apps/api/src/features/inventory/dto/create-product.dto.ts @@ -0,0 +1,32 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsInt, IsOptional, IsString } from 'class-validator'; + +export class CreateProductDto { + @ApiProperty() + @IsString() + title: string; + + @ApiProperty() + @IsString() + description: string; + + @ApiProperty() + @IsString({ + message: 'price must be a string', + }) + @IsOptional() + price: string; + + @ApiProperty() + @IsString({ + message: 'stock must be a number', + }) + @IsOptional() + stock: number; + + @ApiProperty() + @IsString({ + message: 'urlImg must be a string', + }) + urlImg: string; +} diff --git a/apps/api/src/features/inventory/dto/update-product.dto.ts b/apps/api/src/features/inventory/dto/update-product.dto.ts new file mode 100644 index 0000000..0c03785 --- /dev/null +++ b/apps/api/src/features/inventory/dto/update-product.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty, PartialType } from '@nestjs/swagger'; +import { CreateProductDto } from './create-product.dto'; + +// import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional, IsString } from 'class-validator'; + +export class UpdateProductDto extends PartialType(CreateProductDto) { + @IsOptional() + title: string; + + @IsOptional() + description: string; + + @IsOptional() + price: string; + + @IsOptional() + stock: number; + + @IsOptional() + urlImg: string; +} diff --git a/apps/api/src/features/inventory/entities/inventory.entity.ts b/apps/api/src/features/inventory/entities/inventory.entity.ts new file mode 100644 index 0000000..3556333 --- /dev/null +++ b/apps/api/src/features/inventory/entities/inventory.entity.ts @@ -0,0 +1,8 @@ +export class Product { + id?: number; + title: string; + description: string; + price: string; + stock: number; + urlImg: string; +} \ No newline at end of file diff --git a/apps/api/src/features/inventory/inventory.controller.ts b/apps/api/src/features/inventory/inventory.controller.ts new file mode 100644 index 0000000..4d618b6 --- /dev/null +++ b/apps/api/src/features/inventory/inventory.controller.ts @@ -0,0 +1,70 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common'; +import { InventoryService } from './inventory.service'; +import { CreateProductDto } from './dto/create-product.dto'; +import { UpdateProductDto } from './dto/update-product.dto'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { Roles } from '../../common/decorators/roles.decorator'; +import { PaginationDto } from '../../common/dto/pagination.dto'; + +@ApiTags('inventory') +@Controller('inventory') +export class UsersController { + constructor(private readonly inventoryService: InventoryService) {} + + @Get() + // @Roles('admin') + @ApiOperation({ summary: 'Get all products with pagination and filters' }) + @ApiResponse({ status: 200, description: 'Return paginated products.' }) + async findAll(@Query() paginationDto: PaginationDto) { + const result = await this.inventoryService.findAll(paginationDto); + return { + message: 'products fetched successfully', + data: result.data, + meta: result.meta + }; + } + + @Get(':id') + // @Roles('admin') + @ApiOperation({ summary: 'Get a product by ID' }) + @ApiResponse({ status: 200, description: 'Return the product.' }) + @ApiResponse({ status: 404, description: 'product not found.' }) + async findOne(@Param('id') id: string) { + const data = await this.inventoryService.findOne(id); + return { message: 'product fetched successfully', data }; + } + + @Post() + // @Roles('admin') + @ApiOperation({ summary: 'Create a new product' }) + @ApiResponse({ status: 201, description: 'Product created successfully.' }) + async create( + @Body() createUserDto: CreateProductDto, + @Query('roleId') roleId?: string, + ) { + const data = await this.inventoryService.create( + createUserDto, + roleId ? parseInt(roleId) : undefined, + ); + return { message: 'User created successfully', data }; + } + + // @Patch(':id') + // @Roles('admin') + // @ApiOperation({ summary: 'Update a user' }) + // @ApiResponse({ status: 200, description: 'User updated successfully.' }) + // @ApiResponse({ status: 404, description: 'User not found.' }) + // async update(@Param('id') id: string, @Body() UpdateProductDto: UpdateProductDto) { + // const data = await this.inventoryService.update(id, UpdateProductDto); + // return { message: 'User updated successfully', data }; + // } + + // @Delete(':id') + // @Roles('admin') + // @ApiOperation({ summary: 'Delete a user' }) + // @ApiResponse({ status: 200, description: 'User deleted successfully.' }) + // @ApiResponse({ status: 404, description: 'User not found.' }) + // async remove(@Param('id') id: string) { + // return await this.inventoryService.remove(id); + // } +} diff --git a/apps/api/src/features/inventory/inventory.module.ts b/apps/api/src/features/inventory/inventory.module.ts new file mode 100644 index 0000000..7ed08a8 --- /dev/null +++ b/apps/api/src/features/inventory/inventory.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { UsersController } from './inventory.controller'; +import { InventoryService } from './inventory.service'; +import { DrizzleModule } from '@/database/drizzle.module'; + +@Module({ + imports: [DrizzleModule], + controllers: [UsersController], + providers: [InventoryService], +}) +export class InventoryModule {} diff --git a/apps/api/src/features/inventory/inventory.service.ts b/apps/api/src/features/inventory/inventory.service.ts new file mode 100644 index 0000000..ff1f222 --- /dev/null +++ b/apps/api/src/features/inventory/inventory.service.ts @@ -0,0 +1,209 @@ +import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider'; +import { Env, validateString } from '@/common/utils'; +import { Inject, Injectable, HttpException, HttpStatus, NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import * as schema from 'src/database/index'; +import { products } from 'src/database/index'; +import { eq, like, or, SQL, sql, and, not } from 'drizzle-orm'; +import * as bcrypt from 'bcryptjs'; +import { CreateProductDto } from './dto/create-product.dto'; +import { UpdateUserDto } from './dto/update-product.dto'; +import { Product } from './entities/inventory.entity'; +import { PaginationDto } from '../../common/dto/pagination.dto'; + +@Injectable() +export class InventoryService { + constructor( + @Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase, + ) { } + + async findAll(paginationDto?: PaginationDto): Promise<{ data: Product[], meta: any }> { + const { page = 1, limit = 10, search = '', sortBy = 'id', sortOrder = 'asc' } = paginationDto || {}; + + // Calculate offset + const offset = (page - 1) * limit; + + // Build search condition + let searchCondition: SQL | undefined; + if (search) { + searchCondition = or( + like(products.title, `%${search}%`), + like(products.description, `%${search}%`), + // like(users.fullname, `%${search}%`) + ); + } + + // Build sort condition + const orderBy = sortOrder === 'asc' + ? sql`${products[sortBy as keyof typeof products]} asc` + : sql`${products[sortBy as keyof typeof products]} desc`; + + // Get total count for pagination + const totalCountResult = await this.drizzle + .select({ count: sql`count(*)` }) + .from(products) + .where(searchCondition); + + const totalCount = Number(totalCountResult[0].count); + const totalPages = Math.ceil(totalCount / limit); + + // Get paginated data + const data = await this.drizzle + .select({ + id: products.id, + title: products.title, + description: products.description, + valuePerUnit: products.valuePerUnit, + urlImg: products.urlImg, + // price: products.price, + // quantity: products.quantity, + // isActive: products.isActive + }) + .from(products) + // .leftJoin(usersRole, eq(usersRole.userId, users.id)) + // .leftJoin(roles, eq(roles.id, usersRole.roleId)) + .where(searchCondition) + .orderBy(orderBy) + .limit(limit) + .offset(offset); + + // Build pagination metadata + const meta = { + page, + limit, + totalCount, + totalPages, + hasNextPage: page < totalPages, + hasPreviousPage: page > 1, + nextPage: page < totalPages ? page + 1 : null, + previousPage: page > 1 ? page - 1 : null, + }; + + // console.log(data); + + return { data, meta }; + } + + async findOne(id: string): Promise { + const find = await this.drizzle + .select({ + id: products.id, + title: products.title, + description: products.description, + valuePerUnit: products.valuePerUnit, + urlImg: products.urlImg, + }) + .from(products) + // .leftJoin(usersRole, eq(usersRole.userId, users.id)) + // .leftJoin(roles, eq(roles.id, usersRole.roleId)) + // .leftJoin(schema.states, eq(schema.states.id, users.state)) + // .leftJoin(schema.municipalities, eq(schema.municipalities.id, users.municipality)) + // .leftJoin(schema.parishes, eq(schema.parishes.id, users.parish)) + + .where(eq(products.id, parseInt(id))); + + if (find.length === 0) { + throw new HttpException('User does not exist', HttpStatus.BAD_REQUEST); + } + + return find[0]; + } + + // Rest of the service remains the same + async create( + createProductDto: CreateProductDto, + roleId: number = 2, + ): Promise { + + + // Start a transaction + return await this.drizzle.transaction(async (tx) => { + // Create the user + const [newProduct] = await tx + .insert(products) + .values({ + title: createProductDto.title, + description: createProductDto.description, + valuePerUnit: createProductDto.valuePerUnit, + urlImg: createProductDto.urlImg, + }) + .returning(); + + // Assign role to user + // await tx.insert(usersRole).values({ + // userId: newProduct.id, + // roleId: roleId, + // }); + + // Return the created user with role + // const [userWithRole] = await tx + // .select({ + // id: users.id, + // username: users.username, + // email: users.email, + // fullname: users.fullname, + // phone: users.phone, + // isActive: users.isActive, + // role: roles.name, + // }) + // .from(users) + // .leftJoin(usersRole, eq(usersRole.userId, users.id)) + // .leftJoin(roles, eq(roles.id, usersRole.roleId)) + // .where(eq(users.id, newProduct.id)); + + + + return this.findOne(String(newProduct.id)); + }); + } + + // async update(id: string, updateUserDto: UpdateUserDto): Promise { + // const userId = parseInt(id); + + // // Check if user exists + // await this.findOne(id); + + // // Prepare update data + // const updateData: any = {}; + // if (updateUserDto.username) updateData.username = updateUserDto.username; + // if (updateUserDto.email) updateData.email = updateUserDto.email; + // if (updateUserDto.fullname) updateData.fullname = updateUserDto.fullname; + // if (updateUserDto.password) { + // updateData.password = await bcrypt.hash(updateUserDto.password, 10); + // } + // if (updateUserDto.phone) updateData.phone = updateUserDto.phone; + // if (updateUserDto.isActive) updateData.isActive = updateUserDto.isActive; + + // const updateDataRole: any = {}; + // if (updateUserDto.role) updateDataRole.roleId = updateUserDto.role; + // // Update user + // await this.drizzle + // .update(users) + // .set(updateData) + // .where(eq(users.id, userId)); + + // await this.drizzle + // .update(usersRole) + // .set(updateDataRole) + // .where(eq(usersRole.userId, userId)); + + + // // Return updated user + // return this.findOne(id); + // } + + + // async remove(id: string): Promise<{ message: string, data: User }> { + // const userId = parseInt(id); + + // // Check if user exists + // const user = await this.findOne(id); + + // // Delete user (this will cascade delete related records due to foreign key constraints) + // // await this.drizzle.delete(users).where(eq(users.id, userId)); + // await this.drizzle.update(users).set({ isActive: false }).where(eq(users.id, userId)); + + // return { message: 'User deleted successfully', data: user }; + // } +} + diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 6c53025..9c867c5 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -20,12 +20,12 @@ const GeistMono = localFont({ export const metadata = { metadataBase: new URL('https://turbo-npn.onrender.com'), title: { - default: 'Caja de Ahorro', - template: '%s | Caja de Ahorro', + default: 'fondemi', + template: '%s | fondemi', }, openGraph: { type: 'website', - title: 'Caja de Ahorro', + title: 'fondemi', description: 'Sistema integral para cajas de ahorro', url: 'https://turbo-npn.onrender.com', images: [