diff --git a/apps/api/src/database/seeds/inventory.seed.ts b/apps/api/src/database/seeds/inventory.seed.ts index 2cb8a9e..5485ee1 100644 --- a/apps/api/src/database/seeds/inventory.seed.ts +++ b/apps/api/src/database/seeds/inventory.seed.ts @@ -17,7 +17,7 @@ export async function seedProducts(db: NodePgDatabase) { status:'PUBLICADO', // PUBLICADO, AGOTADO, BORRADOR urlImg:'apple.avif', userId:1, - gallery: ["Pruebas"] + gallery: ["Pruebas.png","Pruebas2.png"] } ]; diff --git a/apps/api/src/features/auth/auth.controller.ts b/apps/api/src/features/auth/auth.controller.ts index a8899fd..55de095 100644 --- a/apps/api/src/features/auth/auth.controller.ts +++ b/apps/api/src/features/auth/auth.controller.ts @@ -54,13 +54,16 @@ export class AuthController { @Patch('refresh-token') //@RequirePermissions('auth:refresh-token') async refreshToken(@Body() refreshTokenDto: RefreshTokenDto) { + console.log("Refrescando token..."); + console.log(refreshTokenDto); + return await this.authService.refreshToken(refreshTokenDto); } - @Public() - @HttpCode(200) - @Get('test') - async test() { - return 'aplication test success'; - } + // @Public() + // @HttpCode(200) + // @Get('test') + // async test() { + // return 'aplication test success'; + // } } diff --git a/apps/api/src/features/inventory/dto/update-product.dto.ts b/apps/api/src/features/inventory/dto/update-product.dto.ts index 58dbe43..1b1b1ae 100644 --- a/apps/api/src/features/inventory/dto/update-product.dto.ts +++ b/apps/api/src/features/inventory/dto/update-product.dto.ts @@ -5,6 +5,12 @@ import { CreateProductDto } from './create-product.dto'; import { IsOptional, IsString } from 'class-validator'; export class UpdateProductDto extends PartialType(CreateProductDto) { + @ApiProperty() + @IsString({ + message: 'id must be a number', + }) + id: string; + @IsOptional() title: string; @@ -25,4 +31,10 @@ export class UpdateProductDto extends PartialType(CreateProductDto) { @IsOptional() urlImg: string; + + // @ApiProperty() + // @IsString({ + // message: 'userId must be a number', + // }) + // userId: number; } diff --git a/apps/api/src/features/inventory/entities/inventory.entity.ts b/apps/api/src/features/inventory/entities/inventory.entity.ts index 6599083..dee341f 100644 --- a/apps/api/src/features/inventory/entities/inventory.entity.ts +++ b/apps/api/src/features/inventory/entities/inventory.entity.ts @@ -16,6 +16,9 @@ export class Inventory { price: string | null; stock: number | null; urlImg: string | null; + gallery: string[] | null; + address: string | null; + status: string | null; } export class Store { diff --git a/apps/api/src/features/inventory/inventory.controller.ts b/apps/api/src/features/inventory/inventory.controller.ts index 119b13d..738bd65 100644 --- a/apps/api/src/features/inventory/inventory.controller.ts +++ b/apps/api/src/features/inventory/inventory.controller.ts @@ -1,4 +1,5 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete, Query, Req } from '@nestjs/common'; +import { Controller, Get, Post, Body, Patch, Param, Delete, Query, Req, UseInterceptors, UploadedFiles } from '@nestjs/common'; +import { FilesInterceptor } from '@nestjs/platform-express'; import { InventoryService } from './inventory.service'; import { CreateProductDto } from './dto/create-product.dto'; import { UpdateProductDto } from './dto/update-product.dto'; @@ -62,7 +63,7 @@ export class UsersController { return { message: 'User created successfully', data }; } - @Patch(':id') + @Patch('/id/:id') // @Roles('admin') @ApiOperation({ summary: 'Update a product' }) @ApiResponse({ status: 200, description: 'Product updated successfully.' }) @@ -72,6 +73,27 @@ export class UsersController { return { message: 'User updated successfully', data }; } + @Patch('/upload') + @ApiOperation({ summary: 'Update a product' }) + @ApiResponse({ status: 200, description: 'Product uploaded successfully.'}) + @ApiResponse({ status: 404, description: 'Product not found.' }) + @ApiResponse({ status: 500, description: 'Internal server error.' }) + @UseInterceptors(FilesInterceptor('urlImg')) + async uploadFile(@Req() req: Request, @UploadedFiles() files: Express.Multer.File[], @Body() body: any) { + // Aquí puedes acceder a los campos del formulario + // console.log('Archivos:', files); + const id = Number(req['user'].id); + // console.log(req['user'].id) + // console.log('Otros campos del formulario:', body); + const result = await this.inventoryService.saveImages(files,body,id); + + // const result = ['result'] + + return { data: result }; + } + + + // @Delete(':id') // @Roles('admin') // @ApiOperation({ summary: 'Delete a user' }) diff --git a/apps/api/src/features/inventory/inventory.service.ts b/apps/api/src/features/inventory/inventory.service.ts index 36a380d..0742d45 100644 --- a/apps/api/src/features/inventory/inventory.service.ts +++ b/apps/api/src/features/inventory/inventory.service.ts @@ -8,6 +8,9 @@ import { CreateProductDto } from './dto/create-product.dto'; import { UpdateProductDto } from './dto/update-product.dto'; import { Product, Store, Inventory } from './entities/inventory.entity'; import { PaginationDto } from '../../common/dto/pagination.dto'; +// Para guardar la imagen +import { writeFile, mkdir, unlink, rmdir, rm } from 'fs/promises'; +import { join } from 'path'; @Injectable() export class InventoryService { @@ -29,7 +32,7 @@ export class InventoryService { like(products.title, `%${search}%`), like(products.description, `%${search}%`), ), eq(products.userId, id)); - }else{ + } else { searchCondition = eq(products.userId, id); } @@ -57,7 +60,9 @@ export class InventoryService { price: products.price, stock: products.stock, status: products.status, - urlImg: products.urlImg + urlImg: products.urlImg, + gallery: products.gallery, + userId: products.userId }) .from(products) .where(searchCondition) @@ -92,15 +97,15 @@ export class InventoryService { or( like(viewProductsStore.title, `%${search}%`), like(viewProductsStore.description, `%${search}%`) - ), + ), or(eq(viewProductsStore.status, 'PUBLICADO'), eq(viewProductsStore.status, 'AGOTADO')) ) - } else if(search){ + } else if (search) { or( like(viewProductsStore.title, `%${search}%`), like(viewProductsStore.description, `%${search}%`) ) - } else if(isStore){ + } else if (isStore) { searchCondition = or(eq(viewProductsStore.status, 'PUBLICADO'), eq(viewProductsStore.status, 'AGOTADO')) } @@ -202,14 +207,14 @@ export class InventoryService { userId: createProductDto.userId }) .returning(); - return newProduct + return newProduct }); } async update(id: string, updateProductDto: UpdateProductDto): Promise { const productId = parseInt(id); - console.log(updateProductDto); - + // console.log(updateProductDto); + // Check if exists await this.findOne(id); @@ -229,6 +234,54 @@ export class InventoryService { // return this.findOne(id); } + /** + * Guarda una imagen en el directorio de imágenes. + * @param file - El archivo de imagen a guardar. + * @returns La ruta de la imagen guardada. + */ + async saveImages(file: Express.Multer.File[], updateProductDto: UpdateProductDto, id: number): Promise { + const productId = parseInt(id.toString()); + + // Construye la ruta al directorio de imágenes. + const picturesPath = join(__dirname, '..', '..', '..', '..', 'web', 'public', 'uploads', 'inventory', id.toString()); + + // --- NUEVA LÓGICA: Borrar el directorio anterior --- + try { + // Borra el directorio y todos sus contenidos de forma recursiva y forzada. + await rm(picturesPath, { recursive: true, force: true }); + } catch (error) { + // Es buena práctica manejar el error, aunque `force: true` lo hace menos probable. + // Podrías registrar el error, pero no detener la ejecución. + console.error(`No se pudo eliminar el directorio ${picturesPath}:`, error); + } + // --- FIN DE LA NUEVA LÓGICA --- + + // Crea el directorio si no existe (ya que lo acabamos de borrar o no existía). + await mkdir(picturesPath, { recursive: true }); + + let gallery: string[] = []; + + // Usamos `Promise.all` para manejar las operaciones asíncronas de forma correcta. + await Promise.all(file.map(async (f, index) => { + const fileName = `${index + 1}-${f.originalname}`; + gallery.push(fileName); + const filePath = join(picturesPath, fileName); + await writeFile(filePath, f.buffer); + })); + + // 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.address) updateData.address = updateProductDto.address; + if (updateProductDto.status) updateData.status = updateProductDto.status; + if (updateProductDto.stock) updateData.stock = updateProductDto.stock; + if (file && file.length > 0) updateData.gallery = gallery; + + const [updatedProduct] = await this.drizzle.update(products).set(updateData).where(eq(products.id, productId)).returning(); + return updatedProduct; + } // async remove(id: string): Promise<{ message: string, data: User }> { // const userId = parseInt(id); diff --git a/apps/api/src/features/pictures/pictures.controller.ts b/apps/api/src/features/pictures/pictures.controller.ts index 033cec5..ee54831 100644 --- a/apps/api/src/features/pictures/pictures.controller.ts +++ b/apps/api/src/features/pictures/pictures.controller.ts @@ -12,9 +12,7 @@ export class PicturesController { // Aquí puedes acceder a los campos del formulario // console.log('Archivos:', files); // console.log('Otros campos del formulario:', body); - const result = await this.picturesService.saveImages(files); - console.log(result); return { data: result }; } diff --git a/apps/api/src/features/pictures/pictures.service.ts b/apps/api/src/features/pictures/pictures.service.ts index 9f3c645..6ec7537 100644 --- a/apps/api/src/features/pictures/pictures.service.ts +++ b/apps/api/src/features/pictures/pictures.service.ts @@ -11,6 +11,7 @@ export class PicturesService { * @returns La ruta de la imagen guardada. */ async saveImages(file: Express.Multer.File[]): Promise { + // Construye la ruta al directorio de imágenes. const picturesPath = join(__dirname, '..', '..', '..', '..', 'uploads','pict'); @@ -22,27 +23,19 @@ export class PicturesService { file.forEach(async (file) => { count++ + // Crea un nombre de archivo único para la imagen. const fileName = `${Date.now()}-${count}-${file.originalname}`; images.push(fileName); // console.log(fileName); + + // Construye la ruta completa al archivo de imagen. const filePath = join(picturesPath, fileName); + + // Escribe el archivo de imagen en el disco. await writeFile(filePath, file.buffer); }); + // Devuelve la ruta de la imagen guardada. // return [file[0].originalname] return images; - - - // // Construye la ruta al directorio de imágenes. - // const picturesPath = join(__dirname, '..', '..', 'pictures'); - // // Crea un nombre de archivo único para la imagen. - // const fileName = `${Date.now()}-${file.originalname}`; - // // Construye la ruta completa al archivo de imagen. - // const filePath = join(picturesPath, fileName); - - // // Escribe el archivo de imagen en el disco. - // await writeFile(filePath, file.buffer); - - // // Devuelve la ruta de la imagen guardada. - // return `/pictures/${fileName}`; } } diff --git a/apps/uploads/inventory/1/1-ibuki-douji-sign-noise-sd.jpg b/apps/uploads/inventory/1/1-ibuki-douji-sign-noise-sd.jpg new file mode 100644 index 0000000..0dd96ce Binary files /dev/null and b/apps/uploads/inventory/1/1-ibuki-douji-sign-noise-sd.jpg differ diff --git a/apps/web/feactures/auth/actions/refresh-token-action.ts b/apps/web/feactures/auth/actions/refresh-token-action.ts index 316d5ae..34ef204 100644 --- a/apps/web/feactures/auth/actions/refresh-token-action.ts +++ b/apps/web/feactures/auth/actions/refresh-token-action.ts @@ -6,6 +6,7 @@ import { } from '../schemas/refreshToken'; export const resfreshTokenAction = async (refreshToken: RefreshTokenValue) => { + // return null // Descomentar esto evita que se tenga que borrar cache al navegador const [error, data] = await safeFetchApi( RefreshTokenResponseSchema, '/auth/refreshToken', diff --git a/apps/web/feactures/inventory/actions/actions.ts b/apps/web/feactures/inventory/actions/actions.ts index 16402e3..c17b446 100644 --- a/apps/web/feactures/inventory/actions/actions.ts +++ b/apps/web/feactures/inventory/actions/actions.ts @@ -64,7 +64,7 @@ export const getAllProducts = async (params: { sortOrder?: 'asc' | 'desc'; }) => { - const session = await auth() + // const session = await auth() const searchParams = new URLSearchParams({ page: (params.page || 1).toString(), @@ -74,7 +74,7 @@ export const getAllProducts = async (params: { ...(params.sortOrder && { sortOrder: params.sortOrder }), }) - const id = session?.user.id + // const id = session?.user.id const [error, response] = await safeFetchApi( productApiResponseSchema, @@ -152,8 +152,8 @@ export const updateUserAction2 = async (payload: InventoryTable) => { const [error, data] = await safeFetchApi( test, - `/pictures/upload`, - 'POST', + `/products/upload`, + 'PATCH', payload, ); @@ -161,7 +161,7 @@ export const updateUserAction2 = async (payload: InventoryTable) => { console.error(error); throw new Error(error?.message || 'Error al actualizar el producto'); } - // console.log(data); + console.log(data); return data; } catch (error) { diff --git a/apps/web/feactures/inventory/components/inventory/product-inventory-list.tsx b/apps/web/feactures/inventory/components/inventory/product-inventory-list.tsx index 0260407..6233dc6 100644 --- a/apps/web/feactures/inventory/components/inventory/product-inventory-list.tsx +++ b/apps/web/feactures/inventory/components/inventory/product-inventory-list.tsx @@ -27,7 +27,7 @@ export default function UsersAdminList({ const {data, isLoading} = useProductQuery(filters) - // console.log(data?.data); + console.log(data?.data); if (isLoading) { return ; diff --git a/apps/web/feactures/inventory/components/inventory/product-tables/columns.tsx b/apps/web/feactures/inventory/components/inventory/product-tables/columns.tsx index f03db52..a118e96 100644 --- a/apps/web/feactures/inventory/components/inventory/product-tables/columns.tsx +++ b/apps/web/feactures/inventory/components/inventory/product-tables/columns.tsx @@ -5,13 +5,16 @@ import { CellAction } from './cell-action'; import { InventoryTable } from '../../../schemas/inventory'; export const columns: ColumnDef[] = [ + { + accessorKey: 'userId', + header: 'ID', + }, { accessorKey: 'urlImg', header: 'img', cell: ({ row }) => { - const url = row.getValue("urlImg") as string | undefined; return ( - + ) }, }, 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 b3bc4e5..921645a 100644 --- a/apps/web/feactures/inventory/components/inventory/update-product-form.tsx +++ b/apps/web/feactures/inventory/components/inventory/update-product-form.tsx @@ -22,7 +22,7 @@ import { useUpdateUser } from "@/feactures/inventory/hooks/use-mutation"; import { editInventory, EditInventory } from '@/feactures/inventory/schemas/inventory'; // Renombrado EditInventory para claridad import { Textarea } from '@repo/shadcn/components/ui/textarea'; import {STATUS} from '@/constants/status' -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import {sizeFormate} from "@/feactures/inventory/utils/sizeFormate" import { z } from 'zod'; // Asegúrate de importar Zod @@ -58,6 +58,13 @@ export function UpdateForm({ } = useUpdateUser(); const [sizeFile, setSizeFile] = useState('0 bytes'); + const [previewUrls, setPreviewUrls] = useState([]); + + useEffect(() => { + return () => { + previewUrls.forEach(url => URL.revokeObjectURL(url)); + }; + }, [previewUrls]); const defaultformValues: EditInventory = { // Usamos el nuevo tipo aquí id: defaultValues?.id, @@ -226,7 +233,7 @@ export function UpdateForm({ render={({ field: { onChange, onBlur, name, ref } }) => ( Imagen -

Peso máximo: 5MB / {sizeFile}

+

Peso máximo: 5MB / {sizeFile} (Máximo 10 archivos)

size += element.size) - const tamañoFormateado = sizeFormate(size) + const newPreviewUrls: string[] = []; + + files.forEach(element => { + size += element.size; + newPreviewUrls.push(URL.createObjectURL(element)); + }); + + const tamañoFormateado = sizeFormate(size); setSizeFile(tamañoFormateado); - onChange(e.target.files); // Esto ahora pasará FileList a react-hook-form + setPreviewUrls(newPreviewUrls); + onChange(e.target.files); + } else { + setPreviewUrls([]); } }} /> + {previewUrls.length > 0 && ( +
+ {previewUrls.map((url, index) => ( + {`Preview + ))} +
+ )}
)} /> diff --git a/apps/web/feactures/inventory/schemas/inventory.ts b/apps/web/feactures/inventory/schemas/inventory.ts index 7b6a59a..6048191 100644 --- a/apps/web/feactures/inventory/schemas/inventory.ts +++ b/apps/web/feactures/inventory/schemas/inventory.ts @@ -20,6 +20,7 @@ export const product = z.object({ stock: z.number(), price: z.string(), urlImg: z.custom().optional(), + gallery: z.array(z.string()).optional(), // urlImg: z.string(), status: z.string(), userId: z.number().optional() diff --git a/apps/web/lib/auth.config.ts b/apps/web/lib/auth.config.ts index 321687c..c79aa96 100644 --- a/apps/web/lib/auth.config.ts +++ b/apps/web/lib/auth.config.ts @@ -69,7 +69,7 @@ const authConfig: NextAuthConfig = { response.type === 'UNKNOWN_ERROR') // Incluye todos los tipos de error posibles ) { // Si es un error, lánzalo. Este camino termina aquí. - throw new CredentialsSignin(response.message); + throw new CredentialsSignin( "Error en la API:" + response.message); } if (!('user' in response)) { @@ -136,7 +136,7 @@ const authConfig: NextAuthConfig = { token.refresh_token = res.tokens.refresh_token; token.refresh_expire_in = res.tokens.refresh_expire_in; } catch (error) { - console.log(error); + console.log("error: ",error); return null; } } diff --git a/apps/web/lib/fetch.api.ts b/apps/web/lib/fetch.api.ts index d117e8d..56029ea 100644 --- a/apps/web/lib/fetch.api.ts +++ b/apps/web/lib/fetch.api.ts @@ -54,7 +54,7 @@ export const safeFetchApi = async >( [{ type: string; message: string; details?: any } | null, z.infer | null] > => { try { - // console.log(url,method,data); + console.log(url,method,data); const response = await fetchApi({ method, diff --git a/apps/web/public/uploads/apple.avif b/apps/web/public/uploads/apple.avif new file mode 100644 index 0000000..60b2ba0 Binary files /dev/null and b/apps/web/public/uploads/apple.avif differ diff --git a/apps/web/public/uploads/inventory/1/1-ibuki-douji-sign-noise-sd.jpg b/apps/web/public/uploads/inventory/1/1-ibuki-douji-sign-noise-sd.jpg new file mode 100644 index 0000000..0dd96ce Binary files /dev/null and b/apps/web/public/uploads/inventory/1/1-ibuki-douji-sign-noise-sd.jpg differ diff --git a/packages/shadcn/src/components/ui/dialog.tsx b/packages/shadcn/src/components/ui/dialog.tsx index 3fac2b4..b9994f5 100644 --- a/packages/shadcn/src/components/ui/dialog.tsx +++ b/packages/shadcn/src/components/ui/dialog.tsx @@ -60,7 +60,7 @@ function DialogContent({ onInteractOutside={(e) => e.preventDefault()} onEscapeKeyDown={(e) => e.preventDefault()} className={cn( - "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg", + "bg-background max-h-[90vh] overflow-auto data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg", className )} {...props}