recibir la imagen en la api corectamente

This commit is contained in:
2025-07-30 13:47:13 -04:00
parent a15505ff2c
commit 339ce85e46
13 changed files with 493 additions and 37 deletions

View File

@@ -29,7 +29,7 @@ export class UsersController {
@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)
// console.log(req['user'].id)
// const id = 1
const id = Number(req['user'].id);
const result = await this.inventoryService.findAllByUserId(id,paginationDto);

View File

@@ -8,8 +8,14 @@ export class PicturesController {
constructor(private readonly picturesService: PicturesService) {}
@Post('upload')
@UseInterceptors(FilesInterceptor('files'))
@UseInterceptors(FilesInterceptor('urlImg'))
async uploadFile(@UploadedFiles() files: Express.Multer.File[]) {
return this.picturesService.saveImages(files);
console.log(files);
// const result = await this.picturesService.saveImages(files);
// console.log(result);
return {data: ["result"]}
}
}

View File

@@ -12,18 +12,24 @@ export class PicturesService {
*/
async saveImages(file: Express.Multer.File[]): Promise<string[]> {
const picturesPath = join(__dirname, '..', '..', 'pictures');
const picturesPath = join(__dirname, '..', '..', '..', '..', 'uploads','pict');
let images : string[] = [];
console.log(file);
file.forEach(async (pic) => {
const fileName = `${Date.now()}-${pic.originalname}`;
const filePath = join(picturesPath, fileName);
await writeFile(filePath, pic.buffer);
images.push(`/pictures/${fileName}`);
});
let count = 0;
// file.forEach(async (file) => {
// // count++
// // const fileName = `${Date.now()}-${count++}-${file.originalname}`;
// // console.log(fileName);
// // const filePath = join(picturesPath, fileName);
// // await writeFile(filePath, file.buffer);
// // images.push(fileName);
// });
// return [file[0].originalname]
return images;

View File

@@ -4,6 +4,7 @@ import {
ApiResponseSchema,
InventoryTable,
productMutate,
test,
// editInventory,
productApiResponseSchema,
getProduct
@@ -138,6 +139,36 @@ export const createProductAction = async (payload: InventoryTable) => {
return payloadWithoutId;
};
export const updateUserAction2 = async (payload: InventoryTable) => {
try {
// const { id, urlImg, ...payloadWithoutId } = payload;
// const formData = new FormData();
// formData.append('file', urlImg);
// console.log(formData);
const [error, data] = await safeFetchApi(
test,
`/pictures/upload`,
'POST',
payload,
);
if (error) {
console.error(error);
throw new Error(error?.message || 'Error al actualizar el producto');
}
// console.log(data);
return data;
} catch (error) {
console.error(error);
}
}
export const updateUserAction = async (payload: InventoryTable) => {
try {
const { id, ...payloadWithoutId } = payload;

View File

@@ -0,0 +1,231 @@
'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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@repo/shadcn/select';
import { Input } from '@repo/shadcn/input';
import { useForm } from 'react-hook-form';
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 {sizeFormate} from "@/feactures/inventory/utils/sizeFormate"
interface UpdateFormProps {
onSuccess?: () => void;
onCancel?: () => void;
defaultValues?: Partial<EditInventory>;
}
export function UpdateForm({
onSuccess,
onCancel,
defaultValues,
}: UpdateFormProps) {
const {
mutate: saveAccountingAccounts,
isPending: isSaving,
isError, // isError no se usa en el template, podrías usarlo para mostrar un mensaje global
} = useUpdateUser();
const [sizeFile, setSizeFile] = useState('0 bytes');
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 ?? 0,
urlImg: [],
userId: defaultValues?.userId
};
const form = useForm<EditInventory>({
resolver: zodResolver(editInventory),
defaultValues: defaultformValues,
mode: 'onChange', // Enable real-time validation
});
const onSubmit = async (data: EditInventory) => {
// console.log(data);
saveAccountingAccounts(data, {
onSuccess: () => {
form.reset();
onSuccess?.();
},
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.message || 'Error al guardar el producto', // Mejor mensaje de error
});
},
});
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{form.formState.errors.root && (
<div className="text-destructive text-sm">
{form.formState.errors.root.message}
</div>
)}
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Nombre/Título</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="price"
render={({ field }) => (
<FormItem >
<FormLabel>Precio</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="address"
render={({ field }) => (
<FormItem className='col-span-2'>
<FormLabel>Dirección</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className='col-span-2'>
<FormLabel>Descripción</FormLabel>
<FormControl>
<Textarea {...field} className="resize-none"/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="stock"
render={({ field }) => (
<FormItem>
<FormLabel>Cantidad/Stock</FormLabel>
<FormControl>
<Input {...field} type="number" onChange={(e) => field.onChange(Number(e.target.value))}/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="status"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Estatus</FormLabel>
<Select value={field.value} onValueChange={(value) => field.onChange(value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Seleccione un estatus" />
</SelectTrigger>
<SelectContent>
{Object.entries(STATUS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="col-span-2">
<FormField
control={form.control}
name="urlImg"
render={({ field: { onChange, onBlur, name, ref } }) => (
<FormItem>
<FormLabel>Imagen</FormLabel>
<p>Peso máximo: 2MB / {sizeFile}</p>
<FormControl>
<Input
type="file"
multiple
onBlur={onBlur}
name={name}
ref={ref}
onChange={(e) => {
if (e.target.files) {
const files = Array.from(e.target.files);
// Calcular el tamaño total de los archivos
let size = 0;
files.forEach(element => size += element.size)
const tamañoFormateado = sizeFormate(size)
setSizeFile(tamañoFormateado);
}
onChange(e.target.files);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<div className="flex justify-end gap-4">
<Button variant="outline" type="button" onClick={onCancel}>
Cancelar
</Button>
<Button type="submit" disabled={isSaving}>
{isSaving ? 'Guardando...' : 'Guardar'}
</Button>
</div>
</form>
</Form>
);
}

View File

@@ -22,6 +22,23 @@ 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 {sizeFormate} from "@/feactures/inventory/utils/sizeFormate"
import { z } from 'zod'; // Asegúrate de importar Zod
// --- MODIFICACIÓN CLAVE ---
// Extiende tu esquema para incluir el campo de imagen como FileList para el frontend
// En el esquema Zod principal (editInventory), urlImg puede seguir siendo string[] si es lo que guardas en DB.
// Pero para la validación del formulario temporalmente, necesitamos manejar FileList.
// Si tu EditInventory original no contempla FileList, crea un esquema para el formulario.
// Ejemplo de cómo podrías adaptar tu esquema para el formulario
const formSchemaWithFiles = editInventory.extend({
urlImg: z.custom<FileList | undefined | null>().optional(), // Ahora permite FileList para el input file
});
// Define un tipo para los datos del formulario que incluye el FileList
type FormDataWithFiles = z.infer<typeof formSchemaWithFiles>;
interface UpdateFormProps {
onSuccess?: () => void;
@@ -37,10 +54,12 @@ export function UpdateForm({
const {
mutate: saveAccountingAccounts,
isPending: isSaving,
isError, // isError no se usa en el template, podrías usarlo para mostrar un mensaje global
isError,
} = useUpdateUser();
const defaultformValues: EditInventory = {
const [sizeFile, setSizeFile] = useState('0 bytes');
const defaultformValues: FormDataWithFiles = { // Usamos el nuevo tipo aquí
id: defaultValues?.id,
title: defaultValues?.title || '',
description: defaultValues?.description || '',
@@ -48,29 +67,52 @@ export function UpdateForm({
address: defaultValues?.address || '',
status: defaultValues?.status || 'BORRADOR',
stock: defaultValues?.stock ?? 0,
urlImg: [],
urlImg: undefined, // Inicializamos como undefined o null para el FileList
userId: defaultValues?.userId
};
const form = useForm<EditInventory>({
resolver: zodResolver(editInventory),
const form = useForm<FormDataWithFiles>({ // Usamos el nuevo tipo aquí
resolver: zodResolver(formSchemaWithFiles), // Usamos el esquema extendido
defaultValues: defaultformValues,
mode: 'onChange', // Enable real-time validation
mode: 'onChange',
});
const onSubmit = async (data: EditInventory) => {
console.log(data);
const onSubmit = async (data: FormDataWithFiles) => {
// --- MODIFICACIÓN CLAVE: Crear FormData ---
const formData = new FormData();
saveAccountingAccounts(data, {
// Añadir otros campos de texto al FormData
// Asegúrate de que los nombres coincidan con lo que tu backend espera
formData.append('id', data.id ? String(data.id) : ''); // Los IDs a menudo son numéricos, conviértelos a string
formData.append('title', data.title);
formData.append('description', data.description);
formData.append('price', String(data.price)); // Convertir a string
formData.append('address', data.address);
formData.append('status', data.status);
formData.append('stock', String(data.stock)); // Convertir a string
formData.append('userId', data.userId ? String(data.userId) : '');
// Añadir los archivos al FormData
if (data.urlImg && data.urlImg.length > 0) {
// Importante: El nombre del campo 'files' debe coincidir con el interceptor de NestJS (FilesInterceptor('files'))
for (let i = 0; i < data.urlImg.length; i++) {
formData.append('urlImg', data.urlImg[i]); // Asegúrate de que el nombre del campo sea 'files'
}
}
// --- IMPORTANTE: Tu hook `useUpdateUser` DEBE ser capaz de aceptar FormData ---
// Si `useUpdateUser` llama a `safeFetchApi`, entonces `safeFetchApi` ya está preparado
// para recibir `FormData`.
saveAccountingAccounts(formData as any, { // Forzamos el tipo a 'any' si `useUpdateUser` no espera FormData en su tipo
onSuccess: () => {
form.reset();
onSuccess?.();
},
onError: (error) => { // Captura el error para mostrar un mensaje más específico si es posible
onError: (error) => {
console.error("Error al guardar el producto:", error);
form.setError('root', {
type: 'manual',
message: error.message || 'Error al guardar el producto', // Mejor mensaje de error
message: error.message || 'Error al guardar el producto',
});
},
});
@@ -185,6 +227,7 @@ export function UpdateForm({
render={({ field: { onChange, onBlur, name, ref } }) => (
<FormItem>
<FormLabel>Imagen</FormLabel>
<p>Peso máximo: 2MB / {sizeFile}</p>
<FormControl>
<Input
type="file"
@@ -193,7 +236,14 @@ export function UpdateForm({
name={name}
ref={ref}
onChange={(e) => {
onChange(e.target.files);
if (e.target.files) {
const files = Array.from(e.target.files);
let size = 0;
files.forEach(element => size += element.size)
const tamañoFormateado = sizeFormate(size)
setSizeFile(tamañoFormateado);
onChange(e.target.files); // Esto ahora pasará FileList a react-hook-form
}
}}
/>
</FormControl>

View File

@@ -1,6 +1,6 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { EditInventory } from "../schemas/inventory";
import { updateUserAction, createProductAction } from "../actions/actions";
import { updateUserAction, createProductAction, updateUserAction2 } from "../actions/actions";
// Create mutation
export function useCreateUser() {
@@ -8,7 +8,6 @@ export function useCreateUser() {
const mutation = useMutation({
mutationFn: (data: EditInventory) => createProductAction(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['product'] }),
// onError: (e) => console.error('Error:', e),
})
return mutation
}
@@ -17,7 +16,8 @@ export function useCreateUser() {
export function useUpdateUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (data: EditInventory) => updateUserAction(data),
// mutationFn: (data: EditInventory) => updateUserAction(data),
mutationFn: (data: any) => updateUserAction2(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['product'] }),
onError: (e) => console.error('Error:', e)
})

View File

@@ -106,6 +106,11 @@ export const productApiResponseSchema = z.object({
}),
})
export const test = z.object({
// message: z.string(),
data: z.array(z.string()),
})
export const productMutate = z.object({
message: z.string(),
data: product,

View File

@@ -0,0 +1,13 @@
export const sizeFormate = (size: number) => {
let tamañoFormateado = '';
if (size < 1024) {
tamañoFormateado = size + ' bytes';
} else if (size < 1024 * 1024) {
tamañoFormateado = (size / 1024).toFixed(2) + ' KB';
} else if (size < 1024 * 1024 * 1024) {
tamañoFormateado = (size / (1024 * 1024)).toFixed(2) + ' MB';
} else {
tamañoFormateado = (size / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
}
return tamañoFormateado;
}

View File

@@ -6,9 +6,7 @@ import { z } from 'zod';
// Crear instancia de Axios con la URL base validada
const fetchApi = axios.create({
baseURL: env.API_URL, // Aquí usamos env.API_URL en vez de process.env.BACKEND_URL
headers: {
'Content-Type': 'application/json',
},
// No establecer Content-Type aquí. Axios lo manejará automáticamente con FormData.
});
// Interceptor para incluir el token automáticamente en las peticiones
@@ -22,6 +20,16 @@ fetchApi.interceptors.request.use(async (config: any) => {
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// **Importante:** Si el body es FormData, elimina el Content-Type para que Axios lo configure automáticamente.
// Esto es necesario porque 'multipart/form-data' requiere un 'boundary' que Axios añade.
if (config.data instanceof FormData) {
delete config.headers['Content-Type'];
} else {
// Para otros tipos de datos, asegura que el Content-Type sea 'application/json'
config.headers['Content-Type'] = 'application/json';
}
} catch (error) {
console.error('Error getting auth token:', error);
}
@@ -33,22 +41,25 @@ fetchApi.interceptors.request.use(async (config: any) => {
* Función para hacer peticiones con validación de respuesta
* @param schema - Esquema de Zod para validar la respuesta
* @param url - Endpoint a consultar
* @param config - Configuración opcional de Axios
* @param method - Método HTTP (GET, POST, PUT, PATCH, DELETE)
* @param data - Datos a enviar (puede ser un objeto JSON o FormData para archivos)
* @returns [error, data] -> Devuelve el error como objeto estructurado si hay fallo, o los datos validados
*/
export const safeFetchApi = async <T extends z.ZodSchema<any>>(
schema: T,
url: string,
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'GET',
body?: any,
data?: any, // Renombrado a 'data' para mayor claridad y consistencia con Axios
): Promise<
[{ type: string; message: string; details?: any } | null, z.infer<T> | null]
> => {
try {
console.log(url,method,data);
const response = await fetchApi({
method,
url,
data: body,
data, // Axios usa 'data' para el body de POST/PUT/PATCH
});
const parsed = schema.safeParse(response.data);
@@ -60,7 +71,6 @@ export const safeFetchApi = async <T extends z.ZodSchema<any>>(
expectedSchema: schema,
data: response.data.data,
});
// console.error(parsed.error.errors)
return [
{
type: 'VALIDATION_ERROR',
@@ -84,7 +94,6 @@ export const safeFetchApi = async <T extends z.ZodSchema<any>>(
headers: error.config?.headers,
};
// console.log(error)
return [
{
type: 'API_ERROR',

View File

@@ -0,0 +1,99 @@
'use server';
import { env } from '@/lib/env'; // Importamos la configuración de entorno validada
import axios from 'axios';
import { z } from 'zod';
// Crear instancia de Axios con la URL base validada
const fetchApi = axios.create({
baseURL: env.API_URL, // Aquí usamos env.API_URL en vez de process.env.BACKEND_URL
headers: {
'Content-Type': 'application/json',
},
});
// Interceptor para incluir el token automáticamente en las peticiones
fetchApi.interceptors.request.use(async (config: any) => {
try {
// Importación dinámica para evitar la referencia circular
const { auth } = await import('@/lib/auth');
const session = await auth();
const token = session?.access_token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
} catch (error) {
console.error('Error getting auth token:', error);
}
return config;
});
/**
* Función para hacer peticiones con validación de respuesta
* @param schema - Esquema de Zod para validar la respuesta
* @param url - Endpoint a consultar
* @param config - Configuración opcional de Axios
* @returns [error, data] -> Devuelve el error como objeto estructurado si hay fallo, o los datos validados
*/
export const safeFetchApi = async <T extends z.ZodSchema<any>>(
schema: T,
url: string,
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'GET',
body?: any,
): Promise<
[{ type: string; message: string; details?: any } | null, z.infer<T> | null]
> => {
try {
const response = await fetchApi({
method,
url,
data: body,
});
const parsed = schema.safeParse(response.data);
if (!parsed.success) {
console.error('Validation Error Details:', {
errors: parsed.error.errors,
receivedData: response.data,
expectedSchema: schema,
data: response.data.data,
});
// console.error(parsed.error.errors)
return [
{
type: 'VALIDATION_ERROR',
message: 'Validation error',
details: parsed.error.errors,
},
null,
];
}
return [null, parsed.data];
} catch (error: any) {
const errorDetails = {
status: error.response?.status,
statusText: error.response?.statusText,
message: error.message,
url: error.config?.url,
method: error.config?.method,
requestData: error.config?.data,
responseData: error.response?.data,
headers: error.config?.headers,
};
// console.log(error)
return [
{
type: 'API_ERROR',
message: error.response?.data?.message || 'Unknown API error',
details: errorDetails,
},
null,
];
}
};
export { fetchApi };

View File

@@ -1,4 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
const nextConfig = {
experimental: {
serverActions: {
bodySizeLimit: '2mb',
},
}
};
export default nextConfig;