317 lines
13 KiB
TypeScript
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>
|
|
);
|
|
}
|