From 62b1ab9cb1e65464cf447d68e0c052cef61682eb Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Sat, 30 Aug 2025 19:11:15 +0200 Subject: [PATCH] Add new page designs --- .../src/components/layout/Sidebar/Sidebar.tsx | 120 ++-- .../src/components/ui/Stats/StatsCard.tsx | 215 +++++++ .../src/components/ui/Stats/StatsExample.tsx | 201 ++++++ .../src/components/ui/Stats/StatsGrid.tsx | 94 +++ .../src/components/ui/Stats/StatsPresets.ts | 237 +++++++ frontend/src/components/ui/Stats/index.ts | 4 + frontend/src/components/ui/index.ts | 4 +- .../operations/inventory/InventoryPage.tsx | 467 ++++++++++---- .../app/operations/orders/OrdersPage.tsx | 488 ++++++-------- .../procurement/ProcurementPage.tsx | 360 ++++++----- .../operations/production/ProductionPage.tsx | 572 ++++++++--------- .../app/operations/recipes/RecipesPage.tsx | 607 +++++++++--------- 12 files changed, 2129 insertions(+), 1240 deletions(-) create mode 100644 frontend/src/components/ui/Stats/StatsCard.tsx create mode 100644 frontend/src/components/ui/Stats/StatsExample.tsx create mode 100644 frontend/src/components/ui/Stats/StatsGrid.tsx create mode 100644 frontend/src/components/ui/Stats/StatsPresets.ts create mode 100644 frontend/src/components/ui/Stats/index.ts diff --git a/frontend/src/components/layout/Sidebar/Sidebar.tsx b/frontend/src/components/layout/Sidebar/Sidebar.tsx index eed7ce14..8c8fff49 100644 --- a/frontend/src/components/layout/Sidebar/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar/Sidebar.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, forwardRef } from 'react'; +import React, { useState, useCallback, forwardRef, useMemo } from 'react'; import { clsx } from 'clsx'; import { useLocation, useNavigate } from 'react-router-dom'; import { useAuthUser, useIsAuthenticated } from '../../../stores'; @@ -127,53 +127,55 @@ export const Sidebar = forwardRef(({ const [expandedItems, setExpandedItems] = useState>(new Set()); const sidebarRef = React.useRef(null); - // Get navigation routes from config - const navigationRoutes = getNavigationRoutes(); - - // Convert route config to navigation items - const convertRoutesToItems = (routes: typeof navigationRoutes): NavigationItem[] => { - return routes.map(route => ({ - id: route.path, - label: route.title, - path: route.path, - icon: route.icon ? iconMap[route.icon] : undefined, - requiredPermissions: route.requiredPermissions, - requiredRoles: route.requiredRoles, - children: route.children ? convertRoutesToItems(route.children) : undefined, - })); - }; + // Get navigation routes from config and convert to navigation items - memoized + const navigationItems = useMemo(() => { + const navigationRoutes = getNavigationRoutes(); + + const convertRoutesToItems = (routes: typeof navigationRoutes): NavigationItem[] => { + return routes.map(route => ({ + id: route.path, + label: route.title, + path: route.path, + icon: route.icon ? iconMap[route.icon] : undefined, + requiredPermissions: route.requiredPermissions, + requiredRoles: route.requiredRoles, + children: route.children ? convertRoutesToItems(route.children) : undefined, + })); + }; - const navigationItems = customItems || convertRoutesToItems(navigationRoutes); + return customItems || convertRoutesToItems(navigationRoutes); + }, [customItems]); - // Filter items based on user permissions - const filterItemsByPermissions = (items: NavigationItem[]): NavigationItem[] => { - if (!isAuthenticated || !user) return []; + // Filter items based on user permissions - memoized to prevent infinite re-renders + const visibleItems = useMemo(() => { + const filterItemsByPermissions = (items: NavigationItem[]): NavigationItem[] => { + if (!isAuthenticated || !user) return []; - return items.filter(item => { - const userRoles = user.role ? [user.role] : []; - const userPermissions: string[] = user?.permissions || []; + return items.map(item => ({ + ...item, // Create a shallow copy to avoid mutation + children: item.children ? filterItemsByPermissions(item.children) : item.children + })).filter(item => { + const userRoles = user.role ? [user.role] : []; + const userPermissions: string[] = user?.permissions || []; - const hasAccess = !item.requiredPermissions && !item.requiredRoles || - canAccessRoute( - { - path: item.path, - requiredRoles: item.requiredRoles, - requiredPermissions: item.requiredPermissions - } as any, - isAuthenticated, - userRoles, - userPermissions - ); + const hasAccess = !item.requiredPermissions && !item.requiredRoles || + canAccessRoute( + { + path: item.path, + requiredRoles: item.requiredRoles, + requiredPermissions: item.requiredPermissions + } as any, + isAuthenticated, + userRoles, + userPermissions + ); - if (hasAccess && item.children) { - item.children = filterItemsByPermissions(item.children); - } + return hasAccess; + }); + }; - return hasAccess; - }); - }; - - const visibleItems = filterItemsByPermissions(navigationItems); + return filterItemsByPermissions(navigationItems); + }, [navigationItems, isAuthenticated, user]); // Handle item click const handleItemClick = useCallback((item: NavigationItem) => { @@ -224,30 +226,30 @@ export const Sidebar = forwardRef(({ }, []); // Auto-expand parent items for active path - React.useEffect(() => { - const findParentPaths = (items: NavigationItem[], targetPath: string, parents: string[] = []): string[] => { - for (const item of items) { - const currentPath = [...parents, item.id]; - - if (item.path === targetPath) { - return parents; - } - - if (item.children) { - const found = findParentPaths(item.children, targetPath, currentPath); - if (found.length > 0) { - return found; - } + const findParentPaths = useCallback((items: NavigationItem[], targetPath: string, parents: string[] = []): string[] => { + for (const item of items) { + const currentPath = [...parents, item.id]; + + if (item.path === targetPath) { + return parents; + } + + if (item.children) { + const found = findParentPaths(item.children, targetPath, currentPath); + if (found.length > 0) { + return found; } } - return []; - }; + } + return []; + }, []); + React.useEffect(() => { const parentPaths = findParentPaths(visibleItems, location.pathname); if (parentPaths.length > 0) { setExpandedItems(prev => new Set([...prev, ...parentPaths])); } - }, [location.pathname, visibleItems]); + }, [location.pathname, findParentPaths, visibleItems]); // Expose ref methods React.useImperativeHandle(ref, () => ({ diff --git a/frontend/src/components/ui/Stats/StatsCard.tsx b/frontend/src/components/ui/Stats/StatsCard.tsx new file mode 100644 index 00000000..c0b0b9dc --- /dev/null +++ b/frontend/src/components/ui/Stats/StatsCard.tsx @@ -0,0 +1,215 @@ +import { forwardRef } from 'react'; +import { clsx } from 'clsx'; +import { LucideIcon } from 'lucide-react'; +import { Card } from '../Card'; + +export type StatsCardVariant = 'default' | 'success' | 'info' | 'warning' | 'error' | 'purple'; +export type StatsCardSize = 'sm' | 'md' | 'lg'; + +export interface StatsCardProps { + title: string; + value: string | number; + icon?: LucideIcon; + variant?: StatsCardVariant; + size?: StatsCardSize; + trend?: { + value: number; + label?: string; + direction: 'up' | 'down' | 'neutral'; + }; + subtitle?: string; + loading?: boolean; + className?: string; + formatValue?: (value: string | number) => string; + onClick?: () => void; +} + +const StatsCard = forwardRef(({ + title, + value, + icon: Icon, + variant = 'default', + size = 'md', + trend, + subtitle, + loading = false, + className, + formatValue, + onClick, + ...props +}, ref) => { + const formattedValue = formatValue ? formatValue(value) : value; + + const variantStyles = { + default: { + iconColor: 'var(--text-tertiary)', + valueColor: 'var(--text-primary)', + iconBg: 'var(--bg-tertiary)', + }, + success: { + iconColor: 'var(--color-success)', + valueColor: 'var(--color-success)', + iconBg: 'var(--color-success-50)', + }, + info: { + iconColor: 'var(--color-info)', + valueColor: 'var(--color-info)', + iconBg: 'var(--color-info-50)', + }, + warning: { + iconColor: 'var(--color-warning)', + valueColor: 'var(--color-warning)', + iconBg: 'var(--color-warning-50)', + }, + error: { + iconColor: 'var(--color-error)', + valueColor: 'var(--color-error)', + iconBg: 'var(--color-error-50)', + }, + purple: { + iconColor: '#a78bfa', + valueColor: '#a78bfa', + iconBg: 'rgba(167, 139, 250, 0.1)', + }, + }; + + const sizeStyles = { + sm: { + padding: 'p-4', + iconSize: 20, + valueSize: 'text-2xl', + titleSize: 'text-sm', + iconPadding: 'p-2', + }, + md: { + padding: 'p-6', + iconSize: 24, + valueSize: 'text-3xl', + titleSize: 'text-sm', + iconPadding: 'p-2.5', + }, + lg: { + padding: 'p-8', + iconSize: 28, + valueSize: 'text-4xl', + titleSize: 'text-base', + iconPadding: 'p-3', + }, + }; + + const currentVariant = variantStyles[variant]; + const currentSize = sizeStyles[size]; + const isInteractive = !!onClick; + + const cardClasses = clsx( + { + 'animate-pulse': loading, + }, + className + ); + + const iconContainerClasses = clsx( + 'rounded-xl flex items-center justify-center', + currentSize.iconPadding + ); + + const iconContainerStyle = { + backgroundColor: currentVariant.iconBg, + }; + + if (loading) { + return ( + +
+
+
+
+ {subtitle &&
} +
+
+
+
+ ); + } + + return ( + +
+
+

+ {title} +

+ +
+ + {formattedValue} + + + {trend && ( + + {trend.direction === 'up' && '+'} + {trend.value} + {trend.label && ` ${trend.label}`} + + )} +
+ + {subtitle && ( +

+ {subtitle} +

+ )} +
+ + {Icon && ( +
+ +
+ )} +
+
+ ); +}); + +StatsCard.displayName = 'StatsCard'; + +export default StatsCard; \ No newline at end of file diff --git a/frontend/src/components/ui/Stats/StatsExample.tsx b/frontend/src/components/ui/Stats/StatsExample.tsx new file mode 100644 index 00000000..2dfea7ce --- /dev/null +++ b/frontend/src/components/ui/Stats/StatsExample.tsx @@ -0,0 +1,201 @@ +import { FC } from 'react'; +import { StatsGrid, StatsCard } from './index'; +import { pagePresets, businessMetrics, formatters } from './StatsPresets'; +import { + Calendar, + CheckCircle, + Clock, + AlertTriangle, + Zap, + Shield +} from 'lucide-react'; + +// Example: Production Management Stats (matching your screenshot) +export const ProductionStatsExample: FC = () => { + // Sample data + const productionData = { + dailyTarget: 150, + completed: 85, + inProgress: 12, + pending: 53, + efficiency: 78, + quality: 94, + }; + + return ( + + ); +}; + +// Example: Custom Stats Configuration +export const CustomStatsExample = () => { + return ( + + ); +}; + +// Example: Sales Dashboard +export const SalesStatsExample = () => { + const salesData = { + revenue: 15420, + revenueGrowth: 12.5, + orders: 142, + ordersGrowth: -3.2, + customers: 89, + }; + + return ( + + ); +}; + +// Example: Individual StatsCard Usage +export const IndividualStatsExample = () => { + return ( +
+ + + + + + + console.log('Navigate to satisfaction details')} + /> +
+ ); +}; + +// Example: Loading State +export const LoadingStatsExample = () => { + return ( + + ); +}; + +// Example: Using Business Metrics Directly +export const BusinessMetricsExample = () => { + return ( +
+ + + + + +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/ui/Stats/StatsGrid.tsx b/frontend/src/components/ui/Stats/StatsGrid.tsx new file mode 100644 index 00000000..99b8ad1d --- /dev/null +++ b/frontend/src/components/ui/Stats/StatsGrid.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { clsx } from 'clsx'; +import StatsCard, { StatsCardProps } from './StatsCard'; + +export interface StatsGridProps { + stats: StatsCardProps[]; + columns?: 1 | 2 | 3 | 4 | 5 | 6; + gap?: 'sm' | 'md' | 'lg'; + className?: string; + loading?: boolean; + title?: string; + description?: string; +} + +const StatsGrid: React.FC = ({ + stats, + columns = 3, + gap = 'md', + className, + loading = false, + title, + description, +}) => { + const gridClasses = { + 1: 'grid-cols-1', + 2: 'grid-cols-1 sm:grid-cols-2', + 3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3', + 4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4', + 5: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5', + 6: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6', + }; + + const gapClasses = { + sm: 'gap-3', + md: 'gap-4', + lg: 'gap-6', + }; + + const containerClasses = clsx( + 'grid', + gridClasses[columns], + gapClasses[gap], + className + ); + + return ( +
+ {/* Header */} + {(title || description) && ( +
+ {title && ( +

+ {title} +

+ )} + {description && ( +

+ {description} +

+ )} +
+ )} + + {/* Stats Grid */} +
+ {loading + ? Array.from({ length: stats.length || 6 }).map((_, index) => ( + + )) + : stats.map((stat, index) => ( + + )) + } +
+
+ ); +}; + +export default StatsGrid; \ No newline at end of file diff --git a/frontend/src/components/ui/Stats/StatsPresets.ts b/frontend/src/components/ui/Stats/StatsPresets.ts new file mode 100644 index 00000000..4f6eca3e --- /dev/null +++ b/frontend/src/components/ui/Stats/StatsPresets.ts @@ -0,0 +1,237 @@ +import { + Calendar, + CheckCircle, + Clock, + AlertTriangle, + Zap, + Shield, + TrendingUp, + Package, + Users, + DollarSign, + BarChart3, + Target, + Activity, + Award +} from 'lucide-react'; +import { StatsCardProps, StatsCardVariant } from './StatsCard'; + +// Common formatting functions +export const formatters = { + percentage: (value: string | number): string => `${value}%`, + currency: (value: string | number): string => `€${parseFloat(String(value)).toFixed(2)}`, + number: (value: string | number): string => parseFloat(String(value)).toLocaleString('es-ES'), + compact: (value: string | number): string => { + const num = parseFloat(String(value)); + if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`; + if (num >= 1000) return `${(num / 1000).toFixed(1)}K`; + return num.toString(); + }, +}; + +// Icon mappings for common stat types +export const statIcons = { + target: Calendar, + completed: CheckCircle, + inProgress: Clock, + pending: AlertTriangle, + efficiency: Zap, + quality: Shield, + growth: TrendingUp, + inventory: Package, + users: Users, + revenue: DollarSign, + analytics: BarChart3, + goals: Target, + activity: Activity, + achievement: Award, +}; + +// Variant mappings for common stat types +export const statVariants: Record = { + target: 'default', + completed: 'success', + inProgress: 'info', + pending: 'warning', + efficiency: 'purple', + quality: 'success', + error: 'error', + revenue: 'success', + growth: 'success', + decline: 'error', +}; + +// Predefined stat configurations for common business metrics +export const businessMetrics = { + production: { + dailyTarget: (value: number): StatsCardProps => ({ + title: 'Meta Diaria', + value, + icon: statIcons.target, + variant: statVariants.target, + formatValue: formatters.number, + }), + completed: (value: number): StatsCardProps => ({ + title: 'Completado', + value, + icon: statIcons.completed, + variant: statVariants.completed, + formatValue: formatters.number, + }), + inProgress: (value: number): StatsCardProps => ({ + title: 'En Proceso', + value, + icon: statIcons.inProgress, + variant: statVariants.inProgress, + formatValue: formatters.number, + }), + pending: (value: number): StatsCardProps => ({ + title: 'Pendiente', + value, + icon: statIcons.pending, + variant: statVariants.pending, + formatValue: formatters.number, + }), + efficiency: (value: number): StatsCardProps => ({ + title: 'Eficiencia', + value, + icon: statIcons.efficiency, + variant: statVariants.efficiency, + formatValue: formatters.percentage, + }), + quality: (value: number): StatsCardProps => ({ + title: 'Calidad', + value, + icon: statIcons.quality, + variant: statVariants.quality, + formatValue: formatters.percentage, + }), + }, + + sales: { + revenue: (value: number, trend?: { value: number; direction: 'up' | 'down' | 'neutral'; label?: string }): StatsCardProps => ({ + title: 'Ingresos', + value, + icon: statIcons.revenue, + variant: statVariants.revenue, + formatValue: formatters.currency, + trend, + }), + orders: (value: number, trend?: { value: number; direction: 'up' | 'down' | 'neutral'; label?: string }): StatsCardProps => ({ + title: 'Pedidos', + value, + icon: statIcons.analytics, + variant: statVariants.target, + formatValue: formatters.number, + trend, + }), + customers: (value: number): StatsCardProps => ({ + title: 'Clientes', + value, + icon: statIcons.users, + variant: statVariants.target, + formatValue: formatters.number, + }), + }, + + inventory: { + totalItems: (value: number): StatsCardProps => ({ + title: 'Total Items', + value, + icon: statIcons.inventory, + variant: statVariants.target, + formatValue: formatters.number, + }), + lowStock: (value: number): StatsCardProps => ({ + title: 'Stock Bajo', + value, + icon: statIcons.pending, + variant: value > 0 ? statVariants.pending : statVariants.completed, + formatValue: formatters.number, + }), + outOfStock: (value: number): StatsCardProps => ({ + title: 'Sin Stock', + value, + icon: statIcons.pending, + variant: value > 0 ? statVariants.error : statVariants.completed, + formatValue: formatters.number, + }), + }, + + performance: { + growth: (value: number): StatsCardProps => ({ + title: 'Crecimiento', + value, + icon: statIcons.growth, + variant: value >= 0 ? statVariants.growth : statVariants.decline, + formatValue: formatters.percentage, + trend: { + value: Math.abs(value), + direction: value >= 0 ? 'up' : 'down', + label: 'vs mes anterior', + }, + }), + satisfaction: (value: number): StatsCardProps => ({ + title: 'Satisfacción', + value, + icon: statIcons.achievement, + variant: value >= 80 ? statVariants.quality : value >= 60 ? statVariants.pending : statVariants.error, + formatValue: formatters.percentage, + }), + }, +}; + +// Quick preset configurations for common page layouts +export const pagePresets = { + production: (data: { + dailyTarget: number; + completed: number; + inProgress: number; + pending: number; + efficiency: number; + quality: number; + }): StatsCardProps[] => [ + businessMetrics.production.dailyTarget(data.dailyTarget), + businessMetrics.production.completed(data.completed), + businessMetrics.production.inProgress(data.inProgress), + businessMetrics.production.pending(data.pending), + businessMetrics.production.efficiency(data.efficiency), + businessMetrics.production.quality(data.quality), + ], + + sales: (data: { + revenue: number; + revenueGrowth?: number; + orders: number; + ordersGrowth?: number; + customers: number; + }): StatsCardProps[] => [ + businessMetrics.sales.revenue( + data.revenue, + data.revenueGrowth ? { + value: data.revenueGrowth, + direction: data.revenueGrowth >= 0 ? 'up' : 'down', + label: '%', + } : undefined + ), + businessMetrics.sales.orders( + data.orders, + data.ordersGrowth ? { + value: data.ordersGrowth, + direction: data.ordersGrowth >= 0 ? 'up' : 'down', + label: '%', + } : undefined + ), + businessMetrics.sales.customers(data.customers), + ], + + inventory: (data: { + totalItems: number; + lowStock: number; + outOfStock: number; + }): StatsCardProps[] => [ + businessMetrics.inventory.totalItems(data.totalItems), + businessMetrics.inventory.lowStock(data.lowStock), + businessMetrics.inventory.outOfStock(data.outOfStock), + ], +}; \ No newline at end of file diff --git a/frontend/src/components/ui/Stats/index.ts b/frontend/src/components/ui/Stats/index.ts new file mode 100644 index 00000000..8cf12e40 --- /dev/null +++ b/frontend/src/components/ui/Stats/index.ts @@ -0,0 +1,4 @@ +export { default as StatsCard } from './StatsCard'; +export { default as StatsGrid } from './StatsGrid'; +export type { StatsCardProps, StatsCardVariant, StatsCardSize } from './StatsCard'; +export type { StatsGridProps } from './StatsGrid'; \ No newline at end of file diff --git a/frontend/src/components/ui/index.ts b/frontend/src/components/ui/index.ts index de04872a..584d3f20 100644 --- a/frontend/src/components/ui/index.ts +++ b/frontend/src/components/ui/index.ts @@ -13,6 +13,7 @@ export { ThemeToggle } from './ThemeToggle'; export { ProgressBar } from './ProgressBar'; export { StatusIndicator } from './StatusIndicator'; export { ListItem } from './ListItem'; +export { StatsCard, StatsGrid } from './Stats'; // Export types export type { ButtonProps } from './Button'; @@ -28,4 +29,5 @@ export type { DatePickerProps } from './DatePicker'; export type { ThemeToggleProps } from './ThemeToggle'; export type { ProgressBarProps } from './ProgressBar'; export type { StatusIndicatorProps } from './StatusIndicator'; -export type { ListItemProps } from './ListItem'; \ No newline at end of file +export type { ListItemProps } from './ListItem'; +export type { StatsCardProps, StatsCardVariant, StatsCardSize, StatsGridProps } from './Stats'; \ No newline at end of file diff --git a/frontend/src/pages/app/operations/inventory/InventoryPage.tsx b/frontend/src/pages/app/operations/inventory/InventoryPage.tsx index 59b60a30..1efa6263 100644 --- a/frontend/src/pages/app/operations/inventory/InventoryPage.tsx +++ b/frontend/src/pages/app/operations/inventory/InventoryPage.tsx @@ -1,15 +1,14 @@ import React, { useState } from 'react'; -import { Plus, Search, Filter, Download, AlertTriangle } from 'lucide-react'; -import { Button, Input, Card, Badge } from '../../../../components/ui'; +import { Plus, Download, AlertTriangle, Package, Clock, CheckCircle, Eye, Edit, Calendar, DollarSign } from 'lucide-react'; +import { Button, Input, Card, Badge, StatsGrid } from '../../../../components/ui'; +import { formatters } from '../../../../components/ui/Stats/StatsPresets'; import { PageHeader } from '../../../../components/layout'; -import { InventoryTable, InventoryForm, LowStockAlert } from '../../../../components/domain/inventory'; +import { InventoryForm, LowStockAlert } from '../../../../components/domain/inventory'; const InventoryPage: React.FC = () => { const [searchTerm, setSearchTerm] = useState(''); const [showForm, setShowForm] = useState(false); - const [selectedItem, setSelectedItem] = useState(null); - const [filterCategory, setFilterCategory] = useState('all'); - const [filterStatus, setFilterStatus] = useState('all'); + const [selectedItem, setSelectedItem] = useState(null); const mockInventoryItems = [ { @@ -54,157 +53,355 @@ const InventoryPage: React.FC = () => { expirationDate: '2024-02-10', status: 'normal', }, + { + id: '4', + name: 'Azúcar Blanco', + category: 'Azúcares', + currentStock: 0, + minStock: 15, + maxStock: 50, + unit: 'kg', + cost: 0.95, + supplier: 'Distribuidora Central', + lastRestocked: '2024-01-10', + expirationDate: '2024-12-31', + status: 'out', + }, + { + id: '5', + name: 'Leche Entera', + category: 'Lácteos', + currentStock: 3, + minStock: 10, + maxStock: 40, + unit: 'L', + cost: 1.45, + supplier: 'Lácteos Frescos', + lastRestocked: '2024-01-22', + expirationDate: '2024-01-28', + status: 'expired', + }, ]; - const lowStockItems = mockInventoryItems.filter(item => item.status === 'low'); - - const stats = { - totalItems: mockInventoryItems.length, - lowStockItems: lowStockItems.length, - totalValue: mockInventoryItems.reduce((sum, item) => sum + (item.currentStock * item.cost), 0), - needsReorder: lowStockItems.length, + const getStockStatusBadge = (item: typeof mockInventoryItems[0]) => { + const { currentStock, minStock, status } = item; + + if (status === 'expired') { + return ( + } + text="Caducado" + /> + ); + } + + if (currentStock === 0) { + return ( + } + text="Sin Stock" + /> + ); + } + + if (currentStock <= minStock) { + return ( + } + text="Stock Bajo" + /> + ); + } + + return ( + } + 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' }, + }; + + const config = categoryConfig[category as keyof typeof categoryConfig] || { color: 'default' }; + return ( + + ); + }; + + const filteredItems = mockInventoryItems.filter(item => { + const matchesSearch = item.name.toLowerCase().includes(searchTerm.toLowerCase()) || + item.category.toLowerCase().includes(searchTerm.toLowerCase()) || + item.supplier.toLowerCase().includes(searchTerm.toLowerCase()); + + return matchesSearch; + }); + + const lowStockItems = mockInventoryItems.filter(item => + item.currentStock <= item.minStock || item.status === 'low' || item.status === 'out' || item.status === 'expired' + ); + + const mockInventoryStats = { + totalItems: mockInventoryItems.length, + lowStockItems: lowStockItems.length, + outOfStock: mockInventoryItems.filter(item => item.currentStock === 0).length, + expiringSoon: mockInventoryItems.filter(item => item.status === 'expired').length, + totalValue: mockInventoryItems.reduce((sum, item) => sum + (item.currentStock * item.cost), 0), + categories: [...new Set(mockInventoryItems.map(item => item.category))].length, + }; + + const inventoryStats = [ + { + title: 'Total Artículos', + value: mockInventoryStats.totalItems, + variant: 'default' as const, + icon: Package, + }, + { + title: 'Stock Bajo', + value: mockInventoryStats.lowStockItems, + variant: 'warning' as const, + icon: AlertTriangle, + }, + { + title: 'Sin Stock', + value: mockInventoryStats.outOfStock, + variant: 'error' as const, + icon: AlertTriangle, + }, + { + title: 'Por Caducar', + value: mockInventoryStats.expiringSoon, + variant: 'error' as const, + icon: Clock, + }, + { + title: 'Valor Total', + value: formatters.currency(mockInventoryStats.totalValue), + variant: 'success' as const, + icon: DollarSign, + }, + { + title: 'Categorías', + value: mockInventoryStats.categories, + variant: 'info' as const, + icon: Package, + }, + ]; + return ( -
+
setShowForm(true)}> - - Nuevo Artículo - - } + 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) + } + ]} /> - {/* Stats Cards */} -
- -
-
-

Total Artículos

-

{stats.totalItems}

-
-
- - - -
-
-
- - -
-
-

Stock Bajo

-

{stats.lowStockItems}

-
-
- -
-
-
- - -
-
-

Valor Total

-

€{stats.totalValue.toFixed(2)}

-
-
- - - -
-
-
- - -
-
-

Necesita Reorden

-

{stats.needsReorder}

-
-
- - - -
-
-
-
+ {/* Stats Grid */} + {/* Low Stock Alert */} {lowStockItems.length > 0 && ( )} - {/* Filters and Search */} - + {/* Simplified Controls */} +
-
- - setSearchTerm(e.target.value)} - className="pl-10" - /> -
-
- -
- - - - - - - + setSearchTerm(e.target.value)} + className="w-full" + />
+
- {/* Inventory Table */} - - { - setSelectedItem(item); - setShowForm(true); - }} - /> - + {/* Inventory Items Grid */} +
+ {filteredItems.map((item) => ( + +
+ {/* Header */} +
+
+
+ +
+
+
+ {item.name} +
+
+ {item.supplier} +
+
+
+ {getStockStatusBadge(item)} +
+ + {/* Category and Stock */} +
+
+ {getCategoryBadge(item.category)} +
+
+
+ {item.currentStock} {item.unit} +
+
+ Mín: {item.minStock} | Máx: {item.maxStock} +
+
+
+ + {/* Value and Dates */} +
+
+
Costo unitario:
+
+ {formatters.currency(item.cost)} +
+
+
+
Valor total:
+
+ {formatters.currency(item.currentStock * item.cost)} +
+
+
+ + {/* Dates */} +
+
+ Último restock: + + {new Date(item.lastRestocked).toLocaleDateString('es-ES')} + +
+
+ Caducidad: + + {new Date(item.expirationDate).toLocaleDateString('es-ES')} + +
+
+ + {/* Stock Level Progress */} +
+
+ Nivel de stock + + {Math.round((item.currentStock / item.maxStock) * 100)}% + +
+
+
+
+
+ + {/* Actions */} +
+ + +
+
+ + ))} +
+ + {/* Empty State */} + {filteredItems.length === 0 && ( +
+ +

+ No se encontraron artículos +

+

+ Intenta ajustar la búsqueda o agregar un nuevo artículo al inventario +

+ +
+ )} {/* Inventory Form Modal */} {showForm && ( diff --git a/frontend/src/pages/app/operations/orders/OrdersPage.tsx b/frontend/src/pages/app/operations/orders/OrdersPage.tsx index e4d13196..10e5df47 100644 --- a/frontend/src/pages/app/operations/orders/OrdersPage.tsx +++ b/frontend/src/pages/app/operations/orders/OrdersPage.tsx @@ -1,15 +1,15 @@ import React, { useState } from 'react'; -import { Plus, Search, Filter, Download, Calendar, Clock, User, Package } from 'lucide-react'; -import { Button, Input, Card, Badge, Table } from '../../../../components/ui'; -import type { TableColumn } from '../../../../components/ui'; +import { Plus, Download, Clock, Package, Eye, Edit, CheckCircle, AlertCircle, Timer } from 'lucide-react'; +import { Button, Input, Card, Badge, StatsGrid } from '../../../../components/ui'; +import { formatters } from '../../../../components/ui/Stats/StatsPresets'; import { PageHeader } from '../../../../components/layout'; -import { OrdersTable, OrderForm } from '../../../../components/domain/sales'; +import { OrderForm } from '../../../../components/domain/sales'; const OrdersPage: React.FC = () => { - const [activeTab, setActiveTab] = useState('all'); + const [activeTab] = useState('all'); const [searchTerm, setSearchTerm] = useState(''); const [showForm, setShowForm] = useState(false); - const [selectedOrder, setSelectedOrder] = useState(null); + const [selectedOrder, setSelectedOrder] = useState(null); const mockOrders = [ { @@ -82,148 +82,24 @@ const OrdersPage: React.FC = () => { const getStatusBadge = (status: string) => { const statusConfig = { - pending: { color: 'yellow', text: 'Pendiente' }, - in_progress: { color: 'blue', text: 'En Proceso' }, - ready: { color: 'green', text: 'Listo' }, - completed: { color: 'green', text: 'Completado' }, - cancelled: { color: 'red', text: 'Cancelado' }, + 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 }, }; const config = statusConfig[status as keyof typeof statusConfig]; - return {config?.text || status}; + const Icon = config?.icon; + return ( + } + text={config?.text || status} + /> + ); }; - const getPriorityBadge = (priority: string) => { - const priorityConfig = { - low: { color: 'gray', text: 'Baja' }, - normal: { color: 'blue', text: 'Normal' }, - high: { color: 'orange', text: 'Alta' }, - urgent: { color: 'red', text: 'Urgente' }, - }; - - const config = priorityConfig[priority as keyof typeof priorityConfig]; - return {config?.text || priority}; - }; - - const getPaymentStatusBadge = (status: string) => { - const statusConfig = { - pending: { color: 'yellow', text: 'Pendiente' }, - paid: { color: 'green', text: 'Pagado' }, - failed: { color: 'red', text: 'Fallido' }, - }; - - const config = statusConfig[status as keyof typeof statusConfig]; - return {config?.text || status}; - }; - - const columns: TableColumn[] = [ - { - key: 'id', - title: 'Pedido', - dataIndex: 'id', - render: (value, record: any) => ( -
-
{value}
-
{record.deliveryMethod === 'delivery' ? 'Entrega' : 'Recogida'}
-
- ), - }, - { - key: 'customer', - title: 'Cliente', - render: (_, record: any) => ( -
- -
-
{record.customerName}
-
{record.customerEmail}
-
-
- ), - }, - { - key: 'status', - title: 'Estado', - dataIndex: 'status', - render: (value) => getStatusBadge(value), - }, - { - key: 'priority', - title: 'Prioridad', - dataIndex: 'priority', - render: (value) => getPriorityBadge(value), - }, - { - key: 'orderDate', - title: 'Fecha Pedido', - dataIndex: 'orderDate', - render: (value) => ( -
-
{new Date(value).toLocaleDateString('es-ES')}
-
- {new Date(value).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })} -
-
- ), - }, - { - key: 'deliveryDate', - title: 'Entrega', - dataIndex: 'deliveryDate', - render: (value) => ( -
-
{new Date(value).toLocaleDateString('es-ES')}
-
- {new Date(value).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })} -
-
- ), - }, - { - key: 'total', - title: 'Total', - dataIndex: 'total', - render: (value, record: any) => ( -
-
€{value.toFixed(2)}
-
{record.items.length} artículos
-
- ), - }, - { - key: 'payment', - title: 'Pago', - render: (_, record: any) => ( -
- {getPaymentStatusBadge(record.paymentStatus)} -
{record.paymentMethod}
-
- ), - }, - { - key: 'actions', - title: 'Acciones', - align: 'right' as const, - render: (_, record: any) => ( -
- - -
- ), - }, - ]; - const filteredOrders = mockOrders.filter(order => { const matchesSearch = order.customerName.toLowerCase().includes(searchTerm.toLowerCase()) || order.id.toLowerCase().includes(searchTerm.toLowerCase()) || @@ -234,181 +110,211 @@ const OrdersPage: React.FC = () => { return matchesSearch && matchesTab; }); - const stats = { + const mockOrderStats = { total: mockOrders.length, pending: mockOrders.filter(o => o.status === 'pending').length, inProgress: mockOrders.filter(o => o.status === 'in_progress').length, completed: mockOrders.filter(o => o.status === 'completed').length, + cancelled: mockOrders.filter(o => o.status === 'cancelled').length, totalRevenue: mockOrders.reduce((sum, order) => sum + order.total, 0), + averageOrder: mockOrders.reduce((sum, order) => sum + order.total, 0) / mockOrders.length, + todayOrders: mockOrders.filter(o => + new Date(o.orderDate).toDateString() === new Date().toDateString() + ).length, }; - const tabs = [ - { id: 'all', label: 'Todos', count: stats.total }, - { id: 'pending', label: 'Pendientes', count: stats.pending }, - { id: 'in_progress', label: 'En Proceso', count: stats.inProgress }, - { id: 'ready', label: 'Listos', count: 0 }, - { id: 'completed', label: 'Completados', count: stats.completed }, + const stats = [ + { + title: 'Total Pedidos', + value: mockOrderStats.total, + variant: 'default' as const, + icon: Package, + }, + { + title: 'Pendientes', + value: mockOrderStats.pending, + variant: 'warning' as const, + icon: Clock, + }, + { + title: 'En Proceso', + value: mockOrderStats.inProgress, + variant: 'info' as const, + icon: Timer, + }, + { + title: 'Completados', + value: mockOrderStats.completed, + variant: 'success' as const, + icon: CheckCircle, + }, + { + title: 'Ingresos Total', + value: formatters.currency(mockOrderStats.totalRevenue), + variant: 'success' as const, + icon: Package, + }, + { + title: 'Promedio', + value: formatters.currency(mockOrderStats.averageOrder), + variant: 'info' as const, + icon: Package, + }, ]; return ( -
- + console.log('Export orders') + }, + { + id: "new", + label: "Nuevo Pedido", + variant: "primary" as const, + icon: Plus, + onClick: () => setShowForm(true) + } + ]} + /> + + {/* Stats Grid */} + + + {/* Simplified Controls */} + +
+
+ setSearchTerm(e.target.value)} + className="w-full" + /> +
+ +
+
+ + {/* Orders Grid */} +
+ {filteredOrders.map((order) => ( + +
+ {/* Header */} +
+
+
+ +
+
+
+ {order.id} +
+ + {order.customerName} + +
+
+ {getStatusBadge(order.status)} +
+ + {/* Key Info */} +
+
+
+ {formatters.currency(order.total)} +
+
+ {order.items?.length} artículos +
+
+
+
+ {new Date(order.deliveryDate).toLocaleDateString('es-ES')} +
+
+ Entrega: {new Date(order.deliveryDate).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })} +
+
+
+ + {/* Actions */} +
+ + +
+
+
+ ))} +
+ + {/* Empty State */} + {filteredOrders.length === 0 && ( +
+ +

+ No se encontraron pedidos +

+

+ Intenta ajustar la búsqueda o crear un nuevo pedido +

- } - /> - - {/* Stats Cards */} -
- -
-
-

Total Pedidos

-

{stats.total}

-
- -
-
- - -
-
-

Pendientes

-

{stats.pending}

-
- -
-
- - -
-
-

En Proceso

-

{stats.inProgress}

-
-
- - - -
-
-
- - -
-
-

Completados

-

{stats.completed}

-
-
- - - -
-
-
- - -
-
-

Ingresos

-

€{stats.totalRevenue.toFixed(2)}

-
-
- - - -
-
-
-
- - {/* Tabs Navigation */} -
- -
- - {/* Search and Filters */} - -
-
-
- - setSearchTerm(e.target.value)} - className="pl-10" - /> -
-
- -
- - - -
-
- - {/* Orders Table */} - - - + )} {/* Order Form Modal */} {showForm && ( { + orderId={selectedOrder?.id} + onOrderCancel={() => { setShowForm(false); setSelectedOrder(null); }} - onSave={(order) => { + onOrderSave={async (order: any) => { // Handle save logic console.log('Saving order:', order); setShowForm(false); setSelectedOrder(null); + return true; }} /> )} diff --git a/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx b/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx index 104f7bf5..c6229a24 100644 --- a/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx +++ b/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; -import { Plus, Search, Filter, Download, ShoppingCart, Truck, DollarSign, Calendar } from 'lucide-react'; -import { Button, Input, Card, Badge, Table } from '../../../../components/ui'; -import type { TableColumn } from '../../../../components/ui'; +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 { formatters } from '../../../../components/ui/Stats/StatsPresets'; import { PageHeader } from '../../../../components/layout'; const ProcurementPage: React.FC = () => { @@ -103,152 +103,126 @@ const ProcurementPage: React.FC = () => { const getStatusBadge = (status: string) => { const statusConfig = { - pending: { color: 'yellow', text: 'Pendiente' }, - approved: { color: 'blue', text: 'Aprobado' }, - in_transit: { color: 'purple', text: 'En Tránsito' }, - delivered: { color: 'green', text: 'Entregado' }, - cancelled: { color: 'red', text: 'Cancelado' }, + 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 }, }; const config = statusConfig[status as keyof typeof statusConfig]; - return {config?.text || status}; + const Icon = config?.icon; + return ( + } + text={config?.text || status} + /> + ); }; const getPaymentStatusBadge = (status: string) => { const statusConfig = { - pending: { color: 'yellow', text: 'Pendiente' }, - paid: { color: 'green', text: 'Pagado' }, - overdue: { color: 'red', text: 'Vencido' }, + pending: { color: 'warning', text: 'Pendiente', icon: Clock }, + paid: { color: 'success', text: 'Pagado', icon: CheckCircle }, + overdue: { color: 'error', text: 'Vencido', icon: AlertCircle }, }; const config = statusConfig[status as keyof typeof statusConfig]; - return {config?.text || status}; + const Icon = config?.icon; + return ( + } + text={config?.text || status} + /> + ); }; - const columns: TableColumn[] = [ - { - key: 'id', - title: 'Orden', - dataIndex: 'id', - render: (value, record: any) => ( -
-
{value}
- {record.notes && ( -
{record.notes}
- )} -
- ), - }, - { - key: 'supplier', - title: 'Proveedor', - dataIndex: 'supplier', - }, - { - key: 'status', - title: 'Estado', - dataIndex: 'status', - render: (value) => getStatusBadge(value), - }, - { - key: 'orderDate', - title: 'Fecha Pedido', - dataIndex: 'orderDate', - render: (value) => new Date(value).toLocaleDateString('es-ES'), - }, - { - key: 'deliveryDate', - title: 'Fecha Entrega', - dataIndex: 'deliveryDate', - render: (value) => new Date(value).toLocaleDateString('es-ES'), - }, - { - key: 'totalAmount', - title: 'Monto Total', - dataIndex: 'totalAmount', - render: (value) => `€${value.toLocaleString()}`, - }, - { - key: 'paymentStatus', - title: 'Pago', - dataIndex: 'paymentStatus', - render: (value) => getPaymentStatusBadge(value), - }, - { - key: 'actions', - title: 'Acciones', - align: 'right' as const, - render: () => ( -
- - -
- ), - }, - ]; + const filteredOrders = mockPurchaseOrders.filter(order => { + const matchesSearch = order.supplier.toLowerCase().includes(searchTerm.toLowerCase()) || + order.id.toLowerCase().includes(searchTerm.toLowerCase()) || + order.notes.toLowerCase().includes(searchTerm.toLowerCase()); + + return matchesSearch; + }); - const stats = { + const mockPurchaseStats = { totalOrders: mockPurchaseOrders.length, pendingOrders: mockPurchaseOrders.filter(o => o.status === 'pending').length, + inTransit: mockPurchaseOrders.filter(o => o.status === 'in_transit').length, + delivered: mockPurchaseOrders.filter(o => o.status === 'delivered').length, totalSpent: mockPurchaseOrders.reduce((sum, order) => sum + order.totalAmount, 0), activeSuppliers: mockSuppliers.filter(s => s.status === 'active').length, }; + const purchaseOrderStats = [ + { + title: 'Total Órdenes', + value: mockPurchaseStats.totalOrders, + variant: 'default' as const, + icon: ShoppingCart, + }, + { + title: 'Pendientes', + value: mockPurchaseStats.pendingOrders, + variant: 'warning' as const, + icon: Clock, + }, + { + title: 'En Tránsito', + value: mockPurchaseStats.inTransit, + variant: 'info' as const, + icon: Truck, + }, + { + title: 'Entregadas', + value: mockPurchaseStats.delivered, + variant: 'success' as const, + icon: CheckCircle, + }, + { + title: 'Gasto Total', + value: formatters.currency(mockPurchaseStats.totalSpent), + variant: 'success' as const, + icon: DollarSign, + }, + { + title: 'Proveedores', + value: mockPurchaseStats.activeSuppliers, + variant: 'info' as const, + icon: Package, + }, + ]; + return (
- - Nueva Orden de Compra - - } + actions={[ + { + id: "export", + label: "Exportar", + variant: "outline" as const, + icon: Download, + onClick: () => console.log('Export purchase orders') + }, + { + id: "new", + label: "Nueva Orden de Compra", + variant: "primary" as const, + icon: Plus, + onClick: () => console.log('New purchase order') + } + ]} /> - {/* Stats Cards */} -
- -
-
-

Órdenes Totales

-

{stats.totalOrders}

-
- -
-
- - -
-
-

Órdenes Pendientes

-

{stats.pendingOrders}

-
- -
-
- - -
-
-

Gasto Total

-

€{stats.totalSpent.toLocaleString()}

-
- -
-
- - -
-
-

Proveedores Activos

-

{stats.activeSuppliers}

-
- -
-
-
+ {/* Stats Grid */} + {/* Tabs Navigation */}
@@ -286,48 +260,128 @@ const ProcurementPage: React.FC = () => {
- {/* Search and Filters */} - -
-
-
- + {activeTab === 'orders' && ( + +
+
setSearchTerm(e.target.value)} - className="pl-10" + className="w-full" />
-
- -
- -
-
- - - {/* Tab Content */} - {activeTab === 'orders' && ( - -
)} + {/* Purchase Orders Grid */} + {activeTab === 'orders' && ( +
+ {filteredOrders.map((order) => ( + +
+ {/* Header */} +
+
+
+ +
+
+
+ {order.id} +
+ + {order.supplier} + +
+
+ {getStatusBadge(order.status)} +
+ + {/* Key Info */} +
+
+
+ {formatters.currency(order.totalAmount)} +
+
+ {order.items?.length} artículos +
+
+
+
+ {new Date(order.deliveryDate).toLocaleDateString('es-ES')} +
+
+ Entrega prevista +
+
+
+ + {/* Payment Status */} +
+
+ Estado del pago: +
+ {getPaymentStatusBadge(order.paymentStatus)} +
+ + {/* Notes */} + {order.notes && ( +
+ "{order.notes}" +
+ )} + + {/* Actions */} +
+ + +
+
+
+ ))} +
+ )} + + {/* Empty State for Purchase Orders */} + {activeTab === 'orders' && filteredOrders.length === 0 && ( +
+ +

+ No se encontraron órdenes de compra +

+

+ Intenta ajustar la búsqueda o crear una nueva orden de compra +

+ +
+ )} + {activeTab === 'suppliers' && (
{mockSuppliers.map((supplier) => ( diff --git a/frontend/src/pages/app/operations/production/ProductionPage.tsx b/frontend/src/pages/app/operations/production/ProductionPage.tsx index 154e95ea..db20fece 100644 --- a/frontend/src/pages/app/operations/production/ProductionPage.tsx +++ b/frontend/src/pages/app/operations/production/ProductionPage.tsx @@ -1,22 +1,15 @@ import React, { useState } from 'react'; -import { Plus, Calendar, Clock, Users, AlertCircle, Search, Download, Filter } from 'lucide-react'; -import { Button, Card, Badge } from '../../../../components/ui'; -import { DataTable } from '../../../../components/shared'; -import type { DataTableColumn, DataTableFilter, DataTablePagination, DataTableSelection } from '../../../../components/shared'; +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 { pagePresets } from '../../../../components/ui/Stats/StatsPresets'; import { PageHeader } from '../../../../components/layout'; import { ProductionSchedule, BatchTracker, QualityControl } from '../../../../components/domain/production'; const ProductionPage: React.FC = () => { const [activeTab, setActiveTab] = useState('schedule'); const [searchQuery, setSearchQuery] = useState(''); - const [filters, setFilters] = useState([]); - const [selectedBatches, setSelectedBatches] = useState([]); - const [pagination, setPagination] = useState({ - page: 1, - pageSize: 10, - total: 8 // Updated to match the number of mock orders - }); - const [isLoading, setIsLoading] = useState(false); + const [selectedOrder, setSelectedOrder] = useState(null); + const [showForm, setShowForm] = useState(false); const mockProductionStats = { dailyTarget: 150, @@ -27,41 +20,6 @@ const ProductionPage: React.FC = () => { quality: 94, }; - // Handler functions for table actions - const handleViewBatch = (batch: any) => { - console.log('Ver lote:', batch); - // Implement view logic - }; - - const handleEditBatch = (batch: any) => { - console.log('Editar lote:', batch); - // Implement edit logic - }; - - const handleSearchChange = (query: string) => { - setSearchQuery(query); - // Implement search logic - }; - - const handleFiltersChange = (newFilters: DataTableFilter[]) => { - setFilters(newFilters); - // Implement filtering logic - }; - - const handlePageChange = (page: number, pageSize: number) => { - setPagination(prev => ({ ...prev, page, pageSize })); - // Implement pagination logic - }; - - const handleBatchSelection = (selectedRows: any[]) => { - setSelectedBatches(selectedRows); - }; - - const handleExport = (format: 'csv' | 'xlsx') => { - console.log(`Exportando en formato ${format}`); - // Implement export logic - }; - const mockProductionOrders = [ { id: '1', @@ -155,248 +113,78 @@ const ProductionPage: React.FC = () => { const getStatusBadge = (status: string) => { const statusConfig = { - pending: { color: 'yellow', text: 'Pendiente' }, - in_progress: { color: 'blue', text: 'En Proceso' }, - completed: { color: 'green', text: 'Completado' }, - cancelled: { color: 'red', text: 'Cancelado' }, + 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 }, }; const config = statusConfig[status as keyof typeof statusConfig]; - return {config.text}; + const Icon = config?.icon; + return ( + } + text={config?.text || status} + /> + ); }; const getPriorityBadge = (priority: string) => { const priorityConfig = { - low: { color: 'gray', text: 'Baja' }, - medium: { color: 'yellow', text: 'Media' }, - high: { color: 'orange', text: 'Alta' }, - urgent: { color: 'red', text: 'Urgente' }, + low: { color: 'outline', text: 'Baja' }, + medium: { color: 'secondary', text: 'Media' }, + high: { color: 'warning', text: 'Alta' }, + urgent: { color: 'error', text: 'Urgente', icon: Zap }, }; const config = priorityConfig[priority as keyof typeof priorityConfig]; - return {config.text}; + const Icon = config?.icon; + return ( + } + text={config?.text || priority} + /> + ); }; - const columns: DataTableColumn[] = [ - { - id: 'recipeName', - key: 'recipeName', - header: 'Receta', - sortable: true, - filterable: true, - type: 'text', - width: 200, - cell: (value) => ( -
{value}
- ), - }, - { - id: 'quantity', - key: 'quantity', - header: 'Cantidad', - sortable: true, - filterable: true, - type: 'number', - width: 120, - align: 'center', - cell: (value) => `${value} unidades`, - }, - { - id: 'status', - key: 'status', - header: 'Estado', - sortable: true, - filterable: true, - type: 'select', - width: 130, - align: 'center', - selectOptions: [ - { value: 'pending', label: 'Pendiente' }, - { value: 'in_progress', label: 'En Proceso' }, - { value: 'completed', label: 'Completado' }, - { value: 'cancelled', label: 'Cancelado' } - ], - cell: (value) => getStatusBadge(value), - }, - { - id: 'priority', - key: 'priority', - header: 'Prioridad', - sortable: true, - filterable: true, - type: 'select', - width: 120, - align: 'center', - selectOptions: [ - { value: 'low', label: 'Baja' }, - { value: 'medium', label: 'Media' }, - { value: 'high', label: 'Alta' }, - { value: 'urgent', label: 'Urgente' } - ], - cell: (value) => getPriorityBadge(value), - }, - { - id: 'assignedTo', - key: 'assignedTo', - header: 'Asignado a', - sortable: true, - filterable: true, - type: 'text', - width: 180, - hideOnMobile: true, - cell: (value) => ( -
- - {value} -
- ), - }, - { - id: 'progress', - key: 'progress', - header: 'Progreso', - sortable: true, - filterable: true, - type: 'number', - width: 150, - align: 'center', - hideOnMobile: true, - cell: (value) => ( -
-
-
-
- {value}% -
- ), - }, - { - id: 'estimatedCompletion', - key: 'estimatedCompletion', - header: 'Tiempo Estimado', - sortable: true, - filterable: true, - type: 'date', - width: 140, - align: 'center', - hideOnMobile: true, - cell: (value) => new Date(value).toLocaleTimeString('es-ES', { - hour: '2-digit', - minute: '2-digit' - }), - }, - { - id: 'actions', - key: 'actions', - header: 'Acciones', - sortable: false, - filterable: false, - width: 150, - align: 'right', - sticky: 'right', - cell: (value, row) => ( -
- - -
- ), - }, - ]; + const filteredOrders = mockProductionOrders.filter(order => { + const matchesSearch = order.recipeName.toLowerCase().includes(searchQuery.toLowerCase()) || + order.assignedTo.toLowerCase().includes(searchQuery.toLowerCase()) || + order.id.toLowerCase().includes(searchQuery.toLowerCase()); + + return matchesSearch; + }); return ( -
+
- - Nueva Orden de Producción - - } + actions={[ + { + id: "export", + label: "Exportar", + variant: "outline" as const, + icon: Download, + onClick: () => console.log('Export production orders') + }, + { + id: "new", + label: "Nueva Orden de Producción", + variant: "primary" as const, + icon: Plus, + onClick: () => setShowForm(true) + } + ]} /> {/* Production Stats */} -
- -
-
-

Meta Diaria

-

{mockProductionStats.dailyTarget}

-
- -
-
- - -
-
-

Completado

-

{mockProductionStats.completed}

-
-
- - - -
-
-
- - -
-
-

En Proceso

-

{mockProductionStats.inProgress}

-
- -
-
- - -
-
-

Pendiente

-

{mockProductionStats.pending}

-
- -
-
- - -
-
-

Eficiencia

-

{mockProductionStats.efficiency}%

-
-
- - - -
-
-
- - -
-
-

Calidad

-

{mockProductionStats.quality}%

-
-
- - - -
-
-
-
+ {/* Tabs Navigation */}
@@ -434,56 +222,162 @@ const ProductionPage: React.FC = () => {
- {/* Tab Content */} + {/* Production Orders Tab */} {activeTab === 'schedule' && ( - -
-
-

Órdenes de Producción

-
- {selectedBatches.length > 0 && ( - - )} - + <> + {/* Simplified Controls */} + +
+
+ setSearchQuery(e.target.value)} + className="w-full" + />
+
- - row.id - }} - enableExport={true} - onExport={handleExport} - density="normal" - horizontalScroll={true} - emptyStateMessage="No se encontraron órdenes de producción" - emptyStateAction={{ - label: "Nueva Orden", - onClick: () => console.log('Nueva orden de producción') - }} - onRowClick={(row) => console.log('Ver detalles:', row)} - /> +
+ + {/* Production Orders Grid */} +
+ {filteredOrders.map((order) => ( + +
+ {/* Header */} +
+
+
+ +
+
+
+ {order.recipeName} +
+
+ ID: {order.id} +
+
+
+ {getStatusBadge(order.status)} +
+ + {/* Priority and Quantity */} +
+
+ {getPriorityBadge(order.priority)} +
+
+
+ {order.quantity} +
+
+ unidades +
+
+
+ + {/* Assigned Worker */} +
+ + + {order.assignedTo} + +
+ + {/* Time Information */} +
+
+ Inicio: + + {new Date(order.startTime).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })} + +
+
+ Est. finalización: + + {new Date(order.estimatedCompletion).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })} + +
+
+ + {/* Progress Bar */} +
+
+ Progreso + + {order.progress}% + +
+
+
50 + ? 'bg-[var(--color-info)]' + : order.progress > 0 + ? 'bg-[var(--color-warning)]' + : 'bg-[var(--bg-quaternary)]' + }`} + style={{ width: `${order.progress}%` }} + /> +
+
+ + {/* Actions */} +
+ + +
+
+ + ))}
-
+ + {/* Empty State */} + {filteredOrders.length === 0 && ( +
+ +

+ No se encontraron órdenes de producción +

+

+ Intenta ajustar la búsqueda o crear una nueva orden de producción +

+ +
+ )} + )} {activeTab === 'batches' && ( @@ -493,6 +387,54 @@ const ProductionPage: React.FC = () => { {activeTab === 'quality' && ( )} + + {/* Production Order Form Modal - Placeholder */} + {showForm && ( +
+ +
+

+ {selectedOrder ? 'Ver Orden de Producción' : 'Nueva Orden de Producción'} +

+ +
+ {selectedOrder && ( +
+

{selectedOrder.recipeName}

+
+
+ Cantidad: {selectedOrder.quantity} unidades +
+
+ Asignado a: {selectedOrder.assignedTo} +
+
+ Estado: {selectedOrder.status} +
+
+ Progreso: {selectedOrder.progress}% +
+
+ Inicio: {new Date(selectedOrder.startTime).toLocaleString('es-ES')} +
+
+ Finalización: {new Date(selectedOrder.estimatedCompletion).toLocaleString('es-ES')} +
+
+
+ )} +
+
+ )}
); }; diff --git a/frontend/src/pages/app/operations/recipes/RecipesPage.tsx b/frontend/src/pages/app/operations/recipes/RecipesPage.tsx index bbff2f0e..55ad287e 100644 --- a/frontend/src/pages/app/operations/recipes/RecipesPage.tsx +++ b/frontend/src/pages/app/operations/recipes/RecipesPage.tsx @@ -1,13 +1,13 @@ import React, { useState } from 'react'; -import { Plus, Search, Filter, Star, Clock, Users, DollarSign } from 'lucide-react'; -import { Button, Input, Card, Badge } from '../../../../components/ui'; +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 { formatters } from '../../../../components/ui/Stats/StatsPresets'; import { PageHeader } from '../../../../components/layout'; const RecipesPage: React.FC = () => { const [searchTerm, setSearchTerm] = useState(''); - const [selectedCategory, setSelectedCategory] = useState('all'); - const [selectedDifficulty, setSelectedDifficulty] = useState('all'); - const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); + const [showForm, setShowForm] = useState(false); + const [selectedRecipe, setSelectedRecipe] = useState(null); const mockRecipes = [ { @@ -76,46 +76,62 @@ const RecipesPage: React.FC = () => { { name: 'Azúcar', quantity: 100, unit: 'g' }, ], }, - ]; - - const categories = [ - { value: 'all', label: 'Todas las categorías' }, - { value: 'bread', label: 'Panes' }, - { value: 'pastry', label: 'Bollería' }, - { value: 'cake', label: 'Tartas' }, - { value: 'cookie', label: 'Galletas' }, - { value: 'other', label: 'Otros' }, - ]; - - const difficulties = [ - { value: 'all', label: 'Todas las dificultades' }, - { value: 'easy', label: 'Fácil' }, - { value: 'medium', label: 'Medio' }, - { value: 'hard', label: 'Difícil' }, + { + id: '4', + name: 'Magdalenas de Limón', + category: 'pastry', + difficulty: 'easy', + prepTime: 20, + bakingTime: 25, + yield: 12, + rating: 4.4, + cost: 3.80, + price: 9.00, + profit: 5.20, + image: '/api/placeholder/300/200', + tags: ['cítrico', 'esponjoso', 'individual'], + description: 'Magdalenas suaves y esponjosas con ralladura de limón.', + ingredients: [ + { name: 'Harina', quantity: 200, unit: 'g' }, + { name: 'Huevos', quantity: 3, unit: 'uds' }, + { name: 'Azúcar', quantity: 150, unit: 'g' }, + { name: 'Limón', quantity: 2, unit: 'uds' }, + ], + }, ]; const getCategoryBadge = (category: string) => { const categoryConfig = { - bread: { color: 'brown', text: 'Pan' }, - pastry: { color: 'yellow', text: 'Bollería' }, - cake: { color: 'pink', text: 'Tarta' }, - cookie: { color: 'orange', text: 'Galleta' }, - other: { color: 'gray', text: 'Otro' }, + 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' }, }; const config = categoryConfig[category as keyof typeof categoryConfig] || categoryConfig.other; - return {config.text}; + return ( + + ); }; const getDifficultyBadge = (difficulty: string) => { const difficultyConfig = { - easy: { color: 'green', text: 'Fácil' }, - medium: { color: 'yellow', text: 'Medio' }, - hard: { color: 'red', text: 'Difícil' }, + easy: { color: 'success', text: 'Fácil' }, + medium: { color: 'warning', text: 'Medio' }, + hard: { color: 'error', text: 'Difícil' }, }; const config = difficultyConfig[difficulty as keyof typeof difficultyConfig]; - return {config.text}; + return ( + + ); }; const formatTime = (minutes: number) => { @@ -129,281 +145,300 @@ const RecipesPage: React.FC = () => { recipe.description.toLowerCase().includes(searchTerm.toLowerCase()) || recipe.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase())); - const matchesCategory = selectedCategory === 'all' || recipe.category === selectedCategory; - const matchesDifficulty = selectedDifficulty === 'all' || recipe.difficulty === selectedDifficulty; - - return matchesSearch && matchesCategory && matchesDifficulty; + return matchesSearch; }); + const mockRecipeStats = { + totalRecipes: mockRecipes.length, + popularRecipes: mockRecipes.filter(r => r.rating > 4.7).length, + easyRecipes: mockRecipes.filter(r => r.difficulty === 'easy').length, + averageCost: mockRecipes.reduce((sum, r) => sum + r.cost, 0) / mockRecipes.length, + averageProfit: mockRecipes.reduce((sum, r) => sum + r.profit, 0) / mockRecipes.length, + categories: [...new Set(mockRecipes.map(r => r.category))].length, + }; + + const recipeStats = [ + { + title: 'Total Recetas', + value: mockRecipeStats.totalRecipes, + variant: 'default' as const, + icon: ChefHat, + }, + { + title: 'Populares', + value: mockRecipeStats.popularRecipes, + variant: 'warning' as const, + icon: Star, + }, + { + title: 'Fáciles', + value: mockRecipeStats.easyRecipes, + variant: 'success' as const, + icon: Timer, + }, + { + title: 'Costo Promedio', + value: formatters.currency(mockRecipeStats.averageCost), + variant: 'info' as const, + icon: DollarSign, + }, + { + title: 'Margen Promedio', + value: formatters.currency(mockRecipeStats.averageProfit), + variant: 'success' as const, + icon: DollarSign, + }, + { + title: 'Categorías', + value: mockRecipeStats.categories, + variant: 'info' as const, + icon: Package, + }, + ]; + return ( -
+
- - Nueva Receta - - } + actions={[ + { + id: "export", + label: "Exportar", + variant: "outline" as const, + icon: Download, + onClick: () => console.log('Export recipes') + }, + { + id: "new", + label: "Nueva Receta", + variant: "primary" as const, + icon: Plus, + onClick: () => setShowForm(true) + } + ]} /> - {/* Stats Cards */} -
- -
-
-

Total Recetas

-

{mockRecipes.length}

-
-
- - - -
-
-
+ {/* Stats Grid */} + - -
-
-

Más Populares

-

- {mockRecipes.filter(r => r.rating > 4.7).length} -

-
- -
-
- - -
-
-

Costo Promedio

-

- €{(mockRecipes.reduce((sum, r) => sum + r.cost, 0) / mockRecipes.length).toFixed(2)} -

-
- -
-
- - -
-
-

Margen Promedio

-

- €{(mockRecipes.reduce((sum, r) => sum + r.profit, 0) / mockRecipes.length).toFixed(2)} -

-
-
- - - -
-
-
-
- - {/* Filters and Search */} - -
+ {/* Simplified Controls */} + +
-
- - setSearchTerm(e.target.value)} - className="pl-10" - /> -
-
- -
- - - - - + setSearchTerm(e.target.value)} + className="w-full" + />
+
- {/* Recipes Grid/List */} - {viewMode === 'grid' ? ( -
- {filteredRecipes.map((recipe) => ( - -
- {recipe.name} -
-
-
-

+ {/* Recipes Grid */} +
+ {filteredRecipes.map((recipe) => ( + +
+ {/* Header with Image */} +
+
+ {recipe.name} +
+
+
{recipe.name} -

-
- - {recipe.rating}
-
- -

- {recipe.description} -

- -
- {getCategoryBadge(recipe.category)} - {getDifficultyBadge(recipe.difficulty)} -
- -
-
- - {formatTime(recipe.prepTime + recipe.bakingTime)} +
+ + {recipe.rating}
-
- - {recipe.yield} porciones -
-
- -
-
- Costo: - €{recipe.cost.toFixed(2)} -
-
- Precio: - €{recipe.price.toFixed(2)} -
-
- -
- - +

+ {recipe.description} +

- - ))} + + {/* Badges */} +
+ {getCategoryBadge(recipe.category)} + {getDifficultyBadge(recipe.difficulty)} +
+ + {/* Time and Yield */} +
+
+ + + {formatTime(recipe.prepTime + recipe.bakingTime)} + +
+
+ + + {recipe.yield} porciones + +
+
+ + {/* Financial Info */} +
+
+ Costo: + + {formatters.currency(recipe.cost)} + +
+
+ Precio: + + {formatters.currency(recipe.price)} + +
+
+ Margen: + + {formatters.currency(recipe.profit)} + +
+
+ + {/* Profit Margin Bar */} +
+
+ Margen de beneficio + + {Math.round((recipe.profit / recipe.price) * 100)}% + +
+
+
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)}%` }} + /> +
+
+ + {/* Ingredients Count */} +
+ {recipe.ingredients.length} ingredientes principales +
+ + {/* Actions */} +
+ + +
+
+ + ))} +
+ + {/* Empty State */} + {filteredRecipes.length === 0 && ( +
+ +

+ No se encontraron recetas +

+

+ Intenta ajustar la búsqueda o crear una nueva receta +

+ +
+ )} + + {/* Recipe Form Modal - Placeholder */} + {showForm && ( +
+ +
+

+ {selectedRecipe ? 'Ver Receta' : 'Nueva Receta'} +

+ +
+ {selectedRecipe && ( +
+ {selectedRecipe.name} +

{selectedRecipe.name}

+

{selectedRecipe.description}

+
+
+ Tiempo total: {formatTime(selectedRecipe.prepTime + selectedRecipe.bakingTime)} +
+
+ Rendimiento: {selectedRecipe.yield} porciones +
+
+
+

Ingredientes:

+
    + {selectedRecipe.ingredients.map((ing, i) => ( +
  • + {ing.name} + {ing.quantity} {ing.unit} +
  • + ))} +
+
+
+ )} +
- ) : ( - -
-
- - - - - - - - - - - - - - - {filteredRecipes.map((recipe) => ( - - - - - - - - - - - - ))} - -
- Receta - - Categoría - - Dificultad - - Tiempo Total - - Rendimiento - - Costo - - Precio - - Margen - - Acciones -
-
- {recipe.name} -
-
{recipe.name}
-
- - {recipe.rating} -
-
-
-
- {getCategoryBadge(recipe.category)} - - {getDifficultyBadge(recipe.difficulty)} - - {formatTime(recipe.prepTime + recipe.bakingTime)} - - {recipe.yield} porciones - - €{recipe.cost.toFixed(2)} - - €{recipe.price.toFixed(2)} - - €{recipe.profit.toFixed(2)} - -
- - -
-
-
- )}
);