From f87f235967216d817d1d7c08c02d62bede32af27 Mon Sep 17 00:00:00 2001 From: Sergio Ramirez Date: Fri, 20 Jun 2025 12:24:14 -0400 Subject: [PATCH 01/22] placeholder al crear un usuario corregido --- .../feactures/users/components/admin/create-user-form.tsx | 7 ++----- .../feactures/users/components/admin/update-user-form.tsx | 4 +--- packages/shadcn/src/components/ui/input.tsx | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/apps/web/feactures/users/components/admin/create-user-form.tsx b/apps/web/feactures/users/components/admin/create-user-form.tsx index ebc08f8..b0339d9 100644 --- a/apps/web/feactures/users/components/admin/create-user-form.tsx +++ b/apps/web/feactures/users/components/admin/create-user-form.tsx @@ -59,7 +59,7 @@ export function CreateUserForm({ confirmPassword: '', id: defaultValues?.id, phone: defaultValues?.phone || '', - role: defaultValues?.role, + role: undefined, } const form = useForm({ @@ -185,10 +185,7 @@ export function CreateUserForm({ render={({ field }) => ( Rol - field.onChange(Number(value))}> diff --git a/apps/web/feactures/users/components/admin/update-user-form.tsx b/apps/web/feactures/users/components/admin/update-user-form.tsx index b96d14f..c9feb3f 100644 --- a/apps/web/feactures/users/components/admin/update-user-form.tsx +++ b/apps/web/feactures/users/components/admin/update-user-form.tsx @@ -171,9 +171,7 @@ export function UpdateUserForm({ render={({ field }) => ( Rol - field.onChange(Number(value))}> diff --git a/packages/shadcn/src/components/ui/input.tsx b/packages/shadcn/src/components/ui/input.tsx index 36b6812..a65aa6b 100644 --- a/packages/shadcn/src/components/ui/input.tsx +++ b/packages/shadcn/src/components/ui/input.tsx @@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) { type={type} data-slot="input" className={cn( - 'border-gray-400 dark:border-input border-2 file:text-foreground selection:bg-primary selection:text-primary-foreground aria-invalid:outline-destructive/60 aria-invalid:ring-destructive/20 dark:aria-invalid:outline-destructive dark:aria-invalid:ring-destructive/50 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 aria-invalid:border-destructive/60 dark:aria-invalid:border-destructive flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-4 focus-visible:outline-1 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:focus-visible:ring-[3px] aria-invalid:focus-visible:outline-none md:text-sm dark:aria-invalid:focus-visible:ring-4', + 'border-gray-400 dark:border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground aria-invalid:outline-destructive/60 aria-invalid:ring-destructive/20 dark:aria-invalid:outline-destructive dark:aria-invalid:ring-destructive/50 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 aria-invalid:border-destructive/60 dark:aria-invalid:border-destructive flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-4 focus-visible:outline-1 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:focus-visible:ring-[3px] aria-invalid:focus-visible:outline-none md:text-sm dark:aria-invalid:focus-visible:ring-4', className, )} {...props} From 0a65946a8aba26b66cd9617ca92812eeed2faf62 Mon Sep 17 00:00:00 2001 From: Sergio Ramirez Date: Fri, 20 Jun 2025 12:56:47 -0400 Subject: [PATCH 02/22] inventario --- apps/api/src/database/index.ts | 1 + apps/api/src/database/schema/inventory.ts | 17 ++ apps/api/src/database/seeds/index.ts | 2 + apps/api/src/database/seeds/inventory.seed.ts | 28 +++ .../inventory/dto/create-product.dto.ts | 32 +++ .../inventory/dto/update-product.dto.ts | 22 ++ .../inventory/entities/inventory.entity.ts | 8 + .../inventory/inventory.controller.ts | 70 ++++++ .../features/inventory/inventory.module.ts | 11 + .../features/inventory/inventory.service.ts | 209 ++++++++++++++++++ apps/web/app/layout.tsx | 6 +- 11 files changed, 403 insertions(+), 3 deletions(-) create mode 100644 apps/api/src/database/schema/inventory.ts create mode 100644 apps/api/src/database/seeds/inventory.seed.ts create mode 100644 apps/api/src/features/inventory/dto/create-product.dto.ts create mode 100644 apps/api/src/features/inventory/dto/update-product.dto.ts create mode 100644 apps/api/src/features/inventory/entities/inventory.entity.ts create mode 100644 apps/api/src/features/inventory/inventory.controller.ts create mode 100644 apps/api/src/features/inventory/inventory.module.ts create mode 100644 apps/api/src/features/inventory/inventory.service.ts 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: [ From ed2a1da038d57c157bb0c3acc64577427509c2b0 Mon Sep 17 00:00:00 2001 From: Sergio Ramirez Date: Fri, 20 Jun 2025 14:43:35 -0400 Subject: [PATCH 03/22] Tabla de inventario agregada --- apps/api/src/app.module.ts | 5 +- .../0002_polite_franklin_richards.sql | 13 + .../migrations/meta/0002_snapshot.json | 1441 +++++++++++++++++ .../database/migrations/meta/_journal.json | 7 + .../features/inventory/inventory.service.ts | 15 +- apps/web/app/dashboard/inventario/page.tsx | 37 + apps/web/components/icons.tsx | 2 + apps/web/constants/data.ts | 9 +- .../feactures/inventory/actions/actions.ts | 153 ++ .../components/inventory/create-user-form.tsx | 222 +++ .../inventory/product-inventory-list.tsx | 47 + .../inventory/product-tables/cell-action.tsx | 90 + .../inventory/product-tables/columns.tsx | 41 + .../use-survey-table-filters.tsx | 59 + .../product-tables/users-table-action.tsx | 36 + .../components/inventory/update-user-form.tsx | 227 +++ .../components/inventory/user-modal.tsx | 69 + .../components/inventory/users-header.tsx | 27 + .../inventory/components/modal-profile.tsx | 57 + .../inventory/components/selectList.tsx | 85 + .../feactures/inventory/components/survey.tsx | 28 + .../inventory/components/update-user-form.tsx | 268 +++ .../inventory/components/user-profile.tsx | 75 + .../inventory/hooks/use-mutation-users.ts | 45 + .../inventory/hooks/use-query-surveys.ts | 12 + .../inventory/hooks/use-query-users.ts | 8 + .../inventory/schemas/account-plan-options.ts | 19 + .../inventory/schemas/account-plan.schema.ts | 83 + .../feactures/inventory/schemas/inventory.ts | 69 + .../inventory/schemas/surveys-options.ts | 6 + .../feactures/inventory/utils/date-utils.ts | 11 + .../feactures/inventory/utils/searchparams.ts | 16 + apps/web/public/apple.avif | Bin 0 -> 8030 bytes 33 files changed, 3272 insertions(+), 10 deletions(-) create mode 100644 apps/api/src/database/migrations/0002_polite_franklin_richards.sql create mode 100644 apps/api/src/database/migrations/meta/0002_snapshot.json create mode 100644 apps/web/app/dashboard/inventario/page.tsx create mode 100644 apps/web/feactures/inventory/actions/actions.ts create mode 100644 apps/web/feactures/inventory/components/inventory/create-user-form.tsx create mode 100644 apps/web/feactures/inventory/components/inventory/product-inventory-list.tsx create mode 100644 apps/web/feactures/inventory/components/inventory/product-tables/cell-action.tsx create mode 100644 apps/web/feactures/inventory/components/inventory/product-tables/columns.tsx create mode 100644 apps/web/feactures/inventory/components/inventory/product-tables/use-survey-table-filters.tsx create mode 100644 apps/web/feactures/inventory/components/inventory/product-tables/users-table-action.tsx create mode 100644 apps/web/feactures/inventory/components/inventory/update-user-form.tsx create mode 100644 apps/web/feactures/inventory/components/inventory/user-modal.tsx create mode 100644 apps/web/feactures/inventory/components/inventory/users-header.tsx create mode 100644 apps/web/feactures/inventory/components/modal-profile.tsx create mode 100644 apps/web/feactures/inventory/components/selectList.tsx create mode 100644 apps/web/feactures/inventory/components/survey.tsx create mode 100644 apps/web/feactures/inventory/components/update-user-form.tsx create mode 100644 apps/web/feactures/inventory/components/user-profile.tsx create mode 100644 apps/web/feactures/inventory/hooks/use-mutation-users.ts create mode 100644 apps/web/feactures/inventory/hooks/use-query-surveys.ts create mode 100644 apps/web/feactures/inventory/hooks/use-query-users.ts create mode 100644 apps/web/feactures/inventory/schemas/account-plan-options.ts create mode 100644 apps/web/feactures/inventory/schemas/account-plan.schema.ts create mode 100644 apps/web/feactures/inventory/schemas/inventory.ts create mode 100644 apps/web/feactures/inventory/schemas/surveys-options.ts create mode 100644 apps/web/feactures/inventory/utils/date-utils.ts create mode 100644 apps/web/feactures/inventory/utils/searchparams.ts create mode 100644 apps/web/public/apple.avif diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index ac53a17..d9c59be 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -18,7 +18,7 @@ import { MailModule } from './features/mail/mail.module'; import { RolesModule } from './features/roles/roles.module'; import { UserRolesModule } from './features/user-roles/user-roles.module'; import { SurveysModule } from './features/surveys/surveys.module'; - +import {InventoryModule} from './features/inventory/inventory.module' @Module({ providers: [ @@ -58,7 +58,8 @@ import { SurveysModule } from './features/surveys/surveys.module'; UserRolesModule, ConfigurationsModule, SurveysModule, - LocationModule + LocationModule, + InventoryModule ], }) export class AppModule {} diff --git a/apps/api/src/database/migrations/0002_polite_franklin_richards.sql b/apps/api/src/database/migrations/0002_polite_franklin_richards.sql new file mode 100644 index 0000000..a4b57f2 --- /dev/null +++ b/apps/api/src/database/migrations/0002_polite_franklin_richards.sql @@ -0,0 +1,13 @@ +CREATE TABLE "products" ( + "id" serial PRIMARY KEY NOT NULL, + "title" text NOT NULL, + "description" text NOT NULL, + "price" numeric NOT NULL, + "stock" integer NOT NULL, + "url_img" text NOT NULL, + "user_id" integer, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp (3) +); +--> statement-breakpoint +ALTER TABLE "products" ADD CONSTRAINT "products_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/apps/api/src/database/migrations/meta/0002_snapshot.json b/apps/api/src/database/migrations/meta/0002_snapshot.json new file mode 100644 index 0000000..8424e82 --- /dev/null +++ b/apps/api/src/database/migrations/meta/0002_snapshot.json @@ -0,0 +1,1441 @@ +{ + "id": "e3f08a0a-764c-4a4d-8473-c666398b8722", + "prevId": "cdbb3495-688f-4b2d-ab8e-e62e42328fd5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_logs": { + "name": "activity_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "activityLogs_idx": { + "name": "activityLogs_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_logs_user_id_users_id_fk": { + "name": "activity_logs_user_id_users_id_fk", + "tableFrom": "activity_logs", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.roles": { + "name": "roles", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "roles_idx": { + "name": "roles_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.sessions": { + "name": "sessions", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sessions_idx": { + "name": "sessions_idx", + "columns": [ + { + "expression": "session_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.users": { + "name": "users", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fullname": { + "name": "fullname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "municipality": { + "name": "municipality", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parish": { + "name": "parish", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_two_factor_enabled": { + "name": "is_two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "two_factor_secret": { + "name": "two_factor_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_email_verified": { + "name": "is_email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_idx": { + "name": "users_idx", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_state_states_id_fk": { + "name": "users_state_states_id_fk", + "tableFrom": "users", + "tableTo": "states", + "columnsFrom": [ + "state" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "users_municipality_municipalities_id_fk": { + "name": "users_municipality_municipalities_id_fk", + "tableFrom": "users", + "tableTo": "municipalities", + "columnsFrom": [ + "municipality" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "users_parish_parishes_id_fk": { + "name": "users_parish_parishes_id_fk", + "tableFrom": "users", + "tableTo": "parishes", + "columnsFrom": [ + "parish" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.user_role": { + "name": "user_role", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_role_idx": { + "name": "user_role_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_role_user_id_users_id_fk": { + "name": "user_role_user_id_users_id_fk", + "tableFrom": "user_role", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_role_role_id_roles_id_fk": { + "name": "user_role_role_id_roles_id_fk", + "tableFrom": "user_role", + "tableTo": "roles", + "schemaTo": "auth", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.verificationToken": { + "name": "verificationToken", + "schema": "auth", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.category_type": { + "name": "category_type", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "group": { + "name": "group", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "category_typeIx0": { + "name": "category_typeIx0", + "columns": [ + { + "expression": "group", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "category_typeIx1": { + "name": "category_typeIx1", + "columns": [ + { + "expression": "description", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.localities": { + "name": "localities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "state_id": { + "name": "state_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parish_id": { + "name": "parish_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "localities_index_03": { + "name": "localities_index_03", + "columns": [ + { + "expression": "state_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parish_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "localities_index_00": { + "name": "localities_index_00", + "columns": [ + { + "expression": "state_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "localities_index_01": { + "name": "localities_index_01", + "columns": [ + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "localities_index_02": { + "name": "localities_index_02", + "columns": [ + { + "expression": "parish_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "localities_state_id_states_id_fk": { + "name": "localities_state_id_states_id_fk", + "tableFrom": "localities", + "tableTo": "states", + "columnsFrom": [ + "state_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "localities_municipality_id_municipalities_id_fk": { + "name": "localities_municipality_id_municipalities_id_fk", + "tableFrom": "localities", + "tableTo": "municipalities", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "localities_parish_id_parishes_id_fk": { + "name": "localities_parish_id_parishes_id_fk", + "tableFrom": "localities", + "tableTo": "parishes", + "columnsFrom": [ + "parish_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "localities_name_unique": { + "name": "localities_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.municipalities": { + "name": "municipalities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_id": { + "name": "state_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "municipalities_index_00": { + "name": "municipalities_index_00", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "municipalities_state_id_states_id_fk": { + "name": "municipalities_state_id_states_id_fk", + "tableFrom": "municipalities", + "tableTo": "states", + "columnsFrom": [ + "state_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.parishes": { + "name": "parishes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "parishes_index_00": { + "name": "parishes_index_00", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "parishes_municipality_id_municipalities_id_fk": { + "name": "parishes_municipality_id_municipalities_id_fk", + "tableFrom": "parishes", + "tableTo": "municipalities", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.states": { + "name": "states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "states_index_00": { + "name": "states_index_00", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.products": { + "name": "products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "stock": { + "name": "stock", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "url_img": { + "name": "url_img", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "products_user_id_users_id_fk": { + "name": "products_user_id_users_id_fk", + "tableFrom": "products", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.answers_surveys": { + "name": "answers_surveys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "survey_id": { + "name": "survey_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "answers": { + "name": "answers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "answers_index_00": { + "name": "answers_index_00", + "columns": [ + { + "expression": "answers", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "answers_index_01": { + "name": "answers_index_01", + "columns": [ + { + "expression": "survey_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "answers_index_02": { + "name": "answers_index_02", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "answers_surveys_survey_id_surveys_id_fk": { + "name": "answers_surveys_survey_id_surveys_id_fk", + "tableFrom": "answers_surveys", + "tableTo": "surveys", + "columnsFrom": [ + "survey_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "answers_surveys_user_id_users_id_fk": { + "name": "answers_surveys_user_id_users_id_fk", + "tableFrom": "answers_surveys", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.surveys": { + "name": "surveys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_audience": { + "name": "target_audience", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "closing_date": { + "name": "closing_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "questions": { + "name": "questions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "surveys_index_00": { + "name": "surveys_index_00", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "auth.gender": { + "name": "gender", + "schema": "auth", + "values": [ + "FEMENINO", + "MASCULINO" + ] + }, + "public.nationality": { + "name": "nationality", + "schema": "public", + "values": [ + "VENEZOLANO", + "EXTRANJERO" + ] + }, + "auth.status": { + "name": "status", + "schema": "auth", + "values": [ + "ACTIVE", + "INACTIVE" + ] + } + }, + "schemas": { + "auth": "auth" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "auth.user_access_view": { + "columns": { + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "role_name": { + "name": "role_name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT\n u.id AS user_id,\n u.username,\n u.email,\n u.fullname,\n r.id AS role_id,\n r.name AS role_name\nFROM\n auth.users u\nLEFT JOIN\n auth.user_role ur ON u.id = ur.user_id \nLEFT JOIN\n auth.roles r ON ur.role_id = r.id", + "name": "user_access_view", + "schema": "auth", + "isExisting": false, + "materialized": false + }, + "public.v_surveys": { + "columns": { + "survey_id": { + "name": "survey_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "closing_date": { + "name": "closing_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "target_audience": { + "name": "target_audience", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select id as survey_id, title, description, created_at, closing_date, target_audience from surveys\nwhere published = true", + "name": "v_surveys", + "schema": "public", + "isExisting": false, + "materialized": false + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/src/database/migrations/meta/_journal.json b/apps/api/src/database/migrations/meta/_journal.json index 0f874c3..2ef8b9a 100644 --- a/apps/api/src/database/migrations/meta/_journal.json +++ b/apps/api/src/database/migrations/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1747665408016, "tag": "0001_massive_kylun", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1750442271575, + "tag": "0002_polite_franklin_richards", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/api/src/features/inventory/inventory.service.ts b/apps/api/src/features/inventory/inventory.service.ts index ff1f222..115fcdc 100644 --- a/apps/api/src/features/inventory/inventory.service.ts +++ b/apps/api/src/features/inventory/inventory.service.ts @@ -1,13 +1,11 @@ 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 { UpdateProductDto } from './dto/update-product.dto'; import { Product } from './entities/inventory.entity'; import { PaginationDto } from '../../common/dto/pagination.dto'; @@ -53,8 +51,9 @@ export class InventoryService { id: products.id, title: products.title, description: products.description, - valuePerUnit: products.valuePerUnit, + price: products.price, urlImg: products.urlImg, + stock: products.stock, // price: products.price, // quantity: products.quantity, // isActive: products.isActive @@ -79,7 +78,7 @@ export class InventoryService { previousPage: page > 1 ? page - 1 : null, }; - // console.log(data); + console.log(data); return { data, meta }; } @@ -90,8 +89,9 @@ export class InventoryService { id: products.id, title: products.title, description: products.description, - valuePerUnit: products.valuePerUnit, + price: products.price, urlImg: products.urlImg, + stock: products.stock }) .from(products) // .leftJoin(usersRole, eq(usersRole.userId, users.id)) @@ -124,8 +124,9 @@ export class InventoryService { .values({ title: createProductDto.title, description: createProductDto.description, - valuePerUnit: createProductDto.valuePerUnit, + price: createProductDto.price, urlImg: createProductDto.urlImg, + stock: createProductDto.stock }) .returning(); diff --git a/apps/web/app/dashboard/inventario/page.tsx b/apps/web/app/dashboard/inventario/page.tsx new file mode 100644 index 0000000..2534d48 --- /dev/null +++ b/apps/web/app/dashboard/inventario/page.tsx @@ -0,0 +1,37 @@ +import PageContainer from '@/components/layout/page-container'; +import UsersAdminList from '@/feactures/inventory/components/inventory/product-inventory-list'; +import { UsersHeader } from '@/feactures/inventory/components/inventory/users-header'; +import UsersTableAction from '@/feactures/inventory/components/inventory/product-tables/users-table-action'; +import { searchParamsCache, serialize } from '@/feactures/inventory/utils/searchparams'; +import { SearchParams } from 'nuqs'; + +type pageProps = { + searchParams: Promise; +}; + + +export default async function SurveyAdminPage(props: pageProps) { + const searchParams = await props.searchParams; + searchParamsCache.parse(searchParams); + const key = serialize({ ...searchParams }); + + const page = Number(searchParamsCache.get('page')) || 1; + const search = searchParamsCache.get('q'); + const pageLimit = Number(searchParamsCache.get('limit')) || 10; + const type = searchParamsCache.get('type'); + + return ( + +
+ + + +
+
+ ); +} \ No newline at end of file diff --git a/apps/web/components/icons.tsx b/apps/web/components/icons.tsx index 74a63bb..082e650 100644 --- a/apps/web/components/icons.tsx +++ b/apps/web/components/icons.tsx @@ -1,6 +1,7 @@ import { AlertTriangle, ArrowRight, + Blocks, Check, ChevronLeft, ChevronRight, @@ -40,6 +41,7 @@ export type Icon = LucideIcon; export const Icons = { dashboard: LayoutDashboardIcon, + blocks: Blocks, logo: Command, login: LogIn, close: X, diff --git a/apps/web/constants/data.ts b/apps/web/constants/data.ts index 72ce81c..de2edfe 100644 --- a/apps/web/constants/data.ts +++ b/apps/web/constants/data.ts @@ -10,7 +10,14 @@ export const GeneralItems: NavItem[] = [ isActive: false, items: [], // No child items }, - + { + title: 'Inventario', + url: '/dashboard/inventario/', + icon: 'blocks', + shortcut: ['p', 'p'], + isActive: false, + items: [], // No child items + }, ]; diff --git a/apps/web/feactures/inventory/actions/actions.ts b/apps/web/feactures/inventory/actions/actions.ts new file mode 100644 index 0000000..ec0e850 --- /dev/null +++ b/apps/web/feactures/inventory/actions/actions.ts @@ -0,0 +1,153 @@ +'use server'; +import { safeFetchApi } from '@/lib/fetch.api'; +import { + surveysApiResponseSchema, + CreateUser, + productMutate, + UpdateUser +} from '../schemas/inventory'; + +import { auth } from '@/lib/auth'; + + +export const getProfileAction = async () => { + const session = await auth() + const id = session?.user?.id + + const [error, response] = await safeFetchApi( + productMutate, + `/users/${id}`, + 'GET' + ); + if (error) throw new Error(error.message); + return response; +}; + +export const updateProfileAction = async (payload: UpdateUser) => { + const { id, ...payloadWithoutId } = payload; + + const [error, data] = await safeFetchApi( + productMutate, + `/users/profile/${id}`, + 'PATCH', + payloadWithoutId, + ); + + console.log(payload); + if (error) { + if (error.message === 'Email already exists') { + throw new Error('Ese correo ya está en uso'); + } + // console.error('Error:', error); + throw new Error('Error al crear el usuario'); + } + return data; +}; + +export const getInventoryAction = async (params: { + page?: number; + limit?: number; + search?: string; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; +}) => { + + const searchParams = new URLSearchParams({ + page: (params.page || 1).toString(), + limit: (params.limit || 10).toString(), + ...(params.search && { search: params.search }), + ...(params.sortBy && { sortBy: params.sortBy }), + ...(params.sortOrder && { sortOrder: params.sortOrder }), + }); + + const [error, response] = await safeFetchApi( + surveysApiResponseSchema, + `/inventory?${searchParams}`, + 'GET', + ); + + if (error) { + console.error(error); + + throw new Error(error.message); + } + + + // const transformedData = response?.data ? transformSurvey(response?.data) : undefined; + + return { + data: response?.data || [], + meta: response?.meta || { + page: 1, + limit: 10, + totalCount: 0, + totalPages: 1, + hasNextPage: false, + hasPreviousPage: false, + nextPage: null, + previousPage: null, + }, + }; +} + +export const createUserAction = async (payload: CreateUser) => { + const { id, confirmPassword, ...payloadWithoutId } = payload; + + const [error, data] = await safeFetchApi( + productMutate, + '/users', + 'POST', + payloadWithoutId, + ); + + if (error) { + if (error.message === 'Username already exists') { + throw new Error('Ese usuario ya existe'); + } + if (error.message === 'Email already exists') { + throw new Error('Ese correo ya está en uso'); + } + // console.error('Error:', error); + throw new Error('Error al crear el usuario'); + } + + return payloadWithoutId; +}; + +export const updateUserAction = async (payload: UpdateUser) => { + try { + const { id, ...payloadWithoutId } = payload; + + const [error, data] = await safeFetchApi( + productMutate, + `/users/${id}`, + 'PATCH', + payloadWithoutId, + ); + + // console.log(data); + if (error) { + console.error(error); + + throw new Error(error?.message || 'Error al actualizar el usuario'); + } + return data; + } catch (error) { + console.error(error); + } +} + +export const deleteUserAction = async (id: Number) => { + const [error] = await safeFetchApi( + productMutate, + `/users/${id}`, + 'DELETE' + ) + + console.log(error); + + + // if (error) throw new Error(error.message || 'Error al eliminar el usuario') + + return true; +} \ No newline at end of file diff --git a/apps/web/feactures/inventory/components/inventory/create-user-form.tsx b/apps/web/feactures/inventory/components/inventory/create-user-form.tsx new file mode 100644 index 0000000..86cbc86 --- /dev/null +++ b/apps/web/feactures/inventory/components/inventory/create-user-form.tsx @@ -0,0 +1,222 @@ +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { Button } from '@repo/shadcn/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@repo/shadcn/form'; +import { Input } from '@repo/shadcn/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@repo/shadcn/select'; +import { useForm } from 'react-hook-form'; +import { useCreateUser } from "../../hooks/use-mutation-users"; +import { CreateUser, createUser } from '../../schemas/inventory'; + +const ROLES = { + // 1: 'Superadmin', + 2: 'Administrador', + 3: 'autoridad', + 4: 'Gerente', + 5: 'Usuario', + 6: 'Productor', + 7: 'Organización' +} + +interface CreateUserFormProps { + onSuccess?: () => void; + onCancel?: () => void; + defaultValues?: Partial; +} + +export function CreateUserForm({ + onSuccess, + onCancel, + defaultValues, +}: CreateUserFormProps) { + const { + mutate: saveAccountingAccounts, + isPending: isSaving, + isError, + } = useCreateUser(); + + // const { data: AccoutingAccounts } = useSurveyMutation(); + + const defaultformValues = { + username: defaultValues?.username || '', + fullname: defaultValues?.fullname || '', + email: defaultValues?.email || '', + password: '', + confirmPassword: '', + id: defaultValues?.id, + phone: defaultValues?.phone || '', + role: defaultValues?.role, + } + + const form = useForm({ + resolver: zodResolver(createUser), + defaultValues: defaultformValues, + mode: 'onChange', // Enable real-time validation + }); + + const onSubmit = async (data: CreateUser) => { + + const formData = data + + saveAccountingAccounts(formData, { + onSuccess: () => { + form.reset(); + onSuccess?.(); + }, + onError: (e) => { + form.setError('root', { + type: 'manual', + message: e.message, + }); + }, + }); + }; + + return ( +
+ + {form.formState.errors.root && ( +
+ {form.formState.errors.root.message} +
+ )} +
+ ( + + Usuario + + + + + + )} + /> + + ( + + Nombre completo + + + + + + )} + /> + + ( + + Correo + + + + + + )} + /> + + ( + + Teléfono + + + + + + )} + /> + + ( + + Contraseña + + + + + + )} + /> + + ( + + Confirmar Contraseña + + + + + + )} + /> + + ( + + Rol + + + + )} + /> +
+ +
+ + +
+
+ + ); +} diff --git a/apps/web/feactures/inventory/components/inventory/product-inventory-list.tsx b/apps/web/feactures/inventory/components/inventory/product-inventory-list.tsx new file mode 100644 index 0000000..3fcb913 --- /dev/null +++ b/apps/web/feactures/inventory/components/inventory/product-inventory-list.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { DataTable } from '@repo/shadcn/table/data-table'; +import { DataTableSkeleton } from '@repo/shadcn/table/data-table-skeleton'; +import { columns } from './product-tables/columns'; +import { useProductQuery } from '../../hooks/use-query-users'; + +interface SurveysAdminListProps { + initialPage: number; + initialSearch?: string | null; + initialLimit: number; + initialType?: string | null; +} + +export default function UsersAdminList({ + initialPage, + initialSearch, + initialLimit, + initialType, +}: SurveysAdminListProps) { + const filters = { + page: initialPage, + limit: initialLimit, + ...(initialSearch && { search: initialSearch }), + ...(initialType && { type: initialType }), + }; + + const {data, isLoading} = useProductQuery(filters) + + + // const {data, isLoading} = useUsersQuery(filters) + + console.log(data?.data); + + if (isLoading) { + return ; + } + + return ( + + ); +} diff --git a/apps/web/feactures/inventory/components/inventory/product-tables/cell-action.tsx b/apps/web/feactures/inventory/components/inventory/product-tables/cell-action.tsx new file mode 100644 index 0000000..53fac03 --- /dev/null +++ b/apps/web/feactures/inventory/components/inventory/product-tables/cell-action.tsx @@ -0,0 +1,90 @@ +'use client'; +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { AlertModal } from '@/components/modal/alert-modal'; +import { Button } from '@repo/shadcn/button'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@repo/shadcn/tooltip'; +import { Edit, Trash, User } from 'lucide-react'; +import { InventoryTable } from '@/feactures/inventory/schemas/inventory'; +import { useDeleteUser } from '@/feactures/users/hooks/use-mutation-users'; +import { AccountPlanModal } from '../user-modal'; + +interface CellActionProps { + data: InventoryTable; +} + +export const CellAction: React.FC = ({ data }) => { + const [loading, setLoading] = useState(false); + const [open, setOpen] = useState(false); + const [edit, setEdit] = useState(false); + const { mutate: deleteUser } = useDeleteUser(); + const router = useRouter(); + + const onConfirm = async () => { + try { + setLoading(true); + deleteUser(data.id!); + setOpen(false); + } catch (error) { + console.error('Error:', error); + } finally { + setLoading(false); + } + }; + + return ( + <> + setOpen(false)} + onConfirm={onConfirm} + loading={loading} + title="¿Estás seguro que desea deshabilitar este usuario?" + description="Esta acción no se puede deshacer." + /> + + + +
+ + + + + + +

Editar

+
+
+
+ + + + + + + +

Deshabilitar

+
+
+
+
+ + ); +}; diff --git a/apps/web/feactures/inventory/components/inventory/product-tables/columns.tsx b/apps/web/feactures/inventory/components/inventory/product-tables/columns.tsx new file mode 100644 index 0000000..a9b47bf --- /dev/null +++ b/apps/web/feactures/inventory/components/inventory/product-tables/columns.tsx @@ -0,0 +1,41 @@ +import { Badge } from "@repo/shadcn/badge"; + +import { ColumnDef } from '@tanstack/react-table'; +import { CellAction } from './cell-action'; +import { InventoryTable } from '../../../schemas/inventory'; + +export const columns: ColumnDef[] = [ + { + accessorKey: 'urlImg', + header: 'img', + cell: ({ row }) => { + const status = row.getValue("urlImg") as string | undefined; + return ( + Image + ) + }, + }, + { + accessorKey: 'title', + header: 'Producto', + }, + { + accessorKey: "description", + header: "Descripcion", + }, + { + accessorKey: 'price', + header: 'Precio', + cell: ({ row }) => `${row.original.price}$` + }, + { + accessorKey: 'stock', + header: 'Stock', + }, + + { + id: 'actions', + header: 'Acciones', + cell: ({ row }) => , + }, +]; diff --git a/apps/web/feactures/inventory/components/inventory/product-tables/use-survey-table-filters.tsx b/apps/web/feactures/inventory/components/inventory/product-tables/use-survey-table-filters.tsx new file mode 100644 index 0000000..65162d4 --- /dev/null +++ b/apps/web/feactures/inventory/components/inventory/product-tables/use-survey-table-filters.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { PUBLISHED_TYPES } from '@/feactures/surveys/schemas/surveys-options'; +import { searchParams } from '@repo/shadcn/lib/searchparams'; +import { useQueryState } from 'nuqs'; +import { useCallback, useMemo } from 'react'; + + +export const TYPE_OPTIONS = Object.entries(PUBLISHED_TYPES).map( + ([value, label]) => ({ + value, + label, + }), +); + + +export function useSurveyTableFilters() { + const [searchQuery, setSearchQuery] = useQueryState( + 'q', + searchParams.q + .withOptions({ + shallow: false, + throttleMs: 500, // Add 500ms delay + // Removed dedupingInterval as it's not a valid option + }) + .withDefault(''), + ); + + const [typeFilter, setTypeFilter] = useQueryState( + 'published', + searchParams.q.withOptions({ shallow: false }).withDefault(''), + ); + + const [page, setPage] = useQueryState( + 'page', + searchParams.page.withDefault(1), + ); + + const resetFilters = useCallback(() => { + setSearchQuery(null); + setTypeFilter(null); + setPage(1); + }, [setSearchQuery, setPage]); + + const isAnyFilterActive = useMemo(() => { + return !!searchQuery || !!typeFilter; + }, [searchQuery]); + + return { + searchQuery, + setSearchQuery, + page, + setPage, + resetFilters, + isAnyFilterActive, + typeFilter, + setTypeFilter + }; +} diff --git a/apps/web/feactures/inventory/components/inventory/product-tables/users-table-action.tsx b/apps/web/feactures/inventory/components/inventory/product-tables/users-table-action.tsx new file mode 100644 index 0000000..bc714d0 --- /dev/null +++ b/apps/web/feactures/inventory/components/inventory/product-tables/users-table-action.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { DataTableFilterBox } from '@repo/shadcn/table/data-table-filter-box'; +import { DataTableSearch } from '@repo/shadcn/table/data-table-search'; +import { + TYPE_OPTIONS, + useSurveyTableFilters, +} from './use-survey-table-filters'; + +export default function UserTableAction() { + const { + typeFilter, + searchQuery, + setPage, + setTypeFilter, + setSearchQuery, + } = useSurveyTableFilters(); + + return ( +
+ + {/* */} +
+ ); +} diff --git a/apps/web/feactures/inventory/components/inventory/update-user-form.tsx b/apps/web/feactures/inventory/components/inventory/update-user-form.tsx new file mode 100644 index 0000000..b96d14f --- /dev/null +++ b/apps/web/feactures/inventory/components/inventory/update-user-form.tsx @@ -0,0 +1,227 @@ +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { Button } from '@repo/shadcn/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@repo/shadcn/form'; +import { Input } from '@repo/shadcn/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@repo/shadcn/select'; +import { useForm } from 'react-hook-form'; +import { useUpdateUser } from "@/feactures/users/hooks/use-mutation-users"; +import { UpdateUser, updateUser } from '@/feactures/users/schemas/users'; + +const ROLES = { + // 1: 'Superadmin', + 2: 'Administrador', + 3: 'autoridad', + 4: 'Gerente', + 5: 'Usuario', + 6: 'Productor', + 7: 'Organización' +} + +interface UserFormProps { + onSuccess?: () => void; + onCancel?: () => void; + defaultValues?: Partial; +} + +export function UpdateUserForm({ + onSuccess, + onCancel, + defaultValues, +}: UserFormProps) { + const { + mutate: saveAccountingAccounts, + isPending: isSaving, + isError, + } = useUpdateUser(); + + const defaultformValues = { + username: defaultValues?.username || '', + fullname: defaultValues?.fullname || '', + email: defaultValues?.email || '', + password: '', + id: defaultValues?.id, + phone: defaultValues?.phone || '', + role: undefined, + isActive: defaultValues?.isActive + } + + // console.log(defaultValues); + + const form = useForm({ + resolver: zodResolver(updateUser), + defaultValues: defaultformValues, + mode: 'onChange', // Enable real-time validation + }); + + const onSubmit = async (data: UpdateUser) => { + + const formData = data + + saveAccountingAccounts(formData, { + onSuccess: () => { + form.reset(); + onSuccess?.(); + }, + onError: () => { + form.setError('root', { + type: 'manual', + message: 'Error al guardar la cuenta contable', + }); + }, + }); + }; + + return ( +
+ + {form.formState.errors.root && ( +
+ {form.formState.errors.root.message} +
+ )} +
+ ( + + Usuario + + + + + + )} + /> + + ( + + Nombre completo + + + + + + )} + /> + + ( + + Correo + + + + + + )} + /> + + ( + + Teléfono + + + + + + )} + /> + + ( + + Nueva Contraseña + + + + + + )} + /> + + ( + + Rol + + + + )} + /> + + ( + + Estatus + + + + )} + /> +
+ +
+ + +
+
+ + ); +} diff --git a/apps/web/feactures/inventory/components/inventory/user-modal.tsx b/apps/web/feactures/inventory/components/inventory/user-modal.tsx new file mode 100644 index 0000000..b4230f8 --- /dev/null +++ b/apps/web/feactures/inventory/components/inventory/user-modal.tsx @@ -0,0 +1,69 @@ +'use client'; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@repo/shadcn/dialog'; +import { AccountPlan } from '@/feactures/users/schemas/account-plan.schema'; +import { CreateUserForm } from './create-user-form'; +import { UpdateUserForm } from './update-user-form'; + +interface AccountPlanModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + defaultValues?: Partial; +} + +export function AccountPlanModal({ + open, + onOpenChange, + defaultValues, +}: AccountPlanModalProps) { + const handleSuccess = () => { + onOpenChange(false); + }; + + const handleCancel = () => { + onOpenChange(false); + }; + + return ( + { + if (!open) { + onOpenChange(false); + } + }} + > + + + + {defaultValues?.id + ? 'Actualizar usuario' + : 'Crear usuario'} + + + Complete los campos para {defaultValues?.id ? 'actualizar' : 'crear'} un usuario + + + {defaultValues?.id ? ( + + ): ( + + )} + + + ); +} diff --git a/apps/web/feactures/inventory/components/inventory/users-header.tsx b/apps/web/feactures/inventory/components/inventory/users-header.tsx new file mode 100644 index 0000000..891c9dc --- /dev/null +++ b/apps/web/feactures/inventory/components/inventory/users-header.tsx @@ -0,0 +1,27 @@ +'use client'; +import { useRouter } from 'next/navigation'; +import { Button } from '@repo/shadcn/button'; +import { Heading } from '@repo/shadcn/heading'; +import { Plus } from 'lucide-react'; +import { useState } from 'react'; +import { AccountPlanModal } from './user-modal'; + +export function UsersHeader() { + const [open, setOpen] = useState(false); + // const router = useRouter(); + return ( + <> +
+ + +
+ + + + ); +} \ No newline at end of file diff --git a/apps/web/feactures/inventory/components/modal-profile.tsx b/apps/web/feactures/inventory/components/modal-profile.tsx new file mode 100644 index 0000000..4f1853a --- /dev/null +++ b/apps/web/feactures/inventory/components/modal-profile.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@repo/shadcn/dialog'; +import { AccountPlan } from '@/feactures/users/schemas/account-plan.schema'; +import { ModalForm } from './update-user-form'; + +interface AccountPlanModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + defaultValues?: Partial; +} + +export function AccountPlanModal({ + open, + onOpenChange, + defaultValues, +}: AccountPlanModalProps) { + const handleSuccess = () => { + onOpenChange(false); + }; + + const handleCancel = () => { + onOpenChange(false); + }; + + return ( + { + if (!open) { + onOpenChange(false); + } + }} + > + + + Actualizar Perfil + + Complete los campos para actualizar sus datos.
Los campos vacios no seran actualizados. +
+
+ + +
+
+ ); +} diff --git a/apps/web/feactures/inventory/components/selectList.tsx b/apps/web/feactures/inventory/components/selectList.tsx new file mode 100644 index 0000000..4150cd5 --- /dev/null +++ b/apps/web/feactures/inventory/components/selectList.tsx @@ -0,0 +1,85 @@ +// 'use client'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@repo/shadcn/select'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@repo/shadcn/form'; +import { UpdateUser, updateUser } from '@/feactures/users/schemas/users'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; + +interface SelectListProps { + label: string + // values: + values: Array + form: any + name: string + handleChange: any +} + +export function SelectList({ label, values, form, name, handleChange }: SelectListProps) { + // const { label, values, form, name } = props; + // handleChange + + // const defaultformValues = { + // username: '', + // fullname: '', + // email: '', + // password: '', + // id: 0, + // phone: '', + // role: undefined, + // isActive: false + // } + + // const form = useForm({ + // resolver: zodResolver(updateUser), + // defaultValues: defaultformValues, + // mode: 'onChange', // Enable real-time validation + // }); + + + return ( + + {label} + + + + )} + /> +} \ No newline at end of file diff --git a/apps/web/feactures/inventory/components/survey.tsx b/apps/web/feactures/inventory/components/survey.tsx new file mode 100644 index 0000000..b4b5eb9 --- /dev/null +++ b/apps/web/feactures/inventory/components/survey.tsx @@ -0,0 +1,28 @@ +'use client' + +import { SurveyResponse } from '@/feactures/surveys/components/survey-response'; +import { useSurveysByIdQuery } from '@/feactures/surveys/hooks/use-query-surveys'; + +import { notFound, useParams } from 'next/navigation'; + + +export default function SurveyPage() { + const params = useParams(); + const surveyId = params?.id as string | undefined; + + + if (!surveyId || surveyId === '') { + notFound(); + } + + const { data: survey, isLoading } = useSurveysByIdQuery(Number(surveyId)); + console.log('🎯 useSurveysByIdQuery ejecutado, data:', survey, 'isLoading:', isLoading); + + if (!survey?.data || !survey?.data.published) { + notFound(); + } + + return ( + + ); +} \ No newline at end of file diff --git a/apps/web/feactures/inventory/components/update-user-form.tsx b/apps/web/feactures/inventory/components/update-user-form.tsx new file mode 100644 index 0000000..6398864 --- /dev/null +++ b/apps/web/feactures/inventory/components/update-user-form.tsx @@ -0,0 +1,268 @@ +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { Button } from '@repo/shadcn/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@repo/shadcn/form'; +import { Input } from '@repo/shadcn/input'; +// import { +// Select, +// SelectContent, +// SelectItem, +// SelectTrigger, +// SelectValue, +// } from '@repo/shadcn/select'; +import { SelectSearchable } from '@repo/shadcn/select-searchable' +import { useForm } from 'react-hook-form'; +import { useUpdateProfile } from "@/feactures/users/hooks/use-mutation-users"; +import { UpdateUser, updateUser } from '@/feactures/users/schemas/users'; +import { toast } from 'sonner'; + +import React from 'react'; +import { useStateQuery, useMunicipalityQuery, useParishQuery } from '@/feactures/location/hooks/use-query-location'; + +interface UserFormProps { + onSuccess?: () => void; + onCancel?: () => void; + defaultValues?: Partial; +} + +export function ModalForm({ + onSuccess, + onCancel, + defaultValues, +}: UserFormProps) { + const { + mutate: saveAccountingAccounts, + isPending: isSaving, + isError, + } = useUpdateProfile(); + + const [state, setState] = React.useState(0); + const [municipality, setMunicipality] = React.useState(0); + const [parish, setParish] = React.useState(0); + + const [disabledMunicipality, setDisabledMunicipality] = React.useState(true); + const [disabledParish, setDisabledParish] = React.useState(true); + + const { data : dataState } = useStateQuery() + const { data : dataMunicipality } = useMunicipalityQuery(state) + const { data : dataParish } = useParishQuery(municipality) + + const stateOptions = dataState?.data || [{id:0,name:'Sin estados'}] + + const municipalityOptions = Array.isArray(dataMunicipality?.data) && dataMunicipality.data.length > 0 + ? dataMunicipality.data + : [{id:0,stateId:0,name:'Sin Municipios'}] + // const parishOptions = dataParish?.data || [{id:0,municipalityId:0,name:'Sin Parroquias'}] + const parishOptions = Array.isArray(dataParish?.data) && dataParish.data.length > 0 + ? dataParish.data + : [{id:0,stateId:0,name:'Sin Parroquias'}] + + + const defaultformValues = { + username: defaultValues?.username || '', + fullname: defaultValues?.fullname || '', + email: defaultValues?.email || '', + password: '', + id: defaultValues?.id, + phone: defaultValues?.phone || '', + role: undefined, + isActive: defaultValues?.isActive, + state: defaultValues?.state, + municipality: defaultValues?.municipality, + parish: defaultValues?.parish + } + + + + // console.log(defaultValues); + + const form = useForm({ + resolver: zodResolver(updateUser), + defaultValues: defaultformValues, + mode: 'onChange', // Enable real-time validation + }); + + const onSubmit = async (data: UpdateUser) => { + + const formData = data + + saveAccountingAccounts(formData, { + onSuccess: () => { + form.reset(); + onSuccess?.(); + toast.success('Actualizado exitosamente!'); + }, + onError: (e) => { + form.setError('root', { + type: 'manual', + message: e.message, + }); + // toast.error(e.message); + }, + }); + }; + + return ( +
+ + {form.formState.errors.root && ( +
+ {form.formState.errors.root.message} +
+ )} +
+ ( + + Nombre completo + + + + + + )} + /> + + ( + + Correo + + + + + + )} + /> + + ( + + Teléfono + + + + + + )} + /> + + ( + + Estado + + ({ + value: item.id.toString(), + label: item.name, + })) || [] + } + onValueChange={(value : any) => + {field.onChange(Number(value)); setState(value); setDisabledMunicipality(false); setDisabledParish(true)} + } + placeholder="Selecciona un estado" + defaultValue={field.value?.toString()} + // disabled={readOnly} + /> + + + )} + /> + + ( + + Municipio + + ({ + value: item.id.toString(), + label: item.name, + })) || [] + } + onValueChange={(value : any) => + {field.onChange(Number(value)); setMunicipality(value); setDisabledParish(false)} + } + placeholder="Selecciona un Municipio" + defaultValue={field.value?.toString()} + disabled={disabledMunicipality} + /> + + + )} + /> + + ( + + Parroquia + + ({ + value: item.id.toString(), + label: item.name, + })) || [] + } + onValueChange={(value : any) => + field.onChange(Number(value)) + } + placeholder="Selecciona una Parroquia" + defaultValue={field.value?.toString()} + disabled={disabledParish} + /> + + + )} + /> + + ( + + Nueva Contraseña + + + + + + )} + /> +
+ +
+ + +
+
+ + ); +} diff --git a/apps/web/feactures/inventory/components/user-profile.tsx b/apps/web/feactures/inventory/components/user-profile.tsx new file mode 100644 index 0000000..72c621c --- /dev/null +++ b/apps/web/feactures/inventory/components/user-profile.tsx @@ -0,0 +1,75 @@ +'use client'; +import { useUserByProfile } from '@/feactures/users/hooks/use-query-users'; +import { Button } from '@repo/shadcn/button'; +import { Edit, Edit2 } from 'lucide-react'; +import { useState } from 'react'; +import { AccountPlanModal } from './modal-profile'; + +export function Profile() { + const [open, setOpen] = useState(false); + + const { data } = useUserByProfile(); + + // console.log("🎯 data:", data); + + return ( +
+ + + + +

Datos del usuario

+
+
+

Usuario:

+

{data?.data.username || 'Sin Nombre de Usuario'}

+
+ +
+

Rol:

+

{data?.data.role || 'Sin Rol'}

+
+
+ +

Información personal

+
+
+

Nombre completo:

+

{data?.data.fullname || 'Sin nombre y apellido'}

+
+ +
+

Correo:

+

{data?.data.email || 'Sin correo'}

+
+ +
+

Teléfono:

+

{data?.data.phone || 'Sin teléfono'}

+
+
+ +

Información de ubicación

+
+
+

Estado:

+

{data?.data.state || 'Sin Estado'}

+
+ +
+

Municipio:

+

{data?.data.municipality || 'Sin Municipio'}

+
+ +
+

Parroquia:

+

{data?.data.parish || 'Sin Parroquia'}

+
+
+ +
+ ); +} + diff --git a/apps/web/feactures/inventory/hooks/use-mutation-users.ts b/apps/web/feactures/inventory/hooks/use-mutation-users.ts new file mode 100644 index 0000000..dbbf733 --- /dev/null +++ b/apps/web/feactures/inventory/hooks/use-mutation-users.ts @@ -0,0 +1,45 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { CreateUser, UpdateUser } from "../schemas/inventory"; +import { updateUserAction, createUserAction, deleteUserAction, updateProfileAction } from "../actions/actions"; + +// Create mutation +export function useCreateUser() { + const queryClient = useQueryClient(); + const mutation = useMutation({ + mutationFn: (data: CreateUser) => createUserAction(data), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }), + // onError: (e) => console.error('Error:', e), + }) + return mutation +} + +// Update mutation +export function useUpdateUser() { + const queryClient = useQueryClient(); + const mutation = useMutation({ + mutationFn: (data: UpdateUser) => updateUserAction(data), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }), + onError: (e) => console.error('Error:', e) + }) + return mutation; +} + +export function useUpdateProfile() { + const queryClient = useQueryClient(); + const mutation = useMutation({ + mutationFn: (data: UpdateUser) => updateProfileAction(data), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }), + // onError: (e) => console.error('Error:', e) + }) + return mutation; +} + +// Delete mutation +export function useDeleteUser() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => deleteUserAction(id), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }), + onError: (e) => console.error('Error:', e) + }) +} \ No newline at end of file diff --git a/apps/web/feactures/inventory/hooks/use-query-surveys.ts b/apps/web/feactures/inventory/hooks/use-query-surveys.ts new file mode 100644 index 0000000..6610191 --- /dev/null +++ b/apps/web/feactures/inventory/hooks/use-query-surveys.ts @@ -0,0 +1,12 @@ +'use client' +import { useSafeQuery } from "@/hooks/use-safe-query"; +import { getUsersAction,getProfileAction} from "../actions/actions"; + +// Hook for users +export function useUsersQuery(params = {}) { + return useSafeQuery(['users',params], () => getUsersAction(params)) +} + +export function useUserByProfile() { + return useSafeQuery(['users'], () => getProfileAction()) +} diff --git a/apps/web/feactures/inventory/hooks/use-query-users.ts b/apps/web/feactures/inventory/hooks/use-query-users.ts new file mode 100644 index 0000000..2572cfd --- /dev/null +++ b/apps/web/feactures/inventory/hooks/use-query-users.ts @@ -0,0 +1,8 @@ +'use client' +import { useSafeQuery } from "@/hooks/use-safe-query"; +import { getInventoryAction} from "../actions/actions"; + +// Hook for users +export function useProductQuery(params = {}) { + return useSafeQuery(['product',params], () => getInventoryAction(params)) +} \ No newline at end of file diff --git a/apps/web/feactures/inventory/schemas/account-plan-options.ts b/apps/web/feactures/inventory/schemas/account-plan-options.ts new file mode 100644 index 0000000..b4d997d --- /dev/null +++ b/apps/web/feactures/inventory/schemas/account-plan-options.ts @@ -0,0 +1,19 @@ +export const ACCOUNT_TYPES = { + activo: 'Activo', + pasivo: 'Pasivo', + patrimonio: 'Patrimonio', + ingreso: 'Ingreso', + gasto: 'Gasto', + costo: 'Costo', + cuenta_orden: 'Cuenta de Orden', +} as const; + +export const ACCOUNT_LEVELS = { + 1: 'Nivel 1 - Cuenta Principal', + 2: 'Nivel 2 - Subcuenta', + 3: 'Nivel 3 - Cuenta Detallada', + 4: 'Nivel 4 - Cuenta Auxiliar', +} as const; + +export type AccountType = keyof typeof ACCOUNT_TYPES; +export type AccountLevel = keyof typeof ACCOUNT_LEVELS; \ No newline at end of file diff --git a/apps/web/feactures/inventory/schemas/account-plan.schema.ts b/apps/web/feactures/inventory/schemas/account-plan.schema.ts new file mode 100644 index 0000000..8edc268 --- /dev/null +++ b/apps/web/feactures/inventory/schemas/account-plan.schema.ts @@ -0,0 +1,83 @@ +import { z } from 'zod'; + +export const accountPlanSchema = z + .object({ + id: z.number().optional(), + savingBankId: z.number(), + code: z + .string() + .min(1, 'El código es requerido') + .max(50, 'El código no puede tener más de 50 caracteres') + .regex(/^[\d.]+$/, 'El código debe contener solo números y puntos'), + name: z + .string() + .min(1, 'El nombre es requerido') + .max(100, 'El nombre no puede tener más de 100 caracteres'), + type: z.enum( + [ + 'activo', + 'pasivo', + 'patrimonio', + 'ingreso', + 'gasto', + 'costo', + 'cuenta_orden', + ], + { + required_error: 'El tipo de cuenta es requerido', + invalid_type_error: 'Tipo de cuenta inválido', + }, + ), + description: z.string().optional().nullable(), + level: z + .number() + .min(1, 'El nivel debe ser mayor a 0') + .max(4, 'El nivel no puede ser mayor a 4'), + parent_account_id: z.number().nullable(), + created_at: z.string().optional(), + updated_at: z.string().optional(), + }) + .refine( + (data) => { + if (data.level > 1 && !data.parent_account_id) { + return false; + } + return true; + }, + { + message: 'Las cuentas de nivel superior a 1 requieren una cuenta padre', + path: ['parent_account_id'], + }, + ); + +export type AccountPlan = z.infer; + +// Response schemas for the API +export const accountPlanResponseSchema = z.object({ + message: z.string(), + data: accountPlanSchema, +}); + +export const accountPlanDeleteResponseSchema = z.object({ + message: z.string(), +}); + +export const accountPlanListResponseSchema = z.object({ + message: z.string(), + data: z.array(accountPlanSchema), +}); + +export const accountPlanPaginationResponseSchema = z.object({ + message: z.string(), + data: z.array(accountPlanSchema), + meta: z.object({ + page: z.number(), + limit: z.number(), + totalCount: z.number(), + totalPages: z.number(), + hasNextPage: z.boolean(), + hasPreviousPage: z.boolean(), + nextPage: z.number().nullable(), + previousPage: z.number().nullable(), + }), +}); diff --git a/apps/web/feactures/inventory/schemas/inventory.ts b/apps/web/feactures/inventory/schemas/inventory.ts new file mode 100644 index 0000000..640dd85 --- /dev/null +++ b/apps/web/feactures/inventory/schemas/inventory.ts @@ -0,0 +1,69 @@ +import { title } from 'process'; +import { z } from 'zod'; + +export type InventoryTable = z.infer; +export type CreateUser = z.infer; +export type UpdateUser = z.infer; + +export const product = z.object({ + id: z.number().optional(), + title: z.string(), + description: z.string(), + // price: z.number(), + // quantity: z.number(), + // category: z.string(), + // image: z.string().optional(), + stock: z.number(), + price: z.string(), + urlImg: z.string(), + userId: z.number().optional(), +}); + +export const createUser = z.object({ + id: z.number().optional(), + username: z.string().min(5, { message: "Debe de tener 5 o más caracteres" }), + password: z.string().min(8, { message: "Debe de tener 8 o más caracteres" }), + email: z.string().email({ message: "Correo no válido" }), + fullname: z.string(), + phone: z.string(), + confirmPassword: z.string(), + role: z.number() +}) +.refine((data) => data.password === data.confirmPassword, { + message: 'La contraseña no coincide', + path: ['confirmPassword'], +}) + +export const updateUser = z.object({ + id: z.number(), + username: z.string().min(5, { message: "Debe de tener 5 o más caracteres" }).or(z.literal('')), + password: z.string().min(6, { message: "Debe de tener 6 o más caracteres" }).or(z.literal('')), + email: z.string().email({ message: "Correo no válido" }).or(z.literal('')), + fullname: z.string().optional(), + phone: z.string().optional(), + role: z.number().optional(), + isActive: z.boolean().optional(), + state: z.number().optional().nullable(), + municipality: z.number().optional().nullable(), + parish: z.number().optional().nullable(), +}) + +export const surveysApiResponseSchema = z.object({ + message: z.string(), + data: z.array(product), + meta: z.object({ + page: z.number(), + limit: z.number(), + totalCount: z.number(), + totalPages: z.number(), + hasNextPage: z.boolean(), + hasPreviousPage: z.boolean(), + nextPage: z.number().nullable(), + previousPage: z.number().nullable(), + }), +}) + +export const productMutate = z.object({ + message: z.string(), + data: product, +}) \ No newline at end of file diff --git a/apps/web/feactures/inventory/schemas/surveys-options.ts b/apps/web/feactures/inventory/schemas/surveys-options.ts new file mode 100644 index 0000000..06c4c94 --- /dev/null +++ b/apps/web/feactures/inventory/schemas/surveys-options.ts @@ -0,0 +1,6 @@ +export const PUBLISHED_TYPES = { + published: 'Publicada', + draft: 'Borrador', +} as const; + +export type PublishedType = keyof typeof PUBLISHED_TYPES; diff --git a/apps/web/feactures/inventory/utils/date-utils.ts b/apps/web/feactures/inventory/utils/date-utils.ts new file mode 100644 index 0000000..ec39a0e --- /dev/null +++ b/apps/web/feactures/inventory/utils/date-utils.ts @@ -0,0 +1,11 @@ +export function formatDate(date: Date): string { + return new Intl.DateTimeFormat('es-ES', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }).format(date); +} + + diff --git a/apps/web/feactures/inventory/utils/searchparams.ts b/apps/web/feactures/inventory/utils/searchparams.ts new file mode 100644 index 0000000..2d79317 --- /dev/null +++ b/apps/web/feactures/inventory/utils/searchparams.ts @@ -0,0 +1,16 @@ +import { + createSearchParamsCache, + createSerializer, + parseAsInteger, + parseAsString, +} from 'nuqs/server'; + +export const searchParams = { + page: parseAsInteger.withDefault(1), + limit: parseAsInteger.withDefault(10), + q: parseAsString, + type: parseAsString, +}; + +export const searchParamsCache = createSearchParamsCache(searchParams); +export const serialize = createSerializer(searchParams); diff --git a/apps/web/public/apple.avif b/apps/web/public/apple.avif new file mode 100644 index 0000000000000000000000000000000000000000..60b2ba014a9c68482b3307578e5b05ced2c59dff GIT binary patch literal 8030 zcmXwdWl$VS)9&K#5ZoPt23Xu71a}CsxGWZ6vEUxuU4sU9hv4pR!JXjl?9F*^eLXe( zNcB|r{F|*~v0rM}@fbG7S{imeG0Wkm8zktE~{|Eq3sQ#0*V6emgn*Prv_$Mr2 z|5x_k7@S=HQ5&$Mv*SNh06Us}`-gaD-|U@q=2=}jyLf78h#03|Cib_Po z;mC)KY|TT(-6f184g-%ZA>IJ$*deaLtu`GGNT^e)L|OY;m_Qomdd9b~^PM>vCH77_ z$inbPPc%MY<#KZ)c^nDboY*bd=&13#zZRiu7mz^RLu5Gh4^L!}6Z2k*d53i~LXE3W zQ~bkF<*#v1+C+oqU}feIfr71kDMY!{6pop0swsHVIMx_=Zi0}O2?lYu0fpXK*Ex(N ztMhKyk=@Yh5)U5fv_=hWLDhAdPyGZuNLXpeEZ_K{cn6Icj;c=r6zw5l0OZG)aR{Tl zG)?)#0NNFdD_M0u+vtKLt5LpyC=NHZwKrgT-{uO168k(y!XwIo>YR72jp%CtWcT#s zJ+c=RXu+--{*IaL$AX@jUynZ$KmD>78anW$v;G zP^~x&{;6%vm1nh9ir&4lHEK^tU`1nnZ3^$1JdW< z9gRe@+lUt#)!CFW1yuneS5QynCa$Wig6+cxY2rH9@-#qq@yL5yJO=)je{hAnHz2C4X82FeA7m_$7 z0CxQCQ60hDkk&TK*&d__MmcR)1SCtQMMS*0 zH3>?^BWJsBPSaMuM@#=AROTDh97%f7zsemdZ)vhht`dCvN}4;j@S?yPT@~M@IBSm!*C&X*Ti+?5b>OHREf+K2zC61LdxTkf0ksU6z?EcDEuc@1stnMHyo#Amg~* zPV$V^(CJatShGbJnT_mC3>w_WX_L|xlc(TI-09FmUFU7>*Op8yoIE%;<`q#%kl^-oHJ$4i0d(rO&HiA2^^^5dD`6O|ul*gjAzFNZ4IFWLYmi~8XjVlcm3Ciu7- z{T4$o;2k;1ZZRBa5(&ehp^Ym}!8!>voq(dhk;d~C^}k-qSy?yq2*Ghb;?`gVW`usZ z+yrR-t)IB9wvVoq&{HZ!&VuI-dpG%<_GhSj>6%AK6CythG0w7wF=107QPNzHe|1lY zF?d@GtiidWBkG+v&Zx~YZ{67AY`dBBXwh9?`WglI{vvb5HKw^#Vc85`EiVAq~o?1L7=`vwQE3fvDF3fDI%Gkx^=kJ(7K4<7JPyuRixGSvUtt1SJC$0T`$|FcqGWIg;62;w1Z6$3~roW<)}J4+)M+2 zBHQGfjW=`Nog(=a*e|(;L$jVAc2X*Z3?Q-LEX=W1Gx}}_e?$|sG&^Fcl9l*eN2t}y2iWepWt;whZ# zI_&f=M+%k0z`5H?xuSlZab*e7y$!$`vROZGmZ@Oq7+p6JQdE6cx_4(U@sMQ569Wk} z32&izb?{EBjjE;EYlaX$7koW7jL7&wNWd)1iF8I=B>nytOORP@v(1)F`&MAvh=LEo zF5n!CK}^8_R4KaOTD*!FAcrCtf2KGJ*xrx2l;%s3LX$PHsum_ZT>JGhog0SOvn2J` ze5X!{?O^N0Jmg+|%6RqL%=NoqD-28UT{M%%wvk4yO=)vjOvmh~Mq`klZ6e6HP;!pq3$=avbc4l@yaFj_`IR1f%OkQa)*vZn#NtBC` z_7wF{=e79b)@3!TPy2n8W5*Vn5YPlu3zxudy@Nl^e%*L$AkxFet0qmMTL?$}VVJ?~ zO18FTO(%H@ub0jMArVM9jfnli`|CSLFA;p?z_(c_cRw$j5zN6$FFf-b6w`X9k*od`QtQgj6 zltq}};M2V6#EG0Iyz`|3xq^THfzyq9UU8~92matauFh`Eit%Ea#>3(QKOA4xTwz0C z^u#MAyoEw^iZ|gWGZi^Q;R#-Z#qS2@VMPwm=VR(BhMvoO z)-pJ;|V!TziKLN=CbzAXzISF5^&INT$qY z|ppTrL~NpTWfj zUyKNO=GbJ7vPMeN(kkPT_H&YorKurTA;jE=Kz7Sx7WU27lhLN(u5~vXF#=LmZS9;e ziMm}alGhJUH13%~$4;T3>Q#LEvVI|GAd*6wOC>XtG?#T1DwfZ)jhvi6f@WvuH(C7q z>I;R!0?F$OA)xgj=;4U-Y8Jy)iUr-!a`Cc$|DZJo+Nzp!OA6BLT@pvE@HQsbJ-R$}Jr&;Fh{0H*hW1Ku<_~)mE)78ef8KW7m6vUt9!i@ZD? zdA6rZfiu#oJoAKB)2xs3Av}R@P&&=C!px>}+^7E5rR!S*FSrX8XEMb-)8URupjC+$p{dPwc>HEy~#`^S3-ANo60(P z{@&bM{_q2kW`A=MBKI?0!7QzuZz<$prqe_B2Cv~b0*bzH77i02RdX(owC+tmy%gCl z-hKK>UoKprofKg)Q4r;tCE?Hb{M=}nzehd{^lnIzu6tj1F4wlo+D{4LTXyX>R+rw$ zg!xN`x-O6@I&eB}4r4MThXNbhvTLb4K0c{%YhK7$i8g0k>_W)=K|RL$!xsmH>Mv+z z+r9zZ$WfbQub(_9+SlL!TUL+3zDG0TN+ zsRi4o)kd~RWWMAnoxs;0V|?kn{L{DtTAkUVdO;v?ru?y5dnlvsctUsK|5TpgGll^ zi1%(`%hn3$0vSt4Yb*ibh{oGNAC}+9pJ6inW|Ar|$p20`@{%WMT)@RMsktk0URO+sBI zYZ@>)5xkA4nCwyT02nf^3?^U7#Cc84pn#+ZAYA^BIB`?5bC<8b+zBYDzayI|8CYNLVG!lcDV{3ReX~% z;O=PMs@zRUa_OL8x=^&-^|lnH`(4zU0iVWQgPfb#!Tvh`y8qdRQ&xD9zm7j$Z*w9Z zKAikd;uR?D;dBFkC6($No@J3}jp7bDoRv*=^v&!18MAjvG5VCYi#cg_eqz?))t8?^ zcywK((NQ2Hhu96iIaF(lCQ~uhpO|GS{q-}s+e--+@Jno2obg2GY+VHV(^E&}d?AZg z+Kp)aHiq6urv_26R@1jnOZe8wdv7^|qx-2cr#qgVUJGMcZvB_TokK|hR-4=kjg?{*Efbo)O7OO zUP2j|f1&VjdQ$4KxRY`s%lU0UB%?^_slY`rMZemDtVE@J9U*zaFX!T$KCT1pUP%RJWx_?T=vyj zxasQ$=SEQA*kI(vNztwB`LwA|0DOXv;DW=Jx8R92sC`R(D9$?i`hMd4O?Dh+6G{)ZJB5x;r-M{nRMj$*LkW$kwI5&6q!gR)d5iu3HP zX08E?(cp#&8!sx(4yX9f7x5j}!knTe@#)BqO6H%8*+3umP4#L!KPdyLEma>-Gng=U zu_%&iR-!xvDMj9(pKsNRNpUdrvVL}bS8D5azSrTh)^{T(7+oJ+(^+gE`%>nXAENu10K$-kIn@(FxSI>eA&TNAA7o%W+?l6wdY)1`m+Z4>{vM%A5h zaZ~s*-NajB?%&Uv;=I)5*}2)46(wp_+RC;jNr=o%4h~oxcSKaLg#j41riL7$%UB5QWLhxSFjas1GLEbN@j`wGhXcJ*J2z7npR~ zc+{A{HXiGp#ylV>!my%uluWk9>-Hgwg!OVx zo<$&T=TjL{is);SFmh9gtiO}ETNOJod7S?6IUfEe2qOTeyIOGvfoK(VL9HtgedIyO zUvZ~Z;wYNSaxOlRa5wd^h+U*e{R0XbGRhA3c$Jw9k>KA*q)ChNQpg@^T9-4%sM-g$ z_wCcaS+S&tFg*gUI)52nnPAgL94%ngzaZL7>JN;^1N^G74Mn)Cju}caUBv42vr<)d zy5S3=2eiu>0%sm1)zzxkYBK4g?$Q+=^Ccozvtr0-CyXr7w6X9!+^dlfUr_M7y=O=2 z5tMn*Zq*3KVGP-otPnicVJ?f{MH`mRsae$Sp#?=RSR<`UKI(SXJo7`Px-LJ&%TI#onpllaKke#&B&@-*X_2Ll*KIiX9k=~Hxnac^M-(v|DoBs& z#sF6|8}RCP!JD3xEQ;9aU(4XF+OBMy)zL)3qP@syO1v6 z&UK?4sW!QTj-_^Rc z$y~D)NA#R`mccm?VATImGINBK#PUSBr%%iUf~h)<9N`QF)PBh8#nPJOQ`Z+$(Lr4W zGcZ{h^-X>ClDj!$VQFLT0?0ZWM!-)zRdI0p>act(3upZ+k>z?bH?Osa$47+&Qg6hi ztHYylk=WtYf=vcs@)1SG5g73LLqJ>0tZ$MR1HE^K>YQjHSfvz4^XJ^+z~^y?>g?dNSpCu{8ftLM zkW{C$e;mn=YtCsShS}i@9QLFFyt%lL4(?Nm!8HDCxCMiq5)){c&&+AgM7}KCa3Dp) zh_+(dng<@wxsYPzxRH~nq zkIThzbd1V=I4lX(BprhjBr`2AxQia9+x^ZzRm%Z~NHFt$2<~5oq?}d9w&?3qDb1{` z@B3s8O2$#kN9*4bq{hDSXZ6;*Q%C)5sjub@lSpZ-qx74Iyr~QmMm&!Yf063gu6g5TAwk{_(syhX}brv?2zV;*1pwX zgf(g$XJkDFrZUiQ3n@oAn#)0axNRicq46zcj7}3L%so0php|ZVBvNFZh8_VWv#uZZDXCwdb#E>Y9AO0Wd@_sla=21_{C$D8wXn#8&ZBAHFvmK0x|u%)?0RiRvRk_ za{v&_RBUhPkODO^1J`dXYq;{_xM?Z?)=MDzb`eZxD`kqe+z|O279aGePiyq$`s6*i9SXTvBAqh*w26>wq00Xs!M?<)mitkY+(hABn@pi}cBKi29e zIXi<<(`ww943d>969`^&Nq8^<`wYuL);TDN3n!+g4@*Mn?clcmcycrlC7S6B?+MT(Pf;y;#73@AR$l`^^dL{Nx31QE>onvzlowE1Dp zl4`|JOey&MNRgDKXcxY5!~g8`;qxoU{@6@DYlE8Mez&HX_4o7#4%Ql7nXL6#hHYSF z4l?<8TisXMcZ*BZq(8WhXm&kC=C$+fZ2Iuv)HOUsdlU=wk`WYACJkiZ=hG-&Z zPtU0#qp`b{D($OQ3#++Hw3_T+ij8%atHI=sJFzhrn`Cbi2Rr?!>#ot5j?`8a5%H9 zAYJEBSqU_`6sdCaBjIsLJbrR=7cHl({FGJQ+C(2+*B3`Lj6IfrdyCM1Vjo6?zg*G# z81YCojCFeEWc*2-U%ES`3wy}vjq&Rn>qNSQ9=dG`hY4zq5b5K9x~gwI`b{rqQTHZ4 zo-AED506#f?Kg&wm09(Yx(n~-%2_0w5wqiDD$|v7u~z-~>u!2GN literal 0 HcmV?d00001 From 2840bbec11cdb29fb551858aaf3c5f3866e9462d Mon Sep 17 00:00:00 2001 From: Sergio Ramirez Date: Fri, 27 Jun 2025 13:14:50 -0400 Subject: [PATCH 04/22] Crear-editar productos, depuracion de archivos users --- .../inventory/dto/create-product.dto.ts | 9 +- .../inventory/entities/inventory.entity.ts | 11 + .../inventory/inventory.controller.ts | 23 +- .../features/inventory/inventory.service.ts | 102 ++----- .../feactures/inventory/actions/actions.ts | 67 +---- ...-user-form.tsx => create-product-form.tsx} | 126 +++----- .../{user-modal.tsx => inventory-modal.tsx} | 23 +- .../inventory/product-inventory-list.tsx | 5 +- .../inventory/product-tables/cell-action.tsx | 2 +- .../inventory/update-product-form.tsx | 168 +++++++++++ .../components/inventory/update-user-form.tsx | 227 --------------- .../components/inventory/users-header.tsx | 6 +- .../inventory/components/modal-profile.tsx | 57 ---- .../inventory/components/selectList.tsx | 85 ------ .../feactures/inventory/components/survey.tsx | 28 -- .../inventory/components/update-user-form.tsx | 268 ------------------ .../inventory/components/user-profile.tsx | 75 ----- .../inventory/hooks/use-mutation-users.ts | 45 --- .../feactures/inventory/hooks/use-mutation.ts | 35 +++ .../inventory/hooks/use-query-surveys.ts | 12 - .../feactures/inventory/schemas/inventory.ts | 41 +-- .../feactures/users/components/selectList.tsx | 83 ------ .../web/feactures/users/components/survey.tsx | 28 -- 23 files changed, 333 insertions(+), 1193 deletions(-) rename apps/web/feactures/inventory/components/inventory/{create-user-form.tsx => create-product-form.tsx} (51%) rename apps/web/feactures/inventory/components/inventory/{user-modal.tsx => inventory-modal.tsx} (71%) create mode 100644 apps/web/feactures/inventory/components/inventory/update-product-form.tsx delete mode 100644 apps/web/feactures/inventory/components/inventory/update-user-form.tsx delete mode 100644 apps/web/feactures/inventory/components/modal-profile.tsx delete mode 100644 apps/web/feactures/inventory/components/selectList.tsx delete mode 100644 apps/web/feactures/inventory/components/survey.tsx delete mode 100644 apps/web/feactures/inventory/components/update-user-form.tsx delete mode 100644 apps/web/feactures/inventory/components/user-profile.tsx delete mode 100644 apps/web/feactures/inventory/hooks/use-mutation-users.ts create mode 100644 apps/web/feactures/inventory/hooks/use-mutation.ts delete mode 100644 apps/web/feactures/inventory/hooks/use-query-surveys.ts delete mode 100644 apps/web/feactures/users/components/selectList.tsx delete mode 100644 apps/web/feactures/users/components/survey.tsx diff --git a/apps/api/src/features/inventory/dto/create-product.dto.ts b/apps/api/src/features/inventory/dto/create-product.dto.ts index 7a4728b..6cc64eb 100644 --- a/apps/api/src/features/inventory/dto/create-product.dto.ts +++ b/apps/api/src/features/inventory/dto/create-product.dto.ts @@ -18,12 +18,19 @@ export class CreateProductDto { price: string; @ApiProperty() - @IsString({ + @IsInt({ message: 'stock must be a number', }) @IsOptional() stock: number; + @ApiProperty() + @IsInt({ + message: 'stock must be a number', + }) + @IsOptional() + userId: number; + @ApiProperty() @IsString({ message: 'urlImg must be a string', diff --git a/apps/api/src/features/inventory/entities/inventory.entity.ts b/apps/api/src/features/inventory/entities/inventory.entity.ts index 3556333..3f511e8 100644 --- a/apps/api/src/features/inventory/entities/inventory.entity.ts +++ b/apps/api/src/features/inventory/entities/inventory.entity.ts @@ -5,4 +5,15 @@ export class Product { price: string; stock: number; urlImg: string; + UserId?: number; +} + +export class CreateProduct { + id: number; + title: string; + description: string; + price: string; + stock: string; + urlImg: string; + UserId: number; } \ 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 index 4d618b6..6078262 100644 --- a/apps/api/src/features/inventory/inventory.controller.ts +++ b/apps/api/src/features/inventory/inventory.controller.ts @@ -3,7 +3,7 @@ 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 { Roles } from '../../common/decorators/roles.decorator'; import { PaginationDto } from '../../common/dto/pagination.dto'; @ApiTags('inventory') @@ -42,22 +42,19 @@ export class UsersController { @Body() createUserDto: CreateProductDto, @Query('roleId') roleId?: string, ) { - const data = await this.inventoryService.create( - createUserDto, - roleId ? parseInt(roleId) : undefined, - ); + const data = await this.inventoryService.create(createUserDto) return { message: 'User created successfully', data }; } - // @Patch(':id') + @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 }; - // } + @ApiOperation({ summary: 'Update a product' }) + @ApiResponse({ status: 200, description: 'Product updated successfully.' }) + @ApiResponse({ status: 404, description: 'Product 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') diff --git a/apps/api/src/features/inventory/inventory.service.ts b/apps/api/src/features/inventory/inventory.service.ts index 115fcdc..732d41b 100644 --- a/apps/api/src/features/inventory/inventory.service.ts +++ b/apps/api/src/features/inventory/inventory.service.ts @@ -6,7 +6,7 @@ import { products } from 'src/database/index'; import { eq, like, or, SQL, sql, and, not } from 'drizzle-orm'; import { CreateProductDto } from './dto/create-product.dto'; import { UpdateProductDto } from './dto/update-product.dto'; -import { Product } from './entities/inventory.entity'; +import { Product, CreateProduct } from './entities/inventory.entity'; import { PaginationDto } from '../../common/dto/pagination.dto'; @Injectable() @@ -54,13 +54,8 @@ export class InventoryService { price: products.price, urlImg: products.urlImg, stock: products.stock, - // 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) @@ -77,9 +72,6 @@ export class InventoryService { nextPage: page < totalPages ? page + 1 : null, previousPage: page > 1 ? page - 1 : null, }; - - console.log(data); - return { data, meta }; } @@ -94,16 +86,10 @@ export class InventoryService { stock: products.stock }) .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); + throw new HttpException('Product does not exist', HttpStatus.BAD_REQUEST); } return find[0]; @@ -111,14 +97,13 @@ export class InventoryService { // Rest of the service remains the same async create( - createProductDto: CreateProductDto, - roleId: number = 2, - ): Promise { + createProductDto: CreateProductDto + ): Promise { // Start a transaction return await this.drizzle.transaction(async (tx) => { - // Create the user + const [newProduct] = await tx .insert(products) .values({ @@ -126,72 +111,33 @@ export class InventoryService { description: createProductDto.description, price: createProductDto.price, urlImg: createProductDto.urlImg, - stock: createProductDto.stock + stock: createProductDto.stock, + userId: createProductDto.userId }) .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)); + return newProduct }); } - // async update(id: string, updateUserDto: UpdateUserDto): Promise { - // const userId = parseInt(id); + async update(id: string, updateProductDto: UpdateProductDto): Promise { + const productId = parseInt(id); - // // Check if user exists - // await this.findOne(id); + // Check if 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; + // Prepare update data + const updateData: any = {}; + if (updateProductDto.title) updateData.title = updateProductDto.title; + if (updateProductDto.description) updateData.description = updateProductDto.description; + if (updateProductDto.price) updateData.price = updateProductDto.price; + if (updateProductDto.stock) updateData.stock = updateProductDto.stock; + if (updateProductDto.urlImg) updateData.urlImg = updateProductDto.urlImg; - // 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); - // } + const [updatedProduct] = await this.drizzle.update(products).set(updateData).where(eq(products.id, productId)).returning(); + return updatedProduct + // Return updated user + // return this.findOne(id); + } // async remove(id: string): Promise<{ message: string, data: User }> { diff --git a/apps/web/feactures/inventory/actions/actions.ts b/apps/web/feactures/inventory/actions/actions.ts index ec0e850..e5220dc 100644 --- a/apps/web/feactures/inventory/actions/actions.ts +++ b/apps/web/feactures/inventory/actions/actions.ts @@ -1,49 +1,14 @@ 'use server'; import { safeFetchApi } from '@/lib/fetch.api'; import { - surveysApiResponseSchema, - CreateUser, + ApiResponseSchema, + InventoryTable, productMutate, - UpdateUser + editInventory } from '../schemas/inventory'; import { auth } from '@/lib/auth'; - -export const getProfileAction = async () => { - const session = await auth() - const id = session?.user?.id - - const [error, response] = await safeFetchApi( - productMutate, - `/users/${id}`, - 'GET' - ); - if (error) throw new Error(error.message); - return response; -}; - -export const updateProfileAction = async (payload: UpdateUser) => { - const { id, ...payloadWithoutId } = payload; - - const [error, data] = await safeFetchApi( - productMutate, - `/users/profile/${id}`, - 'PATCH', - payloadWithoutId, - ); - - console.log(payload); - if (error) { - if (error.message === 'Email already exists') { - throw new Error('Ese correo ya está en uso'); - } - // console.error('Error:', error); - throw new Error('Error al crear el usuario'); - } - return data; -}; - export const getInventoryAction = async (params: { page?: number; limit?: number; @@ -61,7 +26,7 @@ export const getInventoryAction = async (params: { }); const [error, response] = await safeFetchApi( - surveysApiResponseSchema, + ApiResponseSchema, `/inventory?${searchParams}`, 'GET', ); @@ -71,8 +36,6 @@ export const getInventoryAction = async (params: { throw new Error(error.message); } - - // const transformedData = response?.data ? transformSurvey(response?.data) : undefined; return { @@ -90,37 +53,35 @@ export const getInventoryAction = async (params: { }; } -export const createUserAction = async (payload: CreateUser) => { - const { id, confirmPassword, ...payloadWithoutId } = payload; +export const createProductAction = async (payload: InventoryTable) => { + const session = await auth() + const userId = session?.user?.id + const { id, ...payloadWithoutId } = payload; + + payloadWithoutId.userId = userId const [error, data] = await safeFetchApi( productMutate, - '/users', + '/inventory', 'POST', payloadWithoutId, ); if (error) { - if (error.message === 'Username already exists') { - throw new Error('Ese usuario ya existe'); - } - if (error.message === 'Email already exists') { - throw new Error('Ese correo ya está en uso'); - } - // console.error('Error:', error); + console.error(error); throw new Error('Error al crear el usuario'); } return payloadWithoutId; }; -export const updateUserAction = async (payload: UpdateUser) => { +export const updateUserAction = async (payload: InventoryTable) => { try { const { id, ...payloadWithoutId } = payload; const [error, data] = await safeFetchApi( productMutate, - `/users/${id}`, + `/inventory/${id}`, 'PATCH', payloadWithoutId, ); diff --git a/apps/web/feactures/inventory/components/inventory/create-user-form.tsx b/apps/web/feactures/inventory/components/inventory/create-product-form.tsx similarity index 51% rename from apps/web/feactures/inventory/components/inventory/create-user-form.tsx rename to apps/web/feactures/inventory/components/inventory/create-product-form.tsx index 86cbc86..297721e 100644 --- a/apps/web/feactures/inventory/components/inventory/create-user-form.tsx +++ b/apps/web/feactures/inventory/components/inventory/create-product-form.tsx @@ -1,5 +1,4 @@ 'use client'; - import { zodResolver } from '@hookform/resolvers/zod'; import { Button } from '@repo/shadcn/button'; import { @@ -11,38 +10,30 @@ import { FormMessage, } from '@repo/shadcn/form'; import { Input } from '@repo/shadcn/input'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@repo/shadcn/select'; +// import { +// Select, +// SelectContent, +// SelectItem, +// SelectTrigger, +// SelectValue, +// } from '@repo/shadcn/select'; import { useForm } from 'react-hook-form'; -import { useCreateUser } from "../../hooks/use-mutation-users"; -import { CreateUser, createUser } from '../../schemas/inventory'; +import { useCreateUser } from "@/feactures/inventory/hooks/use-mutation"; +import { EditInventory, editInventory } from '@/feactures/inventory/schemas/inventory'; +import { parse } from 'path'; -const ROLES = { - // 1: 'Superadmin', - 2: 'Administrador', - 3: 'autoridad', - 4: 'Gerente', - 5: 'Usuario', - 6: 'Productor', - 7: 'Organización' -} -interface CreateUserFormProps { +interface CreateFormProps { onSuccess?: () => void; onCancel?: () => void; - defaultValues?: Partial; + defaultValues?: Partial; } -export function CreateUserForm({ +export function CreateForm({ onSuccess, onCancel, defaultValues, -}: CreateUserFormProps) { +}: CreateFormProps) { const { mutate: saveAccountingAccounts, isPending: isSaving, @@ -52,23 +43,20 @@ export function CreateUserForm({ // const { data: AccoutingAccounts } = useSurveyMutation(); const defaultformValues = { - username: defaultValues?.username || '', - fullname: defaultValues?.fullname || '', - email: defaultValues?.email || '', - password: '', - confirmPassword: '', - id: defaultValues?.id, - phone: defaultValues?.phone || '', - role: defaultValues?.role, + title: defaultValues?.title || '', + description: defaultValues?.description || '', + price: defaultValues?.price || '', + stock: defaultValues?.stock || 0, + urlImg: defaultValues?.urlImg || '' } - const form = useForm({ - resolver: zodResolver(createUser), + const form = useForm({ + resolver: zodResolver(editInventory), defaultValues: defaultformValues, mode: 'onChange', // Enable real-time validation }); - const onSubmit = async (data: CreateUser) => { + const onSubmit = async (data: EditInventory) => { const formData = data @@ -97,10 +85,10 @@ export function CreateUserForm({
( - Usuario + Nombre/Título @@ -111,10 +99,10 @@ export function CreateUserForm({ ( - Nombre completo + Descripción @@ -125,12 +113,14 @@ export function CreateUserForm({ ( - Correo + Precio - + @@ -139,12 +129,12 @@ export function CreateUserForm({ ( - Teléfono + Cantidad/Stock - + @@ -153,59 +143,17 @@ export function CreateUserForm({ ( - Contraseña + Imagen - + )} /> - - ( - - Confirmar Contraseña - - - - - - )} - /> - - ( - - Rol - - - - )} - />
diff --git a/apps/web/feactures/inventory/components/inventory/user-modal.tsx b/apps/web/feactures/inventory/components/inventory/inventory-modal.tsx similarity index 71% rename from apps/web/feactures/inventory/components/inventory/user-modal.tsx rename to apps/web/feactures/inventory/components/inventory/inventory-modal.tsx index b4230f8..28a4348 100644 --- a/apps/web/feactures/inventory/components/inventory/user-modal.tsx +++ b/apps/web/feactures/inventory/components/inventory/inventory-modal.tsx @@ -7,21 +7,22 @@ import { DialogHeader, DialogTitle, } from '@repo/shadcn/dialog'; -import { AccountPlan } from '@/feactures/users/schemas/account-plan.schema'; -import { CreateUserForm } from './create-user-form'; -import { UpdateUserForm } from './update-user-form'; +// import { AccountPlan } from '@/feactures/users/schemas/account-plan.schema'; +import { EditInventory, editInventory } from '../../schemas/inventory'; +import { CreateForm } from './create-product-form'; +import { UpdateForm } from './update-product-form'; -interface AccountPlanModalProps { +interface ModalProps { open: boolean; onOpenChange: (open: boolean) => void; - defaultValues?: Partial; + defaultValues?: Partial; } export function AccountPlanModal({ open, onOpenChange, defaultValues, -}: AccountPlanModalProps) { +}: ModalProps) { const handleSuccess = () => { onOpenChange(false); }; @@ -43,21 +44,21 @@ export function AccountPlanModal({ {defaultValues?.id - ? 'Actualizar usuario' - : 'Crear usuario'} + ? 'Actualizar producto' + : 'Registrar producto'} - Complete los campos para {defaultValues?.id ? 'actualizar' : 'crear'} un usuario + Complete los campos para {defaultValues?.id ? 'actualizar' : 'registrar'} un producto {defaultValues?.id ? ( - ): ( - ; diff --git a/apps/web/feactures/inventory/components/inventory/product-tables/cell-action.tsx b/apps/web/feactures/inventory/components/inventory/product-tables/cell-action.tsx index 53fac03..0e682ec 100644 --- a/apps/web/feactures/inventory/components/inventory/product-tables/cell-action.tsx +++ b/apps/web/feactures/inventory/components/inventory/product-tables/cell-action.tsx @@ -12,7 +12,7 @@ import { import { Edit, Trash, User } from 'lucide-react'; import { InventoryTable } from '@/feactures/inventory/schemas/inventory'; import { useDeleteUser } from '@/feactures/users/hooks/use-mutation-users'; -import { AccountPlanModal } from '../user-modal'; +import { AccountPlanModal } from '../inventory-modal'; interface CellActionProps { data: InventoryTable; diff --git a/apps/web/feactures/inventory/components/inventory/update-product-form.tsx b/apps/web/feactures/inventory/components/inventory/update-product-form.tsx new file mode 100644 index 0000000..c4ee45d --- /dev/null +++ b/apps/web/feactures/inventory/components/inventory/update-product-form.tsx @@ -0,0 +1,168 @@ +'use client'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Button } from '@repo/shadcn/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@repo/shadcn/form'; +import { Input } from '@repo/shadcn/input'; +// import { +// Select, +// SelectContent, +// SelectItem, +// SelectTrigger, +// SelectValue, +// } from '@repo/shadcn/select'; +import { useForm } from 'react-hook-form'; +import { useUpdateUser } from "@/feactures/inventory/hooks/use-mutation"; +import { EditInventory, editInventory } from '@/feactures/inventory/schemas/inventory'; + + +interface UpdateFormProps { + onSuccess?: () => void; + onCancel?: () => void; + defaultValues?: Partial; +} + +export function UpdateForm({ + onSuccess, + onCancel, + defaultValues, +}: UpdateFormProps) { + const { + mutate: saveAccountingAccounts, + isPending: isSaving, + isError, + } = useUpdateUser(); + + const defaultformValues = { + id: defaultValues?.id, + title: defaultValues?.title || '', + description: defaultValues?.description || '', + price: defaultValues?.price || '', + stock: defaultValues?.stock || 0, + urlImg: defaultValues?.urlImg || '', + } + + // console.log(defaultValues); + + const form = useForm({ + resolver: zodResolver(editInventory), + defaultValues: defaultformValues, + mode: 'onChange', // Enable real-time validation + }); + + const onSubmit = async (data: EditInventory) => { + + const formData = data + + saveAccountingAccounts(formData, { + onSuccess: () => { + form.reset(); + onSuccess?.(); + }, + onError: () => { + form.setError('root', { + type: 'manual', + message: 'Error al guardar la cuenta contable', + }); + }, + }); + }; + + return ( +
+ + {form.formState.errors.root && ( +
+ {form.formState.errors.root.message} +
+ )} +
+ ( + + Nombre/Título + + + + + + )} + /> + + ( + + Descripción + + + + + + )} + /> + + ( + + Precio + + + + + + )} + /> + + ( + + Cantidad/Stock + + + + + + )} + /> + + ( + + Imagen + + + + + + )} + /> +
+ +
+ + +
+
+ + ); +} diff --git a/apps/web/feactures/inventory/components/inventory/update-user-form.tsx b/apps/web/feactures/inventory/components/inventory/update-user-form.tsx deleted file mode 100644 index b96d14f..0000000 --- a/apps/web/feactures/inventory/components/inventory/update-user-form.tsx +++ /dev/null @@ -1,227 +0,0 @@ -'use client'; - -import { zodResolver } from '@hookform/resolvers/zod'; -import { Button } from '@repo/shadcn/button'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@repo/shadcn/form'; -import { Input } from '@repo/shadcn/input'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@repo/shadcn/select'; -import { useForm } from 'react-hook-form'; -import { useUpdateUser } from "@/feactures/users/hooks/use-mutation-users"; -import { UpdateUser, updateUser } from '@/feactures/users/schemas/users'; - -const ROLES = { - // 1: 'Superadmin', - 2: 'Administrador', - 3: 'autoridad', - 4: 'Gerente', - 5: 'Usuario', - 6: 'Productor', - 7: 'Organización' -} - -interface UserFormProps { - onSuccess?: () => void; - onCancel?: () => void; - defaultValues?: Partial; -} - -export function UpdateUserForm({ - onSuccess, - onCancel, - defaultValues, -}: UserFormProps) { - const { - mutate: saveAccountingAccounts, - isPending: isSaving, - isError, - } = useUpdateUser(); - - const defaultformValues = { - username: defaultValues?.username || '', - fullname: defaultValues?.fullname || '', - email: defaultValues?.email || '', - password: '', - id: defaultValues?.id, - phone: defaultValues?.phone || '', - role: undefined, - isActive: defaultValues?.isActive - } - - // console.log(defaultValues); - - const form = useForm({ - resolver: zodResolver(updateUser), - defaultValues: defaultformValues, - mode: 'onChange', // Enable real-time validation - }); - - const onSubmit = async (data: UpdateUser) => { - - const formData = data - - saveAccountingAccounts(formData, { - onSuccess: () => { - form.reset(); - onSuccess?.(); - }, - onError: () => { - form.setError('root', { - type: 'manual', - message: 'Error al guardar la cuenta contable', - }); - }, - }); - }; - - return ( -
- - {form.formState.errors.root && ( -
- {form.formState.errors.root.message} -
- )} -
- ( - - Usuario - - - - - - )} - /> - - ( - - Nombre completo - - - - - - )} - /> - - ( - - Correo - - - - - - )} - /> - - ( - - Teléfono - - - - - - )} - /> - - ( - - Nueva Contraseña - - - - - - )} - /> - - ( - - Rol - - - - )} - /> - - ( - - Estatus - - - - )} - /> -
- -
- - -
-
- - ); -} diff --git a/apps/web/feactures/inventory/components/inventory/users-header.tsx b/apps/web/feactures/inventory/components/inventory/users-header.tsx index 891c9dc..ee32e27 100644 --- a/apps/web/feactures/inventory/components/inventory/users-header.tsx +++ b/apps/web/feactures/inventory/components/inventory/users-header.tsx @@ -1,10 +1,10 @@ 'use client'; -import { useRouter } from 'next/navigation'; +// import { useRouter } from 'next/navigation'; import { Button } from '@repo/shadcn/button'; import { Heading } from '@repo/shadcn/heading'; import { Plus } from 'lucide-react'; import { useState } from 'react'; -import { AccountPlanModal } from './user-modal'; +import { AccountPlanModal } from './inventory-modal'; export function UsersHeader() { const [open, setOpen] = useState(false); @@ -17,7 +17,7 @@ export function UsersHeader() { description="Gestione aquí los productos que usted registre en la plataforma" />
diff --git a/apps/web/feactures/inventory/components/modal-profile.tsx b/apps/web/feactures/inventory/components/modal-profile.tsx deleted file mode 100644 index 4f1853a..0000000 --- a/apps/web/feactures/inventory/components/modal-profile.tsx +++ /dev/null @@ -1,57 +0,0 @@ -'use client'; - -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from '@repo/shadcn/dialog'; -import { AccountPlan } from '@/feactures/users/schemas/account-plan.schema'; -import { ModalForm } from './update-user-form'; - -interface AccountPlanModalProps { - open: boolean; - onOpenChange: (open: boolean) => void; - defaultValues?: Partial; -} - -export function AccountPlanModal({ - open, - onOpenChange, - defaultValues, -}: AccountPlanModalProps) { - const handleSuccess = () => { - onOpenChange(false); - }; - - const handleCancel = () => { - onOpenChange(false); - }; - - return ( - { - if (!open) { - onOpenChange(false); - } - }} - > - - - Actualizar Perfil - - Complete los campos para actualizar sus datos.
Los campos vacios no seran actualizados. -
-
- - -
-
- ); -} diff --git a/apps/web/feactures/inventory/components/selectList.tsx b/apps/web/feactures/inventory/components/selectList.tsx deleted file mode 100644 index 4150cd5..0000000 --- a/apps/web/feactures/inventory/components/selectList.tsx +++ /dev/null @@ -1,85 +0,0 @@ -// 'use client'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@repo/shadcn/select'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@repo/shadcn/form'; -import { UpdateUser, updateUser } from '@/feactures/users/schemas/users'; -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; - -interface SelectListProps { - label: string - // values: - values: Array - form: any - name: string - handleChange: any -} - -export function SelectList({ label, values, form, name, handleChange }: SelectListProps) { - // const { label, values, form, name } = props; - // handleChange - - // const defaultformValues = { - // username: '', - // fullname: '', - // email: '', - // password: '', - // id: 0, - // phone: '', - // role: undefined, - // isActive: false - // } - - // const form = useForm({ - // resolver: zodResolver(updateUser), - // defaultValues: defaultformValues, - // mode: 'onChange', // Enable real-time validation - // }); - - - return ( - - {label} - - - - )} - /> -} \ No newline at end of file diff --git a/apps/web/feactures/inventory/components/survey.tsx b/apps/web/feactures/inventory/components/survey.tsx deleted file mode 100644 index b4b5eb9..0000000 --- a/apps/web/feactures/inventory/components/survey.tsx +++ /dev/null @@ -1,28 +0,0 @@ -'use client' - -import { SurveyResponse } from '@/feactures/surveys/components/survey-response'; -import { useSurveysByIdQuery } from '@/feactures/surveys/hooks/use-query-surveys'; - -import { notFound, useParams } from 'next/navigation'; - - -export default function SurveyPage() { - const params = useParams(); - const surveyId = params?.id as string | undefined; - - - if (!surveyId || surveyId === '') { - notFound(); - } - - const { data: survey, isLoading } = useSurveysByIdQuery(Number(surveyId)); - console.log('🎯 useSurveysByIdQuery ejecutado, data:', survey, 'isLoading:', isLoading); - - if (!survey?.data || !survey?.data.published) { - notFound(); - } - - return ( - - ); -} \ No newline at end of file diff --git a/apps/web/feactures/inventory/components/update-user-form.tsx b/apps/web/feactures/inventory/components/update-user-form.tsx deleted file mode 100644 index 6398864..0000000 --- a/apps/web/feactures/inventory/components/update-user-form.tsx +++ /dev/null @@ -1,268 +0,0 @@ -'use client'; - -import { zodResolver } from '@hookform/resolvers/zod'; -import { Button } from '@repo/shadcn/button'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@repo/shadcn/form'; -import { Input } from '@repo/shadcn/input'; -// import { -// Select, -// SelectContent, -// SelectItem, -// SelectTrigger, -// SelectValue, -// } from '@repo/shadcn/select'; -import { SelectSearchable } from '@repo/shadcn/select-searchable' -import { useForm } from 'react-hook-form'; -import { useUpdateProfile } from "@/feactures/users/hooks/use-mutation-users"; -import { UpdateUser, updateUser } from '@/feactures/users/schemas/users'; -import { toast } from 'sonner'; - -import React from 'react'; -import { useStateQuery, useMunicipalityQuery, useParishQuery } from '@/feactures/location/hooks/use-query-location'; - -interface UserFormProps { - onSuccess?: () => void; - onCancel?: () => void; - defaultValues?: Partial; -} - -export function ModalForm({ - onSuccess, - onCancel, - defaultValues, -}: UserFormProps) { - const { - mutate: saveAccountingAccounts, - isPending: isSaving, - isError, - } = useUpdateProfile(); - - const [state, setState] = React.useState(0); - const [municipality, setMunicipality] = React.useState(0); - const [parish, setParish] = React.useState(0); - - const [disabledMunicipality, setDisabledMunicipality] = React.useState(true); - const [disabledParish, setDisabledParish] = React.useState(true); - - const { data : dataState } = useStateQuery() - const { data : dataMunicipality } = useMunicipalityQuery(state) - const { data : dataParish } = useParishQuery(municipality) - - const stateOptions = dataState?.data || [{id:0,name:'Sin estados'}] - - const municipalityOptions = Array.isArray(dataMunicipality?.data) && dataMunicipality.data.length > 0 - ? dataMunicipality.data - : [{id:0,stateId:0,name:'Sin Municipios'}] - // const parishOptions = dataParish?.data || [{id:0,municipalityId:0,name:'Sin Parroquias'}] - const parishOptions = Array.isArray(dataParish?.data) && dataParish.data.length > 0 - ? dataParish.data - : [{id:0,stateId:0,name:'Sin Parroquias'}] - - - const defaultformValues = { - username: defaultValues?.username || '', - fullname: defaultValues?.fullname || '', - email: defaultValues?.email || '', - password: '', - id: defaultValues?.id, - phone: defaultValues?.phone || '', - role: undefined, - isActive: defaultValues?.isActive, - state: defaultValues?.state, - municipality: defaultValues?.municipality, - parish: defaultValues?.parish - } - - - - // console.log(defaultValues); - - const form = useForm({ - resolver: zodResolver(updateUser), - defaultValues: defaultformValues, - mode: 'onChange', // Enable real-time validation - }); - - const onSubmit = async (data: UpdateUser) => { - - const formData = data - - saveAccountingAccounts(formData, { - onSuccess: () => { - form.reset(); - onSuccess?.(); - toast.success('Actualizado exitosamente!'); - }, - onError: (e) => { - form.setError('root', { - type: 'manual', - message: e.message, - }); - // toast.error(e.message); - }, - }); - }; - - return ( -
- - {form.formState.errors.root && ( -
- {form.formState.errors.root.message} -
- )} -
- ( - - Nombre completo - - - - - - )} - /> - - ( - - Correo - - - - - - )} - /> - - ( - - Teléfono - - - - - - )} - /> - - ( - - Estado - - ({ - value: item.id.toString(), - label: item.name, - })) || [] - } - onValueChange={(value : any) => - {field.onChange(Number(value)); setState(value); setDisabledMunicipality(false); setDisabledParish(true)} - } - placeholder="Selecciona un estado" - defaultValue={field.value?.toString()} - // disabled={readOnly} - /> - - - )} - /> - - ( - - Municipio - - ({ - value: item.id.toString(), - label: item.name, - })) || [] - } - onValueChange={(value : any) => - {field.onChange(Number(value)); setMunicipality(value); setDisabledParish(false)} - } - placeholder="Selecciona un Municipio" - defaultValue={field.value?.toString()} - disabled={disabledMunicipality} - /> - - - )} - /> - - ( - - Parroquia - - ({ - value: item.id.toString(), - label: item.name, - })) || [] - } - onValueChange={(value : any) => - field.onChange(Number(value)) - } - placeholder="Selecciona una Parroquia" - defaultValue={field.value?.toString()} - disabled={disabledParish} - /> - - - )} - /> - - ( - - Nueva Contraseña - - - - - - )} - /> -
- -
- - -
-
- - ); -} diff --git a/apps/web/feactures/inventory/components/user-profile.tsx b/apps/web/feactures/inventory/components/user-profile.tsx deleted file mode 100644 index 72c621c..0000000 --- a/apps/web/feactures/inventory/components/user-profile.tsx +++ /dev/null @@ -1,75 +0,0 @@ -'use client'; -import { useUserByProfile } from '@/feactures/users/hooks/use-query-users'; -import { Button } from '@repo/shadcn/button'; -import { Edit, Edit2 } from 'lucide-react'; -import { useState } from 'react'; -import { AccountPlanModal } from './modal-profile'; - -export function Profile() { - const [open, setOpen] = useState(false); - - const { data } = useUserByProfile(); - - // console.log("🎯 data:", data); - - return ( -
- - - - -

Datos del usuario

-
-
-

Usuario:

-

{data?.data.username || 'Sin Nombre de Usuario'}

-
- -
-

Rol:

-

{data?.data.role || 'Sin Rol'}

-
-
- -

Información personal

-
-
-

Nombre completo:

-

{data?.data.fullname || 'Sin nombre y apellido'}

-
- -
-

Correo:

-

{data?.data.email || 'Sin correo'}

-
- -
-

Teléfono:

-

{data?.data.phone || 'Sin teléfono'}

-
-
- -

Información de ubicación

-
-
-

Estado:

-

{data?.data.state || 'Sin Estado'}

-
- -
-

Municipio:

-

{data?.data.municipality || 'Sin Municipio'}

-
- -
-

Parroquia:

-

{data?.data.parish || 'Sin Parroquia'}

-
-
- -
- ); -} - diff --git a/apps/web/feactures/inventory/hooks/use-mutation-users.ts b/apps/web/feactures/inventory/hooks/use-mutation-users.ts deleted file mode 100644 index dbbf733..0000000 --- a/apps/web/feactures/inventory/hooks/use-mutation-users.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { CreateUser, UpdateUser } from "../schemas/inventory"; -import { updateUserAction, createUserAction, deleteUserAction, updateProfileAction } from "../actions/actions"; - -// Create mutation -export function useCreateUser() { - const queryClient = useQueryClient(); - const mutation = useMutation({ - mutationFn: (data: CreateUser) => createUserAction(data), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }), - // onError: (e) => console.error('Error:', e), - }) - return mutation -} - -// Update mutation -export function useUpdateUser() { - const queryClient = useQueryClient(); - const mutation = useMutation({ - mutationFn: (data: UpdateUser) => updateUserAction(data), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }), - onError: (e) => console.error('Error:', e) - }) - return mutation; -} - -export function useUpdateProfile() { - const queryClient = useQueryClient(); - const mutation = useMutation({ - mutationFn: (data: UpdateUser) => updateProfileAction(data), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }), - // onError: (e) => console.error('Error:', e) - }) - return mutation; -} - -// Delete mutation -export function useDeleteUser() { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: (id: number) => deleteUserAction(id), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }), - onError: (e) => console.error('Error:', e) - }) -} \ No newline at end of file diff --git a/apps/web/feactures/inventory/hooks/use-mutation.ts b/apps/web/feactures/inventory/hooks/use-mutation.ts new file mode 100644 index 0000000..fbe334c --- /dev/null +++ b/apps/web/feactures/inventory/hooks/use-mutation.ts @@ -0,0 +1,35 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { EditInventory } from "../schemas/inventory"; +import { updateUserAction, createProductAction } from "../actions/actions"; + +// Create mutation +export function useCreateUser() { + const queryClient = useQueryClient(); + const mutation = useMutation({ + mutationFn: (data: EditInventory) => createProductAction(data), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['product'] }), + // onError: (e) => console.error('Error:', e), + }) + return mutation +} + +// Update mutation +export function useUpdateUser() { + const queryClient = useQueryClient(); + const mutation = useMutation({ + mutationFn: (data: EditInventory) => updateUserAction(data), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['product'] }), + onError: (e) => console.error('Error:', e) + }) + return mutation; +} + +// Delete mutation +// export function useDeleteUser() { +// const queryClient = useQueryClient(); +// return useMutation({ +// mutationFn: (id: number) => deleteUserAction(id), +// onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }), +// onError: (e) => console.error('Error:', e) +// }) +// } \ No newline at end of file diff --git a/apps/web/feactures/inventory/hooks/use-query-surveys.ts b/apps/web/feactures/inventory/hooks/use-query-surveys.ts deleted file mode 100644 index 6610191..0000000 --- a/apps/web/feactures/inventory/hooks/use-query-surveys.ts +++ /dev/null @@ -1,12 +0,0 @@ -'use client' -import { useSafeQuery } from "@/hooks/use-safe-query"; -import { getUsersAction,getProfileAction} from "../actions/actions"; - -// Hook for users -export function useUsersQuery(params = {}) { - return useSafeQuery(['users',params], () => getUsersAction(params)) -} - -export function useUserByProfile() { - return useSafeQuery(['users'], () => getProfileAction()) -} diff --git a/apps/web/feactures/inventory/schemas/inventory.ts b/apps/web/feactures/inventory/schemas/inventory.ts index 640dd85..a20ee35 100644 --- a/apps/web/feactures/inventory/schemas/inventory.ts +++ b/apps/web/feactures/inventory/schemas/inventory.ts @@ -1,54 +1,31 @@ -import { title } from 'process'; import { z } from 'zod'; export type InventoryTable = z.infer; -export type CreateUser = z.infer; -export type UpdateUser = z.infer; +export type EditInventory = z.infer; export const product = z.object({ id: z.number().optional(), title: z.string(), description: z.string(), - // price: z.number(), - // quantity: z.number(), // category: z.string(), - // image: z.string().optional(), stock: z.number(), price: z.string(), urlImg: z.string(), userId: z.number().optional(), }); -export const createUser = z.object({ +export const editInventory = z.object({ id: z.number().optional(), - username: z.string().min(5, { message: "Debe de tener 5 o más caracteres" }), - password: z.string().min(8, { message: "Debe de tener 8 o más caracteres" }), - email: z.string().email({ message: "Correo no válido" }), - fullname: z.string(), - phone: z.string(), - confirmPassword: z.string(), - role: z.number() -}) -.refine((data) => data.password === data.confirmPassword, { - message: 'La contraseña no coincide', - path: ['confirmPassword'], + title: z.string().min(5, { message: "Debe de tener 5 o más caracteres" }), + description: z.string().min(10, { message: "Debe de tener 10 o más caracteres" }), + stock: z.string().transform(val => Number(val)), + price: z.string(), + urlImg: z.string(), + userId: z.number().optional(), }) -export const updateUser = z.object({ - id: z.number(), - username: z.string().min(5, { message: "Debe de tener 5 o más caracteres" }).or(z.literal('')), - password: z.string().min(6, { message: "Debe de tener 6 o más caracteres" }).or(z.literal('')), - email: z.string().email({ message: "Correo no válido" }).or(z.literal('')), - fullname: z.string().optional(), - phone: z.string().optional(), - role: z.number().optional(), - isActive: z.boolean().optional(), - state: z.number().optional().nullable(), - municipality: z.number().optional().nullable(), - parish: z.number().optional().nullable(), -}) -export const surveysApiResponseSchema = z.object({ +export const ApiResponseSchema = z.object({ message: z.string(), data: z.array(product), meta: z.object({ diff --git a/apps/web/feactures/users/components/selectList.tsx b/apps/web/feactures/users/components/selectList.tsx deleted file mode 100644 index eec6688..0000000 --- a/apps/web/feactures/users/components/selectList.tsx +++ /dev/null @@ -1,83 +0,0 @@ -// 'use client'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@repo/shadcn/select'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@repo/shadcn/form'; - - -interface SelectListProps { - label: string - // values: - values: Array - form: any - name: string - handleChange: any -} - -export function SelectList({ label, values, form, name, handleChange }: SelectListProps) { - // const { label, values, form, name } = props; - // handleChange - - // const defaultformValues = { - // username: '', - // fullname: '', - // email: '', - // password: '', - // id: 0, - // phone: '', - // role: undefined, - // isActive: false - // } - - // const form = useForm({ - // resolver: zodResolver(updateUser), - // defaultValues: defaultformValues, - // mode: 'onChange', // Enable real-time validation - // }); - - - return ( - - {label} - - - - )} - /> -} \ No newline at end of file diff --git a/apps/web/feactures/users/components/survey.tsx b/apps/web/feactures/users/components/survey.tsx deleted file mode 100644 index b4b5eb9..0000000 --- a/apps/web/feactures/users/components/survey.tsx +++ /dev/null @@ -1,28 +0,0 @@ -'use client' - -import { SurveyResponse } from '@/feactures/surveys/components/survey-response'; -import { useSurveysByIdQuery } from '@/feactures/surveys/hooks/use-query-surveys'; - -import { notFound, useParams } from 'next/navigation'; - - -export default function SurveyPage() { - const params = useParams(); - const surveyId = params?.id as string | undefined; - - - if (!surveyId || surveyId === '') { - notFound(); - } - - const { data: survey, isLoading } = useSurveysByIdQuery(Number(surveyId)); - console.log('🎯 useSurveysByIdQuery ejecutado, data:', survey, 'isLoading:', isLoading); - - if (!survey?.data || !survey?.data.published) { - notFound(); - } - - return ( - - ); -} \ No newline at end of file From f5962efb8b2abdef23261c4e81a90cca17d6dc49 Mon Sep 17 00:00:00 2001 From: Sergio Ramirez Date: Fri, 27 Jun 2025 14:38:08 -0400 Subject: [PATCH 05/22] correciones en las validaciones de crear-editar productos --- .../inventory/create-product-form.tsx | 32 ++++-------- .../components/inventory/inventory-modal.tsx | 1 - .../inventory/update-product-form.tsx | 50 ++++++++----------- .../feactures/inventory/schemas/inventory.ts | 7 ++- 4 files changed, 35 insertions(+), 55 deletions(-) diff --git a/apps/web/feactures/inventory/components/inventory/create-product-form.tsx b/apps/web/feactures/inventory/components/inventory/create-product-form.tsx index 297721e..b241d9c 100644 --- a/apps/web/feactures/inventory/components/inventory/create-product-form.tsx +++ b/apps/web/feactures/inventory/components/inventory/create-product-form.tsx @@ -10,18 +10,9 @@ import { FormMessage, } from '@repo/shadcn/form'; import { Input } from '@repo/shadcn/input'; -// import { -// Select, -// SelectContent, -// SelectItem, -// SelectTrigger, -// SelectValue, -// } from '@repo/shadcn/select'; import { useForm } from 'react-hook-form'; import { useCreateUser } from "@/feactures/inventory/hooks/use-mutation"; -import { EditInventory, editInventory } from '@/feactures/inventory/schemas/inventory'; -import { parse } from 'path'; - +import { EditInventory, editInventory, formDataInput } from '@/feactures/inventory/schemas/inventory'; interface CreateFormProps { onSuccess?: () => void; @@ -31,8 +22,7 @@ interface CreateFormProps { export function CreateForm({ onSuccess, - onCancel, - defaultValues, + onCancel }: CreateFormProps) { const { mutate: saveAccountingAccounts, @@ -40,17 +30,15 @@ export function CreateForm({ isError, } = useCreateUser(); - // const { data: AccoutingAccounts } = useSurveyMutation(); - const defaultformValues = { - title: defaultValues?.title || '', - description: defaultValues?.description || '', - price: defaultValues?.price || '', - stock: defaultValues?.stock || 0, - urlImg: defaultValues?.urlImg || '' + title: '', + description: '', + price: '', + stock: '', + urlImg: '' } - const form = useForm({ + const form = useForm({ resolver: zodResolver(editInventory), defaultValues: defaultformValues, mode: 'onChange', // Enable real-time validation @@ -58,9 +46,7 @@ export function CreateForm({ const onSubmit = async (data: EditInventory) => { - const formData = data - - saveAccountingAccounts(formData, { + saveAccountingAccounts(data, { onSuccess: () => { form.reset(); onSuccess?.(); diff --git a/apps/web/feactures/inventory/components/inventory/inventory-modal.tsx b/apps/web/feactures/inventory/components/inventory/inventory-modal.tsx index 28a4348..41a36c6 100644 --- a/apps/web/feactures/inventory/components/inventory/inventory-modal.tsx +++ b/apps/web/feactures/inventory/components/inventory/inventory-modal.tsx @@ -61,7 +61,6 @@ export function AccountPlanModal({ )} diff --git a/apps/web/feactures/inventory/components/inventory/update-product-form.tsx b/apps/web/feactures/inventory/components/inventory/update-product-form.tsx index c4ee45d..a84962f 100644 --- a/apps/web/feactures/inventory/components/inventory/update-product-form.tsx +++ b/apps/web/feactures/inventory/components/inventory/update-product-form.tsx @@ -10,17 +10,9 @@ import { FormMessage, } from '@repo/shadcn/form'; import { Input } from '@repo/shadcn/input'; -// import { -// Select, -// SelectContent, -// SelectItem, -// SelectTrigger, -// SelectValue, -// } from '@repo/shadcn/select'; import { useForm } from 'react-hook-form'; import { useUpdateUser } from "@/feactures/inventory/hooks/use-mutation"; -import { EditInventory, editInventory } from '@/feactures/inventory/schemas/inventory'; - +import { editInventory, formDataInput, EditInventory } from '@/feactures/inventory/schemas/inventory'; // Renombrado EditInventory para claridad interface UpdateFormProps { onSuccess?: () => void; @@ -36,39 +28,37 @@ export function UpdateForm({ const { mutate: saveAccountingAccounts, isPending: isSaving, - isError, + isError, // isError no se usa en el template, podrías usarlo para mostrar un mensaje global } = useUpdateUser(); - const defaultformValues = { - id: defaultValues?.id, + const defaultformValues: formDataInput = { + id: defaultValues?.id, title: defaultValues?.title || '', description: defaultValues?.description || '', - price: defaultValues?.price || '', - stock: defaultValues?.stock || 0, + price: defaultValues?.price || '', + stock: (defaultValues?.stock ?? '').toString(), urlImg: defaultValues?.urlImg || '', - } + userId: defaultValues?.userId + }; - // console.log(defaultValues); - - const form = useForm({ + const form = useForm({ resolver: zodResolver(editInventory), defaultValues: defaultformValues, mode: 'onChange', // Enable real-time validation }); - const onSubmit = async (data: EditInventory) => { - - const formData = data - - saveAccountingAccounts(formData, { + const onSubmit = async (data: formDataInput) => { + + saveAccountingAccounts(data, { onSuccess: () => { form.reset(); onSuccess?.(); }, - onError: () => { + onError: (error) => { // Captura el error para mostrar un mensaje más específico si es posible + console.error("Error al guardar el producto:", error); form.setError('root', { type: 'manual', - message: 'Error al guardar la cuenta contable', + message: error.message || 'Error al guardar el producto', // Mejor mensaje de error }); }, }); @@ -118,7 +108,8 @@ export function UpdateForm({ Precio - + {/* Simplificado. price es z.string(), field.value ya es string o undefined. */} + @@ -132,7 +123,8 @@ export function UpdateForm({ Cantidad/Stock - + {/* Añadido type="number" para UX. field.value ya es string debido a formDataInput */} + @@ -146,7 +138,7 @@ export function UpdateForm({ Imagen - + @@ -165,4 +157,4 @@ export function UpdateForm({ ); -} +} \ No newline at end of file diff --git a/apps/web/feactures/inventory/schemas/inventory.ts b/apps/web/feactures/inventory/schemas/inventory.ts index a20ee35..e37cd34 100644 --- a/apps/web/feactures/inventory/schemas/inventory.ts +++ b/apps/web/feactures/inventory/schemas/inventory.ts @@ -1,7 +1,8 @@ import { z } from 'zod'; export type InventoryTable = z.infer; -export type EditInventory = z.infer; +export type EditInventory = z.infer; //output +export type formDataInput = z.input; export const product = z.object({ id: z.number().optional(), @@ -18,7 +19,9 @@ export const editInventory = z.object({ id: z.number().optional(), title: z.string().min(5, { message: "Debe de tener 5 o más caracteres" }), description: z.string().min(10, { message: "Debe de tener 10 o más caracteres" }), - stock: z.string().transform(val => Number(val)), + stock: z.string().transform(val => Number(val)).pipe(z.number( + { invalid_type_error: 'El stock debe ser un número' }).min(0, { message: "El stock debe ser mayor a 0" }) +), price: z.string(), urlImg: z.string(), userId: z.number().optional(), From 365cbd0d7a9ce3ddfa6aee4efb5cf3ec64f79182 Mon Sep 17 00:00:00 2001 From: Sergio Ramirez Date: Wed, 2 Jul 2025 15:10:54 -0400 Subject: [PATCH 06/22] Vista (intefaz y bd), esquema y endpoints de store agregados --- .../migrations/0003_icy_gertrude_yorkes.sql | 5 + .../migrations/meta/0003_snapshot.json | 1498 +++++++++++++++++ .../database/migrations/meta/_journal.json | 7 + apps/api/src/database/schema/inventory.ts | 19 +- .../inventory/entities/inventory.entity.ts | 52 +- .../inventory/inventory.controller.ts | 24 +- .../features/inventory/inventory.service.ts | 79 +- .../web/app/dashboard/productos/[id]/page.tsx | 33 + apps/web/app/dashboard/productos/page.tsx | 21 + .../feactures/inventory/actions/actions.ts | 72 +- .../inventory/create-product-form.tsx | 30 +- .../inventory/product-inventory-list.tsx | 2 +- .../inventory/update-product-form.tsx | 30 +- .../components/inventory/users-header.tsx | 2 +- .../components/products/product-list.tsx | 71 + ...e-query-users.ts => use-query-products.ts} | 6 +- .../feactures/inventory/schemas/inventory.ts | 31 + apps/web/lib/fetch.api.ts | 1 + .../src/components/ui/table/data-table.tsx | 2 +- 19 files changed, 1909 insertions(+), 76 deletions(-) create mode 100644 apps/api/src/database/migrations/0003_icy_gertrude_yorkes.sql create mode 100644 apps/api/src/database/migrations/meta/0003_snapshot.json create mode 100644 apps/web/app/dashboard/productos/[id]/page.tsx create mode 100644 apps/web/app/dashboard/productos/page.tsx create mode 100644 apps/web/feactures/inventory/components/products/product-list.tsx rename apps/web/feactures/inventory/hooks/{use-query-users.ts => use-query-products.ts} (51%) diff --git a/apps/api/src/database/migrations/0003_icy_gertrude_yorkes.sql b/apps/api/src/database/migrations/0003_icy_gertrude_yorkes.sql new file mode 100644 index 0000000..34910cc --- /dev/null +++ b/apps/api/src/database/migrations/0003_icy_gertrude_yorkes.sql @@ -0,0 +1,5 @@ +ALTER TABLE "products" ALTER COLUMN "user_id" SET NOT NULL;--> statement-breakpoint +CREATE VIEW "public"."v_product_store" AS ( + select p.id as product_id, p.title, p.description, p.price, p.stock, p.url_img, p.user_id, u.fullname + from products p + left join auth.users as u on u.id = p.user_id); \ No newline at end of file diff --git a/apps/api/src/database/migrations/meta/0003_snapshot.json b/apps/api/src/database/migrations/meta/0003_snapshot.json new file mode 100644 index 0000000..e1adda3 --- /dev/null +++ b/apps/api/src/database/migrations/meta/0003_snapshot.json @@ -0,0 +1,1498 @@ +{ + "id": "35c786b6-b0ae-4cf9-b748-9f1b51f17a10", + "prevId": "e3f08a0a-764c-4a4d-8473-c666398b8722", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_logs": { + "name": "activity_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "activityLogs_idx": { + "name": "activityLogs_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_logs_user_id_users_id_fk": { + "name": "activity_logs_user_id_users_id_fk", + "tableFrom": "activity_logs", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.roles": { + "name": "roles", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "roles_idx": { + "name": "roles_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.sessions": { + "name": "sessions", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sessions_idx": { + "name": "sessions_idx", + "columns": [ + { + "expression": "session_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.users": { + "name": "users", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fullname": { + "name": "fullname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "municipality": { + "name": "municipality", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parish": { + "name": "parish", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_two_factor_enabled": { + "name": "is_two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "two_factor_secret": { + "name": "two_factor_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_email_verified": { + "name": "is_email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_idx": { + "name": "users_idx", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_state_states_id_fk": { + "name": "users_state_states_id_fk", + "tableFrom": "users", + "tableTo": "states", + "columnsFrom": [ + "state" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "users_municipality_municipalities_id_fk": { + "name": "users_municipality_municipalities_id_fk", + "tableFrom": "users", + "tableTo": "municipalities", + "columnsFrom": [ + "municipality" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "users_parish_parishes_id_fk": { + "name": "users_parish_parishes_id_fk", + "tableFrom": "users", + "tableTo": "parishes", + "columnsFrom": [ + "parish" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.user_role": { + "name": "user_role", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_role_idx": { + "name": "user_role_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_role_user_id_users_id_fk": { + "name": "user_role_user_id_users_id_fk", + "tableFrom": "user_role", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_role_role_id_roles_id_fk": { + "name": "user_role_role_id_roles_id_fk", + "tableFrom": "user_role", + "tableTo": "roles", + "schemaTo": "auth", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.verificationToken": { + "name": "verificationToken", + "schema": "auth", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.category_type": { + "name": "category_type", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "group": { + "name": "group", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "category_typeIx0": { + "name": "category_typeIx0", + "columns": [ + { + "expression": "group", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "category_typeIx1": { + "name": "category_typeIx1", + "columns": [ + { + "expression": "description", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.localities": { + "name": "localities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "state_id": { + "name": "state_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parish_id": { + "name": "parish_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "localities_index_03": { + "name": "localities_index_03", + "columns": [ + { + "expression": "state_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parish_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "localities_index_00": { + "name": "localities_index_00", + "columns": [ + { + "expression": "state_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "localities_index_01": { + "name": "localities_index_01", + "columns": [ + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "localities_index_02": { + "name": "localities_index_02", + "columns": [ + { + "expression": "parish_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "localities_state_id_states_id_fk": { + "name": "localities_state_id_states_id_fk", + "tableFrom": "localities", + "tableTo": "states", + "columnsFrom": [ + "state_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "localities_municipality_id_municipalities_id_fk": { + "name": "localities_municipality_id_municipalities_id_fk", + "tableFrom": "localities", + "tableTo": "municipalities", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "localities_parish_id_parishes_id_fk": { + "name": "localities_parish_id_parishes_id_fk", + "tableFrom": "localities", + "tableTo": "parishes", + "columnsFrom": [ + "parish_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "localities_name_unique": { + "name": "localities_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.municipalities": { + "name": "municipalities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_id": { + "name": "state_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "municipalities_index_00": { + "name": "municipalities_index_00", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "municipalities_state_id_states_id_fk": { + "name": "municipalities_state_id_states_id_fk", + "tableFrom": "municipalities", + "tableTo": "states", + "columnsFrom": [ + "state_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.parishes": { + "name": "parishes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "parishes_index_00": { + "name": "parishes_index_00", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "parishes_municipality_id_municipalities_id_fk": { + "name": "parishes_municipality_id_municipalities_id_fk", + "tableFrom": "parishes", + "tableTo": "municipalities", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.states": { + "name": "states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "states_index_00": { + "name": "states_index_00", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.products": { + "name": "products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "stock": { + "name": "stock", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "url_img": { + "name": "url_img", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "products_user_id_users_id_fk": { + "name": "products_user_id_users_id_fk", + "tableFrom": "products", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.answers_surveys": { + "name": "answers_surveys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "survey_id": { + "name": "survey_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "answers": { + "name": "answers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "answers_index_00": { + "name": "answers_index_00", + "columns": [ + { + "expression": "answers", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "answers_index_01": { + "name": "answers_index_01", + "columns": [ + { + "expression": "survey_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "answers_index_02": { + "name": "answers_index_02", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "answers_surveys_survey_id_surveys_id_fk": { + "name": "answers_surveys_survey_id_surveys_id_fk", + "tableFrom": "answers_surveys", + "tableTo": "surveys", + "columnsFrom": [ + "survey_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "answers_surveys_user_id_users_id_fk": { + "name": "answers_surveys_user_id_users_id_fk", + "tableFrom": "answers_surveys", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.surveys": { + "name": "surveys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_audience": { + "name": "target_audience", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "closing_date": { + "name": "closing_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "questions": { + "name": "questions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "surveys_index_00": { + "name": "surveys_index_00", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "auth.gender": { + "name": "gender", + "schema": "auth", + "values": [ + "FEMENINO", + "MASCULINO" + ] + }, + "public.nationality": { + "name": "nationality", + "schema": "public", + "values": [ + "VENEZOLANO", + "EXTRANJERO" + ] + }, + "auth.status": { + "name": "status", + "schema": "auth", + "values": [ + "ACTIVE", + "INACTIVE" + ] + } + }, + "schemas": { + "auth": "auth" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "auth.user_access_view": { + "columns": { + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "role_name": { + "name": "role_name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT\n u.id AS user_id,\n u.username,\n u.email,\n u.fullname,\n r.id AS role_id,\n r.name AS role_name\nFROM\n auth.users u\nLEFT JOIN\n auth.user_role ur ON u.id = ur.user_id \nLEFT JOIN\n auth.roles r ON ur.role_id = r.id", + "name": "user_access_view", + "schema": "auth", + "isExisting": false, + "materialized": false + }, + "public.v_product_store": { + "columns": { + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "price": { + "name": "price", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "stock": { + "name": "stock", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "url_img": { + "name": "url_img", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fullname": { + "name": "fullname", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n select p.id as product_id, p.title, p.description, p.price, p.stock, p.url_img, p.user_id, u.fullname\n from products p\n left join auth.users as u on u.id = p.user_id", + "name": "v_product_store", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.v_surveys": { + "columns": { + "survey_id": { + "name": "survey_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "closing_date": { + "name": "closing_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "target_audience": { + "name": "target_audience", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select id as survey_id, title, description, created_at, closing_date, target_audience from surveys\nwhere published = true", + "name": "v_surveys", + "schema": "public", + "isExisting": false, + "materialized": false + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/src/database/migrations/meta/_journal.json b/apps/api/src/database/migrations/meta/_journal.json index 2ef8b9a..88a1ed8 100644 --- a/apps/api/src/database/migrations/meta/_journal.json +++ b/apps/api/src/database/migrations/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1750442271575, "tag": "0002_polite_franklin_richards", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1751482400155, + "tag": "0003_icy_gertrude_yorkes", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/api/src/database/schema/inventory.ts b/apps/api/src/database/schema/inventory.ts index 9cd06e6..300d452 100644 --- a/apps/api/src/database/schema/inventory.ts +++ b/apps/api/src/database/schema/inventory.ts @@ -1,6 +1,7 @@ import * as t from 'drizzle-orm/pg-core'; import { timestamps } from '../timestamps'; import { users } from './auth'; +import { sql } from 'drizzle-orm'; export const products = t.pgTable( 'products', @@ -11,7 +12,21 @@ export const products = t.pgTable( 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' }), + userId: t.integer('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(), ...timestamps, } -); \ No newline at end of file +); + +export const viewProductsStore = t.pgView('v_product_store', { + id: t.integer('product_id'), + title: t.text('title'), + description: t.text('description'), + price: t.numeric('price'), + stock: t.integer('stock'), + urlImg: t.text('url_img'), + userId: t.integer('user_id'), + fullname: t.text('fullname') +}).as(sql` + select p.id as product_id, p.title, p.description, p.price, p.stock, p.url_img, p.user_id, u.fullname + from products p + left join auth.users as u on u.id = p.user_id`); \ No newline at end of file diff --git a/apps/api/src/features/inventory/entities/inventory.entity.ts b/apps/api/src/features/inventory/entities/inventory.entity.ts index 3f511e8..cbd6420 100644 --- a/apps/api/src/features/inventory/entities/inventory.entity.ts +++ b/apps/api/src/features/inventory/entities/inventory.entity.ts @@ -1,19 +1,39 @@ export class Product { - id?: number; - title: string; - description: string; - price: string; - stock: number; - urlImg: string; - UserId?: number; + id?: number | null; + title: string | null; + description: string | null; + price: string | null; + stock: number | null; + urlImg: string | null; + userId?: number | null; } -export class CreateProduct { - id: number; - title: string; - description: string; - price: string; - stock: string; - urlImg: string; - UserId: number; -} \ No newline at end of file +export class Inventory { + id: number | null; + title: string | null; + description: string | null; + price: string | null; + stock: number | null; + urlImg: string | null; +} + +export class Store { + id: number | null; + title: string | null; + description: string | null; + price: string | null; + stock: number | null; + urlImg: string | null; + userId: number | null; + fullname?: string | null; +} + +// export class CreateProduct { +// id: number; +// title: string; +// description: string; +// price: string; +// stock: string; +// urlImg: string; +// UserId: number; +// } \ 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 index 6078262..80779c5 100644 --- a/apps/api/src/features/inventory/inventory.controller.ts +++ b/apps/api/src/features/inventory/inventory.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common'; +import { Controller, Get, Post, Body, Patch, Param, Delete, Query, Req } from '@nestjs/common'; import { InventoryService } from './inventory.service'; import { CreateProductDto } from './dto/create-product.dto'; import { UpdateProductDto } from './dto/update-product.dto'; @@ -6,12 +6,12 @@ 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') +@ApiTags('products') +@Controller('products') export class UsersController { constructor(private readonly inventoryService: InventoryService) {} - @Get() + @Get('/store') // @Roles('admin') @ApiOperation({ summary: 'Get all products with pagination and filters' }) @ApiResponse({ status: 200, description: 'Return paginated products.' }) @@ -24,6 +24,22 @@ export class UsersController { }; } + @Get('/inventory') + // @Roles('admin') + @ApiOperation({ summary: 'Get all products with pagination and filters' }) + @ApiResponse({ status: 200, description: 'Return paginated products.' }) + async findAllByUserId(@Req() req: Request, @Query() paginationDto: PaginationDto) { + console.log(req['user'].id) + const id = 1 + // const id = Number(req['user'].id); + const result = await this.inventoryService.findAllByUserId(id,paginationDto); + return { + message: 'products fetched successfully', + data: result.data, + meta: result.meta + }; + } + @Get(':id') // @Roles('admin') @ApiOperation({ summary: 'Get a product by ID' }) diff --git a/apps/api/src/features/inventory/inventory.service.ts b/apps/api/src/features/inventory/inventory.service.ts index 732d41b..b0bcf9d 100644 --- a/apps/api/src/features/inventory/inventory.service.ts +++ b/apps/api/src/features/inventory/inventory.service.ts @@ -1,12 +1,12 @@ import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider'; -import { Inject, Injectable, HttpException, HttpStatus, NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { Inject, Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import * as schema from 'src/database/index'; -import { products } from 'src/database/index'; +import { products, viewProductsStore } from 'src/database/index'; import { eq, like, or, SQL, sql, and, not } from 'drizzle-orm'; import { CreateProductDto } from './dto/create-product.dto'; import { UpdateProductDto } from './dto/update-product.dto'; -import { Product, CreateProduct } from './entities/inventory.entity'; +import { Product, Store, Inventory } from './entities/inventory.entity'; import { PaginationDto } from '../../common/dto/pagination.dto'; @Injectable() @@ -15,7 +15,7 @@ export class InventoryService { @Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase, ) { } - async findAll(paginationDto?: PaginationDto): Promise<{ data: Product[], meta: any }> { + async findAllByUserId(id: number, paginationDto?: PaginationDto): Promise<{ data: Inventory[], meta: any }> { const { page = 1, limit = 10, search = '', sortBy = 'id', sortOrder = 'asc' } = paginationDto || {}; // Calculate offset @@ -23,12 +23,14 @@ export class InventoryService { // Build search condition let searchCondition: SQL | undefined; + if (search) { - searchCondition = or( + searchCondition = and(or( like(products.title, `%${search}%`), like(products.description, `%${search}%`), - // like(users.fullname, `%${search}%`) - ); + ), eq(products.userId, id)); + }else{ + searchCondition = eq(products.userId, id); } // Build sort condition @@ -52,8 +54,8 @@ export class InventoryService { title: products.title, description: products.description, price: products.price, - urlImg: products.urlImg, stock: products.stock, + urlImg: products.urlImg }) .from(products) .where(searchCondition) @@ -75,6 +77,67 @@ export class InventoryService { return { data, meta }; } + async findAll(paginationDto?: PaginationDto): Promise<{ data: Store[], 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(viewProductsStore.title, `%${search}%`), + like(viewProductsStore.description, `%${search}%`), + ); + } + + // Build sort condition + const orderBy = sortOrder === 'asc' + ? sql`${viewProductsStore[sortBy as keyof typeof viewProductsStore]} asc` + : sql`${viewProductsStore[sortBy as keyof typeof viewProductsStore]} desc`; + + // Get total count for pagination + const totalCountResult = await this.drizzle + .select({ count: sql`count(*)` }) + .from(viewProductsStore) + .where(searchCondition); + + const totalCount = Number(totalCountResult[0].count); + const totalPages = Math.ceil(totalCount / limit); + + // Get paginated data + const data = await this.drizzle + .select({ + id: viewProductsStore.id, + title: viewProductsStore.title, + description: viewProductsStore.description, + price: viewProductsStore.price, + urlImg: viewProductsStore.urlImg, + stock: viewProductsStore.stock, + userId: viewProductsStore.userId, + fullname: viewProductsStore.fullname + }) + .from(viewProductsStore) + .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, + }; + return { data, meta }; + } + async findOne(id: string): Promise { const find = await this.drizzle .select({ diff --git a/apps/web/app/dashboard/productos/[id]/page.tsx b/apps/web/app/dashboard/productos/[id]/page.tsx new file mode 100644 index 0000000..fb4037e --- /dev/null +++ b/apps/web/app/dashboard/productos/[id]/page.tsx @@ -0,0 +1,33 @@ +import PageContainer from '@/components/layout/page-container'; +import { getSurveyByIdAction } from '@/feactures/surveys/actions/surveys-actions'; +import { SurveyResponse } from '@/feactures/surveys/components/survey-response'; + + + +export default async function SurveyResponsePage({ + params, +}: { + params: Promise<{ id: string }> +}) { + const { id } = await params; // You can still destructure id from params + + if (!id || id === '') { + // Handle the case where no id is provided + return null; + } + + // Call the function passing the dynamic id + const data = await getSurveyByIdAction(Number(id)); + + if (!data?.data) { + return
Encuesta no encontrada
; + } + + return ( + +
+ +
+
+ ); +} \ No newline at end of file diff --git a/apps/web/app/dashboard/productos/page.tsx b/apps/web/app/dashboard/productos/page.tsx new file mode 100644 index 0000000..03de14f --- /dev/null +++ b/apps/web/app/dashboard/productos/page.tsx @@ -0,0 +1,21 @@ +import PageContainer from '@/components/layout/page-container'; +import { SurveyList } from '@/feactures/inventory/components/products/product-list'; +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Productos - Fondemi', + description: 'Listado de productos disponibles', +}; + +export default function SurveysPage() { + + return ( + +
+

Productos Disponibles

+ +
+
+ + ); +} \ No newline at end of file diff --git a/apps/web/feactures/inventory/actions/actions.ts b/apps/web/feactures/inventory/actions/actions.ts index e5220dc..f9a0eb7 100644 --- a/apps/web/feactures/inventory/actions/actions.ts +++ b/apps/web/feactures/inventory/actions/actions.ts @@ -4,7 +4,8 @@ import { ApiResponseSchema, InventoryTable, productMutate, - editInventory + // editInventory, + productApiResponseSchema } from '../schemas/inventory'; import { auth } from '@/lib/auth'; @@ -27,8 +28,8 @@ export const getInventoryAction = async (params: { const [error, response] = await safeFetchApi( ApiResponseSchema, - `/inventory?${searchParams}`, - 'GET', + `/products/inventory?${searchParams}`, + 'GET' ); if (error) { @@ -53,6 +54,52 @@ export const getInventoryAction = async (params: { }; } +export const getAllProducts = async (params: { + page?: number; + limit?: number; + search?: string; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; +}) => { + + const session = await auth() + + const searchParams = new URLSearchParams({ + page: (params.page || 1).toString(), + limit: (params.limit || 10).toString(), + ...(params.search && { search: params.search }), + ...(params.sortBy && { sortBy: params.sortBy }), + ...(params.sortOrder && { sortOrder: params.sortOrder }), + }) + + const id = session?.user.id + + const [error, response] = await safeFetchApi( + productApiResponseSchema, + `/products/store?${searchParams}`, + 'GET' + ); + + if (error) { + console.error('Error:', error); + throw new Error(error.message); + } + + return { + data: response?.data || [], + meta: response?.meta || { + page: 1, + limit: 10, + totalCount: 0, + totalPages: 1, + hasNextPage: false, + hasPreviousPage: false, + nextPage: null, + previousPage: null, + }, + }; +}; + export const createProductAction = async (payload: InventoryTable) => { const session = await auth() const userId = session?.user?.id @@ -62,14 +109,14 @@ export const createProductAction = async (payload: InventoryTable) => { const [error, data] = await safeFetchApi( productMutate, - '/inventory', + '/products', 'POST', payloadWithoutId, ); if (error) { console.error(error); - throw new Error('Error al crear el usuario'); + throw new Error('Error al crear el producto'); } return payloadWithoutId; @@ -81,16 +128,14 @@ export const updateUserAction = async (payload: InventoryTable) => { const [error, data] = await safeFetchApi( productMutate, - `/inventory/${id}`, + `/products/${id}`, 'PATCH', payloadWithoutId, ); - // console.log(data); if (error) { console.error(error); - - throw new Error(error?.message || 'Error al actualizar el usuario'); + throw new Error(error?.message || 'Error al actualizar el producto'); } return data; } catch (error) { @@ -98,17 +143,14 @@ export const updateUserAction = async (payload: InventoryTable) => { } } -export const deleteUserAction = async (id: Number) => { +export const deleteProductAction = async (id: Number) => { const [error] = await safeFetchApi( productMutate, - `/users/${id}`, + `/products/${id}`, 'DELETE' ) - console.log(error); - - - // if (error) throw new Error(error.message || 'Error al eliminar el usuario') + if (error) throw new Error(error.message || 'Error al eliminar el usuario') return true; } \ No newline at end of file diff --git a/apps/web/feactures/inventory/components/inventory/create-product-form.tsx b/apps/web/feactures/inventory/components/inventory/create-product-form.tsx index b241d9c..98693c1 100644 --- a/apps/web/feactures/inventory/components/inventory/create-product-form.tsx +++ b/apps/web/feactures/inventory/components/inventory/create-product-form.tsx @@ -9,6 +9,7 @@ import { FormLabel, FormMessage, } from '@repo/shadcn/form'; +import { Textarea } from '@repo/shadcn/textarea'; import { Input } from '@repo/shadcn/input'; import { useForm } from 'react-hook-form'; import { useCreateUser } from "@/feactures/inventory/hooks/use-mutation"; @@ -83,20 +84,6 @@ export function CreateForm({ )} /> - ( - - Descripción - - - - - - )} - /> - + ( + + Descripción + + {/* */} +