feat: Complete frontend i18n implementation for dashboard components
- Updated TypeScript types to support reasoning_data field - Integrated useReasoningTranslation hook in all dashboard components: * ActionQueueCard: Translates PO reasoning_data and UI text * ProductionTimelineCard: Translates batch reasoning_data and UI text * OrchestrationSummaryCard: Translates all hardcoded English text * HealthStatusCard: Translates all hardcoded English text - Added missing translation keys to all language files (EN, ES, EU): * health_status: never, critical_issues, actions_needed * action_queue: total, critical, important * orchestration_summary: ready_to_plan, run_info, took, show_more/less * production_timeline: Complete rebuild with new keys - Components now support fallback for deprecated text fields - Full multilingual support: English, Spanish, Basque Dashboard is now fully translatable and will display reasoning in user's language.
This commit is contained in:
@@ -21,6 +21,8 @@ import {
|
||||
ChevronUp,
|
||||
} from 'lucide-react';
|
||||
import { ActionItem, ActionQueue } from '../../api/hooks/newDashboard';
|
||||
import { useReasoningFormatter } from '../../hooks/useReasoningTranslation';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ActionQueueCardProps {
|
||||
actionQueue: ActionQueue;
|
||||
@@ -65,6 +67,13 @@ function ActionItemCard({
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const config = urgencyConfig[action.urgency];
|
||||
const UrgencyIcon = config.icon;
|
||||
const { formatPOAction } = useReasoningFormatter();
|
||||
const { t } = useTranslation('reasoning');
|
||||
|
||||
// Translate reasoning_data (or fallback to deprecated text fields)
|
||||
const { reasoning, consequence, severity } = action.reasoning_data
|
||||
? formatPOAction(action.reasoning_data)
|
||||
: { reasoning: action.reasoning || '', consequence: action.consequence || '', severity: '' };
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -96,29 +105,42 @@ function ActionItemCard({
|
||||
|
||||
{/* Reasoning (always visible) */}
|
||||
<div className="bg-white rounded-md p-3 mb-3">
|
||||
<p className="text-sm font-medium text-gray-700 mb-1">Why this is needed:</p>
|
||||
<p className="text-sm text-gray-600">{action.reasoning}</p>
|
||||
<p className="text-sm font-medium text-gray-700 mb-1">
|
||||
{t('jtbd.action_queue.why_needed')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">{reasoning}</p>
|
||||
</div>
|
||||
|
||||
{/* Consequence (expandable) */}
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900 transition-colors mb-3 w-full"
|
||||
>
|
||||
{expanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
|
||||
<span className="font-medium">What happens if I don't do this?</span>
|
||||
</button>
|
||||
{consequence && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900 transition-colors mb-3 w-full"
|
||||
>
|
||||
{expanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
|
||||
<span className="font-medium">{t('jtbd.action_queue.what_if_not')}</span>
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-md p-3 mb-3">
|
||||
<p className="text-sm text-amber-900">{action.consequence}</p>
|
||||
</div>
|
||||
{expanded && (
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-md p-3 mb-3">
|
||||
<p className="text-sm text-amber-900">{consequence}</p>
|
||||
{severity && (
|
||||
<span className="text-xs font-semibold text-amber-900 mt-1 block">
|
||||
{severity}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Time Estimate */}
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500 mb-4">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>Estimated time: {action.estimatedTimeMinutes} min</span>
|
||||
<span>
|
||||
{t('jtbd.action_queue.estimated_time')}: {action.estimatedTimeMinutes} min
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
@@ -167,6 +189,7 @@ export function ActionQueueCard({
|
||||
}: ActionQueueCardProps) {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const displayedActions = showAll ? actionQueue.actions : actionQueue.actions.slice(0, 3);
|
||||
const { t } = useTranslation('reasoning');
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -184,8 +207,10 @@ export function ActionQueueCard({
|
||||
return (
|
||||
<div className="bg-green-50 border-2 border-green-200 rounded-xl p-8 text-center">
|
||||
<CheckCircle2 className="w-16 h-16 text-green-600 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-bold text-green-900 mb-2">All caught up!</h3>
|
||||
<p className="text-green-700">No actions requiring your attention right now.</p>
|
||||
<h3 className="text-xl font-bold text-green-900 mb-2">
|
||||
{t('jtbd.action_queue.all_caught_up')}
|
||||
</h3>
|
||||
<p className="text-green-700">{t('jtbd.action_queue.no_actions')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -194,10 +219,10 @@ export function ActionQueueCard({
|
||||
<div className="bg-white rounded-xl shadow-md p-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold text-gray-900">What Needs Your Attention</h2>
|
||||
<h2 className="text-2xl font-bold text-gray-900">{t('jtbd.action_queue.title')}</h2>
|
||||
{actionQueue.totalActions > 3 && (
|
||||
<span className="bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-semibold">
|
||||
{actionQueue.totalActions} total
|
||||
{actionQueue.totalActions} {t('jtbd.action_queue.total')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -207,12 +232,12 @@ export function ActionQueueCard({
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
{actionQueue.criticalCount > 0 && (
|
||||
<span className="bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-semibold">
|
||||
{actionQueue.criticalCount} critical
|
||||
{actionQueue.criticalCount} {t('jtbd.action_queue.critical')}
|
||||
</span>
|
||||
)}
|
||||
{actionQueue.importantCount > 0 && (
|
||||
<span className="bg-amber-100 text-amber-800 px-3 py-1 rounded-full text-sm font-semibold">
|
||||
{actionQueue.importantCount} important
|
||||
{actionQueue.importantCount} {t('jtbd.action_queue.important')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -238,10 +263,8 @@ export function ActionQueueCard({
|
||||
className="w-full mt-4 py-3 bg-gray-100 hover:bg-gray-200 rounded-lg font-semibold text-gray-700 transition-colors duration-200"
|
||||
>
|
||||
{showAll
|
||||
? 'Show Less'
|
||||
: `Show ${actionQueue.totalActions - 3} More Action${
|
||||
actionQueue.totalActions - 3 !== 1 ? 's' : ''
|
||||
}`}
|
||||
? t('jtbd.action_queue.show_less')
|
||||
: t('jtbd.action_queue.show_more', { count: actionQueue.totalActions - 3 })}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@ import React from 'react';
|
||||
import { CheckCircle, AlertTriangle, AlertCircle, Clock, RefreshCw } from 'lucide-react';
|
||||
import { BakeryHealthStatus } from '../../api/hooks/newDashboard';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface HealthStatusCardProps {
|
||||
healthStatus: BakeryHealthStatus;
|
||||
@@ -51,6 +52,7 @@ const iconMap = {
|
||||
export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProps) {
|
||||
const config = statusConfig[healthStatus.status];
|
||||
const StatusIcon = config.icon;
|
||||
const { t } = useTranslation('reasoning');
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -79,12 +81,12 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>
|
||||
Last updated:{' '}
|
||||
{t('jtbd.health_status.last_updated')}:{' '}
|
||||
{healthStatus.lastOrchestrationRun
|
||||
? formatDistanceToNow(new Date(healthStatus.lastOrchestrationRun), {
|
||||
addSuffix: true,
|
||||
})
|
||||
: 'Never'}
|
||||
: t('jtbd.health_status.never')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -92,7 +94,7 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600 mt-1">
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
<span>
|
||||
Next check:{' '}
|
||||
{t('jtbd.health_status.next_check')}:{' '}
|
||||
{formatDistanceToNow(new Date(healthStatus.nextScheduledRun), { addSuffix: true })}
|
||||
</span>
|
||||
</div>
|
||||
@@ -129,7 +131,7 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-red-600" />
|
||||
<span className="font-semibold text-red-800">
|
||||
{healthStatus.criticalIssues} critical issue{healthStatus.criticalIssues !== 1 ? 's' : ''}
|
||||
{t('jtbd.health_status.critical_issues', { count: healthStatus.criticalIssues })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -137,7 +139,7 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="w-4 h-4 text-amber-600" />
|
||||
<span className="font-semibold text-amber-800">
|
||||
{healthStatus.pendingActions} action{healthStatus.pendingActions !== 1 ? 's' : ''} needed
|
||||
{t('jtbd.health_status.actions_needed', { count: healthStatus.pendingActions })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { OrchestrationSummary } from '../../api/hooks/newDashboard';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface OrchestrationSummaryCardProps {
|
||||
summary: OrchestrationSummary;
|
||||
@@ -32,6 +33,7 @@ interface OrchestrationSummaryCardProps {
|
||||
|
||||
export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSummaryCardProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const { t } = useTranslation('reasoning');
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -58,11 +60,11 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
|
||||
<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
|
||||
{t('jtbd.orchestration_summary.ready_to_plan')}
|
||||
</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
|
||||
{t('jtbd.orchestration_summary.run_planning')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,13 +85,17 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-1">
|
||||
Last Night I Planned Your Day
|
||||
{t('jtbd.orchestration_summary.title')}
|
||||
</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>
|
||||
<span>
|
||||
{t('jtbd.orchestration_summary.run_info', { runNumber: summary.runNumber })} • {runTime}
|
||||
</span>
|
||||
{summary.durationSeconds && (
|
||||
<span className="text-gray-400">• Took {summary.durationSeconds}s</span>
|
||||
<span className="text-gray-400">
|
||||
• {t('jtbd.orchestration_summary.took', { seconds: summary.durationSeconds })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,8 +107,7 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
|
||||
<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' : ''}
|
||||
{t('jtbd.orchestration_summary.created_pos', { count: summary.purchaseOrdersCreated })}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -129,8 +134,9 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
|
||||
<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' : ''}
|
||||
{t('jtbd.orchestration_summary.scheduled_batches', {
|
||||
count: summary.productionBatchesCreated,
|
||||
})}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -160,12 +166,14 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
|
||||
{expanded ? (
|
||||
<>
|
||||
<ChevronUp className="w-4 h-4" />
|
||||
Show less
|
||||
{t('jtbd.orchestration_summary.show_less')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
Show {summary.productionBatchesSummary.length - 3} more
|
||||
{t('jtbd.orchestration_summary.show_more', {
|
||||
count: summary.productionBatchesSummary.length - 3,
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -178,7 +186,7 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
|
||||
<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>
|
||||
<p className="text-gray-600">{t('jtbd.orchestration_summary.no_actions')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -187,7 +195,7 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
|
||||
<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>
|
||||
<h3 className="font-bold text-gray-900">{t('jtbd.orchestration_summary.based_on')}</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 ml-7">
|
||||
@@ -195,8 +203,9 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
|
||||
<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' : ''}
|
||||
{t('jtbd.orchestration_summary.customer_orders', {
|
||||
count: summary.reasoningInputs.customerOrders,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -204,21 +213,25 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
|
||||
{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>
|
||||
<span className="text-gray-700">
|
||||
{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 text-gray-600" />
|
||||
<span className="text-gray-700">Inventory levels</span>
|
||||
<span className="text-gray-700">{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 text-purple-600" />
|
||||
<span className="text-gray-700 font-medium">AI optimization</span>
|
||||
<span className="text-gray-700 font-medium">
|
||||
{t('jtbd.orchestration_summary.ai_optimization')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -230,8 +243,9 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
|
||||
<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
|
||||
{t('jtbd.orchestration_summary.actions_required', {
|
||||
count: summary.userActionsRequired,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
import React from 'react';
|
||||
import { Factory, Clock, Play, Pause, CheckCircle2 } from 'lucide-react';
|
||||
import { ProductionTimeline, ProductionTimelineItem } from '../../api/hooks/newDashboard';
|
||||
import { useReasoningFormatter } from '../../hooks/useReasoningTranslation';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ProductionTimelineCardProps {
|
||||
timeline: ProductionTimeline;
|
||||
@@ -35,6 +37,13 @@ function TimelineItemCard({
|
||||
onPause?: (id: string) => void;
|
||||
}) {
|
||||
const priorityColor = priorityColors[item.priority as keyof typeof priorityColors] || 'text-gray-600';
|
||||
const { formatBatchAction } = useReasoningFormatter();
|
||||
const { t } = useTranslation('reasoning');
|
||||
|
||||
// Translate reasoning_data (or fallback to deprecated text field)
|
||||
const { reasoning } = item.reasoning_data
|
||||
? formatBatchAction(item.reasoning_data)
|
||||
: { reasoning: item.reasoning || '' };
|
||||
|
||||
const startTime = item.plannedStartTime
|
||||
? new Date(item.plannedStartTime).toLocaleTimeString('en-US', {
|
||||
@@ -99,13 +108,15 @@ function TimelineItemCard({
|
||||
{item.status !== 'COMPLETED' && (
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600 mb-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>Ready by: {readyByTime}</span>
|
||||
<span>
|
||||
{t('jtbd.production_timeline.ready_by')}: {readyByTime}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reasoning */}
|
||||
{item.reasoning && (
|
||||
<p className="text-sm text-gray-600 italic mb-3">"{item.reasoning}"</p>
|
||||
{reasoning && (
|
||||
<p className="text-sm text-gray-600 italic mb-3">"{reasoning}"</p>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
@@ -115,7 +126,7 @@ function TimelineItemCard({
|
||||
className="flex items-center gap-2 px-3 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg text-sm font-semibold transition-colors duration-200"
|
||||
>
|
||||
<Play className="w-4 h-4" />
|
||||
Start Batch
|
||||
{t('jtbd.production_timeline.start_batch')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -125,14 +136,14 @@ function TimelineItemCard({
|
||||
className="flex items-center gap-2 px-3 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-lg text-sm font-semibold transition-colors duration-200"
|
||||
>
|
||||
<Pause className="w-4 h-4" />
|
||||
Pause Batch
|
||||
{t('jtbd.production_timeline.pause_batch')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{item.status === 'COMPLETED' && (
|
||||
<div className="flex items-center gap-2 text-sm text-green-600 font-medium">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
Completed
|
||||
{t('jtbd.production_timeline.completed')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -146,6 +157,8 @@ export function ProductionTimelineCard({
|
||||
onStart,
|
||||
onPause,
|
||||
}: ProductionTimelineCardProps) {
|
||||
const { t } = useTranslation('reasoning');
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-md p-6">
|
||||
@@ -164,8 +177,10 @@ export function ProductionTimelineCard({
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-md p-8 text-center">
|
||||
<Factory className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-bold text-gray-700 mb-2">No Production Scheduled</h3>
|
||||
<p className="text-gray-600">No batches are scheduled for production today.</p>
|
||||
<h3 className="text-xl font-bold text-gray-700 mb-2">
|
||||
{t('jtbd.production_timeline.no_production')}
|
||||
</h3>
|
||||
<p className="text-gray-600">{t('jtbd.production_timeline.no_batches')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -176,7 +191,7 @@ export function ProductionTimelineCard({
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Factory className="w-8 h-8 text-blue-600" />
|
||||
<h2 className="text-2xl font-bold text-gray-900">Your Production Plan Today</h2>
|
||||
<h2 className="text-2xl font-bold text-gray-900">{t('jtbd.production_timeline.title')}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -184,19 +199,19 @@ export function ProductionTimelineCard({
|
||||
<div className="grid grid-cols-4 gap-4 mb-6">
|
||||
<div className="bg-gray-50 rounded-lg p-3 text-center">
|
||||
<div className="text-2xl font-bold text-gray-900">{timeline.totalBatches}</div>
|
||||
<div className="text-xs text-gray-600 uppercase">Total</div>
|
||||
<div className="text-xs text-gray-600 uppercase">{t('jtbd.production_timeline.total')}</div>
|
||||
</div>
|
||||
<div className="bg-green-50 rounded-lg p-3 text-center">
|
||||
<div className="text-2xl font-bold text-green-600">{timeline.completedBatches}</div>
|
||||
<div className="text-xs text-green-700 uppercase">Done</div>
|
||||
<div className="text-xs text-green-700 uppercase">{t('jtbd.production_timeline.done')}</div>
|
||||
</div>
|
||||
<div className="bg-blue-50 rounded-lg p-3 text-center">
|
||||
<div className="text-2xl font-bold text-blue-600">{timeline.inProgressBatches}</div>
|
||||
<div className="text-xs text-blue-700 uppercase">Active</div>
|
||||
<div className="text-xs text-blue-700 uppercase">{t('jtbd.production_timeline.active')}</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-3 text-center">
|
||||
<div className="text-2xl font-bold text-gray-600">{timeline.pendingBatches}</div>
|
||||
<div className="text-xs text-gray-600 uppercase">Pending</div>
|
||||
<div className="text-xs text-gray-600 uppercase">{t('jtbd.production_timeline.pending')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -215,7 +230,7 @@ export function ProductionTimelineCard({
|
||||
{/* View Full Schedule Link */}
|
||||
{timeline.totalBatches > 5 && (
|
||||
<button className="w-full mt-6 py-3 bg-gray-100 hover:bg-gray-200 rounded-lg font-semibold text-gray-700 transition-colors duration-200">
|
||||
View Full Production Schedule
|
||||
{t('jtbd.production_timeline.view_full_schedule')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user