Files
bakery-ia/fdev-ffrontend/src/components/alerts/AlertCard.tsx
2025-08-28 10:41:04 +02:00

304 lines
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// frontend/src/components/alerts/AlertCard.tsx
/**
* Individual alert/recommendation card component
* Displays alert details with appropriate styling and actions
*/
import React, { useState } from 'react';
import { AlertItem, ItemSeverity, ItemType } from '../../types/alerts';
import { formatDistanceToNow } from 'date-fns';
import { es } from 'date-fns/locale';
interface AlertCardProps {
item: AlertItem;
onAcknowledge: (itemId: string) => void;
onResolve: (itemId: string) => void;
compact?: boolean;
showActions?: boolean;
}
const getSeverityConfig = (severity: ItemSeverity, itemType: ItemType) => {
if (itemType === 'recommendation') {
switch (severity) {
case 'high':
return {
color: 'bg-blue-50 border-blue-200 text-blue-900',
icon: '💡',
badge: 'bg-blue-100 text-blue-800'
};
case 'medium':
return {
color: 'bg-blue-50 border-blue-100 text-blue-800',
icon: '💡',
badge: 'bg-blue-50 text-blue-600'
};
case 'low':
return {
color: 'bg-gray-50 border-gray-200 text-gray-700',
icon: '💡',
badge: 'bg-gray-100 text-gray-600'
};
default:
return {
color: 'bg-blue-50 border-blue-200 text-blue-900',
icon: '💡',
badge: 'bg-blue-100 text-blue-800'
};
}
} else {
switch (severity) {
case 'urgent':
return {
color: 'bg-red-50 border-red-300 text-red-900',
icon: '🚨',
badge: 'bg-red-100 text-red-800',
pulse: true
};
case 'high':
return {
color: 'bg-orange-50 border-orange-200 text-orange-900',
icon: '⚠️',
badge: 'bg-orange-100 text-orange-800'
};
case 'medium':
return {
color: 'bg-yellow-50 border-yellow-200 text-yellow-900',
icon: '🔔',
badge: 'bg-yellow-100 text-yellow-800'
};
case 'low':
return {
color: 'bg-green-50 border-green-200 text-green-900',
icon: '',
badge: 'bg-green-100 text-green-800'
};
default:
return {
color: 'bg-gray-50 border-gray-200 text-gray-700',
icon: '📋',
badge: 'bg-gray-100 text-gray-600'
};
}
}
};
const getStatusConfig = (status: string) => {
switch (status) {
case 'acknowledged':
return {
color: 'bg-blue-100 text-blue-800',
label: 'Reconocido'
};
case 'resolved':
return {
color: 'bg-green-100 text-green-800',
label: 'Resuelto'
};
default:
return {
color: 'bg-gray-100 text-gray-800',
label: 'Activo'
};
}
};
export const AlertCard: React.FC<AlertCardProps> = ({
item,
onAcknowledge,
onResolve,
compact = false,
showActions = true
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const [actionLoading, setActionLoading] = useState<string | null>(null);
const severityConfig = getSeverityConfig(item.severity, item.item_type);
const statusConfig = getStatusConfig(item.status);
const handleAction = async (action: () => void, actionType: string) => {
setActionLoading(actionType);
try {
await action();
} finally {
setActionLoading(null);
}
};
const timeAgo = formatDistanceToNow(new Date(item.timestamp), {
addSuffix: true,
locale: es
});
return (
<div className={`
rounded-lg border-2 transition-all duration-200 hover:shadow-md
${severityConfig.color}
${severityConfig.pulse ? 'animate-pulse' : ''}
${item.status !== 'active' ? 'opacity-75' : ''}
`}>
{/* Header */}
<div className="p-4">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-3 flex-1 min-w-0">
{/* Icon and Type Badge */}
<div className="flex-shrink-0">
<span className="text-2xl">{severityConfig.icon}</span>
</div>
<div className="flex-1 min-w-0">
{/* Title and Badges */}
<div className="flex items-start justify-between mb-2">
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold truncate">
{item.title}
</h3>
<div className="flex items-center space-x-2 mt-1">
<span className={`
inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
${severityConfig.badge}
`}>
{item.item_type === 'alert' ? 'Alerta' : 'Recomendación'} - {item.severity}
</span>
<span className={`
inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
${statusConfig.color}
`}>
{statusConfig.label}
</span>
<span className="text-xs text-gray-500">
{item.service}
</span>
</div>
</div>
{/* Expand Button */}
{!compact && (
<button
onClick={() => setIsExpanded(!isExpanded)}
className="ml-2 text-gray-400 hover:text-gray-600 transition-colors"
>
<svg
className={`w-5 h-5 transform transition-transform ${
isExpanded ? 'rotate-180' : ''
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
)}
</div>
{/* Message */}
<p className={`text-sm ${compact ? 'line-clamp-2' : ''}`}>
{item.message}
</p>
{/* Timestamp */}
<p className="text-xs text-gray-500 mt-2">
{timeAgo} {new Date(item.timestamp).toLocaleString('es-ES')}
</p>
</div>
</div>
</div>
{/* Quick Actions */}
{showActions && item.status === 'active' && (
<div className="flex items-center space-x-2 mt-3">
<button
onClick={() => handleAction(() => onAcknowledge(item.id), 'acknowledge')}
disabled={actionLoading === 'acknowledge'}
className="inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
>
{actionLoading === 'acknowledge' ? (
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-blue-700" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
) : (
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)}
Reconocer
</button>
<button
onClick={() => handleAction(() => onResolve(item.id), 'resolve')}
disabled={actionLoading === 'resolve'}
className="inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-green-700 bg-green-100 hover:bg-green-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50"
>
{actionLoading === 'resolve' ? (
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-green-700" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
) : (
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)}
Resolver
</button>
</div>
)}
</div>
{/* Expanded Details */}
{isExpanded && (
<div className="border-t border-gray-200 px-4 py-3 bg-gray-50 bg-opacity-50">
{/* Actions */}
{item.actions.length > 0 && (
<div className="mb-3">
<h4 className="text-sm font-medium text-gray-700 mb-2">Acciones sugeridas:</h4>
<ul className="list-disc list-inside space-y-1">
{item.actions.map((action, index) => (
<li key={index} className="text-sm text-gray-600">
{action}
</li>
))}
</ul>
</div>
)}
{/* Metadata */}
{Object.keys(item.metadata).length > 0 && (
<div className="mb-3">
<h4 className="text-sm font-medium text-gray-700 mb-2">Detalles técnicos:</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{Object.entries(item.metadata).map(([key, value]) => (
<div key={key} className="text-sm">
<span className="font-medium text-gray-600">{key}:</span>{' '}
<span className="text-gray-800">
{typeof value === 'object' ? JSON.stringify(value) : String(value)}
</span>
</div>
))}
</div>
</div>
)}
{/* Acknowledgment/Resolution Info */}
{(item.acknowledged_at || item.resolved_at) && (
<div className="text-xs text-gray-500 space-y-1">
{item.acknowledged_at && (
<p>
Reconocido: {new Date(item.acknowledged_at).toLocaleString('es-ES')}
{item.acknowledged_by && ` por ${item.acknowledged_by}`}
</p>
)}
{item.resolved_at && (
<p>
Resuelto: {new Date(item.resolved_at).toLocaleString('es-ES')}
{item.resolved_by && ` por ${item.resolved_by}`}
</p>
)}
</div>
)}
</div>
)}
</div>
);
};