Delete legacy alerts

This commit is contained in:
Urtzi Alfaro
2025-08-22 15:31:52 +02:00
parent c6dd6fd1de
commit 90100a66c6
40 changed files with 25 additions and 3308 deletions

View File

@@ -21,12 +21,12 @@ const InventoryDashboardWidget: React.FC<InventoryDashboardWidgetProps> = ({
onViewInventory,
className = ''
}) => {
const { dashboardData, alerts, isLoading, error, refresh } = useInventoryDashboard();
const { dashboardData, isLoading, error, refresh } = useInventoryDashboard();
// Get alert counts
const criticalAlerts = alerts.filter(a => !a.is_acknowledged && a.severity === 'critical').length;
const lowStockAlerts = alerts.filter(a => !a.is_acknowledged && a.alert_type === 'low_stock').length;
const expiringAlerts = alerts.filter(a => !a.is_acknowledged && a.alert_type === 'expiring_soon').length;
const criticalAlerts = 0;
const lowStockAlerts = 0;
const expiringAlerts = 0;
if (isLoading) {
return (

View File

@@ -1,359 +0,0 @@
import React, { useState } from 'react';
import {
AlertTriangle,
Clock,
Package,
TrendingDown,
CheckCircle,
X,
Filter,
Bell,
BellOff,
Calendar
} from 'lucide-react';
import { StockAlert } from '../../api/services/inventory.service';
interface StockAlertsPanelProps {
alerts: StockAlert[];
onAcknowledge?: (alertId: string) => void;
onAcknowledgeAll?: (alertIds: string[]) => void;
onViewItem?: (itemId: string) => void;
className?: string;
}
type AlertFilter = 'all' | 'unacknowledged' | 'low_stock' | 'expired' | 'expiring_soon';
const StockAlertsPanel: React.FC<StockAlertsPanelProps> = ({
alerts,
onAcknowledge,
onAcknowledgeAll,
onViewItem,
className = ''
}) => {
const [filter, setFilter] = useState<AlertFilter>('all');
const [selectedAlerts, setSelectedAlerts] = useState<Set<string>>(new Set());
// Filter alerts based on current filter
const filteredAlerts = alerts.filter(alert => {
switch (filter) {
case 'unacknowledged':
return !alert.is_acknowledged;
case 'low_stock':
return alert.alert_type === 'low_stock';
case 'expired':
return alert.alert_type === 'expired';
case 'expiring_soon':
return alert.alert_type === 'expiring_soon';
default:
return true;
}
});
// Get alert icon
const getAlertIcon = (alert: StockAlert) => {
switch (alert.alert_type) {
case 'low_stock':
return <TrendingDown className="w-5 h-5" />;
case 'expired':
return <X className="w-5 h-5" />;
case 'expiring_soon':
return <Clock className="w-5 h-5" />;
case 'overstock':
return <Package className="w-5 h-5" />;
default:
return <AlertTriangle className="w-5 h-5" />;
}
};
// Get alert color classes
const getAlertClasses = (alert: StockAlert) => {
const baseClasses = 'border-l-4';
if (alert.is_acknowledged) {
return `${baseClasses} border-gray-300 bg-gray-50`;
}
switch (alert.severity) {
case 'critical':
return `${baseClasses} border-red-500 bg-red-50`;
case 'high':
return `${baseClasses} border-orange-500 bg-orange-50`;
case 'medium':
return `${baseClasses} border-yellow-500 bg-yellow-50`;
case 'low':
return `${baseClasses} border-blue-500 bg-blue-50`;
default:
return `${baseClasses} border-gray-500 bg-gray-50`;
}
};
// Get alert text color
const getAlertTextColor = (alert: StockAlert) => {
if (alert.is_acknowledged) {
return 'text-gray-600';
}
switch (alert.severity) {
case 'critical':
return 'text-red-700';
case 'high':
return 'text-orange-700';
case 'medium':
return 'text-yellow-700';
case 'low':
return 'text-blue-700';
default:
return 'text-gray-700';
}
};
// Get alert icon color
const getAlertIconColor = (alert: StockAlert) => {
if (alert.is_acknowledged) {
return 'text-gray-400';
}
switch (alert.severity) {
case 'critical':
return 'text-red-500';
case 'high':
return 'text-orange-500';
case 'medium':
return 'text-yellow-500';
case 'low':
return 'text-blue-500';
default:
return 'text-gray-500';
}
};
// Handle alert selection
const toggleAlertSelection = (alertId: string) => {
const newSelection = new Set(selectedAlerts);
if (newSelection.has(alertId)) {
newSelection.delete(alertId);
} else {
newSelection.add(alertId);
}
setSelectedAlerts(newSelection);
};
// Handle acknowledge all selected
const handleAcknowledgeSelected = () => {
if (onAcknowledgeAll && selectedAlerts.size > 0) {
onAcknowledgeAll(Array.from(selectedAlerts));
setSelectedAlerts(new Set());
}
};
// Format time ago
const formatTimeAgo = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
const diffInHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));
if (diffInHours < 1) {
return 'Hace menos de 1 hora';
} else if (diffInHours < 24) {
return `Hace ${diffInHours} horas`;
} else {
const diffInDays = Math.floor(diffInHours / 24);
return `Hace ${diffInDays} días`;
}
};
// Get filter counts
const getFilterCounts = () => {
return {
all: alerts.length,
unacknowledged: alerts.filter(a => !a.is_acknowledged).length,
low_stock: alerts.filter(a => a.alert_type === 'low_stock').length,
expired: alerts.filter(a => a.alert_type === 'expired').length,
expiring_soon: alerts.filter(a => a.alert_type === 'expiring_soon').length,
};
};
const filterCounts = getFilterCounts();
return (
<div className={`bg-white rounded-xl shadow-sm border ${className}`}>
{/* Header */}
<div className="p-6 border-b">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<Bell className="w-5 h-5 text-gray-600" />
<h2 className="text-lg font-semibold text-gray-900">Alertas de Stock</h2>
{filterCounts.unacknowledged > 0 && (
<span className="bg-red-100 text-red-800 text-xs px-2 py-1 rounded-full">
{filterCounts.unacknowledged} pendientes
</span>
)}
</div>
{selectedAlerts.size > 0 && (
<button
onClick={handleAcknowledgeSelected}
className="flex items-center space-x-1 px-3 py-1 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm"
>
<CheckCircle className="w-4 h-4" />
<span>Confirmar ({selectedAlerts.size})</span>
</button>
)}
</div>
{/* Filters */}
<div className="flex flex-wrap gap-2">
{[
{ key: 'all', label: 'Todas', count: filterCounts.all },
{ key: 'unacknowledged', label: 'Pendientes', count: filterCounts.unacknowledged },
{ key: 'low_stock', label: 'Stock Bajo', count: filterCounts.low_stock },
{ key: 'expired', label: 'Vencidas', count: filterCounts.expired },
{ key: 'expiring_soon', label: 'Por Vencer', count: filterCounts.expiring_soon },
].map(({ key, label, count }) => (
<button
key={key}
onClick={() => setFilter(key as AlertFilter)}
className={`px-3 py-1 rounded-full text-sm font-medium transition-colors ${
filter === key
? 'bg-blue-100 text-blue-800'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{label} ({count})
</button>
))}
</div>
</div>
{/* Alerts List */}
<div className="divide-y">
{filteredAlerts.length === 0 ? (
<div className="p-8 text-center">
<BellOff className="w-12 h-12 text-gray-400 mx-auto mb-3" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
{filter === 'all' ? 'No hay alertas' : 'No hay alertas con este filtro'}
</h3>
<p className="text-gray-500">
{filter === 'all'
? 'Tu inventario está en buen estado'
: 'Prueba con un filtro diferente'
}
</p>
</div>
) : (
filteredAlerts.map((alert) => (
<div
key={alert.id}
className={`p-4 hover:bg-gray-50 transition-colors ${getAlertClasses(alert)}`}
>
<div className="flex items-start space-x-3">
{/* Selection checkbox */}
{!alert.is_acknowledged && (
<input
type="checkbox"
checked={selectedAlerts.has(alert.id)}
onChange={() => toggleAlertSelection(alert.id)}
className="mt-1 w-4 h-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"
/>
)}
{/* Alert Icon */}
<div className={`mt-0.5 ${getAlertIconColor(alert)}`}>
{getAlertIcon(alert)}
</div>
{/* Alert Content */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between">
<div>
<h4 className={`font-medium ${getAlertTextColor(alert)}`}>
{alert.item?.name || 'Producto desconocido'}
</h4>
<p className={`text-sm mt-1 ${getAlertTextColor(alert)}`}>
{alert.message}
</p>
{/* Additional Info */}
<div className="flex items-center space-x-4 mt-2 text-xs text-gray-500">
<div className="flex items-center space-x-1">
<Calendar className="w-3 h-3" />
<span>{formatTimeAgo(alert.created_at)}</span>
</div>
{alert.threshold_value && alert.current_value && (
<span>
Umbral: {alert.threshold_value} | Actual: {alert.current_value}
</span>
)}
<span className="capitalize">
Severidad: {alert.severity}
</span>
</div>
{/* Acknowledged Info */}
{alert.is_acknowledged && alert.acknowledged_at && (
<div className="mt-2 text-xs text-gray-500">
<span> Confirmada {formatTimeAgo(alert.acknowledged_at)}</span>
</div>
)}
</div>
{/* Actions */}
<div className="flex items-center space-x-2 ml-4">
{onViewItem && alert.item_id && (
<button
onClick={() => onViewItem(alert.item_id)}
className="text-blue-600 hover:text-blue-800 text-sm font-medium"
>
Ver producto
</button>
)}
{!alert.is_acknowledged && onAcknowledge && (
<button
onClick={() => onAcknowledge(alert.id)}
className="text-green-600 hover:text-green-800 text-sm font-medium"
>
Confirmar
</button>
)}
</div>
</div>
</div>
</div>
</div>
))
)}
</div>
{/* Footer with bulk actions */}
{filteredAlerts.length > 0 && filterCounts.unacknowledged > 0 && (
<div className="p-4 border-t bg-gray-50">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">
{filterCounts.unacknowledged} alertas pendientes
</span>
<button
onClick={() => {
if (onAcknowledgeAll) {
const unacknowledgedIds = alerts
.filter(a => !a.is_acknowledged)
.map(a => a.id);
onAcknowledgeAll(unacknowledgedIds);
}
}}
className="text-blue-600 hover:text-blue-800 text-sm font-medium"
>
Confirmar todas
</button>
</div>
</div>
)}
</div>
);
};
export default StockAlertsPanel;

View File

@@ -1,180 +0,0 @@
import React from 'react';
import { AlertTriangle, Cloud, Package, Clock, ChevronRight } from 'lucide-react';
export interface Alert {
id: string;
type: 'stock' | 'weather' | 'order' | 'production' | 'system';
severity: 'high' | 'medium' | 'low';
title: string;
description: string;
action?: string;
time?: string;
}
interface CriticalAlertsProps {
alerts?: Alert[];
onAlertClick?: (alertId: string) => void;
className?: string;
}
const CriticalAlerts: React.FC<CriticalAlertsProps> = ({
alerts = [
{
id: '1',
type: 'stock',
severity: 'high',
title: 'Stock Bajo',
description: 'Pan integral: solo 5 unidades',
action: 'Hacer más',
time: '10:30'
},
{
id: '2',
type: 'weather',
severity: 'medium',
title: 'Lluvia Esperada',
description: 'Precipitaciones 14:00 - 17:00',
action: 'Ajustar producción',
time: '14:00'
},
{
id: '3',
type: 'order',
severity: 'low',
title: 'Pedido Especial',
description: 'Tarta de cumpleaños Ana - Viernes',
action: 'Ver detalles',
time: 'Viernes'
}
],
onAlertClick,
className = ''
}) => {
const getAlertIcon = (type: Alert['type']) => {
switch (type) {
case 'stock': return Package;
case 'weather': return Cloud;
case 'order': return Clock;
case 'production': return AlertTriangle;
default: return AlertTriangle;
}
};
const getAlertColors = (severity: Alert['severity']) => {
switch (severity) {
case 'high': return {
bg: 'bg-red-50 border-red-200',
icon: 'text-red-600',
title: 'text-red-900',
description: 'text-red-700'
};
case 'medium': return {
bg: 'bg-yellow-50 border-yellow-200',
icon: 'text-yellow-600',
title: 'text-yellow-900',
description: 'text-yellow-700'
};
case 'low': return {
bg: 'bg-blue-50 border-blue-200',
icon: 'text-blue-600',
title: 'text-blue-900',
description: 'text-blue-700'
};
}
};
const visibleAlerts = alerts.slice(0, 3); // Show max 3 alerts
return (
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-6 ${className}`}>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
<AlertTriangle className="h-5 w-5 mr-2 text-orange-600" />
Atención Requerida
</h3>
{alerts.length > 3 && (
<span className="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded-full">
+{alerts.length - 3} más
</span>
)}
</div>
{/* Alerts List */}
{visibleAlerts.length > 0 ? (
<div className="space-y-3">
{visibleAlerts.map((alert) => {
const IconComponent = getAlertIcon(alert.type);
const colors = getAlertColors(alert.severity);
return (
<div
key={alert.id}
onClick={() => onAlertClick?.(alert.id)}
className={`${colors.bg} border rounded-lg p-3 cursor-pointer hover:shadow-sm transition-all duration-200`}
>
<div className="flex items-start space-x-3">
<IconComponent className={`h-4 w-4 mt-0.5 ${colors.icon} flex-shrink-0`} />
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className={`text-sm font-medium ${colors.title}`}>
{alert.title}
</h4>
<p className={`text-xs ${colors.description} mt-1`}>
{alert.description}
</p>
{alert.action && (
<p className={`text-xs ${colors.icon} font-medium mt-1`}>
{alert.action}
</p>
)}
</div>
<div className="flex items-center space-x-2 ml-3">
{alert.time && (
<span className={`text-xs ${colors.description}`}>
{alert.time}
</span>
)}
<ChevronRight className={`h-3 w-3 ${colors.icon}`} />
</div>
</div>
</div>
</div>
</div>
);
})}
</div>
) : (
<div className="text-center py-6">
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-3">
<AlertTriangle className="h-6 w-6 text-green-600" />
</div>
<h4 className="text-sm font-medium text-gray-900">Todo bajo control</h4>
<p className="text-xs text-gray-500 mt-1">No hay alertas que requieran atención</p>
</div>
)}
{/* Quick Summary */}
<div className="flex items-center justify-between pt-4 mt-4 border-t border-gray-100">
<div className="flex items-center space-x-4 text-xs text-gray-500">
<span className="flex items-center">
<div className="w-2 h-2 bg-red-500 rounded-full mr-1"></div>
{alerts.filter(a => a.severity === 'high').length} Urgentes
</span>
<span className="flex items-center">
<div className="w-2 h-2 bg-yellow-500 rounded-full mr-1"></div>
{alerts.filter(a => a.severity === 'medium').length} Importantes
</span>
</div>
<button className="text-xs text-gray-600 hover:text-gray-900 font-medium">
Ver todas
</button>
</div>
</div>
);
};
export default CriticalAlerts;

View File

@@ -1,4 +1,3 @@
export { default as CriticalAlerts } from './CriticalAlerts';
export { default as OrderSuggestions } from './OrderSuggestions';
export { default as QuickActions } from './QuickActions';
export { default as QuickOverview } from './QuickOverview';

View File

@@ -1,155 +0,0 @@
import React from 'react';
import { AlertTriangle, TrendingDown, Package, Clock, Euro } from 'lucide-react';
export interface BusinessAlert {
id: string;
type: 'stockout_risk' | 'overstock' | 'revenue_loss' | 'quality_risk' | 'weather_impact';
severity: 'low' | 'medium' | 'high' | 'critical';
product: string;
message: string;
action: string;
impact?: {
type: 'revenue' | 'units' | 'percentage';
value: number;
currency?: string;
};
urgency?: 'immediate' | 'today' | 'this_week';
}
interface AlertCardProps {
alert: BusinessAlert;
onAction?: (alertId: string, actionType: string) => void;
}
const getSeverityConfig = (severity: BusinessAlert['severity']) => {
switch (severity) {
case 'critical':
return {
bgColor: 'bg-red-50',
borderColor: 'border-red-200',
iconColor: 'text-red-600',
textColor: 'text-red-900',
actionColor: 'bg-red-100 hover:bg-red-200 text-red-800'
};
case 'high':
return {
bgColor: 'bg-orange-50',
borderColor: 'border-orange-200',
iconColor: 'text-orange-600',
textColor: 'text-orange-900',
actionColor: 'bg-orange-100 hover:bg-orange-200 text-orange-800'
};
case 'medium':
return {
bgColor: 'bg-yellow-50',
borderColor: 'border-yellow-200',
iconColor: 'text-yellow-600',
textColor: 'text-yellow-900',
actionColor: 'bg-yellow-100 hover:bg-yellow-200 text-yellow-800'
};
default:
return {
bgColor: 'bg-blue-50',
borderColor: 'border-blue-200',
iconColor: 'text-blue-600',
textColor: 'text-blue-900',
actionColor: 'bg-blue-100 hover:bg-blue-200 text-blue-800'
};
}
};
const getAlertIcon = (type: BusinessAlert['type']) => {
switch (type) {
case 'stockout_risk':
return Package;
case 'overstock':
return TrendingDown;
case 'revenue_loss':
return Euro;
case 'quality_risk':
return Clock;
case 'weather_impact':
return AlertTriangle;
default:
return AlertTriangle;
}
};
const getUrgencyLabel = (urgency?: BusinessAlert['urgency']) => {
switch (urgency) {
case 'immediate':
return { label: 'URGENTE', color: 'bg-red-100 text-red-800' };
case 'today':
return { label: 'HOY', color: 'bg-orange-100 text-orange-800' };
case 'this_week':
return { label: 'ESTA SEMANA', color: 'bg-blue-100 text-blue-800' };
default:
return null;
}
};
const AlertCard: React.FC<AlertCardProps> = ({ alert, onAction }) => {
const config = getSeverityConfig(alert.severity);
const Icon = getAlertIcon(alert.type);
const urgencyInfo = getUrgencyLabel(alert.urgency);
const handleAction = () => {
onAction?.(alert.id, 'primary_action');
};
return (
<div className={`${config.bgColor} ${config.borderColor} border rounded-lg p-4 shadow-sm`}>
<div className="flex items-start space-x-3">
<div className={`${config.iconColor} mt-0.5`}>
<Icon className="h-5 w-5" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center space-x-2">
<h4 className={`font-medium ${config.textColor}`}>
{alert.product}
</h4>
{urgencyInfo && (
<span className={`px-2 py-1 text-xs font-bold rounded ${urgencyInfo.color}`}>
{urgencyInfo.label}
</span>
)}
</div>
<p className={`text-sm mt-1 ${config.textColor.replace('900', '700')}`}>
{alert.message}
</p>
{alert.impact && (
<div className={`text-sm font-medium mt-2 ${config.textColor}`}>
{alert.impact.type === 'revenue' && (
<>Impacto: -{alert.impact.value}{alert.impact.currency || '€'}</>
)}
{alert.impact.type === 'units' && (
<>Unidades afectadas: {alert.impact.value}</>
)}
{alert.impact.type === 'percentage' && (
<>Reducción estimada: {alert.impact.value}%</>
)}
</div>
)}
</div>
</div>
<div className="mt-3">
<button
onClick={handleAction}
className={`px-3 py-2 text-sm font-medium rounded transition-colors ${config.actionColor}`}
>
{alert.action}
</button>
</div>
</div>
</div>
</div>
);
};
export default AlertCard;