242 lines
8.9 KiB
TypeScript
242 lines
8.9 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,
|
||
|
|
Database,
|
||
|
|
Brain,
|
||
|
|
ChevronDown,
|
||
|
|
ChevronUp,
|
||
|
|
} from 'lucide-react';
|
||
|
|
import { OrchestrationSummary } from '../../api/hooks/newDashboard';
|
||
|
|
import { formatDistanceToNow } from 'date-fns';
|
||
|
|
|
||
|
|
interface OrchestrationSummaryCardProps {
|
||
|
|
summary: OrchestrationSummary;
|
||
|
|
loading?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSummaryCardProps) {
|
||
|
|
const [expanded, setExpanded] = useState(false);
|
||
|
|
|
||
|
|
if (loading) {
|
||
|
|
return (
|
||
|
|
<div className="bg-white rounded-xl shadow-md p-6">
|
||
|
|
<div className="animate-pulse space-y-4">
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<div className="w-10 h-10 bg-gray-200 rounded-full"></div>
|
||
|
|
<div className="h-6 bg-gray-200 rounded w-1/2"></div>
|
||
|
|
</div>
|
||
|
|
<div className="space-y-2">
|
||
|
|
<div className="h-4 bg-gray-200 rounded"></div>
|
||
|
|
<div className="h-4 bg-gray-200 rounded w-5/6"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle case where no orchestration has run yet
|
||
|
|
if (summary.status === 'no_runs') {
|
||
|
|
return (
|
||
|
|
<div className="bg-blue-50 border-2 border-blue-200 rounded-xl p-6">
|
||
|
|
<div className="flex items-start gap-4">
|
||
|
|
<Bot className="w-10 h-10 text-blue-600 flex-shrink-0" />
|
||
|
|
<div>
|
||
|
|
<h3 className="text-lg font-bold text-blue-900 mb-2">
|
||
|
|
Ready to Plan Your Bakery Day
|
||
|
|
</h3>
|
||
|
|
<p className="text-blue-700 mb-4">{summary.message}</p>
|
||
|
|
<button className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-semibold transition-colors duration-200">
|
||
|
|
Run Daily Planning
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const runTime = summary.runTimestamp
|
||
|
|
? formatDistanceToNow(new Date(summary.runTimestamp), { addSuffix: true })
|
||
|
|
: 'recently';
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="bg-gradient-to-br from-purple-50 to-blue-50 rounded-xl shadow-md p-6 border border-purple-100">
|
||
|
|
{/* Header */}
|
||
|
|
<div className="flex items-start gap-4 mb-6">
|
||
|
|
<div className="bg-purple-100 p-3 rounded-full">
|
||
|
|
<Bot className="w-8 h-8 text-purple-600" />
|
||
|
|
</div>
|
||
|
|
<div className="flex-1">
|
||
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-1">
|
||
|
|
Last Night I Planned Your Day
|
||
|
|
</h2>
|
||
|
|
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||
|
|
<Clock className="w-4 h-4" />
|
||
|
|
<span>Orchestration run #{summary.runNumber} • {runTime}</span>
|
||
|
|
{summary.durationSeconds && (
|
||
|
|
<span className="text-gray-400">• Took {summary.durationSeconds}s</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Purchase Orders Created */}
|
||
|
|
{summary.purchaseOrdersCreated > 0 && (
|
||
|
|
<div className="bg-white rounded-lg p-4 mb-4">
|
||
|
|
<div className="flex items-center gap-3 mb-3">
|
||
|
|
<CheckCircle className="w-5 h-5 text-green-600" />
|
||
|
|
<h3 className="font-bold text-gray-900">
|
||
|
|
Created {summary.purchaseOrdersCreated} purchase order
|
||
|
|
{summary.purchaseOrdersCreated !== 1 ? 's' : ''}
|
||
|
|
</h3>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{summary.purchaseOrdersSummary.length > 0 && (
|
||
|
|
<ul className="space-y-2 ml-8">
|
||
|
|
{summary.purchaseOrdersSummary.map((po, index) => (
|
||
|
|
<li key={index} className="text-sm text-gray-700">
|
||
|
|
<span className="font-medium">{po.supplierName}</span>
|
||
|
|
{' • '}
|
||
|
|
{po.itemCategories.slice(0, 2).join(', ')}
|
||
|
|
{po.itemCategories.length > 2 && ` +${po.itemCategories.length - 2} more`}
|
||
|
|
{' • '}
|
||
|
|
<span className="font-semibold">€{po.totalAmount.toFixed(2)}</span>
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Production Batches Created */}
|
||
|
|
{summary.productionBatchesCreated > 0 && (
|
||
|
|
<div className="bg-white rounded-lg p-4 mb-4">
|
||
|
|
<div className="flex items-center gap-3 mb-3">
|
||
|
|
<CheckCircle className="w-5 h-5 text-green-600" />
|
||
|
|
<h3 className="font-bold text-gray-900">
|
||
|
|
Scheduled {summary.productionBatchesCreated} production batch
|
||
|
|
{summary.productionBatchesCreated !== 1 ? 'es' : ''}
|
||
|
|
</h3>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{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 text-gray-700">
|
||
|
|
<span className="font-semibold">{batch.quantity}</span> {batch.productName}
|
||
|
|
{' • '}
|
||
|
|
<span className="text-gray-500">
|
||
|
|
ready by {new Date(batch.readyByTime).toLocaleTimeString('en-US', {
|
||
|
|
hour: 'numeric',
|
||
|
|
minute: '2-digit',
|
||
|
|
hour12: true,
|
||
|
|
})}
|
||
|
|
</span>
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{summary.productionBatchesSummary.length > 3 && (
|
||
|
|
<button
|
||
|
|
onClick={() => setExpanded(!expanded)}
|
||
|
|
className="ml-8 mt-2 flex items-center gap-1 text-sm text-purple-600 hover:text-purple-800 font-medium"
|
||
|
|
>
|
||
|
|
{expanded ? (
|
||
|
|
<>
|
||
|
|
<ChevronUp className="w-4 h-4" />
|
||
|
|
Show less
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
<>
|
||
|
|
<ChevronDown className="w-4 h-4" />
|
||
|
|
Show {summary.productionBatchesSummary.length - 3} more
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</button>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* No actions created */}
|
||
|
|
{summary.purchaseOrdersCreated === 0 && summary.productionBatchesCreated === 0 && (
|
||
|
|
<div className="bg-white rounded-lg p-4 mb-4">
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<CheckCircle className="w-5 h-5 text-gray-400" />
|
||
|
|
<p className="text-gray-600">No new actions needed - everything is on track!</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Reasoning Inputs (How decisions were made) */}
|
||
|
|
<div className="bg-white/60 rounded-lg p-4">
|
||
|
|
<div className="flex items-center gap-2 mb-3">
|
||
|
|
<Brain className="w-5 h-5 text-purple-600" />
|
||
|
|
<h3 className="font-bold text-gray-900">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 text-gray-600" />
|
||
|
|
<span className="text-gray-700">
|
||
|
|
{summary.reasoningInputs.customerOrders} customer order
|
||
|
|
{summary.reasoningInputs.customerOrders !== 1 ? 's' : ''}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{summary.reasoningInputs.historicalDemand && (
|
||
|
|
<div className="flex items-center gap-2 text-sm">
|
||
|
|
<TrendingUp className="w-4 h-4 text-gray-600" />
|
||
|
|
<span className="text-gray-700">Historical demand</span>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{summary.reasoningInputs.inventoryLevels && (
|
||
|
|
<div className="flex items-center gap-2 text-sm">
|
||
|
|
<Package className="w-4 h-4 text-gray-600" />
|
||
|
|
<span className="text-gray-700">Inventory levels</span>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{summary.reasoningInputs.aiInsights && (
|
||
|
|
<div className="flex items-center gap-2 text-sm">
|
||
|
|
<Brain className="w-4 h-4 text-purple-600" />
|
||
|
|
<span className="text-gray-700 font-medium">AI optimization</span>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Actions Required Footer */}
|
||
|
|
{summary.userActionsRequired > 0 && (
|
||
|
|
<div className="mt-4 p-4 bg-amber-50 border border-amber-200 rounded-lg">
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<FileText className="w-5 h-5 text-amber-600" />
|
||
|
|
<p className="text-sm font-medium text-amber-900">
|
||
|
|
{summary.userActionsRequired} item{summary.userActionsRequired !== 1 ? 's' : ''} need
|
||
|
|
{summary.userActionsRequired === 1 ? 's' : ''} your approval before proceeding
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|