FE-001: Products Feature (16 archivos) - types/index.ts - Interfaces TypeScript - api/products.api.ts, categories.api.ts - Clientes Axios - hooks/useProducts, useCategories, useProductPricing - components/ProductForm, ProductCard, CategoryTree, VariantSelector, PricingTable - pages/ProductsPage, ProductDetailPage, CategoriesPage FE-002: Warehouses Feature (15 archivos) - types/index.ts - Interfaces TypeScript - api/warehouses.api.ts - Cliente Axios - hooks/useWarehouses, useLocations - components/WarehouseCard, LocationGrid, WarehouseLayout, ZoneCard, badges - pages/WarehousesPage, WarehouseDetailPage, LocationsPage, ZonesPage Ambos siguen patrones de inventory y React Query + react-hook-form + zod Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
275 lines
12 KiB
TypeScript
275 lines
12 KiB
TypeScript
import { useState, useCallback } from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import { Button } from '@components/atoms/Button';
|
|
import { useWarehouse, useLocations } from '../hooks';
|
|
import { WarehouseTypeBadge, LocationGrid, WarehouseLayout } from '../components';
|
|
import type { WarehouseLocation } from '../types';
|
|
|
|
type ViewMode = 'list' | 'layout';
|
|
|
|
export function WarehouseDetailPage() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const navigate = useNavigate();
|
|
const { warehouse, isLoading: loadingWarehouse, error: warehouseError } = useWarehouse(id ?? null);
|
|
const {
|
|
locations,
|
|
isLoading: loadingLocations,
|
|
deleteLocation,
|
|
} = useLocations(id ?? null);
|
|
|
|
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
|
const [selectedLocationId, setSelectedLocationId] = useState<string | undefined>();
|
|
|
|
const handleBack = useCallback(() => {
|
|
navigate('/warehouses');
|
|
}, [navigate]);
|
|
|
|
const handleEdit = useCallback(() => {
|
|
navigate(`/warehouses/${id}/edit`);
|
|
}, [navigate, id]);
|
|
|
|
const handleAddLocation = useCallback(() => {
|
|
navigate(`/warehouses/${id}/locations/new`);
|
|
}, [navigate, id]);
|
|
|
|
const handleLocationClick = useCallback((location: WarehouseLocation) => {
|
|
setSelectedLocationId(location.id);
|
|
}, []);
|
|
|
|
const handleLocationEdit = useCallback((location: WarehouseLocation) => {
|
|
navigate(`/warehouses/${id}/locations/${location.id}/edit`);
|
|
}, [navigate, id]);
|
|
|
|
const handleLocationDelete = useCallback(async (location: WarehouseLocation) => {
|
|
if (!confirm(`Esta seguro de eliminar la ubicacion "${location.name}"?`)) {
|
|
return;
|
|
}
|
|
await deleteLocation(location.id);
|
|
}, [deleteLocation]);
|
|
|
|
if (warehouseError) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center py-12">
|
|
<p className="text-red-600 dark:text-red-400">{warehouseError}</p>
|
|
<Button onClick={handleBack} className="mt-4">
|
|
Volver
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (loadingWarehouse || !warehouse) {
|
|
return (
|
|
<div className="flex justify-center py-12">
|
|
<svg className="h-8 w-8 animate-spin text-primary-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const address = [
|
|
warehouse.addressLine1,
|
|
warehouse.addressLine2,
|
|
warehouse.city,
|
|
warehouse.state,
|
|
warehouse.postalCode,
|
|
warehouse.country,
|
|
].filter(Boolean).join(', ');
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<button
|
|
onClick={handleBack}
|
|
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="h-5 w-5">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
|
</svg>
|
|
</button>
|
|
<div>
|
|
<div className="flex items-center gap-3">
|
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
{warehouse.name}
|
|
</h1>
|
|
<WarehouseTypeBadge type={warehouse.warehouseType} />
|
|
{warehouse.isDefault && (
|
|
<span className="rounded bg-primary-100 px-2 py-0.5 text-xs font-medium text-primary-800 dark:bg-primary-900 dark:text-primary-300">
|
|
Por defecto
|
|
</span>
|
|
)}
|
|
<span
|
|
className={`h-2 w-2 rounded-full ${warehouse.isActive ? 'bg-green-500' : 'bg-gray-300'}`}
|
|
title={warehouse.isActive ? 'Activo' : 'Inactivo'}
|
|
/>
|
|
</div>
|
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
Codigo: {warehouse.code}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Button onClick={handleEdit} variant="outline">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="mr-2 h-4 w-4">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
|
|
</svg>
|
|
Editar
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Info Grid */}
|
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
{/* Details Card */}
|
|
<div className="rounded-lg bg-white p-6 shadow dark:bg-gray-800">
|
|
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Detalles</h3>
|
|
<dl className="mt-4 space-y-3">
|
|
{warehouse.description && (
|
|
<div>
|
|
<dt className="text-sm text-gray-500 dark:text-gray-400">Descripcion</dt>
|
|
<dd className="text-sm text-gray-900 dark:text-white">{warehouse.description}</dd>
|
|
</div>
|
|
)}
|
|
{address && (
|
|
<div>
|
|
<dt className="text-sm text-gray-500 dark:text-gray-400">Direccion</dt>
|
|
<dd className="text-sm text-gray-900 dark:text-white">{address}</dd>
|
|
</div>
|
|
)}
|
|
{warehouse.managerName && (
|
|
<div>
|
|
<dt className="text-sm text-gray-500 dark:text-gray-400">Responsable</dt>
|
|
<dd className="text-sm text-gray-900 dark:text-white">{warehouse.managerName}</dd>
|
|
</div>
|
|
)}
|
|
{warehouse.phone && (
|
|
<div>
|
|
<dt className="text-sm text-gray-500 dark:text-gray-400">Telefono</dt>
|
|
<dd className="text-sm text-gray-900 dark:text-white">{warehouse.phone}</dd>
|
|
</div>
|
|
)}
|
|
{warehouse.email && (
|
|
<div>
|
|
<dt className="text-sm text-gray-500 dark:text-gray-400">Email</dt>
|
|
<dd className="text-sm text-gray-900 dark:text-white">{warehouse.email}</dd>
|
|
</div>
|
|
)}
|
|
</dl>
|
|
</div>
|
|
|
|
{/* Capacity Card */}
|
|
<div className="rounded-lg bg-white p-6 shadow dark:bg-gray-800">
|
|
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Capacidad</h3>
|
|
<div className="mt-4 grid grid-cols-3 gap-4">
|
|
<div className="text-center">
|
|
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
{warehouse.capacityUnits?.toLocaleString() ?? '-'}
|
|
</p>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Unidades</p>
|
|
</div>
|
|
<div className="text-center">
|
|
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
{warehouse.capacityVolume ? `${warehouse.capacityVolume} m3` : '-'}
|
|
</p>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Volumen</p>
|
|
</div>
|
|
<div className="text-center">
|
|
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
{warehouse.capacityWeight ? `${warehouse.capacityWeight} kg` : '-'}
|
|
</p>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Peso</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Settings Card */}
|
|
<div className="rounded-lg bg-white p-6 shadow dark:bg-gray-800">
|
|
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Configuracion</h3>
|
|
<dl className="mt-4 space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<dt className="text-sm text-gray-500 dark:text-gray-400">Permitir stock negativo</dt>
|
|
<dd>
|
|
<span className={`rounded px-2 py-1 text-xs font-medium ${warehouse.settings?.allowNegative ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'}`}>
|
|
{warehouse.settings?.allowNegative ? 'Si' : 'No'}
|
|
</span>
|
|
</dd>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<dt className="text-sm text-gray-500 dark:text-gray-400">Reorden automatico</dt>
|
|
<dd>
|
|
<span className={`rounded px-2 py-1 text-xs font-medium ${warehouse.settings?.autoReorder ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'}`}>
|
|
{warehouse.settings?.autoReorder ? 'Si' : 'No'}
|
|
</span>
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Locations Section */}
|
|
<div className="rounded-lg bg-white p-6 shadow dark:bg-gray-800">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-lg font-medium text-gray-900 dark:text-white">
|
|
Ubicaciones
|
|
</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
{locations.length} ubicaciones configuradas
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{/* View Toggle */}
|
|
<div className="flex rounded-lg border border-gray-200 dark:border-gray-700">
|
|
<button
|
|
onClick={() => setViewMode('list')}
|
|
className={`px-3 py-1.5 text-sm ${viewMode === 'list' ? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300' : 'text-gray-600 hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-700'}`}
|
|
>
|
|
Lista
|
|
</button>
|
|
<button
|
|
onClick={() => setViewMode('layout')}
|
|
className={`px-3 py-1.5 text-sm ${viewMode === 'layout' ? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300' : 'text-gray-600 hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-700'}`}
|
|
>
|
|
Layout
|
|
</button>
|
|
</div>
|
|
<Button onClick={handleAddLocation} size="sm">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="mr-1 h-4 w-4">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
</svg>
|
|
Nueva Ubicacion
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6">
|
|
{loadingLocations ? (
|
|
<div className="flex justify-center py-8">
|
|
<svg className="h-6 w-6 animate-spin text-primary-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
</div>
|
|
) : viewMode === 'list' ? (
|
|
<LocationGrid
|
|
locations={locations}
|
|
onLocationClick={handleLocationClick}
|
|
onLocationEdit={handleLocationEdit}
|
|
onLocationDelete={handleLocationDelete}
|
|
selectedLocationId={selectedLocationId}
|
|
/>
|
|
) : (
|
|
<WarehouseLayout
|
|
locations={locations}
|
|
onLocationClick={handleLocationClick}
|
|
selectedLocationId={selectedLocationId}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|