erp-core-frontend-v2/src/features/warehouses/pages/WarehouseDetailPage.tsx
Adrian Flores Cortes 158ebcb57b [TASK-MASTER] feat: FE-001 products + FE-002 warehouses features
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>
2026-02-04 00:16:34 -06:00

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>
);
}