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>
153 lines
5.4 KiB
TypeScript
153 lines
5.4 KiB
TypeScript
import { cn } from '@utils/cn';
|
|
import { WarehouseTypeBadge } from './WarehouseTypeBadge';
|
|
import type { Warehouse } from '../types';
|
|
|
|
export interface WarehouseCardProps {
|
|
warehouse: Warehouse;
|
|
onClick?: () => void;
|
|
onEdit?: () => void;
|
|
onDelete?: () => void;
|
|
className?: string;
|
|
}
|
|
|
|
export function WarehouseCard({
|
|
warehouse,
|
|
onClick,
|
|
onEdit,
|
|
onDelete,
|
|
className,
|
|
}: WarehouseCardProps) {
|
|
const address = [
|
|
warehouse.addressLine1,
|
|
warehouse.city,
|
|
warehouse.state,
|
|
warehouse.country,
|
|
].filter(Boolean).join(', ');
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-800',
|
|
onClick && 'cursor-pointer',
|
|
className
|
|
)}
|
|
onClick={onClick}
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{warehouse.name}
|
|
</h3>
|
|
{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>
|
|
)}
|
|
</div>
|
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
Codigo: {warehouse.code}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<WarehouseTypeBadge type={warehouse.warehouseType} />
|
|
<span
|
|
className={cn(
|
|
'inline-flex h-2 w-2 rounded-full',
|
|
warehouse.isActive ? 'bg-green-500' : 'bg-gray-300'
|
|
)}
|
|
title={warehouse.isActive ? 'Activo' : 'Inactivo'}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{warehouse.description && (
|
|
<p className="mt-2 text-sm text-gray-600 dark:text-gray-300 line-clamp-2">
|
|
{warehouse.description}
|
|
</p>
|
|
)}
|
|
|
|
{address && (
|
|
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
|
<span className="inline-block w-4" title="Direccion">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="h-4 w-4">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
|
|
</svg>
|
|
</span>
|
|
{address}
|
|
</p>
|
|
)}
|
|
|
|
{warehouse.managerName && (
|
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
<span className="inline-block w-4" title="Responsable">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="h-4 w-4">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
|
|
</svg>
|
|
</span>
|
|
{warehouse.managerName}
|
|
</p>
|
|
)}
|
|
|
|
{(warehouse.capacityUnits || warehouse.capacityVolume || warehouse.capacityWeight) && (
|
|
<div className="mt-3 flex gap-4 border-t border-gray-100 pt-3 dark:border-gray-700">
|
|
{warehouse.capacityUnits && (
|
|
<div className="text-center">
|
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
|
{warehouse.capacityUnits.toLocaleString()}
|
|
</p>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400">Unidades</p>
|
|
</div>
|
|
)}
|
|
{warehouse.capacityVolume && (
|
|
<div className="text-center">
|
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
|
{warehouse.capacityVolume.toLocaleString()} m3
|
|
</p>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400">Volumen</p>
|
|
</div>
|
|
)}
|
|
{warehouse.capacityWeight && (
|
|
<div className="text-center">
|
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
|
{warehouse.capacityWeight.toLocaleString()} kg
|
|
</p>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400">Peso</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{(onEdit || onDelete) && (
|
|
<div className="mt-3 flex justify-end gap-2 border-t border-gray-100 pt-3 dark:border-gray-700">
|
|
{onEdit && (
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onEdit();
|
|
}}
|
|
className="rounded px-3 py-1 text-sm text-primary-600 hover:bg-primary-50 dark:text-primary-400 dark:hover:bg-primary-900/20"
|
|
>
|
|
Editar
|
|
</button>
|
|
)}
|
|
{onDelete && (
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onDelete();
|
|
}}
|
|
className="rounded px-3 py-1 text-sm text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
|
|
>
|
|
Eliminar
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|