cambios en el crear producto y en el refresh token

This commit is contained in:
2025-08-14 15:38:10 -04:00
parent 854b31f594
commit 6a28e141a9
19 changed files with 278 additions and 400 deletions

View File

@@ -2,9 +2,6 @@
import { safeFetchApi } from '@/lib';
import { loginResponseSchema, UserFormValue } from '../schemas/login';
type LoginActionSuccess = {
message: string;
user: {

View File

@@ -1,21 +1,32 @@
'use server';
import { safeFetchApi } from '@/lib';
import { refreshApi } from '@/lib/refreshApi'; // Importa la nueva instancia
import {
RefreshTokenResponseSchema,
RefreshTokenValue,
} 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',
'POST',
refreshToken,
);
if (error) {
console.error('Error:', error);
} else {
return data;
try {
const body = {
token: refreshToken.token,
}
// Usa la nueva instancia `refreshApi`
const response = await refreshApi.patch('/auth/refresh', body);
const parsed = RefreshTokenResponseSchema.safeParse(response.data);
if (!parsed.success) {
console.error('Error de validación en la respuesta de refresh token:', {
errors: parsed.error.errors,
receivedData: response.data,
});
return null;
}
return parsed.data;
} catch (error: any) { // Captura el error para acceso a error.response
console.error('Error al renovar el token:', error.response?.data || error.message);
return null;
}
};
};

View File

@@ -117,18 +117,19 @@ export const getProductById = async (id: number) => {
return data;
};
export const createProductAction = async (payload: InventoryTable) => {
export const createProductAction = async (payload: FormData) => {
const session = await auth()
const userId = session?.user?.id
const { id, ...payloadWithoutId } = payload;
payloadWithoutId.userId = userId
if (userId) {
payload.append('userId', String(userId));
}
const [error, data] = await safeFetchApi(
productMutate,
'/products',
'POST',
payloadWithoutId,
payload,
);
if (error) {
@@ -136,7 +137,7 @@ export const createProductAction = async (payload: InventoryTable) => {
throw new Error('Error al crear el producto');
}
return payloadWithoutId;
return data;
};
export const updateUserAction2 = async (payload: InventoryTable) => {

View File

@@ -16,56 +16,83 @@ import {
SelectTrigger,
SelectValue,
} from '@repo/shadcn/select';
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 } from '@/feactures/inventory/schemas/inventory';
import {STATUS} from '@/constants/status'
import { editInventory, EditInventory } from '@/feactures/inventory/schemas/inventory';
import { Textarea } from '@repo/shadcn/textarea';
import { STATUS } from '@/constants/status'
import { useState, useEffect } from 'react';
import { sizeFormate } from "@/feactures/inventory/utils/sizeFormate"
interface CreateFormProps {
onSuccess?: () => void;
onCancel?: () => void;
defaultValues?: Partial<EditInventory>;
}
export function CreateForm({
onSuccess,
onCancel
onCancel,
}: CreateFormProps) {
const {
mutate: saveAccountingAccounts,
mutate: saveProduct,
isPending: isSaving,
isError,
} = useCreateUser();
const defaultformValues = {
const [sizeFile, setSizeFile] = useState('0 bytes');
const [previewUrls, setPreviewUrls] = useState<string[]>([]);
useEffect(() => {
return () => {
previewUrls.forEach(url => URL.revokeObjectURL(url));
};
}, [previewUrls]);
const defaultformValues: EditInventory = {
title: '',
description: '',
address: '',
price: '',
address: '',
status: 'BORRADOR',
stock: 0,
urlImg: '',
status: ''
}
urlImg: undefined,
};
const form = useForm<EditInventory>({
resolver: zodResolver(editInventory),
defaultValues: defaultformValues,
mode: 'onChange', // Enable real-time validation
mode: 'onChange',
});
const onSubmit = async (data: EditInventory) => {
const formData = new FormData();
saveAccountingAccounts(data, {
formData.append('title', data.title);
formData.append('description', data.description);
formData.append('price', String(data.price));
formData.append('address', data.address);
formData.append('status', data.status);
formData.append('stock', String(data.stock));
if (data.urlImg) {
for (let i = 0; i < data.urlImg.length; i++) {
const file = data.urlImg[i];
if (file) {
formData.append('urlImg', file);
}
}
}
saveProduct(formData as any, {
onSuccess: () => {
form.reset();
onSuccess?.();
},
onError: (e) => {
onError: (error) => {
console.error("Error al guardar el producto:", error);
form.setError('root', {
type: 'manual',
message: e.message,
message: error.message || 'Error al guardar el producto',
});
},
});
@@ -101,9 +128,7 @@ export function CreateForm({
<FormItem>
<FormLabel>Precio</FormLabel>
<FormControl>
<Input {...field}
// value={field.value?.toString() ?? ''}
/>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -131,7 +156,7 @@ export function CreateForm({
<FormItem className='col-span-2'>
<FormLabel>Descripción</FormLabel>
<FormControl>
<Textarea {...field} className="resize-none"/>
<Textarea {...field} className="resize-none" />
</FormControl>
<FormMessage />
</FormItem>
@@ -145,7 +170,7 @@ export function CreateForm({
<FormItem>
<FormLabel>Cantidad/Stock</FormLabel>
<FormControl>
<Input {...field} type='number' onChange={(e) => field.onChange(Number(e.target.value))}/>
<Input {...field} type='number' onChange={(e) => field.onChange(Number(e.target.value))} />
</FormControl>
<FormMessage />
</FormItem>
@@ -158,7 +183,7 @@ export function CreateForm({
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Estatus</FormLabel>
<Select onValueChange={(value) => field.onChange(value)}>
<Select value={field.value} onValueChange={(value) => field.onChange(value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Seleccione un estatus" />
</SelectTrigger>
@@ -175,19 +200,54 @@ export function CreateForm({
)}
/>
<FormField
control={form.control}
name="urlImg"
render={({ field }) => (
<FormItem>
<FormLabel>Imagen</FormLabel>
<FormControl>
<Input {...field}/>
</FormControl>
<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: 5MB / {sizeFile} <span className='text-xs text-destructive'>(Máximo 10 archivos)</span></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);
let size = 0;
const newPreviewUrls: string[] = [];
files.forEach(element => {
size += element.size;
newPreviewUrls.push(URL.createObjectURL(element));
});
const tamañoFormateado = sizeFormate(size);
setSizeFile(tamañoFormateado);
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>
)}
/>
</div>
</div>
<div className="flex justify-end gap-4">

View File

@@ -1,5 +1,4 @@
'use client';
import { DataTable } from '@repo/shadcn/table/data-table';
import { DataTableSkeleton } from '@repo/shadcn/table/data-table-skeleton';
import { columns } from './product-tables/columns';
@@ -27,7 +26,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} />;

View File

@@ -1,231 +0,0 @@
'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

@@ -15,6 +15,7 @@ import { ImageIcon } from 'lucide-react';
export function ProductList() {
const router = useRouter();
const { data: produts } = useAllProductQuery();
console.log(produts);
const handle = (id: number) => {
router.push(`/dashboard/productos/${id}`);
@@ -43,7 +44,7 @@ export function ProductList() {
<CardContent className="p-0 flex-grow">
<img
className="object-cover w-full h-full aspect-square border"
src={`http://localhost:3000/${data.urlImg}`}
src={`http://localhost:3000/uploads/inventory/${data.userId}/${data.urlImg}`}
alt=""
/>
</CardContent>

View File

@@ -6,7 +6,7 @@ import { updateUserAction, createProductAction, updateUserAction2 } from "../act
export function useCreateUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (data: EditInventory) => createProductAction(data),
mutationFn: (data: any) => createProductAction(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['product'] }),
})
return mutation