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