fix demo session 3

This commit is contained in:
Urtzi Alfaro
2026-01-02 13:27:48 +01:00
parent 0a1951051f
commit ddf95958d2
5 changed files with 419 additions and 243 deletions

View File

@@ -10,6 +10,8 @@ import { Package, AlertTriangle, CheckCircle2, Activity, Clock, Warehouse, Shopp
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import StatusCard from '../ui/StatusCard/StatusCard'; import StatusCard from '../ui/StatusCard/StatusCard';
import { useSSEEvents } from '../../hooks/useSSE'; import { useSSEEvents } from '../../hooks/useSSE';
import { useChildTenants } from '../../api/hooks/useEnterpriseDashboard';
import { inventoryService } from '../../api/services/inventory';
interface OutletFulfillmentTabProps { interface OutletFulfillmentTabProps {
tenantId: string; tenantId: string;
@@ -21,67 +23,28 @@ const OutletFulfillmentTab: React.FC<OutletFulfillmentTabProps> = ({ tenantId, o
const [selectedOutlet, setSelectedOutlet] = useState<string | null>(null); const [selectedOutlet, setSelectedOutlet] = useState<string | null>(null);
const [viewMode, setViewMode] = useState<'summary' | 'detailed'>('summary'); const [viewMode, setViewMode] = useState<'summary' | 'detailed'>('summary');
// Real-time SSE events // Get child tenants data
const { events: sseEvents, isConnected: sseConnected } = useSSEEvents({ const { data: childTenants, isLoading: isChildTenantsLoading } = useChildTenants(tenantId);
channels: ['*.alerts', '*.notifications', 'recommendations']
});
// State for real-time inventory data // State for real-time inventory data
const [inventoryData, setInventoryData] = useState([ const [inventoryData, setInventoryData] = useState<any[]>([]);
{ const [loading, setLoading] = useState(true);
id: 'outlet-madrid',
name: 'Madrid Central', // Combine loading states
inventoryCoverage: 85, const isLoading = isChildTenantsLoading || loading;
stockoutRisk: 'low',
criticalItems: 2, // Real-time SSE events
fulfillmentRate: 98, const { events: sseEvents, isConnected: sseConnected } = useSSEEvents({
lastUpdated: '2024-01-15T10:30:00', channels: ['*.alerts', '*.notifications', 'recommendations']
status: 'normal', });
products: [
{ id: 'baguette', name: 'Baguette', coverage: 92, risk: 'low', stock: 450, safetyStock: 300 },
{ id: 'croissant', name: 'Croissant', coverage: 78, risk: 'medium', stock: 280, safetyStock: 250 },
{ id: 'pain-au-chocolat', name: 'Pain au Chocolat', coverage: 65, risk: 'high', stock: 180, safetyStock: 200 }
]
},
{
id: 'outlet-barcelona',
name: 'Barcelona Coastal',
inventoryCoverage: 68,
stockoutRisk: 'medium',
criticalItems: 5,
fulfillmentRate: 92,
lastUpdated: '2024-01-15T10:25:00',
status: 'warning',
products: [
{ id: 'baguette', name: 'Baguette', coverage: 75, risk: 'medium', stock: 320, safetyStock: 300 },
{ id: 'croissant', name: 'Croissant', coverage: 58, risk: 'high', stock: 220, safetyStock: 250 },
{ id: 'ensaimada', name: 'Ensaimada', coverage: 45, risk: 'critical', stock: 120, safetyStock: 200 }
]
},
{
id: 'outlet-valencia',
name: 'Valencia Port',
inventoryCoverage: 72,
stockoutRisk: 'medium',
criticalItems: 3,
fulfillmentRate: 95,
lastUpdated: '2024-01-15T10:20:00',
status: 'warning',
products: [
{ id: 'baguette', name: 'Baguette', coverage: 88, risk: 'low', stock: 420, safetyStock: 300 },
{ id: 'croissant', name: 'Croissant', coverage: 65, risk: 'medium', stock: 240, safetyStock: 250 },
{ id: 'focaccia', name: 'Focaccia', coverage: 55, risk: 'high', stock: 160, safetyStock: 200 }
]
}
]);
// Process SSE events for inventory updates // Process SSE events for inventory updates
useEffect(() => { useEffect(() => {
if (sseEvents.length === 0) return; if (sseEvents.length === 0 || inventoryData.length === 0) return;
// Filter inventory-related events // Filter inventory-related events
const inventoryEvents = sseEvents.filter(event => const inventoryEvents = sseEvents.filter(event =>
event.event_type.includes('inventory_') || event.event_type.includes('inventory_') ||
event.event_type.includes('stock_') || event.event_type.includes('stock_') ||
event.event_type === 'stock_receipt_incomplete' || event.event_type === 'stock_receipt_incomplete' ||
event.entity_type === 'inventory' event.entity_type === 'inventory'
@@ -93,8 +56,8 @@ const OutletFulfillmentTab: React.FC<OutletFulfillmentTabProps> = ({ tenantId, o
setInventoryData(prevData => { setInventoryData(prevData => {
return prevData.map(outlet => { return prevData.map(outlet => {
// Find events for this outlet // Find events for this outlet
const outletEvents = inventoryEvents.filter(event => const outletEvents = inventoryEvents.filter(event =>
event.entity_id === outlet.id || event.entity_id === outlet.id ||
event.event_metadata?.outlet_id === outlet.id event.event_metadata?.outlet_id === outlet.id
); );
@@ -145,7 +108,84 @@ const OutletFulfillmentTab: React.FC<OutletFulfillmentTabProps> = ({ tenantId, o
}; };
}); });
}); });
}, [sseEvents]); }, [sseEvents, inventoryData]);
// Fetch inventory data for each child tenant individually
useEffect(() => {
if (!childTenants) {
setInventoryData([]);
setLoading(true);
return;
}
const fetchAllInventoryData = async () => {
setLoading(true);
try {
const promises = childTenants.map(async (tenant) => {
try {
// Using the imported service directly
const inventoryData = await inventoryService.getDashboardSummary(tenant.id);
return { tenant, inventoryData };
} catch (error) {
console.error(`Error fetching inventory for tenant ${tenant.id}:`, error);
return { tenant, inventoryData: null };
}
});
const results = await Promise.all(promises);
const processedData = results.map(({ tenant, inventoryData }) => {
// Calculate inventory metrics
const totalValue = inventoryData?.total_value || 0;
const outOfStockCount = inventoryData?.out_of_stock_count || 0;
const lowStockCount = inventoryData?.low_stock_count || 0;
const adequateStockCount = inventoryData?.adequate_stock_count || 0;
const totalIngredients = inventoryData?.total_ingredients || 0;
// Calculate coverage percentage (simplified calculation)
const coverage = totalIngredients > 0
? Math.min(100, Math.round(((adequateStockCount + lowStockCount) / totalIngredients) * 100))
: 100;
// Determine risk level based on out-of-stock and low-stock items
let riskLevel = 'low';
if (outOfStockCount > 5 || (outOfStockCount > 0 && lowStockCount > 10)) {
riskLevel = 'critical';
} else if (outOfStockCount > 0 || lowStockCount > 5) {
riskLevel = 'high';
} else if (lowStockCount > 2) {
riskLevel = 'medium';
}
// Determine status based on risk level
let status = 'normal';
if (riskLevel === 'critical') status = 'critical';
else if (riskLevel === 'high' || riskLevel === 'medium') status = 'warning';
return {
id: tenant.id,
name: tenant.name,
inventoryCoverage: coverage,
stockoutRisk: riskLevel,
criticalItems: outOfStockCount,
fulfillmentRate: 95, // Placeholder - would come from actual fulfillment data
lastUpdated: new Date().toISOString(),
status: status,
products: [] // Will be populated if detailed view is needed
};
});
setInventoryData(processedData);
} catch (error) {
console.error('Error fetching inventory data:', error);
setInventoryData([]);
} finally {
setLoading(false);
}
};
fetchAllInventoryData();
}, [childTenants]);
// Calculate network-wide fulfillment metrics // Calculate network-wide fulfillment metrics
const calculateNetworkMetrics = () => { const calculateNetworkMetrics = () => {
@@ -336,56 +376,83 @@ const OutletFulfillmentTab: React.FC<OutletFulfillmentTabProps> = ({ tenantId, o
<Warehouse className="w-5 h-5 text-[var(--color-primary)]" /> <Warehouse className="w-5 h-5 text-[var(--color-primary)]" />
{t('enterprise.outlet_status_overview')} {t('enterprise.outlet_status_overview')}
</h3> </h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> {isLoading ? (
{inventoryData.map((outlet) => { <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
const statusConfig = getOutletStatusConfig(outlet.id); {[...Array(3)].map((_, index) => (
<Card key={index} className="animate-pulse">
return ( <CardContent className="p-6">
<StatusCard <div className="flex items-center justify-between mb-4">
key={outlet.id} <div className="h-4 bg-[var(--bg-tertiary)] rounded w-3/4"></div>
id={outlet.id} <div className="w-8 h-8 rounded-full bg-[var(--bg-tertiary)]"></div>
statusIndicator={statusConfig || { </div>
color: '#6b7280', <div className="h-6 bg-[var(--bg-tertiary)] rounded w-1/2 mb-2"></div>
text: t('enterprise.no_data'), <div className="h-4 bg-[var(--bg-tertiary)] rounded w-full mb-4"></div>
icon: Clock <div className="h-2 bg-[var(--bg-tertiary)] rounded w-full mb-2"></div>
}} <div className="h-2 bg-[var(--bg-tertiary)] rounded w-3/4"></div>
title={outlet.name} </CardContent>
subtitle={`${t('enterprise.inventory_coverage')}: ${outlet.inventoryCoverage}%`} </Card>
primaryValue={`${outlet.fulfillmentRate}%`} ))}
primaryValueLabel={t('enterprise.fulfillment_rate')} </div>
secondaryInfo={{ ) : (
label: t('enterprise.critical_items'), <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
value: `${outlet.criticalItems}` {inventoryData.map((outlet) => {
}} const statusConfig = getOutletStatusConfig(outlet.id);
progress={{
label: t('enterprise.inventory_coverage'), return (
percentage: outlet.inventoryCoverage, <StatusCard
color: statusConfig?.color || '#6b7280' key={outlet.id}
}} id={outlet.id}
metadata={[ statusIndicator={statusConfig || {
`${t('enterprise.stockout_risk')}: ${t(`enterprise.risk_${outlet.stockoutRisk}`)}`, color: '#6b7280',
`${t('enterprise.last_updated')}: ${new Date(outlet.lastUpdated).toLocaleTimeString()}`, text: t('enterprise.no_data'),
sseConnected ? `🟢 ${t('enterprise.live_updates')}` : `🟡 ${t('enterprise.offline')}` icon: Clock
]} }}
actions={onOutletClick ? [{ title={outlet.name}
label: t('enterprise.view_details'), subtitle={`${t('enterprise.inventory_coverage')}: ${outlet.inventoryCoverage}%`}
icon: PackageCheck, primaryValue={`${outlet.fulfillmentRate}%`}
variant: 'outline', primaryValueLabel={t('enterprise.fulfillment_rate')}
onClick: () => { secondaryInfo={{
label: t('enterprise.critical_items'),
value: `${outlet.criticalItems}`
}}
progress={{
label: t('enterprise.inventory_coverage'),
percentage: outlet.inventoryCoverage,
color: statusConfig?.color || '#6b7280'
}}
metadata={[
`${t('enterprise.stockout_risk')}: ${t(`enterprise.risk_${outlet.stockoutRisk}`)}`,
`${t('enterprise.last_updated')}: ${new Date(outlet.lastUpdated).toLocaleTimeString()}`,
sseConnected ? `🟢 ${t('enterprise.live_updates')}` : `🟡 ${t('enterprise.offline')}`
]}
actions={onOutletClick ? [{
label: t('enterprise.view_details'),
icon: PackageCheck,
variant: 'outline',
onClick: () => {
setSelectedOutlet(outlet.id);
setViewMode('detailed');
onOutletClick(outlet.id, outlet.name);
},
priority: 'primary'
}] : []}
onClick={() => {
setSelectedOutlet(outlet.id); setSelectedOutlet(outlet.id);
setViewMode('detailed'); setViewMode('detailed');
onOutletClick(outlet.id, outlet.name); }}
}, />
priority: 'primary' );
}] : []} })}
onClick={() => { {inventoryData.length === 0 && (
setSelectedOutlet(outlet.id); <div className="col-span-full py-12">
setViewMode('detailed'); <div className="text-center text-[var(--text-secondary)]">
}} <Warehouse className="w-12 h-12 mx-auto mb-4 opacity-20" />
/> <p>{t('enterprise.no_outlets')}</p>
); </div>
})} </div>
</div> )}
</div>
)}
</div> </div>
{/* Detailed View - Product Level Inventory */} {/* Detailed View - Product Level Inventory */}

View File

@@ -12,6 +12,7 @@ import { ProductionStatusBlock } from './blocks/ProductionStatusBlock';
import StatusCard from '../ui/StatusCard/StatusCard'; import StatusCard from '../ui/StatusCard/StatusCard';
import { useControlPanelData } from '../../api/hooks/useControlPanelData'; import { useControlPanelData } from '../../api/hooks/useControlPanelData';
import { useSSEEvents } from '../../hooks/useSSE'; import { useSSEEvents } from '../../hooks/useSSE';
import { equipmentService } from '../../api/services/equipment';
interface ProductionTabProps { interface ProductionTabProps {
tenantId: string; tenantId: string;
@@ -31,56 +32,46 @@ const ProductionTab: React.FC<ProductionTabProps> = ({ tenantId }) => {
}); });
// State for equipment data with real-time updates // State for equipment data with real-time updates
const [equipmentData, setEquipmentData] = useState([ const [equipmentData, setEquipmentData] = useState<any[]>([]);
{ const [equipmentLoading, setEquipmentLoading] = useState(true);
id: 'oven-1',
name: 'Oven #1', // Fetch equipment data
status: 'normal', useEffect(() => {
temperature: '180°C', const fetchEquipmentData = async () => {
utilization: 85, setEquipmentLoading(true);
lastMaintenance: '2024-01-15', try {
nextMaintenance: '2024-02-15', const equipmentList = await equipmentService.getEquipment(tenantId);
lastEvent: null // Transform the equipment data to match the expected format
}, const transformedData = equipmentList.map(eq => ({
{ id: eq.id,
id: 'oven-2', name: eq.name,
name: 'Oven #2', status: eq.status.toLowerCase(),
status: 'warning', temperature: eq.currentTemperature ? `${eq.currentTemperature}°C` : 'N/A',
temperature: '195°C', utilization: eq.efficiency || eq.uptime || 0,
utilization: 92, lastMaintenance: eq.lastMaintenance || new Date().toISOString().split('T')[0],
lastMaintenance: '2024-01-10', nextMaintenance: eq.nextMaintenance || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], // Default to 30 days from now
nextMaintenance: '2024-02-10', lastEvent: null
lastEvent: null }));
}, setEquipmentData(transformedData);
{ } catch (error) {
id: 'mixer-1', console.error('Error fetching equipment data:', error);
name: 'Industrial Mixer', // Set empty array but still mark loading as false
status: 'normal', setEquipmentData([]);
temperature: 'N/A', } finally {
utilization: 78, setEquipmentLoading(false);
lastMaintenance: '2024-01-20', }
nextMaintenance: '2024-03-20', };
lastEvent: null
}, fetchEquipmentData();
{ }, [tenantId]);
id: 'proofer',
name: 'Proofing Chamber',
status: 'critical',
temperature: '32°C',
utilization: 65,
lastMaintenance: '2023-12-01',
nextMaintenance: '2024-01-31',
lastEvent: null
}
]);
// Process SSE events for equipment status updates // Process SSE events for equipment status updates
useEffect(() => { useEffect(() => {
if (sseEvents.length === 0) return; if (sseEvents.length === 0 || equipmentData.length === 0) return;
// Filter equipment-related events // Filter equipment-related events
const equipmentEvents = sseEvents.filter(event => const equipmentEvents = sseEvents.filter(event =>
event.event_type.includes('equipment_') || event.event_type.includes('equipment_') ||
event.event_type === 'equipment_maintenance' || event.event_type === 'equipment_maintenance' ||
event.entity_type === 'equipment' event.entity_type === 'equipment'
); );
@@ -91,8 +82,8 @@ const ProductionTab: React.FC<ProductionTabProps> = ({ tenantId }) => {
setEquipmentData(prevEquipment => { setEquipmentData(prevEquipment => {
return prevEquipment.map(equipment => { return prevEquipment.map(equipment => {
// Find the latest event for this equipment // Find the latest event for this equipment
const equipmentEvent = equipmentEvents.find(event => const equipmentEvent = equipmentEvents.find(event =>
event.entity_id === equipment.id || event.entity_id === equipment.id ||
event.event_metadata?.equipment_id === equipment.id event.event_metadata?.equipment_id === equipment.id
); );
@@ -143,7 +134,7 @@ const ProductionTab: React.FC<ProductionTabProps> = ({ tenantId }) => {
return equipment; return equipment;
}); });
}); });
}, [sseEvents]); }, [sseEvents, equipmentData]);
return ( return (
<div className="space-y-8"> <div className="space-y-8">
@@ -170,97 +161,124 @@ const ProductionTab: React.FC<ProductionTabProps> = ({ tenantId }) => {
<Cog className="w-6 h-6 text-[var(--color-secondary)]" /> <Cog className="w-6 h-6 text-[var(--color-secondary)]" />
{t('production.equipment_status')} {t('production.equipment_status')}
</h2> </h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"> {equipmentLoading ? (
{equipmentData.map((equipment) => { <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
// Determine status configuration {[...Array(4)].map((_, index) => (
const getStatusConfig = () => { <Card key={index} className="animate-pulse">
switch (equipment.status) { <CardContent className="p-6">
case 'critical': <div className="flex items-center justify-between mb-4">
return { <div className="h-4 bg-[var(--bg-tertiary)] rounded w-3/4"></div>
color: '#ef4444', // red-500 <div className="w-8 h-8 rounded-full bg-[var(--bg-tertiary)]"></div>
text: t('production.status_critical'), </div>
icon: AlertTriangle, <div className="h-6 bg-[var(--bg-tertiary)] rounded w-1/2 mb-2"></div>
isCritical: true <div className="h-4 bg-[var(--bg-tertiary)] rounded w-full mb-4"></div>
}; <div className="h-2 bg-[var(--bg-tertiary)] rounded w-full mb-2"></div>
case 'warning': <div className="h-2 bg-[var(--bg-tertiary)] rounded w-3/4"></div>
return { </CardContent>
color: '#f59e0b', // amber-500 </Card>
text: t('production.status_warning'), ))}
icon: AlertTriangle, </div>
isHighlight: true ) : (
}; <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
default: {equipmentData.map((equipment) => {
return { // Determine status configuration
color: '#10b981', // emerald-500 const getStatusConfig = () => {
text: t('production.status_normal'), switch (equipment.status) {
icon: CheckCircle2 case 'critical':
}; return {
color: '#ef4444', // red-500
text: t('production.status_critical'),
icon: AlertTriangle,
isCritical: true
};
case 'warning':
return {
color: '#f59e0b', // amber-500
text: t('production.status_warning'),
icon: AlertTriangle,
isHighlight: true
};
default:
return {
color: '#10b981', // emerald-500
text: t('production.status_normal'),
icon: CheckCircle2
};
}
};
const statusConfig = getStatusConfig();
// Add real-time event indicator if there's a recent event
const eventMetadata = [];
if (equipment.lastEvent) {
const eventTime = new Date(equipment.lastEvent.timestamp);
eventMetadata.push(`🔔 ${equipment.lastEvent.type.replace(/_/g, ' ')} - ${eventTime.toLocaleTimeString()}`);
if (equipment.lastEvent.message) {
eventMetadata.push(`${t('production.event_message')}: ${equipment.lastEvent.message}`);
}
} }
};
const statusConfig = getStatusConfig(); // Add SSE connection status to first card
const additionalMetadata = [];
// Add real-time event indicator if there's a recent event if (equipment.id === equipmentData[0]?.id) {
const eventMetadata = []; additionalMetadata.push(
if (equipment.lastEvent) { sseConnected
const eventTime = new Date(equipment.lastEvent.timestamp); ? `🟢 ${t('enterprise.live_updates')}`
eventMetadata.push(`🔔 ${equipment.lastEvent.type.replace(/_/g, ' ')} - ${eventTime.toLocaleTimeString()}`); : `🟡 ${t('enterprise.offline')}`
if (equipment.lastEvent.message) { );
eventMetadata.push(`${t('production.event_message')}: ${equipment.lastEvent.message}`);
} }
}
// Add SSE connection status to first card return (
const additionalMetadata = []; <StatusCard
if (equipment.id === 'oven-1') { key={equipment.id}
additionalMetadata.push( id={equipment.id}
sseConnected statusIndicator={statusConfig}
? `🟢 ${t('enterprise.live_updates')}` title={equipment.name}
: `🟡 ${t('enterprise.offline')}` subtitle={equipment.temperature ? `${t('production.temperature')}: ${equipment.temperature}` : undefined}
primaryValue={`${equipment.utilization}%`}
primaryValueLabel={t('production.utilization')}
secondaryInfo={{
label: t('production.next_maintenance'),
value: new Date(equipment.nextMaintenance).toLocaleDateString()
}}
progress={{
label: t('production.utilization'),
percentage: equipment.utilization,
color: statusConfig.color
}}
metadata={[...eventMetadata, ...additionalMetadata,
`${t('production.last_maintenance')}: ${new Date(equipment.lastMaintenance).toLocaleDateString()}`
]}
actions={[
{
label: t('production.view_details'),
icon: Wrench,
variant: 'outline',
onClick: () => {
// In Phase 2, this will navigate to equipment detail page
console.log(`View details for ${equipment.name}`);
},
priority: 'primary'
}
]}
onClick={() => {
// In Phase 2, this will navigate to equipment detail page
console.log(`Clicked ${equipment.name}`);
}}
/>
); );
} })}
{equipmentData.length === 0 && !equipmentLoading && (
return ( <div className="col-span-full py-12">
<StatusCard <div className="text-center text-[var(--text-secondary)]">
key={equipment.id} <Cog className="w-12 h-12 mx-auto mb-4 opacity-20" />
id={equipment.id} <p>{t('production.no_equipment')}</p>
statusIndicator={statusConfig} </div>
title={equipment.name} </div>
subtitle={equipment.temperature ? `${t('production.temperature')}: ${equipment.temperature}` : undefined} )}
primaryValue={`${equipment.utilization}%`} </div>
primaryValueLabel={t('production.utilization')} )}
secondaryInfo={{
label: t('production.next_maintenance'),
value: new Date(equipment.nextMaintenance).toLocaleDateString()
}}
progress={{
label: t('production.utilization'),
percentage: equipment.utilization,
color: statusConfig.color
}}
metadata={[...eventMetadata, ...additionalMetadata,
`${t('production.last_maintenance')}: ${new Date(equipment.lastMaintenance).toLocaleDateString()}`
]}
actions={[
{
label: t('production.view_details'),
icon: Wrench,
variant: 'outline',
onClick: () => {
// In Phase 2, this will navigate to equipment detail page
console.log(`View details for ${equipment.name}`);
},
priority: 'primary'
}
]}
onClick={() => {
// In Phase 2, this will navigate to equipment detail page
console.log(`Clicked ${equipment.name}`);
}}
/>
);
})}
</div>
</div> </div>
{/* Production Efficiency Metrics */} {/* Production Efficiency Metrics */}

View File

@@ -83,6 +83,7 @@
"status_critical": "Critical", "status_critical": "Critical",
"status_warning": "Warning", "status_warning": "Warning",
"status_normal": "Normal", "status_normal": "Normal",
"no_equipment": "No equipment available",
"efficiency_metrics": "Production Efficiency Metrics", "efficiency_metrics": "Production Efficiency Metrics",
"on_time_start_rate": "On-time Start Rate", "on_time_start_rate": "On-time Start Rate",
"batches_started_on_time": "Batches started on time", "batches_started_on_time": "Batches started on time",
@@ -562,7 +563,29 @@
"view_vehicles": "View Vehicles", "view_vehicles": "View Vehicles",
"live_tracking": "Live GPS Tracking", "live_tracking": "Live GPS Tracking",
"real_time_gps_tracking": "Real-time GPS tracking of all vehicles", "real_time_gps_tracking": "Real-time GPS tracking of all vehicles",
"open_tracking_map": "Open Tracking Map" "open_tracking_map": "Open Tracking Map",
"no_outlets": "No outlets available",
"summary_view": "Summary View",
"detailed_view": "Detailed View",
"product_level_inventory": "Product-Level Inventory",
"back_to_summary": "Back to Summary",
"current_stock": "Current Stock",
"safety_stock": "Safety Stock",
"coverage_of_safety": "Coverage of Safety",
"risk_level": "Risk Level",
"stock_above_safety": "Stock Above Safety",
"yes": "Yes",
"no": "No",
"transfer_stock": "Transfer Stock",
"critical_outlets": "Critical Outlets",
"critical_outlets_description": "There are {count} outlets with critical inventory issues",
"prioritize_transfers": "Prioritize Transfers",
"low_coverage_recommendation": "Inventory coverage is low. Consider increasing stock levels.",
"good_coverage_recommendation": "Good inventory coverage - maintain current levels",
"fulfillment_excellence": "Fulfillment Excellence",
"high_fulfillment_congrats": "Congratulations! Your network has a {rate}% fulfillment rate",
"maintain_excellence": "Maintain Excellence",
"all_outlets_healthy": "All outlets are operating normally"
}, },
"ai_insights": { "ai_insights": {
"title": "AI Insights", "title": "AI Insights",

View File

@@ -83,6 +83,7 @@
"status_critical": "Crítico", "status_critical": "Crítico",
"status_warning": "Advertencia", "status_warning": "Advertencia",
"status_normal": "Normal", "status_normal": "Normal",
"no_equipment": "No hay equipos disponibles",
"efficiency_metrics": "Métricas de Eficiencia de Producción", "efficiency_metrics": "Métricas de Eficiencia de Producción",
"on_time_start_rate": "Tasa de Inicio a Tiempo", "on_time_start_rate": "Tasa de Inicio a Tiempo",
"batches_started_on_time": "Lotes iniciados a tiempo", "batches_started_on_time": "Lotes iniciados a tiempo",
@@ -633,7 +634,29 @@
"view_vehicles": "Ver Vehículos", "view_vehicles": "Ver Vehículos",
"live_tracking": "Seguimiento GPS en Vivo", "live_tracking": "Seguimiento GPS en Vivo",
"real_time_gps_tracking": "Seguimiento GPS en tiempo real de todos los vehículos", "real_time_gps_tracking": "Seguimiento GPS en tiempo real de todos los vehículos",
"open_tracking_map": "Abrir Mapa de Seguimiento" "open_tracking_map": "Abrir Mapa de Seguimiento",
"no_outlets": "No hay tiendas disponibles",
"summary_view": "Vista de Resumen",
"detailed_view": "Vista Detallada",
"product_level_inventory": "Inventario a Nivel de Producto",
"back_to_summary": "Volver al Resumen",
"current_stock": "Stock Actual",
"safety_stock": "Stock de Seguridad",
"coverage_of_safety": "Cobertura del Stock de Seguridad",
"risk_level": "Nivel de Riesgo",
"stock_above_safety": "Stock por Encima del de Seguridad",
"yes": "Sí",
"no": "No",
"transfer_stock": "Transferir Stock",
"critical_outlets": "Tiendas Críticas",
"critical_outlets_description": "Hay {count} tiendas con problemas críticos de inventario",
"prioritize_transfers": "Priorizar Transferencias",
"low_coverage_recommendation": "La cobertura de inventario es baja. Considere aumentar los niveles de stock.",
"good_coverage_recommendation": "Buena cobertura de inventario - mantenga los niveles actuales",
"fulfillment_excellence": "Excelencia en Cumplimiento",
"high_fulfillment_congrats": "¡Felicitaciones! Su red tiene una tasa de cumplimiento del {rate}%",
"maintain_excellence": "Mantener Excelencia",
"all_outlets_healthy": "Todas las tiendas operan con normalidad"
}, },
"ai_insights": { "ai_insights": {
"title": "Insights de IA", "title": "Insights de IA",

View File

@@ -134,7 +134,30 @@
"completed": "OSATUTA", "completed": "OSATUTA",
"in_progress": "MARTXAN", "in_progress": "MARTXAN",
"pending": "ITXAROTEAN" "pending": "ITXAROTEAN"
} },
"status_critical": "Kritikoa",
"status_warning": "Abisua",
"status_normal": "Normala",
"no_equipment": "Ez dago ekipamendurik erabilgarri",
"efficiency_metrics": "Ekoizpen Eraginkortasunaren Metrikak",
"on_time_start_rate": "Denboraren Arabera Hasieraren Tasa",
"batches_started_on_time": "Denboraren arabera hasitako sortak",
"efficiency_rate": "Eraginkortasun Tasa",
"overall_efficiency": "Ekoizpen eraginkortasun orokorra",
"active_alerts": "Alerta Aktiboak",
"issues_require_attention": "Arreta behar duten arazoak",
"ai_prevented": "ADk Saihestutako Arazoak",
"problems_prevented": "ADk saihestutako arazoak",
"quick_actions": "Ekintza Azkarrak",
"create_batch": "Sortu Ekoizpen Sorta",
"create_batch_description": "Sortu ekoizpen sorta berria sarearentzat",
"maintenance": "Mantentzea",
"schedule_maintenance": "Programatu mantentzea ekoizpen ekipamendurako",
"manage_equipment": "Kudeatu Ekipamendua",
"quality_checks": "Kalitate Egiaztapenak",
"manage_quality": "Kudeatu kalitate kontrol prozesuak",
"quality_management": "Kalitate Kudeaketa",
"event_message": "Mezua"
}, },
"messages": { "messages": {
"welcome": "Ongi etorri berriro", "welcome": "Ongi etorri berriro",
@@ -390,7 +413,29 @@
"in_transit": "Bidaiatzen", "in_transit": "Bidaiatzen",
"delivered": "Entregatua", "delivered": "Entregatua",
"failed": "Huts egin du", "failed": "Huts egin du",
"distribution_routes": "Banaketa Ibilbideak" "distribution_routes": "Banaketa Ibilbideak",
"no_outlets": "Ez dago dendarik erabilgarri",
"summary_view": "Laburpen Ikuspegia",
"detailed_view": "Ikuspegi Xehatua",
"product_level_inventory": "Produktu Mailako Inbentarioa",
"back_to_summary": "Bueltatu Laburpena-ra",
"current_stock": "Uneko Stock-a",
"safety_stock": "Segurtasun Stock-a",
"coverage_of_safety": "Segurtasun Stockaren Estaldura",
"risk_level": "Arrisku Maila",
"stock_above_safety": "Segurtasunaren Gainetik Stock-a",
"yes": "Bai",
"no": "Ez",
"transfer_stock": "Stock-a Transferitu",
"critical_outlets": "Denda Kritikoak",
"critical_outlets_description": "{count} denda daude inbentario arazo kritikoekin",
"prioritize_transfers": "Transferentziak Lehentasun",
"low_coverage_recommendation": "Inbentario estaldura baxua da. Kontutan izan stock mailak handitzea.",
"good_coverage_recommendation": "Inbentario estaldura ona - mantendu uneko mailak",
"fulfillment_excellence": "Betetze Bikaintasuna",
"high_fulfillment_congrats": "Zorionak! Zure sareak %{rate}ko betetze tasa du",
"maintain_excellence": "Mantendu Bikaintasuna",
"all_outlets_healthy": "Denda guztiak arrunta lanean"
}, },
"new_dashboard": { "new_dashboard": {
"system_status": { "system_status": {