Add a ne model and card design across pages

This commit is contained in:
Urtzi Alfaro
2025-08-31 10:46:13 +02:00
parent ab21149acf
commit a8b73e22ea
14 changed files with 1865 additions and 820 deletions

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { Plus, Download, AlertTriangle, Package, Clock, CheckCircle, Eye, Edit, Calendar, DollarSign } from 'lucide-react';
import { Button, Input, Card, Badge, StatsGrid } from '../../../../components/ui';
import { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout';
import { InventoryForm, LowStockAlert } from '../../../../components/domain/inventory';
@@ -8,6 +8,7 @@ import { InventoryForm, LowStockAlert } from '../../../../components/domain/inve
const InventoryPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
const [selectedItem, setSelectedItem] = useState<typeof mockInventoryItems[0] | null>(null);
const mockInventoryItems = [
@@ -83,65 +84,46 @@ const InventoryPage: React.FC = () => {
},
];
const getStockStatusBadge = (item: typeof mockInventoryItems[0]) => {
const getInventoryStatusConfig = (item: typeof mockInventoryItems[0]) => {
const { currentStock, minStock, status } = item;
if (status === 'expired') {
return (
<Badge
variant="error"
icon={<AlertTriangle size={12} />}
text="Caducado"
/>
);
return {
color: getStatusColor('expired'),
text: 'Caducado',
icon: AlertTriangle,
isCritical: true,
isHighlight: false
};
}
if (currentStock === 0) {
return (
<Badge
variant="error"
icon={<AlertTriangle size={12} />}
text="Sin Stock"
/>
);
return {
color: getStatusColor('out'),
text: 'Sin Stock',
icon: AlertTriangle,
isCritical: true,
isHighlight: false
};
}
if (currentStock <= minStock) {
return (
<Badge
variant="warning"
icon={<AlertTriangle size={12} />}
text="Stock Bajo"
/>
);
return {
color: getStatusColor('low'),
text: 'Stock Bajo',
icon: AlertTriangle,
isCritical: false,
isHighlight: true
};
}
return (
<Badge
variant="success"
icon={<CheckCircle size={12} />}
text="Normal"
/>
);
};
const getCategoryBadge = (category: string) => {
const categoryConfig = {
'Harinas': { color: 'default' },
'Levaduras': { color: 'info' },
'Lácteos': { color: 'secondary' },
'Grasas': { color: 'warning' },
'Azúcares': { color: 'primary' },
'Especias': { color: 'success' },
return {
color: getStatusColor('normal'),
text: 'Normal',
icon: CheckCircle,
isCritical: false,
isHighlight: false
};
const config = categoryConfig[category as keyof typeof categoryConfig] || { color: 'default' };
return (
<Badge
variant={config.color as any}
text={category}
/>
);
};
const filteredItems = mockInventoryItems.filter(item => {
@@ -258,132 +240,60 @@ const InventoryPage: React.FC = () => {
{/* Inventory Items Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredItems.map((item) => (
<Card key={item.id} className="p-4">
<div className="space-y-4">
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 bg-[var(--color-primary)]/10 p-2 rounded-lg">
<Package className="w-4 h-4 text-[var(--text-tertiary)]" />
</div>
<div>
<div className="font-medium text-[var(--text-primary)]">
{item.name}
</div>
<div className="text-sm text-[var(--text-secondary)]">
{item.supplier}
</div>
</div>
</div>
{getStockStatusBadge(item)}
</div>
{/* Category and Stock */}
<div className="flex items-center justify-between">
<div>
{getCategoryBadge(item.category)}
</div>
<div className="text-right">
<div className="text-lg font-bold text-[var(--text-primary)]">
{item.currentStock} {item.unit}
</div>
<div className="text-xs text-[var(--text-tertiary)]">
Mín: {item.minStock} | Máx: {item.maxStock}
</div>
</div>
</div>
{/* Value and Dates */}
<div className="flex items-center justify-between text-sm">
<div>
<div className="text-[var(--text-secondary)]">Costo unitario:</div>
<div className="font-medium text-[var(--text-primary)]">
{formatters.currency(item.cost)}
</div>
</div>
<div className="text-right">
<div className="text-[var(--text-secondary)]">Valor total:</div>
<div className="font-medium text-[var(--text-primary)]">
{formatters.currency(item.currentStock * item.cost)}
</div>
</div>
</div>
{/* Dates */}
<div className="space-y-2 text-xs">
<div className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">Último restock:</span>
<span className="text-[var(--text-primary)]">
{new Date(item.lastRestocked).toLocaleDateString('es-ES')}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">Caducidad:</span>
<span className={`font-medium ${
new Date(item.expirationDate) < new Date()
? 'text-[var(--color-error)]'
: new Date(item.expirationDate) < new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
? 'text-[var(--color-warning)]'
: 'text-[var(--text-primary)]'
}`}>
{new Date(item.expirationDate).toLocaleDateString('es-ES')}
</span>
</div>
</div>
{/* Stock Level Progress */}
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span className="text-[var(--text-secondary)]">Nivel de stock</span>
<span className="text-[var(--text-primary)]">
{Math.round((item.currentStock / item.maxStock) * 100)}%
</span>
</div>
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
item.currentStock <= item.minStock
? 'bg-[var(--color-error)]'
: item.currentStock <= item.minStock * 1.5
? 'bg-[var(--color-warning)]'
: 'bg-[var(--color-success)]'
}`}
style={{ width: `${Math.min((item.currentStock / item.maxStock) * 100, 100)}%` }}
/>
</div>
</div>
{/* Actions */}
<div className="flex gap-2 pt-2 border-t border-[var(--border-primary)]">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
{filteredItems.map((item) => {
const statusConfig = getInventoryStatusConfig(item);
const stockPercentage = Math.round((item.currentStock / item.maxStock) * 100);
const isExpiringSoon = new Date(item.expirationDate) < new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
const isExpired = new Date(item.expirationDate) < new Date();
return (
<StatusCard
key={item.id}
id={item.id}
statusIndicator={statusConfig}
title={item.name}
subtitle={`${item.category}${item.supplier}`}
primaryValue={item.currentStock}
primaryValueLabel={item.unit}
secondaryInfo={{
label: 'Valor total',
value: `${formatters.currency(item.currentStock * item.cost)} (${formatters.currency(item.cost)}/${item.unit})`
}}
progress={{
label: 'Nivel de stock',
percentage: stockPercentage,
color: statusConfig.color
}}
metadata={[
`Rango: ${item.minStock} - ${item.maxStock} ${item.unit}`,
`Caduca: ${new Date(item.expirationDate).toLocaleDateString('es-ES')}${isExpired ? ' (CADUCADO)' : isExpiringSoon ? ' (PRONTO)' : ''}`,
`Último restock: ${new Date(item.lastRestocked).toLocaleDateString('es-ES')}`
]}
actions={[
{
label: 'Ver',
icon: Eye,
variant: 'outline',
onClick: () => {
setSelectedItem(item);
setModalMode('view');
setShowForm(true);
}}
>
<Eye className="w-4 h-4 mr-1" />
Ver
</Button>
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
}
},
{
label: 'Editar',
icon: Edit,
variant: 'outline',
onClick: () => {
setSelectedItem(item);
setModalMode('edit');
setShowForm(true);
}}
>
<Edit className="w-4 h-4 mr-1" />
Editar
</Button>
</div>
</div>
</Card>
))}
}
}
]}
/>
);
})}
</div>
{/* Empty State */}
@@ -403,19 +313,107 @@ const InventoryPage: React.FC = () => {
</div>
)}
{/* Inventory Form Modal */}
{showForm && (
<InventoryForm
item={selectedItem}
{/* Inventory Item Modal */}
{showForm && selectedItem && (
<StatusModal
isOpen={showForm}
onClose={() => {
setShowForm(false);
setSelectedItem(null);
setModalMode('view');
}}
onSave={(item) => {
// Handle save logic
console.log('Saving item:', item);
setShowForm(false);
setSelectedItem(null);
mode={modalMode}
onModeChange={setModalMode}
title={selectedItem.name}
subtitle={`${selectedItem.category} - ${selectedItem.supplier}`}
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: 'Proveedor',
value: selectedItem.supplier
},
{
label: 'Unidad de medida',
value: selectedItem.unit
}
]
},
{
title: 'Stock y Niveles',
icon: Package,
fields: [
{
label: 'Stock actual',
value: `${selectedItem.currentStock} ${selectedItem.unit}`,
highlight: true
},
{
label: 'Stock mínimo',
value: `${selectedItem.minStock} ${selectedItem.unit}`
},
{
label: 'Stock máximo',
value: `${selectedItem.maxStock} ${selectedItem.unit}`
},
{
label: 'Porcentaje de stock',
value: Math.round((selectedItem.currentStock / selectedItem.maxStock) * 100),
type: 'percentage',
highlight: selectedItem.currentStock <= selectedItem.minStock
}
]
},
{
title: 'Información Financiera',
icon: DollarSign,
fields: [
{
label: 'Costo por unidad',
value: selectedItem.cost,
type: 'currency'
},
{
label: 'Valor total en stock',
value: selectedItem.currentStock * selectedItem.cost,
type: 'currency',
highlight: true
}
]
},
{
title: 'Fechas Importantes',
icon: Calendar,
fields: [
{
label: 'Último restock',
value: selectedItem.lastRestocked,
type: 'date'
},
{
label: 'Fecha de caducidad',
value: selectedItem.expirationDate,
type: 'date',
highlight: new Date(selectedItem.expirationDate) < new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
}
]
}
]}
onEdit={() => {
console.log('Editing inventory item:', selectedItem.id);
}}
/>
)}

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { Plus, Download, Clock, Package, Eye, Edit, CheckCircle, AlertCircle, Timer } from 'lucide-react';
import { Button, Input, Card, Badge, StatsGrid } from '../../../../components/ui';
import { Plus, Download, Clock, Package, Eye, Edit, CheckCircle, AlertCircle, Timer, Users, DollarSign } from 'lucide-react';
import { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout';
import { OrderForm } from '../../../../components/domain/sales';
@@ -9,6 +9,7 @@ const OrdersPage: React.FC = () => {
const [activeTab] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
const [selectedOrder, setSelectedOrder] = useState<typeof mockOrders[0] | null>(null);
const mockOrders = [
@@ -80,24 +81,26 @@ const OrdersPage: React.FC = () => {
},
];
const getStatusBadge = (status: string) => {
const getOrderStatusConfig = (status: string, paymentStatus: string) => {
const statusConfig = {
pending: { color: 'warning', text: 'Pendiente', icon: Clock },
in_progress: { color: 'info', text: 'En Proceso', icon: Timer },
ready: { color: 'success', text: 'Listo', icon: CheckCircle },
completed: { color: 'success', text: 'Completado', icon: CheckCircle },
cancelled: { color: 'error', text: 'Cancelado', icon: AlertCircle },
pending: { text: 'Pendiente', icon: Clock },
in_progress: { text: 'En Proceso', icon: Timer },
ready: { text: 'Listo', icon: CheckCircle },
completed: { text: 'Completado', icon: CheckCircle },
cancelled: { text: 'Cancelado', icon: AlertCircle },
};
const config = statusConfig[status as keyof typeof statusConfig];
const Icon = config?.icon;
return (
<Badge
variant={config?.color as any}
icon={Icon && <Icon size={12} />}
text={config?.text || status}
/>
);
const isPaymentPending = paymentStatus === 'pending';
return {
color: getStatusColor(status),
text: config?.text || status,
icon: Icon,
isCritical: false,
isHighlight: isPaymentPending
};
};
const filteredOrders = mockOrders.filter(order => {
@@ -211,77 +214,53 @@ const OrdersPage: React.FC = () => {
{/* Orders Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredOrders.map((order) => (
<Card key={order.id} className="p-4">
<div className="space-y-4">
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 bg-[var(--color-primary)]/10 p-2 rounded-lg">
<Package className="w-4 h-4 text-[var(--text-tertiary)]" />
</div>
<div>
<div className="font-mono text-sm font-semibold text-[var(--color-primary)]">
{order.id}
</div>
<span className="font-medium text-[var(--text-primary)]">
{order.customerName}
</span>
</div>
</div>
{getStatusBadge(order.status)}
</div>
{/* Key Info */}
<div className="flex items-center justify-between">
<div className="text-right">
<div className="text-lg font-bold text-[var(--text-primary)]">
{formatters.currency(order.total)}
</div>
<div className="text-xs text-[var(--text-tertiary)]">
{order.items?.length} artículos
</div>
</div>
<div className="text-right">
<div className="text-sm text-[var(--text-primary)]">
{new Date(order.deliveryDate).toLocaleDateString('es-ES')}
</div>
<div className="text-xs text-[var(--text-tertiary)]">
Entrega: {new Date(order.deliveryDate).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
</div>
{/* Actions */}
<div className="flex gap-2 pt-2 border-t border-[var(--border-primary)]">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
{filteredOrders.map((order) => {
const statusConfig = getOrderStatusConfig(order.status, order.paymentStatus);
const paymentNote = order.paymentStatus === 'pending' ? 'Pago pendiente' : '';
return (
<StatusCard
key={order.id}
id={order.id}
statusIndicator={statusConfig}
title={order.customerName}
subtitle={order.id}
primaryValue={formatters.currency(order.total)}
primaryValueLabel={`${order.items?.length} artículos`}
secondaryInfo={{
label: 'Entrega',
value: `${new Date(order.deliveryDate).toLocaleDateString('es-ES')} - ${new Date(order.deliveryDate).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })}`
}}
metadata={[
order.customerEmail,
order.customerPhone,
...(paymentNote ? [paymentNote] : [])
]}
actions={[
{
label: 'Ver',
icon: Eye,
variant: 'outline',
onClick: () => {
setSelectedOrder(order);
setModalMode('view');
setShowForm(true);
}}
>
<Eye className="w-4 h-4 mr-1" />
Ver
</Button>
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
}
},
{
label: 'Editar',
icon: Edit,
variant: 'outline',
onClick: () => {
setSelectedOrder(order);
setModalMode('edit');
setShowForm(true);
}}
>
<Edit className="w-4 h-4 mr-1" />
Editar
</Button>
</div>
</div>
</Card>
))}
}
}
]}
/>
);
})}
</div>
{/* Empty State */}
@@ -301,20 +280,117 @@ const OrdersPage: React.FC = () => {
</div>
)}
{/* Order Form Modal */}
{showForm && (
<OrderForm
orderId={selectedOrder?.id}
onOrderCancel={() => {
{/* Order Details Modal */}
{showForm && selectedOrder && (
<StatusModal
isOpen={showForm}
onClose={() => {
setShowForm(false);
setSelectedOrder(null);
setModalMode('view');
}}
onOrderSave={async (order: any) => {
// Handle save logic
console.log('Saving order:', order);
setShowForm(false);
setSelectedOrder(null);
return true;
mode={modalMode}
onModeChange={setModalMode}
title={selectedOrder.customerName}
subtitle={`Pedido ${selectedOrder.id}`}
statusIndicator={getOrderStatusConfig(selectedOrder.status, selectedOrder.paymentStatus)}
size="lg"
sections={[
{
title: 'Información del Cliente',
icon: Users,
fields: [
{
label: 'Nombre',
value: selectedOrder.customerName,
highlight: true
},
{
label: 'Email',
value: selectedOrder.customerEmail
},
{
label: 'Teléfono',
value: selectedOrder.customerPhone
},
{
label: 'Método de entrega',
value: selectedOrder.deliveryMethod === 'pickup' ? 'Recogida' : 'Entrega a domicilio'
}
]
},
{
title: 'Detalles del Pedido',
icon: Package,
fields: [
{
label: 'Fecha del pedido',
value: selectedOrder.orderDate,
type: 'datetime'
},
{
label: 'Fecha de entrega',
value: selectedOrder.deliveryDate,
type: 'datetime',
highlight: true
},
{
label: 'Artículos',
value: selectedOrder.items?.map(item => `${item.name} (${item.quantity})`),
type: 'list',
span: 2
}
]
},
{
title: 'Información Financiera',
icon: DollarSign,
fields: [
{
label: 'Subtotal',
value: selectedOrder.subtotal,
type: 'currency'
},
{
label: 'Impuestos',
value: selectedOrder.tax,
type: 'currency'
},
{
label: 'Descuento',
value: selectedOrder.discount,
type: 'currency'
},
{
label: 'Total',
value: selectedOrder.total,
type: 'currency',
highlight: true
},
{
label: 'Método de pago',
value: selectedOrder.paymentMethod === 'card' ? 'Tarjeta' : selectedOrder.paymentMethod === 'cash' ? 'Efectivo' : 'Transferencia'
},
{
label: 'Estado del pago',
value: selectedOrder.paymentStatus === 'paid' ? 'Pagado' : 'Pendiente',
type: 'status'
}
]
},
...(selectedOrder.notes ? [{
title: 'Notas',
fields: [
{
label: 'Observaciones',
value: selectedOrder.notes,
span: 2 as const
}
]
}] : [])
]}
onEdit={() => {
console.log('Editing order:', selectedOrder.id);
}}
/>
)}

View File

@@ -1,12 +1,15 @@
import React, { useState } from 'react';
import { Plus, Search, Download, ShoppingCart, Truck, DollarSign, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Edit } from 'lucide-react';
import { Button, Input, Card, Badge, StatsGrid } from '../../../../components/ui';
import { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout';
const ProcurementPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('orders');
const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
const [selectedOrder, setSelectedOrder] = useState<typeof mockPurchaseOrders[0] | null>(null);
const mockPurchaseOrders = [
{
@@ -101,42 +104,27 @@ const ProcurementPage: React.FC = () => {
},
];
const getStatusBadge = (status: string) => {
const getPurchaseStatusConfig = (status: string, paymentStatus: string) => {
const statusConfig = {
pending: { color: 'warning', text: 'Pendiente', icon: Clock },
approved: { color: 'info', text: 'Aprobado', icon: CheckCircle },
in_transit: { color: 'secondary', text: 'En Tránsito', icon: Truck },
delivered: { color: 'success', text: 'Entregado', icon: CheckCircle },
cancelled: { color: 'error', text: 'Cancelado', icon: AlertCircle },
pending: { text: 'Pendiente', icon: Clock },
approved: { text: 'Aprobado', icon: CheckCircle },
in_transit: { text: 'En Tránsito', icon: Truck },
delivered: { text: 'Entregado', icon: CheckCircle },
cancelled: { text: 'Cancelado', icon: AlertCircle },
};
const config = statusConfig[status as keyof typeof statusConfig];
const Icon = config?.icon;
return (
<Badge
variant={config?.color as any}
icon={Icon && <Icon size={12} />}
text={config?.text || status}
/>
);
};
const getPaymentStatusBadge = (status: string) => {
const statusConfig = {
pending: { color: 'warning', text: 'Pendiente', icon: Clock },
paid: { color: 'success', text: 'Pagado', icon: CheckCircle },
overdue: { color: 'error', text: 'Vencido', icon: AlertCircle },
};
const isPaymentPending = paymentStatus === 'pending';
const isOverdue = paymentStatus === 'overdue';
const config = statusConfig[status as keyof typeof statusConfig];
const Icon = config?.icon;
return (
<Badge
variant={config?.color as any}
icon={Icon && <Icon size={12} />}
text={config?.text || status}
/>
);
return {
color: getStatusColor(status === 'in_transit' ? 'inTransit' : status),
text: config?.text || status,
icon: Icon,
isCritical: isOverdue,
isHighlight: isPaymentPending
};
};
const filteredOrders = mockPurchaseOrders.filter(order => {
@@ -282,86 +270,52 @@ const ProcurementPage: React.FC = () => {
{/* Purchase Orders Grid */}
{activeTab === 'orders' && (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredOrders.map((order) => (
<Card key={order.id} className="p-4">
<div className="space-y-4">
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 bg-[var(--color-primary)]/10 p-2 rounded-lg">
<ShoppingCart className="w-4 h-4 text-[var(--text-tertiary)]" />
</div>
<div>
<div className="font-mono text-sm font-semibold text-[var(--color-primary)]">
{order.id}
</div>
<span className="font-medium text-[var(--text-primary)]">
{order.supplier}
</span>
</div>
</div>
{getStatusBadge(order.status)}
</div>
{/* Key Info */}
<div className="flex items-center justify-between">
<div className="text-right">
<div className="text-lg font-bold text-[var(--text-primary)]">
{formatters.currency(order.totalAmount)}
</div>
<div className="text-xs text-[var(--text-tertiary)]">
{order.items?.length} artículos
</div>
</div>
<div className="text-right">
<div className="text-sm text-[var(--text-primary)]">
{new Date(order.deliveryDate).toLocaleDateString('es-ES')}
</div>
<div className="text-xs text-[var(--text-tertiary)]">
Entrega prevista
</div>
</div>
</div>
{/* Payment Status */}
<div className="flex items-center justify-between">
<div className="text-sm text-[var(--text-secondary)]">
Estado del pago:
</div>
{getPaymentStatusBadge(order.paymentStatus)}
</div>
{/* Notes */}
{order.notes && (
<div className="bg-[var(--bg-secondary)] p-2 rounded text-xs text-[var(--text-secondary)] italic">
"{order.notes}"
</div>
)}
{/* Actions */}
<div className="flex gap-2 pt-2 border-t border-[var(--border-primary)]">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => console.log('View order', order.id)}
>
<Eye className="w-4 h-4 mr-1" />
Ver
</Button>
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => console.log('Edit order', order.id)}
>
<Edit className="w-4 h-4 mr-1" />
Editar
</Button>
</div>
</div>
</Card>
))}
{filteredOrders.map((order) => {
const statusConfig = getPurchaseStatusConfig(order.status, order.paymentStatus);
const paymentNote = order.paymentStatus === 'pending' ? 'Pago pendiente' : order.paymentStatus === 'overdue' ? 'Pago vencido' : '';
return (
<StatusCard
key={order.id}
id={order.id}
statusIndicator={statusConfig}
title={order.supplier}
subtitle={order.id}
primaryValue={formatters.currency(order.totalAmount)}
primaryValueLabel={`${order.items?.length} artículos`}
secondaryInfo={{
label: 'Entrega',
value: `${new Date(order.deliveryDate).toLocaleDateString('es-ES')} (pedido: ${new Date(order.orderDate).toLocaleDateString('es-ES')})`
}}
metadata={[
...(order.notes ? [`"${order.notes}"`] : []),
...(paymentNote ? [paymentNote] : [])
]}
actions={[
{
label: 'Ver',
icon: Eye,
variant: 'outline',
onClick: () => {
setSelectedOrder(order);
setModalMode('view');
setShowForm(true);
}
},
{
label: 'Editar',
icon: Edit,
variant: 'outline',
onClick: () => {
setSelectedOrder(order);
setModalMode('edit');
setShowForm(true);
}
}
]}
/>
);
})}
</div>
)}
@@ -499,6 +453,115 @@ const ProcurementPage: React.FC = () => {
</Card>
</div>
)}
{/* Purchase Order Modal */}
{showForm && selectedOrder && (
<StatusModal
isOpen={showForm}
onClose={() => {
setShowForm(false);
setSelectedOrder(null);
setModalMode('view');
}}
mode={modalMode}
onModeChange={setModalMode}
title={selectedOrder.supplier}
subtitle={`Orden de Compra ${selectedOrder.id}`}
statusIndicator={getPurchaseStatusConfig(selectedOrder.status, selectedOrder.paymentStatus)}
size="lg"
sections={[
{
title: 'Información del Proveedor',
icon: Package,
fields: [
{
label: 'Proveedor',
value: selectedOrder.supplier,
highlight: true,
editable: true,
required: true,
placeholder: 'Nombre del proveedor'
},
{
label: 'ID de Orden',
value: selectedOrder.id
},
{
label: 'Estado de Pago',
value: selectedOrder.paymentStatus === 'paid' ? 'Pagado' : selectedOrder.paymentStatus === 'pending' ? 'Pendiente' : 'Vencido',
type: 'status'
}
]
},
{
title: 'Fechas Importantes',
icon: Calendar,
fields: [
{
label: 'Fecha de pedido',
value: selectedOrder.orderDate,
type: 'date',
editable: true
},
{
label: 'Fecha de entrega',
value: selectedOrder.deliveryDate,
type: 'date',
highlight: true,
editable: true,
required: true
}
]
},
{
title: 'Información Financiera',
icon: DollarSign,
fields: [
{
label: 'Importe total',
value: selectedOrder.totalAmount,
type: 'currency',
highlight: true,
editable: true,
required: true,
placeholder: '0.00'
},
{
label: 'Número de artículos',
value: `${selectedOrder.items?.length} productos`
}
]
},
{
title: 'Artículos Pedidos',
icon: ShoppingCart,
fields: [
{
label: 'Lista de productos',
value: selectedOrder.items?.map(item => `${item.name}: ${item.quantity} ${item.unit} - ${formatters.currency(item.total)}`),
type: 'list',
span: 2
}
]
},
...(selectedOrder.notes ? [{
title: 'Notas Adicionales',
fields: [
{
label: 'Observaciones',
value: selectedOrder.notes,
span: 2 as const,
editable: true,
placeholder: 'Añadir notas sobre la orden de compra...'
}
]
}] : [])
]}
onEdit={() => {
console.log('Editing purchase order:', selectedOrder.id);
}}
/>
)}
</div>
);
};

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { Plus, Download, Clock, Users, AlertCircle, CheckCircle, Timer, ChefHat, Eye, Edit, Calendar, Zap } from 'lucide-react';
import { Button, Input, Card, Badge, StatsGrid } from '../../../../components/ui';
import { Plus, Download, Clock, Users, AlertCircle, CheckCircle, Timer, ChefHat, Eye, Edit, Calendar, Zap, Package } from 'lucide-react';
import { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
import { pagePresets } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout';
import { ProductionSchedule, BatchTracker, QualityControl } from '../../../../components/domain/production';
@@ -10,6 +10,7 @@ const ProductionPage: React.FC = () => {
const [searchQuery, setSearchQuery] = useState('');
const [selectedOrder, setSelectedOrder] = useState<typeof mockProductionOrders[0] | null>(null);
const [showForm, setShowForm] = useState(false);
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
const mockProductionStats = {
dailyTarget: 150,
@@ -111,42 +112,25 @@ const ProductionPage: React.FC = () => {
},
];
const getStatusBadge = (status: string) => {
const getProductionStatusConfig = (status: string, priority: string) => {
const statusConfig = {
pending: { color: 'warning', text: 'Pendiente', icon: Clock },
in_progress: { color: 'info', text: 'En Proceso', icon: Timer },
completed: { color: 'success', text: 'Completado', icon: CheckCircle },
cancelled: { color: 'error', text: 'Cancelado', icon: AlertCircle },
pending: { text: 'Pendiente', icon: Clock },
in_progress: { text: 'En Proceso', icon: Timer },
completed: { text: 'Completado', icon: CheckCircle },
cancelled: { text: 'Cancelado', icon: AlertCircle },
};
const config = statusConfig[status as keyof typeof statusConfig];
const Icon = config?.icon;
return (
<Badge
variant={config?.color as any}
icon={Icon && <Icon size={12} />}
text={config?.text || status}
/>
);
};
const getPriorityBadge = (priority: string) => {
const priorityConfig = {
low: { color: 'outline', text: 'Baja' },
medium: { color: 'secondary', text: 'Media' },
high: { color: 'warning', text: 'Alta' },
urgent: { color: 'error', text: 'Urgente', icon: Zap },
};
const isUrgent = priority === 'urgent';
const config = priorityConfig[priority as keyof typeof priorityConfig];
const Icon = config?.icon;
return (
<Badge
variant={config?.color as any}
icon={Icon && <Icon size={12} />}
text={config?.text || priority}
/>
);
return {
color: getStatusColor(status),
text: config?.text || status,
icon: Icon,
isCritical: isUrgent,
isHighlight: false
};
};
const filteredOrders = mockProductionOrders.filter(order => {
@@ -245,120 +229,52 @@ const ProductionPage: React.FC = () => {
{/* Production Orders Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredOrders.map((order) => (
<Card key={order.id} className="p-4">
<div className="space-y-4">
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 bg-[var(--color-primary)]/10 p-2 rounded-lg">
<ChefHat className="w-4 h-4 text-[var(--text-tertiary)]" />
</div>
<div>
<div className="font-medium text-[var(--text-primary)]">
{order.recipeName}
</div>
<div className="text-sm text-[var(--text-secondary)]">
ID: {order.id}
</div>
</div>
</div>
{getStatusBadge(order.status)}
</div>
{/* Priority and Quantity */}
<div className="flex items-center justify-between">
<div>
{getPriorityBadge(order.priority)}
</div>
<div className="text-right">
<div className="text-lg font-bold text-[var(--text-primary)]">
{order.quantity}
</div>
<div className="text-xs text-[var(--text-tertiary)]">
unidades
</div>
</div>
</div>
{/* Assigned Worker */}
<div className="flex items-center gap-2">
<Users className="w-4 h-4 text-[var(--text-tertiary)]" />
<span className="text-sm text-[var(--text-primary)]">
{order.assignedTo}
</span>
</div>
{/* Time Information */}
<div className="space-y-2 text-xs">
<div className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">Inicio:</span>
<span className="text-[var(--text-primary)]">
{new Date(order.startTime).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">Est. finalización:</span>
<span className="text-[var(--text-primary)]">
{new Date(order.estimatedCompletion).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
</div>
{/* Progress Bar */}
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span className="text-[var(--text-secondary)]">Progreso</span>
<span className="text-[var(--text-primary)]">
{order.progress}%
</span>
</div>
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
order.progress === 100
? 'bg-[var(--color-success)]'
: order.progress > 50
? 'bg-[var(--color-info)]'
: order.progress > 0
? 'bg-[var(--color-warning)]'
: 'bg-[var(--bg-quaternary)]'
}`}
style={{ width: `${order.progress}%` }}
/>
</div>
</div>
{/* Actions */}
<div className="flex gap-2 pt-2 border-t border-[var(--border-primary)]">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
{filteredOrders.map((order) => {
const statusConfig = getProductionStatusConfig(order.status, order.priority);
return (
<StatusCard
key={order.id}
id={order.id}
statusIndicator={statusConfig}
title={order.recipeName}
subtitle={`Asignado a: ${order.assignedTo}`}
primaryValue={order.quantity}
primaryValueLabel="unidades"
secondaryInfo={{
label: 'Horario',
value: `${new Date(order.startTime).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })} → ${new Date(order.estimatedCompletion).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })}`
}}
progress={{
label: 'Progreso',
percentage: order.progress,
color: statusConfig.color
}}
actions={[
{
label: 'Ver',
icon: Eye,
variant: 'outline',
onClick: () => {
setSelectedOrder(order);
setModalMode('view');
setShowForm(true);
}}
>
<Eye className="w-4 h-4 mr-1" />
Ver
</Button>
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
}
},
{
label: 'Editar',
icon: Edit,
variant: 'outline',
onClick: () => {
setSelectedOrder(order);
setModalMode('edit');
setShowForm(true);
}}
>
<Edit className="w-4 h-4 mr-1" />
Editar
</Button>
</div>
</div>
</Card>
))}
}
}
]}
/>
);
})}
</div>
{/* Empty State */}
@@ -388,52 +304,70 @@ const ProductionPage: React.FC = () => {
<QualityControl />
)}
{/* Production Order Form Modal - Placeholder */}
{showForm && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<Card className="w-full max-w-2xl max-h-[90vh] overflow-auto m-4 p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-[var(--text-primary)]">
{selectedOrder ? 'Ver Orden de Producción' : 'Nueva Orden de Producción'}
</h2>
<Button
variant="outline"
size="sm"
onClick={() => {
setShowForm(false);
setSelectedOrder(null);
}}
>
Cerrar
</Button>
</div>
{selectedOrder && (
<div className="space-y-4">
<h3 className="text-lg font-medium">{selectedOrder.recipeName}</h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium">Cantidad:</span> {selectedOrder.quantity} unidades
</div>
<div>
<span className="font-medium">Asignado a:</span> {selectedOrder.assignedTo}
</div>
<div>
<span className="font-medium">Estado:</span> {selectedOrder.status}
</div>
<div>
<span className="font-medium">Progreso:</span> {selectedOrder.progress}%
</div>
<div>
<span className="font-medium">Inicio:</span> {new Date(selectedOrder.startTime).toLocaleString('es-ES')}
</div>
<div>
<span className="font-medium">Finalización:</span> {new Date(selectedOrder.estimatedCompletion).toLocaleString('es-ES')}
</div>
</div>
</div>
)}
</Card>
</div>
{/* Production Order Modal */}
{showForm && selectedOrder && (
<StatusModal
isOpen={showForm}
onClose={() => {
setShowForm(false);
setSelectedOrder(null);
setModalMode('view');
}}
mode={modalMode}
onModeChange={setModalMode}
title={selectedOrder.recipeName}
subtitle={`Orden de Producción #${selectedOrder.id}`}
statusIndicator={getProductionStatusConfig(selectedOrder.status, selectedOrder.priority)}
size="lg"
sections={[
{
title: 'Información General',
icon: Package,
fields: [
{
label: 'Cantidad',
value: `${selectedOrder.quantity} unidades`,
highlight: true
},
{
label: 'Asignado a',
value: selectedOrder.assignedTo,
},
{
label: 'Prioridad',
value: selectedOrder.priority,
type: 'status'
},
{
label: 'Progreso',
value: selectedOrder.progress,
type: 'percentage',
highlight: true
}
]
},
{
title: 'Cronograma',
icon: Clock,
fields: [
{
label: 'Hora de inicio',
value: selectedOrder.startTime,
type: 'datetime'
},
{
label: 'Finalización estimada',
value: selectedOrder.estimatedCompletion,
type: 'datetime'
}
]
}
]}
onEdit={() => {
// Handle edit mode
console.log('Editing production order:', selectedOrder.id);
}}
/>
)}
</div>
);

View File

@@ -1,12 +1,13 @@
import React, { useState } from 'react';
import { Plus, Download, Star, Clock, Users, DollarSign, Package, Eye, Edit, ChefHat, Timer } from 'lucide-react';
import { Button, Input, Card, Badge, StatsGrid } from '../../../../components/ui';
import { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout';
const RecipesPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
const [selectedRecipe, setSelectedRecipe] = useState<typeof mockRecipes[0] | null>(null);
const mockRecipes = [
@@ -100,38 +101,34 @@ const RecipesPage: React.FC = () => {
},
];
const getCategoryBadge = (category: string) => {
const getRecipeStatusConfig = (category: string, difficulty: string, rating: number) => {
const categoryConfig = {
bread: { color: 'default', text: 'Pan' },
pastry: { color: 'warning', text: 'Bollería' },
cake: { color: 'secondary', text: 'Tarta' },
cookie: { color: 'info', text: 'Galleta' },
other: { color: 'outline', text: 'Otro' },
bread: { text: 'Pan', icon: ChefHat },
pastry: { text: 'Bollería', icon: ChefHat },
cake: { text: 'Tarta', icon: ChefHat },
cookie: { text: 'Galleta', icon: ChefHat },
other: { text: 'Otro', icon: ChefHat },
};
const config = categoryConfig[category as keyof typeof categoryConfig] || categoryConfig.other;
return (
<Badge
variant={config.color as any}
text={config.text}
/>
);
};
const getDifficultyBadge = (difficulty: string) => {
const difficultyConfig = {
easy: { color: 'success', text: 'Fácil' },
medium: { color: 'warning', text: 'Medio' },
hard: { color: 'error', text: 'Difícil' },
easy: { icon: '', label: 'Fácil' },
medium: { icon: '●●', label: 'Medio' },
hard: { icon: '●●●', label: 'Difícil' },
};
const config = difficultyConfig[difficulty as keyof typeof difficultyConfig];
return (
<Badge
variant={config?.color as any}
text={config?.text || difficulty}
/>
);
const categoryInfo = categoryConfig[category as keyof typeof categoryConfig] || categoryConfig.other;
const difficultyInfo = difficultyConfig[difficulty as keyof typeof difficultyConfig];
const isPopular = rating >= 4.7;
return {
color: getStatusColor(category),
text: categoryInfo.text,
icon: categoryInfo.icon,
difficultyIcon: difficultyInfo?.icon || '●',
difficultyLabel: difficultyInfo?.label || difficulty,
isCritical: false,
isHighlight: isPopular
};
};
const formatTime = (minutes: number) => {
@@ -245,130 +242,65 @@ const RecipesPage: React.FC = () => {
{/* Recipes Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredRecipes.map((recipe) => (
<Card key={recipe.id} className="p-4">
<div className="space-y-4">
{/* Header with Image */}
<div className="flex items-start gap-3">
<div className="flex-shrink-0">
<img
src={recipe.image}
alt={recipe.name}
className="w-16 h-16 rounded-lg object-cover bg-[var(--bg-secondary)]"
/>
</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-[var(--text-primary)] truncate">
{recipe.name}
</div>
<div className="flex items-center gap-1 mt-1">
<Star className="w-3 h-3 text-yellow-400 fill-current" />
<span className="text-xs text-[var(--text-secondary)]">{recipe.rating}</span>
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1 line-clamp-2">
{recipe.description}
</p>
</div>
</div>
{/* Badges */}
<div className="flex gap-2 flex-wrap">
{getCategoryBadge(recipe.category)}
{getDifficultyBadge(recipe.difficulty)}
</div>
{/* Time and Yield */}
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-1">
<Clock className="w-4 h-4 text-[var(--text-tertiary)]" />
<span className="text-[var(--text-primary)]">
{formatTime(recipe.prepTime + recipe.bakingTime)}
</span>
</div>
<div className="flex items-center gap-1">
<Users className="w-4 h-4 text-[var(--text-tertiary)]" />
<span className="text-[var(--text-primary)]">
{recipe.yield} porciones
</span>
</div>
</div>
{/* Financial Info */}
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-[var(--text-secondary)]">Costo:</span>
<span className="font-medium text-[var(--text-primary)]">
{formatters.currency(recipe.cost)}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-[var(--text-secondary)]">Precio:</span>
<span className="font-medium text-[var(--color-success)]">
{formatters.currency(recipe.price)}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-[var(--text-secondary)]">Margen:</span>
<span className="font-bold text-[var(--color-success)]">
{formatters.currency(recipe.profit)}
</span>
</div>
</div>
{/* Profit Margin Bar */}
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span className="text-[var(--text-secondary)]">Margen de beneficio</span>
<span className="text-[var(--text-primary)]">
{Math.round((recipe.profit / recipe.price) * 100)}%
</span>
</div>
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
(recipe.profit / recipe.price) > 0.5
? 'bg-[var(--color-success)]'
: (recipe.profit / recipe.price) > 0.3
? 'bg-[var(--color-warning)]'
: 'bg-[var(--color-error)]'
}`}
style={{ width: `${Math.min((recipe.profit / recipe.price) * 100, 100)}%` }}
/>
</div>
</div>
{/* Ingredients Count */}
<div className="text-xs text-[var(--text-secondary)]">
{recipe.ingredients.length} ingredientes principales
</div>
{/* Actions */}
<div className="flex gap-2 pt-2 border-t border-[var(--border-primary)]">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
{filteredRecipes.map((recipe) => {
const statusConfig = getRecipeStatusConfig(recipe.category, recipe.difficulty, recipe.rating);
const profitMargin = Math.round((recipe.profit / recipe.price) * 100);
const totalTime = formatTime(recipe.prepTime + recipe.bakingTime);
return (
<StatusCard
key={recipe.id}
id={recipe.id}
statusIndicator={statusConfig}
title={recipe.name}
subtitle={`${statusConfig.text}${statusConfig.difficultyLabel}${statusConfig.isHighlight ? ' ★' + recipe.rating : ''}`}
primaryValue={formatters.currency(recipe.profit)}
primaryValueLabel="margen"
secondaryInfo={{
label: 'Precio de venta',
value: `${formatters.currency(recipe.price)} (costo: ${formatters.currency(recipe.cost)})`
}}
progress={{
label: 'Margen de beneficio',
percentage: profitMargin,
color: profitMargin > 50 ? '#10b981' : profitMargin > 30 ? '#f59e0b' : '#ef4444'
}}
metadata={[
`Tiempo: ${totalTime}`,
`Porciones: ${recipe.yield}`,
`${recipe.ingredients.length} ingredientes principales`
]}
actions={[
{
label: 'Ver',
icon: Eye,
variant: 'outline',
onClick: () => {
setSelectedRecipe(recipe);
setModalMode('view');
setShowForm(true);
}}
>
<Eye className="w-4 h-4 mr-1" />
Ver
</Button>
<Button
variant="primary"
size="sm"
className="flex-1"
onClick={() => console.log('Produce recipe', recipe.id)}
>
<ChefHat className="w-4 h-4 mr-1" />
Producir
</Button>
</div>
</div>
</Card>
))}
}
},
{
label: 'Editar',
icon: Edit,
variant: 'outline',
onClick: () => {
setSelectedRecipe(recipe);
setModalMode('edit');
setShowForm(true);
}
},
{
label: 'Producir',
icon: ChefHat,
variant: 'primary',
onClick: () => console.log('Produce recipe', recipe.id)
}
]}
/>
);
})}
</div>
{/* Empty State */}
@@ -388,57 +320,133 @@ const RecipesPage: React.FC = () => {
</div>
)}
{/* Recipe Form Modal - Placeholder */}
{showForm && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<Card className="w-full max-w-2xl max-h-[90vh] overflow-auto m-4 p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-[var(--text-primary)]">
{selectedRecipe ? 'Ver Receta' : 'Nueva Receta'}
</h2>
<Button
variant="outline"
size="sm"
onClick={() => {
setShowForm(false);
setSelectedRecipe(null);
}}
>
Cerrar
</Button>
</div>
{selectedRecipe && (
<div className="space-y-4">
<img
src={selectedRecipe.image}
alt={selectedRecipe.name}
className="w-full h-48 object-cover rounded-lg"
/>
<h3 className="text-lg font-medium">{selectedRecipe.name}</h3>
<p className="text-[var(--text-secondary)]">{selectedRecipe.description}</p>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium">Tiempo total:</span> {formatTime(selectedRecipe.prepTime + selectedRecipe.bakingTime)}
</div>
<div>
<span className="font-medium">Rendimiento:</span> {selectedRecipe.yield} porciones
</div>
</div>
<div>
<h4 className="font-medium mb-2">Ingredientes:</h4>
<ul className="space-y-1 text-sm">
{selectedRecipe.ingredients.map((ing, i) => (
<li key={i} className="flex justify-between">
<span>{ing.name}</span>
<span>{ing.quantity} {ing.unit}</span>
</li>
))}
</ul>
</div>
</div>
)}
</Card>
</div>
{/* Recipe Details Modal */}
{showForm && selectedRecipe && (
<StatusModal
isOpen={showForm}
onClose={() => {
setShowForm(false);
setSelectedRecipe(null);
setModalMode('view');
}}
mode={modalMode}
onModeChange={setModalMode}
title={selectedRecipe.name}
subtitle={selectedRecipe.description}
statusIndicator={getRecipeStatusConfig(selectedRecipe.category, selectedRecipe.difficulty, selectedRecipe.rating)}
image={selectedRecipe.image}
size="xl"
sections={[
{
title: 'Información Básica',
icon: ChefHat,
fields: [
{
label: 'Categoría',
value: selectedRecipe.category,
type: 'status'
},
{
label: 'Dificultad',
value: selectedRecipe.difficulty
},
{
label: 'Valoración',
value: `${selectedRecipe.rating}`,
highlight: selectedRecipe.rating >= 4.7
},
{
label: 'Rendimiento',
value: `${selectedRecipe.yield} porciones`
}
]
},
{
title: 'Tiempos',
icon: Clock,
fields: [
{
label: 'Tiempo de preparación',
value: formatTime(selectedRecipe.prepTime)
},
{
label: 'Tiempo de horneado',
value: formatTime(selectedRecipe.bakingTime)
},
{
label: 'Tiempo total',
value: formatTime(selectedRecipe.prepTime + selectedRecipe.bakingTime),
highlight: true
}
]
},
{
title: 'Análisis Financiero',
icon: DollarSign,
fields: [
{
label: 'Costo de producción',
value: selectedRecipe.cost,
type: 'currency'
},
{
label: 'Precio de venta',
value: selectedRecipe.price,
type: 'currency'
},
{
label: 'Margen de beneficio',
value: selectedRecipe.profit,
type: 'currency',
highlight: true
},
{
label: 'Porcentaje de margen',
value: Math.round((selectedRecipe.profit / selectedRecipe.price) * 100),
type: 'percentage',
highlight: true
}
]
},
{
title: 'Ingredientes',
icon: Package,
fields: [
{
label: 'Lista de ingredientes',
value: selectedRecipe.ingredients.map(ing => `${ing.name}: ${ing.quantity} ${ing.unit}`),
type: 'list',
span: 2
}
]
},
{
title: 'Etiquetas',
fields: [
{
label: 'Tags',
value: selectedRecipe.tags.join(', '),
span: 2
}
]
}
]}
actions={[
{
label: 'Producir',
icon: ChefHat,
variant: 'primary',
onClick: () => {
console.log('Producing recipe:', selectedRecipe.id);
setShowForm(false);
setSelectedRecipe(null);
}
}
]}
onEdit={() => {
console.log('Editing recipe:', selectedRecipe.id);
}}
/>
)}
</div>
);