diff --git a/app/store/components/product-management/collection-form/config/amplifyConfig.ts b/app/store/components/product-management/collection-form/config/amplifyConfig.ts deleted file mode 100644 index e8caaad5..00000000 --- a/app/store/components/product-management/collection-form/config/amplifyConfig.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Amplify } from 'aws-amplify' -import outputs from '@/amplify_outputs.json' - -export const configureAmplify = () => { - Amplify.configure(outputs) - const existingConfig = Amplify.getConfig() - Amplify.configure({ - ...existingConfig, - API: { - ...existingConfig.API, - REST: outputs.custom.APIs, - }, - }) -} diff --git a/app/store/components/product-management/collection-form/form-page.tsx b/app/store/components/product-management/collection-form/form-page.tsx index aea2f287..6fbdc04d 100644 --- a/app/store/components/product-management/collection-form/form-page.tsx +++ b/app/store/components/product-management/collection-form/form-page.tsx @@ -5,13 +5,13 @@ import { useCollections } from '@/app/store/hooks/useCollections' import { getStoreId } from '@/utils/store-utils' import { UnsavedChangesAlert } from '@/components/ui/unsaved-changes-alert' import { useCollectionForm } from '@/app/store/components/product-management/utils/collection-form-utils' -import { configureAmplify } from '@/app/store/components/product-management/collection-form/config/amplifyConfig' + import { CollectionHeader } from '@/app/store/components/product-management/collection-form/components/CollectionHeader' import { CollectionContent } from '@/app/store/components/product-management/collection-form/components/CollectionContent' import { CollectionSidebar } from '@/app/store/components/product-management/collection-form/components/CollectionSidebar' import { CollectionFooter } from '@/app/store/components/product-management/collection-form/components/CollectionFooter' +import { configureAmplify } from '@/lib/amplify-config' -// Configure Amplify configureAmplify() export function FormPage() { diff --git a/app/store/components/product-management/collections/collections-page.tsx b/app/store/components/product-management/collections/collections-page.tsx index 3ee7a1f3..eb9259a2 100644 --- a/app/store/components/product-management/collections/collections-page.tsx +++ b/app/store/components/product-management/collections/collections-page.tsx @@ -5,8 +5,7 @@ import CollectionsHeader from '@/app/store/components/product-management/collect import CollectionsTabs from '@/app/store/components/product-management/collections/collections-tabs' import CollectionsTable from '@/app/store/components/product-management/collections/collections-table' import CollectionsFooter from '@/app/store/components/product-management/collections/collections-footer' -import { Amplify } from 'aws-amplify' -import outputs from '@/amplify_outputs.json' +import { configureAmplify } from '@/lib/amplify-config' import { useCollections } from '@/app/store/hooks/useCollections' import { Skeleton } from '@/components/ui/skeleton' import { @@ -18,15 +17,7 @@ import { TableRow, } from '@/components/ui/table' -Amplify.configure(outputs) -const existingConfig = Amplify.getConfig() -Amplify.configure({ - ...existingConfig, - API: { - ...existingConfig.API, - REST: outputs.custom.APIs, - }, -}) +configureAmplify() type FilterType = 'all' | 'active' | 'inactive' diff --git a/app/store/components/product-management/main-components/ProductForm.tsx b/app/store/components/product-management/main-components/ProductForm.tsx index a5bcca11..1fc4eff8 100644 --- a/app/store/components/product-management/main-components/ProductForm.tsx +++ b/app/store/components/product-management/main-components/ProductForm.tsx @@ -1,4 +1,6 @@ -import { useState, useEffect } from 'react' +'use client' + +import { useState, useEffect, useCallback } from 'react' import { useRouter } from 'next/navigation' import { zodResolver } from '@hookform/resolvers/zod' import { useForm } from 'react-hook-form' @@ -12,7 +14,7 @@ import { type ProductFormValues, defaultValues, } from '@/lib/zod-schemas/product-schema' -import { useProducts, type IProduct } from '@/app/store/hooks/useProducts' +import { useProducts } from '@/app/store/hooks/useProducts' import { mapProductToFormValues, prepareProductData, @@ -32,34 +34,65 @@ interface ProductFormProps { productId?: string } -export function ProductForm({ storeId, productId }: ProductFormProps) { - const router = useRouter() - const [isSubmitting, setIsSubmitting] = useState(false) - const [isRedirecting, setIsRedirecting] = useState(false) - const { createProduct, updateProduct, products, fetchProduct } = useProducts(storeId, { - skipInitialFetch: true, - }) +// Función helper para validar el status +const normalizeStatus = (status: any): 'draft' | 'pending' | 'active' | 'inactive' => { + const validStatuses = ['draft', 'pending', 'active', 'inactive'] as const - const [productToEdit, setProductToEdit] = useState(null) + // Si el status es undefined, null o string vacío, retornar 'draft' + if (!status || status === '') { + return 'draft' + } - useEffect(() => { - const loadProduct = async () => { - if (!productId) return + // Si el status es válido, retornarlo; sino, retornar 'draft' + return validStatuses.includes(status) ? status : 'draft' +} - const existingProduct = products.find(p => p.id === productId && p.storeId === storeId) +// Componente de Loading reutilizable +function ProductLoadingState() { + return ( +
+ +
+ ) +} - if (existingProduct) { - setProductToEdit(existingProduct) - } else { - const product = await fetchProduct(productId) - if (product && product.storeId === storeId) { - setProductToEdit(product) - } - } - } +// Componente del botón de volver +function BackButton({ onClick }: { onClick: () => void }) { + return ( + + ) +} - loadProduct() - }, [productId, storeId, fetchProduct]) +export function ProductForm({ storeId, productId }: ProductFormProps) { + const router = useRouter() + const [isSubmitting, setIsSubmitting] = useState(false) + const [isLoadingProduct, setIsLoadingProduct] = useState(!!productId) + + const { createProduct, updateProduct, fetchProduct } = useProducts(storeId, { + skipInitialFetch: true, + }) const form = useForm({ resolver: zodResolver(productFormSchema), @@ -78,163 +111,143 @@ export function ProductForm({ storeId, productId }: ProductFormProps) { isSubmitting, }) + // Cargar producto para edición useEffect(() => { - if (productToEdit) { - form.reset(defaultValues) + if (!productId) { + setIsLoadingProduct(false) + return + } - const formValues = mapProductToFormValues(productToEdit) + const loadProduct = async () => { + try { + const product = await fetchProduct(productId) + if (product) { + const formValues = mapProductToFormValues(product) - if (formValues.status) { - const validStatuses = ['draft', 'pending', 'active', 'inactive'] - formValues.status = validStatuses.includes(formValues.status) ? formValues.status : 'draft' - } else { - formValues.status = 'draft' + // Normalizar valores antes de establecerlos en el formulario + formValues.status = normalizeStatus(formValues.status) + formValues.category = formValues.category || '' + + // Establecer los valores en el formulario + form.reset(formValues) + } + } catch (error) { + console.error('Error loading product:', error) + toast.error('Error', { + description: 'No se pudo cargar el producto. Por favor, inténtelo de nuevo.', + }) + } finally { + setIsLoadingProduct(false) } + } - formValues.category = formValues.category || '' + loadProduct() + }, [productId, fetchProduct, form]) + + // Función optimizada para manejar el guardado + const handleSave = useCallback( + async (isNavigating = false) => { + try { + const isValid = await form.trigger() + if (!isValid) { + throw new Error('Validation failed') + } - setTimeout(() => { - form.reset(formValues) - }, 100) - } - }, [productToEdit, form]) + const data = form.getValues() + const basicProductData = prepareProductData(data, storeId) + + const result = productId + ? await handleProductUpdate(basicProductData, productId, storeId, updateProduct) + : await handleProductCreate(basicProductData, createProduct) + + if (result) { + resetUnsavedChanges() + // Mantener isSubmitting true durante la redirección + if (isNavigating && pendingNavigation) { + pendingNavigation() + } else { + router.push(`/store/${storeId}/products`) + } + // No resetear isSubmitting aquí, se hará cuando se complete la navegación + } else { + throw new Error( + productId ? 'Error al actualizar el producto' : 'Error al crear el producto' + ) + } + } catch (error) { + console.error('Error al guardar producto:', error) + + if (!(error instanceof Error && error.message === 'Validation failed')) { + toast.error('Error', { + description: + 'Ha ocurrido un error al guardar el producto. Por favor, inténtelo de nuevo.', + }) + } + throw error + } + }, + [ + form, + storeId, + productId, + updateProduct, + createProduct, + resetUnsavedChanges, + pendingNavigation, + router, + ] + ) - async function onSubmit(data: ProductFormValues) { + // Función para manejar el submit del formulario + const onSubmit = async (data: ProductFormValues) => { if (isSubmitting) return setIsSubmitting(true) try { - const basicProductData = prepareProductData(data, storeId) - let result: IProduct | null - - if (productId) { - result = await handleProductUpdate(basicProductData, productId, storeId, updateProduct) - } else { - result = await handleProductCreate(basicProductData, createProduct) - } + await handleSave() + } catch (error) { + setIsSubmitting(false) + } + } - if (result) { - resetUnsavedChanges() - router.push(`/store/${storeId}/products`) - return - } else { - throw new Error('The product could not be saved') - } + // Función para manejar el guardado desde UnsavedChangesAlert + const handleUnsavedSave = useCallback(async () => { + setIsSubmitting(true) + try { + await handleSave(true) + // Mantener isSubmitting true hasta la redirección } catch (error) { - console.error('The product could not be saved', error) - toast.error('Error', { - description: 'Ha ocurrido un error al guardar el producto. Por favor, inténtelo de nuevo.', - }) setIsSubmitting(false) + throw error } + }, [handleSave]) + + // Función para manejar la navegación con confirmación + const handleNavigation = useCallback( + (destination: () => void) => { + confirmNavigation(destination) + }, + [confirmNavigation] + ) + + // Si está cargando, mostrar el loader + if (isLoadingProduct) { + return } return ( <> {hasUnsavedChanges && ( { - if (productId) { - try { - const isValid = await form.trigger() - if (!isValid) { - return Promise.reject(new Error('Validation failed')) - } - - const data = form.getValues() - const basicProductData = prepareProductData(data, storeId) - const result = await handleProductUpdate( - basicProductData, - productId, - storeId, - updateProduct - ) - - if (result) { - resetUnsavedChanges() - setIsRedirecting(true) - if (pendingNavigation) { - pendingNavigation() - } else { - router.push(`/store/${storeId}/products`) - } - } - } catch (error) { - console.error('The product could not be saved', error) - - if (!(error instanceof Error && error.message === 'Validation failed')) { - toast.error('Error', { - description: - 'Ha ocurrido un error al guardar el producto. Por favor, inténtelo de nuevo.', - }) - } - throw error - } - } else { - try { - const isValid = await form.trigger() - if (!isValid) { - return Promise.reject(new Error('Validation failed')) - } - - const data = form.getValues() - const basicProductData = prepareProductData(data, storeId) - const result = await handleProductCreate(basicProductData, createProduct) - - if (result) { - resetUnsavedChanges() - setIsRedirecting(true) - if (pendingNavigation) { - pendingNavigation() - } else { - router.push(`/store/${storeId}/products`) - } - } else { - throw new Error('The product could not be created') - } - } catch (error) { - console.error('Error al guardar producto:', error) - if (!(error instanceof Error && error.message === 'Validation failed')) { - toast.error('Error', { - description: - 'Ha ocurrido un error al guardar el producto. Por favor, inténtelo de nuevo.', - }) - } - throw error - } - } - }} + onSave={handleUnsavedSave} onDiscard={discardChanges} setIsSubmitting={setIsSubmitting} /> )} +
- + handleNavigation(() => router.back())} />
@@ -269,69 +282,27 @@ export function ProductForm({ storeId, productId }: ProductFormProps) { - {productId ? ( - - ) : ( - - )} +
diff --git a/app/store/hooks/useProducts.ts b/app/store/hooks/useProducts.ts index 674f6d68..a6886517 100644 --- a/app/store/hooks/useProducts.ts +++ b/app/store/hooks/useProducts.ts @@ -1,4 +1,5 @@ import { useMutation, useQueryClient, useInfiniteQuery } from '@tanstack/react-query' +import { useCallback } from 'react' import { generateClient } from 'aws-amplify/api' import { getCurrentUser } from 'aws-amplify/auth' import type { Schema } from '@/amplify/data/resource' @@ -272,64 +273,49 @@ export function useProducts( }) // Consulta para obtener un producto específico - const fetchProductById = async (id: string): Promise => { - if (!storeId) { - console.error('Cannot get product: storeId not defined') - return null - } + const fetchProductById = useCallback( + async (id: string): Promise => { + if (!storeId) { + console.error('Cannot get product: storeId not defined') + return null + } - // Primero verificamos si ya tenemos el producto en la caché - const cachedProducts = queryClient.getQueryData([ - 'products', - storeId, - limit, - sortDirection, - sortField, - ]) as any - - if (cachedProducts && cachedProducts.pages) { - for (const page of cachedProducts.pages) { - const existingProduct = page.products.find((p: IProduct) => p.id === id) - if (existingProduct) { - // Verificar que el producto pertenezca a la tienda actual - if (existingProduct.storeId === storeId) { + // Primero verificamos si ya tenemos el producto en la caché + const cachedProducts = queryClient.getQueryData([ + 'products', + storeId, + limit, + sortDirection, + sortField, + ]) as any + + if (cachedProducts && cachedProducts.pages) { + for (const page of cachedProducts.pages) { + const existingProduct = page.products.find((p: IProduct) => p.id === id) + if (existingProduct) { return existingProduct - } else { - console.error( - `Access denied: Product ${id} does not belong to the current store ${storeId}` - ) - return null } } } - } - // Verificar si el producto pertenece a la tienda actual antes de hacer la petición - try { - // Primero obtenemos todos los productos de la tienda actual - const { data: storeProducts } = await client.models.Product.list({ - filter: { storeId: { eq: storeId } }, - }) + // Si no está en caché, obtenemos el producto por ID + try { + const { data: product } = await client.models.Product.get({ id }) - // Buscamos el producto en los productos de la tienda - const productInStore = storeProducts?.find(p => p.id === id) - - if (productInStore) { - // El producto pertenece a la tienda actual, lo añadimos a la caché - queryClient.setQueryData(['product', id], productInStore) - return productInStore as IProduct - } else { - // El producto no pertenece a la tienda actual o no existe - console.error( - `Access denied: Product ${id} does not belong to the current store ${storeId}` - ) + if (product) { + // Añadimos el producto a la caché + queryClient.setQueryData(['product', id], product) + return product as IProduct + } + + return null + } catch (error) { + console.error(`Error fetching product ${id}:`, error) return null } - } catch (error) { - console.error(`Error verifying product ${id}:`, error) - return null - } - } + }, + [storeId, limit, sortDirection, sortField, queryClient] + ) // Extraer productos de todas las páginas const products = data?.pages.flatMap(page => page.products) || [] diff --git a/components/providers/AmplifyProvider.tsx b/components/providers/AmplifyProvider.tsx new file mode 100644 index 00000000..b6ebef22 --- /dev/null +++ b/components/providers/AmplifyProvider.tsx @@ -0,0 +1,35 @@ +'use client' + +import { useEffect, useRef } from 'react' +import { configureAmplify, configureAmplifySSR } from '@/lib/amplify-config' + +interface AmplifyProviderProps { + children: React.ReactNode +} + +export function AmplifyProvider({ children }: AmplifyProviderProps) { + const isConfigured = useRef(false) + + useEffect(() => { + // Solo configurar una vez en el cliente + if (!isConfigured.current) { + if (typeof window !== 'undefined') { + configureAmplify() + } else { + configureAmplifySSR() + } + isConfigured.current = true + } + }, []) + + return <>{children} +} + +// También exportar una versión que se ejecuta inmediatamente +export function initializeAmplify() { + if (typeof window !== 'undefined') { + configureAmplify() + } else { + configureAmplifySSR() + } +} diff --git a/lib/amplify-config.ts b/lib/amplify-config.ts index b77afc87..0b37a9c3 100644 --- a/lib/amplify-config.ts +++ b/lib/amplify-config.ts @@ -56,14 +56,5 @@ export function reconfigureAmplify() { configureAmplify() } -// Auto-configuración al importar -if (typeof window !== 'undefined') { - // Cliente - configuración completa - configureAmplify() -} else { - // Servidor - configuración SSR - configureAmplifySSR() -} - // Re-exportar Amplify configurado export { Amplify }