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

View File

@@ -21,6 +21,8 @@ import {
ChevronUp, ChevronUp,
} from 'lucide-react'; } from 'lucide-react';
import { ActionItem, ActionQueue } from '../../api/hooks/newDashboard'; import { ActionItem, ActionQueue } from '../../api/hooks/newDashboard';
import { useReasoningFormatter } from '../../hooks/useReasoningTranslation';
import { useTranslation } from 'react-i18next';
interface ActionQueueCardProps { interface ActionQueueCardProps {
actionQueue: ActionQueue; actionQueue: ActionQueue;
@@ -65,6 +67,13 @@ function ActionItemCard({
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const config = urgencyConfig[action.urgency]; const config = urgencyConfig[action.urgency];
const UrgencyIcon = config.icon; 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 ( return (
<div <div
@@ -96,29 +105,42 @@ function ActionItemCard({
{/* Reasoning (always visible) */} {/* Reasoning (always visible) */}
<div className="bg-white rounded-md p-3 mb-3"> <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 font-medium text-gray-700 mb-1">
<p className="text-sm text-gray-600">{action.reasoning}</p> {t('jtbd.action_queue.why_needed')}
</p>
<p className="text-sm text-gray-600">{reasoning}</p>
</div> </div>
{/* Consequence (expandable) */} {/* Consequence (expandable) */}
<button {consequence && (
onClick={() => setExpanded(!expanded)} <>
className="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900 transition-colors mb-3 w-full" <button
> onClick={() => setExpanded(!expanded)}
{expanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />} className="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900 transition-colors mb-3 w-full"
<span className="font-medium">What happens if I don't do this?</span> >
</button> {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 && ( {expanded && (
<div className="bg-amber-50 border border-amber-200 rounded-md p-3 mb-3"> <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> <p className="text-sm text-amber-900">{consequence}</p>
</div> {severity && (
<span className="text-xs font-semibold text-amber-900 mt-1 block">
{severity}
</span>
)}
</div>
)}
</>
)} )}
{/* Time Estimate */} {/* Time Estimate */}
<div className="flex items-center gap-2 text-xs text-gray-500 mb-4"> <div className="flex items-center gap-2 text-xs text-gray-500 mb-4">
<Clock className="w-4 h-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> </div>
{/* Action Buttons */} {/* Action Buttons */}
@@ -167,6 +189,7 @@ export function ActionQueueCard({
}: ActionQueueCardProps) { }: ActionQueueCardProps) {
const [showAll, setShowAll] = useState(false); const [showAll, setShowAll] = useState(false);
const displayedActions = showAll ? actionQueue.actions : actionQueue.actions.slice(0, 3); const displayedActions = showAll ? actionQueue.actions : actionQueue.actions.slice(0, 3);
const { t } = useTranslation('reasoning');
if (loading) { if (loading) {
return ( return (
@@ -184,8 +207,10 @@ export function ActionQueueCard({
return ( return (
<div className="bg-green-50 border-2 border-green-200 rounded-xl p-8 text-center"> <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" /> <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> <h3 className="text-xl font-bold text-green-900 mb-2">
<p className="text-green-700">No actions requiring your attention right now.</p> {t('jtbd.action_queue.all_caught_up')}
</h3>
<p className="text-green-700">{t('jtbd.action_queue.no_actions')}</p>
</div> </div>
); );
} }
@@ -194,10 +219,10 @@ export function ActionQueueCard({
<div className="bg-white rounded-xl shadow-md p-6"> <div className="bg-white rounded-xl shadow-md p-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-6"> <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 && ( {actionQueue.totalActions > 3 && (
<span className="bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-semibold"> <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> </span>
)} )}
</div> </div>
@@ -207,12 +232,12 @@ export function ActionQueueCard({
<div className="flex flex-wrap gap-2 mb-6"> <div className="flex flex-wrap gap-2 mb-6">
{actionQueue.criticalCount > 0 && ( {actionQueue.criticalCount > 0 && (
<span className="bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-semibold"> <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> </span>
)} )}
{actionQueue.importantCount > 0 && ( {actionQueue.importantCount > 0 && (
<span className="bg-amber-100 text-amber-800 px-3 py-1 rounded-full text-sm font-semibold"> <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> </span>
)} )}
</div> </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" 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 {showAll
? 'Show Less' ? t('jtbd.action_queue.show_less')
: `Show ${actionQueue.totalActions - 3} More Action${ : t('jtbd.action_queue.show_more', { count: actionQueue.totalActions - 3 })}
actionQueue.totalActions - 3 !== 1 ? 's' : ''
}`}
</button> </button>
)} )}
</div> </div>

View File

@@ -12,6 +12,7 @@ import React from 'react';
import { CheckCircle, AlertTriangle, AlertCircle, Clock, RefreshCw } from 'lucide-react'; import { CheckCircle, AlertTriangle, AlertCircle, Clock, RefreshCw } from 'lucide-react';
import { BakeryHealthStatus } from '../../api/hooks/newDashboard'; import { BakeryHealthStatus } from '../../api/hooks/newDashboard';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { useTranslation } from 'react-i18next';
interface HealthStatusCardProps { interface HealthStatusCardProps {
healthStatus: BakeryHealthStatus; healthStatus: BakeryHealthStatus;
@@ -51,6 +52,7 @@ const iconMap = {
export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProps) { export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProps) {
const config = statusConfig[healthStatus.status]; const config = statusConfig[healthStatus.status];
const StatusIcon = config.icon; const StatusIcon = config.icon;
const { t } = useTranslation('reasoning');
if (loading) { if (loading) {
return ( return (
@@ -79,12 +81,12 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
<div className="flex items-center gap-2 text-sm text-gray-600"> <div className="flex items-center gap-2 text-sm text-gray-600">
<Clock className="w-4 h-4" /> <Clock className="w-4 h-4" />
<span> <span>
Last updated:{' '} {t('jtbd.health_status.last_updated')}:{' '}
{healthStatus.lastOrchestrationRun {healthStatus.lastOrchestrationRun
? formatDistanceToNow(new Date(healthStatus.lastOrchestrationRun), { ? formatDistanceToNow(new Date(healthStatus.lastOrchestrationRun), {
addSuffix: true, addSuffix: true,
}) })
: 'Never'} : t('jtbd.health_status.never')}
</span> </span>
</div> </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"> <div className="flex items-center gap-2 text-sm text-gray-600 mt-1">
<RefreshCw className="w-4 h-4" /> <RefreshCw className="w-4 h-4" />
<span> <span>
Next check:{' '} {t('jtbd.health_status.next_check')}:{' '}
{formatDistanceToNow(new Date(healthStatus.nextScheduledRun), { addSuffix: true })} {formatDistanceToNow(new Date(healthStatus.nextScheduledRun), { addSuffix: true })}
</span> </span>
</div> </div>
@@ -129,7 +131,7 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-red-600" /> <AlertCircle className="w-4 h-4 text-red-600" />
<span className="font-semibold text-red-800"> <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> </span>
</div> </div>
)} )}
@@ -137,7 +139,7 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AlertTriangle className="w-4 h-4 text-amber-600" /> <AlertTriangle className="w-4 h-4 text-amber-600" />
<span className="font-semibold text-amber-800"> <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> </span>
</div> </div>
)} )}

View File

@@ -24,6 +24,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { OrchestrationSummary } from '../../api/hooks/newDashboard'; import { OrchestrationSummary } from '../../api/hooks/newDashboard';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { useTranslation } from 'react-i18next';
interface OrchestrationSummaryCardProps { interface OrchestrationSummaryCardProps {
summary: OrchestrationSummary; summary: OrchestrationSummary;
@@ -32,6 +33,7 @@ interface OrchestrationSummaryCardProps {
export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSummaryCardProps) { export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSummaryCardProps) {
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const { t } = useTranslation('reasoning');
if (loading) { if (loading) {
return ( return (
@@ -58,11 +60,11 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
<Bot className="w-10 h-10 text-blue-600 flex-shrink-0" /> <Bot className="w-10 h-10 text-blue-600 flex-shrink-0" />
<div> <div>
<h3 className="text-lg font-bold text-blue-900 mb-2"> <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> </h3>
<p className="text-blue-700 mb-4">{summary.message}</p> <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"> <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> </button>
</div> </div>
</div> </div>
@@ -83,13 +85,17 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
</div> </div>
<div className="flex-1"> <div className="flex-1">
<h2 className="text-2xl font-bold text-gray-900 mb-1"> <h2 className="text-2xl font-bold text-gray-900 mb-1">
Last Night I Planned Your Day {t('jtbd.orchestration_summary.title')}
</h2> </h2>
<div className="flex items-center gap-2 text-sm text-gray-600"> <div className="flex items-center gap-2 text-sm text-gray-600">
<Clock className="w-4 h-4" /> <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 && ( {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>
</div> </div>
@@ -101,8 +107,7 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
<div className="flex items-center gap-3 mb-3"> <div className="flex items-center gap-3 mb-3">
<CheckCircle className="w-5 h-5 text-green-600" /> <CheckCircle className="w-5 h-5 text-green-600" />
<h3 className="font-bold text-gray-900"> <h3 className="font-bold text-gray-900">
Created {summary.purchaseOrdersCreated} purchase order {t('jtbd.orchestration_summary.created_pos', { count: summary.purchaseOrdersCreated })}
{summary.purchaseOrdersCreated !== 1 ? 's' : ''}
</h3> </h3>
</div> </div>
@@ -129,8 +134,9 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
<div className="flex items-center gap-3 mb-3"> <div className="flex items-center gap-3 mb-3">
<CheckCircle className="w-5 h-5 text-green-600" /> <CheckCircle className="w-5 h-5 text-green-600" />
<h3 className="font-bold text-gray-900"> <h3 className="font-bold text-gray-900">
Scheduled {summary.productionBatchesCreated} production batch {t('jtbd.orchestration_summary.scheduled_batches', {
{summary.productionBatchesCreated !== 1 ? 'es' : ''} count: summary.productionBatchesCreated,
})}
</h3> </h3>
</div> </div>
@@ -160,12 +166,14 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
{expanded ? ( {expanded ? (
<> <>
<ChevronUp className="w-4 h-4" /> <ChevronUp className="w-4 h-4" />
Show less {t('jtbd.orchestration_summary.show_less')}
</> </>
) : ( ) : (
<> <>
<ChevronDown className="w-4 h-4" /> <ChevronDown className="w-4 h-4" />
Show {summary.productionBatchesSummary.length - 3} more {t('jtbd.orchestration_summary.show_more', {
count: summary.productionBatchesSummary.length - 3,
})}
</> </>
)} )}
</button> </button>
@@ -178,7 +186,7 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
<div className="bg-white rounded-lg p-4 mb-4"> <div className="bg-white rounded-lg p-4 mb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<CheckCircle className="w-5 h-5 text-gray-400" /> <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>
</div> </div>
)} )}
@@ -187,7 +195,7 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
<div className="bg-white/60 rounded-lg p-4"> <div className="bg-white/60 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<Brain className="w-5 h-5 text-purple-600" /> <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>
<div className="grid grid-cols-2 gap-3 ml-7"> <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"> <div className="flex items-center gap-2 text-sm">
<Users className="w-4 h-4 text-gray-600" /> <Users className="w-4 h-4 text-gray-600" />
<span className="text-gray-700"> <span className="text-gray-700">
{summary.reasoningInputs.customerOrders} customer order {t('jtbd.orchestration_summary.customer_orders', {
{summary.reasoningInputs.customerOrders !== 1 ? 's' : ''} count: summary.reasoningInputs.customerOrders,
})}
</span> </span>
</div> </div>
)} )}
@@ -204,21 +213,25 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
{summary.reasoningInputs.historicalDemand && ( {summary.reasoningInputs.historicalDemand && (
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<TrendingUp className="w-4 h-4 text-gray-600" /> <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> </div>
)} )}
{summary.reasoningInputs.inventoryLevels && ( {summary.reasoningInputs.inventoryLevels && (
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<Package className="w-4 h-4 text-gray-600" /> <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> </div>
)} )}
{summary.reasoningInputs.aiInsights && ( {summary.reasoningInputs.aiInsights && (
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<Brain className="w-4 h-4 text-purple-600" /> <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>
)} )}
</div> </div>
@@ -230,8 +243,9 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FileText className="w-5 h-5 text-amber-600" /> <FileText className="w-5 h-5 text-amber-600" />
<p className="text-sm font-medium text-amber-900"> <p className="text-sm font-medium text-amber-900">
{summary.userActionsRequired} item{summary.userActionsRequired !== 1 ? 's' : ''} need {t('jtbd.orchestration_summary.actions_required', {
{summary.userActionsRequired === 1 ? 's' : ''} your approval before proceeding count: summary.userActionsRequired,
})}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -10,6 +10,8 @@
import React from 'react'; import React from 'react';
import { Factory, Clock, Play, Pause, CheckCircle2 } from 'lucide-react'; import { Factory, Clock, Play, Pause, CheckCircle2 } from 'lucide-react';
import { ProductionTimeline, ProductionTimelineItem } from '../../api/hooks/newDashboard'; import { ProductionTimeline, ProductionTimelineItem } from '../../api/hooks/newDashboard';
import { useReasoningFormatter } from '../../hooks/useReasoningTranslation';
import { useTranslation } from 'react-i18next';
interface ProductionTimelineCardProps { interface ProductionTimelineCardProps {
timeline: ProductionTimeline; timeline: ProductionTimeline;
@@ -35,6 +37,13 @@ function TimelineItemCard({
onPause?: (id: string) => void; onPause?: (id: string) => void;
}) { }) {
const priorityColor = priorityColors[item.priority as keyof typeof priorityColors] || 'text-gray-600'; 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 const startTime = item.plannedStartTime
? new Date(item.plannedStartTime).toLocaleTimeString('en-US', { ? new Date(item.plannedStartTime).toLocaleTimeString('en-US', {
@@ -99,13 +108,15 @@ function TimelineItemCard({
{item.status !== 'COMPLETED' && ( {item.status !== 'COMPLETED' && (
<div className="flex items-center gap-2 text-sm text-gray-600 mb-2"> <div className="flex items-center gap-2 text-sm text-gray-600 mb-2">
<Clock className="w-4 h-4" /> <Clock className="w-4 h-4" />
<span>Ready by: {readyByTime}</span> <span>
{t('jtbd.production_timeline.ready_by')}: {readyByTime}
</span>
</div> </div>
)} )}
{/* Reasoning */} {/* Reasoning */}
{item.reasoning && ( {reasoning && (
<p className="text-sm text-gray-600 italic mb-3">"{item.reasoning}"</p> <p className="text-sm text-gray-600 italic mb-3">"{reasoning}"</p>
)} )}
{/* Actions */} {/* 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" 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" /> <Play className="w-4 h-4" />
Start Batch {t('jtbd.production_timeline.start_batch')}
</button> </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" 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 className="w-4 h-4" />
Pause Batch {t('jtbd.production_timeline.pause_batch')}
</button> </button>
)} )}
{item.status === 'COMPLETED' && ( {item.status === 'COMPLETED' && (
<div className="flex items-center gap-2 text-sm text-green-600 font-medium"> <div className="flex items-center gap-2 text-sm text-green-600 font-medium">
<CheckCircle2 className="w-4 h-4" /> <CheckCircle2 className="w-4 h-4" />
Completed {t('jtbd.production_timeline.completed')}
</div> </div>
)} )}
</div> </div>
@@ -146,6 +157,8 @@ export function ProductionTimelineCard({
onStart, onStart,
onPause, onPause,
}: ProductionTimelineCardProps) { }: ProductionTimelineCardProps) {
const { t } = useTranslation('reasoning');
if (loading) { if (loading) {
return ( return (
<div className="bg-white rounded-xl shadow-md p-6"> <div className="bg-white rounded-xl shadow-md p-6">
@@ -164,8 +177,10 @@ export function ProductionTimelineCard({
return ( return (
<div className="bg-white rounded-xl shadow-md p-8 text-center"> <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" /> <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> <h3 className="text-xl font-bold text-gray-700 mb-2">
<p className="text-gray-600">No batches are scheduled for production today.</p> {t('jtbd.production_timeline.no_production')}
</h3>
<p className="text-gray-600">{t('jtbd.production_timeline.no_batches')}</p>
</div> </div>
); );
} }
@@ -176,7 +191,7 @@ export function ProductionTimelineCard({
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Factory className="w-8 h-8 text-blue-600" /> <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>
</div> </div>
@@ -184,19 +199,19 @@ export function ProductionTimelineCard({
<div className="grid grid-cols-4 gap-4 mb-6"> <div className="grid grid-cols-4 gap-4 mb-6">
<div className="bg-gray-50 rounded-lg p-3 text-center"> <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-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>
<div className="bg-green-50 rounded-lg p-3 text-center"> <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-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>
<div className="bg-blue-50 rounded-lg p-3 text-center"> <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-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>
<div className="bg-gray-50 rounded-lg p-3 text-center"> <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-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>
</div> </div>
@@ -215,7 +230,7 @@ export function ProductionTimelineCard({
{/* View Full Schedule Link */} {/* View Full Schedule Link */}
{timeline.totalBatches > 5 && ( {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"> <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> </button>
)} )}
</div> </div>

View File

@@ -50,52 +50,55 @@
"yellow": "Some items need attention", "yellow": "Some items need attention",
"red": "Critical issues require immediate action", "red": "Critical issues require immediate action",
"last_updated": "Last updated", "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": { "action_queue": {
"title": "What Needs Your Attention", "title": "What Needs Your Attention",
"why_needed": "Why this is needed:", "why_needed": "Why this is needed:",
"what_if_not": "What happens if I don't do this?", "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!", "all_caught_up": "All caught up!",
"no_actions": "No actions requiring your attention right now.", "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", "show_less": "Show Less",
"critical_badge": "{{count}} critical", "total": "total",
"important_badge": "{{count}} important" "critical": "critical",
"important": "important"
}, },
"orchestration_summary": { "orchestration_summary": {
"title": "Last Night I Planned Your Day", "title": "Last Night I Planned Your Day",
"no_runs": "Ready to Plan Your Bakery Day", "ready_to_plan": "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_planning": "Run Daily Planning",
"run_number": "Orchestration run #{{number}}", "run_info": "Orchestration run #{{runNumber}}",
"duration": "Took {{seconds}}s", "took": "Took {{seconds}}s",
"pos_created": "Created {{count}} purchase order{{plural}}", "created_pos": "Created {{count}} purchase order{{count, plural, one {} other {s}}}",
"batches_created": "Scheduled {{count}} production batch{{plural}}", "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!", "no_actions": "No new actions needed - everything is on track!",
"based_on": "Based on:", "based_on": "Based on:",
"customer_orders": "{{count}} customer order{{plural}}", "customer_orders": "{{count}} customer order{{count, plural, one {} other {s}}}",
"historical_demand": "Historical demand", "historical_demand": "Historical demand",
"inventory_levels": "Inventory levels", "inventory_levels": "Inventory levels",
"ai_optimization": "AI optimization", "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": { "production_timeline": {
"title": "Today's Production Timeline", "title": "Your Production Plan Today",
"no_batches": "No production batches scheduled for today", "no_production": "No Production Scheduled",
"status": { "no_batches": "No batches are scheduled for production today.",
"pending": "Pending", "ready_by": "Ready by",
"in_progress": "In Progress", "start_batch": "Start Batch",
"completed": "Completed", "pause_batch": "Pause Batch",
"cancelled": "Cancelled" "completed": "Completed",
}, "total": "Total",
"ready_by": "Ready by {{time}}", "done": "Done",
"priority": { "active": "Active",
"low": "Low Priority", "pending": "Pending",
"normal": "Normal", "view_full_schedule": "View Full Production Schedule"
"high": "High Priority",
"urgent": "Urgent"
}
}, },
"insights": { "insights": {
"savings": "Savings This Week", "savings": "Savings This Week",

View File

@@ -50,52 +50,55 @@
"yellow": "Algunos elementos necesitan atención", "yellow": "Algunos elementos necesitan atención",
"red": "Problemas críticos requieren acción inmediata", "red": "Problemas críticos requieren acción inmediata",
"last_updated": "Última actualización", "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": { "action_queue": {
"title": "Qué Necesita Tu Atención", "title": "Qué Necesita Tu Atención",
"why_needed": "Por qué es necesario esto:", "why_needed": "Por qué es necesario esto:",
"what_if_not": "¿Qué pasa si no hago 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!", "all_caught_up": "¡Todo al día!",
"no_actions": "No hay acciones que requieran tu atención en este momento.", "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", "show_less": "Mostrar Menos",
"critical_badge": "{{count}} crítico{{plural}}", "total": "total",
"important_badge": "{{count}} importante{{plural}}" "critical": "críticas",
"important": "importantes"
}, },
"orchestration_summary": { "orchestration_summary": {
"title": "Anoche Planifiqué Tu Día", "title": "Anoche Planifiqué Tu Día",
"no_runs": "Listo para Planificar Tu Día en la Panadería", "ready_to_plan": "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_planning": "Ejecutar Planificación Diaria",
"run_number": "Ejecución de orquestación #{{number}}", "run_info": "Ejecución de orquestación #{{runNumber}}",
"duration": "Tardó {{seconds}}s", "took": "Duró {{seconds}}s",
"pos_created": "Creé {{count}} orden{{plural}} de compra", "created_pos": "{{count}} orden{{count, plural, one {} other {es}}} de compra creada{{count, plural, one {} other {s}}}",
"batches_created": "Programé {{count}} lote{{plural}} de producción", "scheduled_batches": "{{count}} lote{{count, plural, one {} other {s}}} de producción programado{{count, plural, one {} other {s}}}",
"no_actions": "¡No se necesitan nuevas acciones - todo está en marcha!", "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:", "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", "historical_demand": "Demanda histórica",
"inventory_levels": "Niveles de inventario", "inventory_levels": "Niveles de inventario",
"ai_optimization": "Optimización con IA", "ai_optimization": "Optimización por IA",
"actions_required": "{{count}} elemento{{plural}} necesita{{verb}} tu aprobación antes de proceder" "actions_required": "{{count}} elemento{{count, plural, one {} other {s}}} necesita{{count, plural, one {} other {n}}} tu aprobación antes de continuar"
}, },
"production_timeline": { "production_timeline": {
"title": "Línea de Tiempo de Producción de Hoy", "title": "Tu Plan de Producción de Hoy",
"no_batches": "No hay lotes de producción programados para hoy", "no_production": "No Hay Producción Programada",
"status": { "no_batches": "No hay lotes programados para producción hoy.",
"pending": "Pendiente", "ready_by": "Listo para",
"in_progress": "En Proceso", "start_batch": "Iniciar Lote",
"completed": "Completado", "pause_batch": "Pausar Lote",
"cancelled": "Cancelado" "completed": "Completado",
}, "total": "Total",
"ready_by": "Listo para {{time}}", "done": "Hecho",
"priority": { "active": "Activo",
"low": "Prioridad Baja", "pending": "Pendiente",
"normal": "Normal", "view_full_schedule": "Ver Cronograma Completo de Producción"
"high": "Prioridad Alta",
"urgent": "Urgente"
}
}, },
"insights": { "insights": {
"savings": "Ahorros Esta Semana", "savings": "Ahorros Esta Semana",

View File

@@ -52,52 +52,55 @@ ko da.",
"yellow": "Elementu batzuek arreta behar dute", "yellow": "Elementu batzuek arreta behar dute",
"red": "Arazo kritikoek ekintza berehalakoa behar dute", "red": "Arazo kritikoek ekintza berehalakoa behar dute",
"last_updated": "Azken eguneratzea", "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": { "action_queue": {
"title": "Zer Behar Du Zure Arreta", "title": "Zer Behar Du Zure Arreta",
"why_needed": "Zergatik behar da hau:", "why_needed": "Zergatik behar da hau:",
"what_if_not": "Zer gertatzen da hau egiten ez badut?", "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!", "all_caught_up": "Dena egunean!",
"no_actions": "Ez dago une honetan zure arreta behar duen ekintzarik.", "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", "show_less": "Erakutsi Gutxiago",
"critical_badge": "{{count}} kritiko{{plural}}", "total": "guztira",
"important_badge": "{{count}} garrantzitsu{{plural}}" "critical": "kritiko",
"important": "garrantzitsu"
}, },
"orchestration_summary": { "orchestration_summary": {
"title": "Bart Gauean Zure Eguna Planifikatu Nuen", "title": "Bart Gauean Zure Eguna Planifikatu Nuen",
"no_runs": "Zure Okindegiko Eguna Planifikatzeko Prest", "ready_to_plan": "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_planning": "Exekutatu Eguneko Plangintza",
"run_number": "Orkestazio exekuzioa #{{number}}", "run_info": "Orkestazio exekuzioa #{{runNumber}}",
"duration": "{{seconds}}s behar izan zituen", "took": "{{seconds}}s behar izan zituen",
"pos_created": "{{count}} erosketa agindu{{plural}} sortu nituen", "created_pos": "{{count}} erosketa agindu sortu",
"batches_created": "{{count}} ekoizpen lote{{plural}} programatu nituen", "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!", "no_actions": "Ez dira ekintza berriak behar - dena bidean dago!",
"based_on": "Oinarrituta:", "based_on": "Oinarrituta:",
"customer_orders": "{{count}} bezero eskaera{{plural}}", "customer_orders": "{{count}} bezero eskaera",
"historical_demand": "Eskaera historikoa", "historical_demand": "Eskaera historikoa",
"inventory_levels": "Inbentario mailak", "inventory_levels": "Inbentario mailak",
"ai_optimization": "IA optimizazioa", "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": { "production_timeline": {
"title": "Gaurko Ekoizpen Denbora-lerroa", "title": "Zure Gaurko Ekoizpen Plana",
"no_batches": "Ez dago ekoizpen loterik programatuta gaurko", "no_production": "Ez Dago Ekoizpenik Programatuta",
"status": { "no_batches": "Ez dago loterik programatuta gaur ekoizpenerako.",
"pending": "Zain", "ready_by": "Prest egongo da",
"in_progress": "Prozesuan", "start_batch": "Hasi Lotea",
"completed": "Osatua", "pause_batch": "Pausatu Lotea",
"cancelled": "Bertan behera utzita" "completed": "Osatua",
}, "total": "Guztira",
"ready_by": "{{time}}rako prest", "done": "Eginda",
"priority": { "active": "Aktibo",
"low": "Lehentasun Baxua", "pending": "Zain",
"normal": "Normala", "view_full_schedule": "Ikusi Ekoizpen Egutegi Osoa"
"high": "Lehentasun Altua",
"urgent": "Larria"
}
}, },
"insights": { "insights": {
"savings": "Aste Honetako Aurrezkiak", "savings": "Aste Honetako Aurrezkiak",