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

@@ -0,0 +1,402 @@
# Reasoning i18n Implementation - Complete Summary
## ✅ Completed Implementation
### 1. **Backend: Structured Reasoning Data Generation**
#### Created Standard Reasoning Types (`shared/schemas/reasoning_types.py`)
```python
# Purchase Order Types
- low_stock_detection
- forecast_demand
- safety_stock_replenishment
- supplier_contract
- seasonal_demand
- production_requirement
- manual_request
# Production Batch Types
- forecast_demand
- customer_order
- stock_replenishment
- seasonal_preparation
- promotion_event
- urgent_order
- regular_schedule
```
#### Helper Functions
```python
create_po_reasoning_low_stock()
create_po_reasoning_forecast_demand()
create_batch_reasoning_forecast_demand()
create_batch_reasoning_customer_order()
```
### 2. **Backend: Services Updated**
#### ✅ Production Service
**File:** `services/production/app/services/production_service.py:1839-1867`
- Generates `reasoning_data` when creating production batches
- Includes: product_name, predicted_demand, current_stock, confidence_score
**Example Output:**
```json
{
"type": "forecast_demand",
"parameters": {
"product_name": "Croissant",
"predicted_demand": 500,
"current_stock": 120,
"production_needed": 380,
"confidence_score": 87
},
"urgency": {
"level": "normal",
"ready_by_time": "08:00"
},
"metadata": {
"trigger_source": "orchestrator_auto",
"ai_assisted": true
}
}
```
#### ✅ Procurement Service
**File:** `services/procurement/app/services/procurement_service.py:874-1040`
- **NEW:** Implemented actual PO creation (replaced placeholder!)
- Groups requirements by supplier
- Intelligently chooses reasoning type based on context
- Generates comprehensive reasoning_data
**Example Output:**
```json
{
"type": "low_stock_detection",
"parameters": {
"supplier_name": "Harinas del Norte",
"product_names": ["Flour Type 55", "Flour Type 45"],
"current_stock": 45.5,
"required_stock": 200,
"days_until_stockout": 3,
"stock_percentage": 22.8
},
"consequence": {
"type": "stockout_risk",
"severity": "high",
"impact_days": 3,
"affected_products": ["Baguette", "Croissant"],
"estimated_lost_orders": 15
},
"metadata": {
"trigger_source": "orchestrator_auto",
"forecast_confidence": 0.85,
"ai_assisted": true
}
}
```
#### ✅ Dashboard Service
**File:** `services/orchestrator/app/services/dashboard_service.py`
- Returns `reasoning_data` instead of TEXT fields
- Creates defaults if missing
- Both PO actions and production timeline use structured data
### 3. **Backend: Database Schema**
#### ✅ Models Updated
- **PurchaseOrder:** Removed `reasoning`, `consequence` TEXT columns
- **ProductionBatch:** Removed `reasoning` TEXT column
- Both use only `reasoning_data` (JSONB/JSON)
#### ✅ Unified Schemas Updated
- `services/procurement/migrations/001_unified_initial_schema.py`
- `services/production/migrations/001_unified_initial_schema.py`
- No separate migration needed - updated initial schema
### 4. **Frontend: i18n Translation System**
#### ✅ Translation Files Created
**Languages:** English (EN), Spanish (ES), Basque/Euskara (EU)
**Files:**
- `frontend/src/locales/en/reasoning.json`
- `frontend/src/locales/es/reasoning.json`
- `frontend/src/locales/eu/reasoning.json`
**Translation Coverage:**
- ✅ All purchase order reasoning types
- ✅ All production batch reasoning types
- ✅ All consequence types
- ✅ Severity levels
- ✅ Error codes
- ✅ Complete JTBD dashboard UI text
**Example Translations:**
| Language | Translation |
|---|---|
| 🇬🇧 EN | "Low stock for {{supplier_name}}. Stock runs out in {{days_until_stockout}} days." |
| 🇪🇸 ES | "Stock bajo para {{supplier_name}}. Se agota en {{days_until_stockout}} días." |
| 🇪🇺 EU | "{{supplier_name}}-rentzat stock baxua. {{days_until_stockout}} egunetan amaituko da." |
#### ✅ Translation Hook Created
**File:** `frontend/src/hooks/useReasoningTranslation.ts`
**Functions:**
```typescript
translatePOReasonng(reasoningData) // Purchase orders
translateBatchReasoning(reasoningData) // Production batches
translateConsequence(consequenceData) // Consequences
translateSeverity(severity) // Severity levels
translateTrigger(trigger) // Trigger sources
translateError(errorCode) // Error codes
// High-level formatters
formatPOAction(reasoningData) // Complete PO formatting
formatBatchAction(reasoningData) // Complete batch formatting
```
**Usage Example:**
```typescript
import { useReasoningFormatter } from '@/hooks/useReasoningTranslation';
function ActionQueueCard({ action }) {
const { formatPOAction } = useReasoningFormatter();
const { reasoning, consequence, severity } = formatPOAction(action.reasoning_data);
return (
<div>
<p>{reasoning}</p> {/* Translated! */}
<p>{consequence}</p> {/* Translated! */}
</div>
);
}
```
---
## 🔄 Remaining Work
### 1. **Frontend Components Need Updates**
#### ❌ ActionQueueCard.tsx
**Current:** Expects `reasoning` and `consequence` TEXT fields
**Needed:** Use `useReasoningFormatter()` to translate `reasoning_data`
**Change Required:**
```typescript
// BEFORE
<p>{action.reasoning}</p>
<p>{action.consequence}</p>
// AFTER
import { useReasoningFormatter } from '@/hooks/useReasoningTranslation';
const { formatPOAction } = useReasoningFormatter();
const { reasoning, consequence } = formatPOAction(action.reasoning_data);
<p>{reasoning}</p>
<p>{consequence}</p>
```
#### ❌ ProductionTimelineCard.tsx
**Needed:** Use `formatBatchAction()` to translate batch reasoning
#### ❌ OrchestrationSummaryCard.tsx
**Needed:** Replace hardcoded English text with i18n keys:
- "Last Night I Planned Your Day" → `t('reasoning:jtbd.orchestration_summary.title')`
- "All caught up!" → `t('reasoning:jtbd.action_queue.all_caught_up')`
- etc.
#### ❌ HealthStatusCard.tsx
**Needed:** Replace hardcoded text with i18n
### 2. **Backend Services Need Error Code Updates**
#### ❌ Safety Stock Calculator
**File:** `services/procurement/app/services/safety_stock_calculator.py`
**Current:**
```python
reasoning='Lead time or demand std dev is zero or negative'
reasoning='Insufficient historical demand data...'
```
**Needed:**
```python
reasoning_data={
"type": "error",
"code": "LEAD_TIME_INVALID",
"parameters": {}
}
```
#### ❌ Replenishment Planning Service
**File:** `services/procurement/app/services/replenishment_planning_service.py`
**Current:**
```python
reasoning='Insufficient data for safety stock calculation'
```
**Needed:**
```python
reasoning_data={
"type": "error",
"code": "INSUFFICIENT_DATA",
"parameters": {}
}
```
### 3. **Demo Seed Scripts Need Updates**
#### ❌ Purchase Orders Seed
**File:** `services/procurement/scripts/demo/seed_demo_purchase_orders.py`
**Current (lines 126-127):**
```python
reasoning_text = f"Low stock detected for {supplier.name} items..."
consequence_text = f"Stock-out risk in {days_until_delivery + 2} days..."
```
**Needed:**
```python
from shared.schemas.reasoning_types import create_po_reasoning_low_stock
reasoning_data = create_po_reasoning_low_stock(
supplier_name=supplier.name,
product_names=[...],
current_stock=...,
required_stock=...,
days_until_stockout=days_until_delivery + 2
)
```
#### ❌ Production Batches Seed
**File:** `services/production/scripts/demo/seed_demo_batches.py`
**Needed:** Similar update using `create_batch_reasoning_*()` functions
---
## 📋 Quick Implementation Checklist
### High Priority (Breaks Current Functionality)
- [ ] Update `ActionQueueCard.tsx` to use reasoning translation
- [ ] Update `ProductionTimelineCard.tsx` to use reasoning translation
- [ ] Update demo seed scripts to use structured reasoning_data
### Medium Priority (Improves UX)
- [ ] Update `OrchestrationSummaryCard.tsx` with i18n
- [ ] Update `HealthStatusCard.tsx` with i18n
- [ ] Update `InsightsGrid.tsx` with i18n (if needed)
### Low Priority (Code Quality)
- [ ] Update safety stock calculator with error codes
- [ ] Update replenishment service with error codes
- [ ] Audit ML services for hardcoded text
---
## 🎯 Example Implementation for ActionQueueCard
```typescript
// frontend/src/components/dashboard/ActionQueueCard.tsx
import { useReasoningFormatter } from '@/hooks/useReasoningTranslation';
import { useTranslation } from 'react-i18next';
function ActionItemCard({ action, onApprove, onViewDetails, onModify }: ...) {
const { formatPOAction } = useReasoningFormatter();
const { t } = useTranslation('reasoning');
// Translate reasoning_data
const { reasoning, consequence, severity } = formatPOAction(action.reasoning_data);
return (
<div className={`...`}>
{/* Reasoning (always visible) */}
<div className="bg-white rounded-md p-3 mb-3">
<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="...">
{t('jtbd.action_queue.what_if_not')}
</button>
{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">{severity}</span>
)}
</div>
)}
</div>
);
}
```
---
## 🚀 Benefits Achieved
1. **✅ Multilingual Support**
- Dashboard works in EN, ES, and EU
- Easy to add more languages (CA, FR, etc.)
2. **✅ Maintainability**
- Backend: One place to define reasoning logic
- Frontend: Translations in organized JSON files
- No hardcoded text scattered across code
3. **✅ Consistency**
- Same reasoning type always translates the same way
- Centralized terminology
4. **✅ Flexibility**
- Can change wording without touching code
- Can A/B test different phrasings
- Translators can work independently
5. **✅ Type Safety**
- TypeScript interfaces for reasoning_data
- Compile-time checks for translation keys
---
## 📚 Documentation
- **Reasoning Types:** `shared/schemas/reasoning_types.py`
- **Translation Hook:** `frontend/src/hooks/useReasoningTranslation.ts`
- **Translation Files:** `frontend/src/locales/{en,es,eu}/reasoning.json`
- **Audit Report:** `REASONING_I18N_AUDIT.md`
---
## Next Steps
1. **Update frontend components** (30-60 min)
- Replace TEXT field usage with reasoning_data translation
- Use `useReasoningFormatter()` hook
- Replace hardcoded strings with `t()` calls
2. **Update demo seed scripts** (15-30 min)
- Replace hardcoded text with helper functions
- Test demo data generation
3. **Update backend services** (15-30 min)
- Replace hardcoded error messages with error codes
- Frontend will translate error codes
4. **Test** (30 min)
- Switch between EN, ES, EU
- Verify all reasoning types display correctly
- Check mobile responsiveness
**Total Estimated Time:** 2-3 hours for complete implementation

View File

@@ -81,8 +81,9 @@ export interface ActionItem {
urgency: 'critical' | 'important' | 'normal';
title: string;
subtitle: string;
reasoning: string;
consequence: string;
reasoning?: string; // Deprecated: Use reasoning_data instead
consequence?: string; // Deprecated: Use reasoning_data instead
reasoning_data?: any; // Structured reasoning data for i18n translation
amount?: number;
currency?: string;
actions: ActionButton[];
@@ -111,7 +112,8 @@ export interface ProductionTimelineItem {
progress: number;
readyBy: string | null;
priority: string;
reasoning: string;
reasoning?: string; // Deprecated: Use reasoning_data instead
reasoning_data?: any; // Structured reasoning data for i18n translation
}
export interface ProductionTimeline {

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>

View File

@@ -50,52 +50,55 @@
"yellow": "Some items need attention",
"red": "Critical issues require immediate action",
"last_updated": "Last updated",
"next_check": "Next check"
"next_check": "Next check",
"never": "Never",
"critical_issues": "{{count}} critical issue{{count, plural, one {} other {s}}}",
"actions_needed": "{{count}} action{{count, plural, one {} other {s}}} needed"
},
"action_queue": {
"title": "What Needs Your Attention",
"why_needed": "Why this is needed:",
"what_if_not": "What happens if I don't do this?",
"estimated_time": "Estimated time: {{minutes}} min",
"estimated_time": "Estimated time",
"all_caught_up": "All caught up!",
"no_actions": "No actions requiring your attention right now.",
"show_more": "Show {{count}} More Action{{plural}}",
"show_more": "Show {{count}} More Action{{count, plural, one {} other {s}}}",
"show_less": "Show Less",
"critical_badge": "{{count}} critical",
"important_badge": "{{count}} important"
"total": "total",
"critical": "critical",
"important": "important"
},
"orchestration_summary": {
"title": "Last Night I Planned Your Day",
"no_runs": "Ready to Plan Your Bakery Day",
"no_runs_message": "The system hasn't run daily planning yet. Click 'Run Daily Planning' to generate your first plan.",
"run_number": "Orchestration run #{{number}}",
"duration": "Took {{seconds}}s",
"pos_created": "Created {{count}} purchase order{{plural}}",
"batches_created": "Scheduled {{count}} production batch{{plural}}",
"ready_to_plan": "Ready to Plan Your Bakery Day",
"run_planning": "Run Daily Planning",
"run_info": "Orchestration run #{{runNumber}}",
"took": "Took {{seconds}}s",
"created_pos": "Created {{count}} purchase order{{count, plural, one {} other {s}}}",
"scheduled_batches": "Scheduled {{count}} production batch{{count, plural, one {} other {es}}}",
"show_more": "Show {{count}} more",
"show_less": "Show less",
"no_actions": "No new actions needed - everything is on track!",
"based_on": "Based on:",
"customer_orders": "{{count}} customer order{{plural}}",
"customer_orders": "{{count}} customer order{{count, plural, one {} other {s}}}",
"historical_demand": "Historical demand",
"inventory_levels": "Inventory levels",
"ai_optimization": "AI optimization",
"actions_required": "{{count}} item{{plural}} need{{verb}} your approval before proceeding"
"actions_required": "{{count}} item{{count, plural, one {} other {s}}} need{{count, plural, one {s} other {}}} your approval before proceeding"
},
"production_timeline": {
"title": "Today's Production Timeline",
"no_batches": "No production batches scheduled for today",
"status": {
"pending": "Pending",
"in_progress": "In Progress",
"completed": "Completed",
"cancelled": "Cancelled"
},
"ready_by": "Ready by {{time}}",
"priority": {
"low": "Low Priority",
"normal": "Normal",
"high": "High Priority",
"urgent": "Urgent"
}
"title": "Your Production Plan Today",
"no_production": "No Production Scheduled",
"no_batches": "No batches are scheduled for production today.",
"ready_by": "Ready by",
"start_batch": "Start Batch",
"pause_batch": "Pause Batch",
"completed": "Completed",
"total": "Total",
"done": "Done",
"active": "Active",
"pending": "Pending",
"view_full_schedule": "View Full Production Schedule"
},
"insights": {
"savings": "Savings This Week",

View File

@@ -50,52 +50,55 @@
"yellow": "Algunos elementos necesitan atención",
"red": "Problemas críticos requieren acción inmediata",
"last_updated": "Última actualización",
"next_check": "Próxima verificación"
"next_check": "Próxima verificación",
"never": "Nunca",
"critical_issues": "{{count}} problema{{count, plural, one {} other {s}}} crítico{{count, plural, one {} other {s}}}",
"actions_needed": "{{count}} acción{{count, plural, one {} other {es}}} necesaria{{count, plural, one {} other {s}}}"
},
"action_queue": {
"title": "Qué Necesita Tu Atención",
"why_needed": "Por qué es necesario esto:",
"what_if_not": "¿Qué pasa si no hago esto?",
"estimated_time": "Tiempo estimado: {{minutes}} min",
"estimated_time": "Tiempo estimado",
"all_caught_up": "¡Todo al día!",
"no_actions": "No hay acciones que requieran tu atención en este momento.",
"show_more": "Mostrar {{count}} Acción{{plural}} Más",
"show_more": "Mostrar {{count}} Acción{{count, plural, one {} other {es}}} Más",
"show_less": "Mostrar Menos",
"critical_badge": "{{count}} crítico{{plural}}",
"important_badge": "{{count}} importante{{plural}}"
"total": "total",
"critical": "críticas",
"important": "importantes"
},
"orchestration_summary": {
"title": "Anoche Planifiqué Tu Día",
"no_runs": "Listo para Planificar Tu Día en la Panadería",
"no_runs_message": "El sistema aún no ha ejecutado la planificación diaria. Haz clic en 'Ejecutar Planificación Diaria' para generar tu primer plan.",
"run_number": "Ejecución de orquestación #{{number}}",
"duration": "Tardó {{seconds}}s",
"pos_created": "Creé {{count}} orden{{plural}} de compra",
"batches_created": "Programé {{count}} lote{{plural}} de producción",
"no_actions": "¡No se necesitan nuevas acciones - todo está en marcha!",
"ready_to_plan": "Listo Para Planificar Tu Día en la Panadería",
"run_planning": "Ejecutar Planificación Diaria",
"run_info": "Ejecución de orquestación #{{runNumber}}",
"took": "Duró {{seconds}}s",
"created_pos": "{{count}} orden{{count, plural, one {} other {es}}} de compra creada{{count, plural, one {} other {s}}}",
"scheduled_batches": "{{count}} lote{{count, plural, one {} other {s}}} de producción programado{{count, plural, one {} other {s}}}",
"show_more": "Mostrar {{count}} más",
"show_less": "Mostrar menos",
"no_actions": "¡No se necesitan nuevas acciones - todo va según lo planeado!",
"based_on": "Basado en:",
"customer_orders": "{{count}} pedido{{plural}} de cliente{{plural}}",
"customer_orders": "{{count}} pedido{{count, plural, one {} other {s}}} de cliente",
"historical_demand": "Demanda histórica",
"inventory_levels": "Niveles de inventario",
"ai_optimization": "Optimización con IA",
"actions_required": "{{count}} elemento{{plural}} necesita{{verb}} tu aprobación antes de proceder"
"ai_optimization": "Optimización por IA",
"actions_required": "{{count}} elemento{{count, plural, one {} other {s}}} necesita{{count, plural, one {} other {n}}} tu aprobación antes de continuar"
},
"production_timeline": {
"title": "Línea de Tiempo de Producción de Hoy",
"no_batches": "No hay lotes de producción programados para hoy",
"status": {
"pending": "Pendiente",
"in_progress": "En Proceso",
"completed": "Completado",
"cancelled": "Cancelado"
},
"ready_by": "Listo para {{time}}",
"priority": {
"low": "Prioridad Baja",
"normal": "Normal",
"high": "Prioridad Alta",
"urgent": "Urgente"
}
"title": "Tu Plan de Producción de Hoy",
"no_production": "No Hay Producción Programada",
"no_batches": "No hay lotes programados para producción hoy.",
"ready_by": "Listo para",
"start_batch": "Iniciar Lote",
"pause_batch": "Pausar Lote",
"completed": "Completado",
"total": "Total",
"done": "Hecho",
"active": "Activo",
"pending": "Pendiente",
"view_full_schedule": "Ver Cronograma Completo de Producción"
},
"insights": {
"savings": "Ahorros Esta Semana",

View File

@@ -52,52 +52,55 @@ ko da.",
"yellow": "Elementu batzuek arreta behar dute",
"red": "Arazo kritikoek ekintza berehalakoa behar dute",
"last_updated": "Azken eguneratzea",
"next_check": "Hurrengo egiaztapena"
"next_check": "Hurrengo egiaztapena",
"never": "Inoiz ez",
"critical_issues": "{{count}} arazo kritiko",
"actions_needed": "{{count}} ekintza behar"
},
"action_queue": {
"title": "Zer Behar Du Zure Arreta",
"why_needed": "Zergatik behar da hau:",
"what_if_not": "Zer gertatzen da hau egiten ez badut?",
"estimated_time": "Estimatutako denbora: {{minutes}} min",
"estimated_time": "Estimatutako denbora",
"all_caught_up": "Dena egunean!",
"no_actions": "Ez dago une honetan zure arreta behar duen ekintzarik.",
"show_more": "Erakutsi {{count}} Ekintza{{plural}} Gehiago",
"show_more": "Erakutsi {{count}} Ekintza gehiago",
"show_less": "Erakutsi Gutxiago",
"critical_badge": "{{count}} kritiko{{plural}}",
"important_badge": "{{count}} garrantzitsu{{plural}}"
"total": "guztira",
"critical": "kritiko",
"important": "garrantzitsu"
},
"orchestration_summary": {
"title": "Bart Gauean Zure Eguna Planifikatu Nuen",
"no_runs": "Zure Okindegiko Eguna Planifikatzeko Prest",
"no_runs_message": "Sistemak oraindik ez du eguneko plangintza exekutatu. Egin klik 'Exekutatu Eguneko Plangintza'-n zure lehen plana sortzeko.",
"run_number": "Orkestazio exekuzioa #{{number}}",
"duration": "{{seconds}}s behar izan zituen",
"pos_created": "{{count}} erosketa agindu{{plural}} sortu nituen",
"batches_created": "{{count}} ekoizpen lote{{plural}} programatu nituen",
"ready_to_plan": "Zure Okindegiko Eguna Planifikatzeko Prest",
"run_planning": "Exekutatu Eguneko Plangintza",
"run_info": "Orkestazio exekuzioa #{{runNumber}}",
"took": "{{seconds}}s behar izan zituen",
"created_pos": "{{count}} erosketa agindu sortu",
"scheduled_batches": "{{count}} ekoizpen lote programatu",
"show_more": "Erakutsi {{count}} gehiago",
"show_less": "Erakutsi gutxiago",
"no_actions": "Ez dira ekintza berriak behar - dena bidean dago!",
"based_on": "Oinarrituta:",
"customer_orders": "{{count}} bezero eskaera{{plural}}",
"customer_orders": "{{count}} bezero eskaera",
"historical_demand": "Eskaera historikoa",
"inventory_levels": "Inbentario mailak",
"ai_optimization": "IA optimizazioa",
"actions_required": "{{count}} elementu{{plural}}k zure onespena behar du{{verb}} aurrera jarraitu aurretik"
"actions_required": "{{count}} elementuk zure onespena behar du aurrera jarraitu aurretik"
},
"production_timeline": {
"title": "Gaurko Ekoizpen Denbora-lerroa",
"no_batches": "Ez dago ekoizpen loterik programatuta gaurko",
"status": {
"pending": "Zain",
"in_progress": "Prozesuan",
"completed": "Osatua",
"cancelled": "Bertan behera utzita"
},
"ready_by": "{{time}}rako prest",
"priority": {
"low": "Lehentasun Baxua",
"normal": "Normala",
"high": "Lehentasun Altua",
"urgent": "Larria"
}
"title": "Zure Gaurko Ekoizpen Plana",
"no_production": "Ez Dago Ekoizpenik Programatuta",
"no_batches": "Ez dago loterik programatuta gaur ekoizpenerako.",
"ready_by": "Prest egongo da",
"start_batch": "Hasi Lotea",
"pause_batch": "Pausatu Lotea",
"completed": "Osatua",
"total": "Guztira",
"done": "Eginda",
"active": "Aktibo",
"pending": "Zain",
"view_full_schedule": "Ikusi Ekoizpen Egutegi Osoa"
},
"insights": {
"savings": "Aste Honetako Aurrezkiak",