Files
bakery-ia/frontend/src/components/dashboard/blocks/AIInsightsBlock.tsx
2025-12-15 21:14:22 +01:00

213 lines
8.9 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.

/**
* AIInsightsBlock - AI Insights Dashboard Block
*
* Displays AI-generated insights for professional/enterprise tiers
* Shows top 2-3 insights with links to full AI Insights page
*/
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Lightbulb, ArrowRight, BarChart2, TrendingUp, TrendingDown, Shield, AlertTriangle } from 'lucide-react';
interface AIInsight {
id: string;
title: string;
description: string;
type: 'cost_optimization' | 'waste_reduction' | 'safety_stock' | 'demand_forecast' | 'risk_alert';
impact: 'high' | 'medium' | 'low';
impact_value?: string;
impact_currency?: string;
created_at: string;
recommendation_actions?: Array<{
label: string;
action: string;
}>;
}
interface AIInsightsBlockProps {
insights: AIInsight[];
loading?: boolean;
onViewAll: () => void;
}
export function AIInsightsBlock({ insights = [], loading = false, onViewAll }: AIInsightsBlockProps) {
const { t } = useTranslation(['dashboard', 'common']);
// Get icon based on insight type
const getInsightIcon = (type: string) => {
switch (type) {
case 'cost_optimization': return <TrendingUp className="w-5 h-5 text-[var(--color-success-600)]" />;
case 'waste_reduction': return <TrendingDown className="w-5 h-5 text-[var(--color-success-600)]" />;
case 'safety_stock': return <Shield className="w-5 h-5 text-[var(--color-info-600)]" />;
case 'demand_forecast': return <BarChart2 className="w-5 h-5 text-[var(--color-primary-600)]" />;
case 'risk_alert': return <AlertTriangle className="w-5 h-5 text-[var(--color-error-600)]" />;
default: return <Lightbulb className="w-5 h-5 text-[var(--color-primary-600)]" />;
}
};
// Get impact color based on level
const getImpactColor = (impact: string) => {
switch (impact) {
case 'high': return 'bg-[var(--color-error-100)] text-[var(--color-error-700)]';
case 'medium': return 'bg-[var(--color-warning-100)] text-[var(--color-warning-700)]';
case 'low': return 'bg-[var(--color-info-100)] text-[var(--color-info-700)]';
default: return 'bg-[var(--bg-secondary)] text-[var(--text-secondary)]';
}
};
// Get impact label
const getImpactLabel = (impact: string) => {
switch (impact) {
case 'high': return t('dashboard:ai_insights.impact_high');
case 'medium': return t('dashboard:ai_insights.impact_medium');
case 'low': return t('dashboard:ai_insights.impact_low');
default: return '';
}
};
if (loading) {
return (
<div className="rounded-xl shadow-lg p-6 border border-[var(--border-primary)] bg-[var(--bg-primary)] animate-pulse">
<div className="flex items-center gap-4 mb-4">
<div className="w-12 h-12 bg-[var(--bg-secondary)] rounded-full"></div>
<div className="flex-1 space-y-2">
<div className="h-5 bg-[var(--bg-secondary)] rounded w-1/3"></div>
<div className="h-4 bg-[var(--bg-secondary)] rounded w-1/4"></div>
</div>
</div>
<div className="space-y-4">
<div className="h-16 bg-[var(--bg-secondary)] rounded"></div>
<div className="h-16 bg-[var(--bg-secondary)] rounded"></div>
</div>
</div>
);
}
// Show top 3 insights
const topInsights = insights.slice(0, 3);
return (
<div className="rounded-xl shadow-lg border border-[var(--border-primary)] bg-[var(--bg-primary)] overflow-hidden">
{/* Header */}
<div className="p-6 pb-4">
<div className="flex items-center gap-4">
{/* Icon */}
<div className="w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0 bg-[var(--color-primary-100)]">
<Lightbulb className="w-6 h-6 text-[var(--color-primary-600)]" />
</div>
{/* Title & Description */}
<div className="flex-1">
<h2 className="text-xl font-bold text-[var(--text-primary)]">
{t('dashboard:ai_insights.title')}
</h2>
<p className="text-sm text-[var(--text-secondary)]">
{t('dashboard:ai_insights.subtitle')}
</p>
</div>
{/* View All Button */}
<button
onClick={onViewAll}
className="flex items-center gap-2 px-3 py-2 rounded-lg border border-[var(--border-primary)] text-[var(--text-primary)] hover:bg-[var(--bg-secondary)] transition-colors text-sm font-medium"
>
<span>{t('dashboard:ai_insights.view_all')}</span>
<ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
{/* Insights List */}
{topInsights.length > 0 ? (
<div className="border-t border-[var(--border-primary)]">
{topInsights.map((insight, index) => (
<div
key={insight.id || index}
className={`p-4 ${index < topInsights.length - 1 ? 'border-b border-[var(--border-primary)]' : ''}`}
>
<div className="flex items-start gap-3">
{/* Icon */}
<div className="flex-shrink-0 mt-1">
{getInsightIcon(insight.type)}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-semibold text-[var(--text-primary)] text-sm">
{insight.title}
</h3>
{/* Impact Badge */}
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${getImpactColor(insight.impact)}`}>
{getImpactLabel(insight.impact)}
</span>
</div>
<p className="text-sm text-[var(--text-secondary)] mb-2">
{insight.description}
</p>
{/* Recommendations */}
{insight.recommendation_actions && insight.recommendation_actions.length > 0 && (
<div className="mb-2 p-2 bg-[var(--color-primary-50)] border border-[var(--color-primary-100)] rounded">
<div className="flex items-start gap-1.5">
<Lightbulb className="w-4 h-4 text-[var(--color-primary-600)] flex-shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-[var(--color-primary-700)] mb-1">
{t('dashboard:ai_insights.recommendations', 'Recomendaciones')}:
</p>
<ul className="space-y-1">
{insight.recommendation_actions.slice(0, 2).map((action, idx) => (
<li key={idx} className="text-xs text-[var(--color-primary-700)] flex items-start gap-1">
<span className="flex-shrink-0"></span>
<span className="flex-1">{action.label || action.action}</span>
</li>
))}
</ul>
</div>
</div>
</div>
)}
{/* Impact Value */}
{insight.impact_value && (
<div className="flex items-center gap-2 mt-2">
{insight.type === 'cost_optimization' && (
<span className="text-sm font-semibold text-[var(--color-success-600)]">
💰 {insight.impact_currency}{insight.impact_value} {t('dashboard:ai_insights.savings')}
</span>
)}
{insight.type === 'waste_reduction' && (
<span className="text-sm font-semibold text-[var(--color-success-600)]">
{insight.impact_value} {t('dashboard:ai_insights.reduction')}
</span>
)}
{!['cost_optimization', 'waste_reduction'].includes(insight.type) && (
<span className="text-sm font-semibold text-[var(--color-success-600)]">
💰 {insight.impact_currency}{insight.impact_value}
</span>
)}
</div>
)}
</div>
</div>
</div>
))}
</div>
) : (
/* Empty State */
<div className="px-6 pb-6">
<div className="flex items-center gap-3 p-4 rounded-lg bg-[var(--color-info-50)] border border-[var(--color-info-100)]">
<Lightbulb className="w-6 h-6 text-[var(--color-info-600)]" />
<p className="text-sm text-[var(--color-info-700)]">
{t('dashboard:ai_insights.no_insights')}
</p>
</div>
</div>
)}
</div>
);
}
export default AIInsightsBlock;