Fix UI for inventory page
This commit is contained in:
@@ -1,34 +1,42 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Plus, Download, AlertTriangle, Package, Clock, CheckCircle, Eye, Edit, Calendar, DollarSign, ArrowRight, TrendingUp, Shield } from 'lucide-react';
|
||||
import { Plus, AlertTriangle, Package, CheckCircle, Eye, Edit, Clock, Euro, ArrowRight } from 'lucide-react';
|
||||
import { Button, Input, Card, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
|
||||
import { LoadingSpinner } from '../../../../components/shared';
|
||||
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { LowStockAlert } from '../../../../components/domain/inventory';
|
||||
import { useIngredients, useStockAnalytics } from '../../../../api/hooks/inventory';
|
||||
import { LowStockAlert, InventoryModal } from '../../../../components/domain/inventory';
|
||||
import { useIngredients, useStockAnalytics, useUpdateIngredient, useCreateIngredient } from '../../../../api/hooks/inventory';
|
||||
import { useCurrentTenant } from '../../../../stores/tenant.store';
|
||||
import { IngredientResponse } from '../../../../api/types/inventory';
|
||||
import { IngredientResponse, IngredientUpdate, IngredientCreate, UnitOfMeasure, IngredientCategory, ProductType } from '../../../../api/types/inventory';
|
||||
|
||||
const InventoryPage: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
|
||||
const [modalMode, setModalMode] = useState<'create' | 'view' | 'edit'>('view');
|
||||
const [selectedItem, setSelectedItem] = useState<IngredientResponse | null>(null);
|
||||
|
||||
const [formData, setFormData] = useState<Partial<IngredientCreate & IngredientUpdate & { initial_stock?: number; product_type?: string }>>({});
|
||||
|
||||
const currentTenant = useCurrentTenant();
|
||||
const tenantId = currentTenant?.id || '';
|
||||
|
||||
|
||||
|
||||
// API Data
|
||||
const {
|
||||
data: ingredientsData,
|
||||
isLoading: ingredientsLoading,
|
||||
error: ingredientsError
|
||||
const {
|
||||
data: ingredientsData,
|
||||
isLoading: ingredientsLoading,
|
||||
error: ingredientsError
|
||||
} = useIngredients(tenantId, { search: searchTerm || undefined });
|
||||
|
||||
|
||||
const {
|
||||
data: analyticsData,
|
||||
isLoading: analyticsLoading
|
||||
const {
|
||||
data: analyticsData,
|
||||
isLoading: analyticsLoading
|
||||
} = useStockAnalytics(tenantId);
|
||||
|
||||
// Mutations
|
||||
const updateIngredientMutation = useUpdateIngredient();
|
||||
const createIngredientMutation = useCreateIngredient();
|
||||
|
||||
const ingredients = ingredientsData || [];
|
||||
const lowStockItems = ingredients.filter(ingredient => ingredient.stock_status === 'low_stock');
|
||||
@@ -73,6 +81,125 @@ const InventoryPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Form handlers
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (modalMode === 'create') {
|
||||
// Create new ingredient - ensure required fields are present
|
||||
const createData: IngredientCreate = {
|
||||
name: formData.name || '',
|
||||
unit_of_measure: formData.unit_of_measure || UnitOfMeasure.GRAMS,
|
||||
low_stock_threshold: formData.low_stock_threshold || 10,
|
||||
reorder_point: formData.reorder_point || 20,
|
||||
description: formData.description,
|
||||
category: formData.category,
|
||||
max_stock_level: formData.max_stock_level,
|
||||
shelf_life_days: formData.shelf_life_days,
|
||||
requires_refrigeration: formData.requires_refrigeration,
|
||||
requires_freezing: formData.requires_freezing,
|
||||
is_seasonal: formData.is_seasonal,
|
||||
supplier_id: formData.supplier_id,
|
||||
average_cost: formData.average_cost,
|
||||
notes: formData.notes
|
||||
};
|
||||
|
||||
const createdItem = await createIngredientMutation.mutateAsync({
|
||||
tenantId,
|
||||
ingredientData: createData
|
||||
});
|
||||
|
||||
// TODO: Handle initial stock if provided
|
||||
// if (formData.initial_stock && formData.initial_stock > 0) {
|
||||
// // Add initial stock using stock transaction API
|
||||
// await addStockMutation.mutateAsync({
|
||||
// tenantId,
|
||||
// ingredientId: createdItem.id,
|
||||
// quantity: formData.initial_stock,
|
||||
// transaction_type: 'addition',
|
||||
// notes: 'Stock inicial'
|
||||
// });
|
||||
// }
|
||||
} else {
|
||||
// Update existing ingredient
|
||||
if (!selectedItem) return;
|
||||
await updateIngredientMutation.mutateAsync({
|
||||
tenantId,
|
||||
ingredientId: selectedItem.id,
|
||||
updateData: formData as IngredientUpdate
|
||||
});
|
||||
}
|
||||
|
||||
// Reset form data and close modal
|
||||
setFormData({});
|
||||
setModalMode('view');
|
||||
setShowForm(false);
|
||||
setSelectedItem(null);
|
||||
} catch (error) {
|
||||
console.error(`Error ${modalMode === 'create' ? 'creating' : 'updating'} ingredient:`, error);
|
||||
// TODO: Show error toast
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setFormData({});
|
||||
setModalMode('view');
|
||||
setShowForm(false);
|
||||
setSelectedItem(null);
|
||||
};
|
||||
|
||||
const handleFieldChange = (sectionIndex: number, fieldIndex: number, value: string | number) => {
|
||||
// Map section and field indexes to actual field names
|
||||
// Different mapping for create vs view/edit modes
|
||||
const createFieldMap: { [key: string]: string } = {
|
||||
'0-0': 'product_type', // Basic Info - Product Type
|
||||
'0-1': 'name', // Basic Info - Name
|
||||
'0-2': 'category', // Basic Info - Category
|
||||
'0-3': 'unit_of_measure', // Basic Info - Unit of measure
|
||||
'1-0': 'initial_stock', // Stock Config - Initial stock
|
||||
'1-1': 'low_stock_threshold', // Stock Config - Threshold
|
||||
'1-2': 'reorder_point', // Stock Config - Reorder point
|
||||
};
|
||||
|
||||
const viewEditFieldMap: { [key: string]: string } = {
|
||||
'0-0': 'name', // Basic Info - Name
|
||||
'0-1': 'category', // Basic Info - Category
|
||||
'0-2': 'description', // Basic Info - Description
|
||||
'0-3': 'unit_of_measure', // Basic Info - Unit of measure
|
||||
'1-1': 'low_stock_threshold', // Stock - Threshold
|
||||
'1-2': 'max_stock_level', // Stock - Max level
|
||||
'1-3': 'reorder_point', // Stock - Reorder point
|
||||
'1-4': 'reorder_quantity', // Stock - Reorder quantity
|
||||
'2-0': 'average_cost', // Financial - Average cost
|
||||
'3-1': 'shelf_life_days', // Additional - Shelf life
|
||||
'3-2': 'requires_refrigeration', // Additional - Refrigeration
|
||||
'3-3': 'requires_freezing', // Additional - Freezing
|
||||
'3-4': 'is_seasonal', // Additional - Seasonal
|
||||
'4-0': 'notes' // Notes - Observations
|
||||
};
|
||||
|
||||
// Use appropriate field map based on modal mode
|
||||
const fieldMap = modalMode === 'create' ? createFieldMap : viewEditFieldMap;
|
||||
|
||||
// Boolean field mapping for proper conversion
|
||||
const booleanFields = ['requires_refrigeration', 'requires_freezing', 'is_seasonal'];
|
||||
|
||||
const fieldKey = `${sectionIndex}-${fieldIndex}`;
|
||||
const fieldName = fieldMap[fieldKey];
|
||||
|
||||
if (fieldName) {
|
||||
// Convert string boolean values to actual booleans for boolean fields
|
||||
const processedValue = booleanFields.includes(fieldName)
|
||||
? value === 'true'
|
||||
: value;
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[fieldName]: processedValue
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
if (!searchTerm) return ingredients;
|
||||
|
||||
@@ -84,6 +211,55 @@ const InventoryPage: React.FC = () => {
|
||||
});
|
||||
}, [ingredients, searchTerm]);
|
||||
|
||||
// Helper function to get category display name
|
||||
const getCategoryDisplayName = (category?: string): string => {
|
||||
const categoryMappings: Record<string, string> = {
|
||||
'flour': 'Harina',
|
||||
'dairy': 'Lácteos',
|
||||
'eggs': 'Huevos',
|
||||
'sugar': 'Azúcar',
|
||||
'yeast': 'Levadura',
|
||||
'fats': 'Grasas',
|
||||
'spices': 'Especias',
|
||||
'croissants': 'Croissants',
|
||||
'pastries': 'Pastelería',
|
||||
'beverages': 'Bebidas',
|
||||
'bread': 'Pan',
|
||||
'other': 'Otros'
|
||||
};
|
||||
|
||||
return categoryMappings[category?.toLowerCase() || ''] || category || 'Sin categoría';
|
||||
};
|
||||
|
||||
// Simplified item action handler
|
||||
const handleItemAction = (ingredient: any, action: 'view' | 'edit') => {
|
||||
setSelectedItem(ingredient);
|
||||
setModalMode(action);
|
||||
setFormData({});
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
// Handle new item creation
|
||||
const handleNewItem = () => {
|
||||
setSelectedItem(null);
|
||||
setModalMode('create');
|
||||
setFormData({
|
||||
product_type: 'ingredient', // Default to ingredient
|
||||
name: '',
|
||||
unit_of_measure: UnitOfMeasure.GRAMS,
|
||||
low_stock_threshold: 10,
|
||||
reorder_point: 20,
|
||||
reorder_quantity: 50,
|
||||
category: IngredientCategory.OTHER,
|
||||
requires_refrigeration: false,
|
||||
requires_freezing: false,
|
||||
is_seasonal: false
|
||||
});
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const inventoryStats = useMemo(() => {
|
||||
if (!analyticsData) {
|
||||
return {
|
||||
@@ -91,8 +267,8 @@ const InventoryPage: React.FC = () => {
|
||||
lowStockItems: lowStockItems.length,
|
||||
outOfStock: ingredients.filter(item => item.stock_status === 'out_of_stock').length,
|
||||
expiringSoon: 0, // This would come from expired stock API
|
||||
totalValue: ingredients.reduce((sum, item) => sum + (item.current_stock_level * (item.average_cost || 0)), 0),
|
||||
categories: [...new Set(ingredients.map(item => item.category))].length,
|
||||
totalValue: ingredients.reduce((sum, item) => sum + ((item.current_stock || 0) * (item.average_cost || 0)), 0),
|
||||
categories: Array.from(new Set(ingredients.map(item => item.category))).length,
|
||||
turnoverRate: 0,
|
||||
fastMovingItems: 0,
|
||||
qualityScore: 85,
|
||||
@@ -101,7 +277,7 @@ const InventoryPage: React.FC = () => {
|
||||
}
|
||||
|
||||
// Extract data from new analytics structure
|
||||
const stockoutCount = Object.values(analyticsData.stockout_frequency || {}).reduce((sum: number, count) => sum + count, 0);
|
||||
const stockoutCount = Object.values(analyticsData.stockout_frequency || {}).reduce((sum: number, count: unknown) => sum + (typeof count === 'number' ? count : 0), 0);
|
||||
const fastMovingCount = (analyticsData.fast_moving_items || []).length;
|
||||
|
||||
return {
|
||||
@@ -110,7 +286,7 @@ const InventoryPage: React.FC = () => {
|
||||
outOfStock: stockoutCount || ingredients.filter(item => item.stock_status === 'out_of_stock').length,
|
||||
expiringSoon: Math.round(Number(analyticsData.quality_incidents_rate || 0) * ingredients.length), // Estimate from quality incidents
|
||||
totalValue: Number(analyticsData.total_inventory_cost || 0),
|
||||
categories: Object.keys(analyticsData.cost_by_category || {}).length || [...new Set(ingredients.map(item => item.category))].length,
|
||||
categories: Object.keys(analyticsData.cost_by_category || {}).length || Array.from(new Set(ingredients.map(item => item.category))).length,
|
||||
turnoverRate: Number(analyticsData.inventory_turnover_rate || 0),
|
||||
fastMovingItems: fastMovingCount,
|
||||
qualityScore: Number(analyticsData.food_safety_score || 85),
|
||||
@@ -147,7 +323,7 @@ const InventoryPage: React.FC = () => {
|
||||
title: 'Valor Total',
|
||||
value: formatters.currency(inventoryStats.totalValue),
|
||||
variant: 'success' as const,
|
||||
icon: DollarSign,
|
||||
icon: Euro,
|
||||
},
|
||||
{
|
||||
title: 'Tasa Rotación',
|
||||
@@ -155,18 +331,6 @@ const InventoryPage: React.FC = () => {
|
||||
variant: 'info' as const,
|
||||
icon: ArrowRight,
|
||||
},
|
||||
{
|
||||
title: 'Items Dinámicos',
|
||||
value: inventoryStats.fastMovingItems,
|
||||
variant: 'success' as const,
|
||||
icon: TrendingUp,
|
||||
},
|
||||
{
|
||||
title: 'Puntuación Calidad',
|
||||
value: `${inventoryStats.qualityScore}%`,
|
||||
variant: inventoryStats.qualityScore >= 90 ? 'success' as const : inventoryStats.qualityScore >= 70 ? 'warning' as const : 'error' as const,
|
||||
icon: Shield,
|
||||
},
|
||||
];
|
||||
|
||||
// Loading and error states
|
||||
@@ -201,19 +365,12 @@ const InventoryPage: React.FC = () => {
|
||||
title="Gestión de Inventario"
|
||||
description="Controla el stock de ingredientes y materias primas"
|
||||
actions={[
|
||||
{
|
||||
id: "export",
|
||||
label: "Exportar",
|
||||
variant: "outline" as const,
|
||||
icon: Download,
|
||||
onClick: () => console.log('Export inventory')
|
||||
},
|
||||
{
|
||||
id: "new",
|
||||
label: "Nuevo Artículo",
|
||||
variant: "primary" as const,
|
||||
icon: Plus,
|
||||
onClick: () => setShowForm(true)
|
||||
onClick: handleNewItem
|
||||
}
|
||||
]}
|
||||
/>
|
||||
@@ -221,96 +378,9 @@ const InventoryPage: React.FC = () => {
|
||||
{/* Stats Grid */}
|
||||
<StatsGrid
|
||||
stats={stats}
|
||||
columns={4}
|
||||
columns={3}
|
||||
/>
|
||||
|
||||
{/* Analytics Section */}
|
||||
{analyticsData && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Fast Moving Items */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5 text-green-500" />
|
||||
Items de Alta Rotación
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{(analyticsData.fast_moving_items || []).slice(0, 5).map((item: any, index: number) => (
|
||||
<div key={item.ingredient_id || index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<p className="font-medium">{item.name}</p>
|
||||
<p className="text-sm text-gray-600">{item.movement_count} movimientos</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium">{formatters.currency(item.avg_cost)}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{(!analyticsData.fast_moving_items || analyticsData.fast_moving_items.length === 0) && (
|
||||
<p className="text-gray-500 text-center py-4">No hay datos de items de alta rotación</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Cost by Category */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<Package className="w-5 h-5 text-blue-500" />
|
||||
Costos por Categoría
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{Object.entries(analyticsData.cost_by_category || {}).slice(0, 5).map(([category, cost]) => (
|
||||
<div key={category} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<p className="font-medium capitalize">{category}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium">{formatters.currency(Number(cost))}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{Object.keys(analyticsData.cost_by_category || {}).length === 0 && (
|
||||
<p className="text-gray-500 text-center py-4">No hay datos de costos por categoría</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Efficiency Metrics */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<Shield className="w-5 h-5 text-purple-500" />
|
||||
Métricas de Eficiencia
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="text-center p-3 bg-gray-50 rounded-lg">
|
||||
<p className="text-2xl font-bold text-blue-600">{inventoryStats.reorderAccuracy}%</p>
|
||||
<p className="text-sm text-gray-600">Precisión Reorden</p>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-gray-50 rounded-lg">
|
||||
<p className="text-2xl font-bold text-green-600">{inventoryStats.turnoverRate.toFixed(1)}</p>
|
||||
<p className="text-sm text-gray-600">Tasa Rotación</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Quality Metrics */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||
Indicadores de Calidad
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="text-center p-3 bg-gray-50 rounded-lg">
|
||||
<p className="text-2xl font-bold text-green-600">{inventoryStats.qualityScore}%</p>
|
||||
<p className="text-sm text-gray-600">Puntuación Seguridad</p>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-gray-50 rounded-lg">
|
||||
<p className="text-2xl font-bold text-blue-600">{Number(analyticsData.temperature_compliance_rate || 95).toFixed(1)}%</p>
|
||||
<p className="text-sm text-gray-600">Cumplimiento Temp.</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Low Stock Alert */}
|
||||
{lowStockItems.length > 0 && (
|
||||
@@ -328,10 +398,6 @@ const InventoryPage: React.FC = () => {
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => console.log('Export filtered')}>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Exportar
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -339,10 +405,18 @@ const InventoryPage: React.FC = () => {
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{filteredItems.map((ingredient) => {
|
||||
const statusConfig = getInventoryStatusConfig(ingredient);
|
||||
const stockPercentage = ingredient.max_stock_level ?
|
||||
Math.round((ingredient.current_stock_level / ingredient.max_stock_level) * 100) : 0;
|
||||
const averageCost = ingredient.average_cost || 0;
|
||||
const totalValue = ingredient.current_stock_level * averageCost;
|
||||
|
||||
// Safe number conversions with fallbacks
|
||||
const currentStock = Number(ingredient.current_stock) || 0;
|
||||
const maxStock = Number(ingredient.max_stock_level) || 0;
|
||||
const averageCost = Number(ingredient.average_cost) || 0;
|
||||
|
||||
// Calculate stock percentage safely
|
||||
const stockPercentage = maxStock > 0 ?
|
||||
Math.round((currentStock / maxStock) * 100) : 0;
|
||||
|
||||
// Calculate total value safely
|
||||
const totalValue = currentStock * averageCost;
|
||||
|
||||
return (
|
||||
<StatusCard
|
||||
@@ -350,47 +424,30 @@ const InventoryPage: React.FC = () => {
|
||||
id={ingredient.id}
|
||||
statusIndicator={statusConfig}
|
||||
title={ingredient.name}
|
||||
subtitle={`${ingredient.category}${ingredient.description ? ` • ${ingredient.description}` : ''}`}
|
||||
primaryValue={ingredient.current_stock_level}
|
||||
subtitle={getCategoryDisplayName(ingredient.category)}
|
||||
primaryValue={currentStock}
|
||||
primaryValueLabel={ingredient.unit_of_measure}
|
||||
secondaryInfo={{
|
||||
label: 'Valor total',
|
||||
value: `${formatters.currency(totalValue)}${averageCost > 0 ? ` (${formatters.currency(averageCost)}/${ingredient.unit_of_measure})` : ''}`
|
||||
label: 'Valor',
|
||||
value: formatters.currency(totalValue)
|
||||
}}
|
||||
progress={ingredient.max_stock_level ? {
|
||||
label: 'Nivel de stock',
|
||||
progress={maxStock > 0 ? {
|
||||
label: `${stockPercentage}% stock`,
|
||||
percentage: stockPercentage,
|
||||
color: statusConfig.color
|
||||
} : undefined}
|
||||
metadata={[
|
||||
`Rango: ${ingredient.low_stock_threshold} - ${ingredient.max_stock_level || 'Sin límite'} ${ingredient.unit_of_measure}`,
|
||||
`Stock disponible: ${ingredient.available_stock} ${ingredient.unit_of_measure}`,
|
||||
`Stock reservado: ${ingredient.reserved_stock} ${ingredient.unit_of_measure}`,
|
||||
ingredient.last_restocked ? `Último restock: ${new Date(ingredient.last_restocked).toLocaleDateString('es-ES')}` : 'Sin historial de restock',
|
||||
...(ingredient.requires_refrigeration ? ['Requiere refrigeración'] : []),
|
||||
...(ingredient.requires_freezing ? ['Requiere congelación'] : []),
|
||||
...(ingredient.is_seasonal ? ['Producto estacional'] : [])
|
||||
]}
|
||||
actions={[
|
||||
{
|
||||
label: 'Ver',
|
||||
icon: Eye,
|
||||
variant: 'outline',
|
||||
onClick: () => {
|
||||
setSelectedItem(ingredient);
|
||||
setModalMode('view');
|
||||
setShowForm(true);
|
||||
}
|
||||
onClick: () => handleItemAction(ingredient, 'view')
|
||||
},
|
||||
{
|
||||
label: 'Editar',
|
||||
icon: Edit,
|
||||
variant: 'outline',
|
||||
onClick: () => {
|
||||
setSelectedItem(ingredient);
|
||||
setModalMode('edit');
|
||||
setShowForm(true);
|
||||
}
|
||||
variant: 'primary',
|
||||
onClick: () => handleItemAction(ingredient, 'edit')
|
||||
}
|
||||
]}
|
||||
/>
|
||||
@@ -408,148 +465,31 @@ const InventoryPage: React.FC = () => {
|
||||
<p className="text-[var(--text-secondary)] mb-4">
|
||||
Intenta ajustar la búsqueda o agregar un nuevo artículo al inventario
|
||||
</p>
|
||||
<Button onClick={() => setShowForm(true)}>
|
||||
<Button onClick={handleNewItem}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Nuevo Artículo
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Inventory Item Modal */}
|
||||
{showForm && selectedItem && (
|
||||
<StatusModal
|
||||
{/* Unified Inventory Modal */}
|
||||
{showForm && (
|
||||
<InventoryModal
|
||||
isOpen={showForm}
|
||||
onClose={() => {
|
||||
setShowForm(false);
|
||||
setSelectedItem(null);
|
||||
setModalMode('view');
|
||||
setFormData({});
|
||||
}}
|
||||
mode={modalMode}
|
||||
onModeChange={setModalMode}
|
||||
title={selectedItem.name}
|
||||
subtitle={`${selectedItem.category}${selectedItem.description ? ` - ${selectedItem.description}` : ''}`}
|
||||
statusIndicator={getInventoryStatusConfig(selectedItem)}
|
||||
size="lg"
|
||||
sections={[
|
||||
{
|
||||
title: 'Información Básica',
|
||||
icon: Package,
|
||||
fields: [
|
||||
{
|
||||
label: 'Nombre',
|
||||
value: selectedItem.name,
|
||||
highlight: true
|
||||
},
|
||||
{
|
||||
label: 'Categoría',
|
||||
value: selectedItem.category
|
||||
},
|
||||
{
|
||||
label: 'Descripción',
|
||||
value: selectedItem.description || 'Sin descripción'
|
||||
},
|
||||
{
|
||||
label: 'Unidad de medida',
|
||||
value: selectedItem.unit_of_measure
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Stock y Niveles',
|
||||
icon: Package,
|
||||
fields: [
|
||||
{
|
||||
label: 'Stock actual',
|
||||
value: `${selectedItem.current_stock_level} ${selectedItem.unit_of_measure}`,
|
||||
highlight: true
|
||||
},
|
||||
{
|
||||
label: 'Stock disponible',
|
||||
value: `${selectedItem.available_stock} ${selectedItem.unit_of_measure}`
|
||||
},
|
||||
{
|
||||
label: 'Stock reservado',
|
||||
value: `${selectedItem.reserved_stock} ${selectedItem.unit_of_measure}`
|
||||
},
|
||||
{
|
||||
label: 'Umbral mínimo',
|
||||
value: `${selectedItem.low_stock_threshold} ${selectedItem.unit_of_measure}`
|
||||
},
|
||||
{
|
||||
label: 'Stock máximo',
|
||||
value: selectedItem.max_stock_level ? `${selectedItem.max_stock_level} ${selectedItem.unit_of_measure}` : 'Sin límite'
|
||||
},
|
||||
{
|
||||
label: 'Punto de reorden',
|
||||
value: `${selectedItem.reorder_point} ${selectedItem.unit_of_measure}`
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Información Financiera',
|
||||
icon: DollarSign,
|
||||
fields: [
|
||||
{
|
||||
label: 'Costo promedio por unidad',
|
||||
value: selectedItem.average_cost || 0,
|
||||
type: 'currency'
|
||||
},
|
||||
{
|
||||
label: 'Valor total en stock',
|
||||
value: selectedItem.current_stock_level * (selectedItem.average_cost || 0),
|
||||
type: 'currency',
|
||||
highlight: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Información Adicional',
|
||||
icon: Calendar,
|
||||
fields: [
|
||||
{
|
||||
label: 'Último restock',
|
||||
value: selectedItem.last_restocked || 'Sin historial',
|
||||
type: selectedItem.last_restocked ? 'datetime' : undefined
|
||||
},
|
||||
{
|
||||
label: 'Vida útil',
|
||||
value: selectedItem.shelf_life_days ? `${selectedItem.shelf_life_days} días` : 'No especificada'
|
||||
},
|
||||
{
|
||||
label: 'Requiere refrigeración',
|
||||
value: selectedItem.requires_refrigeration ? 'Sí' : 'No',
|
||||
highlight: selectedItem.requires_refrigeration
|
||||
},
|
||||
{
|
||||
label: 'Requiere congelación',
|
||||
value: selectedItem.requires_freezing ? 'Sí' : 'No',
|
||||
highlight: selectedItem.requires_freezing
|
||||
},
|
||||
{
|
||||
label: 'Producto estacional',
|
||||
value: selectedItem.is_seasonal ? 'Sí' : 'No'
|
||||
},
|
||||
{
|
||||
label: 'Creado',
|
||||
value: selectedItem.created_at,
|
||||
type: 'datetime'
|
||||
}
|
||||
]
|
||||
},
|
||||
...(selectedItem.notes ? [{
|
||||
title: 'Notas',
|
||||
fields: [
|
||||
{
|
||||
label: 'Observaciones',
|
||||
value: selectedItem.notes,
|
||||
span: 2 as const
|
||||
}
|
||||
]
|
||||
}] : [])
|
||||
]}
|
||||
onEdit={() => {
|
||||
console.log('Editing inventory item:', selectedItem.id);
|
||||
}}
|
||||
onModeChange={(mode) => setModalMode(mode as 'create' | 'view' | 'edit')}
|
||||
selectedItem={selectedItem}
|
||||
formData={formData}
|
||||
onFieldChange={handleFieldChange}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
loading={updateIngredientMutation.isPending || createIngredientMutation.isPending}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user