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/[slug]/inventory/page.tsx b/app/store/[slug]/inventory/page.tsx deleted file mode 100644 index 72ae590a..00000000 --- a/app/store/[slug]/inventory/page.tsx +++ /dev/null @@ -1,19 +0,0 @@ -'use client' - -import { InventoryTracking } from '@/app/store/components/product-management/InventoryTracking' -import { Amplify } from 'aws-amplify' -import outputs from '@/amplify_outputs.json' - -Amplify.configure(outputs) -const existingConfig = Amplify.getConfig() -Amplify.configure({ - ...existingConfig, - API: { - ...existingConfig.API, - REST: outputs.custom.APIs, - }, -}) - -export default function InventoryPage() { - return -} 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]/products/inventory/page.tsx b/app/store/[slug]/products/inventory/page.tsx new file mode 100644 index 00000000..893af607 --- /dev/null +++ b/app/store/[slug]/products/inventory/page.tsx @@ -0,0 +1,24 @@ +'use client' + +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) +const existingConfig = Amplify.getConfig() +Amplify.configure({ + ...existingConfig, + API: { + ...existingConfig.API, + REST: outputs.custom.APIs, + }, +}) + +export default function InventoryPage() { + 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 a2720cfc..c78cf16c 100644 --- a/app/store/components/product-management/InventoryTracking.tsx +++ b/app/store/components/product-management/InventoryTracking.tsx @@ -1,33 +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 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() { +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/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/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-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-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-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..06033b21 --- /dev/null +++ b/app/store/components/product-management/inventory/inventory-table-row.tsx @@ -0,0 +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 ( + <> + + + + + +
+ {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 new file mode 100644 index 00000000..e484790a --- /dev/null +++ b/app/store/components/product-management/inventory/inventory-table.tsx @@ -0,0 +1,40 @@ +import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/components/ui/table' +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[] +} + +export default function InventoryTable({ data }: InventoryTableProps) { + return ( +
+ {/* 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/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/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/components/sidebar/nav-main.tsx b/app/store/components/sidebar/nav-main.tsx index 5643ab8e..8d4f1795 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,81 @@ export function NavMain({ items }: NavMainProps) { return ( - Mi tienda - {isLoading ? 'Cargando...' : currentStore?.storeName} + {isLoading ? : <> {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/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 8ed31815..46066bc3 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,21 +1,33 @@ 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 { 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 - Corregido el patrón de URL - if (path.includes('/products/') && path.match(/\/products\/([^\/]+)/)) { + // Verificar propiedad de productos específicos + if ( + path.match(/^\/store\/[^\/]+\/products\/[^\/]+$/) && + !path.includes('/products/inventory') && + !path.includes('/products/collections') + ) { return handleProductOwnershipMiddleware(request) } + // verificar propiedad de coleccione especifica + if ( + path.match(/^\/store\/[^\/]+\/products\/collections\/[^\/]+$/) && + !path.includes('/collections/new') + ) { + return handleCollectionOwnershipMiddleware(request) + } - // Proteger todas las rutas de tienda if (path.match(/^\/store\/[^\/]+/)) { + // Proteger todas las rutas de tienda return handleStoreAccessMiddleware(request) } @@ -31,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 85% rename from middlewares/auth.ts rename to middlewares/auth/auth.ts index a688d453..7613c7ea 100644 --- a/middlewares/auth.ts +++ b/middlewares/auth/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({ @@ -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/ownership/collectionOwnership.ts b/middlewares/ownership/collectionOwnership.ts new file mode 100644 index 00000000..62c92720 --- /dev/null +++ b/middlewares/ownership/collectionOwnership.ts @@ -0,0 +1,129 @@ +import { NextRequest, NextResponse } from 'next/server' +import { cookiesClient } from '@/utils/AmplifyUtils' +import { getSession } from '../auth/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 handleCollectionOwnershipMiddleware(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) { + 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 + 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) { + const userStoreResult = await cookiesClient.models.UserStore.list({ + filter: { + storeId: { + eq: currentStoreId, + }, + userId: { + eq: userId, + }, + }, + authMode: 'userPool', + }) + + 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 colección de la URL + const collectionMatches = path.match(/\/collections\/([^\/]+)$/) + const collectionId = collectionMatches ? collectionMatches[1] : null + + // 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() + } + + // Para colecciones existentes, verificar que pertenezcan a la tienda actual + const { data: collection } = await cookiesClient.models.Collection.get( + { + id: collectionId, + }, + { + authMode: 'userPool', + } + ) + + if (!collection) { + 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) { + const redirectUrl = new URL(`/store/${currentStoreId}/products/collections`, request.url) + const response = NextResponse.redirect(redirectUrl) + response.headers.set('x-redirect-check', 'true') + return response + } + + return NextResponse.next() + } catch (error) { + 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/ownership/productOwnership.ts similarity index 81% rename from middlewares/productOwnership.ts rename to middlewares/ownership/productOwnership.ts index a1843b22..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/amplify-utils' -import { getSession } from './auth' +import { cookiesClient } from '@/utils/AmplifyUtils' +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 95% rename from middlewares/store.ts rename to middlewares/store-access/store.ts index aea09219..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/amplify-utils' -import { getSession } from './auth' +import { cookiesClient } from '@/utils/AmplifyUtils' +import { getSession } from '../auth/auth' const STORE_LIMITS = { Imperial: 5, diff --git a/middlewares/storeAccess.ts b/middlewares/store-access/storeAccess.ts similarity index 94% rename from middlewares/storeAccess.ts rename to middlewares/store-access/storeAccess.ts index bc5eb5c8..9b3c7f53 100644 --- a/middlewares/storeAccess.ts +++ b/middlewares/store-access/storeAccess.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { getSession } from './auth' -import { cookiesClient } from '@/utils/amplify-utils' +import { getSession } from '../auth/auth' +import { cookiesClient } from '@/utils/AmplifyUtils' /** * Middleware para proteger las rutas de tienda 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/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 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`,