-
-
-
-
-
-
- {tenant.name}
-
-
- {tenant.city}
-
+ {/* Dropdown Content */}
+
+ {/* Mobile Handle */}
+ {dropdownPosition.isMobile && (
+
+ )}
+
+ {/* Header */}
+
+
+ Organizations
+
+ {dropdownPosition.isMobile && (
+ setIsOpen(false)}
+ className="p-2 hover:bg-[var(--bg-secondary)] rounded-full transition-colors"
+ >
+
+
+ )}
+
+
+ {/* Error State */}
+ {error && (
+
+
+
+
{error}
+
+ Retry
+
+
+
+ )}
+
+ {/* Tenant List */}
+
+ {availableTenants.map((tenant) => (
+
handleTenantSwitch(tenant.id)}
+ disabled={isLoading}
+ className={`
+ w-full text-left rounded-lg transition-all duration-200
+ hover:bg-[var(--bg-secondary)] focus:bg-[var(--bg-secondary)]
+ active:scale-[0.98] active:bg-[var(--bg-tertiary)]
+ focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20
+ disabled:opacity-50 disabled:cursor-not-allowed
+ ${dropdownPosition.isMobile ? 'px-4 py-4 mx-2 my-1 min-h-[60px]' : 'px-3 py-3 mx-1 my-0.5'}
+ ${tenant.id === currentTenant?.id ? 'bg-[var(--color-primary)]/5 ring-1 ring-[var(--color-primary)]/20' : ''}
+ `}
+ role="option"
+ aria-selected={tenant.id === currentTenant?.id}
+ >
+
+
+
+
+
+
+
+
+ {tenant.name}
+
+ {tenant.city && (
+
+ {tenant.city}
+
+ )}
+
+
+ {tenant.id === currentTenant?.id && (
+
+ )}
+
+ ))}
+
- {tenant.id === currentTenant?.id && (
-
- )}
-
+ {/* Footer */}
+
+
+
+ Agregar Nueva Organización
- ))}
+
-
- {/* Footer */}
-
-
-
-
- Add New Organization
-
-
-
-
,
+ >,
document.body
)}
diff --git a/frontend/src/hooks/useLanguageSwitcher.ts b/frontend/src/hooks/useLanguageSwitcher.ts
new file mode 100644
index 00000000..f8f441f2
--- /dev/null
+++ b/frontend/src/hooks/useLanguageSwitcher.ts
@@ -0,0 +1,30 @@
+import { useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useUIStore } from '../stores/ui.store';
+import { type SupportedLanguage } from '../locales';
+
+/**
+ * Hook for managing language switching with proper synchronization
+ * between i18n and UI store
+ */
+export function useLanguageSwitcher() {
+ const { i18n } = useTranslation();
+ const { language: uiLanguage, setLanguage: setUILanguage } = useUIStore();
+
+ const changeLanguage = useCallback(async (newLanguage: SupportedLanguage) => {
+ try {
+ // Only change i18n language - let the i18n event handler update UI store
+ await i18n.changeLanguage(newLanguage);
+ } catch (error) {
+ console.error('Failed to change language:', error);
+ }
+ }, [i18n]);
+
+ const isChanging = i18n.isLanguageChangingTo !== false;
+
+ return {
+ currentLanguage: i18n.language as SupportedLanguage,
+ changeLanguage,
+ isChanging,
+ };
+}
\ No newline at end of file
diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts
index e19b6306..c9875848 100644
--- a/frontend/src/i18n/index.ts
+++ b/frontend/src/i18n/index.ts
@@ -1,13 +1,33 @@
// frontend/src/i18n/index.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
-import { resources, defaultLanguage } from '../locales';
+import { resources, defaultLanguage, supportedLanguages } from '../locales';
+
+// Get saved language from localStorage or default
+const getSavedLanguage = () => {
+ try {
+ const stored = localStorage.getItem('ui-storage');
+ if (stored) {
+ const { state } = JSON.parse(stored);
+ if (state?.language && supportedLanguages.includes(state.language)) {
+ console.log(`🔍 Found stored language: ${state.language}`);
+ return state.language;
+ }
+ }
+ } catch (error) {
+ console.warn('Failed to parse stored language:', error);
+ }
+ console.log(`📌 Using default language: ${defaultLanguage}`);
+ return defaultLanguage;
+};
+
+const initialLanguage = getSavedLanguage();
i18n
.use(initReactI18next)
.init({
resources,
- lng: defaultLanguage,
+ lng: initialLanguage,
fallbackLng: defaultLanguage,
interpolation: {
@@ -33,6 +53,29 @@ i18n
react: {
useSuspense: false,
},
+
+ // Return key with namespace if translation is missing
+ saveMissing: process.env.NODE_ENV === 'development',
+ })
+ .then(() => {
+ console.log(`🚀 i18n initialized with language: ${initialLanguage}`);
});
+// Listen for language changes and update UI store
+i18n.on('languageChanged', (lng) => {
+ if (typeof window !== 'undefined') {
+ // Update document direction for RTL languages
+ document.dir = i18n.dir(lng);
+
+ // Update UI store to keep it in sync (without triggering i18n again)
+ import('../stores/ui.store').then(({ useUIStore }) => {
+ const currentLanguage = useUIStore.getState().language;
+ if (currentLanguage !== lng) {
+ // Set directly to avoid recursive calls
+ useUIStore.setState({ language: lng as any });
+ }
+ });
+ }
+});
+
export default i18n;
\ No newline at end of file
diff --git a/frontend/src/locales/en/common.json b/frontend/src/locales/en/common.json
new file mode 100644
index 00000000..2c0fefe1
--- /dev/null
+++ b/frontend/src/locales/en/common.json
@@ -0,0 +1,240 @@
+{
+ "navigation": {
+ "dashboard": "Dashboard",
+ "operations": "Operations",
+ "inventory": "Inventory",
+ "production": "Production",
+ "recipes": "Recipes",
+ "orders": "Orders",
+ "procurement": "Procurement",
+ "pos": "Point of Sale",
+ "analytics": "Analytics",
+ "forecasting": "Forecasting",
+ "sales": "Sales",
+ "performance": "Performance",
+ "insights": "AI Insights",
+ "data": "Data",
+ "weather": "Weather",
+ "traffic": "Traffic",
+ "events": "Events",
+ "communications": "Communications",
+ "notifications": "Notifications",
+ "alerts": "Alerts",
+ "preferences": "Preferences",
+ "settings": "Settings",
+ "team": "Team",
+ "bakery": "Bakery",
+ "training": "Training",
+ "system": "System",
+ "onboarding": "Initial Setup"
+ },
+ "actions": {
+ "save": "Save",
+ "cancel": "Cancel",
+ "edit": "Edit",
+ "delete": "Delete",
+ "add": "Add",
+ "create": "Create",
+ "update": "Update",
+ "view": "View",
+ "search": "Search",
+ "filter": "Filter",
+ "export": "Export",
+ "import": "Import",
+ "download": "Download",
+ "upload": "Upload",
+ "print": "Print",
+ "refresh": "Refresh",
+ "reset": "Reset",
+ "clear": "Clear",
+ "submit": "Submit",
+ "close": "Close",
+ "open": "Open",
+ "back": "Back",
+ "next": "Next",
+ "previous": "Previous",
+ "finish": "Finish",
+ "continue": "Continue",
+ "confirm": "Confirm",
+ "expand": "Expand",
+ "collapse": "Collapse"
+ },
+ "status": {
+ "active": "Active",
+ "inactive": "Inactive",
+ "pending": "Pending",
+ "completed": "Completed",
+ "cancelled": "Cancelled",
+ "draft": "Draft",
+ "published": "Published",
+ "archived": "Archived",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "available": "Available",
+ "unavailable": "Unavailable",
+ "in_progress": "In Progress",
+ "failed": "Failed",
+ "success": "Success",
+ "warning": "Warning",
+ "error": "Error",
+ "info": "Information",
+ "undefined": "Not defined",
+ "no_rating": "No rating",
+ "disconnected": "Disconnected",
+ "no_realtime_connection": "No real-time connection"
+ },
+ "time": {
+ "today": "Today",
+ "yesterday": "Yesterday",
+ "tomorrow": "Tomorrow",
+ "this_week": "This Week",
+ "last_week": "Last Week",
+ "next_week": "Next Week",
+ "this_month": "This Month",
+ "last_month": "Last Month",
+ "next_month": "Next Month",
+ "this_year": "This Year",
+ "last_year": "Last Year",
+ "next_year": "Next Year",
+ "morning": "Morning",
+ "afternoon": "Afternoon",
+ "evening": "Evening",
+ "night": "Night",
+ "now": "Now",
+ "recently": "Recently",
+ "soon": "Soon",
+ "later": "Later"
+ },
+ "units": {
+ "kg": "kg",
+ "g": "g",
+ "l": "l",
+ "ml": "ml",
+ "pieces": "pieces",
+ "units": "units",
+ "portions": "portions",
+ "minutes": "minutes",
+ "hours": "hours",
+ "days": "days",
+ "weeks": "weeks",
+ "months": "months",
+ "years": "years"
+ },
+ "categories": {
+ "bread": "Breads",
+ "pastry": "Pastries",
+ "cake": "Cakes",
+ "cookie": "Cookies",
+ "other": "Others",
+ "flour": "Flours",
+ "dairy": "Dairy",
+ "eggs": "Eggs",
+ "fats": "Fats",
+ "sugar": "Sugars",
+ "yeast": "Yeasts",
+ "spices": "Spices",
+ "salted": "Savory"
+ },
+ "priority": {
+ "low": "Low",
+ "normal": "Normal",
+ "medium": "Medium",
+ "high": "High",
+ "urgent": "Urgent",
+ "critical": "Critical",
+ "undefined": "Priority not defined"
+ },
+ "difficulty": {
+ "easy": "Easy",
+ "medium": "Medium",
+ "hard": "Hard",
+ "expert": "Expert"
+ },
+ "payment_methods": {
+ "cash": "Cash",
+ "card": "Card",
+ "transfer": "Transfer",
+ "other": "Other"
+ },
+ "delivery_methods": {
+ "pickup": "Pickup",
+ "delivery": "Home Delivery"
+ },
+ "weekdays": {
+ "monday": "Monday",
+ "tuesday": "Tuesday",
+ "wednesday": "Wednesday",
+ "thursday": "Thursday",
+ "friday": "Friday",
+ "saturday": "Saturday",
+ "sunday": "Sunday"
+ },
+ "months": {
+ "january": "January",
+ "february": "February",
+ "march": "March",
+ "april": "April",
+ "may": "May",
+ "june": "June",
+ "july": "July",
+ "august": "August",
+ "september": "September",
+ "october": "October",
+ "november": "November",
+ "december": "December"
+ },
+ "forms": {
+ "required": "Required",
+ "optional": "Optional",
+ "loading": "Loading...",
+ "no_data": "No data available",
+ "no_results": "No results found",
+ "empty_state": "No items to display",
+ "select_option": "Select option",
+ "enter_text": "Enter text",
+ "choose_file": "Choose file",
+ "drag_drop": "Drag and drop here",
+ "or": "or",
+ "no_terms": "No terms defined",
+ "search_placeholder": "Search..."
+ },
+ "table": {
+ "no_data": "No data to display",
+ "loading": "Loading data...",
+ "error": "Error loading data",
+ "rows_per_page": "Rows per page",
+ "showing": "Showing",
+ "of": "of",
+ "entries": "entries",
+ "page": "Page",
+ "first": "First",
+ "last": "Last",
+ "sort_asc": "Sort ascending",
+ "sort_desc": "Sort descending"
+ },
+ "alerts": {
+ "confirm_delete": "Are you sure you want to delete this item?",
+ "confirm_action": "Are you sure you want to perform this action?",
+ "unsaved_changes": "You have unsaved changes. Are you sure you want to leave?",
+ "success_save": "Saved successfully",
+ "success_delete": "Deleted successfully",
+ "success_update": "Updated successfully",
+ "success_create": "Created successfully",
+ "operation_completed": "Operation completed successfully"
+ },
+ "accessibility": {
+ "close": "Close",
+ "menu": "Main navigation",
+ "open_menu": "Open menu",
+ "close_menu": "Close menu",
+ "toggle": "Toggle",
+ "expand": "Expand",
+ "collapse": "Collapse",
+ "loading": "Loading",
+ "image": "Image",
+ "button": "Button",
+ "link": "Link",
+ "tooltip": "Additional information",
+ "search": "Search in the application"
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/locales/en/dashboard.json b/frontend/src/locales/en/dashboard.json
new file mode 100644
index 00000000..0504d928
--- /dev/null
+++ b/frontend/src/locales/en/dashboard.json
@@ -0,0 +1,74 @@
+{
+ "title": "Dashboard",
+ "subtitle": "Overview of your bakery operations",
+ "stats": {
+ "sales_today": "Sales Today",
+ "pending_orders": "Pending Orders",
+ "stock_alerts": "Stock Alerts",
+ "production_efficiency": "Production Efficiency",
+ "revenue_growth": "Revenue Growth",
+ "customer_satisfaction": "Customer Satisfaction",
+ "inventory_turnover": "Inventory Turnover",
+ "daily_profit": "Daily Profit",
+ "products_sold": "Products Sold"
+ },
+ "trends": {
+ "vs_yesterday": "% vs yesterday",
+ "vs_last_week": "% vs last week",
+ "vs_last_month": "% vs last month",
+ "growth": "growth",
+ "decrease": "decrease",
+ "stable": "stable"
+ },
+ "sections": {
+ "critical_stats": "Critical Statistics",
+ "real_time_alerts": "Real-time Alerts",
+ "procurement_today": "Procurement Today",
+ "production_today": "Production Today",
+ "recent_activity": "Recent Activity",
+ "quick_actions": "Quick Actions"
+ },
+ "quick_actions": {
+ "add_new_bakery": "Add New Bakery",
+ "create_order": "Create Order",
+ "start_production": "Start Production",
+ "check_inventory": "Check Inventory",
+ "view_reports": "View Reports",
+ "manage_staff": "Manage Staff"
+ },
+ "alerts": {
+ "low_stock": "Low Stock",
+ "production_delay": "Production Delay",
+ "quality_issue": "Quality Issue",
+ "equipment_maintenance": "Equipment Maintenance",
+ "order_pending": "Order Pending",
+ "delivery_due": "Delivery Due"
+ },
+ "messages": {
+ "welcome": "Welcome back",
+ "good_morning": "Good morning",
+ "good_afternoon": "Good afternoon",
+ "good_evening": "Good evening",
+ "no_data": "No data available",
+ "loading": "Loading dashboard data...",
+ "error_loading": "Error loading data",
+ "last_updated": "Last updated",
+ "auto_refresh": "Auto refresh in",
+ "more_than_yesterday": "more than yesterday",
+ "require_attention": "Require attention",
+ "more_units": "more units",
+ "action_required": "Action required",
+ "manage_organizations": "Manage your organizations",
+ "setup_new_business": "Set up a new business from scratch",
+ "active_organizations": "Active Organizations"
+ },
+ "time_periods": {
+ "today": "Today",
+ "this_week": "This Week",
+ "this_month": "This Month",
+ "this_year": "This Year",
+ "last_7_days": "Last 7 days",
+ "last_30_days": "Last 30 days",
+ "last_90_days": "Last 90 days"
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/locales/en/production.json b/frontend/src/locales/en/production.json
new file mode 100644
index 00000000..f3902d63
--- /dev/null
+++ b/frontend/src/locales/en/production.json
@@ -0,0 +1,80 @@
+{
+ "title": "Production",
+ "subtitle": "Manage your bakery production",
+ "production_status": {
+ "PENDING": "Pending",
+ "IN_PROGRESS": "In Progress",
+ "COMPLETED": "Completed",
+ "CANCELLED": "Cancelled",
+ "ON_HOLD": "On Hold",
+ "QUALITY_CHECK": "Quality Check",
+ "FAILED": "Failed"
+ },
+ "production_priority": {
+ "LOW": "Low",
+ "MEDIUM": "Medium",
+ "HIGH": "High",
+ "URGENT": "Urgent"
+ },
+ "batch_status": {
+ "PLANNED": "Planned",
+ "IN_PROGRESS": "In Progress",
+ "COMPLETED": "Completed",
+ "CANCELLED": "Cancelled",
+ "ON_HOLD": "On Hold"
+ },
+ "quality_check_status": {
+ "PENDING": "Pending",
+ "IN_PROGRESS": "In Progress",
+ "PASSED": "Passed",
+ "FAILED": "Failed",
+ "REQUIRES_ATTENTION": "Requires Attention"
+ },
+ "fields": {
+ "batch_number": "Batch Number",
+ "production_date": "Production Date",
+ "planned_quantity": "Planned Quantity",
+ "actual_quantity": "Actual Quantity",
+ "yield_percentage": "Yield Percentage",
+ "priority": "Priority",
+ "assigned_staff": "Assigned Staff",
+ "production_notes": "Production Notes",
+ "quality_score": "Quality Score",
+ "quality_notes": "Quality Notes",
+ "defect_rate": "Defect Rate",
+ "rework_required": "Rework Required",
+ "waste_quantity": "Waste Quantity",
+ "waste_reason": "Waste Reason",
+ "efficiency": "Efficiency",
+ "material_cost": "Material Cost",
+ "labor_cost": "Labor Cost",
+ "overhead_cost": "Overhead Cost",
+ "total_cost": "Total Cost",
+ "cost_per_unit": "Cost per Unit"
+ },
+ "actions": {
+ "start_production": "Start Production",
+ "complete_batch": "Complete Batch",
+ "pause_production": "Pause Production",
+ "cancel_batch": "Cancel Batch",
+ "quality_check": "Quality Check",
+ "create_batch": "Create Batch",
+ "view_details": "View Details",
+ "edit_batch": "Edit Batch",
+ "duplicate_batch": "Duplicate Batch"
+ },
+ "labels": {
+ "current_production": "Current Production",
+ "production_queue": "Production Queue",
+ "completed_today": "Completed Today",
+ "efficiency_rate": "Efficiency Rate",
+ "quality_score": "Quality Score",
+ "active_batches": "Active Batches",
+ "pending_quality_checks": "Pending Quality Checks"
+ },
+ "descriptions": {
+ "production_efficiency": "Percentage of efficiency in current production",
+ "quality_average": "Average quality score in recent batches",
+ "waste_reduction": "Waste reduction compared to previous month"
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/locales/es/common.json b/frontend/src/locales/es/common.json
index 7a03717b..201094bc 100644
--- a/frontend/src/locales/es/common.json
+++ b/frontend/src/locales/es/common.json
@@ -55,7 +55,9 @@
"previous": "Anterior",
"finish": "Finalizar",
"continue": "Continuar",
- "confirm": "Confirmar"
+ "confirm": "Confirmar",
+ "expand": "Expandir",
+ "collapse": "Contraer"
},
"status": {
"active": "Activo",
@@ -75,7 +77,11 @@
"success": "Éxito",
"warning": "Advertencia",
"error": "Error",
- "info": "Información"
+ "info": "Información",
+ "undefined": "No definido",
+ "no_rating": "Sin calificación",
+ "disconnected": "Desconectado",
+ "no_realtime_connection": "Sin conexión en tiempo real"
},
"time": {
"today": "Hoy",
@@ -135,7 +141,8 @@
"medium": "Media",
"high": "Alta",
"urgent": "Urgente",
- "critical": "Crítica"
+ "critical": "Crítica",
+ "undefined": "Prioridad no definida"
},
"difficulty": {
"easy": "Fácil",
@@ -187,7 +194,9 @@
"enter_text": "Ingresa texto",
"choose_file": "Elegir archivo",
"drag_drop": "Arrastra y suelta aquí",
- "or": "o"
+ "or": "o",
+ "no_terms": "Sin términos definidos",
+ "search_placeholder": "Buscar..."
},
"table": {
"no_data": "No hay datos para mostrar",
@@ -215,7 +224,7 @@
},
"accessibility": {
"close": "Cerrar",
- "menu": "Menú",
+ "menu": "Navegación principal",
"open_menu": "Abrir menú",
"close_menu": "Cerrar menú",
"toggle": "Alternar",
@@ -225,6 +234,7 @@
"image": "Imagen",
"button": "Botón",
"link": "Enlace",
- "tooltip": "Información adicional"
+ "tooltip": "Información adicional",
+ "search": "Buscar en la aplicación"
}
}
\ No newline at end of file
diff --git a/frontend/src/locales/es/dashboard.json b/frontend/src/locales/es/dashboard.json
new file mode 100644
index 00000000..bf726423
--- /dev/null
+++ b/frontend/src/locales/es/dashboard.json
@@ -0,0 +1,74 @@
+{
+ "title": "Panel de Control",
+ "subtitle": "Resumen general de tu panadería",
+ "stats": {
+ "sales_today": "Ventas Hoy",
+ "pending_orders": "Órdenes Pendientes",
+ "stock_alerts": "Alertas de Stock",
+ "production_efficiency": "Eficiencia de Producción",
+ "revenue_growth": "Crecimiento de Ingresos",
+ "customer_satisfaction": "Satisfacción del Cliente",
+ "inventory_turnover": "Rotación de Inventario",
+ "daily_profit": "Ganancia Diaria",
+ "products_sold": "Productos Vendidos"
+ },
+ "trends": {
+ "vs_yesterday": "% vs ayer",
+ "vs_last_week": "% vs semana pasada",
+ "vs_last_month": "% vs mes pasado",
+ "growth": "crecimiento",
+ "decrease": "disminución",
+ "stable": "estable"
+ },
+ "sections": {
+ "critical_stats": "Estadísticas Críticas",
+ "real_time_alerts": "Alertas en Tiempo Real",
+ "procurement_today": "Compras Hoy",
+ "production_today": "Producción Hoy",
+ "recent_activity": "Actividad Reciente",
+ "quick_actions": "Acciones Rápidas"
+ },
+ "quick_actions": {
+ "add_new_bakery": "Agregar Nueva Panadería",
+ "create_order": "Crear Pedido",
+ "start_production": "Iniciar Producción",
+ "check_inventory": "Revisar Inventario",
+ "view_reports": "Ver Reportes",
+ "manage_staff": "Gestionar Personal"
+ },
+ "alerts": {
+ "low_stock": "Stock Bajo",
+ "production_delay": "Retraso en Producción",
+ "quality_issue": "Problema de Calidad",
+ "equipment_maintenance": "Mantenimiento de Equipo",
+ "order_pending": "Pedido Pendiente",
+ "delivery_due": "Entrega Vencida"
+ },
+ "messages": {
+ "welcome": "Bienvenido de vuelta",
+ "good_morning": "Buenos días",
+ "good_afternoon": "Buenas tardes",
+ "good_evening": "Buenas noches",
+ "no_data": "No hay datos disponibles",
+ "loading": "Cargando datos del panel...",
+ "error_loading": "Error al cargar los datos",
+ "last_updated": "Última actualización",
+ "auto_refresh": "Actualización automática en",
+ "more_than_yesterday": "más que ayer",
+ "require_attention": "Requieren atención",
+ "more_units": "unidades más",
+ "action_required": "Acción requerida",
+ "manage_organizations": "Gestiona tus organizaciones",
+ "setup_new_business": "Configurar un nuevo negocio desde cero",
+ "active_organizations": "Organizaciones Activas"
+ },
+ "time_periods": {
+ "today": "Hoy",
+ "this_week": "Esta Semana",
+ "this_month": "Este Mes",
+ "this_year": "Este Año",
+ "last_7_days": "Últimos 7 días",
+ "last_30_days": "Últimos 30 días",
+ "last_90_days": "Últimos 90 días"
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/locales/es/production.json b/frontend/src/locales/es/production.json
new file mode 100644
index 00000000..33c06af7
--- /dev/null
+++ b/frontend/src/locales/es/production.json
@@ -0,0 +1,80 @@
+{
+ "title": "Producción",
+ "subtitle": "Gestiona la producción de tu panadería",
+ "production_status": {
+ "PENDING": "Pendiente",
+ "IN_PROGRESS": "En Proceso",
+ "COMPLETED": "Completado",
+ "CANCELLED": "Cancelado",
+ "ON_HOLD": "En Pausa",
+ "QUALITY_CHECK": "Control Calidad",
+ "FAILED": "Fallido"
+ },
+ "production_priority": {
+ "LOW": "Baja",
+ "MEDIUM": "Media",
+ "HIGH": "Alta",
+ "URGENT": "Urgente"
+ },
+ "batch_status": {
+ "PLANNED": "Planificado",
+ "IN_PROGRESS": "En Proceso",
+ "COMPLETED": "Completado",
+ "CANCELLED": "Cancelado",
+ "ON_HOLD": "En Pausa"
+ },
+ "quality_check_status": {
+ "PENDING": "Pendiente",
+ "IN_PROGRESS": "En Proceso",
+ "PASSED": "Aprobado",
+ "FAILED": "Reprobado",
+ "REQUIRES_ATTENTION": "Requiere Atención"
+ },
+ "fields": {
+ "batch_number": "Número de Lote",
+ "production_date": "Fecha de Producción",
+ "planned_quantity": "Cantidad Planificada",
+ "actual_quantity": "Cantidad Real",
+ "yield_percentage": "Porcentaje de Rendimiento",
+ "priority": "Prioridad",
+ "assigned_staff": "Personal Asignado",
+ "production_notes": "Notas de Producción",
+ "quality_score": "Puntuación de Calidad",
+ "quality_notes": "Notas de Calidad",
+ "defect_rate": "Tasa de Defectos",
+ "rework_required": "Requiere Retrabajo",
+ "waste_quantity": "Cantidad de Desperdicio",
+ "waste_reason": "Razón del Desperdicio",
+ "efficiency": "Eficiencia",
+ "material_cost": "Costo de Materiales",
+ "labor_cost": "Costo de Mano de Obra",
+ "overhead_cost": "Costo Indirecto",
+ "total_cost": "Costo Total",
+ "cost_per_unit": "Costo por Unidad"
+ },
+ "actions": {
+ "start_production": "Iniciar Producción",
+ "complete_batch": "Completar Lote",
+ "pause_production": "Pausar Producción",
+ "cancel_batch": "Cancelar Lote",
+ "quality_check": "Control de Calidad",
+ "create_batch": "Crear Lote",
+ "view_details": "Ver Detalles",
+ "edit_batch": "Editar Lote",
+ "duplicate_batch": "Duplicar Lote"
+ },
+ "labels": {
+ "current_production": "Producción Actual",
+ "production_queue": "Cola de Producción",
+ "completed_today": "Completado Hoy",
+ "efficiency_rate": "Tasa de Eficiencia",
+ "quality_score": "Puntuación de Calidad",
+ "active_batches": "Lotes Activos",
+ "pending_quality_checks": "Controles de Calidad Pendientes"
+ },
+ "descriptions": {
+ "production_efficiency": "Porcentaje de eficiencia en la producción actual",
+ "quality_average": "Puntuación promedio de calidad en los últimos lotes",
+ "waste_reduction": "Reducción de desperdicio comparado con el mes anterior"
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/locales/eu/common.json b/frontend/src/locales/eu/common.json
new file mode 100644
index 00000000..3c796803
--- /dev/null
+++ b/frontend/src/locales/eu/common.json
@@ -0,0 +1,240 @@
+{
+ "navigation": {
+ "dashboard": "Aginte Panela",
+ "operations": "Eragiketak",
+ "inventory": "Inbentarioa",
+ "production": "Ekoizpena",
+ "recipes": "Errezetak",
+ "orders": "Eskaerak",
+ "procurement": "Erosketak",
+ "pos": "Salmenta Puntua",
+ "analytics": "Analisiak",
+ "forecasting": "Aurreikuspena",
+ "sales": "Salmentak",
+ "performance": "Errendimendua",
+ "insights": "AI Jakintza",
+ "data": "Datuak",
+ "weather": "Eguraldia",
+ "traffic": "Trafikoa",
+ "events": "Gertaerak",
+ "communications": "Komunikazioak",
+ "notifications": "Jakinarazpenak",
+ "alerts": "Alertak",
+ "preferences": "Lehentasunak",
+ "settings": "Ezarpenak",
+ "team": "Taldea",
+ "bakery": "Okindegi",
+ "training": "Trebakuntza",
+ "system": "Sistema",
+ "onboarding": "Hasierako Konfigurazioa"
+ },
+ "actions": {
+ "save": "Gorde",
+ "cancel": "Ezeztatu",
+ "edit": "Editatu",
+ "delete": "Ezabatu",
+ "add": "Gehitu",
+ "create": "Sortu",
+ "update": "Eguneratu",
+ "view": "Ikusi",
+ "search": "Bilatu",
+ "filter": "Iragazi",
+ "export": "Esportatu",
+ "import": "Inportatu",
+ "download": "Deskargatu",
+ "upload": "Kargatu",
+ "print": "Inprimatu",
+ "refresh": "Eguneratu",
+ "reset": "Berrezarri",
+ "clear": "Garbitu",
+ "submit": "Bidali",
+ "close": "Itxi",
+ "open": "Ireki",
+ "back": "Atzera",
+ "next": "Hurrengoa",
+ "previous": "Aurrekoa",
+ "finish": "Amaitu",
+ "continue": "Jarraitu",
+ "confirm": "Berretsi",
+ "expand": "Zabaldu",
+ "collapse": "Tolestu"
+ },
+ "status": {
+ "active": "Aktibo",
+ "inactive": "Ez aktibo",
+ "pending": "Zain",
+ "completed": "Amaituta",
+ "cancelled": "Bertan behera utzi",
+ "draft": "Zirriborroa",
+ "published": "Argitaratua",
+ "archived": "Artxibatua",
+ "enabled": "Gaituta",
+ "disabled": "Desgaituta",
+ "available": "Erabilgarri",
+ "unavailable": "Ez erabilgarri",
+ "in_progress": "Abian",
+ "failed": "Huts egin du",
+ "success": "Arrakasta",
+ "warning": "Abisua",
+ "error": "Errorea",
+ "info": "Informazioa",
+ "undefined": "Zehaztu gabe",
+ "no_rating": "Baloraziorik ez",
+ "disconnected": "Deskonektatuta",
+ "no_realtime_connection": "Denbora errealeko konexiorik ez"
+ },
+ "time": {
+ "today": "Gaur",
+ "yesterday": "Atzo",
+ "tomorrow": "Bihar",
+ "this_week": "Aste honetan",
+ "last_week": "Azken astean",
+ "next_week": "Hurrengo astean",
+ "this_month": "Hilabete honetan",
+ "last_month": "Azken hilabetean",
+ "next_month": "Hurrengo hilabetean",
+ "this_year": "Urte honetan",
+ "last_year": "Azken urtean",
+ "next_year": "Hurrengo urtean",
+ "morning": "Goiza",
+ "afternoon": "Arratsaldea",
+ "evening": "Iluntzea",
+ "night": "Gaua",
+ "now": "Orain",
+ "recently": "Duela gutxi",
+ "soon": "Laster",
+ "later": "Geroago"
+ },
+ "units": {
+ "kg": "kg",
+ "g": "g",
+ "l": "l",
+ "ml": "ml",
+ "pieces": "zatiak",
+ "units": "unitateak",
+ "portions": "zatiak",
+ "minutes": "minutuak",
+ "hours": "orduak",
+ "days": "egunak",
+ "weeks": "asteak",
+ "months": "hilabeteak",
+ "years": "urteak"
+ },
+ "categories": {
+ "bread": "Ogiak",
+ "pastry": "Gozogintza",
+ "cake": "Tartoak",
+ "cookie": "Galletak",
+ "other": "Besteak",
+ "flour": "Irinak",
+ "dairy": "Esnekiak",
+ "eggs": "Arrautzak",
+ "fats": "Gantzak",
+ "sugar": "Azukreak",
+ "yeast": "Legamiak",
+ "spices": "Espezieak",
+ "salted": "Gazidunak"
+ },
+ "priority": {
+ "low": "Baxua",
+ "normal": "Normala",
+ "medium": "Ertaina",
+ "high": "Altua",
+ "urgent": "Larria",
+ "critical": "Kritikoa",
+ "undefined": "Lehentasuna zehaztu gabe"
+ },
+ "difficulty": {
+ "easy": "Erraza",
+ "medium": "Ertaina",
+ "hard": "Zaila",
+ "expert": "Adituarena"
+ },
+ "payment_methods": {
+ "cash": "Dirua",
+ "card": "Txartela",
+ "transfer": "Transferentzia",
+ "other": "Besteak"
+ },
+ "delivery_methods": {
+ "pickup": "Hartzera",
+ "delivery": "Etxera banatzea"
+ },
+ "weekdays": {
+ "monday": "Astelehena",
+ "tuesday": "Asteartea",
+ "wednesday": "Asteazkena",
+ "thursday": "Osteguna",
+ "friday": "Ostirala",
+ "saturday": "Larunbata",
+ "sunday": "Igandea"
+ },
+ "months": {
+ "january": "Urtarrila",
+ "february": "Otsaila",
+ "march": "Martxoa",
+ "april": "Apirila",
+ "may": "Maiatza",
+ "june": "Ekaina",
+ "july": "Uztaila",
+ "august": "Abuztua",
+ "september": "Iraila",
+ "october": "Urria",
+ "november": "Azaroa",
+ "december": "Abendua"
+ },
+ "forms": {
+ "required": "Beharrezkoa",
+ "optional": "Aukerakoa",
+ "loading": "Kargatzen...",
+ "no_data": "Ez dago daturik",
+ "no_results": "Ez da emaitzarik aurkitu",
+ "empty_state": "Ez dago elementurik erakusteko",
+ "select_option": "Aukera hautatu",
+ "enter_text": "Testua sartu",
+ "choose_file": "Fitxategia aukeratu",
+ "drag_drop": "Arrastatu eta jaregin hemen",
+ "or": "edo",
+ "no_terms": "Baldintzarik zehaztu gabe",
+ "search_placeholder": "Bilatu..."
+ },
+ "table": {
+ "no_data": "Ez dago daturik erakusteko",
+ "loading": "Datuak kargatzen...",
+ "error": "Errorea datuak kargatzean",
+ "rows_per_page": "Errenkadak orrialdepo",
+ "showing": "Erakusten",
+ "of": "-tik",
+ "entries": "sarrerak",
+ "page": "Orrialdea",
+ "first": "Lehena",
+ "last": "Azkena",
+ "sort_asc": "Ordenatu gorantz",
+ "sort_desc": "Ordenatu beherantz"
+ },
+ "alerts": {
+ "confirm_delete": "Ziur zaude elementu hau ezabatu nahi duzula?",
+ "confirm_action": "Ziur zaude ekintza hau egin nahi duzula?",
+ "unsaved_changes": "Gorde gabeko aldaketak dituzu. Ziur zaude irten nahi duzula?",
+ "success_save": "Ongi gorde da",
+ "success_delete": "Ongi ezabatu da",
+ "success_update": "Ongi eguneratu da",
+ "success_create": "Ongi sortu da",
+ "operation_completed": "Eragiketa ongi burutu da"
+ },
+ "accessibility": {
+ "close": "Itxi",
+ "menu": "Nabigazio nagusia",
+ "open_menu": "Menua ireki",
+ "close_menu": "Menua itxi",
+ "toggle": "Aldatu",
+ "expand": "Zabaldu",
+ "collapse": "Tolestu",
+ "loading": "Kargatzen",
+ "image": "Irudia",
+ "button": "Botoia",
+ "link": "Esteka",
+ "tooltip": "Informazio gehigarria",
+ "search": "Aplikazioan bilatu"
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/locales/eu/dashboard.json b/frontend/src/locales/eu/dashboard.json
new file mode 100644
index 00000000..0e0dceb3
--- /dev/null
+++ b/frontend/src/locales/eu/dashboard.json
@@ -0,0 +1,74 @@
+{
+ "title": "Aginte Panela",
+ "subtitle": "Zure okindegiaren eragiketen ikuspegi orokorra",
+ "stats": {
+ "sales_today": "Gaurko Salmentak",
+ "pending_orders": "Eskaera Zain",
+ "stock_alerts": "Stock Alertak",
+ "production_efficiency": "Ekoizpen Eraginkortasuna",
+ "revenue_growth": "Diru-sarrera Hazkundea",
+ "customer_satisfaction": "Bezeroaren Gogobetetasuna",
+ "inventory_turnover": "Inbentario Biraketa",
+ "daily_profit": "Eguneko Irabazia",
+ "products_sold": "Saldutako Produktuak"
+ },
+ "trends": {
+ "vs_yesterday": "% atzokoarekin alderatuta",
+ "vs_last_week": "% azken astearekin alderatuta",
+ "vs_last_month": "% azken hilarekin alderatuta",
+ "growth": "hazkundea",
+ "decrease": "beherakada",
+ "stable": "egonkorra"
+ },
+ "sections": {
+ "critical_stats": "Estatistika Kritikoak",
+ "real_time_alerts": "Denbora Errealeko Alertak",
+ "procurement_today": "Gaurko Erosketak",
+ "production_today": "Gaurko Ekoizpena",
+ "recent_activity": "Azken Jarduera",
+ "quick_actions": "Ekintza Azkarrak"
+ },
+ "quick_actions": {
+ "add_new_bakery": "Okindegi Berria Gehitu",
+ "create_order": "Eskaera Sortu",
+ "start_production": "Ekoizpena Hasi",
+ "check_inventory": "Inbentarioa Begiratu",
+ "view_reports": "Txostenak Ikusi",
+ "manage_staff": "Langilea Kudeatu"
+ },
+ "alerts": {
+ "low_stock": "Stock Baxua",
+ "production_delay": "Ekoizpen Atzerapena",
+ "quality_issue": "Kalitate Arazoa",
+ "equipment_maintenance": "Ekipo Mantentze",
+ "order_pending": "Eskaera Zain",
+ "delivery_due": "Entrega Atzeratua"
+ },
+ "messages": {
+ "welcome": "Ongi etorri berriro",
+ "good_morning": "Egun on",
+ "good_afternoon": "Arratsalde on",
+ "good_evening": "Iluntzeko on",
+ "no_data": "Ez dago daturik",
+ "loading": "Aginte panelaren datuak kargatzen...",
+ "error_loading": "Errorea datuak kargatzean",
+ "last_updated": "Azken eguneraketa",
+ "auto_refresh": "Eguneraketa automatikoa",
+ "more_than_yesterday": "atzo baino gehiago",
+ "require_attention": "Arreta behar dute",
+ "more_units": "unitate gehiago",
+ "action_required": "Ekintza beharrezkoa",
+ "manage_organizations": "Zure erakundeak kudeatu",
+ "setup_new_business": "Negozio berri bat hutsetik konfiguratu",
+ "active_organizations": "Erakunde Aktiboak"
+ },
+ "time_periods": {
+ "today": "Gaur",
+ "this_week": "Aste Hau",
+ "this_month": "Hilabete Hau",
+ "this_year": "Urte Hau",
+ "last_7_days": "Azken 7 egun",
+ "last_30_days": "Azken 30 egun",
+ "last_90_days": "Azken 90 egun"
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/locales/eu/production.json b/frontend/src/locales/eu/production.json
new file mode 100644
index 00000000..e6bd2a32
--- /dev/null
+++ b/frontend/src/locales/eu/production.json
@@ -0,0 +1,80 @@
+{
+ "title": "Ekoizpena",
+ "subtitle": "Zure okindegiaren ekoizpena kudeatu",
+ "production_status": {
+ "PENDING": "Zain",
+ "IN_PROGRESS": "Abian",
+ "COMPLETED": "Amaituta",
+ "CANCELLED": "Bertan behera utzi",
+ "ON_HOLD": "Pausatuta",
+ "QUALITY_CHECK": "Kalitate Kontrola",
+ "FAILED": "Huts egin"
+ },
+ "production_priority": {
+ "LOW": "Baxua",
+ "MEDIUM": "Ertaina",
+ "HIGH": "Altua",
+ "URGENT": "Larria"
+ },
+ "batch_status": {
+ "PLANNED": "Planifikatuta",
+ "IN_PROGRESS": "Abian",
+ "COMPLETED": "Amaituta",
+ "CANCELLED": "Bertan behera utzi",
+ "ON_HOLD": "Pausatuta"
+ },
+ "quality_check_status": {
+ "PENDING": "Zain",
+ "IN_PROGRESS": "Abian",
+ "PASSED": "Onartuta",
+ "FAILED": "Baztertuta",
+ "REQUIRES_ATTENTION": "Arreta Behar du"
+ },
+ "fields": {
+ "batch_number": "Lote Zenbakia",
+ "production_date": "Ekoizpen Data",
+ "planned_quantity": "Planifikatutako Kantitatea",
+ "actual_quantity": "Benetako Kantitatea",
+ "yield_percentage": "Errendimendu Ehunekoa",
+ "priority": "Lehentasuna",
+ "assigned_staff": "Esleitutako Langilea",
+ "production_notes": "Ekoizpen Oharrak",
+ "quality_score": "Kalitate Puntuazioa",
+ "quality_notes": "Kalitate Oharrak",
+ "defect_rate": "Akats Tasa",
+ "rework_required": "Berrlana Behar",
+ "waste_quantity": "Hondakin Kantitatea",
+ "waste_reason": "Hondakin Arrazoia",
+ "efficiency": "Eraginkortasuna",
+ "material_cost": "Material Kostua",
+ "labor_cost": "Lan Kostua",
+ "overhead_cost": "Kostu Orokorra",
+ "total_cost": "Kostu Osoa",
+ "cost_per_unit": "Unitateko Kostua"
+ },
+ "actions": {
+ "start_production": "Ekoizpena Hasi",
+ "complete_batch": "Lotea Amaitu",
+ "pause_production": "Ekoizpena Pausatu",
+ "cancel_batch": "Lotea Ezeztatu",
+ "quality_check": "Kalitate Kontrola",
+ "create_batch": "Lotea Sortu",
+ "view_details": "Xehetasunak Ikusi",
+ "edit_batch": "Lotea Editatu",
+ "duplicate_batch": "Lotea Bikoiztu"
+ },
+ "labels": {
+ "current_production": "Uneko Ekoizpena",
+ "production_queue": "Ekoizpen Ilara",
+ "completed_today": "Gaur Amaitutakoak",
+ "efficiency_rate": "Eraginkortasun Tasa",
+ "quality_score": "Kalitate Puntuazioa",
+ "active_batches": "Lote Aktiboak",
+ "pending_quality_checks": "Kalitate Kontrol Zain"
+ },
+ "descriptions": {
+ "production_efficiency": "Uneko ekoizpenaren eraginkortasun ehunekoa",
+ "quality_average": "Azken loteen batez besteko kalitate puntuazioa",
+ "waste_reduction": "Hondakin murrizketa aurreko hilarekin alderatuta"
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/locales/index.ts b/frontend/src/locales/index.ts
index 292df896..95a504dc 100644
--- a/frontend/src/locales/index.ts
+++ b/frontend/src/locales/index.ts
@@ -7,6 +7,19 @@ import suppliersEs from './es/suppliers.json';
import ordersEs from './es/orders.json';
import recipesEs from './es/recipes.json';
import errorsEs from './es/errors.json';
+import dashboardEs from './es/dashboard.json';
+import productionEs from './es/production.json';
+
+// English translations
+import commonEn from './en/common.json';
+import recipesEn from './en/recipes.json';
+import dashboardEn from './en/dashboard.json';
+import productionEn from './en/production.json';
+
+// Basque translations
+import commonEu from './eu/common.json';
+import dashboardEu from './eu/dashboard.json';
+import productionEu from './eu/production.json';
// Translation resources by language
export const resources = {
@@ -19,11 +32,24 @@ export const resources = {
orders: ordersEs,
recipes: recipesEs,
errors: errorsEs,
+ dashboard: dashboardEs,
+ production: productionEs,
+ },
+ en: {
+ common: commonEn,
+ recipes: recipesEn,
+ dashboard: dashboardEn,
+ production: productionEn,
+ },
+ eu: {
+ common: commonEu,
+ dashboard: dashboardEu,
+ production: productionEu,
},
};
// Supported languages
-export const supportedLanguages = ['es'] as const;
+export const supportedLanguages = ['es', 'en', 'eu'] as const;
export type SupportedLanguage = typeof supportedLanguages[number];
// Default language
@@ -38,10 +64,24 @@ export const languageConfig = {
flag: '🇪🇸',
rtl: false,
},
+ en: {
+ name: 'English',
+ nativeName: 'English',
+ code: 'en',
+ flag: '🇺🇸',
+ rtl: false,
+ },
+ eu: {
+ name: 'Euskera',
+ nativeName: 'Euskera',
+ code: 'eu',
+ flag: '🏴',
+ rtl: false,
+ },
};
// Namespaces available in translations
-export const namespaces = ['common', 'auth', 'inventory', 'foodSafety', 'suppliers', 'orders', 'recipes', 'errors'] as const;
+export const namespaces = ['common', 'auth', 'inventory', 'foodSafety', 'suppliers', 'orders', 'recipes', 'errors', 'dashboard', 'production'] as const;
export type Namespace = typeof namespaces[number];
// Helper function to get language display name
diff --git a/frontend/src/pages/app/DashboardPage.tsx b/frontend/src/pages/app/DashboardPage.tsx
index 2811efca..5627ed9b 100644
--- a/frontend/src/pages/app/DashboardPage.tsx
+++ b/frontend/src/pages/app/DashboardPage.tsx
@@ -1,67 +1,82 @@
import React from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
import { PageHeader } from '../../components/layout';
+import { Button } from '../../components/ui/Button';
+import { Card, CardHeader, CardBody } from '../../components/ui/Card';
import StatsGrid from '../../components/ui/Stats/StatsGrid';
import RealTimeAlerts from '../../components/domain/dashboard/RealTimeAlerts';
import ProcurementPlansToday from '../../components/domain/dashboard/ProcurementPlansToday';
import ProductionPlansToday from '../../components/domain/dashboard/ProductionPlansToday';
+import { useTenant } from '../../stores/tenant.store';
import {
AlertTriangle,
Clock,
DollarSign,
Package,
TrendingUp,
- TrendingDown
+ TrendingDown,
+ Plus,
+ Building2
} from 'lucide-react';
const DashboardPage: React.FC = () => {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const { availableTenants } = useTenant();
+
+ const handleAddNewBakery = () => {
+ navigate('/app/onboarding?new=true');
+ };
+
const criticalStats = [
{
- title: 'Ventas Hoy',
+ title: t('dashboard:stats.sales_today', 'Sales Today'),
value: '€1,247',
icon: DollarSign,
variant: 'success' as const,
trend: {
value: 12,
direction: 'up' as const,
- label: '% vs ayer'
+ label: t('dashboard:trends.vs_yesterday', '% vs yesterday')
},
- subtitle: '+€135 más que ayer'
+ subtitle: '+€135 ' + t('dashboard:messages.more_than_yesterday', 'more than yesterday')
},
{
- title: 'Órdenes Pendientes',
+ title: t('dashboard:stats.pending_orders', 'Pending Orders'),
value: '23',
icon: Clock,
variant: 'warning' as const,
trend: {
value: 4,
direction: 'down' as const,
- label: '% vs ayer'
+ label: t('dashboard:trends.vs_yesterday', '% vs yesterday')
},
- subtitle: 'Requieren atención'
+ subtitle: t('dashboard:messages.require_attention', 'Require attention')
},
{
- title: 'Productos Vendidos',
+ title: t('dashboard:stats.products_sold', 'Products Sold'),
value: '156',
icon: Package,
variant: 'info' as const,
trend: {
value: 8,
direction: 'up' as const,
- label: '% vs ayer'
+ label: t('dashboard:trends.vs_yesterday', '% vs yesterday')
},
- subtitle: '+12 unidades más'
+ subtitle: '+12 ' + t('dashboard:messages.more_units', 'more units')
},
{
- title: 'Stock Crítico',
+ title: t('dashboard:stats.stock_alerts', 'Critical Stock'),
value: '4',
icon: AlertTriangle,
variant: 'error' as const,
trend: {
value: 100,
direction: 'up' as const,
- label: '% vs ayer'
+ label: t('dashboard:trends.vs_yesterday', '% vs yesterday')
},
- subtitle: 'Acción requerida'
+ subtitle: t('dashboard:messages.action_required', 'Action required')
}
];
@@ -88,20 +103,54 @@ const DashboardPage: React.FC = () => {
return (
{/* Critical Metrics using StatsGrid */}
+ {/* Quick Actions - Add New Bakery */}
+ {availableTenants && availableTenants.length > 0 && (
+
+
+ {t('dashboard:sections.quick_actions', 'Quick Actions')}
+ {t('dashboard:messages.manage_organizations', 'Manage your organizations')}
+
+
+
+
+
+
+
{t('dashboard:quick_actions.add_new_bakery', 'Add New Bakery')}
+
{t('dashboard:messages.setup_new_business', 'Set up a new business from scratch')}
+
+
+
+
+
+
+
{t('dashboard:messages.active_organizations', 'Active Organizations')}
+
{availableTenants.length}
+
+
+
+
+
+ )}
+
{/* Full width blocks - one after another */}
{/* 1. Real-time alerts block */}
diff --git a/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx b/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx
index 172778a3..29908bec 100644
--- a/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx
+++ b/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx
@@ -538,7 +538,7 @@ const ProcurementPage: React.FC = () => {
{/* Critical Requirements Modal */}
{showCriticalRequirements && selectedPlanForRequirements && (
-
+
{/* Header */}
diff --git a/frontend/src/pages/app/settings/organizations/OrganizationsPage.tsx b/frontend/src/pages/app/settings/organizations/OrganizationsPage.tsx
new file mode 100644
index 00000000..7067d9e9
--- /dev/null
+++ b/frontend/src/pages/app/settings/organizations/OrganizationsPage.tsx
@@ -0,0 +1,283 @@
+import React, { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { PageHeader } from '../../../../components/layout';
+import { Button } from '../../../../components/ui/Button';
+import { Card, CardHeader, CardBody } from '../../../../components/ui/Card';
+import { Badge } from '../../../../components/ui/Badge';
+import { Tooltip } from '../../../../components/ui/Tooltip';
+import { useTenant } from '../../../../stores/tenant.store';
+import { useAuthUser } from '../../../../stores/auth.store';
+import {
+ Plus,
+ Building2,
+ Settings,
+ Users,
+ Calendar,
+ MapPin,
+ Phone,
+ Mail,
+ Globe,
+ MoreHorizontal,
+ ArrowRight,
+ Crown,
+ Shield,
+ Eye
+} from 'lucide-react';
+
+const OrganizationsPage: React.FC = () => {
+ const navigate = useNavigate();
+ const user = useAuthUser();
+ const { currentTenant, availableTenants, switchTenant } = useTenant();
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleAddNewOrganization = () => {
+ navigate('/app/onboarding?new=true');
+ };
+
+ const handleSwitchToTenant = async (tenantId: string) => {
+ if (tenantId === currentTenant?.id) return;
+
+ setIsLoading(true);
+ await switchTenant(tenantId);
+ setIsLoading(false);
+ };
+
+ const handleManageTenant = (tenantId: string) => {
+ // Navigate to tenant settings
+ navigate(`/app/database/bakery-config`);
+ };
+
+ const handleManageTeam = (tenantId: string) => {
+ // Navigate to team management
+ navigate(`/app/database/team`);
+ };
+
+ const getRoleIcon = (ownerId: string) => {
+ if (user?.id === ownerId) {
+ return
;
+ }
+ return
;
+ };
+
+ const getRoleLabel = (ownerId: string) => {
+ if (user?.id === ownerId) {
+ return 'Propietario';
+ }
+ return 'Miembro';
+ };
+
+ const formatDate = (dateString: string) => {
+ return new Date(dateString).toLocaleDateString('es-ES', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric'
+ });
+ };
+
+ return (
+
+
+
+ Nueva Organización
+
+ }
+ />
+
+ {/* Statistics */}
+
+
+
+
+
+ {availableTenants?.length || 0}
+
+ Organizaciones Totales
+
+
+
+
+
+
+
+ {availableTenants?.filter(t => t.owner_id === user?.id).length || 0}
+
+ Propietario
+
+
+
+
+
+
+
+ {availableTenants?.filter(t => t.owner_id !== user?.id).length || 0}
+
+ Miembro
+
+
+
+
+ {/* Organizations List */}
+
+
Tus Organizaciones
+
+ {availableTenants && availableTenants.length > 0 ? (
+
+ {availableTenants.map((tenant) => (
+
+
+
+
+
+
+
+
+
+ {tenant.name}
+
+ {currentTenant?.id === tenant.id && (
+ Activa
+ )}
+
+
+ {getRoleIcon(tenant.owner_id)}
+ {getRoleLabel(tenant.owner_id)}
+
+
+
+
+
+
+ handleManageTenant(tenant.id)}
+ className="w-8 h-8 p-0"
+ >
+
+
+
+
+ {user?.id === tenant.owner_id && (
+
+ handleManageTeam(tenant.id)}
+ className="w-8 h-8 p-0"
+ >
+
+
+
+ )}
+
+
+
+
+ {/* Organization details */}
+
+ {tenant.business_type && (
+
+ {tenant.business_type}
+
+ )}
+
+ {tenant.address && (
+
+
+ {tenant.address}
+
+ )}
+
+ {tenant.city && (
+
+
+ {tenant.city}
+
+ )}
+
+ {tenant.phone && (
+
+ )}
+
+
+
+ Creada el {formatDate(tenant.created_at)}
+
+
+
+ {/* Actions */}
+
+ {currentTenant?.id !== tenant.id ? (
+
handleSwitchToTenant(tenant.id)}
+ variant="primary"
+ size="sm"
+ disabled={isLoading}
+ className="flex items-center gap-2"
+ >
+
+ Cambiar a esta organización
+
+ ) : (
+
navigate('/app/dashboard')}
+ variant="outline"
+ size="sm"
+ className="flex items-center gap-2"
+ >
+
+ Ver dashboard
+
+ )}
+
+
+
+ ))}
+
+ ) : (
+
+
+
+
+ No tienes organizaciones
+
+
+ Crea tu primera organización para comenzar a usar Bakery IA
+
+
+
+ Crear Primera Organización
+
+
+
+ )}
+
+
+ );
+};
+
+export default OrganizationsPage;
\ No newline at end of file
diff --git a/frontend/src/router/AppRouter.tsx b/frontend/src/router/AppRouter.tsx
index 42407e57..9fb2f020 100644
--- a/frontend/src/router/AppRouter.tsx
+++ b/frontend/src/router/AppRouter.tsx
@@ -30,6 +30,7 @@ const PerformanceAnalyticsPage = React.lazy(() => import('../pages/app/analytics
const ProfilePage = React.lazy(() => import('../pages/app/settings/profile/ProfilePage'));
const BakeryConfigPage = React.lazy(() => import('../pages/app/settings/bakery-config/BakeryConfigPage'));
const TeamPage = React.lazy(() => import('../pages/app/settings/team/TeamPage'));
+const OrganizationsPage = React.lazy(() => import('../pages/app/settings/organizations/OrganizationsPage'));
// Database pages
const DatabasePage = React.lazy(() => import('../pages/app/database/DatabasePage'));
@@ -232,15 +233,25 @@ export const AppRouter: React.FC = () => {
{/* Settings Routes */}
-
- }
+ }
+ />
+
+
+
+
+
+ }
/>
{/* Data Routes */}
diff --git a/frontend/src/router/routes.config.ts b/frontend/src/router/routes.config.ts
index 99ac08fe..20eb15e0 100644
--- a/frontend/src/router/routes.config.ts
+++ b/frontend/src/router/routes.config.ts
@@ -428,6 +428,16 @@ export const routesConfig: RouteConfig[] = [
showInNavigation: true,
showInBreadcrumbs: true,
},
+ {
+ path: '/app/settings/organizations',
+ name: 'Organizations',
+ component: 'OrganizationsPage',
+ title: 'Mis Organizaciones',
+ icon: 'database',
+ requiresAuth: true,
+ showInNavigation: true,
+ showInBreadcrumbs: true,
+ },
],
},
diff --git a/frontend/src/stores/tenant.store.ts b/frontend/src/stores/tenant.store.ts
index 8c405d9f..aa03d53f 100644
--- a/frontend/src/stores/tenant.store.ts
+++ b/frontend/src/stores/tenant.store.ts
@@ -40,9 +40,11 @@ export const useTenantStore = create()(
setCurrentTenant: (tenant: TenantResponse) => {
set({ currentTenant: tenant, currentTenantAccess: null });
// Update API client with new tenant ID
- tenantService.setCurrentTenant(tenant);
- // Load tenant access info
- get().loadCurrentTenantAccess();
+ if (tenant) {
+ tenantService.setCurrentTenant(tenant);
+ // Load tenant access info
+ get().loadCurrentTenantAccess();
+ }
},
switchTenant: async (tenantId: string): Promise => {
diff --git a/frontend/src/stores/ui.store.ts b/frontend/src/stores/ui.store.ts
index f5f663e9..5ae3d9eb 100644
--- a/frontend/src/stores/ui.store.ts
+++ b/frontend/src/stores/ui.store.ts
@@ -2,7 +2,7 @@ import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
export type Theme = 'light' | 'dark' | 'auto';
-export type Language = 'es' | 'en' | 'fr' | 'pt' | 'it';
+export type Language = 'es' | 'en' | 'eu';
export type ViewMode = 'list' | 'grid' | 'card';
export type SidebarState = 'expanded' | 'collapsed' | 'hidden';
@@ -152,7 +152,12 @@ export const useUIStore = create()(
setLanguage: (language: Language) => {
set({ language });
- // You might want to trigger i18n language change here
+ // Trigger i18n language change only if different
+ import('../i18n').then(({ default: i18n }) => {
+ if (i18n.language !== language) {
+ i18n.changeLanguage(language);
+ }
+ });
},
setSidebarState: (sidebarState: SidebarState) => {
diff --git a/frontend/src/utils/enumHelpers.ts b/frontend/src/utils/enumHelpers.ts
index f6869b63..824a5f8c 100644
--- a/frontend/src/utils/enumHelpers.ts
+++ b/frontend/src/utils/enumHelpers.ts
@@ -80,8 +80,8 @@ export function useSupplierEnums() {
}),
getSupplierTypeLabel: (type: SupplierType): string => {
- if (!type) return 'Tipo no definido';
- return t(`types.${type}`);
+ if (!type) return t('common:status.undefined', 'Type not defined');
+ return t(`types.${type}`, type);
},
// Supplier Status
@@ -89,8 +89,8 @@ export function useSupplierEnums() {
enumToSelectOptions(SupplierStatus, 'status', t),
getSupplierStatusLabel: (status: SupplierStatus): string => {
- if (!status) return 'Estado no definido';
- return t(`status.${status}`);
+ if (!status) return t('common:status.undefined', 'Status not defined');
+ return t(`status.${status}`, status);
},
// Payment Terms
@@ -101,8 +101,8 @@ export function useSupplierEnums() {
}),
getPaymentTermsLabel: (terms: PaymentTerms): string => {
- if (!terms) return 'Sin términos definidos';
- return t(`payment_terms.${terms}`);
+ if (!terms) return t('common:forms.no_terms', 'No terms defined');
+ return t(`payment_terms.${terms}`, terms);
},
// Purchase Order Status
@@ -110,8 +110,8 @@ export function useSupplierEnums() {
enumToSelectOptions(PurchaseOrderStatus, 'purchase_order_status', t),
getPurchaseOrderStatusLabel: (status: PurchaseOrderStatus): string => {
- if (!status) return 'Estado no definido';
- return t(`purchase_order_status.${status}`);
+ if (!status) return t('common:status.undefined', 'Status not defined');
+ return t(`purchase_order_status.${status}`, status);
},
// Delivery Status
@@ -119,8 +119,8 @@ export function useSupplierEnums() {
enumToSelectOptions(DeliveryStatus, 'delivery_status', t),
getDeliveryStatusLabel: (status: DeliveryStatus): string => {
- if (!status) return 'Estado no definido';
- return t(`delivery_status.${status}`);
+ if (!status) return t('common:status.undefined', 'Status not defined');
+ return t(`delivery_status.${status}`, status);
},
// Quality Rating
@@ -131,8 +131,8 @@ export function useSupplierEnums() {
}),
getQualityRatingLabel: (rating: QualityRating): string => {
- if (rating === undefined || rating === null) return 'Sin calificación';
- return t(`quality_rating.${rating}`);
+ if (rating === undefined || rating === null) return t('common:status.no_rating', 'No rating');
+ return t(`quality_rating.${rating}`, rating.toString());
},
// Delivery Rating
@@ -143,8 +143,8 @@ export function useSupplierEnums() {
}),
getDeliveryRatingLabel: (rating: DeliveryRating): string => {
- if (rating === undefined || rating === null) return 'Sin calificación';
- return t(`delivery_rating.${rating}`);
+ if (rating === undefined || rating === null) return t('common:status.no_rating', 'No rating');
+ return t(`delivery_rating.${rating}`, rating.toString());
},
// Invoice Status
@@ -152,8 +152,8 @@ export function useSupplierEnums() {
enumToSelectOptions(InvoiceStatus, 'invoice_status', t),
getInvoiceStatusLabel: (status: InvoiceStatus): string => {
- if (!status) return 'Estado no definido';
- return t(`invoice_status.${status}`);
+ if (!status) return t('common:status.undefined', 'Status not defined');
+ return t(`invoice_status.${status}`, status);
},
// Field Labels
@@ -197,12 +197,8 @@ export function useOrderEnums() {
enumToSelectOptions(CustomerType, 'customer_types', t),
getCustomerTypeLabel: (type: CustomerType): string => {
- if (!type) return 'Tipo no definido';
- const translated = t(`customer_types.${type}`);
- if (translated === `customer_types.${type}`) {
- return type.charAt(0).toUpperCase() + type.slice(1);
- }
- return translated;
+ if (!type) return t('common:status.undefined', 'Type not defined');
+ return t(`customer_types.${type}`, type.charAt(0).toUpperCase() + type.slice(1));
},
// Delivery Method
@@ -210,12 +206,8 @@ export function useOrderEnums() {
enumToSelectOptions(DeliveryMethod, 'delivery_methods', t),
getDeliveryMethodLabel: (method: DeliveryMethod): string => {
- if (!method) return 'Método no definido';
- const translated = t(`delivery_methods.${method}`);
- if (translated === `delivery_methods.${method}`) {
- return method.charAt(0).toUpperCase() + method.slice(1);
- }
- return translated;
+ if (!method) return t('common:status.undefined', 'Method not defined');
+ return t(`delivery_methods.${method}`, method.charAt(0).toUpperCase() + method.slice(1));
},
// Payment Terms
@@ -223,8 +215,8 @@ export function useOrderEnums() {
enumToSelectOptions(OrderPaymentTerms, 'payment_terms', t),
getPaymentTermsLabel: (terms: OrderPaymentTerms): string => {
- if (!terms) return 'Términos no definidos';
- return t(`payment_terms.${terms}`);
+ if (!terms) return t('common:forms.no_terms', 'Terms not defined');
+ return t(`payment_terms.${terms}`, terms);
},
// Payment Method
@@ -232,8 +224,8 @@ export function useOrderEnums() {
enumToSelectOptions(PaymentMethod, 'payment_methods', t),
getPaymentMethodLabel: (method: PaymentMethod): string => {
- if (!method) return 'Método no definido';
- return t(`payment_methods.${method}`);
+ if (!method) return t('common:status.undefined', 'Method not defined');
+ return t(`payment_methods.${method}`, method);
},
// Payment Status
@@ -241,8 +233,8 @@ export function useOrderEnums() {
enumToSelectOptions(PaymentStatus, 'payment_status', t),
getPaymentStatusLabel: (status: PaymentStatus): string => {
- if (!status) return 'Estado no definido';
- return t(`payment_status.${status}`);
+ if (!status) return t('common:status.undefined', 'Status not defined');
+ return t(`payment_status.${status}`, status);
},
// Customer Segment
@@ -250,8 +242,8 @@ export function useOrderEnums() {
enumToSelectOptions(CustomerSegment, 'customer_segments', t),
getCustomerSegmentLabel: (segment: CustomerSegment): string => {
- if (!segment) return 'Segmento no definido';
- return t(`customer_segments.${segment}`);
+ if (!segment) return t('common:status.undefined', 'Segment not defined');
+ return t(`customer_segments.${segment}`, segment);
},
// Priority Level
@@ -259,8 +251,8 @@ export function useOrderEnums() {
enumToSelectOptions(PriorityLevel, 'priority_levels', t),
getPriorityLevelLabel: (level: PriorityLevel): string => {
- if (!level) return 'Prioridad no definida';
- return t(`priority_levels.${level}`);
+ if (!level) return t('common:priority.undefined', 'Priority not defined');
+ return t(`priority_levels.${level}`, level);
},
// Order Type
@@ -268,13 +260,8 @@ export function useOrderEnums() {
enumToSelectOptions(OrderType, 'order_types', t),
getOrderTypeLabel: (type: OrderType): string => {
- if (!type) return 'Tipo no definido';
- const translated = t(`order_types.${type}`);
- // If translation failed, return a fallback
- if (translated === `order_types.${type}`) {
- return type.charAt(0).toUpperCase() + type.slice(1);
- }
- return translated;
+ if (!type) return t('common:status.undefined', 'Type not defined');
+ return t(`order_types.${type}`, type.charAt(0).toUpperCase() + type.slice(1));
},
// Order Status
@@ -282,8 +269,8 @@ export function useOrderEnums() {
enumToSelectOptions(OrderStatus, 'order_status', t),
getOrderStatusLabel: (status: OrderStatus): string => {
- if (!status) return 'Estado no definido';
- return t(`order_status.${status}`);
+ if (!status) return t('common:status.undefined', 'Status not defined');
+ return t(`order_status.${status}`, status);
},
// Order Source
@@ -291,8 +278,8 @@ export function useOrderEnums() {
enumToSelectOptions(OrderSource, 'order_sources', t),
getOrderSourceLabel: (source: OrderSource): string => {
- if (!source) return 'Origen no definido';
- return t(`order_sources.${source}`);
+ if (!source) return t('common:status.undefined', 'Source not defined');
+ return t(`order_sources.${source}`, source);
},
// Sales Channel
@@ -300,8 +287,8 @@ export function useOrderEnums() {
enumToSelectOptions(SalesChannel, 'sales_channels', t),
getSalesChannelLabel: (channel: SalesChannel): string => {
- if (!channel) return 'Canal no definido';
- return t(`sales_channels.${channel}`);
+ if (!channel) return t('common:status.undefined', 'Channel not defined');
+ return t(`sales_channels.${channel}`, channel);
},
// Field Labels
@@ -325,22 +312,8 @@ export function useProductionEnums() {
enumToSelectOptions(ProductionStatusEnum, 'production_status', t),
getProductionStatusLabel: (status: ProductionStatusEnum): string => {
- if (!status) return 'Estado no definido';
- const translated = t(`production_status.${status}`);
- // If translation failed, return a fallback
- if (translated === `production_status.${status}`) {
- const fallbacks = {
- [ProductionStatusEnum.PENDING]: 'Pendiente',
- [ProductionStatusEnum.IN_PROGRESS]: 'En Proceso',
- [ProductionStatusEnum.COMPLETED]: 'Completado',
- [ProductionStatusEnum.CANCELLED]: 'Cancelado',
- [ProductionStatusEnum.ON_HOLD]: 'En Pausa',
- [ProductionStatusEnum.QUALITY_CHECK]: 'Control Calidad',
- [ProductionStatusEnum.FAILED]: 'Fallido'
- };
- return fallbacks[status] || status;
- }
- return translated;
+ if (!status) return t('common:status.undefined', 'Status not defined');
+ return t(`production_status.${status}`, status);
},
// Production Priority
@@ -348,19 +321,8 @@ export function useProductionEnums() {
enumToSelectOptions(ProductionPriorityEnum, 'production_priority', t),
getProductionPriorityLabel: (priority: ProductionPriorityEnum): string => {
- if (!priority) return 'Prioridad no definida';
- const translated = t(`production_priority.${priority}`);
- // If translation failed, return a fallback
- if (translated === `production_priority.${priority}`) {
- const fallbacks = {
- [ProductionPriorityEnum.LOW]: 'Baja',
- [ProductionPriorityEnum.MEDIUM]: 'Media',
- [ProductionPriorityEnum.HIGH]: 'Alta',
- [ProductionPriorityEnum.URGENT]: 'Urgente'
- };
- return fallbacks[priority] || priority;
- }
- return translated;
+ if (!priority) return t('common:priority.undefined', 'Priority not defined');
+ return t(`production_priority.${priority}`, priority);
},
// Production Batch Status
@@ -368,20 +330,8 @@ export function useProductionEnums() {
enumToSelectOptions(ProductionBatchStatus, 'batch_status', t),
getProductionBatchStatusLabel: (status: ProductionBatchStatus): string => {
- if (!status) return 'Estado no definido';
- const translated = t(`batch_status.${status}`);
- // If translation failed, return a fallback
- if (translated === `batch_status.${status}`) {
- const fallbacks = {
- [ProductionBatchStatus.PLANNED]: 'Planificado',
- [ProductionBatchStatus.IN_PROGRESS]: 'En Proceso',
- [ProductionBatchStatus.COMPLETED]: 'Completado',
- [ProductionBatchStatus.CANCELLED]: 'Cancelado',
- [ProductionBatchStatus.ON_HOLD]: 'En Pausa'
- };
- return fallbacks[status] || status;
- }
- return translated;
+ if (!status) return t('common:status.undefined', 'Status not defined');
+ return t(`batch_status.${status}`, status);
},
// Quality Check Status
@@ -389,20 +339,8 @@ export function useProductionEnums() {
enumToSelectOptions(QualityCheckStatus, 'quality_check_status', t),
getQualityCheckStatusLabel: (status: QualityCheckStatus): string => {
- if (!status) return 'Estado no definido';
- const translated = t(`quality_check_status.${status}`);
- // If translation failed, return a fallback
- if (translated === `quality_check_status.${status}`) {
- const fallbacks = {
- [QualityCheckStatus.PENDING]: 'Pendiente',
- [QualityCheckStatus.IN_PROGRESS]: 'En Proceso',
- [QualityCheckStatus.PASSED]: 'Aprobado',
- [QualityCheckStatus.FAILED]: 'Reprobado',
- [QualityCheckStatus.REQUIRES_ATTENTION]: 'Requiere Atención'
- };
- return fallbacks[status] || status;
- }
- return translated;
+ if (!status) return t('common:status.undefined', 'Status not defined');
+ return t(`quality_check_status.${status}`, status);
},
// Field Labels