From 9fca7448c6ec597efabc90233370f708a7b97540 Mon Sep 17 00:00:00 2001 From: Stivenjs Date: Mon, 19 May 2025 14:29:58 -0500 Subject: [PATCH 1/3] feat(middleware): enhance product and collection ownership checks Added a new middleware for verifying collection ownership and improved the existing product ownership check. Updated the middleware logic to handle specific URL patterns for collections. Additionally, refactored comments for clarity. refactor(config): update Next.js configuration for asset handling Modified the Next.js configuration to allow wildcard hostnames and pathnames for asset URLs, enhancing flexibility in CDN usage. style(global.css): add active submenu item styling Introduced styles for active submenu items to improve UI visibility and user experience. refactor(product-management): streamline imports and enhance routing Updated import paths in product management components for consistency and clarity. Enhanced routing logic in InventoryTracking and ProductTableDesktop components to utilize dynamic store IDs. --- app/global.css | 15 ++ .../product-management/InventoryTracking.tsx | 9 +- .../product-management/ProductManager.tsx | 6 +- .../inventory/inventory-actions.tsx | 14 ++ .../inventory/inventory-filter.tsx | 14 ++ .../inventory/inventory-footer.tsx | 10 ++ .../inventory/inventory-header.tsx | 10 ++ .../inventory/inventory-table-row.tsx | 38 +++++ .../inventory/inventory-table.tsx | 31 ++++ .../product-table/product-table-desktop.tsx | 14 +- .../utils/collection-form-utils.ts | 4 +- .../product-management/utils/exportUtils.ts | 4 +- .../product-management/utils/productUtils.ts | 2 +- app/store/components/sidebar/nav-main.tsx | 131 +++++++++++------ middleware.ts | 9 +- middlewares/auth.ts | 2 +- middlewares/collectionOwnership.ts | 137 ++++++++++++++++++ middlewares/productOwnership.ts | 2 +- middlewares/store.ts | 2 +- middlewares/storeAccess.ts | 2 +- next.config.ts | 3 +- utils/{amplify-utils.ts => AmplifyUtils.ts} | 0 22 files changed, 394 insertions(+), 65 deletions(-) create mode 100644 app/store/components/product-management/inventory/inventory-actions.tsx create mode 100644 app/store/components/product-management/inventory/inventory-filter.tsx create mode 100644 app/store/components/product-management/inventory/inventory-footer.tsx create mode 100644 app/store/components/product-management/inventory/inventory-header.tsx create mode 100644 app/store/components/product-management/inventory/inventory-table-row.tsx create mode 100644 app/store/components/product-management/inventory/inventory-table.tsx create mode 100644 middlewares/collectionOwnership.ts rename utils/{amplify-utils.ts => AmplifyUtils.ts} (100%) diff --git a/app/global.css b/app/global.css index 91d6dc80..59254fd2 100644 --- a/app/global.css +++ b/app/global.css @@ -92,3 +92,18 @@ @apply bg-background text-foreground; } } + +.submenu-item-active { + position: relative; +} + +.submenu-item-active::before { + content: ''; + position: absolute; + left: -4px; + top: 0; + height: 100%; + width: 2px; + background-color: currentColor; + border-radius: 1px; +} diff --git a/app/store/components/product-management/InventoryTracking.tsx b/app/store/components/product-management/InventoryTracking.tsx index a2720cfc..c2c1fe4f 100644 --- a/app/store/components/product-management/InventoryTracking.tsx +++ b/app/store/components/product-management/InventoryTracking.tsx @@ -2,8 +2,15 @@ import Link from 'next/link' import { Button } from '@/components/ui/button' import { Card } from '@/components/ui/card' import { Icons } from '@/app/store/icons/index' +import { getStoreId } from '@/utils/store-utils' +import { useParams, usePathname } from 'next/navigation' +import { routes } from '@/utils/routes' export function InventoryTracking() { + const params = useParams() + const pathname = usePathname() + const storeId = getStoreId(params, pathname) + return (

Inventario

@@ -24,7 +31,7 @@ export function InventoryTracking() {

{/* Botón */} - + diff --git a/app/store/components/product-management/ProductManager.tsx b/app/store/components/product-management/ProductManager.tsx index 62b15bf7..8b5f449f 100644 --- a/app/store/components/product-management/ProductManager.tsx +++ b/app/store/components/product-management/ProductManager.tsx @@ -1,7 +1,7 @@ import { useProducts } from '@/app/store/hooks/useProducts' -import { ProductForm } from './ProductForm' -import { ProductList } from './ProductList' -import { ProductsPage } from './ProductPage' +import { ProductForm } from '@/app/store/components/product-management/ProductForm' +import { ProductList } from '@/app/store/components/product-management/ProductList' +import { ProductsPage } from '@/app/store/components/product-management/ProductPage' import { Loader } from '@/components/ui/loader' interface ProductManagerProps { diff --git a/app/store/components/product-management/inventory/inventory-actions.tsx b/app/store/components/product-management/inventory/inventory-actions.tsx new file mode 100644 index 00000000..1825cc57 --- /dev/null +++ b/app/store/components/product-management/inventory/inventory-actions.tsx @@ -0,0 +1,14 @@ +import { Button } from '@/components/ui/button' + +export default function InventoryActions() { + return ( +
+ + +
+ ) +} diff --git a/app/store/components/product-management/inventory/inventory-filter.tsx b/app/store/components/product-management/inventory/inventory-filter.tsx new file mode 100644 index 00000000..d83453bf --- /dev/null +++ b/app/store/components/product-management/inventory/inventory-filter.tsx @@ -0,0 +1,14 @@ +import { Button } from '@/components/ui/button' + +export default function InventoryFilter() { + return ( +
+ + +
+ ) +} diff --git a/app/store/components/product-management/inventory/inventory-footer.tsx b/app/store/components/product-management/inventory/inventory-footer.tsx new file mode 100644 index 00000000..2e843ba4 --- /dev/null +++ b/app/store/components/product-management/inventory/inventory-footer.tsx @@ -0,0 +1,10 @@ +export default function InventoryFooter() { + return ( +
+ Más información sobre{' '} + + gestionar el inventario + +
+ ) +} diff --git a/app/store/components/product-management/inventory/inventory-header.tsx b/app/store/components/product-management/inventory/inventory-header.tsx new file mode 100644 index 00000000..68a156b3 --- /dev/null +++ b/app/store/components/product-management/inventory/inventory-header.tsx @@ -0,0 +1,10 @@ +import { Home } from 'lucide-react' + +export default function InventoryHeader() { + return ( +
+ +

Inventario

+
+ ) +} diff --git a/app/store/components/product-management/inventory/inventory-table-row.tsx b/app/store/components/product-management/inventory/inventory-table-row.tsx new file mode 100644 index 00000000..6df3b9ff --- /dev/null +++ b/app/store/components/product-management/inventory/inventory-table-row.tsx @@ -0,0 +1,38 @@ +import { TableRow, TableCell } from '@/components/ui/table' +import { Checkbox } from '@/components/ui/checkbox' + +export interface InventoryRowProps { + id: string + sku: string + unavailable: number + committed: number + available: number + inStock: number +} + +export default function InventoryTableRow({ + id, + sku, + unavailable, + committed, + available, + inStock, +}: InventoryRowProps) { + return ( + + + + + {id} + {sku} + {unavailable} + {committed} + + + + + + + + ) +} diff --git a/app/store/components/product-management/inventory/inventory-table.tsx b/app/store/components/product-management/inventory/inventory-table.tsx new file mode 100644 index 00000000..b6ed6626 --- /dev/null +++ b/app/store/components/product-management/inventory/inventory-table.tsx @@ -0,0 +1,31 @@ +import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import InventoryTableRow, { InventoryRowProps } from './inventory-table-row' + +interface InventoryTableProps { + data: InventoryRowProps[] +} + +export default function InventoryTable({ data }: InventoryTableProps) { + return ( +
+ + + + + Product + SKU + Unavailable + Committed + Available + In Stock + + + + {data.map(row => ( + + ))} + +
+
+ ) +} diff --git a/app/store/components/product-management/product-table/product-table-desktop.tsx b/app/store/components/product-management/product-table/product-table-desktop.tsx index 03213b57..dc9a24ce 100644 --- a/app/store/components/product-management/product-table/product-table-desktop.tsx +++ b/app/store/components/product-management/product-table/product-table-desktop.tsx @@ -16,6 +16,10 @@ import type { SortField, VisibleColumns, } from '@/app/store/components/product-management/types/product-types' +import { getStoreId } from '@/utils/store-utils' +import { useParams, usePathname } from 'next/navigation' +import { routes } from '@/utils/routes' +import Link from 'next/link' interface ProductTableDesktopProps { products: IProduct[] @@ -40,6 +44,10 @@ export function ProductTableDesktop({ toggleSort, renderSortIndicator, }: ProductTableDesktopProps) { + const pathname = usePathname() + const params = useParams() + const storeId = getStoreId(params, pathname) + return (
@@ -135,7 +143,11 @@ export function ProductTableDesktop({ )} - {product.name} + + + {visibleColumns.status && ( diff --git a/app/store/components/product-management/utils/collection-form-utils.ts b/app/store/components/product-management/utils/collection-form-utils.ts index 5933914e..9585dfb5 100644 --- a/app/store/components/product-management/utils/collection-form-utils.ts +++ b/app/store/components/product-management/utils/collection-form-utils.ts @@ -285,7 +285,7 @@ export const useCollectionForm = ({ router.push(routes.store.collections(storeId)) // No desactivamos isSubmitting para mantener el botón deshabilitado hasta la redirección } catch (error) { - console.error('Error al eliminar la colección:', error) + console.error('Error deleting collection:', error) toast.error('Ocurrió un error al eliminar la colección') setIsSubmitting(false) // Solo desactivamos en caso de error } @@ -323,7 +323,7 @@ export const useCollectionForm = ({ isSubmitting, hasUnsavedChanges, selectedProducts, - isDataLoaded, // Add this to the returned object + isDataLoaded, // Acciones setTitle, diff --git a/app/store/components/product-management/utils/exportUtils.ts b/app/store/components/product-management/utils/exportUtils.ts index 26473e74..12a279ab 100644 --- a/app/store/components/product-management/utils/exportUtils.ts +++ b/app/store/components/product-management/utils/exportUtils.ts @@ -8,7 +8,7 @@ import { IProduct } from '@/app/store/hooks/useProducts' */ export const exportProductsToCSV = (products: IProduct[], fileName: string): boolean => { if (!products || products.length === 0) { - console.error('No hay productos para exportar') + console.error('There are no products to export') return false } @@ -126,7 +126,7 @@ export const exportProductsToCSV = (products: IProduct[], fileName: string): boo return true } catch (error) { - console.error('Error al exportar productos a CSV:', error) + console.error('Error exporting products to CSV:', error) return false } } diff --git a/app/store/components/product-management/utils/productUtils.ts b/app/store/components/product-management/utils/productUtils.ts index 40239cb6..1cd93169 100644 --- a/app/store/components/product-management/utils/productUtils.ts +++ b/app/store/components/product-management/utils/productUtils.ts @@ -123,7 +123,7 @@ export async function handleProductCreate( return result } - throw new Error('No se pudo crear el producto') + throw new Error('The product could not be created') } catch (error) { console.error('Error in handleProductCreate:', error) toast.error('Error', { diff --git a/app/store/components/sidebar/nav-main.tsx b/app/store/components/sidebar/nav-main.tsx index 5643ab8e..37d19e95 100644 --- a/app/store/components/sidebar/nav-main.tsx +++ b/app/store/components/sidebar/nav-main.tsx @@ -1,3 +1,5 @@ +'use client' + import { ChevronRight, type LucideIcon } from 'lucide-react' import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' import { @@ -6,13 +8,12 @@ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem, + useSidebar, } from '@/components/ui/sidebar' import Link from 'next/link' import { useState, useEffect } from 'react' import { usePathname } from 'next/navigation' +import { Skeleton } from '@/components/ui/skeleton' import useStoreDataStore from '@/context/core/storeDataStore' interface NavMainProps { @@ -32,8 +33,10 @@ interface NavMainProps { export function NavMain({ items }: NavMainProps) { const pathname = usePathname() - const { currentStore, isLoading, clearStore } = useStoreDataStore() + const { currentStore, isLoading } = useStoreDataStore() const [openItem, setOpenItem] = useState(null) + const { state } = useSidebar() + const isExpanded = state === 'expanded' useEffect(() => { const activeItem = items.find(item => { @@ -54,53 +57,85 @@ export function NavMain({ items }: NavMainProps) { return ( - Mi tienda - {isLoading ? 'Cargando...' : currentStore?.storeName} + {isLoading ? ( + + ) : ( + <>Mi tienda - {currentStore?.storeName} + )} - {items.map(item => ( - { - if (isOpen) { - setOpenItem(item.title) - } else if (openItem === item.title) { - setOpenItem(null) - } - }} - className="group/collapsible" - > - - - handleItemClick(item.title)}> - {item.icon && } - {item.url ? ( - {}}> + {items.map(item => { + // Check if this item is active based on the current path + const isItemActive = item.url && pathname.startsWith(item.url) + const hasActiveChild = item.items?.some(subItem => pathname === subItem.url) + + return ( + { + if (isOpen) { + setOpenItem(item.title) + } else if (openItem === item.title) { + setOpenItem(null) + } + }} + className="group/collapsible" + > + + + handleItemClick(item.title)} + isActive={isItemActive || hasActiveChild} + > + {item.icon && } + {item.url ? ( + {}}> + {item.title} + + ) : ( {item.title} - - ) : ( - {item.title} - )} - - - - - - {item.items?.map(subItem => ( - - - - {subItem.title} - - - - ))} - - - - - ))} + )} + {isExpanded && ( + + )} + + + {isExpanded && ( + + {/* Custom styled submenu with line indicators */} +
+ {item.items?.map(subItem => { + // Check if this sub-item is active based on the exact path match + const isSubItemActive = pathname === subItem.url + + return ( +
+ {/* Horizontal connector line */} +
+ + + {subItem.title} + +
+ ) + })} +
+
+ )} +
+
+ ) + })}
) diff --git a/middleware.ts b/middleware.ts index 8ed31815..80f100d9 100644 --- a/middleware.ts +++ b/middleware.ts @@ -5,17 +5,22 @@ import { handleStoreMiddleware } from './middlewares/store' import { handleStoreAccessMiddleware } from './middlewares/storeAccess' import { handleProductOwnershipMiddleware } from './middlewares/productOwnership' import { handleAuthenticatedRedirect } from './middlewares/auth' +import { handleCollectionOwnership } from './middlewares/collectionOwnership' export async function middleware(request: NextRequest) { const path = request.nextUrl.pathname - // Verificar propiedad de productos específicos - Corregido el patrón de URL + // Verificar propiedad de productos específicos if (path.includes('/products/') && path.match(/\/products\/([^\/]+)/)) { return handleProductOwnershipMiddleware(request) } + // verificar propiedad de coleccione especifica + if (path.includes('/collection') && path.match(/\/collections\/([^\/]+)/)) { + return handleCollectionOwnership(request) + } - // Proteger todas las rutas de tienda if (path.match(/^\/store\/[^\/]+/)) { + // Proteger todas las rutas de tienda return handleStoreAccessMiddleware(request) } diff --git a/middlewares/auth.ts b/middlewares/auth.ts index a688d453..a8719f08 100644 --- a/middlewares/auth.ts +++ b/middlewares/auth.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import { fetchAuthSession } from 'aws-amplify/auth/server' -import { runWithAmplifyServerContext } from '@/utils/amplify-utils' +import { runWithAmplifyServerContext } from '@/utils/AmplifyUtils' export async function getSession(request: NextRequest, response: NextResponse) { return runWithAmplifyServerContext({ diff --git a/middlewares/collectionOwnership.ts b/middlewares/collectionOwnership.ts new file mode 100644 index 00000000..1adf19f0 --- /dev/null +++ b/middlewares/collectionOwnership.ts @@ -0,0 +1,137 @@ +import { NextRequest, NextResponse } from 'next/server' +import { cookiesClient } from '@/utils/AmplifyUtils' +import { getSession } from './auth' + +/** + * Middleware para verificar que un usuario solo pueda acceder a colecciones + * que pertenecen a la tienda que está visualizando actualmente. + * + * Este middleware realiza las siguientes verificaciones: + * 1. Comprueba si el usuario está autenticado + * 2. Verifica que el usuario tenga acceso a la tienda (como propietario o colaborador) + * 3. Para colecciones existentes, verifica que pertenezcan a la tienda actual + * 4. Permite acceso a la ruta "new" si el usuario tiene acceso a la tienda + * + * @param request - La solicitud HTTP entrante + * @returns Respuesta HTTP apropiada según la verificación de propiedad + */ +export async function handleCollectionOwnership(request: NextRequest) { + // Verificar si esta es una redirección para evitar bucles + const isRedirect = request.headers.get('x-redirect-check') === 'true' + if (isRedirect) { + return NextResponse.next() + } + + // Verificar autenticación del usuario + const session = await getSession(request, NextResponse.next()) + + if (!session) { + return NextResponse.redirect(new URL('/login', request.url)) + } + + const userId = session.tokens?.idToken?.payload?.['cognito:username'] + + if (!userId || typeof userId !== 'string') { + return NextResponse.redirect(new URL('/login', request.url)) + } + + // Extraer información de la URL + const path = request.nextUrl.pathname + const storeIdMatch = path.match(/\/store\/([^\/]+)/) + const storeIdFromUrl = storeIdMatch ? storeIdMatch[1] : null + const currentStoreId = request.cookies.get('currentStore')?.value || storeIdFromUrl + + if (!currentStoreId) { + // Redirigir a la selección de tienda si no se puede determinar la tienda actual + const redirectUrl = new URL('/my-store', request.url) + const response = NextResponse.redirect(redirectUrl) + response.headers.set('x-redirect-check', 'true') + return response + } + + try { + // Verificar que el usuario tenga acceso a la tienda + // Primero intentamos verificar si es propietario + const storeResult = await cookiesClient.models.UserStore.get( + { + id: currentStoreId, + }, + { + authMode: 'userPool', + } + ) + + // Si la tienda no existe o no pertenece al usuario, verificar si es colaborador + if (!storeResult.data || storeResult.data.userId !== userId) { + // Intentar verificar acceso a través de UserStore (para colaboradores) + const userStoreResult = await cookiesClient.models.UserStore.list({ + filter: { + storeId: { + eq: currentStoreId, + }, + userId: { + eq: userId, + }, + }, + authMode: 'userPool', + }) + + // Si no hay registros de UserStore, el usuario no tiene acceso + if (!userStoreResult.data || userStoreResult.data.length === 0) { + const redirectUrl = new URL('/my-store', request.url) + const response = NextResponse.redirect(redirectUrl) + response.headers.set('x-redirect-check', 'true') + return response + } + } + + // Extraer el ID de la coleccion de la URL + const collectionMatches = path.match(/\/collections\/([^\/]+)/) + const collectionId = collectionMatches ? collectionMatches[1] : null + + // Si es la ruta "new", permitir el acceso (ya verificamos que el usuario tiene acceso a la tienda) + if (collectionId === 'new') { + return NextResponse.next() + } + + if (!collectionId) { + return NextResponse.next() + } + + // Para colecciones existentes, verificar que pertenezcan a la tienda actual + const { data: collection } = await cookiesClient.models.Collection.get( + { + id: collectionId, + }, + { + authMode: 'userPool', + } + ) + + // Verificar que la coleccion exista y pertenezca a la tienda actual + if (!collection) { + // La coleccion no existe, redirigir a la lista de coleccioness + const redirectUrl = new URL(`/store/${currentStoreId}/collections`, request.url) + const response = NextResponse.redirect(redirectUrl) + response.headers.set('x-redirect-check', 'true') + return response + } + + if (collection.storeId !== currentStoreId) { + // La coleccion pertenece a otra tienda, denegar acceso + const redirectUrl = new URL(`/store/${currentStoreId}/collections`, request.url) + const response = NextResponse.redirect(redirectUrl) + response.headers.set('x-redirect-check', 'true') + return response + } + + // La coleccion pertenece a la tienda actual, continuar con la solicitud + return NextResponse.next() + } catch (error) { + // Error al verificar la coleccion o la tienda, redirigir por seguridad + const redirectUrl = new URL(`/my-store`, request.url) + const response = NextResponse.redirect(redirectUrl) + response.headers.set('x-redirect-check', 'true') + return response + } +} diff --git a/middlewares/productOwnership.ts b/middlewares/productOwnership.ts index a1843b22..a13d6294 100644 --- a/middlewares/productOwnership.ts +++ b/middlewares/productOwnership.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { cookiesClient } from '@/utils/amplify-utils' +import { cookiesClient } from '@/utils/AmplifyUtils' import { getSession } from './auth' /** diff --git a/middlewares/store.ts b/middlewares/store.ts index aea09219..571e9a4a 100644 --- a/middlewares/store.ts +++ b/middlewares/store.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { cookiesClient } from '@/utils/amplify-utils' +import { cookiesClient } from '@/utils/AmplifyUtils' import { getSession } from './auth' const STORE_LIMITS = { diff --git a/middlewares/storeAccess.ts b/middlewares/storeAccess.ts index bc5eb5c8..4e809840 100644 --- a/middlewares/storeAccess.ts +++ b/middlewares/storeAccess.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import { getSession } from './auth' -import { cookiesClient } from '@/utils/amplify-utils' +import { cookiesClient } from '@/utils/AmplifyUtils' /** * Middleware para proteger las rutas de tienda diff --git a/next.config.ts b/next.config.ts index 8fadab22..37add973 100644 --- a/next.config.ts +++ b/next.config.ts @@ -31,8 +31,9 @@ const nextConfig = { }, { protocol: 'https', - hostname: 'cdn.fasttify.com', + hostname: '*cdn.fasttify.com', port: '', + pathname: '/**', }, ], }, diff --git a/utils/amplify-utils.ts b/utils/AmplifyUtils.ts similarity index 100% rename from utils/amplify-utils.ts rename to utils/AmplifyUtils.ts From e39c155ec4aaed8869c6dfa476a18df878c31f14 Mon Sep 17 00:00:00 2001 From: Stivenjs Date: Mon, 19 May 2025 20:36:32 -0500 Subject: [PATCH 2/3] refactor(inventory-management): replace InventoryTracking with InventoryManager and enhance inventory table Updated the InventoryPage to use InventoryManager instead of InventoryTracking, incorporating store ID retrieval via `useParams` and `usePathname`. Enhanced the InventoryTracking component with additional props for better data handling and pagination. Refactored InventoryTableRow to support inline editing of stock quantities and improved the overall structure of inventory management components. --- app/store/[slug]/inventory/page.tsx | 9 +- app/store/[slug]/products/page.tsx | 1 - .../product-management/InventoryManager.tsx | 76 +++++++++ .../product-management/InventoryPage.tsx | 33 ++++ .../product-management/InventoryTracking.tsx | 87 ++++++---- .../product-management/ProductForm.tsx | 18 +-- .../inventory/inventory-card-mobile.tsx | 139 ++++++++++++++++ .../inventory/inventory-table-row.tsx | 150 ++++++++++++++++-- .../inventory/inventory-table.tsx | 45 +++--- .../ai-generate-button.tsx | 2 +- .../attributes-section.tsx | 0 .../basic-info-section.tsx | 2 +- .../images-section.tsx | 0 .../price-suggestion-panel.tsx | 2 +- .../pricing-inventory-section.tsx | 10 +- .../pricing-section.tsx | 0 .../publication-section.tsx | 0 .../text-shimmer.tsx | 0 .../product-table/product-pagination.tsx | 1 - app/store/components/sidebar/nav-main.tsx | 6 +- 20 files changed, 490 insertions(+), 91 deletions(-) create mode 100644 app/store/components/product-management/InventoryManager.tsx create mode 100644 app/store/components/product-management/InventoryPage.tsx create mode 100644 app/store/components/product-management/inventory/inventory-card-mobile.tsx rename app/store/components/product-management/{sections => product-sections}/ai-generate-button.tsx (98%) rename app/store/components/product-management/{sections => product-sections}/attributes-section.tsx (100%) rename app/store/components/product-management/{sections => product-sections}/basic-info-section.tsx (99%) rename app/store/components/product-management/{sections => product-sections}/images-section.tsx (100%) rename app/store/components/product-management/{sections => product-sections}/price-suggestion-panel.tsx (98%) rename app/store/components/product-management/{sections => product-sections}/pricing-inventory-section.tsx (96%) rename app/store/components/product-management/{sections => product-sections}/pricing-section.tsx (100%) rename app/store/components/product-management/{sections => product-sections}/publication-section.tsx (100%) rename app/store/components/product-management/{sections => product-sections}/text-shimmer.tsx (100%) diff --git a/app/store/[slug]/inventory/page.tsx b/app/store/[slug]/inventory/page.tsx index 72ae590a..893af607 100644 --- a/app/store/[slug]/inventory/page.tsx +++ b/app/store/[slug]/inventory/page.tsx @@ -1,7 +1,9 @@ 'use client' -import { InventoryTracking } from '@/app/store/components/product-management/InventoryTracking' +import { InventoryManager } from '@/app/store/components/product-management/InventoryManager' import { Amplify } from 'aws-amplify' +import { getStoreId } from '@/utils/store-utils' +import { useParams, usePathname } from 'next/navigation' import outputs from '@/amplify_outputs.json' Amplify.configure(outputs) @@ -15,5 +17,8 @@ Amplify.configure({ }) export default function InventoryPage() { - return + const pathname = usePathname() + const params = useParams() + const storeId = getStoreId(params, pathname) + return } diff --git a/app/store/[slug]/products/page.tsx b/app/store/[slug]/products/page.tsx index 3058de2d..97c67904 100644 --- a/app/store/[slug]/products/page.tsx +++ b/app/store/[slug]/products/page.tsx @@ -19,7 +19,6 @@ Amplify.configure({ export default function StoreProductsPage() { const pathname = usePathname() const params = useParams() - const storeId = getStoreId(params, pathname) return } diff --git a/app/store/components/product-management/InventoryManager.tsx b/app/store/components/product-management/InventoryManager.tsx new file mode 100644 index 00000000..f6685bde --- /dev/null +++ b/app/store/components/product-management/InventoryManager.tsx @@ -0,0 +1,76 @@ +import { useProducts } from '@/app/store/hooks/useProducts' +import { InventoryTracking } from './InventoryTracking' +import { InventoryPage } from './InventoryPage' +import { Loader } from '@/components/ui/loader' +import { useProductPagination } from './hooks/useProductPagination' + +interface InventoryManagerProps { + storeId: string +} + +export function InventoryManager({ storeId }: InventoryManagerProps) { + const { + products, + loading, + paginationLoading, + error, + hasNextPage, + loadNextPage, + refreshProducts, + } = useProducts(storeId) + + // Transformar los productos al formato de inventario + const inventoryData = products.map(product => ({ + id: product.id, + name: product.name, + sku: product.sku || `SKU-${product.id}`, + images: product.images, + unavailable: 0, + committed: 0, + available: product.quantity || 0, + inStock: product.quantity || 0, + })) + + const { + currentPage, + itemsPerPage, + setItemsPerPage, + totalPages, + paginatedProducts, + loadingMoreProducts, + handlePageChange, + } = useProductPagination({ + sortedProducts: inventoryData, + hasNextPage, + loadNextPage, + }) + + if (loading) { + return ( +
+ +
+ ) + } + + return inventoryData.length === 0 ? ( + + ) : ( + + ) +} + +export default InventoryManager diff --git a/app/store/components/product-management/InventoryPage.tsx b/app/store/components/product-management/InventoryPage.tsx new file mode 100644 index 00000000..60c05fbc --- /dev/null +++ b/app/store/components/product-management/InventoryPage.tsx @@ -0,0 +1,33 @@ +import Link from 'next/link' +import { Button } from '@/components/ui/button' +import { Card } from '@/components/ui/card' +import { Icons } from '@/app/store/icons/index' + +export function InventoryPage() { + return ( +
+

Inventario

+ +
+
+ +
+
+ + {/* Título y descripción */} +

+ Haz seguimiento de tu inventario +

+

+ Cuando habilites el seguimiento de inventario en tus productos, podrás ver y ajustar sus + recuentos de inventario aquí. +

+ + {/* Botón */} + + + +
+
+ ) +} diff --git a/app/store/components/product-management/InventoryTracking.tsx b/app/store/components/product-management/InventoryTracking.tsx index c2c1fe4f..c78cf16c 100644 --- a/app/store/components/product-management/InventoryTracking.tsx +++ b/app/store/components/product-management/InventoryTracking.tsx @@ -1,40 +1,65 @@ -import Link from 'next/link' -import { Button } from '@/components/ui/button' -import { Card } from '@/components/ui/card' -import { Icons } from '@/app/store/icons/index' -import { getStoreId } from '@/utils/store-utils' -import { useParams, usePathname } from 'next/navigation' -import { routes } from '@/utils/routes' +import InventoryHeader from '@/app/store/components/product-management/inventory/inventory-header' +import InventoryFilter from '@/app/store/components/product-management/inventory/inventory-filter' +import InventoryActions from '@/app/store/components/product-management/inventory/inventory-actions' +import InventoryTable from '@/app/store/components/product-management/inventory/inventory-table' +import InventoryFooter from '@/app/store/components/product-management/inventory/inventory-footer' +import { ProductPagination } from '@/app/store/components/product-management/product-table/product-pagination' +import { InventoryRowProps } from '@/app/store/components/product-management/inventory/inventory-table-row' -export function InventoryTracking() { - const params = useParams() - const pathname = usePathname() - const storeId = getStoreId(params, pathname) +interface InventoryTrackingProps { + data: InventoryRowProps[] + loading: boolean + error: Error | null + hasNextPage: boolean + loadNextPage: () => void + refreshInventory: () => void + currentPage: number + totalPages: number + itemsPerPage: number + setItemsPerPage: (value: number) => void + handlePageChange: (page: number) => void + loadingMoreProducts: boolean +} +export function InventoryTracking({ + data, + loading, + error, + hasNextPage, + currentPage, + totalPages, + itemsPerPage, + setItemsPerPage, + handlePageChange, + loadingMoreProducts, +}: InventoryTrackingProps) { return ( -
-

Inventario

- -
-
- -
+
+
+ {/* Header con título y acciones */} +
+ +
+ + - {/* Título y descripción */} -

- Haz seguimiento de tu inventario -

-

- Cuando habilites el seguimiento de inventario en tus productos, podrás ver y ajustar sus - recuentos de inventario aquí. -

+ {/* Paginación */} + {!loading && !error && data.length > 0 && ( + + )} - {/* Botón */} - - - - + +
) } diff --git a/app/store/components/product-management/ProductForm.tsx b/app/store/components/product-management/ProductForm.tsx index cebfee82..a5bcca11 100644 --- a/app/store/components/product-management/ProductForm.tsx +++ b/app/store/components/product-management/ProductForm.tsx @@ -21,11 +21,11 @@ import { } from '@/app/store/components/product-management/utils/productUtils' import { useUnsavedChangesWarning } from '@/hooks/ui/use-unsaved-changes-warning' import { UnsavedChangesAlert } from '@/components/ui/unsaved-changes-alert' -import { BasicInfoSection } from '@/app/store/components/product-management/sections/basic-info-section' -import { PricingInventorySection } from '@/app/store/components/product-management/sections/pricing-inventory-section' -import { ImagesSection } from '@/app/store/components/product-management/sections/images-section' -import { AttributesSection } from '@/app/store/components/product-management/sections/attributes-section' -import { PublicationSection } from '@/app/store/components/product-management/sections/publication-section' +import { BasicInfoSection } from '@/app/store/components/product-management/product-sections/basic-info-section' +import { PricingInventorySection } from '@/app/store/components/product-management/product-sections/pricing-inventory-section' +import { ImagesSection } from '@/app/store/components/product-management/product-sections/images-section' +import { AttributesSection } from '@/app/store/components/product-management/product-sections/attributes-section' +import { PublicationSection } from '@/app/store/components/product-management/product-sections/publication-section' interface ProductFormProps { storeId: string @@ -118,10 +118,10 @@ export function ProductForm({ storeId, productId }: ProductFormProps) { router.push(`/store/${storeId}/products`) return } else { - throw new Error('No se pudo guardar el producto') + throw new Error('The product could not be saved') } } catch (error) { - console.error('Error al guardar producto:', 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.', }) @@ -160,7 +160,7 @@ export function ProductForm({ storeId, productId }: ProductFormProps) { } } } catch (error) { - console.error('Error al guardar producto:', error) + console.error('The product could not be saved', error) if (!(error instanceof Error && error.message === 'Validation failed')) { toast.error('Error', { @@ -190,7 +190,7 @@ export function ProductForm({ storeId, productId }: ProductFormProps) { router.push(`/store/${storeId}/products`) } } else { - throw new Error('No se pudo crear el producto') + throw new Error('The product could not be created') } } catch (error) { console.error('Error al guardar producto:', error) diff --git a/app/store/components/product-management/inventory/inventory-card-mobile.tsx b/app/store/components/product-management/inventory/inventory-card-mobile.tsx new file mode 100644 index 00000000..3d84e3ac --- /dev/null +++ b/app/store/components/product-management/inventory/inventory-card-mobile.tsx @@ -0,0 +1,139 @@ +import { InventoryRowProps } from './inventory-table-row' +import { Image } from 'lucide-react' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { useState } from 'react' +import { toast } from 'sonner' +import { useProducts } from '@/app/store/hooks/useProducts' +import { getStoreId } from '@/utils/store-utils' +import { useParams, usePathname } from 'next/navigation' +import { UnsavedChangesAlert } from '@/components/ui/unsaved-changes-alert' + +interface InventoryCardMobileProps { + data: InventoryRowProps[] +} + +export function InventoryCardMobile({ data }: InventoryCardMobileProps) { + const pathname = usePathname() + const params = useParams() + const storeId = getStoreId(params, pathname) + const { updateProduct } = useProducts(storeId) + + const [editingId, setEditingId] = useState(null) + const [inStockValues, setInStockValues] = useState>( + data.reduce((acc, item) => ({ ...acc, [item.id]: item.inStock }), {}) + ) + const [hasUnsavedChanges, setHasUnsavedChanges] = useState>({}) + + const handleUpdateQuantity = async (id: string, name: string) => { + try { + await updateProduct({ + id, + quantity: inStockValues[id], + }) + toast.success('Inventario actualizado', { + description: `El inventario de "${name}" ha sido actualizado correctamente.`, + }) + setEditingId(null) + setHasUnsavedChanges(prev => ({ ...prev, [id]: false })) + } catch (error) { + toast.error('Error', { + description: + 'Ha ocurrido un error al actualizar el inventario. Por favor, inténtelo de nuevo.', + }) + // Revertir el valor en caso de error + setInStockValues(prev => ({ ...prev, [id]: data.find(item => item.id === id)?.inStock || 0 })) + } + } + + const handleDiscardChanges = (id: string) => { + setInStockValues(prev => ({ + ...prev, + [id]: data.find(item => item.id === id)?.inStock || 0, + })) + setEditingId(null) + setHasUnsavedChanges(prev => ({ ...prev, [id]: false })) + } + + return ( +
+ {data.map(item => ( +
+
+ {item.images && + (Array.isArray(item.images) + ? item.images.length > 0 + : typeof item.images === 'string' && item.images !== '[]' && item.images !== '') ? ( + {item.name} + ) : ( +
+ +
+ )} +
+
{item.name}
+
SKU: {item.sku}
+
+
+ +
+
+ No disponible + {item.unavailable} +
+
+ Comprometido + {item.committed} +
+
+ Disponible + {item.available} +
+
+ En stock +
+ { + setEditingId(item.id) + setHasUnsavedChanges(prev => ({ ...prev, [item.id]: true })) + setInStockValues(prev => ({ + ...prev, + [item.id]: Number(e.target.value), + })) + }} + onBlur={() => { + if (editingId === item.id && !hasUnsavedChanges[item.id]) { + handleUpdateQuantity(item.id, item.name) + } + }} + /> + {editingId === item.id && ( + + )} +
+
+
+ {hasUnsavedChanges[item.id] && ( + handleUpdateQuantity(item.id, item.name)} + onDiscard={() => handleDiscardChanges(item.id)} + /> + )} +
+ ))} +
+ ) +} diff --git a/app/store/components/product-management/inventory/inventory-table-row.tsx b/app/store/components/product-management/inventory/inventory-table-row.tsx index 6df3b9ff..06033b21 100644 --- a/app/store/components/product-management/inventory/inventory-table-row.tsx +++ b/app/store/components/product-management/inventory/inventory-table-row.tsx @@ -1,38 +1,158 @@ import { TableRow, TableCell } from '@/components/ui/table' import { Checkbox } from '@/components/ui/checkbox' +import { Image } from 'lucide-react' +import { getStoreId } from '@/utils/store-utils' +import { useParams, usePathname } from 'next/navigation' +import { routes } from '@/utils/routes' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import Link from 'next/link' +import { useState } from 'react' +import { toast } from 'sonner' +import { useProducts } from '@/app/store/hooks/useProducts' +import { UnsavedChangesAlert } from '@/components/ui/unsaved-changes-alert' export interface InventoryRowProps { id: string + name: string sku: string unavailable: number committed: number available: number inStock: number + images?: + | Array<{ + url: string + alt?: string + }> + | string } export default function InventoryTableRow({ id, + name, sku, unavailable, committed, available, inStock, + images, }: InventoryRowProps) { + const pathname = usePathname() + const params = useParams() + const storeId = getStoreId(params, pathname) + const { updateProduct } = useProducts(storeId) + + const [availableValue, setAvailableValue] = useState(available) + const [inStockValue, setInStockValue] = useState(inStock) + const [isEditing, setIsEditing] = useState(false) + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false) + + const handleUpdateQuantity = async () => { + try { + await updateProduct({ + id, + quantity: inStockValue, + }) + toast.success('Inventario actualizado', { + description: `El inventario de "${name}" ha sido actualizado correctamente.`, + }) + setIsEditing(false) + setHasUnsavedChanges(false) + } catch (error) { + toast.error('Error', { + description: + 'Ha ocurrido un error al actualizar el inventario. Por favor, inténtelo de nuevo.', + }) + // Revertir los valores en caso de error + setAvailableValue(available) + setInStockValue(inStock) + } + } + + const handleDiscardChanges = () => { + setInStockValue(inStock) + setIsEditing(false) + setHasUnsavedChanges(false) + } + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleUpdateQuantity() + } else if (e.key === 'Escape') { + handleDiscardChanges() + } + } + return ( - - - - - {id} - {sku} - {unavailable} - {committed} - - - - - - - + <> + + + + + +
+ {images && + (Array.isArray(images) + ? images.length > 0 + : typeof images === 'string' && images !== '[]' && images !== '') ? ( + {name} + ) : ( +
+ +
+ )} + + + +
+
+ {sku} + {unavailable} + {committed} + + + + +
+ { + setIsEditing(true) + setHasUnsavedChanges(true) + setInStockValue(Number(e.target.value)) + }} + onKeyDown={handleKeyPress} + onBlur={() => { + if (isEditing && !hasUnsavedChanges) { + handleUpdateQuantity() + } + }} + /> + {isEditing && ( + + )} +
+
+
+ {hasUnsavedChanges && ( + + )} + ) } diff --git a/app/store/components/product-management/inventory/inventory-table.tsx b/app/store/components/product-management/inventory/inventory-table.tsx index b6ed6626..c2431145 100644 --- a/app/store/components/product-management/inventory/inventory-table.tsx +++ b/app/store/components/product-management/inventory/inventory-table.tsx @@ -1,5 +1,6 @@ import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/components/ui/table' import InventoryTableRow, { InventoryRowProps } from './inventory-table-row' +import { InventoryCardMobile } from './inventory-card-mobile' interface InventoryTableProps { data: InventoryRowProps[] @@ -7,25 +8,31 @@ interface InventoryTableProps { export default function InventoryTable({ data }: InventoryTableProps) { return ( -
-
- - - - Product - SKU - Unavailable - Committed - Available - In Stock - - - - {data.map(row => ( - - ))} - -
+
+ {/* Vista de escritorio */} +
+ + + + + Producto + SKU + No disponible + Comprometido + Disponible + En existencia + + + + {data.map(row => ( + + ))} + +
+
+ + {/* Vista móvil */} +
) } diff --git a/app/store/components/product-management/sections/ai-generate-button.tsx b/app/store/components/product-management/product-sections/ai-generate-button.tsx similarity index 98% rename from app/store/components/product-management/sections/ai-generate-button.tsx rename to app/store/components/product-management/product-sections/ai-generate-button.tsx index 2d170881..6e0ea44a 100644 --- a/app/store/components/product-management/sections/ai-generate-button.tsx +++ b/app/store/components/product-management/product-sections/ai-generate-button.tsx @@ -5,7 +5,7 @@ import { motion } from 'framer-motion' import { Sparkles } from 'lucide-react' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' -import { TextShimmer } from '@/app/store/components/product-management/sections/text-shimmer' +import { TextShimmer } from '@/app/store/components/product-management/product-sections/text-shimmer' interface AIGenerateButtonProps { onClick: () => Promise diff --git a/app/store/components/product-management/sections/attributes-section.tsx b/app/store/components/product-management/product-sections/attributes-section.tsx similarity index 100% rename from app/store/components/product-management/sections/attributes-section.tsx rename to app/store/components/product-management/product-sections/attributes-section.tsx diff --git a/app/store/components/product-management/sections/basic-info-section.tsx b/app/store/components/product-management/product-sections/basic-info-section.tsx similarity index 99% rename from app/store/components/product-management/sections/basic-info-section.tsx rename to app/store/components/product-management/product-sections/basic-info-section.tsx index 27a3c3c0..ad9a6b18 100644 --- a/app/store/components/product-management/sections/basic-info-section.tsx +++ b/app/store/components/product-management/product-sections/basic-info-section.tsx @@ -28,7 +28,7 @@ import { toast } from 'sonner' import type { ProductFormValues } from '@/lib/zod-schemas/product-schema' import { useProductDescription } from '@/app/store/components/product-management/hooks/useProductDescription' import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' -import { AIGenerateButton } from '@/app/store/components/product-management/sections/ai-generate-button' +import { AIGenerateButton } from '@/app/store/components/product-management/product-sections/ai-generate-button' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' interface BasicInfoSectionProps { diff --git a/app/store/components/product-management/sections/images-section.tsx b/app/store/components/product-management/product-sections/images-section.tsx similarity index 100% rename from app/store/components/product-management/sections/images-section.tsx rename to app/store/components/product-management/product-sections/images-section.tsx diff --git a/app/store/components/product-management/sections/price-suggestion-panel.tsx b/app/store/components/product-management/product-sections/price-suggestion-panel.tsx similarity index 98% rename from app/store/components/product-management/sections/price-suggestion-panel.tsx rename to app/store/components/product-management/product-sections/price-suggestion-panel.tsx index 3e366b63..a4e6b8a8 100644 --- a/app/store/components/product-management/sections/price-suggestion-panel.tsx +++ b/app/store/components/product-management/product-sections/price-suggestion-panel.tsx @@ -1,6 +1,6 @@ import { HelpCircle } from 'lucide-react' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' -import { AIGenerateButton } from '../sections/ai-generate-button' +import { AIGenerateButton } from './ai-generate-button' import type { PriceSuggestionResult } from '../hooks/usePriceSuggestion' import type { UseFormReturn } from 'react-hook-form' import type { ProductFormValues } from '@/lib/zod-schemas/product-schema' diff --git a/app/store/components/product-management/sections/pricing-inventory-section.tsx b/app/store/components/product-management/product-sections/pricing-inventory-section.tsx similarity index 96% rename from app/store/components/product-management/sections/pricing-inventory-section.tsx rename to app/store/components/product-management/product-sections/pricing-inventory-section.tsx index a669599a..5dda701b 100644 --- a/app/store/components/product-management/sections/pricing-inventory-section.tsx +++ b/app/store/components/product-management/product-sections/pricing-inventory-section.tsx @@ -16,7 +16,7 @@ import { usePriceSuggestion, type PriceSuggestionResult, } from '@/app/store/components/product-management/hooks/usePriceSuggestion' -import { PriceSuggestionPanel } from '@/app/store/components/product-management/sections/price-suggestion-panel' +import { PriceSuggestionPanel } from '@/app/store/components/product-management/product-sections/price-suggestion-panel' import { cn } from '@/lib/utils' import CurrencyInput from 'react-currency-input-field' @@ -60,8 +60,8 @@ export function PricingInventorySection({ form }: PricingInventorySectionProps) setLocalPriceResult(parsedResult) } catch (parseError) { - console.error('Error al parsear el resultado:', parseError) - throw new Error('El formato de respuesta es inválido') + console.error('Error parsing result:', parseError) + throw new Error('The response format is invalid') } } else { parsedResult = rawResult @@ -88,10 +88,10 @@ export function PricingInventorySection({ form }: PricingInventorySectionProps) } } } else { - console.warn('No se recibió un resultado válido de la API') + console.warn('No valid result was received from the API') } } catch (error) { - console.error('Error al generar sugerencia de precio:', error) + console.error('Error generating price suggestion:', error) toast.error('Error', { description: 'No se pudo generar la sugerencia de precio. Inténtelo de nuevo más tarde.', }) diff --git a/app/store/components/product-management/sections/pricing-section.tsx b/app/store/components/product-management/product-sections/pricing-section.tsx similarity index 100% rename from app/store/components/product-management/sections/pricing-section.tsx rename to app/store/components/product-management/product-sections/pricing-section.tsx diff --git a/app/store/components/product-management/sections/publication-section.tsx b/app/store/components/product-management/product-sections/publication-section.tsx similarity index 100% rename from app/store/components/product-management/sections/publication-section.tsx rename to app/store/components/product-management/product-sections/publication-section.tsx diff --git a/app/store/components/product-management/sections/text-shimmer.tsx b/app/store/components/product-management/product-sections/text-shimmer.tsx similarity index 100% rename from app/store/components/product-management/sections/text-shimmer.tsx rename to app/store/components/product-management/product-sections/text-shimmer.tsx diff --git a/app/store/components/product-management/product-table/product-pagination.tsx b/app/store/components/product-management/product-table/product-pagination.tsx index cb3af62b..752b5124 100644 --- a/app/store/components/product-management/product-table/product-pagination.tsx +++ b/app/store/components/product-management/product-table/product-pagination.tsx @@ -47,7 +47,6 @@ export function ProductPagination({ 5 10 20 - 50
diff --git a/app/store/components/sidebar/nav-main.tsx b/app/store/components/sidebar/nav-main.tsx index 37d19e95..8d4f1795 100644 --- a/app/store/components/sidebar/nav-main.tsx +++ b/app/store/components/sidebar/nav-main.tsx @@ -57,11 +57,7 @@ export function NavMain({ items }: NavMainProps) { return ( - {isLoading ? ( - - ) : ( - <>Mi tienda - {currentStore?.storeName} - )} + {isLoading ? : <> {currentStore?.storeName}} {items.map(item => { From 9ee4601ac80084b00d40d583e8e94d14007e1690 Mon Sep 17 00:00:00 2001 From: Stivenjs Date: Mon, 19 May 2025 21:47:34 -0500 Subject: [PATCH 3/3] refactor(middleware): reorganize middleware imports and enhance ownership checks Updated middleware imports to reflect new directory structure, improving clarity and organization. Enhanced product and collection ownership checks with specific URL pattern matching, ensuring better access control. Refactored related middleware functions for consistency and improved readability. --- .../collections/[collectionId]/page.tsx | 0 .../{ => products}/collections/new/page.tsx | 0 .../{ => products}/collections/page.tsx | 0 .../[slug]/{ => products}/inventory/page.tsx | 0 .../collections/collections-header.tsx | 2 +- .../collections/collections-table.tsx | 2 +- .../inventory/inventory-table.tsx | 6 ++-- app/store/components/sidebar/app-sidebar.tsx | 4 +-- app/store/layout.tsx | 2 -- middleware.ts | 29 ++++++++++++------- middlewares/{ => auth}/auth.ts | 5 +++- .../{ => ownership}/collectionOwnership.ts | 24 +++++---------- .../{ => ownership}/productOwnership.ts | 21 +++++--------- middlewares/{ => store-access}/store.ts | 2 +- middlewares/{ => store-access}/storeAccess.ts | 2 +- .../{ => subscription}/subscription.ts | 2 +- utils/routes.ts | 9 +++--- 17 files changed, 54 insertions(+), 56 deletions(-) rename app/store/[slug]/{ => products}/collections/[collectionId]/page.tsx (100%) rename app/store/[slug]/{ => products}/collections/new/page.tsx (100%) rename app/store/[slug]/{ => products}/collections/page.tsx (100%) rename app/store/[slug]/{ => products}/inventory/page.tsx (100%) rename middlewares/{ => auth}/auth.ts (90%) rename middlewares/{ => ownership}/collectionOwnership.ts (78%) rename middlewares/{ => ownership}/productOwnership.ts (82%) rename middlewares/{ => store-access}/store.ts (97%) rename middlewares/{ => store-access}/storeAccess.ts (97%) rename middlewares/{ => subscription}/subscription.ts (93%) diff --git a/app/store/[slug]/collections/[collectionId]/page.tsx b/app/store/[slug]/products/collections/[collectionId]/page.tsx similarity index 100% rename from app/store/[slug]/collections/[collectionId]/page.tsx rename to app/store/[slug]/products/collections/[collectionId]/page.tsx diff --git a/app/store/[slug]/collections/new/page.tsx b/app/store/[slug]/products/collections/new/page.tsx similarity index 100% rename from app/store/[slug]/collections/new/page.tsx rename to app/store/[slug]/products/collections/new/page.tsx diff --git a/app/store/[slug]/collections/page.tsx b/app/store/[slug]/products/collections/page.tsx similarity index 100% rename from app/store/[slug]/collections/page.tsx rename to app/store/[slug]/products/collections/page.tsx diff --git a/app/store/[slug]/inventory/page.tsx b/app/store/[slug]/products/inventory/page.tsx similarity index 100% rename from app/store/[slug]/inventory/page.tsx rename to app/store/[slug]/products/inventory/page.tsx diff --git a/app/store/components/product-management/collections/collections-header.tsx b/app/store/components/product-management/collections/collections-header.tsx index b7f47e14..69abfd07 100644 --- a/app/store/components/product-management/collections/collections-header.tsx +++ b/app/store/components/product-management/collections/collections-header.tsx @@ -19,7 +19,7 @@ export default function CollectionsHeader({ storeId }: CollectionsHeaderProps) { diff --git a/app/store/components/product-management/collections/collections-table.tsx b/app/store/components/product-management/collections/collections-table.tsx index 378255f1..720b6a46 100644 --- a/app/store/components/product-management/collections/collections-table.tsx +++ b/app/store/components/product-management/collections/collections-table.tsx @@ -47,7 +47,7 @@ export default function CollectionsTable({ collections, storeId }: CollectionsTa diff --git a/app/store/components/product-management/inventory/inventory-table.tsx b/app/store/components/product-management/inventory/inventory-table.tsx index c2431145..e484790a 100644 --- a/app/store/components/product-management/inventory/inventory-table.tsx +++ b/app/store/components/product-management/inventory/inventory-table.tsx @@ -1,6 +1,8 @@ import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/components/ui/table' -import InventoryTableRow, { InventoryRowProps } from './inventory-table-row' -import { InventoryCardMobile } from './inventory-card-mobile' +import InventoryTableRow, { + InventoryRowProps, +} from '@/app/store/components/product-management/inventory/inventory-table-row' +import { InventoryCardMobile } from '@/app/store/components/product-management/inventory/inventory-card-mobile' interface InventoryTableProps { data: InventoryRowProps[] diff --git a/app/store/components/sidebar/app-sidebar.tsx b/app/store/components/sidebar/app-sidebar.tsx index c6621188..90a1d393 100644 --- a/app/store/components/sidebar/app-sidebar.tsx +++ b/app/store/components/sidebar/app-sidebar.tsx @@ -55,11 +55,11 @@ export function AppSidebar({ ...props }: React.ComponentProps) { items: [ { title: 'Colecciones', - url: routes.store.collections(storeId), + url: routes.store.products.collections(storeId), }, { title: 'Inventario', - url: routes.store.inventory(storeId), + url: routes.store.products.inventory(storeId), }, { title: 'Categorías', diff --git a/app/store/layout.tsx b/app/store/layout.tsx index e706d000..e7574e91 100644 --- a/app/store/layout.tsx +++ b/app/store/layout.tsx @@ -27,10 +27,8 @@ Amplify.configure({ export default function StoreLayout({ children }: { children: React.ReactNode }) { const pathname = usePathname() const params = useParams() - const storeId = getStoreId(params, pathname) useStore(storeId) - const [prefersReducedMotion, setPrefersReducedMotion] = useState(false) useEffect(() => { diff --git a/middleware.ts b/middleware.ts index 80f100d9..46066bc3 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,22 +1,29 @@ import { NextRequest, NextResponse } from 'next/server' -import { handleAuthenticationMiddleware } from './middlewares/auth' -import { handleSubscriptionMiddleware } from './middlewares/subscription' -import { handleStoreMiddleware } from './middlewares/store' -import { handleStoreAccessMiddleware } from './middlewares/storeAccess' -import { handleProductOwnershipMiddleware } from './middlewares/productOwnership' -import { handleAuthenticatedRedirect } from './middlewares/auth' -import { handleCollectionOwnership } from './middlewares/collectionOwnership' +import { handleAuthenticationMiddleware } from './middlewares/auth/auth' +import { handleSubscriptionMiddleware } from './middlewares/subscription/subscription' +import { handleStoreMiddleware } from './middlewares/store-access/store' +import { handleStoreAccessMiddleware } from './middlewares/store-access/storeAccess' +import { handleProductOwnershipMiddleware } from './middlewares/ownership/productOwnership' +import { handleAuthenticatedRedirectMiddleware } from './middlewares/auth/auth' +import { handleCollectionOwnershipMiddleware } from './middlewares/ownership/collectionOwnership' export async function middleware(request: NextRequest) { const path = request.nextUrl.pathname // Verificar propiedad de productos específicos - if (path.includes('/products/') && path.match(/\/products\/([^\/]+)/)) { + if ( + path.match(/^\/store\/[^\/]+\/products\/[^\/]+$/) && + !path.includes('/products/inventory') && + !path.includes('/products/collections') + ) { return handleProductOwnershipMiddleware(request) } // verificar propiedad de coleccione especifica - if (path.includes('/collection') && path.match(/\/collections\/([^\/]+)/)) { - return handleCollectionOwnership(request) + if ( + path.match(/^\/store\/[^\/]+\/products\/collections\/[^\/]+$/) && + !path.includes('/collections/new') + ) { + return handleCollectionOwnershipMiddleware(request) } if (path.match(/^\/store\/[^\/]+/)) { @@ -36,7 +43,7 @@ export async function middleware(request: NextRequest) { return handleStoreMiddleware(request, NextResponse.next()) } if (path === '/login') { - return handleAuthenticatedRedirect(request, NextResponse.next()) + return handleAuthenticatedRedirectMiddleware(request, NextResponse.next()) } return NextResponse.next() diff --git a/middlewares/auth.ts b/middlewares/auth/auth.ts similarity index 90% rename from middlewares/auth.ts rename to middlewares/auth/auth.ts index a8719f08..7613c7ea 100644 --- a/middlewares/auth.ts +++ b/middlewares/auth/auth.ts @@ -27,7 +27,10 @@ export async function handleAuthenticationMiddleware(request: NextRequest, respo return response } -export async function handleAuthenticatedRedirect(request: NextRequest, response: NextResponse) { +export async function handleAuthenticatedRedirectMiddleware( + request: NextRequest, + response: NextResponse +) { const session = await getSession(request, response) if (session) { diff --git a/middlewares/collectionOwnership.ts b/middlewares/ownership/collectionOwnership.ts similarity index 78% rename from middlewares/collectionOwnership.ts rename to middlewares/ownership/collectionOwnership.ts index 1adf19f0..62c92720 100644 --- a/middlewares/collectionOwnership.ts +++ b/middlewares/ownership/collectionOwnership.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import { cookiesClient } from '@/utils/AmplifyUtils' -import { getSession } from './auth' +import { getSession } from '../auth/auth' /** * Middleware para verificar que un usuario solo pueda acceder a colecciones @@ -15,7 +15,7 @@ import { getSession } from './auth' * @param request - La solicitud HTTP entrante * @returns Respuesta HTTP apropiada según la verificación de propiedad */ -export async function handleCollectionOwnership(request: NextRequest) { +export async function handleCollectionOwnershipMiddleware(request: NextRequest) { // Verificar si esta es una redirección para evitar bucles const isRedirect = request.headers.get('x-redirect-check') === 'true' if (isRedirect) { @@ -42,7 +42,6 @@ export async function handleCollectionOwnership(request: NextRequest) { const currentStoreId = request.cookies.get('currentStore')?.value || storeIdFromUrl if (!currentStoreId) { - // Redirigir a la selección de tienda si no se puede determinar la tienda actual const redirectUrl = new URL('/my-store', request.url) const response = NextResponse.redirect(redirectUrl) response.headers.set('x-redirect-check', 'true') @@ -51,7 +50,6 @@ export async function handleCollectionOwnership(request: NextRequest) { try { // Verificar que el usuario tenga acceso a la tienda - // Primero intentamos verificar si es propietario const storeResult = await cookiesClient.models.UserStore.get( { id: currentStoreId, @@ -63,7 +61,6 @@ export async function handleCollectionOwnership(request: NextRequest) { // Si la tienda no existe o no pertenece al usuario, verificar si es colaborador if (!storeResult.data || storeResult.data.userId !== userId) { - // Intentar verificar acceso a través de UserStore (para colaboradores) const userStoreResult = await cookiesClient.models.UserStore.list({ filter: { storeId: { @@ -76,7 +73,6 @@ export async function handleCollectionOwnership(request: NextRequest) { authMode: 'userPool', }) - // Si no hay registros de UserStore, el usuario no tiene acceso if (!userStoreResult.data || userStoreResult.data.length === 0) { const redirectUrl = new URL('/my-store', request.url) const response = NextResponse.redirect(redirectUrl) @@ -85,15 +81,16 @@ export async function handleCollectionOwnership(request: NextRequest) { } } - // Extraer el ID de la coleccion de la URL - const collectionMatches = path.match(/\/collections\/([^\/]+)/) + // Extraer el ID de la colección de la URL + const collectionMatches = path.match(/\/collections\/([^\/]+)$/) const collectionId = collectionMatches ? collectionMatches[1] : null - // Si es la ruta "new", permitir el acceso (ya verificamos que el usuario tiene acceso a la tienda) + // Si es la ruta "new", permitir el acceso if (collectionId === 'new') { return NextResponse.next() } + // Si no hay ID de colección o es una ruta especial, permitir el acceso if (!collectionId) { return NextResponse.next() } @@ -108,27 +105,22 @@ export async function handleCollectionOwnership(request: NextRequest) { } ) - // Verificar que la coleccion exista y pertenezca a la tienda actual if (!collection) { - // La coleccion no existe, redirigir a la lista de coleccioness - const redirectUrl = new URL(`/store/${currentStoreId}/collections`, request.url) + const redirectUrl = new URL(`/store/${currentStoreId}/products/collections`, request.url) const response = NextResponse.redirect(redirectUrl) response.headers.set('x-redirect-check', 'true') return response } if (collection.storeId !== currentStoreId) { - // La coleccion pertenece a otra tienda, denegar acceso - const redirectUrl = new URL(`/store/${currentStoreId}/collections`, request.url) + const redirectUrl = new URL(`/store/${currentStoreId}/products/collections`, request.url) const response = NextResponse.redirect(redirectUrl) response.headers.set('x-redirect-check', 'true') return response } - // La coleccion pertenece a la tienda actual, continuar con la solicitud return NextResponse.next() } catch (error) { - // Error al verificar la coleccion o la tienda, redirigir por seguridad const redirectUrl = new URL(`/my-store`, request.url) const response = NextResponse.redirect(redirectUrl) response.headers.set('x-redirect-check', 'true') diff --git a/middlewares/productOwnership.ts b/middlewares/ownership/productOwnership.ts similarity index 82% rename from middlewares/productOwnership.ts rename to middlewares/ownership/productOwnership.ts index a13d6294..afb23385 100644 --- a/middlewares/productOwnership.ts +++ b/middlewares/ownership/productOwnership.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import { cookiesClient } from '@/utils/AmplifyUtils' -import { getSession } from './auth' +import { getSession } from '../auth/auth' /** * Middleware para verificar que un usuario solo pueda acceder a productos @@ -42,7 +42,6 @@ export async function handleProductOwnershipMiddleware(request: NextRequest) { const currentStoreId = request.cookies.get('currentStore')?.value || storeIdFromUrl if (!currentStoreId) { - // Redirigir a la selección de tienda si no se puede determinar la tienda actual const redirectUrl = new URL('/my-store', request.url) const response = NextResponse.redirect(redirectUrl) response.headers.set('x-redirect-check', 'true') @@ -51,7 +50,6 @@ export async function handleProductOwnershipMiddleware(request: NextRequest) { try { // Verificar que el usuario tenga acceso a la tienda - // Primero intentamos verificar si es propietario const storeResult = await cookiesClient.models.UserStore.get( { id: currentStoreId, @@ -63,7 +61,6 @@ export async function handleProductOwnershipMiddleware(request: NextRequest) { // Si la tienda no existe o no pertenece al usuario, verificar si es colaborador if (!storeResult.data || storeResult.data.userId !== userId) { - // Intentar verificar acceso a través de UserStore (para colaboradores) const userStoreResult = await cookiesClient.models.UserStore.list({ filter: { storeId: { @@ -76,7 +73,6 @@ export async function handleProductOwnershipMiddleware(request: NextRequest) { authMode: 'userPool', }) - // Si no hay registros de UserStore, el usuario no tiene acceso if (!userStoreResult.data || userStoreResult.data.length === 0) { const redirectUrl = new URL('/my-store', request.url) const response = NextResponse.redirect(redirectUrl) @@ -86,15 +82,19 @@ export async function handleProductOwnershipMiddleware(request: NextRequest) { } // Extraer el ID del producto de la URL - const productMatches = path.match(/\/products\/([^\/]+)/) + const productMatches = path.match(/\/products\/([^\/]+)$/) const productId = productMatches ? productMatches[1] : null - // Si es la ruta "new", permitir el acceso (ya verificamos que el usuario tiene acceso a la tienda) + // Si es la ruta "new", permitir el acceso if (productId === 'new') { return NextResponse.next() } - if (!productId) { + const excludedRoutes = ['/products/inventory', '/products/collections'] + + // Si no hay ID de producto o es una ruta especial, permitir el acceso + + if (!productId || excludedRoutes.some(route => path.includes(route))) { return NextResponse.next() } @@ -108,9 +108,7 @@ export async function handleProductOwnershipMiddleware(request: NextRequest) { } ) - // Verificar que el producto exista y pertenezca a la tienda actual if (!product) { - // El producto no existe, redirigir a la lista de productos const redirectUrl = new URL(`/store/${currentStoreId}/products`, request.url) const response = NextResponse.redirect(redirectUrl) response.headers.set('x-redirect-check', 'true') @@ -118,17 +116,14 @@ export async function handleProductOwnershipMiddleware(request: NextRequest) { } if (product.storeId !== currentStoreId) { - // El producto pertenece a otra tienda, denegar acceso const redirectUrl = new URL(`/store/${currentStoreId}/products`, request.url) const response = NextResponse.redirect(redirectUrl) response.headers.set('x-redirect-check', 'true') return response } - // El producto pertenece a la tienda actual, continuar con la solicitud return NextResponse.next() } catch (error) { - // Error al verificar el producto o la tienda, redirigir por seguridad const redirectUrl = new URL(`/my-store`, request.url) const response = NextResponse.redirect(redirectUrl) response.headers.set('x-redirect-check', 'true') diff --git a/middlewares/store.ts b/middlewares/store-access/store.ts similarity index 97% rename from middlewares/store.ts rename to middlewares/store-access/store.ts index 571e9a4a..db31524e 100644 --- a/middlewares/store.ts +++ b/middlewares/store-access/store.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import { cookiesClient } from '@/utils/AmplifyUtils' -import { getSession } from './auth' +import { getSession } from '../auth/auth' const STORE_LIMITS = { Imperial: 5, diff --git a/middlewares/storeAccess.ts b/middlewares/store-access/storeAccess.ts similarity index 97% rename from middlewares/storeAccess.ts rename to middlewares/store-access/storeAccess.ts index 4e809840..9b3c7f53 100644 --- a/middlewares/storeAccess.ts +++ b/middlewares/store-access/storeAccess.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { getSession } from './auth' +import { getSession } from '../auth/auth' import { cookiesClient } from '@/utils/AmplifyUtils' /** diff --git a/middlewares/subscription.ts b/middlewares/subscription/subscription.ts similarity index 93% rename from middlewares/subscription.ts rename to middlewares/subscription/subscription.ts index 104ff9fc..0ce4e6df 100644 --- a/middlewares/subscription.ts +++ b/middlewares/subscription/subscription.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { getSession } from './auth' +import { getSession } from '../auth/auth' export async function handleSubscriptionMiddleware(request: NextRequest, response: NextResponse) { const session = await getSession(request, response) diff --git a/utils/routes.ts b/utils/routes.ts index 50205c9d..bd874188 100644 --- a/utils/routes.ts +++ b/utils/routes.ts @@ -11,16 +11,17 @@ export const routes = { list: (storeId: string) => `/store/${storeId}/products/inventory `, add: (storeId: string) => `/store/${storeId}/products/new`, edit: (storeId: string, productId: string) => `/store/${storeId}/products/${productId}`, + inventory: (storeId: string) => `/store/${storeId}/products/inventory`, + collectionsNew: (storeId: string) => `/store/${storeId}/products/collections/new`, + collections: (storeId: string) => `/store/${storeId}/products/collections`, + collectionsEdit: (storeId: string, collectionId: string) => + `/store/${storeId}/products/collections/${collectionId}`, }, orders: (storeId: string) => `/store/${storeId}/orders`, customers: (storeId: string) => `/store/${storeId}/customers`, masterShop: (storeId: string) => `/store/${storeId}/mastershop`, collections: (storeId: string) => `/store/${storeId}/collections`, categories: (storeId: string) => `/store/${storeId}/categories`, - inventory: (storeId: string) => `/store/${storeId}/inventory`, - collectionsNew: (storeId: string) => `/store/${storeId}/collections/new`, - collectionsEdit: (storeId: string, collectionId: string) => - `/store/${storeId}/collections/${collectionId}`, setup: { main: (storeId: string) => `/store/${storeId}/setup`,