Guardar y cambiar img al editar productos
This commit is contained in:
@@ -17,7 +17,7 @@ export async function seedProducts(db: NodePgDatabase<typeof schema>) {
|
||||
status:'PUBLICADO', // PUBLICADO, AGOTADO, BORRADOR
|
||||
urlImg:'apple.avif',
|
||||
userId:1,
|
||||
gallery: ["Pruebas"]
|
||||
gallery: ["Pruebas.png","Pruebas2.png"]
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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';
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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<Product> {
|
||||
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<Product> {
|
||||
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);
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export class PicturesService {
|
||||
* @returns La ruta de la imagen guardada.
|
||||
*/
|
||||
async saveImages(file: Express.Multer.File[]): Promise<string[]> {
|
||||
// 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}`;
|
||||
}
|
||||
}
|
||||
|
||||
BIN
apps/uploads/inventory/1/1-ibuki-douji-sign-noise-sd.jpg
Normal file
BIN
apps/uploads/inventory/1/1-ibuki-douji-sign-noise-sd.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 786 KiB |
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function UsersAdminList({
|
||||
|
||||
const {data, isLoading} = useProductQuery(filters)
|
||||
|
||||
// console.log(data?.data);
|
||||
console.log(data?.data);
|
||||
|
||||
if (isLoading) {
|
||||
return <DataTableSkeleton columnCount={6} rowCount={initialLimit} />;
|
||||
|
||||
@@ -5,13 +5,16 @@ import { CellAction } from './cell-action';
|
||||
import { InventoryTable } from '../../../schemas/inventory';
|
||||
|
||||
export const columns: ColumnDef<InventoryTable>[] = [
|
||||
{
|
||||
accessorKey: 'userId',
|
||||
header: 'ID',
|
||||
},
|
||||
{
|
||||
accessorKey: 'urlImg',
|
||||
header: 'img',
|
||||
cell: ({ row }) => {
|
||||
const url = row.getValue("urlImg") as string | undefined;
|
||||
return (
|
||||
<img src={`http://localhost:3000/${url}`} alt="" width={64} height={64} className="rounded"/>
|
||||
<img src={`http://localhost:3000/uploads/inventory/${row.original.userId}/${row.original.urlImg}`} alt="" width={64} height={64} className="rounded"/>
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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<string[]>([]);
|
||||
|
||||
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 } }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Imagen</FormLabel>
|
||||
<p>Peso máximo: 5MB / {sizeFile}</p>
|
||||
<p>Peso máximo: 5MB / {sizeFile} <span className='text-xs text-destructive'>(Máximo 10 archivos)</span></p>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="file"
|
||||
@@ -238,15 +245,31 @@ export function UpdateForm({
|
||||
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)
|
||||
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([]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
{previewUrls.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{previewUrls.map((url, index) => (
|
||||
<img key={index} src={url} alt={`Preview ${index}`} className="w-24 h-24 object-cover rounded-md" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -20,6 +20,7 @@ export const product = z.object({
|
||||
stock: z.number(),
|
||||
price: z.string(),
|
||||
urlImg: z.custom<FileList | undefined>().optional(),
|
||||
gallery: z.array(z.string()).optional(),
|
||||
// urlImg: z.string(),
|
||||
status: z.string(),
|
||||
userId: z.number().optional()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export const safeFetchApi = async <T extends z.ZodSchema<any>>(
|
||||
[{ type: string; message: string; details?: any } | null, z.infer<T> | null]
|
||||
> => {
|
||||
try {
|
||||
// console.log(url,method,data);
|
||||
console.log(url,method,data);
|
||||
|
||||
const response = await fetchApi({
|
||||
method,
|
||||
|
||||
BIN
apps/web/public/uploads/apple.avif
Normal file
BIN
apps/web/public/uploads/apple.avif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 786 KiB |
Reference in New Issue
Block a user