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:
Claude
2025-11-07 18:34:30 +00:00
parent 84d38842ab
commit 28136cf198
9 changed files with 619 additions and 152 deletions

View File

@@ -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>

View File

@@ -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>
)}

View File

@@ -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>

View File

@@ -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>