Files
bakery-ia/frontend/src/components/dashboard/OrchestrationSummaryCard.tsx
2025-11-18 07:17:17 +01:00

317 lines
13 KiB
TypeScript

// ================================================================
// frontend/src/components/dashboard/OrchestrationSummaryCard.tsx
// ================================================================
/**
* Orchestration Summary Card - What the system did for you
*
* Builds trust by showing transparency into automation decisions.
* Narrative format makes it feel like a helpful assistant.
*/
import React, { useState } from 'react';
import {
Bot,
TrendingUp,
Package,
Clock,
CheckCircle,
FileText,
Users,
Brain,
ChevronDown,
ChevronUp,
Loader2,
} from 'lucide-react';
import { OrchestrationSummary } from '../../api/hooks/newDashboard';
import { runDailyWorkflow } from '../../api/services/orchestrator';
import { formatDistanceToNow } from 'date-fns';
import { useTranslation } from 'react-i18next';
import { useTenant } from '../../stores/tenant.store';
import toast from 'react-hot-toast';
interface OrchestrationSummaryCardProps {
summary: OrchestrationSummary;
loading?: boolean;
onWorkflowComplete?: () => void;
}
export function OrchestrationSummaryCard({ summary, loading, onWorkflowComplete }: OrchestrationSummaryCardProps) {
const [expanded, setExpanded] = useState(false);
const [isRunning, setIsRunning] = useState(false);
const { t } = useTranslation('reasoning');
const { currentTenant } = useTenant();
const handleRunPlanning = async () => {
if (!currentTenant?.id) {
toast.error(t('jtbd.orchestration_summary.no_tenant_error') || 'No tenant ID found');
return;
}
setIsRunning(true);
try {
const result = await runDailyWorkflow(currentTenant.id);
if (result.success) {
toast.success(t('jtbd.orchestration_summary.planning_started') || 'Planning started successfully');
// Call callback to refresh the orchestration summary
if (onWorkflowComplete) {
onWorkflowComplete();
}
} else {
toast.error(result.message || t('jtbd.orchestration_summary.planning_failed') || 'Failed to start planning');
}
} catch (error) {
console.error('Error running daily workflow:', error);
toast.error(t('jtbd.orchestration_summary.planning_error') || 'An error occurred while starting planning');
} finally {
setIsRunning(false);
}
};
if (loading || !summary) {
return (
<div className="rounded-xl shadow-lg p-6 border" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-primary)' }}>
<div className="animate-pulse space-y-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div>
<div className="h-6 rounded w-1/2" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div>
</div>
<div className="space-y-2">
<div className="h-4 rounded" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div>
<div className="h-4 rounded w-5/6" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div>
</div>
</div>
</div>
);
}
// Handle case where no orchestration has run yet
if (summary.status === 'no_runs') {
return (
<div
className="border-2 rounded-xl p-6 shadow-lg"
style={{
backgroundColor: 'var(--surface-secondary)',
borderColor: 'var(--color-info-300)',
}}
>
<div className="flex items-start gap-4">
<Bot className="w-10 h-10 flex-shrink-0" style={{ color: 'var(--color-info)' }} />
<div>
<h3 className="text-lg font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
{t('jtbd.orchestration_summary.ready_to_plan')}
</h3>
<p className="mb-4" style={{ color: 'var(--text-secondary)' }}>{summary.message || ''}</p>
<button
onClick={handleRunPlanning}
disabled={isRunning}
className="px-4 py-2 rounded-lg font-semibold transition-colors duration-200 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed hover:opacity-90"
style={{
backgroundColor: 'var(--color-info)',
color: 'var(--bg-primary)',
}}
>
{isRunning && <Loader2 className="w-4 h-4 animate-spin" />}
{t('jtbd.orchestration_summary.run_planning')}
</button>
</div>
</div>
</div>
);
}
const runTime = summary.runTimestamp
? formatDistanceToNow(new Date(summary.runTimestamp), { addSuffix: true })
: 'recently';
return (
<div
className="rounded-xl shadow-lg p-6 border"
style={{
background: 'linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%)',
borderColor: 'var(--border-primary)',
}}
>
{/* Header */}
<div className="flex items-start gap-4 mb-6">
<div className="p-3 rounded-full" style={{ backgroundColor: 'var(--bg-tertiary)' }}>
<Bot className="w-8 h-8" style={{ color: 'var(--color-primary)' }} />
</div>
<div className="flex-1">
<h2 className="text-2xl font-bold mb-1" style={{ color: 'var(--text-primary)' }}>
{t('jtbd.orchestration_summary.title')}
</h2>
<div className="flex items-center gap-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
<Clock className="w-4 h-4" />
<span>
{t('jtbd.orchestration_summary.run_info', { runNumber: summary.runNumber || 0 })} {runTime}
</span>
{summary.durationSeconds && (
<span style={{ color: 'var(--text-tertiary)' }}>
{t('jtbd.orchestration_summary.took', { seconds: summary.durationSeconds })}
</span>
)}
</div>
</div>
</div>
{/* Purchase Orders Created */}
{summary.purchaseOrdersCreated > 0 && (
<div className="rounded-lg p-4 mb-4" style={{ backgroundColor: 'var(--bg-primary)' }}>
<div className="flex items-center gap-3 mb-3">
<CheckCircle className="w-5 h-5" style={{ color: 'var(--color-success-600)' }} />
<h3 className="font-bold" style={{ color: 'var(--text-primary)' }}>
{t('jtbd.orchestration_summary.created_pos', { count: summary.purchaseOrdersCreated })}
</h3>
</div>
{summary.purchaseOrdersSummary && summary.purchaseOrdersSummary.length > 0 && (
<ul className="space-y-2 ml-8">
{summary.purchaseOrdersSummary.map((po, index) => (
<li key={index} className="text-sm" style={{ color: 'var(--text-secondary)' }}>
<span className="font-medium">{po.supplierName || 'Unknown Supplier'}</span>
{' • '}
{(po.itemCategories || []).slice(0, 2).join(', ') || 'Items'}
{(po.itemCategories || []).length > 2 && ` +${po.itemCategories.length - 2} more`}
{' • '}
<span className="font-semibold">{(po.totalAmount || 0).toFixed(2)}</span>
</li>
))}
</ul>
)}
</div>
)}
{/* Production Batches Created */}
{summary.productionBatchesCreated > 0 && (
<div className="rounded-lg p-4 mb-4" style={{ backgroundColor: 'var(--bg-primary)' }}>
<div className="flex items-center gap-3 mb-3">
<CheckCircle className="w-5 h-5" style={{ color: 'var(--color-success-600)' }} />
<h3 className="font-bold" style={{ color: 'var(--text-primary)' }}>
{t('jtbd.orchestration_summary.scheduled_batches', {
count: summary.productionBatchesCreated,
})}
</h3>
</div>
{summary.productionBatchesSummary && summary.productionBatchesSummary.length > 0 && (
<ul className="space-y-2 ml-8">
{summary.productionBatchesSummary.slice(0, expanded ? undefined : 3).map((batch, index) => (
<li key={index} className="text-sm" style={{ color: 'var(--text-secondary)' }}>
<span className="font-semibold">{batch.quantity || 0}</span> {batch.productName || 'Product'}
{' • '}
<span style={{ color: 'var(--text-tertiary)' }}>
ready by {batch.readyByTime ? new Date(batch.readyByTime).toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true,
}) : 'TBD'}
</span>
</li>
))}
</ul>
)}
{summary.productionBatchesSummary && summary.productionBatchesSummary.length > 3 && (
<button
onClick={() => setExpanded(!expanded)}
className="ml-8 mt-2 flex items-center gap-1 text-sm font-medium"
style={{ color: 'var(--color-primary-600)' }}
>
{expanded ? (
<>
<ChevronUp className="w-4 h-4" />
{t('jtbd.orchestration_summary.show_less')}
</>
) : (
<>
<ChevronDown className="w-4 h-4" />
{t('jtbd.orchestration_summary.show_more', {
count: summary.productionBatchesSummary.length - 3,
})}
</>
)}
</button>
)}
</div>
)}
{/* No actions created */}
{summary.purchaseOrdersCreated === 0 && summary.productionBatchesCreated === 0 && (
<div className="rounded-lg p-4 mb-4" style={{ backgroundColor: 'var(--bg-primary)' }}>
<div className="flex items-center gap-3">
<CheckCircle className="w-5 h-5" style={{ color: 'var(--text-tertiary)' }} />
<p style={{ color: 'var(--text-secondary)' }}>{t('jtbd.orchestration_summary.no_actions')}</p>
</div>
</div>
)}
{/* Reasoning Inputs (How decisions were made) */}
<div className="rounded-lg p-4" style={{ backgroundColor: 'var(--surface-secondary)', border: '1px solid var(--border-primary)' }}>
<div className="flex items-center gap-2 mb-3">
<Brain className="w-5 h-5" style={{ color: 'var(--color-primary)' }} />
<h3 className="font-bold" style={{ color: 'var(--text-primary)' }}>{t('jtbd.orchestration_summary.based_on')}</h3>
</div>
<div className="grid grid-cols-2 gap-3 ml-7">
{summary.reasoningInputs.customerOrders > 0 && (
<div className="flex items-center gap-2 text-sm">
<Users className="w-4 h-4" style={{ color: 'var(--text-secondary)' }} />
<span style={{ color: 'var(--text-secondary)' }}>
{t('jtbd.orchestration_summary.customer_orders', {
count: summary.reasoningInputs.customerOrders,
})}
</span>
</div>
)}
{summary.reasoningInputs.historicalDemand && (
<div className="flex items-center gap-2 text-sm">
<TrendingUp className="w-4 h-4" style={{ color: 'var(--text-secondary)' }} />
<span style={{ color: 'var(--text-secondary)' }}>
{t('jtbd.orchestration_summary.historical_demand')}
</span>
</div>
)}
{summary.reasoningInputs.inventoryLevels && (
<div className="flex items-center gap-2 text-sm">
<Package className="w-4 h-4" style={{ color: 'var(--text-secondary)' }} />
<span style={{ color: 'var(--text-secondary)' }}>{t('jtbd.orchestration_summary.inventory_levels')}</span>
</div>
)}
{summary.reasoningInputs.aiInsights && (
<div className="flex items-center gap-2 text-sm">
<Brain className="w-4 h-4" style={{ color: 'var(--color-primary-600)' }} />
<span className="font-medium" style={{ color: 'var(--text-secondary)' }}>
{t('jtbd.orchestration_summary.ai_optimization')}
</span>
</div>
)}
</div>
</div>
{/* Actions Required Footer */}
{summary.userActionsRequired > 0 && (
<div
className="mt-4 p-4 border rounded-lg"
style={{
backgroundColor: 'var(--bg-tertiary)',
borderColor: 'var(--color-warning)',
}}
>
<div className="flex items-center gap-2">
<FileText className="w-5 h-5" style={{ color: 'var(--color-warning)' }} />
<p className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
{t('jtbd.orchestration_summary.actions_required', {
count: summary.userActionsRequired,
})}
</p>
</div>
</div>
)}
</div>
);
}