Add improved production UI

This commit is contained in:
Urtzi Alfaro
2025-09-23 12:49:35 +02:00
parent 8d54202e91
commit 4ae8e14e55
35 changed files with 6848 additions and 415 deletions

View File

@@ -0,0 +1,408 @@
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardHeader, CardBody } from '../../ui/Card';
import { Badge } from '../../ui/Badge';
import { Button } from '../../ui/Button';
import {
Brain,
TrendingUp,
TrendingDown,
AlertTriangle,
Target,
Lightbulb,
BarChart3,
Zap,
Clock,
DollarSign,
Users,
Package
} from 'lucide-react';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { useProductionDashboard } from '../../../api';
export interface AIInsight {
id: string;
type: 'optimization' | 'prediction' | 'alert' | 'recommendation';
title: string;
description: string;
impact: 'high' | 'medium' | 'low';
category: 'cost' | 'quality' | 'efficiency' | 'demand' | 'maintenance';
confidence: number;
potentialSavings?: number;
timeToImplement?: string;
actions: Array<{
label: string;
action: string;
}>;
data?: {
current: number;
predicted: number;
unit: string;
};
createdAt: string;
}
export interface AIInsightsWidgetProps {
className?: string;
insights?: AIInsight[];
onViewInsight?: (insightId: string) => void;
onImplement?: (insightId: string, actionId: string) => void;
onViewAll?: () => void;
}
const AIInsightsWidget: React.FC<AIInsightsWidgetProps> = ({
className,
insights,
onViewInsight,
onImplement,
onViewAll
}) => {
const { t } = useTranslation();
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
const {
data: dashboardData,
isLoading,
error
} = useProductionDashboard(tenantId);
const aiInsights = useMemo((): AIInsight[] => {
if (insights) return insights;
// Mock AI-generated insights for demonstration
return [
{
id: '1',
type: 'optimization',
title: t('ai.insights.reduce_energy_costs', 'Reduce Energy Costs by 15%'),
description: t('ai.insights.energy_description', 'Adjust oven schedules to use off-peak electricity rates. Estimated savings: <20>45/day'),
impact: 'high',
category: 'cost',
confidence: 87,
potentialSavings: 1350,
timeToImplement: '2 days',
actions: [
{ label: t('ai.actions.schedule_optimization', 'Optimize Schedule'), action: 'optimize_schedule' },
{ label: t('ai.actions.view_details', 'View Details'), action: 'view_details' }
],
data: {
current: 95.2,
predicted: 81.0,
unit: '<27>/day'
},
createdAt: '2024-01-23T08:30:00Z'
},
{
id: '2',
type: 'prediction',
title: t('ai.insights.demand_increase', 'Croissant Demand Surge Predicted'),
description: t('ai.insights.demand_description', 'Weather and event data suggest 40% increase in croissant demand this weekend'),
impact: 'high',
category: 'demand',
confidence: 92,
timeToImplement: '1 day',
actions: [
{ label: t('ai.actions.increase_production', 'Increase Production'), action: 'adjust_production' },
{ label: t('ai.actions.order_ingredients', 'Order Ingredients'), action: 'order_ingredients' }
],
data: {
current: 150,
predicted: 210,
unit: 'units/day'
},
createdAt: '2024-01-23T07:15:00Z'
},
{
id: '3',
type: 'alert',
title: t('ai.insights.quality_decline', 'Quality Score Decline Detected'),
description: t('ai.insights.quality_description', 'Bread quality has decreased by 8% over the last 3 days. Check flour moisture levels'),
impact: 'medium',
category: 'quality',
confidence: 78,
timeToImplement: '4 hours',
actions: [
{ label: t('ai.actions.check_ingredients', 'Check Ingredients'), action: 'quality_check' },
{ label: t('ai.actions.adjust_recipe', 'Adjust Recipe'), action: 'recipe_adjustment' }
],
data: {
current: 8.2,
predicted: 7.5,
unit: 'score'
},
createdAt: '2024-01-23T06:45:00Z'
},
{
id: '4',
type: 'recommendation',
title: t('ai.insights.maintenance_due', 'Preventive Maintenance Recommended'),
description: t('ai.insights.maintenance_description', 'Mixer #2 showing early wear patterns. Schedule maintenance to prevent breakdown'),
impact: 'medium',
category: 'maintenance',
confidence: 85,
potentialSavings: 800,
timeToImplement: '1 week',
actions: [
{ label: t('ai.actions.schedule_maintenance', 'Schedule Maintenance'), action: 'schedule_maintenance' },
{ label: t('ai.actions.view_equipment', 'View Equipment'), action: 'view_equipment' }
],
createdAt: '2024-01-22T14:20:00Z'
}
];
}, [insights, t]);
const getInsightConfig = (type: AIInsight['type']) => {
const configs = {
optimization: {
color: 'success' as const,
icon: TrendingUp,
bgColor: 'bg-green-50 dark:bg-green-900/20',
borderColor: 'border-green-200 dark:border-green-800'
},
prediction: {
color: 'info' as const,
icon: BarChart3,
bgColor: 'bg-blue-50 dark:bg-blue-900/20',
borderColor: 'border-blue-200 dark:border-blue-800'
},
alert: {
color: 'warning' as const,
icon: AlertTriangle,
bgColor: 'bg-orange-50 dark:bg-orange-900/20',
borderColor: 'border-orange-200 dark:border-orange-800'
},
recommendation: {
color: 'default' as const,
icon: Lightbulb,
bgColor: 'bg-purple-50 dark:bg-purple-900/20',
borderColor: 'border-purple-200 dark:border-purple-800'
}
};
return configs[type];
};
const getCategoryIcon = (category: AIInsight['category']) => {
const icons = {
cost: DollarSign,
quality: Target,
efficiency: Zap,
demand: BarChart3,
maintenance: Users
};
return icons[category] || Package;
};
const getImpactColor = (impact: AIInsight['impact']) => {
const colors = {
high: 'text-red-600',
medium: 'text-orange-600',
low: 'text-green-600'
};
return colors[impact];
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('es-ES', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0
}).format(amount);
};
const highImpactInsights = aiInsights.filter(i => i.impact === 'high').length;
const totalPotentialSavings = aiInsights.reduce((sum, i) => sum + (i.potentialSavings || 0), 0);
const avgConfidence = aiInsights.reduce((sum, i) => sum + i.confidence, 0) / aiInsights.length;
if (isLoading) {
return (
<Card className={className}>
<CardHeader>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('ai.title', 'AI Insights')}
</h3>
</CardHeader>
<CardBody>
<div className="animate-pulse space-y-4">
<div className="grid grid-cols-3 gap-4">
<div className="h-12 bg-[var(--bg-secondary)] rounded"></div>
<div className="h-12 bg-[var(--bg-secondary)] rounded"></div>
<div className="h-12 bg-[var(--bg-secondary)] rounded"></div>
</div>
<div className="space-y-3">
<div className="h-20 bg-[var(--bg-secondary)] rounded"></div>
<div className="h-20 bg-[var(--bg-secondary)] rounded"></div>
</div>
</div>
</CardBody>
</Card>
);
}
if (error) {
return (
<Card className={className}>
<CardBody className="text-center py-8">
<Brain className="w-12 h-12 text-[var(--text-tertiary)] mx-auto mb-4" />
<p className="text-[var(--text-secondary)]">
{t('ai.error', 'AI insights temporarily unavailable')}
</p>
</CardBody>
</Card>
);
}
return (
<Card className={className}>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Brain className="w-5 h-5 text-[var(--color-primary)]" />
<div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('ai.title', 'AI Insights')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('ai.subtitle', 'AI-powered recommendations and predictions')}
</p>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={onViewAll}
>
{t('common.view_all', 'View All')}
</Button>
</div>
</CardHeader>
<CardBody className="space-y-6">
{/* Summary Stats */}
<div className="grid grid-cols-3 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-red-600">
{highImpactInsights}
</div>
<div className="text-sm text-[var(--text-secondary)]">
{t('ai.high_impact', 'High Impact')}
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-600">
{totalPotentialSavings > 0 ? formatCurrency(totalPotentialSavings) : ''}
</div>
<div className="text-sm text-[var(--text-secondary)]">
{t('ai.potential_savings', 'Potential Savings')}
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-[var(--color-primary)]">
{avgConfidence.toFixed(0)}%
</div>
<div className="text-sm text-[var(--text-secondary)]">
{t('ai.confidence', 'Avg Confidence')}
</div>
</div>
</div>
{/* Insights List */}
<div className="space-y-3">
{aiInsights.slice(0, 3).map((insight) => {
const config = getInsightConfig(insight.type);
const CategoryIcon = getCategoryIcon(insight.category);
const TypeIcon = config.icon;
return (
<div
key={insight.id}
className={`p-4 rounded-lg border ${config.bgColor} ${config.borderColor} hover:shadow-sm transition-all cursor-pointer`}
onClick={() => onViewInsight?.(insight.id)}
>
<div className="flex items-start justify-between">
<div className="flex items-start space-x-3 flex-1">
<div className="flex items-center space-x-1">
<TypeIcon className="w-5 h-5 text-[var(--color-primary)]" />
<CategoryIcon className="w-4 h-4 text-[var(--text-tertiary)]" />
</div>
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1">
<h4 className="font-medium text-[var(--text-primary)] text-sm">
{insight.title}
</h4>
<Badge variant={config.color} size="sm">
{insight.confidence}%
</Badge>
</div>
<p className="text-sm text-[var(--text-secondary)] mb-2">
{insight.description}
</p>
<div className="flex items-center space-x-4 text-xs text-[var(--text-tertiary)]">
<span className={`font-medium ${getImpactColor(insight.impact)}`}>
{insight.impact.toUpperCase()} {t('ai.impact', 'IMPACT')}
</span>
{insight.timeToImplement && (
<span className="flex items-center space-x-1">
<Clock className="w-3 h-3" />
<span>{insight.timeToImplement}</span>
</span>
)}
{insight.potentialSavings && (
<span className="font-medium text-green-600">
{formatCurrency(insight.potentialSavings)}
</span>
)}
</div>
</div>
</div>
{insight.data && (
<div className="text-right ml-3">
<div className="text-sm font-medium text-[var(--text-primary)]">
{insight.data.current} <EFBFBD> {insight.data.predicted}
</div>
<div className="text-xs text-[var(--text-secondary)]">
{insight.data.unit}
</div>
</div>
)}
</div>
{/* Action buttons */}
<div className="flex items-center space-x-2 mt-3 pt-3 border-t border-[var(--border-primary)]">
{insight.actions.slice(0, 2).map((action, index) => (
<Button
key={index}
variant={index === 0 ? "primary" : "outline"}
size="xs"
onClick={(e) => {
e.stopPropagation();
onImplement?.(insight.id, action.action);
}}
>
{action.label}
</Button>
))}
</div>
</div>
);
})}
</div>
{/* AI Status */}
<div className="flex items-center justify-between pt-4 border-t border-[var(--border-primary)]">
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span className="text-sm text-[var(--text-secondary)]">
{t('ai.status.active', 'AI monitoring active')}
</span>
</div>
<div className="text-xs text-[var(--text-tertiary)]">
{t('ai.last_updated', 'Updated 5m ago')}
</div>
</div>
</CardBody>
</Card>
);
};
export default AIInsightsWidget;

View File

@@ -0,0 +1,392 @@
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardHeader, CardBody } from '../../ui/Card';
import { Badge } from '../../ui/Badge';
import { Button } from '../../ui/Button';
import {
Settings,
AlertTriangle,
CheckCircle,
Clock,
Thermometer,
Activity,
Wrench,
Zap,
Calendar,
TrendingUp
} from 'lucide-react';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { useProductionDashboard } from '../../../api';
export interface EquipmentStatus {
id: string;
name: string;
type: 'oven' | 'mixer' | 'proofer' | 'freezer' | 'packaging';
status: 'operational' | 'maintenance' | 'down' | 'warning';
location: string;
temperature?: number;
targetTemperature?: number;
efficiency: number;
uptime: number;
lastMaintenance: string;
nextMaintenance: string;
alerts: string[];
energyUsage: number;
utilizationToday: number;
}
export interface EquipmentStatusWidgetProps {
className?: string;
equipment?: EquipmentStatus[];
onViewEquipment?: (equipmentId: string) => void;
onScheduleMaintenance?: (equipmentId: string) => void;
onViewAll?: () => void;
}
const EquipmentStatusWidget: React.FC<EquipmentStatusWidgetProps> = ({
className,
equipment,
onViewEquipment,
onScheduleMaintenance,
onViewAll
}) => {
const { t } = useTranslation();
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
const {
data: dashboardData,
isLoading,
error
} = useProductionDashboard(tenantId);
const equipmentData = useMemo((): EquipmentStatus[] => {
if (equipment) return equipment;
// Mock data for demonstration
return [
{
id: '1',
name: 'Horno Principal #1',
type: 'oven',
status: 'operational',
location: '<27>rea de Horneado',
temperature: 220,
targetTemperature: 220,
efficiency: 92,
uptime: 98.5,
lastMaintenance: '2024-01-15',
nextMaintenance: '2024-03-15',
alerts: [],
energyUsage: 45.2,
utilizationToday: 87
},
{
id: '2',
name: 'Batidora Industrial #2',
type: 'mixer',
status: 'warning',
location: '<27>rea de Preparaci<63>n',
efficiency: 88,
uptime: 94.2,
lastMaintenance: '2024-01-20',
nextMaintenance: '2024-02-20',
alerts: ['Vibraci<63>n inusual detectada', 'Revisar correas'],
energyUsage: 12.8,
utilizationToday: 76
},
{
id: '3',
name: 'C<>mara de Fermentaci<63>n #1',
type: 'proofer',
status: 'maintenance',
location: '<27>rea de Fermentaci<63>n',
temperature: 32,
targetTemperature: 35,
efficiency: 0,
uptime: 85.1,
lastMaintenance: '2024-01-23',
nextMaintenance: '2024-01-24',
alerts: ['En mantenimiento programado'],
energyUsage: 0,
utilizationToday: 0
},
{
id: '4',
name: 'Congelador R<>pido',
type: 'freezer',
status: 'operational',
location: '<27>rea de Congelado',
temperature: -18,
targetTemperature: -18,
efficiency: 95,
uptime: 99.2,
lastMaintenance: '2024-01-10',
nextMaintenance: '2024-04-10',
alerts: [],
energyUsage: 23.5,
utilizationToday: 65
}
];
}, [equipment]);
const getStatusConfig = (status: EquipmentStatus['status']) => {
const configs = {
operational: {
color: 'success' as const,
icon: CheckCircle,
label: t('equipment.status.operational', 'Operational')
},
warning: {
color: 'warning' as const,
icon: AlertTriangle,
label: t('equipment.status.warning', 'Warning')
},
maintenance: {
color: 'info' as const,
icon: Wrench,
label: t('equipment.status.maintenance', 'Maintenance')
},
down: {
color: 'error' as const,
icon: AlertTriangle,
label: t('equipment.status.down', 'Down')
}
};
return configs[status];
};
const getEquipmentIcon = (type: EquipmentStatus['type']) => {
const icons = {
oven: Thermometer,
mixer: Activity,
proofer: Settings,
freezer: Zap,
packaging: Settings
};
return icons[type] || Settings;
};
const summary = useMemo(() => {
const total = equipmentData.length;
const operational = equipmentData.filter(e => e.status === 'operational').length;
const warning = equipmentData.filter(e => e.status === 'warning').length;
const maintenance = equipmentData.filter(e => e.status === 'maintenance').length;
const down = equipmentData.filter(e => e.status === 'down').length;
const avgEfficiency = equipmentData.reduce((sum, e) => sum + e.efficiency, 0) / total;
const avgUptime = equipmentData.reduce((sum, e) => sum + e.uptime, 0) / total;
const totalEnergyUsage = equipmentData.reduce((sum, e) => sum + e.energyUsage, 0);
return {
total,
operational,
warning,
maintenance,
down,
avgEfficiency,
avgUptime,
totalEnergyUsage
};
}, [equipmentData]);
if (isLoading) {
return (
<Card className={className}>
<CardHeader>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('equipment.title', 'Equipment Status')}
</h3>
</CardHeader>
<CardBody>
<div className="animate-pulse space-y-4">
<div className="grid grid-cols-3 gap-4">
<div className="h-12 bg-[var(--bg-secondary)] rounded"></div>
<div className="h-12 bg-[var(--bg-secondary)] rounded"></div>
<div className="h-12 bg-[var(--bg-secondary)] rounded"></div>
</div>
<div className="space-y-3">
<div className="h-16 bg-[var(--bg-secondary)] rounded"></div>
<div className="h-16 bg-[var(--bg-secondary)] rounded"></div>
</div>
</div>
</CardBody>
</Card>
);
}
if (error) {
return (
<Card className={className}>
<CardBody className="text-center py-8">
<AlertTriangle className="w-12 h-12 text-red-500 mx-auto mb-4" />
<p className="text-[var(--text-secondary)]">
{t('equipment.error', 'Error loading equipment data')}
</p>
</CardBody>
</Card>
);
}
return (
<Card className={className}>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('equipment.title', 'Equipment Status')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('equipment.subtitle', 'Monitor equipment health and performance')}
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={onViewAll}
>
{t('common.view_all', 'View All')}
</Button>
</div>
</CardHeader>
<CardBody className="space-y-6">
{/* Summary Stats */}
<div className="grid grid-cols-3 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-green-600">
{summary.operational}
</div>
<div className="text-sm text-[var(--text-secondary)]">
{t('equipment.operational', 'Operational')}
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-orange-600">
{summary.warning + summary.maintenance}
</div>
<div className="text-sm text-[var(--text-secondary)]">
{t('equipment.needs_attention', 'Needs Attention')}
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-[var(--color-primary)]">
{summary.avgEfficiency.toFixed(1)}%
</div>
<div className="text-sm text-[var(--text-secondary)]">
{t('equipment.efficiency', 'Efficiency')}
</div>
</div>
</div>
{/* Equipment List */}
<div className="space-y-3">
{equipmentData.slice(0, 4).map((item) => {
const statusConfig = getStatusConfig(item.status);
const EquipmentIcon = getEquipmentIcon(item.type);
const StatusIcon = statusConfig.icon;
return (
<div
key={item.id}
className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors cursor-pointer"
onClick={() => onViewEquipment?.(item.id)}
>
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-2">
<EquipmentIcon className="w-5 h-5 text-[var(--text-tertiary)]" />
<StatusIcon className={`w-4 h-4 ${
item.status === 'operational' ? 'text-green-500' :
item.status === 'warning' ? 'text-orange-500' :
item.status === 'maintenance' ? 'text-blue-500' :
'text-red-500'
}`} />
</div>
<div>
<div className="font-medium text-[var(--text-primary)] text-sm">
{item.name}
</div>
<div className="text-xs text-[var(--text-secondary)]">
{item.location}
{item.temperature && (
<span className="ml-2">
{item.temperature}°C
</span>
)}
</div>
</div>
</div>
<div className="flex items-center space-x-3">
<div className="text-right">
<div className="text-sm font-medium text-[var(--text-primary)]">
{item.efficiency}%
</div>
<div className="text-xs text-[var(--text-secondary)]">
{t('equipment.efficiency', 'Efficiency')}
</div>
</div>
<Badge variant={statusConfig.color}>
{statusConfig.label}
</Badge>
</div>
</div>
);
})}
</div>
{/* Key Metrics */}
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-[var(--border-primary)]">
<div className="text-center">
<div className="flex items-center justify-center space-x-2 mb-1">
<TrendingUp className="w-4 h-4 text-[var(--color-primary)]" />
<span className="text-lg font-bold text-[var(--text-primary)]">
{summary.avgUptime.toFixed(1)}%
</span>
</div>
<div className="text-sm text-[var(--text-secondary)]">
{t('equipment.uptime', 'Average Uptime')}
</div>
</div>
<div className="text-center">
<div className="flex items-center justify-center space-x-2 mb-1">
<Zap className="w-4 h-4 text-yellow-500" />
<span className="text-lg font-bold text-[var(--text-primary)]">
{summary.totalEnergyUsage.toFixed(1)} kW
</span>
</div>
<div className="text-sm text-[var(--text-secondary)]">
{t('equipment.energy_usage', 'Energy Usage')}
</div>
</div>
</div>
{/* Alerts */}
{equipmentData.some(e => e.alerts.length > 0) && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-[var(--text-primary)]">
{t('equipment.alerts', 'Recent Alerts')}
</h4>
{equipmentData
.filter(e => e.alerts.length > 0)
.slice(0, 3)
.map((item) => (
<div key={item.id} className="flex items-start space-x-2 p-2 bg-orange-50 dark:bg-orange-900/20 rounded border-l-2 border-orange-500">
<AlertTriangle className="w-4 h-4 text-orange-500 mt-0.5 flex-shrink-0" />
<div>
<div className="text-sm font-medium text-[var(--text-primary)]">
{item.name}
</div>
<div className="text-xs text-[var(--text-secondary)]">
{item.alerts[0]}
</div>
</div>
</div>
))}
</div>
)}
</CardBody>
</Card>
);
};
export default EquipmentStatusWidget;

View File

@@ -0,0 +1,322 @@
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardHeader, CardBody } from '../../ui/Card';
import { StatsGrid } from '../../ui/Stats';
import { Badge } from '../../ui/Badge';
import { Button } from '../../ui/Button';
import {
Euro,
TrendingUp,
TrendingDown,
AlertTriangle,
Target,
Clock,
Activity,
Zap
} from 'lucide-react';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { useProductionDashboard } from '../../../api';
export interface ProductionCostData {
totalCost: number;
laborCost: number;
materialCost: number;
overheadCost: number;
energyCost: number;
costPerUnit: number;
budget: number;
variance: number;
trend: {
direction: 'up' | 'down' | 'stable';
percentage: number;
};
}
export interface ProductionCostMonitorProps {
className?: string;
data?: ProductionCostData;
onViewDetails?: () => void;
onOptimize?: () => void;
}
const ProductionCostMonitor: React.FC<ProductionCostMonitorProps> = ({
className,
data,
onViewDetails,
onOptimize
}) => {
const { t } = useTranslation();
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
const {
data: dashboardData,
isLoading,
error
} = useProductionDashboard(tenantId);
const costData = useMemo((): ProductionCostData => {
if (data) return data;
// Calculate from dashboard data if available
if (dashboardData) {
const mockData: ProductionCostData = {
totalCost: 2840.50,
laborCost: 1200.00,
materialCost: 1450.00,
overheadCost: 190.50,
energyCost: 95.25,
costPerUnit: 14.20,
budget: 3000.00,
variance: -159.50,
trend: {
direction: 'down',
percentage: 5.3
}
};
return mockData;
}
// Default mock data
return {
totalCost: 2840.50,
laborCost: 1200.00,
materialCost: 1450.00,
overheadCost: 190.50,
energyCost: 95.25,
costPerUnit: 14.20,
budget: 3000.00,
variance: -159.50,
trend: {
direction: 'down',
percentage: 5.3
}
};
}, [data, dashboardData]);
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('es-ES', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 2
}).format(amount);
};
const getVarianceColor = (variance: number) => {
if (variance > 0) return 'text-red-600';
if (variance < -50) return 'text-green-600';
return 'text-yellow-600';
};
const getBudgetStatus = () => {
const percentage = (costData.totalCost / costData.budget) * 100;
if (percentage > 100) return { color: 'error', label: t('production.cost.over_budget', 'Over Budget') };
if (percentage > 90) return { color: 'warning', label: t('production.cost.near_budget', 'Near Budget') };
return { color: 'success', label: t('production.cost.on_budget', 'On Budget') };
};
const budgetStatus = getBudgetStatus();
if (isLoading) {
return (
<Card className={className}>
<CardHeader>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('production.cost.title', 'Production Cost Monitor')}
</h3>
</CardHeader>
<CardBody>
<div className="animate-pulse space-y-4">
<div className="h-20 bg-[var(--bg-secondary)] rounded"></div>
<div className="grid grid-cols-2 gap-4">
<div className="h-16 bg-[var(--bg-secondary)] rounded"></div>
<div className="h-16 bg-[var(--bg-secondary)] rounded"></div>
</div>
</div>
</CardBody>
</Card>
);
}
if (error) {
return (
<Card className={className}>
<CardBody className="text-center py-8">
<AlertTriangle className="w-12 h-12 text-red-500 mx-auto mb-4" />
<p className="text-[var(--text-secondary)]">
{t('production.cost.error', 'Error loading cost data')}
</p>
</CardBody>
</Card>
);
}
const costStats = [
{
title: t('production.cost.total_cost', 'Total Cost'),
value: formatCurrency(costData.totalCost),
icon: Euro,
variant: budgetStatus.color as 'success' | 'warning' | 'error',
trend: {
value: costData.trend.percentage,
direction: costData.trend.direction === 'up' ? 'up' as const : 'down' as const,
label: t('production.cost.vs_yesterday', 'vs yesterday')
},
subtitle: `${formatCurrency(Math.abs(costData.variance))} ${costData.variance > 0 ? t('common.over', 'over') : t('common.under', 'under')} ${t('common.budget', 'budget')}`
},
{
title: t('production.cost.cost_per_unit', 'Cost per Unit'),
value: formatCurrency(costData.costPerUnit),
icon: Target,
variant: 'info' as const,
subtitle: t('production.cost.average_today', 'Average today')
}
];
const costBreakdown = [
{
label: t('production.cost.labor', 'Labor'),
amount: costData.laborCost,
percentage: (costData.laborCost / costData.totalCost) * 100,
icon: Clock,
color: 'bg-blue-500'
},
{
label: t('production.cost.materials', 'Materials'),
amount: costData.materialCost,
percentage: (costData.materialCost / costData.totalCost) * 100,
icon: Activity,
color: 'bg-green-500'
},
{
label: t('production.cost.overhead', 'Overhead'),
amount: costData.overheadCost,
percentage: (costData.overheadCost / costData.totalCost) * 100,
icon: Euro,
color: 'bg-orange-500'
},
{
label: t('production.cost.energy', 'Energy'),
amount: costData.energyCost,
percentage: (costData.energyCost / costData.totalCost) * 100,
icon: Zap,
color: 'bg-yellow-500'
}
];
return (
<Card className={className}>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('production.cost.title', 'Production Cost Monitor')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('production.cost.subtitle', 'Track and optimize production costs')}
</p>
</div>
<Badge variant={budgetStatus.color as any}>
{budgetStatus.label}
</Badge>
</div>
</CardHeader>
<CardBody className="space-y-6">
{/* Key Metrics */}
<StatsGrid
stats={costStats}
columns={2}
gap="md"
/>
{/* Cost Breakdown */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-4">
{t('production.cost.breakdown', 'Cost Breakdown')}
</h4>
<div className="space-y-3">
{costBreakdown.map((item, index) => {
const Icon = item.icon;
return (
<div key={index} className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className={`w-3 h-3 rounded-full ${item.color}`}></div>
<Icon className="w-4 h-4 text-[var(--text-tertiary)]" />
<span className="text-sm text-[var(--text-primary)]">
{item.label}
</span>
</div>
<div className="flex items-center space-x-3">
<span className="text-sm font-medium text-[var(--text-primary)]">
{formatCurrency(item.amount)}
</span>
<span className="text-xs text-[var(--text-tertiary)] w-10 text-right">
{item.percentage.toFixed(1)}%
</span>
</div>
</div>
);
})}
</div>
</div>
{/* Budget Progress */}
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-[var(--text-primary)]">
{t('production.cost.budget_usage', 'Budget Usage')}
</span>
<span className="text-sm text-[var(--text-secondary)]">
{formatCurrency(costData.totalCost)} / {formatCurrency(costData.budget)}
</span>
</div>
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
costData.totalCost > costData.budget
? 'bg-red-500'
: costData.totalCost > costData.budget * 0.9
? 'bg-yellow-500'
: 'bg-green-500'
}`}
style={{
width: `${Math.min(100, (costData.totalCost / costData.budget) * 100)}%`
}}
></div>
</div>
<div className="flex items-center justify-between mt-2">
<span className={`text-xs font-medium ${getVarianceColor(costData.variance)}`}>
{costData.variance > 0 ? '+' : ''}{formatCurrency(costData.variance)}
</span>
<span className="text-xs text-[var(--text-tertiary)]">
{((costData.totalCost / costData.budget) * 100).toFixed(1)}% {t('common.used', 'used')}
</span>
</div>
</div>
{/* Actions */}
<div className="flex space-x-3 pt-4 border-t border-[var(--border-primary)]">
<Button
variant="outline"
size="sm"
onClick={onViewDetails}
className="flex-1"
>
{t('production.cost.view_details', 'View Details')}
</Button>
<Button
variant="primary"
size="sm"
onClick={onOptimize}
className="flex-1"
>
{t('production.cost.optimize', 'Optimize Costs')}
</Button>
</div>
</CardBody>
</Card>
);
};
export default ProductionCostMonitor;

View File

@@ -5,9 +5,12 @@ export { default as RealTimeAlerts } from './RealTimeAlerts';
export { default as ProcurementPlansToday } from './ProcurementPlansToday';
export { default as ProductionPlansToday } from './ProductionPlansToday';
// Note: The following components are specified in the design but not yet implemented:
// - DashboardCard
// - DashboardGrid
// - QuickActions
// - RecentActivity
// - KPIWidget
// Production Management Dashboard Widgets
export { default as ProductionCostMonitor } from './ProductionCostMonitor';
export { default as EquipmentStatusWidget } from './EquipmentStatusWidget';
export { default as AIInsightsWidget } from './AIInsightsWidget';
// Types
export type { ProductionCostData } from './ProductionCostMonitor';
export type { EquipmentStatus } from './EquipmentStatusWidget';
export type { AIInsight } from './AIInsightsWidget';