From 8002d89d2ba5bb468b4edb4b234412b9b79fea84 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Fri, 19 Sep 2025 16:17:04 +0200 Subject: [PATCH] Create the forntend panel the control --- .../domain/dashboard/DashboardCard.tsx | 298 ---------- .../domain/dashboard/DashboardGrid.tsx | 122 ---- .../components/domain/dashboard/KPIWidget.tsx | 502 ----------------- .../dashboard/ProcurementPlansToday.tsx | 297 ++++++++++ .../domain/dashboard/ProductionPlansToday.tsx | 371 +++++++++++++ .../domain/dashboard/QuickActions.tsx | 382 ------------- .../domain/dashboard/RealTimeAlerts.tsx | 307 ++++++++++ .../domain/dashboard/RecentActivity.tsx | 524 ------------------ frontend/src/pages/app/DashboardPage.tsx | 385 +++---------- 9 files changed, 1056 insertions(+), 2132 deletions(-) delete mode 100644 frontend/src/components/domain/dashboard/DashboardCard.tsx delete mode 100644 frontend/src/components/domain/dashboard/DashboardGrid.tsx delete mode 100644 frontend/src/components/domain/dashboard/KPIWidget.tsx create mode 100644 frontend/src/components/domain/dashboard/ProcurementPlansToday.tsx create mode 100644 frontend/src/components/domain/dashboard/ProductionPlansToday.tsx delete mode 100644 frontend/src/components/domain/dashboard/QuickActions.tsx create mode 100644 frontend/src/components/domain/dashboard/RealTimeAlerts.tsx delete mode 100644 frontend/src/components/domain/dashboard/RecentActivity.tsx diff --git a/frontend/src/components/domain/dashboard/DashboardCard.tsx b/frontend/src/components/domain/dashboard/DashboardCard.tsx deleted file mode 100644 index 594c18d8..00000000 --- a/frontend/src/components/domain/dashboard/DashboardCard.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import React, { forwardRef, HTMLAttributes, ReactNode, useState } from 'react'; -import { clsx } from 'clsx'; -import { Card, CardHeader, CardBody, CardFooter, Button, Badge } from '../../ui'; - -export interface DashboardCardProps extends Omit, 'title'> { - // Card variants for different dashboard contexts - variant?: 'metric' | 'chart' | 'list' | 'activity' | 'status' | 'action'; - - // Header props - title?: ReactNode; - subtitle?: ReactNode; - icon?: ReactNode; - headerActions?: ReactNode; - - // Loading and state management - isLoading?: boolean; - hasError?: boolean; - errorMessage?: string; - isEmpty?: boolean; - emptyMessage?: string; - - // Footer props - footerActions?: ReactNode; - footerText?: ReactNode; - - // Badge/notification support - badge?: string | number; - badgeVariant?: 'default' | 'primary' | 'success' | 'warning' | 'error'; - - // Interactive behavior - interactive?: boolean; - onClick?: () => void; - onRefresh?: () => void; - - // Layout customization - padding?: 'none' | 'sm' | 'md' | 'lg'; - headerPadding?: 'none' | 'sm' | 'md' | 'lg'; - bodyPadding?: 'none' | 'sm' | 'md' | 'lg'; - - // Accessibility - 'aria-label'?: string; - 'aria-describedby'?: string; -} - -const DashboardCard = forwardRef(({ - variant = 'metric', - title, - subtitle, - icon, - headerActions, - isLoading = false, - hasError = false, - errorMessage = 'Ha ocurrido un error', - isEmpty = false, - emptyMessage = 'No hay datos disponibles', - footerActions, - footerText, - badge, - badgeVariant = 'primary', - interactive = false, - onClick, - onRefresh, - padding = 'md', - headerPadding, - bodyPadding, - className, - children, - 'aria-label': ariaLabel, - 'aria-describedby': ariaDescribedby, - ...props -}, ref) => { - const [isRefreshing, setIsRefreshing] = useState(false); - - const handleRefresh = async () => { - if (onRefresh && !isRefreshing) { - setIsRefreshing(true); - try { - await onRefresh(); - } finally { - setIsRefreshing(false); - } - } - }; - - const variantStyles = { - metric: 'bg-[var(--bg-primary)] border-[var(--border-primary)] hover:border-[var(--border-secondary)]', - chart: 'bg-[var(--bg-primary)] border-[var(--border-primary)] hover:border-[var(--border-secondary)]', - list: 'bg-[var(--bg-primary)] border-[var(--border-primary)] hover:border-[var(--border-secondary)]', - activity: 'bg-[var(--bg-primary)] border-[var(--border-primary)] hover:border-[var(--border-secondary)]', - status: 'bg-[var(--bg-primary)] border-[var(--border-primary)] hover:border-[var(--border-secondary)]', - action: 'bg-[var(--bg-primary)] border-[var(--border-primary)] hover:border-[var(--border-secondary)] hover:shadow-lg' - }; - - const cardClasses = clsx( - variantStyles[variant], - 'transition-all duration-300', - { - 'cursor-pointer transform hover:-translate-y-1': interactive || onClick, - 'opacity-50': isLoading, - 'border-[var(--color-error)]/30 bg-[var(--color-error)]/5': hasError, - }, - className - ); - - const hasHeader = title || subtitle || icon || headerActions || badge || onRefresh; - - const renderSkeletonContent = () => { - switch (variant) { - case 'metric': - return ( -
-
-
-
-
- ); - case 'chart': - return ( -
-
-
-
- ); - case 'list': - return ( -
- {Array.from({ length: 3 }).map((_, i) => ( -
-
-
-
-
-
-
- ))} -
- ); - default: - return
; - } - }; - - const renderErrorContent = () => ( -
-
- - - -
-

{errorMessage}

- {onRefresh && ( - - )} -
- ); - - const renderEmptyContent = () => ( -
-
- - - -
-

{emptyMessage}

-
- ); - - return ( - - {hasHeader && ( - -
- {icon && ( -
- {icon} -
- )} -
- {title && ( -
-

- {title} -

- {badge && ( - - {badge} - - )} -
- )} - {subtitle && ( -

- {subtitle} -

- )} -
-
- -
- {onRefresh && ( - - )} - {headerActions} -
-
- )} - - - {isLoading - ? renderSkeletonContent() - : hasError - ? renderErrorContent() - : isEmpty - ? renderEmptyContent() - : children} - - - {(footerActions || footerText) && ( - -
- {footerText} -
-
- {footerActions} -
-
- )} -
- ); -}); - -DashboardCard.displayName = 'DashboardCard'; - -export default DashboardCard; \ No newline at end of file diff --git a/frontend/src/components/domain/dashboard/DashboardGrid.tsx b/frontend/src/components/domain/dashboard/DashboardGrid.tsx deleted file mode 100644 index 8d96ee93..00000000 --- a/frontend/src/components/domain/dashboard/DashboardGrid.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React from 'react'; -import { - TrendingUp, - Package, - AlertCircle, - DollarSign -} from 'lucide-react'; -import { Card, Badge } from '../../ui'; - -export const DashboardGrid: React.FC = () => { - // Simple placeholder implementation - can be enhanced later - const mockData = { - sales_today: 1247, - sales_change: 12.5, - products_sold: 45, - products_change: 8.3, - active_alerts: 2, - urgent_alerts: 1 - }; - - return ( -
- {/* KPI Cards */} - } - color="green" - /> - - } - color="blue" - /> - - } - color="orange" - /> - - -
-
- - Sistema -
-
-

En línea

-

Todo funcionando

-
-
-
-
- ); -}; - -// KPI Card Component -interface KPICardProps { - title: string; - value: string | number; - change?: number; - urgent?: number; - icon: React.ReactNode; - color: 'green' | 'blue' | 'orange' | 'red'; -} - -const KPICard: React.FC = ({ - title, - value, - change, - urgent, - icon, - color -}) => { - const colorClasses = { - green: 'bg-green-50 text-[var(--color-success)] border-green-200', - blue: 'bg-[var(--color-info)]/5 text-[var(--color-info)] border-[var(--color-info)]/20', - orange: 'bg-orange-50 text-[var(--color-primary)] border-orange-200', - red: 'bg-red-50 text-[var(--color-error)] border-red-200', - }; - - return ( - - -
-
- {icon} -
- {urgent !== undefined && urgent > 0 && ( - - {urgent} Urgente - - )} -
- -
-

{title}

-

{value}

- - {change !== undefined && ( -
- = 0 ? 'text-green-500' : 'text-red-500 rotate-180'}`} /> - = 0 ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'}> - {Math.abs(change)}% - - vs ayer -
- )} -
-
-
- ); -}; - -export default DashboardGrid; \ No newline at end of file diff --git a/frontend/src/components/domain/dashboard/KPIWidget.tsx b/frontend/src/components/domain/dashboard/KPIWidget.tsx deleted file mode 100644 index cd9b9a50..00000000 --- a/frontend/src/components/domain/dashboard/KPIWidget.tsx +++ /dev/null @@ -1,502 +0,0 @@ -import React, { useMemo } from 'react'; -import { clsx } from 'clsx'; -import { Badge } from '../../ui'; -import DashboardCard from './DashboardCard'; - -export interface KPIValue { - current: number; - previous?: number; - target?: number; - format: 'currency' | 'number' | 'percentage'; - prefix?: string; - suffix?: string; -} - -export interface KPITrend { - direction: 'up' | 'down' | 'stable'; - value: number; - isPositive: boolean; - comparisonPeriod: string; - description?: string; -} - -export interface KPIThreshold { - excellent: number; - good: number; - warning: number; - critical: number; -} - -export interface SparklineDataPoint { - date: string; - value: number; - label?: string; -} - -export interface KPIWidgetProps { - title: string; - subtitle?: string; - value: KPIValue; - trend?: KPITrend; - - // Visual configuration - icon?: React.ReactNode; - color?: 'blue' | 'green' | 'orange' | 'red' | 'purple' | 'indigo' | 'teal'; - variant?: 'default' | 'compact' | 'detailed' | 'chart'; - - // Chart data - sparklineData?: SparklineDataPoint[]; - showSparkline?: boolean; - - // Thresholds and status - thresholds?: KPIThreshold; - status?: 'excellent' | 'good' | 'warning' | 'critical' | 'neutral'; - - // Comparison and context - comparisonLabel?: string; - contextInfo?: string; - - // Interactive features - isLoading?: boolean; - onRefresh?: () => void; - onClick?: () => void; - - // Accessibility and styling - className?: string; - 'aria-label'?: string; -} - -// Predefined bakery KPI configurations -export const BAKERY_KPI_CONFIGS = { - dailyRevenue: { - title: 'Ingresos Hoy', - subtitle: 'Ventas del día actual', - icon: ( - - - - ), - color: 'green' as const, - format: 'currency' as const - }, - orderCount: { - title: 'Pedidos', - subtitle: 'Órdenes procesadas hoy', - icon: ( - - - - ), - color: 'blue' as const, - format: 'number' as const - }, - productivity: { - title: 'Productividad', - subtitle: 'Unidades producidas por hora', - icon: ( - - - - ), - color: 'orange' as const, - format: 'number' as const, - suffix: '/h' - }, - stockLevel: { - title: 'Nivel Stock', - subtitle: 'Porcentaje de stock disponible', - icon: ( - - - - ), - color: 'purple' as const, - format: 'percentage' as const - } -}; - -const formatValue = (value: number, format: KPIValue['format'], prefix?: string, suffix?: string): string => { - let formatted: string; - - switch (format) { - case 'currency': - formatted = new Intl.NumberFormat('es-ES', { - style: 'currency', - currency: 'EUR', - minimumFractionDigits: 0, - maximumFractionDigits: 2 - }).format(value); - break; - - case 'percentage': - formatted = new Intl.NumberFormat('es-ES', { - style: 'percent', - minimumFractionDigits: 0, - maximumFractionDigits: 1 - }).format(value / 100); - break; - - case 'number': - default: - formatted = new Intl.NumberFormat('es-ES').format(value); - break; - } - - return `${prefix || ''}${formatted}${suffix || ''}`; -}; - -const calculateTrend = (current: number, previous: number): KPITrend => { - // Handle undefined or null values - if (current == null || previous == null || - typeof current !== 'number' || typeof previous !== 'number' || - isNaN(current) || isNaN(previous)) { - return { - direction: 'stable', - value: 0, - isPositive: true, - comparisonPeriod: 'vs período anterior' - }; - } - - const change = current - previous; - const percentChange = previous !== 0 ? (change / previous) * 100 : 0; - - return { - direction: change > 0 ? 'up' : change < 0 ? 'down' : 'stable', - value: Math.abs(percentChange), - isPositive: change >= 0, - comparisonPeriod: 'vs período anterior' - }; -}; - -const getStatusColor = (status: KPIWidgetProps['status']) => { - switch (status) { - case 'excellent': - return { - bg: 'bg-[var(--color-success)]/10', - text: 'text-[var(--color-success)]', - border: 'border-[var(--color-success)]/20', - icon: 'text-[var(--color-success)]' - }; - case 'good': - return { - bg: 'bg-[var(--color-info)]/10', - text: 'text-[var(--color-info)]', - border: 'border-[var(--color-info)]/20', - icon: 'text-[var(--color-info)]' - }; - case 'warning': - return { - bg: 'bg-[var(--color-warning)]/10', - text: 'text-[var(--color-warning)]', - border: 'border-[var(--color-warning)]/20', - icon: 'text-[var(--color-warning)]' - }; - case 'critical': - return { - bg: 'bg-[var(--color-error)]/10', - text: 'text-[var(--color-error)]', - border: 'border-[var(--color-error)]/20', - icon: 'text-[var(--color-error)]' - }; - default: - return { - bg: 'bg-[var(--bg-tertiary)]', - text: 'text-[var(--text-secondary)]', - border: 'border-[var(--border-primary)]', - icon: 'text-[var(--text-tertiary)]' - }; - } -}; - -const SimpleSparkline: React.FC<{ data: SparklineDataPoint[]; color: string }> = ({ data, color }) => { - const max = Math.max(...data.map(d => d.value)); - const min = Math.min(...data.map(d => d.value)); - const range = max - min; - - const points = data.map((point, index) => { - const x = (index / (data.length - 1)) * 100; - const y = range === 0 ? 50 : ((max - point.value) / range) * 100; - return `${x},${y}`; - }).join(' '); - - const colorClasses = { - blue: 'stroke-[var(--color-info)]', - green: 'stroke-[var(--color-success)]', - orange: 'stroke-[var(--color-primary)]', - red: 'stroke-[var(--color-error)]', - purple: 'stroke-[var(--color-info)]', - indigo: 'stroke-[var(--color-info)]', - teal: 'stroke-[var(--color-success)]' - }; - - return ( -
- - - -
- ); -}; - -const KPIWidget: React.FC = ({ - title, - subtitle, - value, - trend, - icon, - color = 'blue', - variant = 'default', - sparklineData, - showSparkline = false, - thresholds, - status, - comparisonLabel, - contextInfo, - isLoading = false, - onRefresh, - onClick, - className, - 'aria-label': ariaLabel -}) => { - // Calculate trend if not provided - const calculatedTrend = useMemo(() => { - if (trend) return trend; - if (value.previous !== undefined) { - return calculateTrend(value.current, value.previous); - } - return null; - }, [trend, value.current, value.previous]); - - // Determine status based on thresholds - const calculatedStatus = useMemo(() => { - if (status) return status; - if (!thresholds) return 'neutral'; - - const { current } = value; - if (current >= thresholds.excellent) return 'excellent'; - if (current >= thresholds.good) return 'good'; - if (current >= thresholds.warning) return 'warning'; - return 'critical'; - }, [status, thresholds, value.current]); - - const statusStyles = getStatusColor(calculatedStatus); - const formattedValue = formatValue(value.current, value.format, value.prefix, value.suffix); - const formattedTarget = value.target ? formatValue(value.target, value.format, value.prefix, value.suffix) : null; - - const colorClasses = { - blue: 'text-[var(--color-info)]', - green: 'text-[var(--color-success)]', - orange: 'text-[var(--color-primary)]', - red: 'text-[var(--color-error)]', - purple: 'text-[var(--color-info)]', - indigo: 'text-[var(--color-info)]', - teal: 'text-[var(--color-success)]' - }; - - const renderTrendIcon = (direction: KPITrend['direction']) => { - const iconClass = calculatedTrend?.isPositive ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'; - - switch (direction) { - case 'up': - return ( - - - - ); - case 'down': - return ( - - - - ); - default: - return ( - - - - ); - } - }; - - const renderCompactVariant = () => ( -
-
- {icon && ( -
-
- {icon} -
-
- )} -
-

{title}

-

{formattedValue}

-
-
- {calculatedTrend && ( -
- {renderTrendIcon(calculatedTrend.direction)} - - {(calculatedTrend.value || 0).toFixed(1)}% - -
- )} -
- ); - - const renderDetailedVariant = () => ( -
- {/* Header */} -
-
- {icon && ( -
-
- {icon} -
-
- )} -
-

{title}

- {subtitle && ( -

{subtitle}

- )} -
-
- {calculatedStatus !== 'neutral' && ( - - {calculatedStatus === 'excellent' ? 'Excelente' : - calculatedStatus === 'good' ? 'Bueno' : - calculatedStatus === 'warning' ? 'Atención' : 'Crítico'} - - )} -
- - {/* Value and trend */} -
-
- {formattedValue} - {calculatedTrend && ( -
- {renderTrendIcon(calculatedTrend.direction)} - - {(calculatedTrend.value || 0).toFixed(1)}% - - - {calculatedTrend.comparisonPeriod} - -
- )} -
- - {formattedTarget && ( -

- Objetivo: {formattedTarget} -

- )} - - {contextInfo && ( -

{contextInfo}

- )} -
- - {/* Sparkline chart */} - {showSparkline && sparklineData && sparklineData.length > 0 && ( -
-

Tendencia últimos 7 días

- -
- )} -
- ); - - const renderDefaultVariant = () => ( -
-
-
- {icon && ( -
- {icon} -
- )} -
-

{title}

- {subtitle && ( -

{subtitle}

- )} -
-
-
- -
-
- {formattedValue} - {calculatedTrend && ( -
- {renderTrendIcon(calculatedTrend.direction)} - - {(calculatedTrend.value || 0).toFixed(1)}% - -
- )} -
- - {comparisonLabel && ( -

{comparisonLabel}

- )} -
-
- ); - - const renderContent = () => { - switch (variant) { - case 'compact': - return renderCompactVariant(); - case 'detailed': - return renderDetailedVariant(); - case 'chart': - return renderDetailedVariant(); - default: - return renderDefaultVariant(); - } - }; - - return ( - - {renderContent()} - - ); -}; - -KPIWidget.displayName = 'KPIWidget'; - -export default KPIWidget; \ No newline at end of file diff --git a/frontend/src/components/domain/dashboard/ProcurementPlansToday.tsx b/frontend/src/components/domain/dashboard/ProcurementPlansToday.tsx new file mode 100644 index 00000000..8c4fa984 --- /dev/null +++ b/frontend/src/components/domain/dashboard/ProcurementPlansToday.tsx @@ -0,0 +1,297 @@ +import React from 'react'; +import { Card, CardHeader, CardBody } from '../../ui/Card'; +import { StatusCard } from '../../ui/StatusCard/StatusCard'; +import { Badge } from '../../ui/Badge'; +import { Button } from '../../ui/Button'; +import { + ShoppingCart, + Clock, + Package, + AlertTriangle, + CheckCircle, + ChevronRight, + Calendar, + User, + DollarSign, + Truck +} from 'lucide-react'; + +export interface ProcurementItem { + id: string; + ingredient: string; + quantity: number; + unit: string; + supplier: string; + priority: 'urgent' | 'high' | 'medium' | 'low'; + estimatedCost: number; + deliveryTime: string; + currentStock: number; + minStock: number; + plannedFor: string; + status: 'pending' | 'ordered' | 'in_transit' | 'delivered'; + notes?: string; +} + +export interface ProcurementPlansProps { + className?: string; + items?: ProcurementItem[]; + onOrderItem?: (itemId: string) => void; + onViewDetails?: (itemId: string) => void; + onViewAllPlans?: () => void; +} + +const ProcurementPlansToday: React.FC = ({ + className, + items = [], + onOrderItem, + onViewDetails, + onViewAllPlans +}) => { + const defaultItems: ProcurementItem[] = [ + { + id: '1', + ingredient: 'Harina de Trigo', + quantity: 50, + unit: 'kg', + supplier: 'Molinos San José', + priority: 'urgent', + estimatedCost: 87.50, + deliveryTime: '10:00', + currentStock: 3, + minStock: 15, + plannedFor: '09:00', + status: 'pending', + notes: 'Stock crítico - necesario para producción matutina' + }, + { + id: '2', + ingredient: 'Levadura Fresca', + quantity: 5, + unit: 'kg', + supplier: 'Distribuidora Alba', + priority: 'urgent', + estimatedCost: 32.50, + deliveryTime: '11:30', + currentStock: 1, + minStock: 3, + plannedFor: '09:30', + status: 'pending' + }, + { + id: '3', + ingredient: 'Mantequilla', + quantity: 15, + unit: 'kg', + supplier: 'Lácteos Premium', + priority: 'high', + estimatedCost: 105.00, + deliveryTime: '14:00', + currentStock: 8, + minStock: 12, + plannedFor: '10:00', + status: 'ordered' + }, + { + id: '4', + ingredient: 'Azúcar Blanco', + quantity: 25, + unit: 'kg', + supplier: 'Azucarera Local', + priority: 'medium', + estimatedCost: 62.50, + deliveryTime: '16:00', + currentStock: 18, + minStock: 20, + plannedFor: '11:00', + status: 'pending' + } + ]; + + const displayItems = items.length > 0 ? items : defaultItems; + + const getItemStatusConfig = (item: ProcurementItem) => { + const baseConfig = { + isCritical: item.priority === 'urgent', + isHighlight: item.priority === 'high' || item.status === 'pending', + }; + + switch (item.status) { + case 'pending': + return { + ...baseConfig, + color: 'var(--color-warning)', + text: 'Pendiente', + icon: Clock + }; + case 'ordered': + return { + ...baseConfig, + color: 'var(--color-info)', + text: 'Pedido', + icon: CheckCircle + }; + case 'in_transit': + return { + ...baseConfig, + color: 'var(--color-primary)', + text: 'En Camino', + icon: Truck + }; + case 'delivered': + return { + ...baseConfig, + color: 'var(--color-success)', + text: 'Entregado', + icon: Package + }; + default: + return { + ...baseConfig, + color: 'var(--color-warning)', + text: 'Pendiente', + icon: Clock + }; + } + }; + + const urgentItems = displayItems.filter(item => item.priority === 'urgent').length; + const pendingItems = displayItems.filter(item => item.status === 'pending').length; + const totalValue = displayItems.reduce((sum, item) => sum + item.estimatedCost, 0); + + return ( + + +
+
+
+ +
+
+

+ Planes de Compra - Hoy +

+

+ Gestiona los pedidos programados para hoy +

+
+
+ +
+ {urgentItems > 0 && ( + + {urgentItems} urgentes + + )} + + €{totalValue.toFixed(2)} + +
+
+
+ + + {displayItems.length === 0 ? ( +
+
+ +
+

+ No hay compras programadas +

+

+ Todos los suministros están al día +

+
+ ) : ( +
+ {displayItems.map((item) => { + const statusConfig = getItemStatusConfig(item); + const stockPercentage = Math.round((item.currentStock / item.minStock) * 100); + + return ( + onOrderItem?.(item.id), + priority: 'primary' as const + }] : []), + { + label: 'Ver Detalles', + icon: ChevronRight, + variant: 'outline' as const, + onClick: () => onViewDetails?.(item.id), + priority: 'secondary' as const + } + ]} + compact={true} + className="border-l-4" + /> + ); + })} +
+ )} + + {displayItems.length > 0 && ( +
+
+
+ + {pendingItems} pendientes de {displayItems.length} total + +
+ + +
+
+ )} +
+
+ ); +}; + +export default ProcurementPlansToday; \ No newline at end of file diff --git a/frontend/src/components/domain/dashboard/ProductionPlansToday.tsx b/frontend/src/components/domain/dashboard/ProductionPlansToday.tsx new file mode 100644 index 00000000..47b98416 --- /dev/null +++ b/frontend/src/components/domain/dashboard/ProductionPlansToday.tsx @@ -0,0 +1,371 @@ +import React from 'react'; +import { Card, CardHeader, CardBody } from '../../ui/Card'; +import { StatusCard } from '../../ui/StatusCard/StatusCard'; +import { Badge } from '../../ui/Badge'; +import { Button } from '../../ui/Button'; +import { + Factory, + Clock, + Users, + Thermometer, + Play, + Pause, + CheckCircle, + AlertTriangle, + ChevronRight, + Timer, + Package, + Flame +} from 'lucide-react'; + +export interface ProductionOrder { + id: string; + product: string; + quantity: number; + unit: string; + priority: 'urgent' | 'high' | 'medium' | 'low'; + status: 'pending' | 'in_progress' | 'completed' | 'paused' | 'delayed'; + startTime: string; + estimatedDuration: number; // in minutes + assignedBaker: string; + ovenNumber?: number; + temperature?: number; + progress: number; // 0-100 + notes?: string; + recipe: string; + ingredients: Array<{ + name: string; + quantity: number; + unit: string; + available: boolean; + }>; +} + +export interface ProductionPlansProps { + className?: string; + orders?: ProductionOrder[]; + onStartOrder?: (orderId: string) => void; + onPauseOrder?: (orderId: string) => void; + onViewDetails?: (orderId: string) => void; + onViewAllPlans?: () => void; +} + +const ProductionPlansToday: React.FC = ({ + className, + orders = [], + onStartOrder, + onPauseOrder, + onViewDetails, + onViewAllPlans +}) => { + const defaultOrders: ProductionOrder[] = [ + { + id: '1', + product: 'Pan de Molde Integral', + quantity: 20, + unit: 'unidades', + priority: 'urgent', + status: 'in_progress', + startTime: '06:00', + estimatedDuration: 180, + assignedBaker: 'María González', + ovenNumber: 1, + temperature: 220, + progress: 65, + recipe: 'Receta Estándar Integral', + ingredients: [ + { name: 'Harina integral', quantity: 5, unit: 'kg', available: true }, + { name: 'Levadura', quantity: 0.5, unit: 'kg', available: true }, + { name: 'Sal', quantity: 0.2, unit: 'kg', available: true }, + { name: 'Agua', quantity: 3, unit: 'L', available: true } + ] + }, + { + id: '2', + product: 'Croissants de Mantequilla', + quantity: 50, + unit: 'unidades', + priority: 'high', + status: 'pending', + startTime: '07:30', + estimatedDuration: 240, + assignedBaker: 'Carlos Rodríguez', + ovenNumber: 2, + temperature: 200, + progress: 0, + recipe: 'Croissant Francés', + notes: 'Masa preparada ayer, lista para horneado', + ingredients: [ + { name: 'Masa de croissant', quantity: 3, unit: 'kg', available: true }, + { name: 'Mantequilla', quantity: 1, unit: 'kg', available: false }, + { name: 'Huevo', quantity: 6, unit: 'unidades', available: true } + ] + }, + { + id: '3', + product: 'Baguettes Tradicionales', + quantity: 30, + unit: 'unidades', + priority: 'medium', + status: 'completed', + startTime: '05:00', + estimatedDuration: 240, + assignedBaker: 'Ana Martín', + ovenNumber: 3, + temperature: 240, + progress: 100, + recipe: 'Baguette Francesa', + ingredients: [ + { name: 'Harina blanca', quantity: 4, unit: 'kg', available: true }, + { name: 'Levadura', quantity: 0.3, unit: 'kg', available: true }, + { name: 'Sal', quantity: 0.15, unit: 'kg', available: true }, + { name: 'Agua', quantity: 2.5, unit: 'L', available: true } + ] + }, + { + id: '4', + product: 'Magdalenas de Vainilla', + quantity: 100, + unit: 'unidades', + priority: 'medium', + status: 'delayed', + startTime: '09:00', + estimatedDuration: 90, + assignedBaker: 'Luis Fernández', + ovenNumber: 4, + temperature: 180, + progress: 0, + recipe: 'Magdalenas Clásicas', + notes: 'Retraso por falta de moldes', + ingredients: [ + { name: 'Harina', quantity: 2, unit: 'kg', available: true }, + { name: 'Azúcar', quantity: 1.5, unit: 'kg', available: true }, + { name: 'Huevos', quantity: 24, unit: 'unidades', available: true }, + { name: 'Mantequilla', quantity: 1, unit: 'kg', available: false }, + { name: 'Vainilla', quantity: 50, unit: 'ml', available: true } + ] + } + ]; + + const displayOrders = orders.length > 0 ? orders : defaultOrders; + + const getOrderStatusConfig = (order: ProductionOrder) => { + const baseConfig = { + isCritical: order.status === 'delayed' || order.priority === 'urgent', + isHighlight: order.status === 'in_progress' || order.priority === 'high', + }; + + switch (order.status) { + case 'pending': + return { + ...baseConfig, + color: 'var(--color-warning)', + text: 'Pendiente', + icon: Clock + }; + case 'in_progress': + return { + ...baseConfig, + color: 'var(--color-info)', + text: 'En Proceso', + icon: Play + }; + case 'completed': + return { + ...baseConfig, + color: 'var(--color-success)', + text: 'Completado', + icon: CheckCircle + }; + case 'paused': + return { + ...baseConfig, + color: 'var(--color-warning)', + text: 'Pausado', + icon: Pause + }; + case 'delayed': + return { + ...baseConfig, + color: 'var(--color-error)', + text: 'Retrasado', + icon: AlertTriangle + }; + default: + return { + ...baseConfig, + color: 'var(--color-warning)', + text: 'Pendiente', + icon: Clock + }; + } + }; + + const formatDuration = (minutes: number) => { + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + if (hours > 0) { + return `${hours}h ${mins}m`; + } + return `${mins}m`; + }; + + const inProgressOrders = displayOrders.filter(order => order.status === 'in_progress').length; + const completedOrders = displayOrders.filter(order => order.status === 'completed').length; + const delayedOrders = displayOrders.filter(order => order.status === 'delayed').length; + + return ( + + +
+
+
+ +
+
+

+ Planes de Producción - Hoy +

+

+ Gestiona la producción programada para hoy +

+
+
+ +
+ {delayedOrders > 0 && ( + + {delayedOrders} retrasadas + + )} + {inProgressOrders > 0 && ( + + {inProgressOrders} activas + + )} + + {completedOrders} completadas + +
+
+
+ + + {displayOrders.length === 0 ? ( +
+
+ +
+

+ No hay producción programada +

+

+ Día libre de producción +

+
+ ) : ( +
+ {displayOrders.map((order) => { + const statusConfig = getOrderStatusConfig(order); + const availableIngredients = order.ingredients.filter(ing => ing.available).length; + const totalIngredients = order.ingredients.length; + const ingredientsReady = availableIngredients === totalIngredients; + + return ( + 70 ? 'var(--color-info)' : + order.progress > 30 ? 'var(--color-warning)' : 'var(--color-error)' + } : undefined} + metadata={[ + `⏰ Inicio: ${order.startTime}`, + `⏱️ Duración: ${formatDuration(order.estimatedDuration)}`, + ...(order.ovenNumber ? [`🔥 Horno ${order.ovenNumber} - ${order.temperature}°C`] : []), + `📋 Ingredientes: ${availableIngredients}/${totalIngredients} ${ingredientsReady ? '✅' : '❌'}`, + ...(order.notes ? [`📝 ${order.notes}`] : []) + ]} + actions={[ + ...(order.status === 'pending' ? [{ + label: 'Iniciar', + icon: Play, + variant: 'primary' as const, + onClick: () => onStartOrder?.(order.id), + priority: 'primary' as const + }] : []), + ...(order.status === 'in_progress' ? [{ + label: 'Pausar', + icon: Pause, + variant: 'outline' as const, + onClick: () => onPauseOrder?.(order.id), + priority: 'primary' as const, + destructive: true + }] : []), + { + label: 'Ver Detalles', + icon: ChevronRight, + variant: 'outline' as const, + onClick: () => onViewDetails?.(order.id), + priority: 'secondary' as const + } + ]} + compact={true} + className="border-l-4" + /> + ); + })} +
+ )} + + {displayOrders.length > 0 && ( +
+
+
+ + {completedOrders} de {displayOrders.length} órdenes completadas + +
+ + +
+
+ )} +
+
+ ); +}; + +export default ProductionPlansToday; \ No newline at end of file diff --git a/frontend/src/components/domain/dashboard/QuickActions.tsx b/frontend/src/components/domain/dashboard/QuickActions.tsx deleted file mode 100644 index 404c2e64..00000000 --- a/frontend/src/components/domain/dashboard/QuickActions.tsx +++ /dev/null @@ -1,382 +0,0 @@ -import React, { KeyboardEvent, useCallback, useMemo } from 'react'; -import { clsx } from 'clsx'; -import { Button, Badge } from '../../ui'; - -export interface QuickAction { - id: string; - title: string; - description?: string; - icon: React.ReactNode; - onClick: () => void; - href?: string; - - // Badge/notification support - badge?: string | number; - badgeVariant?: 'default' | 'primary' | 'success' | 'warning' | 'error'; - - // Access control - permissions?: string[]; - requiredRole?: string; - isDisabled?: boolean; - disabledReason?: string; - - // Styling - variant?: 'primary' | 'secondary' | 'outline' | 'success' | 'warning' | 'danger'; - color?: string; - backgroundGradient?: string; - - // Keyboard shortcuts - shortcut?: string; - - // Priority for ordering - priority?: number; -} - -export interface QuickActionsProps { - actions: QuickAction[]; - - // Layout configuration - columns?: 2 | 3 | 4 | 5 | 6; - gap?: 'sm' | 'md' | 'lg'; - size?: 'sm' | 'md' | 'lg'; - - // Filtering and user context - userRole?: string; - userPermissions?: string[]; - showDisabled?: boolean; - maxActions?: number; - - // Event handlers - onActionClick?: (action: QuickAction) => void; - onActionHover?: (action: QuickAction) => void; - - // Accessibility - 'aria-label'?: string; - className?: string; -} - -// Predefined bakery actions with Spanish context -export const BAKERY_QUICK_ACTIONS: QuickAction[] = [ - { - id: 'new-order', - title: 'Nuevo Pedido', - description: 'Crear un nuevo pedido de cliente', - icon: ( - - - - ), - onClick: () => console.log('Nuevo pedido'), - variant: 'primary', - backgroundGradient: 'from-blue-500 to-blue-600', - priority: 1 - }, - { - id: 'add-product', - title: 'Agregar Producto', - description: 'Añadir nuevo producto al inventario', - icon: ( - - - - ), - onClick: () => console.log('Agregar producto'), - variant: 'success', - backgroundGradient: 'from-green-500 to-green-600', - priority: 2 - }, - { - id: 'view-inventory', - title: 'Ver Inventario', - description: 'Consultar stock y productos', - icon: ( - - - - ), - onClick: () => console.log('Ver inventario'), - variant: 'outline', - backgroundGradient: 'from-purple-500 to-purple-600', - priority: 3, - badge: '5', - badgeVariant: 'warning' - }, - { - id: 'production-batch', - title: 'Nueva Producción', - description: 'Programar lote de producción', - icon: ( - - - - ), - onClick: () => console.log('Nueva producción'), - variant: 'warning', - backgroundGradient: 'from-orange-500 to-orange-600', - priority: 4 - }, - { - id: 'sales-report', - title: 'Reporte Ventas', - description: 'Ver análisis de ventas', - icon: ( - - - - ), - onClick: () => console.log('Reporte ventas'), - variant: 'secondary', - backgroundGradient: 'from-indigo-500 to-indigo-600', - priority: 5 - }, - { - id: 'manage-suppliers', - title: 'Proveedores', - description: 'Gestionar proveedores', - icon: ( - - - - ), - onClick: () => console.log('Proveedores'), - variant: 'outline', - backgroundGradient: 'from-teal-500 to-teal-600', - priority: 6 - }, - { - id: 'pos-system', - title: 'Sistema POS', - description: 'Punto de venta', - icon: ( - - - - ), - onClick: () => console.log('Sistema POS'), - variant: 'primary', - backgroundGradient: 'from-emerald-500 to-emerald-600', - priority: 7 - }, - { - id: 'quality-control', - title: 'Control Calidad', - description: 'Verificación de calidad', - icon: ( - - - - ), - onClick: () => console.log('Control calidad'), - variant: 'success', - backgroundGradient: 'from-lime-500 to-lime-600', - priority: 8, - requiredRole: 'quality_manager' - } -]; - -const QuickActions: React.FC = ({ - actions, - columns = 3, - gap = 'md', - size = 'md', - userRole, - userPermissions = [], - showDisabled = false, - maxActions, - onActionClick, - onActionHover, - 'aria-label': ariaLabel = 'Acciones rápidas', - className -}) => { - // Filter and sort actions - const visibleActions = useMemo(() => { - let filteredActions = actions.filter(action => { - // Role-based filtering - if (action.requiredRole && userRole !== action.requiredRole) { - return showDisabled; - } - - // Permission-based filtering - if (action.permissions && action.permissions.length > 0) { - const hasPermission = action.permissions.some(perm => - userPermissions.includes(perm) - ); - if (!hasPermission) { - return showDisabled; - } - } - - return true; - }); - - // Sort by priority - filteredActions.sort((a, b) => (a.priority || 999) - (b.priority || 999)); - - // Limit actions if specified - if (maxActions) { - filteredActions = filteredActions.slice(0, maxActions); - } - - return filteredActions; - }, [actions, userRole, userPermissions, showDisabled, maxActions]); - - const handleActionClick = useCallback((action: QuickAction) => { - if (action.isDisabled) return; - - onActionClick?.(action); - action.onClick(); - }, [onActionClick]); - - const handleKeyDown = useCallback((event: KeyboardEvent, action: QuickAction) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - handleActionClick(action); - } - }, [handleActionClick]); - - const gridClasses = { - 2: 'grid-cols-2 sm:grid-cols-2', - 3: 'grid-cols-2 sm:grid-cols-3', - 4: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4', - 5: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-5', - 6: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-6' - }; - - const gapClasses = { - sm: 'gap-2', - md: 'gap-4', - lg: 'gap-6' - }; - - const sizeClasses = { - sm: 'p-3 min-h-[80px]', - md: 'p-4 min-h-[100px]', - lg: 'p-6 min-h-[120px]' - }; - - if (visibleActions.length === 0) { - return ( -
-

No hay acciones disponibles

-
- ); - } - - return ( -
- {visibleActions.map((action) => { - const isDisabled = action.isDisabled || - (action.requiredRole && userRole !== action.requiredRole) || - (action.permissions && !action.permissions.some(perm => userPermissions.includes(perm))); - - const buttonClasses = clsx( - 'relative group transition-all duration-200', - 'border border-[var(--border-primary)] rounded-xl', - 'flex flex-col items-center justify-center text-center', - 'hover:shadow-lg hover:-translate-y-1', - 'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2', - sizeClasses[size], - { - 'bg-[var(--bg-primary)] hover:bg-[var(--bg-secondary)]': !action.backgroundGradient, - 'bg-gradient-to-br text-white hover:opacity-90': action.backgroundGradient, - 'opacity-50 cursor-not-allowed hover:transform-none hover:shadow-none': isDisabled, - } - ); - - const gradientStyle = action.backgroundGradient ? { - background: `linear-gradient(135deg, var(--tw-gradient-stops))`, - } : undefined; - - return ( - - ); - })} -
- ); -}; - -QuickActions.displayName = 'QuickActions'; - -export default QuickActions; \ No newline at end of file diff --git a/frontend/src/components/domain/dashboard/RealTimeAlerts.tsx b/frontend/src/components/domain/dashboard/RealTimeAlerts.tsx new file mode 100644 index 00000000..cb57aa1e --- /dev/null +++ b/frontend/src/components/domain/dashboard/RealTimeAlerts.tsx @@ -0,0 +1,307 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardHeader, CardBody } from '../../ui/Card'; +import { StatusCard } from '../../ui/StatusCard/StatusCard'; +import { Badge } from '../../ui/Badge'; +import { + AlertTriangle, + AlertCircle, + Info, + CheckCircle, + Clock, + X, + Wifi, + WifiOff, + Bell +} from 'lucide-react'; + +export interface Alert { + id: string; + type: 'critical' | 'warning' | 'info' | 'success'; + title: string; + message: string; + timestamp: string; + source: string; + actionRequired?: boolean; + resolved?: boolean; +} + +export interface RealTimeAlertsProps { + className?: string; + maxAlerts?: number; + enableSSE?: boolean; + sseEndpoint?: string; +} + +const RealTimeAlerts: React.FC = ({ + className, + maxAlerts = 10, + enableSSE = true, + sseEndpoint = '/api/alerts/stream' +}) => { + const [alerts, setAlerts] = useState([ + { + id: '1', + type: 'critical', + title: 'Stock Crítico', + message: 'Levadura fresca: Solo quedan 2 unidades (mínimo: 5)', + timestamp: new Date().toISOString(), + source: 'Inventario', + actionRequired: true + }, + { + id: '2', + type: 'warning', + title: 'Temperatura Horno', + message: 'Horno principal: Temperatura fuera del rango óptimo (185°C)', + timestamp: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + source: 'Producción', + actionRequired: true + }, + { + id: '3', + type: 'info', + title: 'Nueva Orden', + message: 'Orden #1247: 50 croissants para las 14:00', + timestamp: new Date(Date.now() - 10 * 60 * 1000).toISOString(), + source: 'Ventas' + } + ]); + + const [isConnected, setIsConnected] = useState(false); + + useEffect(() => { + let eventSource: EventSource | null = null; + + if (enableSSE) { + eventSource = new EventSource(sseEndpoint); + + eventSource.onopen = () => { + setIsConnected(true); + }; + + eventSource.onmessage = (event) => { + try { + const newAlert: Alert = JSON.parse(event.data); + setAlerts(prev => { + const updated = [newAlert, ...prev]; + return updated.slice(0, maxAlerts); + }); + } catch (error) { + console.error('Error parsing alert data:', error); + } + }; + + eventSource.onerror = () => { + setIsConnected(false); + }; + } + + return () => { + if (eventSource) { + eventSource.close(); + } + }; + }, [enableSSE, sseEndpoint, maxAlerts]); + + const getAlertStatusConfig = (alert: Alert) => { + const baseConfig = { + isCritical: alert.type === 'critical', + isHighlight: alert.type === 'warning' || alert.actionRequired, + }; + + switch (alert.type) { + case 'critical': + return { + ...baseConfig, + color: 'var(--color-error)', + text: 'Crítico', + icon: AlertTriangle + }; + case 'warning': + return { + ...baseConfig, + color: 'var(--color-warning)', + text: 'Advertencia', + icon: AlertCircle + }; + case 'info': + return { + ...baseConfig, + color: 'var(--color-info)', + text: 'Información', + icon: Info + }; + case 'success': + return { + ...baseConfig, + color: 'var(--color-success)', + text: 'Éxito', + icon: CheckCircle + }; + default: + return { + ...baseConfig, + color: 'var(--color-info)', + text: 'Información', + icon: Info + }; + } + }; + + const formatTimestamp = (timestamp: string) => { + const date = new Date(timestamp); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / (1000 * 60)); + + if (diffMins < 1) return 'Ahora'; + if (diffMins < 60) return `Hace ${diffMins}m`; + + const diffHours = Math.floor(diffMins / 60); + if (diffHours < 24) return `Hace ${diffHours}h`; + + return date.toLocaleDateString('es-ES', { + day: '2-digit', + month: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + }; + + const dismissAlert = (alertId: string) => { + setAlerts(prev => prev.filter(alert => alert.id !== alertId)); + }; + + const handleViewAlert = (alertId: string) => { + console.log('Viewing alert details:', alertId); + }; + + const unresolvedAlerts = alerts.filter(alert => !alert.resolved); + const criticalCount = unresolvedAlerts.filter(alert => alert.type === 'critical').length; + const warningCount = unresolvedAlerts.filter(alert => alert.type === 'warning').length; + + return ( + + +
+
+
+ +
+
+

+ Alertas en Tiempo Real +

+
+ {isConnected ? ( + + ) : ( + + )} + + {isConnected ? 'Conectado' : 'Desconectado'} + +
+
+
+ +
+ {criticalCount > 0 && ( + + {criticalCount} críticas + + )} + {warningCount > 0 && ( + + {warningCount} advertencias + + )} +
+
+
+ + + {unresolvedAlerts.length === 0 ? ( +
+
+ +
+

+ No hay alertas activas +

+

+ Todo funciona correctamente en tu panadería +

+
+ ) : ( +
+ {unresolvedAlerts.map((alert) => { + const statusConfig = getAlertStatusConfig(alert); + + return ( + handleViewAlert(alert.id), + priority: 'primary' + }, + { + label: 'Descartar', + icon: X, + variant: 'outline', + onClick: () => dismissAlert(alert.id), + priority: 'secondary', + destructive: true + } + ]} + compact={true} + className="border-l-4" + /> + ); + })} +
+ )} + + {unresolvedAlerts.length > 0 && ( +
+

+ {unresolvedAlerts.length} alertas activas • + + Monitoreo automático habilitado + +

+
+ )} +
+
+ ); +}; + +export default RealTimeAlerts; \ No newline at end of file diff --git a/frontend/src/components/domain/dashboard/RecentActivity.tsx b/frontend/src/components/domain/dashboard/RecentActivity.tsx deleted file mode 100644 index fcc31924..00000000 --- a/frontend/src/components/domain/dashboard/RecentActivity.tsx +++ /dev/null @@ -1,524 +0,0 @@ -import React, { useMemo, useState, useCallback } from 'react'; -import { clsx } from 'clsx'; -import { Avatar, Badge, Button } from '../../ui'; - -export interface ActivityUser { - id: string; - name: string; - avatar?: string; - role?: string; -} - -export interface ActivityItem { - id: string; - type: ActivityType; - title: string; - description: string; - timestamp: string; - user?: ActivityUser; - metadata?: Record; - status?: ActivityStatus; - category?: string; - - // Navigation support - href?: string; - onClick?: () => void; - - // Visual styling - icon?: React.ReactNode; - color?: string; - priority?: ActivityPriority; -} - -export enum ActivityType { - ORDER = 'order', - PRODUCTION = 'production', - INVENTORY = 'inventory', - SALES = 'sales', - USER = 'user', - SYSTEM = 'system', - QUALITY = 'quality', - SUPPLIER = 'supplier', - FINANCE = 'finance', - ALERT = 'alert' -} - -export enum ActivityStatus { - SUCCESS = 'success', - WARNING = 'warning', - ERROR = 'error', - INFO = 'info', - PENDING = 'pending', - IN_PROGRESS = 'in_progress', - CANCELLED = 'cancelled' -} - -export enum ActivityPriority { - LOW = 'low', - MEDIUM = 'medium', - HIGH = 'high', - URGENT = 'urgent' -} - -export interface RecentActivityProps { - activities: ActivityItem[]; - - // Display configuration - maxItems?: number; - showTimestamp?: boolean; - showUserAvatar?: boolean; - showTypeIcons?: boolean; - compact?: boolean; - - // Filtering - allowFiltering?: boolean; - filterTypes?: ActivityType[]; - defaultFilter?: ActivityType | 'all'; - - // Pagination and loading - hasMore?: boolean; - isLoading?: boolean; - onLoadMore?: () => void; - - // Event handlers - onActivityClick?: (activity: ActivityItem) => void; - onRefresh?: () => void; - - // Accessibility and styling - className?: string; - 'aria-label'?: string; - emptyMessage?: string; -} - -// Spanish activity type labels and icons -const ACTIVITY_CONFIG = { - [ActivityType.ORDER]: { - label: 'Pedidos', - icon: ( - - - - ), - color: 'blue', - bgColor: 'bg-[var(--color-info)]/10', - textColor: 'text-[var(--color-info)]', - borderColor: 'border-[var(--color-info)]/20' - }, - [ActivityType.PRODUCTION]: { - label: 'Producción', - icon: ( - - - - ), - color: 'orange', - bgColor: 'bg-[var(--color-primary)]/10', - textColor: 'text-[var(--color-primary)]', - borderColor: 'border-orange-200' - }, - [ActivityType.INVENTORY]: { - label: 'Inventario', - icon: ( - - - - ), - color: 'purple', - bgColor: 'bg-purple-100', - textColor: 'text-purple-600', - borderColor: 'border-purple-200' - }, - [ActivityType.SALES]: { - label: 'Ventas', - icon: ( - - - - ), - color: 'green', - bgColor: 'bg-[var(--color-success)]/10', - textColor: 'text-[var(--color-success)]', - borderColor: 'border-green-200' - }, - [ActivityType.USER]: { - label: 'Usuarios', - icon: ( - - - - ), - color: 'indigo', - bgColor: 'bg-indigo-100', - textColor: 'text-indigo-600', - borderColor: 'border-indigo-200' - }, - [ActivityType.SYSTEM]: { - label: 'Sistema', - icon: ( - - - - - ), - color: 'gray', - bgColor: 'bg-[var(--bg-tertiary)]', - textColor: 'text-[var(--text-secondary)]', - borderColor: 'border-[var(--border-primary)]' - }, - [ActivityType.QUALITY]: { - label: 'Calidad', - icon: ( - - - - ), - color: 'green', - bgColor: 'bg-[var(--color-success)]/10', - textColor: 'text-[var(--color-success)]', - borderColor: 'border-green-200' - }, - [ActivityType.SUPPLIER]: { - label: 'Proveedores', - icon: ( - - - - ), - color: 'teal', - bgColor: 'bg-teal-100', - textColor: 'text-teal-600', - borderColor: 'border-teal-200' - }, - [ActivityType.FINANCE]: { - label: 'Finanzas', - icon: ( - - - - ), - color: 'emerald', - bgColor: 'bg-emerald-100', - textColor: 'text-emerald-600', - borderColor: 'border-emerald-200' - }, - [ActivityType.ALERT]: { - label: 'Alertas', - icon: ( - - - - ), - color: 'red', - bgColor: 'bg-[var(--color-error)]/10', - textColor: 'text-[var(--color-error)]', - borderColor: 'border-red-200' - } -}; - -const STATUS_CONFIG = { - [ActivityStatus.SUCCESS]: { color: 'green', bgColor: 'bg-green-500' }, - [ActivityStatus.WARNING]: { color: 'yellow', bgColor: 'bg-yellow-500' }, - [ActivityStatus.ERROR]: { color: 'red', bgColor: 'bg-red-500' }, - [ActivityStatus.INFO]: { color: 'blue', bgColor: 'bg-[var(--color-info)]/50' }, - [ActivityStatus.PENDING]: { color: 'gray', bgColor: 'bg-[var(--bg-secondary)]0' }, - [ActivityStatus.IN_PROGRESS]: { color: 'purple', bgColor: 'bg-purple-500' }, - [ActivityStatus.CANCELLED]: { color: 'gray', bgColor: 'bg-gray-400' } -}; - -const formatRelativeTime = (timestamp: string): string => { - const date = new Date(timestamp); - const now = new Date(); - const diffInMs = now.getTime() - date.getTime(); - const diffInMinutes = Math.floor(diffInMs / (1000 * 60)); - const diffInHours = Math.floor(diffInMinutes / 60); - const diffInDays = Math.floor(diffInHours / 24); - - if (diffInMinutes < 1) { - return 'Ahora mismo'; - } else if (diffInMinutes < 60) { - return `Hace ${diffInMinutes} min`; - } else if (diffInHours < 24) { - return `Hace ${diffInHours}h`; - } else if (diffInDays === 1) { - return 'Ayer'; - } else if (diffInDays < 7) { - return `Hace ${diffInDays} días`; - } else { - return date.toLocaleDateString('es-ES', { - day: 'numeric', - month: 'short' - }); - } -}; - -const RecentActivity: React.FC = ({ - activities, - maxItems = 10, - showTimestamp = true, - showUserAvatar = true, - showTypeIcons = true, - compact = false, - allowFiltering = true, - filterTypes = Object.values(ActivityType), - defaultFilter = 'all', - hasMore = false, - isLoading = false, - onLoadMore, - onActivityClick, - onRefresh, - className, - 'aria-label': ariaLabel = 'Actividad reciente', - emptyMessage = 'No hay actividad reciente' -}) => { - const [activeFilter, setActiveFilter] = useState(defaultFilter); - - const filteredActivities = useMemo(() => { - let filtered = activities; - - if (activeFilter !== 'all') { - filtered = activities.filter(activity => activity.type === activeFilter); - } - - return filtered.slice(0, maxItems); - }, [activities, activeFilter, maxItems]); - - const handleActivityClick = useCallback((activity: ActivityItem) => { - onActivityClick?.(activity); - if (activity.onClick) { - activity.onClick(); - } - }, [onActivityClick]); - - const renderActivityItem = (activity: ActivityItem) => { - const config = ACTIVITY_CONFIG[activity.type] || { - label: 'Actividad', - icon:
, - color: 'gray', - bgColor: 'bg-[var(--bg-tertiary)]', - textColor: 'text-[var(--text-secondary)]', - borderColor: 'border-[var(--border-primary)]' - }; - const statusConfig = activity.status ? STATUS_CONFIG[activity.status] : null; - - const itemClasses = clsx( - 'group relative flex items-start gap-3 p-3 rounded-lg transition-all duration-200', - 'hover:bg-[var(--bg-secondary)] hover:shadow-sm', - { - 'cursor-pointer': activity.onClick || activity.href, - 'p-2': compact, - 'border-l-4': !compact, - [config.borderColor]: !compact - } - ); - - return ( -
handleActivityClick(activity) : undefined} - role={activity.onClick || activity.href ? 'button' : undefined} - tabIndex={activity.onClick || activity.href ? 0 : undefined} - > - {/* Timeline indicator */} -
- {showTypeIcons && ( -
- {activity.icon || config.icon} -
- )} - - {/* Status indicator */} - {activity.status && statusConfig && ( -
- )} -
- - {/* Content */} -
-
-
-

- {activity.title} -

-

- {activity.description} -

- - {/* User info */} - {activity.user && showUserAvatar && ( -
- - - {activity.user.name} - -
- )} -
- - {/* Timestamp */} - {showTimestamp && ( - - )} -
-
-
- ); - }; - - if (activities.length === 0) { - return ( -
-
- - - -
-

{emptyMessage}

-
- ); - } - - return ( -
- {/* Filters */} - {allowFiltering && filterTypes.length > 1 && ( -
- - {filterTypes.map((type) => { - const config = ACTIVITY_CONFIG[type] || { - label: 'Actividad', - icon:
, - color: 'gray', - bgColor: 'bg-[var(--bg-tertiary)]', - textColor: 'text-[var(--text-secondary)]', - borderColor: 'border-[var(--border-primary)]' - }; - const count = activities.filter(a => a.type === type).length; - - return ( - - ); - })} - - {onRefresh && ( -
- -
- )} -
- )} - - {/* Activity list */} -
- {filteredActivities.map(renderActivityItem)} -
- - {/* Loading state */} - {isLoading && ( -
-
-
- )} - - {/* Load more */} - {hasMore && onLoadMore && !isLoading && ( -
- -
- )} -
- ); -}; - -RecentActivity.displayName = 'RecentActivity'; - -export default RecentActivity; \ No newline at end of file diff --git a/frontend/src/pages/app/DashboardPage.tsx b/frontend/src/pages/app/DashboardPage.tsx index 7ee55814..2811efca 100644 --- a/frontend/src/pages/app/DashboardPage.tsx +++ b/frontend/src/pages/app/DashboardPage.tsx @@ -1,350 +1,127 @@ import React from 'react'; -import { TrendingUp, TrendingDown, AlertTriangle, CheckCircle, Clock, DollarSign } from 'lucide-react'; -import { Card, Badge } from '../../components/ui'; import { PageHeader } from '../../components/layout'; -import { DashboardCard, KPIWidget, QuickActions, RecentActivity, ActivityType, ActivityStatus } from '../../components/domain/dashboard'; +import StatsGrid from '../../components/ui/Stats/StatsGrid'; +import RealTimeAlerts from '../../components/domain/dashboard/RealTimeAlerts'; +import ProcurementPlansToday from '../../components/domain/dashboard/ProcurementPlansToday'; +import ProductionPlansToday from '../../components/domain/dashboard/ProductionPlansToday'; +import { + AlertTriangle, + Clock, + DollarSign, + Package, + TrendingUp, + TrendingDown +} from 'lucide-react'; const DashboardPage: React.FC = () => { - const kpiData = [ + const criticalStats = [ { title: 'Ventas Hoy', - value: { - current: 1247, - previous: 1112, - format: 'currency' as const, - prefix: '€' - }, + value: '€1,247', + icon: DollarSign, + variant: 'success' as const, trend: { - direction: 'up' as const, value: 12, - isPositive: true, - comparisonPeriod: 'vs ayer' + direction: 'up' as const, + label: '% vs ayer' }, - icon: , + subtitle: '+€135 más que ayer' }, { title: 'Órdenes Pendientes', - value: { - current: 23, - previous: 24, - format: 'number' as const - }, + value: '23', + icon: Clock, + variant: 'warning' as const, trend: { + value: 4, direction: 'down' as const, - value: 4.2, - isPositive: false, - comparisonPeriod: 'vs ayer' + label: '% vs ayer' }, - icon: , + subtitle: 'Requieren atención' }, { title: 'Productos Vendidos', - value: { - current: 156, - previous: 144, - format: 'number' as const - }, + value: '156', + icon: Package, + variant: 'info' as const, trend: { + value: 8, direction: 'up' as const, - value: 8.3, - isPositive: true, - comparisonPeriod: 'vs ayer' + label: '% vs ayer' }, - icon: , + subtitle: '+12 unidades más' }, { title: 'Stock Crítico', - value: { - current: 4, - previous: 2, - format: 'number' as const - }, + value: '4', + icon: AlertTriangle, + variant: 'error' as const, trend: { - direction: 'up' as const, value: 100, - isPositive: false, - comparisonPeriod: 'vs ayer' + direction: 'up' as const, + label: '% vs ayer' }, - status: 'warning' as const, - icon: , - }, + subtitle: 'Acción requerida' + } ]; - const quickActions = [ - { - id: 'production', - title: 'Nueva Orden de Producción', - description: 'Crear nueva orden de producción', - icon: , - onClick: () => window.location.href = '/app/operations/production', - href: '/app/operations/production' - }, - { - id: 'inventory', - title: 'Gestionar Inventario', - description: 'Administrar stock de productos', - icon: , - onClick: () => window.location.href = '/app/operations/inventory', - href: '/app/operations/inventory' - }, - { - id: 'sales', - title: 'Ver Ventas', - description: 'Analizar ventas y reportes', - icon: , - onClick: () => window.location.href = '/app/analytics/sales', - href: '/app/analytics/sales' - }, - { - id: 'settings', - title: 'Configuración', - description: 'Ajustar configuración del sistema', - icon: , - onClick: () => window.location.href = '/app/settings', - href: '/app/settings' - }, - ]; - - const recentActivities = [ - { - id: '1', - type: ActivityType.PRODUCTION, - title: 'Orden de producción completada', - description: 'Pan de Molde Integral - 20 unidades', - timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), - status: ActivityStatus.SUCCESS, - }, - { - id: '2', - type: ActivityType.INVENTORY, - title: 'Stock bajo detectado', - description: 'Levadura fresca necesita reposición', - timestamp: new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString(), - status: ActivityStatus.WARNING, - }, - { - id: '3', - type: ActivityType.SALES, - title: 'Venta registrada', - description: '€45.50 - Croissants y café', - timestamp: new Date(Date.now() - 4 * 60 * 60 * 1000).toISOString(), - status: ActivityStatus.INFO, - }, - ]; - - const productionStatus = { - today: { - target: 150, - completed: 95, - inProgress: 18, - pending: 37, - }, - efficiency: 85, + const handleOrderItem = (itemId: string) => { + console.log('Ordering item:', itemId); }; - const salesData = { - today: 1247, - yesterday: 1112, - thisWeek: 8934, - thisMonth: 35678, + const handleStartOrder = (orderId: string) => { + console.log('Starting production order:', orderId); }; - const inventoryAlerts = [ - { item: 'Levadura Fresca', current: 2, min: 5, status: 'critical' }, - { item: 'Harina Integral', current: 8, min: 10, status: 'low' }, - { item: 'Mantequilla', current: 15, min: 20, status: 'low' }, - ]; + const handlePauseOrder = (orderId: string) => { + console.log('Pausing production order:', orderId); + }; - const topProducts = [ - { name: 'Pan de Molde', sold: 45, revenue: 202.50 }, - { name: 'Croissants', sold: 32, revenue: 192.00 }, - { name: 'Baguettes', sold: 28, revenue: 84.00 }, - { name: 'Magdalenas', sold: 24, revenue: 72.00 }, - ]; + const handleViewDetails = (id: string) => { + console.log('Viewing details for:', id); + }; + const handleViewAllPlans = () => { + console.log('Viewing all plans'); + }; return ( -
+
- {/* KPI Cards */} -
- {kpiData.map((kpi, index) => ( - - ))} + {/* Critical Metrics using StatsGrid */} + + + {/* Full width blocks - one after another */} +
+ {/* 1. Real-time alerts block */} + + + {/* 2. Procurement plans block */} + + + {/* 3. Production plans block */} +
- -
- {/* Production Status */} - -

Estado de Producción

- -
-
- Progreso del Día - - {productionStatus.today.completed} / {productionStatus.today.target} - -
- -
-
-
- -
-
-

{productionStatus.today.completed}

-

Completado

-
-
-

{productionStatus.today.inProgress}

-

En Proceso

-
-
-

{productionStatus.today.pending}

-

Pendiente

-
-
- -
-
- Eficiencia - {productionStatus.efficiency}% -
-
-
-
- - {/* Sales Summary */} - -

Resumen de Ventas

- -
-
- Hoy - €{salesData.today.toLocaleString()} -
- -
- Ayer -
- €{salesData.yesterday.toLocaleString()} - {salesData.today > salesData.yesterday ? ( - - ) : ( - - )} -
-
- -
- Esta Semana - €{salesData.thisWeek.toLocaleString()} -
- -
- Este Mes - €{salesData.thisMonth.toLocaleString()} -
- -
-
-

Crecimiento vs ayer

-

- +{(((salesData.today - salesData.yesterday) / salesData.yesterday) * 100).toFixed(1)}% -

-
-
-
-
- - {/* Inventory Alerts */} - -

Alertas de Inventario

- -
- {inventoryAlerts.map((alert, index) => ( -
-
-

{alert.item}

-

Stock: {alert.current} / Mín: {alert.min}

-
- - {alert.status === 'critical' ? 'Crítico' : 'Bajo'} - -
- ))} -
- -
- -
-
-
- -
- {/* Top Products */} - -

Productos Más Vendidos

- -
- {topProducts.map((product, index) => ( -
-
- {index + 1}. - {product.name} -
-
-

{product.sold} unidades

-

€{product.revenue.toFixed(2)}

-
-
- ))} -
- -
- -
-
- - {/* Recent Activity */} - -

Actividad Reciente

- - - -
- -
-
-
- - {/* Quick Actions */} - -

Acciones Rápidas

- -
); };