diff --git a/apps/api/package.json b/apps/api/package.json index 712b522..8cbcf80 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -37,19 +37,19 @@ } }, "dependencies": { - "@nestjs/common": "^11.0.0", - "@nestjs/core": "^11.0.0", - "@nestjs/platform-express": "^11.0.0", - "dotenv": "^16.5.0", - "drizzle-orm": "^0.40.0", - "express": "^5.1.0", - "joi": "^17.13.3", - "moment": "^2.30.1", - "path-to-regexp": "^8.2.0", - "pg": "^8.13.3", - "pino-pretty": "^13.0.0", - "reflect-metadata": "^0.2.0", - "rxjs": "^7.8.1" + "@nestjs/common": "11.0.0", + "@nestjs/core": "11.0.0", + "@nestjs/platform-express": "11.0.0", + "dotenv": "16.5.0", + "drizzle-orm": "0.40.0", + "express": "5.1.0", + "joi": "17.13.3", + "moment": "2.30.1", + "path-to-regexp": "8.2.0", + "pg": "8.13.3", + "pino-pretty": "13.0.0", + "reflect-metadata": "0.2.0", + "rxjs": "7.8.1" }, "devDependencies": { "@nestjs-modules/mailer": "^2.0.2", diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index d9c59be..e73e01d 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -19,6 +19,7 @@ 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' +import { PicturesModule } from './features/pictures/pictures.module'; @Module({ providers: [ @@ -59,7 +60,8 @@ import {InventoryModule} from './features/inventory/inventory.module' ConfigurationsModule, SurveysModule, LocationModule, - InventoryModule + InventoryModule, + PicturesModule ], }) export class AppModule {} diff --git a/apps/api/src/features/pictures/pictures.controller.ts b/apps/api/src/features/pictures/pictures.controller.ts new file mode 100644 index 0000000..7e68276 --- /dev/null +++ b/apps/api/src/features/pictures/pictures.controller.ts @@ -0,0 +1,15 @@ + +import { Controller, Post, UploadedFiles, UseInterceptors } from '@nestjs/common'; +import { FilesInterceptor } from '@nestjs/platform-express'; +import { PicturesService } from './pictures.service'; + +@Controller('pictures') +export class PicturesController { + constructor(private readonly picturesService: PicturesService) {} + + @Post('upload') + @UseInterceptors(FilesInterceptor('files')) + async uploadFile(@UploadedFiles() files: Express.Multer.File[]) { + return this.picturesService.saveImages(files); + } +} diff --git a/apps/api/src/features/pictures/pictures.module.ts b/apps/api/src/features/pictures/pictures.module.ts new file mode 100644 index 0000000..274177c --- /dev/null +++ b/apps/api/src/features/pictures/pictures.module.ts @@ -0,0 +1,10 @@ + +import { Module } from '@nestjs/common'; +import { PicturesController } from './pictures.controller'; +import { PicturesService } from './pictures.service'; + +@Module({ + controllers: [PicturesController], + providers: [PicturesService], +}) +export class PicturesModule {} diff --git a/apps/api/src/features/pictures/pictures.service.ts b/apps/api/src/features/pictures/pictures.service.ts new file mode 100644 index 0000000..ade96fc --- /dev/null +++ b/apps/api/src/features/pictures/pictures.service.ts @@ -0,0 +1,43 @@ + +import { Injectable } from '@nestjs/common'; +import { writeFile } from 'fs/promises'; +import { join } from 'path'; + +@Injectable() +export class PicturesService { + /** + * 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[]): Promise { + + const picturesPath = join(__dirname, '..', '..', 'pictures'); + + let images : string[] = []; + + + file.forEach(async (pic) => { + const fileName = `${Date.now()}-${pic.originalname}`; + const filePath = join(picturesPath, fileName); + await writeFile(filePath, pic.buffer); + images.push(`/pictures/${fileName}`); + }); + + 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/web/feactures/inventory/components/inventory/create-product-form.tsx b/apps/web/feactures/inventory/components/inventory/create-product-form.tsx index 712663e..596687c 100644 --- a/apps/web/feactures/inventory/components/inventory/create-product-form.tsx +++ b/apps/web/feactures/inventory/components/inventory/create-product-form.tsx @@ -20,7 +20,7 @@ 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"; -import { EditInventory, editInventory, formDataInput } from '@/feactures/inventory/schemas/inventory'; +import { EditInventory, editInventory } from '@/feactures/inventory/schemas/inventory'; import {STATUS} from '@/constants/status' interface CreateFormProps { @@ -44,12 +44,12 @@ export function CreateForm({ description: '', address: '', price: '', - stock: '', + stock: 0, urlImg: '', status: '' } - const form = useForm({ + const form = useForm({ resolver: zodResolver(editInventory), defaultValues: defaultformValues, mode: 'onChange', // Enable real-time validation @@ -145,7 +145,7 @@ export function CreateForm({ Cantidad/Stock - + field.onChange(Number(e.target.value))}/> @@ -201,4 +201,4 @@ export function CreateForm({ ); -} +} \ No newline at end of file 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 4f65a9b..25f70a7 100644 --- a/apps/web/feactures/inventory/components/inventory/update-product-form.tsx +++ b/apps/web/feactures/inventory/components/inventory/update-product-form.tsx @@ -19,7 +19,7 @@ import { import { Input } from '@repo/shadcn/input'; import { useForm } from 'react-hook-form'; import { useUpdateUser } from "@/feactures/inventory/hooks/use-mutation"; -import { editInventory, formDataInput, EditInventory } from '@/feactures/inventory/schemas/inventory'; // Renombrado EditInventory para claridad +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' @@ -40,25 +40,25 @@ export function UpdateForm({ isError, // isError no se usa en el template, podrías usarlo para mostrar un mensaje global } = useUpdateUser(); - const defaultformValues: formDataInput = { + const defaultformValues: EditInventory = { id: defaultValues?.id, title: defaultValues?.title || '', description: defaultValues?.description || '', price: defaultValues?.price || '', address: defaultValues?.address || '', status: defaultValues?.status || 'BORRADOR', - stock: (defaultValues?.stock ?? '').toString(), - urlImg: defaultValues?.urlImg || '', + stock: defaultValues?.stock ?? 0, + urlImg: [], userId: defaultValues?.userId }; - const form = useForm({ + const form = useForm({ resolver: zodResolver(editInventory), defaultValues: defaultformValues, mode: 'onChange', // Enable real-time validation }); - const onSubmit = async (data: formDataInput) => { + const onSubmit = async (data: EditInventory) => { console.log(data); saveAccountingAccounts(data, { @@ -106,7 +106,6 @@ export function UpdateForm({ Precio - {/* Simplificado. price es z.string(), field.value ya es string o undefined. */} @@ -121,7 +120,6 @@ export function UpdateForm({ Dirección - {/* Simplificado. price es z.string(), field.value ya es string o undefined. */} @@ -150,7 +148,7 @@ export function UpdateForm({ Cantidad/Stock - + field.onChange(Number(e.target.value))}/> @@ -180,19 +178,30 @@ export function UpdateForm({ )} /> - ( - - Imagen - - - - - - )} - /> +
+ ( + + Imagen + + { + onChange(e.target.files); + }} + /> + + + + )} + /> +
diff --git a/apps/web/feactures/inventory/schemas/inventory.ts b/apps/web/feactures/inventory/schemas/inventory.ts index 16a90af..7cf7485 100644 --- a/apps/web/feactures/inventory/schemas/inventory.ts +++ b/apps/web/feactures/inventory/schemas/inventory.ts @@ -1,13 +1,38 @@ -import { user } from '@/feactures/auth/schemas/register'; -import { all } from 'axios'; +// import { user } from '@/feactures/auth/schemas/register'; +// import { all } from 'axios'; import { z } from 'zod'; export type InventoryTable = z.infer; -export type EditInventory = z.infer; //output -export type formDataInput = z.input; +export type EditInventory = z.infer; export type ProductApiResponseSchema = z.infer; export type allProducts = z.infer; +// --- Esquemas de validación para archivos --- + +// Esquema básico para un solo archivo +const fileSchema = z.object({ + name: z.string().min(1, 'El nombre del archivo no puede estar vacío.'), + size: z.number().int().positive('El tamaño del archivo debe ser positivo.'), + type: z.string().refine( + (type) => type.startsWith('image/'), + 'Solo se permiten archivos de imagen.' + ), + // Puedes añadir más validaciones personalizadas aquí + // Por ejemplo, limitar el tamaño máximo a 5MB (5 * 1024 * 1024 bytes) +}).refine( + (file) => file.size <= 5 * 1024 * 1024, + 'El tamaño del archivo no debe exceder los 5MB.' +); + +// Esquema para un array de archivos (cuando el input es multiple) +const filesArraySchema = z.array(fileSchema).max(5, 'Solo se permiten hasta 5 archivos.'); + +// const formSchema = z.object({ +// file: z +// .instanceof(FileList) +// .refine((file) => file?.length == 1, 'File is required.') +// }); + export const product = z.object({ id: z.number().optional(), title: z.string(), @@ -16,7 +41,8 @@ export const product = z.object({ // category: z.string(), stock: z.number(), price: z.string(), - urlImg: z.string(), + urlImg: z.any(), + // urlImg: z.string(), status: z.string(), userId: z.number().optional() }) @@ -31,12 +57,10 @@ 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)).pipe(z.number( - { invalid_type_error: 'El stock debe ser un número' }).min(0, { message: "El stock debe ser mayor a 0" }) - ), + stock: z.number(), address: z.string().min(5, { message: "Debe de tener 5 o más caracteres" }), price: z.string(), - urlImg: z.string(), + urlImg: z.any(), status: z.string().min(1, { message: "Debe de seleccionar un valor" }), userId: z.number().optional(), })