editar, modificar, ver y crear productos listo

This commit is contained in:
2025-08-27 14:52:47 -04:00
parent f050db4359
commit 2d596b93ad
20 changed files with 242 additions and 183 deletions

View File

@@ -4,14 +4,11 @@ import {
ApiResponseSchema,
InventoryTable,
productMutate,
test,
// editInventory,
productApiResponseSchema,
getProduct
getProduct,
deleteProduct
} from '../schemas/inventory';
import { auth } from '@/lib/auth';
export const getInventoryAction = async (params: {
page?: number;
limit?: number;
@@ -53,7 +50,7 @@ export const getInventoryAction = async (params: {
nextPage: null,
previousPage: null,
},
};
}
}
export const getAllProducts = async (params: {
@@ -100,9 +97,12 @@ export const getAllProducts = async (params: {
previousPage: null,
},
};
};
}
export const getProductById = async (id: number) => {
if (!id) {
return null;
}
const [error, data] = await safeFetchApi(
getProduct,
`/products/id/${id}`,
@@ -110,12 +110,15 @@ export const getProductById = async (id: number) => {
);
if (error) {
if (error.details.status === 404){
return null
}
console.error('❌ Error en la API:', error);
throw new Error(error.message);
}
return data;
};
}
export const createProductAction = async (payload: FormData) => {
const [error, data] = await safeFetchApi(
@@ -131,7 +134,7 @@ export const createProductAction = async (payload: FormData) => {
}
return data;
};
}
export const updateProductAction = async (payload: InventoryTable) => {
try {
@@ -155,13 +158,16 @@ export const updateProductAction = async (payload: InventoryTable) => {
}
export const deleteProductAction = async (id: Number) => {
if (!id) {
throw new Error('Error al eliminar el producto')
}
const [error] = await safeFetchApi(
productMutate,
deleteProduct,
`/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 producto')
return true;
}

View File

@@ -19,7 +19,7 @@ import {
import { Input } from '@repo/shadcn/input';
import { useForm } from 'react-hook-form';
import { useCreateProduct } from "@/feactures/inventory/hooks/use-mutation";
import { editInventory, EditInventory } from '@/feactures/inventory/schemas/inventory';
import { createProduct, EditInventory } from '@/feactures/inventory/schemas/inventory';
import { Textarea } from '@repo/shadcn/textarea';
import { STATUS } from '@/constants/status'
import { useState, useEffect } from 'react';
@@ -59,7 +59,7 @@ export function CreateForm({
};
const form = useForm<EditInventory>({
resolver: zodResolver(editInventory),
resolver: zodResolver(createProduct),
defaultValues: defaultformValues,
mode: 'onChange',
});

View File

@@ -8,14 +8,14 @@ import {
DialogTitle,
} from '@repo/shadcn/dialog';
// import { AccountPlan } from '@/feactures/users/schemas/account-plan.schema';
import { EditInventory, editInventory } from '../../schemas/inventory';
import { EditInventory, InventoryTable } from '../../schemas/inventory';
import { CreateForm } from './create-product-form';
import { UpdateForm } from './update-product-form';
interface ModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
defaultValues?: Partial<EditInventory>;
defaultValues?: Partial<InventoryTable>;
}
export function AccountPlanModal({

View File

@@ -9,9 +9,10 @@ import {
TooltipProvider,
TooltipTrigger,
} from '@repo/shadcn/tooltip';
import { Edit, Trash, User } from 'lucide-react';
import { Edit, Trash, Eye } from 'lucide-react';
import { InventoryTable } from '@/feactures/inventory/schemas/inventory';
import { useDeleteUser } from '@/feactures/users/hooks/use-mutation-users';
// import { useDeleteUser } from '@/feactures/users/hooks/use-mutation-users';
import { useDeleteProduct } from "@/feactures/inventory/hooks/use-mutation";
import { AccountPlanModal } from '../inventory-modal';
interface CellActionProps {
@@ -22,7 +23,7 @@ export const CellAction: React.FC<CellActionProps> = ({ data }) => {
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [edit, setEdit] = useState(false);
const { mutate: deleteUser } = useDeleteUser();
const { mutate: deleteUser } = useDeleteProduct();
const router = useRouter();
const onConfirm = async () => {
@@ -51,6 +52,23 @@ export const CellAction: React.FC<CellActionProps> = ({ data }) => {
<AccountPlanModal open={edit} onOpenChange={setEdit} defaultValues={data}/>
<div className="flex gap-1">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
onClick={() => router.push(`/dashboard/productos/${data.id}`)}
>
<Eye className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Ver</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
@@ -80,7 +98,7 @@ export const CellAction: React.FC<CellActionProps> = ({ data }) => {
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Deshabilitar</p>
<p>Eliminar</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@@ -5,16 +5,12 @@ 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 }) => {
return (
<img src={`http://localhost:3000/uploads/inventory/${row.original.userId}/${row.original.id}/${row.original.urlImg}`} alt="" width={64} height={64} className="rounded"/>
<img src={`/uploads/inventory/${row.original.userId}/${row.original.id}/${row.original.urlImg}`} alt="" width={64} height={64} className="rounded"/>
)
},
},

View File

@@ -19,7 +19,7 @@ import {
import { Input } from '@repo/shadcn/input';
import { useForm } from 'react-hook-form';
import { useUpdateProduct } from "@/feactures/inventory/hooks/use-mutation";
import { editInventory, EditInventory } from '@/feactures/inventory/schemas/inventory'; // Renombrado EditInventory para claridad
import { updateInventory, EditInventory, InventoryTable } from '@/feactures/inventory/schemas/inventory'; // Renombrado EditInventory para claridad
import { Textarea } from '@repo/shadcn/components/ui/textarea';
import {STATUS} from '@/constants/status'
import { useState, useEffect } from 'react';
@@ -43,7 +43,7 @@ import { z } from 'zod'; // Asegúrate de importar Zod
interface UpdateFormProps {
onSuccess?: () => void;
onCancel?: () => void;
defaultValues?: Partial<EditInventory>;
defaultValues?: Partial<InventoryTable>;
}
export function UpdateForm({
@@ -78,7 +78,7 @@ export function UpdateForm({
};
const form = useForm<EditInventory>({ // Usamos el nuevo tipo aquí
resolver: zodResolver(editInventory), // Usamos el esquema extendido
resolver: zodResolver(updateInventory), // Usamos el esquema extendido
defaultValues: defaultformValues,
mode: 'onChange',
});

View File

@@ -13,8 +13,8 @@ export function UsersHeader() {
<>
<div className="flex items-start justify-between">
<Heading
title="Administración del inventario"
description="Gestione aquí los productos que usted registre en la plataforma"
title="Mi inventario"
description="Gestione aquí los productos que registre en la plataforma"
/>
<Button onClick={() => setOpen(true)} size="sm">
<Plus className="h-4 w-4" /><span className='hidden md:inline'>Agregar Producto</span>

View File

@@ -10,12 +10,12 @@ import {
import { useRouter } from 'next/navigation';
import { useAllProductQuery } from '@/feactures/inventory/hooks/use-query-products';
import { allProducts } from '../../schemas/inventory';
import { ImageIcon } from 'lucide-react';
// import { ImageIcon } from 'lucide-react';
export function ProductList() {
const router = useRouter();
const { data: produts } = useAllProductQuery();
console.log(produts);
// console.log(produts);
const handle = (id: number) => {
router.push(`/dashboard/productos/${id}`);
@@ -36,15 +36,15 @@ export function ProductList() {
className="cursor-pointer flex flex-col"
onClick={() => handle(Number(data.id))}
>
<CardHeader>
<CardTitle className="text-base font-bold truncate">
{/* <CardHeader> */}
<CardTitle className="text-base font-bold truncate p-3 text-primary">
{data.title.charAt(0).toUpperCase() + data.title.slice(1)}
</CardTitle>
</CardHeader>
{/* </CardHeader> */}
<CardContent className="p-0 flex-grow">
<img
className="object-cover w-full h-full aspect-square border"
src={`http://localhost:3000/uploads/inventory/${data.userId}/${data.id}/${data.urlImg}`}
src={`/uploads/inventory/${data.userId}/${data.id}/${data.urlImg}`}
alt=""
/>
</CardContent>

View File

@@ -1,4 +1,6 @@
import { allProducts, } from "../../schemas/inventory";
'use client';
import { useState } from "react";
import { allProducts } from "../../schemas/inventory";
import {
Card,
CardContent,
@@ -8,6 +10,9 @@ import {
} from '@repo/shadcn/card';
export function ProductList({product}: {product: allProducts}) {
const [selectedImg, setSelectedImg] = useState(`/uploads/inventory/${product.userId}/${product.id}/${product.urlImg}`)
console.log(product);
return (
// <PageContainer>
<main className='px-4 lg:px-6 flex flex-col md:flex-row gap-3 lg:gap-4 md:relative'>
@@ -15,44 +20,60 @@ return (
<img
className="border-2 object-contain w-full f-full min-h-[400px] md:h-[70vh] aspect-square rounded-2xl"
src={`http://localhost:3000/uploads/inventory/${product.userId}/${product.id}/${product.urlImg}`}
src={selectedImg}
alt=""
/>
<section className=''>
<img
className="border-2 object-cover w-[64px] h-[64px] md:w-[96px] md:h-[96px] aspect-square rounded-2xl"
src={`http://localhost:3000/uploads/inventory/${product.userId}/${product.id}/${product.urlImg}`}
<section className="relative flex flex-row flex-nowrap overflow-auto gap-1 md:gap-2 p-2">
{/* <span className="sticky left-0 flex items-center">
<span className="text-xl p-3 cursor-pointer bg-neutral-800/50 rounded-full text-white">
{"<"}
</span>
</span> */}
{product.gallery?.map((img, index) => (
<img
key={index}
className="cursor-pointer border-2 object-cover w-[64px] h-[64px] md:w-[96px] md:h-[96px] aspect-square rounded-2xl"
src={`/uploads/inventory/${product.userId}/${product.id}/${img}`}
alt=""
/>
onClick={() => setSelectedImg(`/uploads/inventory/${product.userId}/${product.id}/${img}`)}
/>
))}
{/* <div className="sticky right-0 flex items-center">
<span className="text-xl p-3 cursor-pointer bg-neutral-800/50 rounded-full text-white">
{">"}
</span>
</div> */}
</section>
</div>
<Card className="flex flex-col md:w-[400px] lg:w-[500px] min-h-[400px] md:h-[85vh] md:overflow-auto md:sticky top-0 right-0">
<CardHeader className='py-2 px-2 md:px-4 lg:px-6'>
<CardTitle className="font-bold text-2xl">
<CardTitle className="font-bold text-2xl text-primary">
{product.title.charAt(0).toUpperCase() + product.title.slice(1)}
</CardTitle>
<p className='font-semibold'>$ {product.price} </p>
{product.status === 'AGOTADO' ? (
<p className="font-semibold text-lg text-red-900">AGOTADO</p>
) : ('')}
<p className='font-semibold'>{product.price}$
{product.status === 'AGOTADO' ? (
<span className="font-semibold text-lg text-red-900"> AGOTADO</span>
) : ('')}
</p>
</CardHeader>
<CardContent className="py-0 px-2 md:px-4 lg:px-6 flex-col justify-between flex-grow md:overflow-auto">
<div>
<p className='font-semibold text-lg border-t border-b'> Descripción</p>
<CardContent className="py-0 px-2 h-full flex flex-col justify-around flex-grow md:px-4 md:overflow-auto lg:px-6">
<section>
<p className='font-semibold text-lg border-t border-b'> Descripción</p>
<p className='p-1'>{product.description}</p>
{/* <p className='p-1'>{lorem+lorem+lorem+lorem}</p> */}
</div>
</section>
<div className='mt-2'>
<p className='font-semibold text-lg border-t border-b'> Dirección</p>
<section>
<p className='font-semibold text-lg border-t border-b'> Dirección</p>
<p className='p-1'>{product.address}</p>
</div>
</section>
</CardContent>
<CardFooter className="px-2 md:px-4 lg:px-6">
<div>
<p className='font-semibold text-lg border-t border-b mt-4'>Información del vendedor</p>
<p className='font-semibold text-lg border-t border-b mt-4'> Información del vendedor</p>
<p>{product.fullname}</p>
<p>{product.phone}</p>
<p>{product.email}</p>

View File

@@ -1,6 +1,6 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
// import { EditInventory } from "../schemas/inventory";
import { updateProductAction, createProductAction, } from "../actions/actions";
import { updateProductAction, createProductAction,deleteProductAction } from "../actions/actions";
// Create mutation
export function useCreateProduct() {
@@ -25,11 +25,11 @@ export function useUpdateProduct() {
}
// 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)
// })
// }
export function useDeleteProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => deleteProductAction(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['product'] }),
onError: (e) => console.error('Error:', e)
})
}

View File

@@ -4,7 +4,8 @@ import { url } from 'inspector';
import { z } from 'zod';
export type InventoryTable = z.infer<typeof seeProduct>;
export type EditInventory = z.infer<typeof editInventory>;
export type EditInventory = z.infer<typeof updateInventory>;
export type CreateInventory = z.infer<typeof createProduct>;
export type ProductApiResponseSchema = z.infer<typeof productApiResponseSchema>;
export type allProducts = z.infer<typeof productDetails>;
@@ -35,19 +36,58 @@ export const productDetails = seeProduct.extend({
email: z.string().email().nullable()
})
export const editInventory = z.object({
const validateProduct = 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.number(),
address: z.string().min(5, { message: "Debe de tener 5 o más caracteres" }),
price: z.string(),
price: z.string().min(1, { message: "Debe de tener 1 o más caracteres" }),
urlImg: z.custom<FileList | undefined>(),
status: z.string().min(1, { message: "Debe de seleccionar un valor" }),
userId: z.number().optional(),
})
export const updateInventory = validateProduct.extend({
urlImg: z.custom<FileList | undefined>()
.refine((files) => (files && files.length <= 10) || files === undefined, "Máximo 10 imágenes")
.refine((files) =>
// (files && Array.from(files).every(file => file.size <= MAX_FILE_SIZE)) || files === undefined
{
if (files) {
let size = 0;
Array.from(files).map(file => {
size += file.size;
})
if (size <= MAX_FILE_SIZE) return true;
return false
}
return true
}
,
`El tamaño máximo entre toda las imagenes es de 5MB`
).refine((files) =>
(files && Array.from(files).every(file => ACCEPTED_IMAGE_TYPES.includes(file.type))) || files === undefined,
"Solo se aceptan archivos .jpg, .jpeg, .png y .webp"
).refine((files) =>
(files && Array.from(files).every(file => file.name.length <= MAX_FILENAME_LENGTH)) || files === undefined,
`El nombre de cada archivo no puede superar los ${MAX_FILENAME_LENGTH} caracteres`
),
})
export const createProduct = validateProduct.extend({
urlImg: z.custom<FileList | undefined>()
.refine((files) => files && files.length > 0, "Se requiere al menos una imagen")
.refine((files) => files && files.length <= 10, "Máximo 10 imágenes")
.refine((files) =>
files && Array.from(files).every(file => file.size <= MAX_FILE_SIZE),
`El tamaño máximo de cada imagen es de 5MB`
.refine((files) => {
let size = 0;
if (files) Array.from(files).map(file => {
size += file.size;
})
if (size <= MAX_FILE_SIZE) return true;
return false
},
`El tamaño máximo entre toda las imagenes es de 5MB`
).refine((files) =>
files && Array.from(files).every(file => ACCEPTED_IMAGE_TYPES.includes(file.type)),
"Solo se aceptan archivos .jpg, .jpeg, .png y .webp"
@@ -55,13 +95,11 @@ export const editInventory = z.object({
files && Array.from(files).every(file => file.name.length <= MAX_FILENAME_LENGTH),
`El nombre de cada archivo no puede superar los ${MAX_FILENAME_LENGTH} caracteres`
),
status: z.string().min(1, { message: "Debe de seleccionar un valor" }),
userId: z.number().optional(),
})
export const ApiResponseSchema = z.object({
message: z.string(),
data: z.array(product),
data: z.array(seeProduct),
meta: z.object({
page: z.number(),
limit: z.number(),
@@ -89,11 +127,6 @@ 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: seeProduct,
@@ -102,4 +135,8 @@ export const productMutate = z.object({
export const getProduct = z.object({
message: z.string(),
data: productDetails,
})
export const deleteProduct = z.object({
message: z.string(),
})