From c07df124fb41a726ae25ff11115a8cc6336956e3 Mon Sep 17 00:00:00 2001
From: Urtzi Alfaro
Date: Tue, 30 Dec 2025 14:40:20 +0100
Subject: [PATCH] Improve UI
---
frontend/src/api/hooks/performance.ts | 6 +-
frontend/src/api/services/aiInsights.ts | 7 +-
frontend/src/api/types/tenant.ts | 9 ++
.../components/charts/PerformanceChart.tsx | 6 +-
.../components/dashboard/DistributionTab.tsx | 4 +-
.../dashboard/NetworkPerformanceTab.tsx | 18 ++--
.../components/dashboard/PerformanceChart.tsx | 6 +-
.../blocks/PendingPurchasesBlock.tsx | 4 +-
.../dashboard/blocks/SystemStatusBlock.tsx | 6 +-
.../domain/analytics/AnalyticsDashboard.tsx | 22 +++--
.../src/components/domain/auth/LoginForm.tsx | 8 +-
.../components/domain/auth/RegisterForm.tsx | 97 ++++++++++++++-----
.../AutoActionCountdownComponent.tsx | 4 +-
.../domain/dashboard/PendingPOApprovals.tsx | 7 +-
.../equipment/MaintenanceHistoryModal.tsx | 4 +-
.../inventory/BatchAddIngredientsModal.tsx | 4 +-
.../inventory/QuickAddIngredientModal.tsx | 4 +-
.../onboarding/steps/UploadSalesDataStep.tsx | 8 +-
.../domain/orders/OrderFormModal.tsx | 6 +-
.../src/components/domain/pos/POSCart.tsx | 13 ++-
.../src/components/domain/pos/POSPayment.tsx | 10 +-
.../components/domain/pos/POSProductCard.tsx | 4 +-
.../procurement/CreatePurchaseOrderModal.tsx | 6 +-
.../procurement/ModifyPurchaseOrderModal.tsx | 4 +-
.../procurement/UnifiedPurchaseOrderModal.tsx | 20 ++--
.../analytics/widgets/AIInsightsWidget.tsx | 10 +-
.../analytics/widgets/CostPerUnitWidget.tsx | 12 ++-
.../widgets/MaintenanceScheduleWidget.tsx | 8 +-
.../widgets/PredictiveMaintenanceWidget.tsx | 8 +-
.../widgets/TopDefectTypesWidget.tsx | 6 +-
.../widgets/WasteDefectTrackerWidget.tsx | 6 +-
.../components/domain/sales/CustomerInfo.tsx | 11 ++-
.../src/components/domain/sales/OrderForm.tsx | 26 ++---
.../components/domain/sales/OrdersTable.tsx | 20 ++--
.../components/domain/sales/SalesChart.tsx | 18 ++--
.../steps/SupplierProductManager.tsx | 6 +-
.../SupplierWizard/SupplierDeliveryStep.tsx | 5 +-
.../SupplierWizard/SupplierReviewStep.tsx | 5 +-
.../sustainability/SustainabilityWidget.tsx | 4 +-
.../wizards/CustomerOrderWizard.tsx | 11 ++-
.../wizards/InventoryWizard.tsx | 31 +++---
.../wizards/PurchaseOrderWizard.tsx | 23 +++--
.../wizards/SalesEntryWizard.tsx | 15 +--
.../src/components/ui/AddModal/AddModal.tsx | 6 +-
.../AdvancedOptionsSection.tsx | 6 +-
.../NotificationPanel/NotificationPanel.tsx | 4 +-
.../src/components/ui/Stats/StatsPresets.ts | 9 +-
frontend/src/contexts/SSEContext.tsx | 7 +-
frontend/src/hooks/useTenantCurrency.ts | 92 ++++++++++++++++++
frontend/src/locales/en/auth.json | 8 +-
frontend/src/locales/es/auth.json | 30 ++----
frontend/src/locales/es/wizards.json | 27 ++++--
frontend/src/locales/eu/auth.json | 8 +-
.../src/pages/app/EnterpriseDashboardPage.tsx | 6 +-
.../analytics/ProcurementAnalyticsPage.tsx | 12 ++-
.../performance/PerformanceAnalyticsPage.tsx | 10 +-
.../sustainability/SustainabilityPage.tsx | 16 +--
.../app/operations/orders/OrdersPage.tsx | 6 +-
.../src/pages/app/operations/pos/POSPage.tsx | 5 +-
.../procurement/ProcurementPage.tsx | 5 +-
.../app/operations/recipes/RecipesPage.tsx | 4 +-
.../operations/suppliers/SuppliersPage.tsx | 4 +-
.../settings/bakery/BakerySettingsPage.tsx | 18 ++--
frontend/src/utils/currency.ts | 10 +-
frontend/src/utils/smartActionHandlers.ts | 4 +-
frontend/src/utils/validation.ts | 41 +++++++-
services/auth/app/services/user_service.py | 36 ++++++-
services/tenant/app/models/tenants.py | 4 +-
services/tenant/app/schemas/tenants.py | 8 ++
.../versions/001_unified_initial_schema.py | 5 +-
.../training/app/services/training_service.py | 9 +-
71 files changed, 647 insertions(+), 265 deletions(-)
create mode 100644 frontend/src/hooks/useTenantCurrency.ts
diff --git a/frontend/src/api/hooks/performance.ts b/frontend/src/api/hooks/performance.ts
index 7b29866b..6c4e0354 100644
--- a/frontend/src/api/hooks/performance.ts
+++ b/frontend/src/api/hooks/performance.ts
@@ -24,6 +24,7 @@ import type { InventoryDashboardSummary } from '../types/inventory';
import { useSalesAnalytics } from './sales';
import { useProcurementDashboard } from './procurement';
import { useOrdersDashboard } from './orders';
+import { useTenantCurrency } from '../../hooks/useTenantCurrency';
// ============================================================================
// Helper Functions
@@ -355,6 +356,7 @@ export const useDepartmentPerformance = (tenantId: string, period: TimePeriod =
const { data: inventory, isLoading: inventoryLoading } = useInventoryPerformance(tenantId);
const { data: sales, isLoading: salesLoading } = useSalesPerformance(tenantId, period);
const { data: procurement, isLoading: procurementLoading } = useProcurementPerformance(tenantId);
+ const { currencySymbol } = useTenantCurrency();
// Extract primitive values before useMemo to prevent unnecessary recalculations
const productionEfficiency = production?.efficiency || 0;
@@ -408,7 +410,7 @@ export const useDepartmentPerformance = (tenantId: string, period: TimePeriod =
primary_metric: {
label: 'Ingresos totales',
value: salesTotalRevenue,
- unit: '€',
+ unit: currencySymbol,
},
secondary_metric: {
label: 'Transacciones',
@@ -418,7 +420,7 @@ export const useDepartmentPerformance = (tenantId: string, period: TimePeriod =
tertiary_metric: {
label: 'Valor promedio',
value: salesAvgTransactionValue,
- unit: '€',
+ unit: currencySymbol,
},
},
},
diff --git a/frontend/src/api/services/aiInsights.ts b/frontend/src/api/services/aiInsights.ts
index 6e9dfc46..5c2a3e4a 100644
--- a/frontend/src/api/services/aiInsights.ts
+++ b/frontend/src/api/services/aiInsights.ts
@@ -16,6 +16,8 @@
*/
import { apiClient } from '../client';
+import { useTenantStore } from '../../stores/tenant.store';
+import { getTenantCurrencySymbol } from '../../hooks/useTenantCurrency';
export interface AIInsight {
id: string;
@@ -380,11 +382,12 @@ export class AIInsightsService {
const value = insight.impact_value;
const unit = insight.impact_unit || 'units';
+ const currencySymbol = getTenantCurrencySymbol(useTenantStore.getState().currentTenant?.currency);
if (unit === 'euros_per_year' || unit === 'eur') {
- return `€${value.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}/year`;
+ return `${currencySymbol}${value.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}/year`;
} else if (unit === 'euros') {
- return `€${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
+ return `${currencySymbol}${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
} else if (unit === 'percentage' || unit === 'percentage_points') {
return `${value.toFixed(1)}%`;
} else if (unit === 'units') {
diff --git a/frontend/src/api/types/tenant.ts b/frontend/src/api/types/tenant.ts
index b7d8ecc6..c2add48c 100644
--- a/frontend/src/api/types/tenant.ts
+++ b/frontend/src/api/types/tenant.ts
@@ -41,6 +41,10 @@ export interface TenantUpdate {
phone?: string | null;
business_type?: string | null;
business_model?: string | null;
+ // Regional/Localization settings
+ currency?: string | null; // Currency code (EUR, USD, GBP)
+ timezone?: string | null;
+ language?: string | null;
}
/**
@@ -130,6 +134,11 @@ export interface TenantResponse {
owner_id: string; // ✅ REQUIRED field
created_at: string; // ISO datetime string
+ // Regional/Localization settings
+ currency?: string | null; // Default: 'EUR' - Currency code (EUR, USD, GBP)
+ timezone?: string | null; // Default: 'Europe/Madrid'
+ language?: string | null; // Default: 'es'
+
// Backward compatibility
/** @deprecated Use subscription_plan instead */
subscription_tier?: string;
diff --git a/frontend/src/components/charts/PerformanceChart.tsx b/frontend/src/components/charts/PerformanceChart.tsx
index 5bc8b031..2bc799ca 100644
--- a/frontend/src/components/charts/PerformanceChart.tsx
+++ b/frontend/src/components/charts/PerformanceChart.tsx
@@ -8,6 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card';
import { Badge } from '../ui/Badge';
import { BarChart3, TrendingUp, TrendingDown, ArrowUp, ArrowDown, ExternalLink, Package, ShoppingCart } from 'lucide-react';
import { useTranslation } from 'react-i18next';
+import { useTenantCurrency } from '../../hooks/useTenantCurrency';
interface PerformanceDataPoint {
rank: number;
@@ -30,6 +31,7 @@ const PerformanceChart: React.FC = ({
onOutletClick
}) => {
const { t } = useTranslation('dashboard');
+ const { currencySymbol } = useTenantCurrency();
// Get metric info
const getMetricInfo = () => {
@@ -38,14 +40,14 @@ const PerformanceChart: React.FC = ({
return {
icon: ,
label: t('enterprise.metrics.sales'),
- unit: '€',
+ unit: currencySymbol,
format: (val: number) => val.toLocaleString('es-ES', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
};
case 'inventory_value':
return {
icon: ,
label: t('enterprise.metrics.inventory_value'),
- unit: '€',
+ unit: currencySymbol,
format: (val: number) => val.toLocaleString('es-ES', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
};
case 'order_frequency':
diff --git a/frontend/src/components/dashboard/DistributionTab.tsx b/frontend/src/components/dashboard/DistributionTab.tsx
index 7dc45f6f..29bdcbe5 100644
--- a/frontend/src/components/dashboard/DistributionTab.tsx
+++ b/frontend/src/components/dashboard/DistributionTab.tsx
@@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next';
import { useDistributionOverview } from '../../api/hooks/useEnterpriseDashboard';
import { useSSEEvents } from '../../hooks/useSSE';
import StatusCard from '../ui/StatusCard/StatusCard';
+import { useTenantCurrency } from '../../hooks/useTenantCurrency';
interface DistributionTabProps {
tenantId: string;
@@ -20,6 +21,7 @@ interface DistributionTabProps {
const DistributionTab: React.FC = ({ tenantId, selectedDate, onDateChange }) => {
const { t } = useTranslation('dashboard');
+ const { currencySymbol } = useTenantCurrency();
// Get distribution data
const {
@@ -317,7 +319,7 @@ const DistributionTab: React.FC = ({ tenantId, selectedDat
- €{optimizationMetrics.fuelSaved.toFixed(2)}
+ {currencySymbol}{optimizationMetrics.fuelSaved.toFixed(2)}
{t('enterprise.estimated_fuel_savings')}
diff --git a/frontend/src/components/dashboard/NetworkPerformanceTab.tsx b/frontend/src/components/dashboard/NetworkPerformanceTab.tsx
index 12f296ee..ab29b78d 100644
--- a/frontend/src/components/dashboard/NetworkPerformanceTab.tsx
+++ b/frontend/src/components/dashboard/NetworkPerformanceTab.tsx
@@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next';
import { useChildrenPerformance } from '../../api/hooks/useEnterpriseDashboard';
import PerformanceChart from '../charts/PerformanceChart';
import StatusCard from '../ui/StatusCard/StatusCard';
+import { useTenantCurrency } from '../../hooks/useTenantCurrency';
interface NetworkPerformanceTabProps {
tenantId: string;
@@ -19,6 +20,7 @@ interface NetworkPerformanceTabProps {
const NetworkPerformanceTab: React.FC = ({ tenantId, onOutletClick }) => {
const { t } = useTranslation('dashboard');
+ const { currencySymbol } = useTenantCurrency();
const [selectedMetric, setSelectedMetric] = useState('sales');
const [selectedPeriod, setSelectedPeriod] = useState(30);
const [viewMode, setViewMode] = useState<'chart' | 'cards'>('chart');
@@ -216,8 +218,8 @@ const NetworkPerformanceTab: React.FC = ({ tenantId,
- {selectedMetric === 'sales' ? `€${networkMetrics.avgSales.toLocaleString()}` :
- selectedMetric === 'inventory_value' ? `€${networkMetrics.avgInventory.toLocaleString()}` :
+ {selectedMetric === 'sales' ? `${currencySymbol}${networkMetrics.avgSales.toLocaleString()}` :
+ selectedMetric === 'inventory_value' ? `${currencySymbol}${networkMetrics.avgInventory.toLocaleString()}` :
networkMetrics.avgOrders.toLocaleString()}
@@ -266,8 +268,8 @@ const NetworkPerformanceTab: React.FC = ({ tenantId,
}}
title={networkMetrics.topPerformer.outlet_name}
subtitle={t('enterprise.best_in_network')}
- primaryValue={selectedMetric === 'sales' ? `€${networkMetrics.topPerformer.metric_value.toLocaleString()}` :
- selectedMetric === 'inventory_value' ? `€${networkMetrics.topPerformer.metric_value.toLocaleString()}` :
+ primaryValue={selectedMetric === 'sales' ? `${currencySymbol}${networkMetrics.topPerformer.metric_value.toLocaleString()}` :
+ selectedMetric === 'inventory_value' ? `${currencySymbol}${networkMetrics.topPerformer.metric_value.toLocaleString()}` :
networkMetrics.topPerformer.metric_value.toLocaleString()}
primaryValueLabel={selectedMetric === 'sales' ? t('enterprise.sales') :
selectedMetric === 'inventory_value' ? t('enterprise.inventory_value') :
@@ -305,8 +307,8 @@ const NetworkPerformanceTab: React.FC = ({ tenantId,
}}
title={networkMetrics.bottomPerformer.outlet_name}
subtitle={t('enterprise.improvement_opportunity')}
- primaryValue={selectedMetric === 'sales' ? `€${networkMetrics.bottomPerformer.metric_value.toLocaleString()}` :
- selectedMetric === 'inventory_value' ? `€${networkMetrics.bottomPerformer.metric_value.toLocaleString()}` :
+ primaryValue={selectedMetric === 'sales' ? `${currencySymbol}${networkMetrics.bottomPerformer.metric_value.toLocaleString()}` :
+ selectedMetric === 'inventory_value' ? `${currencySymbol}${networkMetrics.bottomPerformer.metric_value.toLocaleString()}` :
networkMetrics.bottomPerformer.metric_value.toLocaleString()}
primaryValueLabel={selectedMetric === 'sales' ? t('enterprise.sales') :
selectedMetric === 'inventory_value' ? t('enterprise.inventory_value') :
@@ -429,8 +431,8 @@ const NetworkPerformanceTab: React.FC = ({ tenantId,
}}
title={outlet.outlet_name}
subtitle={`#${index + 1} ${t('enterprise.of')} ${childrenPerformance.rankings.length}`}
- primaryValue={selectedMetric === 'sales' ? `€${outlet.metric_value.toLocaleString()}` :
- selectedMetric === 'inventory_value' ? `€${outlet.metric_value.toLocaleString()}` :
+ primaryValue={selectedMetric === 'sales' ? `${currencySymbol}${outlet.metric_value.toLocaleString()}` :
+ selectedMetric === 'inventory_value' ? `${currencySymbol}${outlet.metric_value.toLocaleString()}` :
outlet.metric_value.toLocaleString()}
primaryValueLabel={selectedMetric === 'sales' ? t('enterprise.sales') :
selectedMetric === 'inventory_value' ? t('enterprise.inventory_value') :
diff --git a/frontend/src/components/dashboard/PerformanceChart.tsx b/frontend/src/components/dashboard/PerformanceChart.tsx
index 59bf9527..e7239419 100644
--- a/frontend/src/components/dashboard/PerformanceChart.tsx
+++ b/frontend/src/components/dashboard/PerformanceChart.tsx
@@ -16,6 +16,7 @@ import {
} from 'chart.js';
import { Card, CardContent } from '../ui/Card';
import { useTranslation } from 'react-i18next';
+import { useTenantCurrency } from '../../hooks/useTenantCurrency';
// Register Chart.js components
ChartJS.register(
@@ -42,6 +43,7 @@ interface PerformanceChartProps {
export const PerformanceChart: React.FC = ({ data, metric, period }) => {
const { t } = useTranslation('dashboard');
+ const { currencySymbol } = useTenantCurrency();
// Prepare chart data
const chartData = {
@@ -76,7 +78,7 @@ export const PerformanceChart: React.FC = ({ data, metric
}
if (context.parsed.y !== null) {
if (metric === 'sales') {
- label += `€${context.parsed.y.toFixed(2)}`;
+ label += `${currencySymbol}${context.parsed.y.toFixed(2)}`;
} else {
label += context.parsed.y;
}
@@ -142,7 +144,7 @@ export const PerformanceChart: React.FC = ({ data, metric
{item.rank}
{item.anonymized_name}
- {metric === 'sales' ? `€${item.metric_value.toFixed(2)}` : item.metric_value}
+ {metric === 'sales' ? `${currencySymbol}${item.metric_value.toFixed(2)}` : item.metric_value}
))}
diff --git a/frontend/src/components/dashboard/blocks/PendingPurchasesBlock.tsx b/frontend/src/components/dashboard/blocks/PendingPurchasesBlock.tsx
index e792565b..d036f444 100644
--- a/frontend/src/components/dashboard/blocks/PendingPurchasesBlock.tsx
+++ b/frontend/src/components/dashboard/blocks/PendingPurchasesBlock.tsx
@@ -19,6 +19,7 @@ import {
ShoppingCart,
X,
} from 'lucide-react';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
interface PendingPurchasesBlockProps {
pendingPOs: any[];
@@ -36,6 +37,7 @@ export function PendingPurchasesBlock({
loading,
}: PendingPurchasesBlockProps) {
const { t } = useTranslation(['dashboard', 'common']);
+ const { currencySymbol } = useTenantCurrency();
const [expandedReasoningId, setExpandedReasoningId] = useState(null);
const [processingId, setProcessingId] = useState(null);
@@ -288,7 +290,7 @@ export function PendingPurchasesBlock({
- €{(po.total_amount || po.total || 0).toLocaleString(undefined, {
+ {currencySymbol}{(po.total_amount || po.total || 0).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
diff --git a/frontend/src/components/dashboard/blocks/SystemStatusBlock.tsx b/frontend/src/components/dashboard/blocks/SystemStatusBlock.tsx
index dea95ae5..86577591 100644
--- a/frontend/src/components/dashboard/blocks/SystemStatusBlock.tsx
+++ b/frontend/src/components/dashboard/blocks/SystemStatusBlock.tsx
@@ -22,6 +22,7 @@ import {
TrendingUp,
} from 'lucide-react';
import type { ControlPanelData, OrchestrationSummary } from '../../../api/hooks/useControlPanelData';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
interface SystemStatusBlockProps {
data: ControlPanelData | undefined;
@@ -30,6 +31,7 @@ interface SystemStatusBlockProps {
export function SystemStatusBlock({ data, loading }: SystemStatusBlockProps) {
const { t } = useTranslation(['dashboard', 'common']);
+ const { currencySymbol } = useTenantCurrency();
const [isExpanded, setIsExpanded] = useState(false);
if (loading) {
@@ -234,7 +236,7 @@ export function SystemStatusBlock({ data, loading }: SystemStatusBlockProps) {
{t('dashboard:new_dashboard.system_status.estimated_savings')}
- €{orchestrationSummary.estimatedSavingsEur.toLocaleString()}
+ {currencySymbol}{orchestrationSummary.estimatedSavingsEur.toLocaleString()}
)}
@@ -266,7 +268,7 @@ export function SystemStatusBlock({ data, loading }: SystemStatusBlockProps) {
{issue.business_impact?.financial_impact_eur && (
- {t('dashboard:new_dashboard.system_status.saved')}: €{issue.business_impact.financial_impact_eur.toLocaleString()}
+ {t('dashboard:new_dashboard.system_status.saved')}: {currencySymbol}{issue.business_impact.financial_impact_eur.toLocaleString()}
)}
diff --git a/frontend/src/components/domain/analytics/AnalyticsDashboard.tsx b/frontend/src/components/domain/analytics/AnalyticsDashboard.tsx
index e7f55123..f3e3eb7b 100644
--- a/frontend/src/components/domain/analytics/AnalyticsDashboard.tsx
+++ b/frontend/src/components/domain/analytics/AnalyticsDashboard.tsx
@@ -4,10 +4,11 @@ import { ChartWidget } from './ChartWidget';
import { ReportsTable } from './ReportsTable';
import { FilterPanel } from './FilterPanel';
import { ExportOptions } from './ExportOptions';
-import type {
- BakeryMetrics,
- AnalyticsReport,
- ChartWidget as ChartWidgetType,
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
+import type {
+ BakeryMetrics,
+ AnalyticsReport,
+ ChartWidget as ChartWidgetType,
FilterPanel as FilterPanelType,
AppliedFilter,
TimeRange,
@@ -45,6 +46,7 @@ export const AnalyticsDashboard: React.FC = ({
onMetricsLoad,
onExport,
}) => {
+ const { currencySymbol } = useTenantCurrency();
const [selectedTimeRange, setSelectedTimeRange] = useState(initialTimeRange);
const [customDateRange, setCustomDateRange] = useState<{ from: Date; to: Date } | null>(null);
const [bakeryMetrics, setBakeryMetrics] = useState(null);
@@ -319,7 +321,7 @@ export const AnalyticsDashboard: React.FC = ({
{renderKPICard(
'Ingresos Totales',
- `€${bakeryMetrics.sales.total_revenue.toLocaleString()}`,
+ `${currencySymbol}${bakeryMetrics.sales.total_revenue.toLocaleString()}`,
undefined,
bakeryMetrics.sales.revenue_growth,
'💰',
@@ -328,7 +330,7 @@ export const AnalyticsDashboard: React.FC
= ({
{renderKPICard(
'Pedidos',
bakeryMetrics.sales.total_orders.toLocaleString(),
- `Ticket medio: €${bakeryMetrics.sales.average_order_value.toFixed(2)}`,
+ `Ticket medio: ${currencySymbol}${bakeryMetrics.sales.average_order_value.toFixed(2)}`,
bakeryMetrics.sales.order_growth,
'📦',
'text-[var(--color-info)]'
@@ -336,7 +338,7 @@ export const AnalyticsDashboard: React.FC = ({
{renderKPICard(
'Margen de Beneficio',
`${bakeryMetrics.financial.profit_margin.toFixed(1)}%`,
- `Beneficio: €${bakeryMetrics.financial.net_profit.toLocaleString()}`,
+ `Beneficio: ${currencySymbol}${bakeryMetrics.financial.net_profit.toLocaleString()}`,
undefined,
'📈',
'text-purple-600'
@@ -366,7 +368,7 @@ export const AnalyticsDashboard: React.FC = ({
- €{channel.revenue.toLocaleString()}
+ {currencySymbol}{channel.revenue.toLocaleString()}
Conv. {channel.conversion_rate.toFixed(1)}%
@@ -390,7 +392,7 @@ export const AnalyticsDashboard: React.FC = ({
- €{product.revenue.toLocaleString()}
+ {currencySymbol}{product.revenue.toLocaleString()}
Margen {product.profit_margin.toFixed(1)}%
@@ -444,7 +446,7 @@ export const AnalyticsDashboard: React.FC = ({
- €{bakeryMetrics.customer.customer_lifetime_value.toFixed(0)}
+ {currencySymbol}{bakeryMetrics.customer.customer_lifetime_value.toFixed(0)}
Valor Cliente
diff --git a/frontend/src/components/domain/auth/LoginForm.tsx b/frontend/src/components/domain/auth/LoginForm.tsx
index 9f519f96..bd77ee53 100644
--- a/frontend/src/components/domain/auth/LoginForm.tsx
+++ b/frontend/src/components/domain/auth/LoginForm.tsx
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Button, Input, Card } from '../../ui';
import { useAuthActions, useAuthLoading, useAuthError } from '../../../stores/auth.store';
import { showToast } from '../../../utils/toast';
+import { validateEmail } from '../../../utils/validation';
interface LoginFormProps {
onSuccess?: () => void;
@@ -50,10 +51,9 @@ export const LoginForm: React.FC = ({
const validateForm = (): boolean => {
const newErrors: Partial = {};
- if (!credentials.email.trim()) {
- newErrors.email = t('auth:validation.email_required', 'El email es requerido');
- } else if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(credentials.email)) {
- newErrors.email = t('auth:validation.email_invalid', 'Por favor, ingrese un email válido');
+ const emailValidation = validateEmail(credentials.email);
+ if (!emailValidation.isValid) {
+ newErrors.email = t('auth:validation.email_invalid', emailValidation.error || 'Por favor, ingrese un email válido');
}
if (!credentials.password) {
diff --git a/frontend/src/components/domain/auth/RegisterForm.tsx b/frontend/src/components/domain/auth/RegisterForm.tsx
index e4692251..0e68292f 100644
--- a/frontend/src/components/domain/auth/RegisterForm.tsx
+++ b/frontend/src/components/domain/auth/RegisterForm.tsx
@@ -12,6 +12,7 @@ import { Elements } from '@stripe/react-stripe-js';
import { CheckCircle, Clock } from 'lucide-react';
import { usePilotDetection } from '../../../hooks/usePilotDetection';
import { subscriptionService } from '../../../api';
+import { validateEmail } from '../../../utils/validation';
// Helper to get Stripe key from runtime config or build-time env
const getStripeKey = (): string => {
@@ -94,6 +95,15 @@ export const RegisterForm: React.FC = ({
const passwordMatchStatus = getPasswordMatchStatus();
+ // Helper function to determine email validation status (real-time)
+ const getEmailValidationStatus = () => {
+ if (!formData.email) return 'empty';
+ const result = validateEmail(formData.email);
+ return result.isValid ? 'valid' : 'invalid';
+ };
+
+ const emailValidationStatus = getEmailValidationStatus();
+
// Load plan metadata when plan changes
useEffect(() => {
const loadPlanMetadata = async () => {
@@ -132,10 +142,9 @@ export const RegisterForm: React.FC = ({
newErrors.full_name = t('auth:validation.field_required', 'El nombre debe tener al menos 2 caracteres');
}
- if (!formData.email.trim()) {
- newErrors.email = t('auth:validation.email_required', 'El email es requerido');
- } else if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(formData.email)) {
- newErrors.email = t('auth:validation.email_invalid', 'Por favor, ingrese un email válido');
+ const emailValidation = validateEmail(formData.email);
+ if (!emailValidation.isValid) {
+ newErrors.email = t('auth:validation.email_invalid', emailValidation.error || 'Por favor, ingrese un email válido');
}
if (!formData.password) {
@@ -344,22 +353,66 @@ export const RegisterForm: React.FC = ({
}
/>
-
-
-
- }
- />
+
+
+
+
+ ) : emailValidationStatus === 'invalid' && formData.email ? (
+
+
+
+ ) : (
+
+
+
+ )
+ }
+ />
+
+ {/* Email Validation Status Message */}
+ {formData.email && (
+
+ {emailValidationStatus === 'valid' ? (
+
+
+
{t('auth:validation.email_valid', 'Email válido')}
+
+ ) : (
+
+
+
{t('auth:validation.email_invalid', 'Por favor, ingrese un email válido')}
+
+ )}
+
+ )}
+
= ({
{t('auth:payment.trial_period', 'Período de prueba:')}
- {isPilot ? t('auth:payment.free_months', {count: trialMonths}) : t('auth:payment.free_days', '14 días gratis')}
+ {isPilot ? t('auth:payment.free_months', {count: trialMonths}) : t('auth:payment.free_days')}
)}
@@ -642,7 +695,7 @@ export const RegisterForm: React.FC = ({
{useTrial
? t('auth:payment.billing_message', {price: subscriptionService.formatPrice(selectedPlanMetadata.monthly_price)})
- : t('auth:payment.payment_required', 'Tarjeta requerida para validación')
+ : t('auth:payment.payment_required')
}
diff --git a/frontend/src/components/domain/dashboard/AutoActionCountdownComponent.tsx b/frontend/src/components/domain/dashboard/AutoActionCountdownComponent.tsx
index df65540c..50e2c8da 100644
--- a/frontend/src/components/domain/dashboard/AutoActionCountdownComponent.tsx
+++ b/frontend/src/components/domain/dashboard/AutoActionCountdownComponent.tsx
@@ -19,6 +19,7 @@ import { AlertTriangle, Clock, XCircle, CheckCircle } from 'lucide-react';
import { Button } from '../../ui/Button';
import { Badge } from '../../ui/Badge';
import { useTranslation } from 'react-i18next';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
export interface AutoActionCountdownProps {
actionDescription: string;
@@ -38,6 +39,7 @@ export function AutoActionCountdownComponent({
className = '',
}: AutoActionCountdownProps) {
const { t } = useTranslation('alerts');
+ const { currencySymbol } = useTenantCurrency();
const [timeRemaining, setTimeRemaining] = useState(countdownSeconds);
const [isCancelling, setIsCancelling] = useState(false);
const [isCancelled, setIsCancelled] = useState(false);
@@ -249,7 +251,7 @@ export function AutoActionCountdownComponent({
{t('auto_action.financial_impact', 'Impact:')}
{' '}
- €{financialImpactEur.toFixed(2)}
+ {currencySymbol}{financialImpactEur.toFixed(2)}
)}
diff --git a/frontend/src/components/domain/dashboard/PendingPOApprovals.tsx b/frontend/src/components/domain/dashboard/PendingPOApprovals.tsx
index 9fdc84e7..c0d4a569 100644
--- a/frontend/src/components/domain/dashboard/PendingPOApprovals.tsx
+++ b/frontend/src/components/domain/dashboard/PendingPOApprovals.tsx
@@ -6,6 +6,7 @@ import { Badge } from '../../ui/Badge';
import { Button } from '../../ui/Button';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { usePendingApprovalPurchaseOrders, useApprovePurchaseOrder, useRejectPurchaseOrder } from '../../../api/hooks/purchase-orders';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
import {
ShoppingCart,
Clock,
@@ -40,6 +41,7 @@ const PendingPOApprovals: React.FC = ({
const { t } = useTranslation(['dashboard']);
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
+ const { currencySymbol } = useTenantCurrency();
const [approvingPO, setApprovingPO] = useState(null);
const [rejectingPO, setRejectingPO] = useState(null);
@@ -145,10 +147,7 @@ const PendingPOApprovals: React.FC = ({
const formatCurrency = (amount: string, currency: string = 'EUR') => {
const value = parseFloat(amount);
- if (currency === 'EUR') {
- return `€${value.toFixed(2)}`;
- }
- return `${value.toFixed(2)} ${currency}`;
+ return `${currencySymbol}${value.toFixed(2)}`;
};
const formatDate = (dateStr: string) => {
diff --git a/frontend/src/components/domain/equipment/MaintenanceHistoryModal.tsx b/frontend/src/components/domain/equipment/MaintenanceHistoryModal.tsx
index 98701ee8..65a19994 100644
--- a/frontend/src/components/domain/equipment/MaintenanceHistoryModal.tsx
+++ b/frontend/src/components/domain/equipment/MaintenanceHistoryModal.tsx
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import { EditViewModal } from '../../ui/EditViewModal/EditViewModal';
import { Equipment, MaintenanceHistory } from '../../../api/types/equipment';
import { statusColors } from '../../../styles/colors';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
interface MaintenanceHistoryModalProps {
isOpen: boolean;
@@ -23,6 +24,7 @@ export const MaintenanceHistoryModal: React.FC = (
loading = false
}) => {
const { t } = useTranslation(['equipment', 'common']);
+ const { currencySymbol } = useTenantCurrency();
// Get maintenance type display info with colors and icons
const getMaintenanceTypeInfo = (type: MaintenanceHistory['type']) => {
@@ -127,7 +129,7 @@ export const MaintenanceHistoryModal: React.FC = (
{t('common:actions.cost', 'Coste')}:
- €{record.cost.toFixed(2)}
+ {currencySymbol}{record.cost.toFixed(2)}
{t('fields.downtime', 'Parada')}:
diff --git a/frontend/src/components/domain/inventory/BatchAddIngredientsModal.tsx b/frontend/src/components/domain/inventory/BatchAddIngredientsModal.tsx
index b8be6c46..82c89fdc 100644
--- a/frontend/src/components/domain/inventory/BatchAddIngredientsModal.tsx
+++ b/frontend/src/components/domain/inventory/BatchAddIngredientsModal.tsx
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { useCreateIngredient } from '../../../api/hooks/inventory';
import type { Ingredient } from '../../../api/types/inventory';
import { commonIngredientTemplates } from './ingredientHelpers';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
interface BatchIngredientRow {
id: string;
@@ -29,6 +30,7 @@ export const BatchAddIngredientsModal: React.FC
=
}) => {
const { t } = useTranslation();
const createIngredient = useCreateIngredient();
+ const { currencySymbol } = useTenantCurrency();
const [rows, setRows] = useState([
{ id: '1', name: '', category: 'Baking Ingredients', unit_of_measure: 'kg' },
@@ -269,7 +271,7 @@ export const BatchAddIngredientsModal: React.FC =
Categoría *
Unidad *
Stock Inicial
- Costo (€)
+ Costo ({currencySymbol})
diff --git a/frontend/src/components/domain/inventory/QuickAddIngredientModal.tsx b/frontend/src/components/domain/inventory/QuickAddIngredientModal.tsx
index 91a0aa55..52c52c02 100644
--- a/frontend/src/components/domain/inventory/QuickAddIngredientModal.tsx
+++ b/frontend/src/components/domain/inventory/QuickAddIngredientModal.tsx
@@ -8,6 +8,7 @@ import {
commonIngredientTemplates,
type IngredientTemplate
} from './ingredientHelpers';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
interface QuickAddIngredientModalProps {
isOpen: boolean;
@@ -26,6 +27,7 @@ export const QuickAddIngredientModal: React.FC = (
}) => {
const { t } = useTranslation();
const createIngredient = useCreateIngredient();
+ const { currencySymbol } = useTenantCurrency();
// Fetch existing ingredients for duplicate detection
const { data: existingIngredients = [] } = useIngredients(tenantId, {}, {
@@ -478,7 +480,7 @@ export const QuickAddIngredientModal: React.FC = (
- Costo por Unidad (€)
+ Costo por Unidad ({currencySymbol})
= ({
isFirstStep
}) => {
const { t } = useTranslation();
+ const { currencySymbol } = useTenantCurrency();
const [selectedFile, setSelectedFile] = useState
(null);
const [isValidating, setIsValidating] = useState(false);
const [validationResult, setValidationResult] = useState(null);
@@ -658,7 +660,7 @@ export const UploadSalesDataStep: React.FC = ({
Stock: {item.stock_quantity} {item.unit_of_measure}
- Costo: €{item.cost_per_unit.toFixed(2)}/{item.unit_of_measure}
+ Costo: {currencySymbol}{item.cost_per_unit.toFixed(2)}/{item.unit_of_measure}
Caducidad: {item.estimated_shelf_life_days} días
{item.sales_data && (
@@ -962,7 +964,7 @@ export const UploadSalesDataStep: React.FC = ({
- Costo por Unidad (€)
+ Costo por Unidad ({currencySymbol})
= ({
- Costo por Unidad (€)
+ Costo por Unidad ({currencySymbol})
= ({
const user = useAuthUser();
const tenantId = currentTenant?.id || user?.tenant_id || '';
const { t } = useTranslation(['orders', 'common']);
+ const { currencySymbol } = useTenantCurrency();
// Create enum options using direct i18n
const orderTypeOptions = Object.values(OrderType).map(value => ({
@@ -327,7 +329,7 @@ export const OrderFormModal: React.FC
= ({
Seleccionar producto...
{finishedProducts.map(product => (
- {product.name} - €{(product.average_cost || product.standard_cost || 0).toFixed(2)}
+ {product.name} - {currencySymbol}{(product.average_cost || product.standard_cost || 0).toFixed(2)}
))}
@@ -362,7 +364,7 @@ export const OrderFormModal: React.FC = ({
{item.product_name}
- €{item.unit_price.toFixed(2)} × {item.quantity} = €{(item.unit_price * item.quantity).toFixed(2)}
+ {currencySymbol}{item.unit_price.toFixed(2)} × {item.quantity} = {currencySymbol}{(item.unit_price * item.quantity).toFixed(2)}
diff --git a/frontend/src/components/domain/pos/POSCart.tsx b/frontend/src/components/domain/pos/POSCart.tsx
index de52c635..19ab12d8 100644
--- a/frontend/src/components/domain/pos/POSCart.tsx
+++ b/frontend/src/components/domain/pos/POSCart.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { ShoppingCart, Plus, Minus, Trash2, X } from 'lucide-react';
import { Card } from '../../ui/Card';
import { Button } from '../../ui/Button';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
interface CartItem {
id: string;
@@ -29,6 +30,8 @@ export const POSCart: React.FC
= ({
onClearCart,
taxRate = 0.21, // 21% IVA by default
}) => {
+ const { currencySymbol } = useTenantCurrency();
+
// Calculate totals
const subtotal = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
const tax = subtotal * taxRate;
@@ -81,7 +84,7 @@ export const POSCart: React.FC = ({
- €{item.price.toFixed(2)}
+ {currencySymbol}{item.price.toFixed(2)}
c/u
@@ -128,7 +131,7 @@ export const POSCart: React.FC = ({
{/* Item Subtotal */}
- €{(item.price * item.quantity).toFixed(2)}
+ {currencySymbol}{(item.price * item.quantity).toFixed(2)}
@@ -145,7 +148,7 @@ export const POSCart: React.FC = ({
Subtotal:
- €{subtotal.toFixed(2)}
+ {currencySymbol}{subtotal.toFixed(2)}
@@ -153,7 +156,7 @@ export const POSCart: React.FC = ({
IVA ({(taxRate * 100).toFixed(0)}%):
- €{tax.toFixed(2)}
+ {currencySymbol}{tax.toFixed(2)}
@@ -163,7 +166,7 @@ export const POSCart: React.FC = ({
TOTAL:
- €{total.toFixed(2)}
+ {currencySymbol}{total.toFixed(2)}
diff --git a/frontend/src/components/domain/pos/POSPayment.tsx b/frontend/src/components/domain/pos/POSPayment.tsx
index 5ccfeac0..0f9d072c 100644
--- a/frontend/src/components/domain/pos/POSPayment.tsx
+++ b/frontend/src/components/domain/pos/POSPayment.tsx
@@ -3,6 +3,7 @@ import { CreditCard, Banknote, ArrowRightLeft, Receipt, User } from 'lucide-reac
import { Card } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
interface CustomerInfo {
name: string;
@@ -30,6 +31,7 @@ export const POSPayment: React.FC
= ({
onProcessPayment,
disabled = false,
}) => {
+ const { currencySymbol } = useTenantCurrency();
const [paymentMethod, setPaymentMethod] = useState<'cash' | 'card' | 'transfer'>('cash');
const [cashReceived, setCashReceived] = useState('');
const [customerInfo, setCustomerInfo] = useState({
@@ -193,7 +195,7 @@ export const POSPayment: React.FC = ({
setCashReceived(e.target.value)}
className="text-lg font-semibold"
@@ -214,7 +216,7 @@ export const POSPayment: React.FC = ({
Cambio:
- €{change.toFixed(2)}
+ {currencySymbol}{change.toFixed(2)}
@@ -230,7 +232,7 @@ export const POSPayment: React.FC = ({
}}
>
- Efectivo insuficiente: falta €{(total - parseFloat(cashReceived)).toFixed(2)}
+ Efectivo insuficiente: falta {currencySymbol}{(total - parseFloat(cashReceived)).toFixed(2)}
)}
@@ -247,7 +249,7 @@ export const POSPayment: React.FC = ({
className="w-full text-lg font-bold py-6 shadow-lg hover:shadow-xl transition-all"
>
- Procesar Venta - €{total.toFixed(2)}
+ Procesar Venta - {currencySymbol}{total.toFixed(2)}
);
diff --git a/frontend/src/components/domain/pos/POSProductCard.tsx b/frontend/src/components/domain/pos/POSProductCard.tsx
index 6a09fa83..526a8839 100644
--- a/frontend/src/components/domain/pos/POSProductCard.tsx
+++ b/frontend/src/components/domain/pos/POSProductCard.tsx
@@ -3,6 +3,7 @@ import { Plus, Package } from 'lucide-react';
import { Card } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Badge } from '../../ui/Badge';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
interface POSProductCardProps {
id: string;
@@ -28,6 +29,7 @@ export const POSProductCard: React.FC = ({
onAddToCart,
onClick,
}) => {
+ const { currencySymbol } = useTenantCurrency();
const remainingStock = stock - cartQuantity;
const isOutOfStock = remainingStock <= 0;
const isLowStock = remainingStock > 0 && remainingStock <= 5;
@@ -97,7 +99,7 @@ export const POSProductCard: React.FC = ({
{/* Price - Large and prominent */}
- €{price.toFixed(2)}
+ {currencySymbol}{price.toFixed(2)}
c/u
diff --git a/frontend/src/components/domain/procurement/CreatePurchaseOrderModal.tsx b/frontend/src/components/domain/procurement/CreatePurchaseOrderModal.tsx
index 14123846..ef2a059d 100644
--- a/frontend/src/components/domain/procurement/CreatePurchaseOrderModal.tsx
+++ b/frontend/src/components/domain/procurement/CreatePurchaseOrderModal.tsx
@@ -6,6 +6,7 @@ import { useCreatePurchaseOrder } from '../../../api/hooks/purchase-orders';
import { useIngredients } from '../../../api/hooks/inventory';
import { useTenantStore } from '../../../stores/tenant.store';
import { suppliersService } from '../../../api/services/suppliers';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
import type { ProcurementRequirementResponse } from '../../../api/types/orders';
import type { PurchaseOrderItemCreate } from '../../../api/services/purchase_orders';
import type { SupplierSummary } from '../../../api/types/suppliers';
@@ -31,6 +32,7 @@ export const CreatePurchaseOrderModal: React.FC =
requirements,
onSuccess
}) => {
+ const { currencySymbol } = useTenantCurrency();
const [loading, setLoading] = useState(false);
const [selectedSupplier, setSelectedSupplier] = useState('');
const [formData, setFormData] = useState>({});
@@ -317,7 +319,7 @@ export const CreatePurchaseOrderModal: React.FC =
},
{
name: 'unit_price',
- label: 'Precio Est. (€)',
+ label: `Precio Est. (${currencySymbol})`,
type: 'currency',
required: true
}
@@ -362,7 +364,7 @@ export const CreatePurchaseOrderModal: React.FC =
},
{
name: 'unit_price',
- label: 'Precio Unitario (€)',
+ label: `Precio Unitario (${currencySymbol})`,
type: 'currency',
required: true,
defaultValue: 0,
diff --git a/frontend/src/components/domain/procurement/ModifyPurchaseOrderModal.tsx b/frontend/src/components/domain/procurement/ModifyPurchaseOrderModal.tsx
index a811412d..d73f5204 100644
--- a/frontend/src/components/domain/procurement/ModifyPurchaseOrderModal.tsx
+++ b/frontend/src/components/domain/procurement/ModifyPurchaseOrderModal.tsx
@@ -3,6 +3,7 @@ import { Edit, Package, Calendar, Building2 } from 'lucide-react';
import { AddModal } from '../../ui/AddModal/AddModal';
import { useUpdatePurchaseOrder, usePurchaseOrder } from '../../../api/hooks/purchase-orders';
import { useTenantStore } from '../../../stores/tenant.store';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
import type { PurchaseOrderItem } from '../../../api/types/orders';
import { statusColors } from '../../../styles/colors';
@@ -23,6 +24,7 @@ export const ModifyPurchaseOrderModal: React.FC =
poId,
onSuccess
}) => {
+ const { currencySymbol } = useTenantCurrency();
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState>({});
@@ -228,7 +230,7 @@ export const ModifyPurchaseOrderModal: React.FC =
},
{
name: 'unit_price',
- label: 'Precio Unitario (€)',
+ label: `Precio Unitario (${currencySymbol})`,
type: 'currency',
required: true,
placeholder: '0.00',
diff --git a/frontend/src/components/domain/procurement/UnifiedPurchaseOrderModal.tsx b/frontend/src/components/domain/procurement/UnifiedPurchaseOrderModal.tsx
index cd9f4358..4a9d2441 100644
--- a/frontend/src/components/domain/procurement/UnifiedPurchaseOrderModal.tsx
+++ b/frontend/src/components/domain/procurement/UnifiedPurchaseOrderModal.tsx
@@ -24,6 +24,7 @@ import { usePurchaseOrder, useUpdatePurchaseOrder } from '../../../api/hooks/pur
import { useUserById } from '../../../api/hooks/user';
import { EditViewModal, EditViewModalSection } from '../../ui/EditViewModal/EditViewModal';
import { Button } from '../../ui/Button';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
import type { PurchaseOrderItem } from '../../../api/services/purchase_orders';
interface UnifiedPurchaseOrderModalProps {
@@ -48,6 +49,7 @@ export const UnifiedPurchaseOrderModal: React.FC
showApprovalActions = false
}) => {
const { t, i18n } = useTranslation(['purchase_orders', 'common']);
+ const { currencySymbol } = useTenantCurrency();
const { data: po, isLoading, refetch } = usePurchaseOrder(tenantId, poId);
const [mode, setMode] = useState<'view' | 'edit'>(initialMode);
const [showApprovalModal, setShowApprovalModal] = useState(false);
@@ -165,7 +167,7 @@ export const UnifiedPurchaseOrderModal: React.FC
- €{itemTotal.toFixed(2)}
+ {currencySymbol}{itemTotal.toFixed(2)}
@@ -178,7 +180,7 @@ export const UnifiedPurchaseOrderModal: React.FC
{t('unit_price')}
-
€{unitPrice.toFixed(2)}
+
{currencySymbol}{unitPrice.toFixed(2)}
{item.quality_requirements && (
@@ -198,7 +200,7 @@ export const UnifiedPurchaseOrderModal: React.FC
})}
{t('total')}
- €{totalAmount.toFixed(2)}
+ {currencySymbol}{totalAmount.toFixed(2)}
);
@@ -296,22 +298,22 @@ export const UnifiedPurchaseOrderModal: React.FC
fields: [
...(po.subtotal !== undefined ? [{
label: t('subtotal'),
- value: `€${formatCurrency(po.subtotal)}`,
+ value: `${currencySymbol}${formatCurrency(po.subtotal)}`,
type: 'text' as const
}] : []),
...(po.tax_amount !== undefined ? [{
label: t('tax'),
- value: `€${formatCurrency(po.tax_amount)}`,
+ value: `${currencySymbol}${formatCurrency(po.tax_amount)}`,
type: 'text' as const
}] : []),
...(po.discount_amount !== undefined ? [{
label: t('discount'),
- value: `€${formatCurrency(po.discount_amount)}`,
+ value: `${currencySymbol}${formatCurrency(po.discount_amount)}`,
type: 'text' as const
}] : []),
{
label: t('total_amount'),
- value: `€${formatCurrency(po.total_amount)}`,
+ value: `${currencySymbol}${formatCurrency(po.total_amount)}`,
type: 'text' as const,
highlight: true
}
@@ -505,7 +507,7 @@ export const UnifiedPurchaseOrderModal: React.FC
- €{itemTotal.toFixed(2)}
+ {currencySymbol}{itemTotal.toFixed(2)}
@@ -555,7 +557,7 @@ export const UnifiedPurchaseOrderModal: React.FC
})}
{t('total')}
- €{totalAmount.toFixed(2)}
+ {currencySymbol}{totalAmount.toFixed(2)}
);
diff --git a/frontend/src/components/domain/production/analytics/widgets/AIInsightsWidget.tsx b/frontend/src/components/domain/production/analytics/widgets/AIInsightsWidget.tsx
index 72f902ef..521e8399 100644
--- a/frontend/src/components/domain/production/analytics/widgets/AIInsightsWidget.tsx
+++ b/frontend/src/components/domain/production/analytics/widgets/AIInsightsWidget.tsx
@@ -4,6 +4,7 @@ import { Brain, TrendingUp, AlertTriangle, Target, Zap, DollarSign, Clock } from
import { AnalyticsWidget } from '../AnalyticsWidget';
import { Badge, Button } from '../../../../ui';
import { useCurrentTenant } from '../../../../../stores/tenant.store';
+import { useTenantCurrency } from '../../../../../hooks/useTenantCurrency';
interface AIInsight {
id: string;
@@ -27,6 +28,7 @@ interface AIInsight {
export const AIInsightsWidget: React.FC = () => {
const { t } = useTranslation('production');
const currentTenant = useCurrentTenant();
+ const { currencySymbol } = useTenantCurrency();
// Mock AI insights data - replace with real AI API call
const aiInsights: AIInsight[] = [
@@ -172,7 +174,7 @@ export const AIInsightsWidget: React.FC = () => {
const formatImpactValue = (impact: AIInsight['impact']) => {
switch (impact.unit) {
- case 'euros': return `€${impact.value}`;
+ case 'euros': return `${currencySymbol}${impact.value}`;
case 'percentage': return `${impact.value}%`;
case 'hours': return `${impact.value}h`;
case 'units': return `${impact.value} unidades`;
@@ -222,9 +224,9 @@ export const AIInsightsWidget: React.FC = () => {
- €
+ {currencySymbol}
-
€{totalPotentialSavings}
+
{currencySymbol}{totalPotentialSavings}
{t('ai.stats.potential_savings')}
@@ -371,7 +373,7 @@ export const AIInsightsWidget: React.FC = () => {
{implementedInsights.length} {t('ai.performance.insights_implemented')}
- {totalPotentialSavings > 0 && `, €${totalPotentialSavings} ${t('ai.performance.in_savings_identified')}`}
+ {totalPotentialSavings > 0 && `, ${currencySymbol}${totalPotentialSavings} ${t('ai.performance.in_savings_identified')}`}
diff --git a/frontend/src/components/domain/production/analytics/widgets/CostPerUnitWidget.tsx b/frontend/src/components/domain/production/analytics/widgets/CostPerUnitWidget.tsx
index 0a3c53f6..ef379f8c 100644
--- a/frontend/src/components/domain/production/analytics/widgets/CostPerUnitWidget.tsx
+++ b/frontend/src/components/domain/production/analytics/widgets/CostPerUnitWidget.tsx
@@ -6,6 +6,7 @@ import { AnalyticsChart, ChartSeries } from '../AnalyticsChart';
import { Button } from '../../../../ui';
import { useActiveBatches } from '../../../../../api/hooks/production';
import { useCurrentTenant } from '../../../../../stores/tenant.store';
+import { useTenantCurrency } from '../../../../../hooks/useTenantCurrency';
interface ProductCostData {
product: string;
@@ -21,6 +22,7 @@ export const CostPerUnitWidget: React.FC = () => {
const { t } = useTranslation('production');
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
+ const { currencySymbol } = useTenantCurrency();
const { data: batchesData, isLoading, error } = useActiveBatches(tenantId);
const batches = batchesData?.batches || [];
@@ -162,7 +164,7 @@ export const CostPerUnitWidget: React.FC = () => {
- €{averageCostPerUnit.toFixed(2)}
+ {currencySymbol}{averageCostPerUnit.toFixed(2)}
{t('cost.average_cost_per_unit')}
@@ -171,7 +173,7 @@ export const CostPerUnitWidget: React.FC = () => {
- €{totalCosts.toFixed(0)}
+ {currencySymbol}{totalCosts.toFixed(0)}
{t('cost.total_production_cost')}
@@ -221,7 +223,7 @@ export const CostPerUnitWidget: React.FC = () => {
- €{item.costPerUnit.toFixed(2)}
+ {currencySymbol}{item.costPerUnit.toFixed(2)}
@@ -229,13 +231,13 @@ export const CostPerUnitWidget: React.FC = () => {
{t('cost.estimated')}
- €{item.estimatedCost.toFixed(2)}
+ {currencySymbol}{item.estimatedCost.toFixed(2)}
{t('cost.actual')}
- €{item.actualCost.toFixed(2)}
+ {currencySymbol}{item.actualCost.toFixed(2)}
diff --git a/frontend/src/components/domain/production/analytics/widgets/MaintenanceScheduleWidget.tsx b/frontend/src/components/domain/production/analytics/widgets/MaintenanceScheduleWidget.tsx
index 54f696d7..daadacc5 100644
--- a/frontend/src/components/domain/production/analytics/widgets/MaintenanceScheduleWidget.tsx
+++ b/frontend/src/components/domain/production/analytics/widgets/MaintenanceScheduleWidget.tsx
@@ -4,6 +4,7 @@ import { Calendar, Clock, Wrench, AlertCircle, CheckCircle2 } from 'lucide-react
import { AnalyticsWidget } from '../AnalyticsWidget';
import { Badge, Button } from '../../../../ui';
import { useCurrentTenant } from '../../../../../stores/tenant.store';
+import { useTenantCurrency } from '../../../../../hooks/useTenantCurrency';
interface MaintenanceTask {
id: string;
@@ -24,6 +25,7 @@ interface MaintenanceTask {
export const MaintenanceScheduleWidget: React.FC = () => {
const { t } = useTranslation('production');
const currentTenant = useCurrentTenant();
+ const { currencySymbol } = useTenantCurrency();
// Mock maintenance data - replace with real API call
const maintenanceTasks: MaintenanceTask[] = [
@@ -185,9 +187,9 @@ export const MaintenanceScheduleWidget: React.FC = () => {
- €
+ {currencySymbol}
-
€{totalCost}
+
{currencySymbol}{totalCost}
{t('equipment.maintenance.total_cost')}
@@ -234,7 +236,7 @@ export const MaintenanceScheduleWidget: React.FC = () => {
{t('equipment.maintenance.scheduled')}: {formatDate(task.scheduledDate)}
{t('equipment.maintenance.duration')}: {task.estimatedDuration}h
- {task.cost && {t('equipment.maintenance.cost')}: €{task.cost} }
+ {task.cost && {t('equipment.maintenance.cost')}: {currencySymbol}{task.cost} }
{task.technician && {t('equipment.maintenance.technician')}: {task.technician} }
diff --git a/frontend/src/components/domain/production/analytics/widgets/PredictiveMaintenanceWidget.tsx b/frontend/src/components/domain/production/analytics/widgets/PredictiveMaintenanceWidget.tsx
index aff208b8..2f8de133 100644
--- a/frontend/src/components/domain/production/analytics/widgets/PredictiveMaintenanceWidget.tsx
+++ b/frontend/src/components/domain/production/analytics/widgets/PredictiveMaintenanceWidget.tsx
@@ -5,6 +5,7 @@ import { AnalyticsWidget } from '../AnalyticsWidget';
import { AnalyticsChart, ChartSeries } from '../AnalyticsChart';
import { Badge, Button } from '../../../../ui';
import { useCurrentTenant } from '../../../../../stores/tenant.store';
+import { useTenantCurrency } from '../../../../../hooks/useTenantCurrency';
interface PredictiveMaintenanceAlert {
id: string;
@@ -34,6 +35,7 @@ interface PredictiveMaintenanceAlert {
export const PredictiveMaintenanceWidget: React.FC = () => {
const { t } = useTranslation('production');
const currentTenant = useCurrentTenant();
+ const { currencySymbol } = useTenantCurrency();
// Mock predictive maintenance data - replace with real ML API call
const maintenanceAlerts: PredictiveMaintenanceAlert[] = [
@@ -239,9 +241,9 @@ export const PredictiveMaintenanceWidget: React.FC = () => {
- €
+ {currencySymbol}
-
€{totalEstimatedCost}
+
{currencySymbol}{totalEstimatedCost}
{t('ai.predictive_maintenance.estimated_cost')}
@@ -365,7 +367,7 @@ export const PredictiveMaintenanceWidget: React.FC = () => {
- {t('ai.predictive_maintenance.estimated_cost')}: €{alert.estimatedCost}
+ {t('ai.predictive_maintenance.estimated_cost')}: {currencySymbol}{alert.estimatedCost}
diff --git a/frontend/src/components/domain/production/analytics/widgets/TopDefectTypesWidget.tsx b/frontend/src/components/domain/production/analytics/widgets/TopDefectTypesWidget.tsx
index 8261e1f4..11048f48 100644
--- a/frontend/src/components/domain/production/analytics/widgets/TopDefectTypesWidget.tsx
+++ b/frontend/src/components/domain/production/analytics/widgets/TopDefectTypesWidget.tsx
@@ -6,6 +6,7 @@ import { AnalyticsChart, ChartSeries } from '../AnalyticsChart';
import { Button, Badge } from '../../../../ui';
import { useActiveBatches } from '../../../../../api/hooks/production';
import { useCurrentTenant } from '../../../../../stores/tenant.store';
+import { useTenantCurrency } from '../../../../../hooks/useTenantCurrency';
interface DefectType {
type: string;
@@ -20,6 +21,7 @@ export const TopDefectTypesWidget: React.FC = () => {
const { t } = useTranslation('production');
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
+ const { currencySymbol } = useTenantCurrency();
const { data: batchesData, isLoading, error } = useActiveBatches(tenantId);
const batches = batchesData?.batches || [];
@@ -193,7 +195,7 @@ export const TopDefectTypesWidget: React.FC = () => {
- €{totalDefectCost.toFixed(0)}
+ {currencySymbol}{totalDefectCost.toFixed(0)}
{t('quality.estimated_cost')}
@@ -229,7 +231,7 @@ export const TopDefectTypesWidget: React.FC = () => {
{defect.count} {t('quality.incidents')}
•
-
€{defect.estimatedCost.toFixed(2)} {t('quality.cost')}
+
{currencySymbol}{defect.estimatedCost.toFixed(2)} {t('quality.cost')}
{getTrendIcon(defect.trend)} {t(`quality.trend.${defect.trend}`)}
diff --git a/frontend/src/components/domain/production/analytics/widgets/WasteDefectTrackerWidget.tsx b/frontend/src/components/domain/production/analytics/widgets/WasteDefectTrackerWidget.tsx
index 309a1df1..1525a850 100644
--- a/frontend/src/components/domain/production/analytics/widgets/WasteDefectTrackerWidget.tsx
+++ b/frontend/src/components/domain/production/analytics/widgets/WasteDefectTrackerWidget.tsx
@@ -6,6 +6,7 @@ import { AnalyticsChart, ChartSeries } from '../AnalyticsChart';
import { Button, Badge } from '../../../../ui';
import { useActiveBatches } from '../../../../../api/hooks/production';
import { useCurrentTenant } from '../../../../../stores/tenant.store';
+import { useTenantCurrency } from '../../../../../hooks/useTenantCurrency';
interface WasteSource {
source: string;
@@ -19,6 +20,7 @@ export const WasteDefectTrackerWidget: React.FC = () => {
const { t } = useTranslation('production');
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
+ const { currencySymbol } = useTenantCurrency();
const { data: batchesData, isLoading, error } = useActiveBatches(tenantId);
const batches = batchesData?.batches || [];
@@ -202,7 +204,7 @@ export const WasteDefectTrackerWidget: React.FC = () => {
- €{totalWasteCost.toFixed(0)}
+ {currencySymbol}{totalWasteCost.toFixed(0)}
{t('cost.waste_cost')}
@@ -241,7 +243,7 @@ export const WasteDefectTrackerWidget: React.FC = () => {
{source.source}
- {source.count} {t('common.units')} • €{source.cost.toFixed(2)}
+ {source.count} {t('common.units')} • {currencySymbol}{source.cost.toFixed(2)}
diff --git a/frontend/src/components/domain/sales/CustomerInfo.tsx b/frontend/src/components/domain/sales/CustomerInfo.tsx
index 6071090a..ef6986d4 100644
--- a/frontend/src/components/domain/sales/CustomerInfo.tsx
+++ b/frontend/src/components/domain/sales/CustomerInfo.tsx
@@ -9,13 +9,14 @@ import {
Tooltip,
Modal
} from '../../ui';
-import {
+import {
SalesRecord,
SalesChannel,
PaymentMethod
} from '../../../types/sales.types';
import { salesService } from '../../../api/services/sales.service';
import { useSales } from '../../../hooks/api/useSales';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
// Customer interfaces
interface Customer {
@@ -221,6 +222,8 @@ export const CustomerInfo: React.FC = ({
allowEditing = true,
className = ''
}) => {
+ const { currencySymbol } = useTenantCurrency();
+
// State
const [customer, setCustomer] = useState(null);
const [customerStats, setCustomerStats] = useState(null);
@@ -531,7 +534,7 @@ export const CustomerInfo: React.FC = ({
Total Gastado
- €{customerStats.total_spent.toFixed(2)}
+ {currencySymbol}{customerStats.total_spent.toFixed(2)}
@@ -563,7 +566,7 @@ export const CustomerInfo: React.FC
= ({
Ticket Promedio
- €{customerStats.average_order_value.toFixed(2)}
+ {currencySymbol}{customerStats.average_order_value.toFixed(2)}
@@ -854,7 +857,7 @@ export const CustomerInfo: React.FC
= ({
-
€{order.total.toFixed(2)}
+
{currencySymbol}{order.total.toFixed(2)}
{order.items_count} artículos
diff --git a/frontend/src/components/domain/sales/OrderForm.tsx b/frontend/src/components/domain/sales/OrderForm.tsx
index 34a325e8..5a3e3927 100644
--- a/frontend/src/components/domain/sales/OrderForm.tsx
+++ b/frontend/src/components/domain/sales/OrderForm.tsx
@@ -13,6 +13,7 @@ import {
PaymentMethod
} from '../../../types/sales.types';
import { salesService } from '../../../api/services/sales.service';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
// Order form interfaces
interface Product {
@@ -274,6 +275,7 @@ export const OrderForm: React.FC = ({
showPricing = true,
className = ''
}) => {
+ const { currencySymbol } = useTenantCurrency();
// Form data state
const [orderData, setOrderData] = useState({
customer: initialCustomer,
@@ -687,7 +689,7 @@ export const OrderForm: React.FC = ({
{item.product_name}
- €{item.unit_price.toFixed(2)} × {item.quantity} = €{item.total_price.toFixed(2)}
+ {currencySymbol}{item.unit_price.toFixed(2)} × {item.quantity} = {currencySymbol}{item.total_price.toFixed(2)}
{item.special_instructions && (
@@ -844,10 +846,10 @@ export const OrderForm: React.FC = ({
- 💡 Envío gratuito en pedidos superiores a €25.
- Tu pedido: €{orderData.subtotal.toFixed(2)}
+ 💡 Envío gratuito en pedidos superiores a {currencySymbol}25.
+ Tu pedido: {currencySymbol}{orderData.subtotal.toFixed(2)}
{orderData.subtotal < 25 && (
- - Faltan €{(25 - orderData.subtotal).toFixed(2)} para envío gratuito
+ - Faltan {currencySymbol}{(25 - orderData.subtotal).toFixed(2)} para envío gratuito
)}
@@ -953,39 +955,39 @@ export const OrderForm: React.FC = ({
Subtotal
- €{orderData.subtotal.toFixed(2)}
+ {currencySymbol}{orderData.subtotal.toFixed(2)}
{orderData.discount_amount > 0 && (
Descuento{orderData.discount_code && ` (${orderData.discount_code})`}
- -€{orderData.discount_amount.toFixed(2)}
+ -{currencySymbol}{orderData.discount_amount.toFixed(2)}
)}
{orderData.delivery_fee > 0 && (
Gastos de envío
- €{orderData.delivery_fee.toFixed(2)}
+ {currencySymbol}{orderData.delivery_fee.toFixed(2)}
)}
{orderData.loyalty_points_to_use > 0 && (
Puntos utilizados ({orderData.loyalty_points_to_use})
- -€{(orderData.loyalty_points_to_use * 0.01).toFixed(2)}
+ -{currencySymbol}{(orderData.loyalty_points_to_use * 0.01).toFixed(2)}
)}
IVA ({(orderData.tax_rate * 100).toFixed(0)}%)
- €{orderData.tax_amount.toFixed(2)}
+ {currencySymbol}{orderData.tax_amount.toFixed(2)}
Total
- €{orderData.total_amount.toFixed(2)}
+ {currencySymbol}{orderData.total_amount.toFixed(2)}
@@ -1025,7 +1027,7 @@ export const OrderForm: React.FC = ({
- Ahorro: €{(orderData.loyalty_points_to_use * 0.01).toFixed(2)}
+ Ahorro: {currencySymbol}{(orderData.loyalty_points_to_use * 0.01).toFixed(2)}
)}
@@ -1135,7 +1137,7 @@ export const OrderForm: React.FC = ({
{product.category}
- €{product.price.toFixed(2)}
+ {currencySymbol}{product.price.toFixed(2)}
diff --git a/frontend/src/components/domain/sales/OrdersTable.tsx b/frontend/src/components/domain/sales/OrdersTable.tsx
index e41b867b..3c40017f 100644
--- a/frontend/src/components/domain/sales/OrdersTable.tsx
+++ b/frontend/src/components/domain/sales/OrdersTable.tsx
@@ -13,6 +13,7 @@ import {
import { SalesDataResponse } from '../../../api/types/sales';
import { salesService } from '../../../api/services/sales';
import { useSalesRecords } from '../../../api/hooks/sales';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
// Define missing types for backwards compatibility
type SalesRecord = SalesDataResponse;
@@ -106,6 +107,7 @@ export const OrdersTable: React.FC = ({
initialFilters = {}
}) => {
const { t } = useTranslation(['sales']);
+ const { currencySymbol } = useTenantCurrency();
// Translation helper functions
const getStatusLabel = (status: OrderStatus) => {
@@ -316,10 +318,10 @@ export const OrdersTable: React.FC = ({
sortable: true,
render: (order: Order) => (
-
€{order.total_revenue.toFixed(2)}
+
{currencySymbol}{order.total_revenue.toFixed(2)}
{order.discount_applied > 0 && (
- -€{order.discount_applied.toFixed(2)}
+ -{currencySymbol}{order.discount_applied.toFixed(2)}
)}
@@ -590,7 +592,7 @@ export const OrdersTable: React.FC = ({
...prev,
min_total: e.target.value ? parseFloat(e.target.value) : undefined
}))}
- placeholder="€0.00"
+ placeholder={`${currencySymbol}0.00`}
/>
= ({
...prev,
max_total: e.target.value ? parseFloat(e.target.value) : undefined
}))}
- placeholder="€999.99"
+ placeholder={`${currencySymbol}999.99`}
/>
@@ -781,8 +783,8 @@ export const OrdersTable: React.FC
= ({
)}
-
€{selectedOrder.unit_price.toFixed(2)} × {selectedOrder.quantity_sold}
-
€{selectedOrder.total_revenue.toFixed(2)}
+
{currencySymbol}{selectedOrder.unit_price.toFixed(2)} × {selectedOrder.quantity_sold}
+
{currencySymbol}{selectedOrder.total_revenue.toFixed(2)}
@@ -800,18 +802,18 @@ export const OrdersTable: React.FC = ({
Total del Pedido:
- €{selectedOrder.total_revenue.toFixed(2)}
+ {currencySymbol}{selectedOrder.total_revenue.toFixed(2)}
{selectedOrder.discount_applied > 0 && (
Descuento aplicado:
- -€{selectedOrder.discount_applied.toFixed(2)}
+ -{currencySymbol}{selectedOrder.discount_applied.toFixed(2)}
)}
{selectedOrder.tax_amount > 0 && (
IVA incluido:
- €{selectedOrder.tax_amount.toFixed(2)}
+ {currencySymbol}{selectedOrder.tax_amount.toFixed(2)}
)}
diff --git a/frontend/src/components/domain/sales/SalesChart.tsx b/frontend/src/components/domain/sales/SalesChart.tsx
index 6b02308a..594073cc 100644
--- a/frontend/src/components/domain/sales/SalesChart.tsx
+++ b/frontend/src/components/domain/sales/SalesChart.tsx
@@ -10,6 +10,7 @@ import { SalesAnalytics } from '../../../api/types/sales';
import { ProductPerformance } from '../analytics/types';
import { salesService } from '../../../api/services/sales';
import { useSalesAnalytics } from '../../../api/hooks/sales';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
// Define missing types
export enum PeriodType {
@@ -137,6 +138,7 @@ export const SalesChart: React.FC = ({
showExport = true,
className = ''
}) => {
+ const { currencySymbol } = useTenantCurrency();
// State
const [analytics, setAnalytics] = useState(null);
const [loading, setLoading] = useState(false);
@@ -247,7 +249,7 @@ export const SalesChart: React.FC = ({
),
datasets: [
{
- label: 'Ingresos (€)',
+ label: `Ingresos (${currencySymbol})`,
data: analytics.daily_trends.map(trend => trend.revenue),
backgroundColor: chartType === ChartType.PIE ?
generateColors(analytics.daily_trends.length) : Colors.primary,
@@ -291,7 +293,7 @@ export const SalesChart: React.FC = ({
),
datasets: [
{
- label: 'Ticket Promedio (€)',
+ label: `Ticket Promedio (${currencySymbol})`,
data: analytics.daily_trends.map(trend => trend.average_order_value),
backgroundColor: chartType === ChartType.PIE ?
generateColors(analytics.daily_trends.length) : Colors.tertiary,
@@ -309,7 +311,7 @@ export const SalesChart: React.FC = ({
labels: topProducts.map(product => product.product_name),
datasets: [
{
- label: 'Ingresos por Producto (€)',
+ label: `Ingresos por Producto (${currencySymbol})`,
data: topProducts.map(product => product.total_revenue),
backgroundColor: generateColors(topProducts.length),
borderColor: Colors.primary,
@@ -323,7 +325,7 @@ export const SalesChart: React.FC = ({
labels: analytics.hourly_patterns.map(pattern => `${pattern.hour}:00`),
datasets: [
{
- label: 'Ventas Promedio por Hora (€)',
+ label: `Ventas Promedio por Hora (${currencySymbol})`,
data: analytics.hourly_patterns.map(pattern => pattern.average_sales),
backgroundColor: Colors.secondary,
borderColor: Colors.secondary,
@@ -474,7 +476,7 @@ export const SalesChart: React.FC = ({
fontSize="12"
fill={Colors.text}
>
- €{(minValue + range * (1 - ratio)).toLocaleString('es-ES', { maximumFractionDigits: 0 })}
+ {currencySymbol}{(minValue + range * (1 - ratio)).toLocaleString('es-ES', { maximumFractionDigits: 0 })}
);
@@ -558,7 +560,7 @@ export const SalesChart: React.FC = ({
strokeWidth={2}
>
- {chartData.labels[index]}: €{dataset.data[index].toLocaleString('es-ES', { minimumFractionDigits: 2 })}
+ {chartData.labels[index]}: {currencySymbol}{dataset.data[index].toLocaleString('es-ES', { minimumFractionDigits: 2 })}
))}
@@ -751,7 +753,7 @@ export const SalesChart: React.FC = ({
Ingresos Totales
- €{summaryStats.totalRevenue.toLocaleString('es-ES', { minimumFractionDigits: 2 })}
+ {currencySymbol}{summaryStats.totalRevenue.toLocaleString('es-ES', { minimumFractionDigits: 2 })}
= 0 ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'}`}>
@@ -777,7 +779,7 @@ export const SalesChart: React.FC
= ({
Ticket Promedio
- €{summaryStats.avgOrderValue.toLocaleString('es-ES', { minimumFractionDigits: 2 })}
+ {currencySymbol}{summaryStats.avgOrderValue.toLocaleString('es-ES', { minimumFractionDigits: 2 })}
diff --git a/frontend/src/components/domain/setup-wizard/steps/SupplierProductManager.tsx b/frontend/src/components/domain/setup-wizard/steps/SupplierProductManager.tsx
index 7777a751..66d4100a 100644
--- a/frontend/src/components/domain/setup-wizard/steps/SupplierProductManager.tsx
+++ b/frontend/src/components/domain/setup-wizard/steps/SupplierProductManager.tsx
@@ -9,6 +9,7 @@ import {
import { useIngredients } from '../../../../api/hooks/inventory';
import type { SupplierPriceListCreate, SupplierPriceListResponse } from '../../../../api/types/suppliers';
import { QuickAddIngredientModal } from '../../inventory/QuickAddIngredientModal';
+import { useTenantCurrency } from '../../../../hooks/useTenantCurrency';
interface SupplierProductManagerProps {
tenantId: string;
@@ -30,6 +31,7 @@ export const SupplierProductManager: React.FC = ({
supplierName
}) => {
const { t } = useTranslation();
+ const { currencySymbol } = useTenantCurrency();
// Fetch existing price lists for this supplier
const { data: priceLists = [], isLoading: priceListsLoading } = useSupplierPriceLists(
@@ -236,7 +238,7 @@ export const SupplierProductManager: React.FC = ({
{getProductName(priceList.inventory_product_id)}
- €{Number(priceList.unit_price || 0).toFixed(2)}/{priceList.unit_of_measure}
+ {currencySymbol}{Number(priceList.unit_price || 0).toFixed(2)}/{priceList.unit_of_measure}
{priceList.minimum_order_quantity && priceList.minimum_order_quantity > 1 && (
@@ -319,7 +321,7 @@ export const SupplierProductManager: React.FC = ({
- {t('setup_wizard:suppliers.unit_price', 'Price')} (€) *
+ {t('setup_wizard:suppliers.unit_price', 'Price')} ({currencySymbol}) *
;
@@ -14,6 +15,8 @@ export const SupplierDeliveryStep: React.FC
= ({
onNext,
onBack
}) => {
+ const { currencySymbol } = useTenantCurrency();
+
const handleFieldChange = (field: keyof SupplierCreate, value: any) => {
onUpdate({ ...supplierData, [field]: value });
};
@@ -87,7 +90,7 @@ export const SupplierDeliveryStep: React.FC = ({
- Pedido Mínimo (€)
+ Pedido Mínimo ({currencySymbol})
;
@@ -14,6 +15,8 @@ export const SupplierReviewStep: React.FC
= ({
onNext,
onBack
}) => {
+ const { currencySymbol } = useTenantCurrency();
+
const handleFieldChange = (field: keyof SupplierCreate, value: any) => {
onUpdate({ ...supplierData, [field]: value });
};
@@ -129,7 +132,7 @@ export const SupplierReviewStep: React.FC = ({
Pedido Mínimo
-
€{supplierData.minimum_order_value}
+
{currencySymbol}{supplierData.minimum_order_value}
)}
diff --git a/frontend/src/components/domain/sustainability/SustainabilityWidget.tsx b/frontend/src/components/domain/sustainability/SustainabilityWidget.tsx
index 8671e772..1cb30d95 100644
--- a/frontend/src/components/domain/sustainability/SustainabilityWidget.tsx
+++ b/frontend/src/components/domain/sustainability/SustainabilityWidget.tsx
@@ -15,6 +15,7 @@ import Card from '../../ui/Card/Card';
import { Button, Badge } from '../../ui';
import { useSustainabilityWidget } from '../../../api/hooks/sustainability';
import { useCurrentTenant } from '../../../stores/tenant.store';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
interface SustainabilityWidgetProps {
days?: number;
@@ -30,6 +31,7 @@ export const SustainabilityWidget: React.FC
= ({
const { t } = useTranslation(['sustainability', 'common']);
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
+ const { currencySymbol } = useTenantCurrency();
const { data, isLoading, error } = useSustainabilityWidget(tenantId, days, {
enabled: !!tenantId
@@ -205,7 +207,7 @@ export const SustainabilityWidget: React.FC = ({
{t('sustainability:financial.potential_savings', 'Potential Monthly Savings')}
- €{data.financial_savings_eur.toFixed(2)}
+ {currencySymbol}{data.financial_savings_eur.toFixed(2)}
diff --git a/frontend/src/components/domain/unified-wizard/wizards/CustomerOrderWizard.tsx b/frontend/src/components/domain/unified-wizard/wizards/CustomerOrderWizard.tsx
index 211b814c..1b463ab2 100644
--- a/frontend/src/components/domain/unified-wizard/wizards/CustomerOrderWizard.tsx
+++ b/frontend/src/components/domain/unified-wizard/wizards/CustomerOrderWizard.tsx
@@ -20,6 +20,7 @@ import { useTenant } from '../../../../stores/tenant.store';
import OrdersService from '../../../../api/services/orders';
import { inventoryService } from '../../../../api/services/inventory';
import { ProductType } from '../../../../api/types/inventory';
+import { useTenantCurrency } from '../../../../hooks/useTenantCurrency';
// Step 1: Customer Selection
const CustomerSelectionStep: React.FC = ({ dataRef, onDataChange }) => {
@@ -293,6 +294,7 @@ const OrderItemsStep: React.FC = ({ dataRef, onDataChange }) =>
const data = dataRef?.current || {};
const { t } = useTranslation('wizards');
const { currentTenant } = useTenant();
+ const { currencySymbol } = useTenantCurrency();
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
@@ -452,7 +454,7 @@ const OrderItemsStep: React.FC = ({ dataRef, onDataChange }) =>
{t('customerOrder.orderItems.selectProduct')}
{products.map((product) => (
- {product.name} - €{(product.average_cost || product.last_purchase_price || 0).toFixed(2)} / {product.unit_of_measure}
+ {product.name} - {currencySymbol}{(product.average_cost || product.last_purchase_price || 0).toFixed(2)} / {product.unit_of_measure}
))}
@@ -502,7 +504,7 @@ const OrderItemsStep: React.FC = ({ dataRef, onDataChange }) =>
- {t('customerOrder.orderItems.subtotal')}: €{item.subtotal.toFixed(2)}
+ {t('customerOrder.orderItems.subtotal')}: {currencySymbol}{item.subtotal.toFixed(2)}
@@ -515,7 +517,7 @@ const OrderItemsStep: React.FC
= ({ dataRef, onDataChange }) =>
{t('customerOrder.messages.orderTotal')}:
- €{calculateTotal().toFixed(2)}
+ {currencySymbol}{calculateTotal().toFixed(2)}
@@ -531,6 +533,7 @@ const OrderItemsStep: React.FC = ({ dataRef, onDataChange }) =>
const DeliveryPaymentStep: React.FC = ({ dataRef, onDataChange }) => {
const data = dataRef?.current || {};
const { t } = useTranslation('wizards');
+ const { currencySymbol } = useTenantCurrency();
// Helper to get field value with defaults
const getValue = (field: string, defaultValue: any = '') => {
@@ -820,7 +823,7 @@ const DeliveryPaymentStep: React.FC = ({ dataRef, onDataChange
{t('customerOrder.messages.total')}:
- €{data.totalAmount?.toFixed(2) || '0.00'}
+ {currencySymbol}{data.totalAmount?.toFixed(2) || '0.00'}
diff --git a/frontend/src/components/domain/unified-wizard/wizards/InventoryWizard.tsx b/frontend/src/components/domain/unified-wizard/wizards/InventoryWizard.tsx
index 92598604..95f6c16c 100644
--- a/frontend/src/components/domain/unified-wizard/wizards/InventoryWizard.tsx
+++ b/frontend/src/components/domain/unified-wizard/wizards/InventoryWizard.tsx
@@ -4,6 +4,7 @@ import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
import Tooltip from '../../../ui/Tooltip/Tooltip';
import { Info, Package, ShoppingBag } from 'lucide-react';
+import { useTenantCurrency } from '../../../../hooks/useTenantCurrency';
// STEP 1: Product Type Selection with advanced fields
const ProductTypeStep: React.FC = ({ dataRef, onDataChange }) => {
@@ -197,24 +198,31 @@ const BasicInfoStep: React.FC = ({ dataRef, onDataChange }) =>
<>
{t('inventory.ingredientCategories.select')}
{t('inventory.ingredientCategories.flour')}
+ {t('inventory.ingredientCategories.yeast')}
{t('inventory.ingredientCategories.dairy')}
{t('inventory.ingredientCategories.eggs')}
+ {t('inventory.ingredientCategories.sugar')}
{t('inventory.ingredientCategories.fats')}
- {t('inventory.ingredientCategories.sweeteners')}
- {t('inventory.ingredientCategories.additives')}
- {t('inventory.ingredientCategories.fruits')}
- {t('inventory.ingredientCategories.nuts')}
+ {t('inventory.ingredientCategories.salt')}
{t('inventory.ingredientCategories.spices')}
- {t('inventory.ingredientCategories.leavening')}
+ {t('inventory.ingredientCategories.additives')}
+ {t('inventory.ingredientCategories.packaging')}
+ {t('inventory.ingredientCategories.cleaning')}
+ {t('inventory.ingredientCategories.other')}
>
) : (
<>
{t('inventory.productCategories.select')}
{t('inventory.productCategories.bread')}
- {t('inventory.productCategories.pastry')}
- {t('inventory.productCategories.cake')}
+ {t('inventory.productCategories.croissants')}
+ {t('inventory.productCategories.pastries')}
+ {t('inventory.productCategories.cakes')}
{t('inventory.productCategories.cookies')}
- {t('inventory.productCategories.specialty')}
+ {t('inventory.productCategories.muffins')}
+ {t('inventory.productCategories.sandwiches')}
+ {t('inventory.productCategories.seasonal')}
+ {t('inventory.productCategories.beverages')}
+ {t('inventory.productCategories.other_products')}
>
)}
@@ -310,6 +318,7 @@ const BasicInfoStep: React.FC = ({ dataRef, onDataChange }) =>
const StockConfigStep: React.FC = ({ dataRef, onDataChange }) => {
const data = dataRef?.current || {};
const { t } = useTranslation('wizards');
+ const { currencySymbol } = useTenantCurrency();
const [lots, setLots] = useState(data.initialLots || []);
const handleFieldChange = (field: string, value: any) => {
@@ -381,7 +390,7 @@ const StockConfigStep: React.FC = ({ dataRef, onDataChange }) =
{t('inventory.stockConfig.totalValue')}
- ${totalValue.toFixed(2)}
+ {currencySymbol}{totalValue.toFixed(2)}
@@ -454,7 +463,7 @@ const StockConfigStep: React.FC = ({ dataRef, onDataChange }) =
{/* Unit Cost */}
- {t('inventory.stockConfig.unitCost')}
+ {t('inventory.stockConfig.unitCost')} ({currencySymbol})
= ({ dataRef, onDataChange }) =
{lot.quantity && lot.unitCost && (
{t('inventory.stockConfig.lotValue')}
- ${(parseFloat(lot.quantity) * parseFloat(lot.unitCost)).toFixed(2)}
+ {currencySymbol}{(parseFloat(lot.quantity) * parseFloat(lot.unitCost)).toFixed(2)}
)}
diff --git a/frontend/src/components/domain/unified-wizard/wizards/PurchaseOrderWizard.tsx b/frontend/src/components/domain/unified-wizard/wizards/PurchaseOrderWizard.tsx
index 9b31e954..a6e758d8 100644
--- a/frontend/src/components/domain/unified-wizard/wizards/PurchaseOrderWizard.tsx
+++ b/frontend/src/components/domain/unified-wizard/wizards/PurchaseOrderWizard.tsx
@@ -19,6 +19,7 @@ import { useSuppliers } from '../../../../api/hooks/suppliers';
import { useIngredients } from '../../../../api/hooks/inventory';
import { suppliersService } from '../../../../api/services/suppliers';
import { useCreatePurchaseOrder } from '../../../../api/hooks/purchase-orders';
+import { useTenantCurrency } from '../../../../hooks/useTenantCurrency';
// Step 1: Supplier Selection
const SupplierSelectionStep: React.FC
= ({ dataRef, onDataChange }) => {
@@ -157,6 +158,7 @@ const AddItemsStep: React.FC = ({ dataRef, onDataChange }) => {
const data = dataRef?.current || {};
const { t } = useTranslation(['wizards', 'procurement']);
const { currentTenant } = useTenant();
+ const { currencySymbol } = useTenantCurrency();
const [supplierProductIds, setSupplierProductIds] = useState([]);
const [isLoadingSupplierProducts, setIsLoadingSupplierProducts] = useState(false);
@@ -338,7 +340,7 @@ const AddItemsStep: React.FC = ({ dataRef, onDataChange }) => {
{t('purchaseOrder.orderItems.selectIngredient')}
{ingredientsData.map((product: any) => (
- {product.name} - €{(product.last_purchase_price || product.average_cost || 0).toFixed(2)} /{' '}
+ {product.name} - {currencySymbol}{(product.last_purchase_price || product.average_cost || 0).toFixed(2)} /{' '}
{product.unit_of_measure}
))}
@@ -393,7 +395,7 @@ const AddItemsStep: React.FC = ({ dataRef, onDataChange }) => {
- {t('purchaseOrder.orderItems.subtotal')}: €{item.subtotal.toFixed(2)}
+ {t('purchaseOrder.orderItems.subtotal')}: {currencySymbol}{item.subtotal.toFixed(2)}
@@ -405,7 +407,7 @@ const AddItemsStep: React.FC = ({ dataRef, onDataChange }) => {
{t('purchaseOrder.orderItems.total')}:
- €{calculateTotal().toFixed(2)}
+ {currencySymbol}{calculateTotal().toFixed(2)}
)}
@@ -537,6 +539,7 @@ const OrderDetailsStep: React.FC = ({ dataRef, onDataChange })
const ReviewSubmitStep: React.FC = ({ dataRef, onDataChange }) => {
const data = dataRef?.current || {};
const { t } = useTranslation(['wizards', 'procurement']);
+ const { currencySymbol } = useTenantCurrency();
const calculateSubtotal = () => {
return (data.items || []).reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0);
@@ -625,11 +628,11 @@ const ReviewSubmitStep: React.FC = ({ dataRef, onDataChange })
{item.product_name || t('purchaseOrder.review.productNoName')}
- {item.ordered_quantity} {item.unit_of_measure} × €{item.unit_price.toFixed(2)}
+ {item.ordered_quantity} {item.unit_of_measure} × {currencySymbol}{item.unit_price.toFixed(2)}
-
€{item.subtotal.toFixed(2)}
+
{currencySymbol}{item.subtotal.toFixed(2)}
))}
@@ -645,29 +648,29 @@ const ReviewSubmitStep: React.FC = ({ dataRef, onDataChange })
{t('purchaseOrder.review.subtotal')}:
- €{calculateSubtotal().toFixed(2)}
+ {currencySymbol}{calculateSubtotal().toFixed(2)}
{(data.tax_amount || 0) > 0 && (
{t('purchaseOrder.review.taxes')}:
- €{(data.tax_amount || 0).toFixed(2)}
+ {currencySymbol}{(data.tax_amount || 0).toFixed(2)}
)}
{(data.shipping_cost || 0) > 0 && (
{t('purchaseOrder.review.shipping')}:
- €{(data.shipping_cost || 0).toFixed(2)}
+ {currencySymbol}{(data.shipping_cost || 0).toFixed(2)}
)}
{(data.discount_amount || 0) > 0 && (
{t('purchaseOrder.review.discount')}:
- -€{(data.discount_amount || 0).toFixed(2)}
+ -{currencySymbol}{(data.discount_amount || 0).toFixed(2)}
)}
{t('purchaseOrder.review.total')}:
- €{calculateTotal().toFixed(2)}
+ {currencySymbol}{calculateTotal().toFixed(2)}
diff --git a/frontend/src/components/domain/unified-wizard/wizards/SalesEntryWizard.tsx b/frontend/src/components/domain/unified-wizard/wizards/SalesEntryWizard.tsx
index 7b701235..9288235e 100644
--- a/frontend/src/components/domain/unified-wizard/wizards/SalesEntryWizard.tsx
+++ b/frontend/src/components/domain/unified-wizard/wizards/SalesEntryWizard.tsx
@@ -19,6 +19,7 @@ import { useTenant } from '../../../../stores/tenant.store';
import { salesService } from '../../../../api/services/sales';
import { inventoryService } from '../../../../api/services/inventory';
import { showToast } from '../../../../utils/toast';
+import { useTenantCurrency } from '../../../../hooks/useTenantCurrency';
// ========================================
// STEP 1: Entry Method Selection
@@ -174,6 +175,7 @@ const ManualEntryStep: React.FC = ({ dataRef, onDataChange, onN
const data = dataRef?.current || {};
const { t } = useTranslation('wizards');
const { currentTenant } = useTenant();
+ const { currencySymbol } = useTenantCurrency();
const [products, setProducts] = useState([]);
const [loadingProducts, setLoadingProducts] = useState(true);
const [error, setError] = useState(null);
@@ -351,7 +353,7 @@ const ManualEntryStep: React.FC = ({ dataRef, onDataChange, onN
{t('salesEntry.manualEntry.products.selectProduct')}
{products.map((product: any) => (
- {product.name} - €{(product.average_cost || product.last_purchase_price || 0).toFixed(2)}
+ {product.name} - {currencySymbol}{(product.average_cost || product.last_purchase_price || 0).toFixed(2)}
))}
@@ -383,7 +385,7 @@ const ManualEntryStep: React.FC = ({ dataRef, onDataChange, onN
/>
- €{item.subtotal.toFixed(2)}
+ {currencySymbol}{item.subtotal.toFixed(2)}
= ({ dataRef, onDataChange, onN
{(data.salesItems || []).length > 0 && (
- {t('salesEntry.manualEntry.products.total')} €{calculateTotal().toFixed(2)}
+ {t('salesEntry.manualEntry.products.total')} {currencySymbol}{calculateTotal().toFixed(2)}
)}
@@ -673,6 +675,7 @@ const FileUploadStep: React.FC = ({ dataRef, onDataChange, onNe
const ReviewStep: React.FC = ({ dataRef }) => {
const data = dataRef?.current || {};
const { t } = useTranslation('wizards');
+ const { currencySymbol } = useTenantCurrency();
const isManual = data.entryMethod === 'manual';
const isUpload = data.entryMethod === 'upload';
@@ -725,12 +728,12 @@ const ReviewStep: React.FC = ({ dataRef }) => {
{item.product}
- {item.quantity} × €{item.unitPrice.toFixed(2)}
+ {item.quantity} × {currencySymbol}{item.unitPrice.toFixed(2)}
- €{item.subtotal.toFixed(2)}
+ {currencySymbol}{item.subtotal.toFixed(2)}
@@ -743,7 +746,7 @@ const ReviewStep: React.FC = ({ dataRef }) => {
{t('salesEntry.review.fields.total')}
- €{data.totalAmount?.toFixed(2)}
+ {currencySymbol}{data.totalAmount?.toFixed(2)}
diff --git a/frontend/src/components/ui/AddModal/AddModal.tsx b/frontend/src/components/ui/AddModal/AddModal.tsx
index 20da3827..94f1a0b0 100644
--- a/frontend/src/components/ui/AddModal/AddModal.tsx
+++ b/frontend/src/components/ui/AddModal/AddModal.tsx
@@ -7,6 +7,7 @@ import { Input } from '../Input';
import { Select } from '../Select';
import { StatusIndicatorConfig } from '../StatusCard';
import { statusColors } from '../../../styles/colors';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
// Constants to prevent re-creation on every render
const EMPTY_VALIDATION_ERRORS = {};
@@ -24,6 +25,7 @@ interface ListFieldRendererProps {
const ListFieldRenderer: React.FC = ({ field, value, onChange, error }) => {
const { t } = useTranslation(['common']);
+ const { currencySymbol } = useTenantCurrency();
const listConfig = field.listConfig!;
const addItem = () => {
@@ -174,7 +176,7 @@ const ListFieldRenderer: React.FC = ({ field, value, onC
{listConfig.showSubtotals && (
- Subtotal: €{calculateSubtotal(item).toFixed(2)}
+ Subtotal: {currencySymbol}{calculateSubtotal(item).toFixed(2)}
)}
@@ -185,7 +187,7 @@ const ListFieldRenderer: React.FC = ({ field, value, onC
{listConfig.showSubtotals && value.length > 0 && (
- Total: €{calculateTotal().toFixed(2)}
+ Total: {currencySymbol}{calculateTotal().toFixed(2)}
)}
diff --git a/frontend/src/components/ui/AdvancedOptionsSection/AdvancedOptionsSection.tsx b/frontend/src/components/ui/AdvancedOptionsSection/AdvancedOptionsSection.tsx
index 28759fb0..12d3571d 100644
--- a/frontend/src/components/ui/AdvancedOptionsSection/AdvancedOptionsSection.tsx
+++ b/frontend/src/components/ui/AdvancedOptionsSection/AdvancedOptionsSection.tsx
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
+import { useTranslation } from 'react-i18next';
import { ChevronDown, ChevronUp } from 'lucide-react';
interface AdvancedOptionsSectionProps {
@@ -14,6 +15,7 @@ export const AdvancedOptionsSection: React.FC = ({
description = 'These fields are optional but help improve data management',
defaultExpanded = false,
}) => {
+ const { t } = useTranslation('wizards');
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
return (
@@ -26,12 +28,12 @@ export const AdvancedOptionsSection: React.FC = ({
{isExpanded ? (
<>
- Hide {title}
+ {t('common.hide')} {title}
>
) : (
<>
- Show {title}
+ {t('common.show')} {title}
>
)}
diff --git a/frontend/src/components/ui/NotificationPanel/NotificationPanel.tsx b/frontend/src/components/ui/NotificationPanel/NotificationPanel.tsx
index aca1d46c..91a8a36f 100644
--- a/frontend/src/components/ui/NotificationPanel/NotificationPanel.tsx
+++ b/frontend/src/components/ui/NotificationPanel/NotificationPanel.tsx
@@ -21,6 +21,7 @@ import { renderEventTitle, renderEventMessage, renderActionLabel, renderAIReason
import { useSmartActionHandler } from '../../../utils/smartActionHandlers';
import { useAuthUser } from '../../../stores/auth.store';
import { useTranslation } from 'react-i18next';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
export interface NotificationPanelProps {
notifications: NotificationData[];
@@ -58,6 +59,7 @@ const EnrichedAlertItem: React.FC<{
actionHandler: any;
}> = ({ alert, isMobile, onMarkAsRead, onRemove, actionHandler }) => {
const { t } = useTranslation();
+ const { currencySymbol } = useTenantCurrency();
const isUnread = alert.status === 'active';
const priorityColor = getPriorityColor(alert.priority_level);
@@ -132,7 +134,7 @@ const EnrichedAlertItem: React.FC<{
{alert.business_impact?.financial_impact_eur && (
- €{alert.business_impact.financial_impact_eur.toFixed(0)} en riesgo
+ {currencySymbol}{alert.business_impact.financial_impact_eur.toFixed(0)} en riesgo
)}
{alert.urgency?.hours_until_consequence && (
diff --git a/frontend/src/components/ui/Stats/StatsPresets.ts b/frontend/src/components/ui/Stats/StatsPresets.ts
index 049cc46f..7a27811a 100644
--- a/frontend/src/components/ui/Stats/StatsPresets.ts
+++ b/frontend/src/components/ui/Stats/StatsPresets.ts
@@ -17,9 +17,12 @@ import {
import { StatsCardProps, StatsCardVariant } from './StatsCard';
// Common formatting functions
+// Note: For currency formatting with dynamic symbols, use the useTenantCurrency hook
+// and createCurrencyFormatter helper function instead of formatters.currency directly
export const formatters = {
percentage: (value: string | number): string => `${value}%`,
- currency: (value: string | number): string => `€${parseFloat(String(value)).toFixed(2)}`,
+ // Currency formatter with configurable symbol (defaults to € for backwards compatibility)
+ currency: (value: string | number, currencySymbol: string = '€'): string => `${currencySymbol}${parseFloat(String(value)).toFixed(2)}`,
number: (value: string | number): string => parseFloat(String(value)).toLocaleString('es-ES'),
compact: (value: string | number): string => {
const num = parseFloat(String(value));
@@ -29,6 +32,10 @@ export const formatters = {
},
};
+// Helper to create a currency formatter with a specific symbol
+export const createCurrencyFormatter = (currencySymbol: string) =>
+ (value: string | number): string => `${currencySymbol}${parseFloat(String(value)).toFixed(2)}`;
+
// Icon mappings for common stat types
export const statIcons = {
target: Calendar,
diff --git a/frontend/src/contexts/SSEContext.tsx b/frontend/src/contexts/SSEContext.tsx
index d5a591da..894ee40f 100644
--- a/frontend/src/contexts/SSEContext.tsx
+++ b/frontend/src/contexts/SSEContext.tsx
@@ -3,6 +3,7 @@ import { useAuthStore } from '../stores/auth.store';
import { useCurrentTenant } from '../stores/tenant.store';
import { showToast } from '../utils/toast';
import i18n from '../i18n';
+import { getTenantCurrencySymbol } from '../hooks/useTenantCurrency';
interface SSEEvent {
type: string;
@@ -211,7 +212,8 @@ export const SSEProvider: React.FC = ({ children }) => {
// Add financial impact to message if available
if (data.business_impact?.financial_impact_eur) {
- message = `${message} • €${data.business_impact.financial_impact_eur} en riesgo`;
+ const currencySymbol = getTenantCurrencySymbol(currentTenant?.currency);
+ message = `${message} • ${currencySymbol}${data.business_impact.financial_impact_eur} en riesgo`;
}
showToast[toastType](message, { title, duration });
@@ -446,7 +448,8 @@ export const SSEProvider: React.FC = ({ children }) => {
if (data.estimated_impact) {
const impact = data.estimated_impact;
if (impact.savings_eur) {
- message = `${message} • €${impact.savings_eur} de ahorro estimado`;
+ const currencySymbol = getTenantCurrencySymbol(currentTenant?.currency);
+ message = `${message} • ${currencySymbol}${impact.savings_eur} de ahorro estimado`;
} else if (impact.risk_reduction_percent) {
message = `${message} • ${impact.risk_reduction_percent}% reducción de riesgo`;
}
diff --git a/frontend/src/hooks/useTenantCurrency.ts b/frontend/src/hooks/useTenantCurrency.ts
new file mode 100644
index 00000000..4f187a6f
--- /dev/null
+++ b/frontend/src/hooks/useTenantCurrency.ts
@@ -0,0 +1,92 @@
+/**
+ * Hook for getting the current tenant's currency configuration
+ *
+ * This hook provides:
+ * - The currency code (EUR, USD, GBP)
+ * - The currency symbol (€, $, £)
+ * - A currency formatter function
+ *
+ * It reads from the current tenant's settings and defaults to EUR.
+ */
+
+import { useMemo } from 'react';
+import { useCurrentTenant } from '../stores/tenant.store';
+import {
+ CURRENCY_CONFIG,
+ DEFAULT_CURRENCY,
+ getCurrencySymbol,
+ formatCurrency,
+ type CurrencyCode
+} from '../utils/currency';
+
+export interface TenantCurrencyInfo {
+ /** Currency code (EUR, USD, GBP) */
+ currencyCode: CurrencyCode;
+ /** Currency symbol (€, $, £) */
+ currencySymbol: string;
+ /** Currency name (Euro, US Dollar, British Pound) */
+ currencyName: string;
+ /** Format a number as currency */
+ format: (amount: number) => string;
+ /** Format a number as currency (compact format for large numbers) */
+ formatCompact: (amount: number) => string;
+}
+
+/**
+ * Hook to get the current tenant's currency configuration
+ *
+ * @returns TenantCurrencyInfo with currency code, symbol, and formatting functions
+ *
+ * @example
+ * const { currencySymbol, format } = useTenantCurrency();
+ *
+ * // Display currency symbol in a label
+ * Costo Unitario ({currencySymbol})
+ *
+ * // Format a value
+ * {format(123.45)} // "123,45 €"
+ */
+export function useTenantCurrency(): TenantCurrencyInfo {
+ const currentTenant = useCurrentTenant();
+
+ return useMemo(() => {
+ // Get currency from tenant, default to EUR
+ const tenantCurrency = currentTenant?.currency;
+ const currencyCode: CurrencyCode =
+ (tenantCurrency && tenantCurrency in CURRENCY_CONFIG)
+ ? (tenantCurrency as CurrencyCode)
+ : DEFAULT_CURRENCY;
+
+ const config = CURRENCY_CONFIG[currencyCode];
+
+ return {
+ currencyCode,
+ currencySymbol: getCurrencySymbol(currencyCode),
+ currencyName: config.name,
+ format: (amount: number) => formatCurrency(amount, currencyCode),
+ formatCompact: (amount: number) => {
+ // Compact format for large numbers (e.g., €1.2K, €1.5M)
+ if (amount >= 1000000) {
+ return `${config.symbol}${(amount / 1000000).toFixed(1)}M`;
+ } else if (amount >= 1000) {
+ return `${config.symbol}${(amount / 1000).toFixed(1)}K`;
+ }
+ return formatCurrency(amount, currencyCode);
+ },
+ };
+ }, [currentTenant?.currency]);
+}
+
+/**
+ * Get currency symbol without hook (for use in non-component code)
+ * Falls back to EUR symbol (€)
+ */
+export function getTenantCurrencySymbol(tenantCurrency?: string | null): string {
+ const currencyCode: CurrencyCode =
+ (tenantCurrency && tenantCurrency in CURRENCY_CONFIG)
+ ? (tenantCurrency as CurrencyCode)
+ : DEFAULT_CURRENCY;
+ return getCurrencySymbol(currencyCode);
+}
+
+export default useTenantCurrency;
diff --git a/frontend/src/locales/en/auth.json b/frontend/src/locales/en/auth.json
index 491ae903..6256a40a 100644
--- a/frontend/src/locales/en/auth.json
+++ b/frontend/src/locales/en/auth.json
@@ -81,7 +81,13 @@
"postal_code": "Postal Code",
"country": "Country",
"card_details": "Card details",
- "card_info_secure": "Your card information is secure"
+ "card_info_secure": "Your card information is secure",
+ "process_payment": "Process Payment",
+ "payment_bypassed": "Payment Bypassed",
+ "bypass_payment": "Bypass Payment",
+ "payment_bypassed_title": "Payment Bypassed",
+ "payment_bypassed_description": "Payment process has been bypassed in development mode. Registration will continue normally.",
+ "continue_registration": "Continue Registration"
},
"alerts": {
"success_create": "Account created successfully",
diff --git a/frontend/src/locales/es/auth.json b/frontend/src/locales/es/auth.json
index 8e51b02a..94d68143 100644
--- a/frontend/src/locales/es/auth.json
+++ b/frontend/src/locales/es/auth.json
@@ -64,7 +64,7 @@
"trial_period": "Período de prueba:",
"total_today": "Total hoy:",
"payment_required": "Tarjeta requerida para validación",
- "billing_message": "Se te cobrará {{price}} después del período de prueba",
+ "billing_message": "Se te cobrará {price} después del período de prueba",
"free_months": "{count} meses GRATIS",
"free_days": "14 días gratis",
"payment_info": "Información de Pago",
@@ -78,7 +78,13 @@
"postal_code": "Código Postal",
"country": "País",
"card_details": "Detalles de la tarjeta",
- "card_info_secure": "Tu información de tarjeta está segura"
+ "card_info_secure": "Tu información de tarjeta está segura",
+ "process_payment": "Procesar Pago",
+ "payment_bypassed": "Pago Omitido",
+ "bypass_payment": "Omitir Pago",
+ "payment_bypassed_title": "Pago Omitido",
+ "payment_bypassed_description": "El proceso de pago ha sido omitido en modo desarrollo. El registro continuará normalmente.",
+ "continue_registration": "Continuar con el Registro"
},
"alerts": {
"success_create": "Cuenta creada exitosamente",
@@ -213,25 +219,5 @@
"selected": "Seleccionado",
"popular": "Más Popular",
"select": "Seleccionar Plan"
- },
- "payment": {
- "payment_info": "Información de Pago",
- "secure_payment": "Tu información de pago está protegida con encriptación de extremo a extremo",
- "dev_mode": "Modo Desarrollo",
- "payment_bypassed": "Pago Bypassed",
- "bypass_payment": "Bypass Pago",
- "cardholder_name": "Nombre del titular",
- "email": "Correo electrónico",
- "address_line1": "Dirección",
- "city": "Ciudad",
- "state": "Estado/Provincia",
- "postal_code": "Código Postal",
- "country": "País",
- "card_details": "Detalles de la tarjeta",
- "card_info_secure": "Tu información de tarjeta está segura",
- "process_payment": "Procesar Pago",
- "payment_bypassed_title": "Pago Bypassed",
- "payment_bypassed_description": "El proceso de pago ha sido omitido en modo desarrollo. El registro continuará normalmente.",
- "continue_registration": "Continuar con el Registro"
}
}
\ No newline at end of file
diff --git a/frontend/src/locales/es/wizards.json b/frontend/src/locales/es/wizards.json
index ffcb1c0e..ebbbda3e 100644
--- a/frontend/src/locales/es/wizards.json
+++ b/frontend/src/locales/es/wizards.json
@@ -44,7 +44,7 @@
"lot": "Lote",
"remove": "Eliminar",
"quantity": "Cantidad",
- "unitCost": "Costo Unitario ($)",
+ "unitCost": "Costo Unitario",
"lotNumber": "Número de Lote",
"expirationDate": "Fecha de Expiración",
"location": "Ubicación",
@@ -161,23 +161,30 @@
"ingredientCategories": {
"select": "Seleccionar...",
"flour": "Harinas",
+ "yeast": "Levaduras",
"dairy": "Lácteos",
"eggs": "Huevos",
+ "sugar": "Azúcares",
"fats": "Grasas y Aceites",
- "sweeteners": "Endulzantes",
- "additives": "Aditivos",
- "fruits": "Frutas",
- "nuts": "Nueces y Semillas",
+ "salt": "Sal",
"spices": "Especias",
- "leavening": "Agentes Leudantes"
+ "additives": "Aditivos",
+ "packaging": "Empaques",
+ "cleaning": "Limpieza",
+ "other": "Otros"
},
"productCategories": {
"select": "Seleccionar...",
- "bread": "Pan",
- "pastry": "Pastelería",
- "cake": "Tortas",
+ "bread": "Panes",
+ "croissants": "Croissants",
+ "pastries": "Bollería",
+ "cakes": "Tartas",
"cookies": "Galletas",
- "specialty": "Artículos Especiales"
+ "muffins": "Muffins",
+ "sandwiches": "Sándwiches",
+ "seasonal": "Temporales",
+ "beverages": "Bebidas",
+ "other_products": "Otros Productos"
}
},
"qualityTemplate": {
diff --git a/frontend/src/locales/eu/auth.json b/frontend/src/locales/eu/auth.json
index be488e98..6c508039 100644
--- a/frontend/src/locales/eu/auth.json
+++ b/frontend/src/locales/eu/auth.json
@@ -62,7 +62,7 @@
"trial_period": "Proba epea:",
"total_today": "Gaurko totala:",
"payment_required": "Ordainketa beharrezkoa balidaziorako",
- "billing_message": "{{price}} kobratuko zaizu proba epea ondoren",
+ "billing_message": "{price} kobratuko zaizu proba epea ondoren",
"free_months": "{count} hilabete DOAN",
"free_days": "14 egun doan",
"payment_info": "Ordainketaren informazioa",
@@ -78,11 +78,11 @@
"card_details": "Txartelaren xehetasunak",
"card_info_secure": "Zure txartelaren informazioa segurua da",
"process_payment": "Prozesatu Ordainketa",
+ "payment_bypassed": "Ordainketa Saltatua",
+ "bypass_payment": "Saltatu Ordainketa",
"payment_bypassed_title": "Ordainketa Saltatua",
"payment_bypassed_description": "Ordainketa prozesua saltatu da garapen moduan. Erregistratzea modu normalean jarraituko du.",
- "continue_registration": "Erregistratzearekin Jarraitu",
- "payment_bypassed": "Ordainketa Saltatua",
- "bypass_payment": "Saltatu Ordainketa"
+ "continue_registration": "Erregistratzearekin Jarraitu"
},
"subscription": {
"select_plan": "Hautatu zure plana",
diff --git a/frontend/src/pages/app/EnterpriseDashboardPage.tsx b/frontend/src/pages/app/EnterpriseDashboardPage.tsx
index d07c8685..7968586f 100644
--- a/frontend/src/pages/app/EnterpriseDashboardPage.tsx
+++ b/frontend/src/pages/app/EnterpriseDashboardPage.tsx
@@ -41,6 +41,7 @@ import { apiClient } from '../../api/client/apiClient';
import { useEnterprise } from '../../contexts/EnterpriseContext';
import { useTenant } from '../../stores/tenant.store';
import { useSSEEvents } from '../../hooks/useSSE';
+import { useTenantCurrency } from '../../hooks/useTenantCurrency';
import { useQueryClient } from '@tanstack/react-query';
// Components for enterprise dashboard
@@ -64,6 +65,7 @@ const EnterpriseDashboardPage: React.FC = ({ tenan
const { t } = useTranslation('dashboard');
const { state: enterpriseState, drillDownToOutlet, returnToNetworkView, enterNetworkView } = useEnterprise();
const { switchTenant } = useTenant();
+ const { currencySymbol } = useTenantCurrency();
const [selectedMetric, setSelectedMetric] = useState('sales');
const [selectedPeriod, setSelectedPeriod] = useState(30);
@@ -315,7 +317,7 @@ const EnterpriseDashboardPage: React.FC = ({ tenan
style={{ borderColor: 'var(--border-primary)' }}>
Network Average Sales:
- €{enterpriseState.networkMetrics.averageSales.toLocaleString()}
+ {currencySymbol}{enterpriseState.networkMetrics.averageSales.toLocaleString()}
Total Outlets:
@@ -323,7 +325,7 @@ const EnterpriseDashboardPage: React.FC = ({ tenan
Network Total:
- €{enterpriseState.networkMetrics.totalSales.toLocaleString()}
+ {currencySymbol}{enterpriseState.networkMetrics.totalSales.toLocaleString()}
)}
diff --git a/frontend/src/pages/app/analytics/ProcurementAnalyticsPage.tsx b/frontend/src/pages/app/analytics/ProcurementAnalyticsPage.tsx
index aabc989b..0d91c05d 100644
--- a/frontend/src/pages/app/analytics/ProcurementAnalyticsPage.tsx
+++ b/frontend/src/pages/app/analytics/ProcurementAnalyticsPage.tsx
@@ -25,11 +25,13 @@ import { useSubscription } from '../../../api/hooks/subscription';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { useProcurementDashboard, useProcurementTrends } from '../../../api/hooks/procurement';
import { formatters } from '../../../components/ui/Stats/StatsPresets';
+import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
const ProcurementAnalyticsPage: React.FC = () => {
const { canAccessAnalytics, subscriptionInfo } = useSubscription();
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
+ const { currencySymbol } = useTenantCurrency();
const [activeTab, setActiveTab] = useState('overview');
@@ -199,7 +201,7 @@ const ProcurementAnalyticsPage: React.FC = () => {
{plan.total_requirements}
- €{formatters.currency(plan.total_estimated_cost)}
+ {currencySymbol}{formatters.currency(plan.total_estimated_cost, '')}
))}
@@ -378,7 +380,7 @@ const ProcurementAnalyticsPage: React.FC = () => {
Costo Total Estimado
- €{formatters.currency(dashboard?.summary?.total_estimated_cost || 0)}
+ {currencySymbol}{formatters.currency(dashboard?.summary?.total_estimated_cost || 0, '')}
@@ -387,7 +389,7 @@ const ProcurementAnalyticsPage: React.FC = () => {
Costo Total Aprobado
- €{formatters.currency(dashboard?.summary?.total_approved_cost || 0)}
+ {currencySymbol}{formatters.currency(dashboard?.summary?.total_approved_cost || 0, '')}
@@ -400,7 +402,7 @@ const ProcurementAnalyticsPage: React.FC = () => {
? 'text-[var(--color-error)]'
: 'text-[var(--color-success)]'
}`}>
- {(dashboard?.summary?.cost_variance || 0) > 0 ? '+' : ''}€{formatters.currency(dashboard?.summary?.cost_variance || 0)}
+ {(dashboard?.summary?.cost_variance || 0) > 0 ? '+' : ''}{currencySymbol}{formatters.currency(dashboard?.summary?.cost_variance || 0, '')}
{
{category.name}
- €{formatters.currency(category.amount)}
+ {currencySymbol}{formatters.currency(category.amount, '')}
diff --git a/frontend/src/pages/app/analytics/performance/PerformanceAnalyticsPage.tsx b/frontend/src/pages/app/analytics/performance/PerformanceAnalyticsPage.tsx
index 31f395d0..05792cbd 100644
--- a/frontend/src/pages/app/analytics/performance/PerformanceAnalyticsPage.tsx
+++ b/frontend/src/pages/app/analytics/performance/PerformanceAnalyticsPage.tsx
@@ -34,6 +34,7 @@ import { Badge, Card } from '../../../../components/ui';
import { AnalyticsPageLayout, AnalyticsCard } from '../../../../components/analytics';
import { useSubscription } from '../../../../api/hooks/subscription';
import { useCurrentTenant } from '../../../../stores/tenant.store';
+import { useTenantCurrency } from '../../../../hooks/useTenantCurrency';
import {
useCycleTimeMetrics,
useProcessEfficiencyScore,
@@ -46,18 +47,19 @@ import {
} from '../../../../api/hooks/performance';
import { TimePeriod } from '../../../../api/types/performance';
-// Formatters for StatsGrid
+// Formatters for StatsGrid - Note: currency uses dynamic symbol from hook in the component
const formatters = {
number: (value: number) => value.toFixed(0),
percentage: (value: number) => `${value.toFixed(1)}%`,
hours: (value: number) => `${value.toFixed(1)}h`,
- currency: (value: number) => `€${value.toLocaleString('es-ES', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`,
+ currency: (value: number, currencySymbol: string = '€') => `${currencySymbol}${value.toLocaleString('es-ES', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`,
};
const PerformanceAnalyticsPage: React.FC = () => {
const { canAccessAnalytics, subscriptionInfo } = useSubscription();
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
+ const { currencySymbol } = useTenantCurrency();
const [selectedPeriod, setSelectedPeriod] = useState
('week');
const [activeTab, setActiveTab] = useState('overview');
@@ -515,13 +517,13 @@ const PerformanceAnalyticsPage: React.FC = () => {
Ingresos Totales
- €{costRevenue.total_revenue.toLocaleString('es-ES')}
+ {currencySymbol}{costRevenue.total_revenue.toLocaleString('es-ES')}
Costos Estimados
- €{costRevenue.estimated_costs.toLocaleString('es-ES')}
+ {currencySymbol}{costRevenue.estimated_costs.toLocaleString('es-ES')}
diff --git a/frontend/src/pages/app/database/sustainability/SustainabilityPage.tsx b/frontend/src/pages/app/database/sustainability/SustainabilityPage.tsx
index cfeb185e..15609bec 100644
--- a/frontend/src/pages/app/database/sustainability/SustainabilityPage.tsx
+++ b/frontend/src/pages/app/database/sustainability/SustainabilityPage.tsx
@@ -18,11 +18,13 @@ import { LoadingSpinner } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { useSustainabilityMetrics } from '../../../../api/hooks/sustainability';
import { useCurrentTenant } from '../../../../stores/tenant.store';
+import { useTenantCurrency } from '../../../../hooks/useTenantCurrency';
const SustainabilityPage: React.FC = () => {
const { t } = useTranslation(['sustainability', 'common']);
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
+ const { currencySymbol } = useTenantCurrency();
// Date range state (default to last 30 days)
const [dateRange, setDateRange] = useState<{ start?: string; end?: string }>({});
@@ -143,7 +145,7 @@ const SustainabilityPage: React.FC = () => {
},
{
title: t('sustainability:stats.monthly_savings', 'Monthly Savings'),
- value: `€${metrics.financial_impact.potential_monthly_savings.toFixed(0)}`,
+ value: `${currencySymbol}${metrics.financial_impact.potential_monthly_savings.toFixed(0)}`,
icon: Euro,
variant: 'success' as const,
subtitle: t('sustainability:stats.from_waste_reduction', 'From waste reduction')
@@ -512,7 +514,7 @@ const SustainabilityPage: React.FC = () => {
{program.funding_eur && program.funding_eur > 0 && (
- {t('sustainability:grant.funding', 'Financiación')}: €{program.funding_eur.toLocaleString()}
+ {t('sustainability:grant.funding', 'Financiación')}: {currencySymbol}{program.funding_eur.toLocaleString()}
)}
@@ -569,10 +571,10 @@ const SustainabilityPage: React.FC = () => {
{t('sustainability:financial.waste_cost', 'Coste de Residuos')}
- €{metrics.financial_impact.waste_cost_eur.toFixed(2)}
+ {currencySymbol}{metrics.financial_impact.waste_cost_eur.toFixed(2)}
- €{metrics.financial_impact.cost_per_kg.toFixed(2)}/kg
+ {currencySymbol}{metrics.financial_impact.cost_per_kg.toFixed(2)}/kg
@@ -581,7 +583,7 @@ const SustainabilityPage: React.FC = () => {
{t('sustainability:financial.monthly_savings', 'Ahorro Mensual')}
- €{metrics.financial_impact.potential_monthly_savings.toFixed(2)}
+ {currencySymbol}{metrics.financial_impact.potential_monthly_savings.toFixed(2)}
{t('sustainability:financial.from_reduction', 'Por reducción')}
@@ -593,7 +595,7 @@ const SustainabilityPage: React.FC = () => {
{t('sustainability:financial.annual_projection', 'Proyección Anual')}
- €{metrics.financial_impact.annual_projection.toFixed(2)}
+ {currencySymbol}{metrics.financial_impact.annual_projection.toFixed(2)}
{t('sustainability:financial.estimated', 'Estimado')}
@@ -605,7 +607,7 @@ const SustainabilityPage: React.FC = () => {
{t('sustainability:financial.roi', 'ROI de IA')}
- €{(metrics.avoided_waste.waste_avoided_kg * metrics.financial_impact.cost_per_kg).toFixed(2)}
+ {currencySymbol}{(metrics.avoided_waste.waste_avoided_kg * metrics.financial_impact.cost_per_kg).toFixed(2)}
{t('sustainability:financial.ai_savings', 'Ahorrado por IA')}
diff --git a/frontend/src/pages/app/operations/orders/OrdersPage.tsx b/frontend/src/pages/app/operations/orders/OrdersPage.tsx
index 6c1f5b5d..ea5925c4 100644
--- a/frontend/src/pages/app/operations/orders/OrdersPage.tsx
+++ b/frontend/src/pages/app/operations/orders/OrdersPage.tsx
@@ -26,6 +26,7 @@ import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useAuthUser } from '../../../../stores/auth.store';
import { OrderFormModal } from '../../../../components/domain/orders';
import { useTranslation } from 'react-i18next';
+import { useTenantCurrency } from '../../../../hooks/useTenantCurrency';
const OrdersPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'orders' | 'customers'>('orders');
@@ -44,6 +45,7 @@ const OrdersPage: React.FC = () => {
const user = useAuthUser();
const tenantId = currentTenant?.id || user?.tenant_id || '';
const { t } = useTranslation(['orders', 'common']);
+ const { currencySymbol } = useTenantCurrency();
// API hooks for orders
const {
@@ -374,7 +376,7 @@ const OrdersPage: React.FC = () => {
primaryValueLabel="artículos"
secondaryInfo={{
label: 'Total',
- value: `€${formatters.compact(order.total_amount)}`
+ value: `${currencySymbol}${formatters.compact(order.total_amount)}`
}}
metadata={[
`Pedido: ${new Date(order.order_date).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' })}`,
@@ -422,7 +424,7 @@ const OrdersPage: React.FC = () => {
primaryValueLabel="pedidos"
secondaryInfo={{
label: 'Total',
- value: `€${formatters.compact(customer.total_spent || 0)}`
+ value: `${currencySymbol}${formatters.compact(customer.total_spent || 0)}`
}}
metadata={[
`${customer.customer_code}`,
diff --git a/frontend/src/pages/app/operations/pos/POSPage.tsx b/frontend/src/pages/app/operations/pos/POSPage.tsx
index 4a053d64..a34fbbe8 100644
--- a/frontend/src/pages/app/operations/pos/POSPage.tsx
+++ b/frontend/src/pages/app/operations/pos/POSPage.tsx
@@ -6,6 +6,7 @@ import { LoadingSpinner } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { useIngredients } from '../../../../api/hooks/inventory';
import { useTenantId } from '../../../../hooks/useTenantId';
+import { useTenantCurrency } from '../../../../hooks/useTenantCurrency';
import { ProductType, ProductCategory, IngredientResponse } from '../../../../api/types/inventory';
import { showToast } from '../../../../utils/toast';
import { usePOSConfigurationData, usePOSConfigurationManager, usePOSTransactions, usePOSTransactionsDashboard, usePOSTransaction } from '../../../../api/hooks/pos';
@@ -548,7 +549,7 @@ const POSPage: React.FC = () => {
const [testingConnection, setTestingConnection] = useState(null);
const tenantId = useTenantId();
-
+ const { currencySymbol } = useTenantCurrency();
// POS Configuration hooks
const posData = usePOSConfigurationData(tenantId);
@@ -780,7 +781,7 @@ const POSPage: React.FC = () => {
}
setCart([]);
- showToast.success(`Venta procesada exitosamente: €${total.toFixed(2)}`);
+ showToast.success(`Venta procesada exitosamente: ${currencySymbol}${total.toFixed(2)}`);
} catch (error: any) {
console.error('Error processing payment:', error);
showToast.error(error.response?.data?.detail || 'Error al procesar la venta');
diff --git a/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx b/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx
index 7699e3d2..ab0a70b5 100644
--- a/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx
+++ b/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx
@@ -18,8 +18,11 @@ import type { PurchaseOrderStatus, PurchaseOrderPriority, PurchaseOrderDetail }
import { useTenantStore } from '../../../../stores/tenant.store';
import { useUserById } from '../../../../api/hooks/user';
import { showToast } from '../../../../utils/toast';
+import { useTenantCurrency } from '../../../../hooks/useTenantCurrency';
const ProcurementPage: React.FC = () => {
+ const { currencySymbol } = useTenantCurrency();
+
// State
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('');
@@ -500,7 +503,7 @@ const ProcurementPage: React.FC = () => {
title={String(po.po_number || 'Sin número')}
subtitle={String(po.supplier_name || po.supplier?.name || 'Proveedor desconocido')}
statusIndicator={statusConfig}
- primaryValue={`€${totalAmount}`}
+ primaryValue={`${currencySymbol}${totalAmount}`}
primaryValueLabel="Total"
metadata={[
`Prioridad: ${priorityText}`,
diff --git a/frontend/src/pages/app/operations/recipes/RecipesPage.tsx b/frontend/src/pages/app/operations/recipes/RecipesPage.tsx
index d1c1105f..0baa7082 100644
--- a/frontend/src/pages/app/operations/recipes/RecipesPage.tsx
+++ b/frontend/src/pages/app/operations/recipes/RecipesPage.tsx
@@ -7,6 +7,7 @@ import { PageHeader } from '../../../../components/layout';
import { useRecipes, useCreateRecipe, useUpdateRecipe, useDeleteRecipe, useArchiveRecipe } from '../../../../api/hooks/recipes';
import { recipesService } from '../../../../api/services/recipes';
import { useCurrentTenant } from '../../../../stores/tenant.store';
+import { useTenantCurrency } from '../../../../hooks/useTenantCurrency';
import type { RecipeResponse, RecipeCreate } from '../../../../api/types/recipes';
import { MeasurementUnit } from '../../../../api/types/recipes';
import { useIngredients } from '../../../../api/hooks/inventory';
@@ -273,6 +274,7 @@ const RecipesPage: React.FC = () => {
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
+ const { currencySymbol } = useTenantCurrency();
const queryClient = useQueryClient();
// Mutations
@@ -1520,7 +1522,7 @@ const RecipesPage: React.FC = () => {
primaryValueLabel="ingredientes"
secondaryInfo={{
label: 'Margen',
- value: `€${formatters.compact(price - cost)}`
+ value: `${currencySymbol}${formatters.compact(price - cost)}`
}}
progress={{
label: 'Margen de beneficio',
diff --git a/frontend/src/pages/app/operations/suppliers/SuppliersPage.tsx b/frontend/src/pages/app/operations/suppliers/SuppliersPage.tsx
index c65bf289..26a3b54d 100644
--- a/frontend/src/pages/app/operations/suppliers/SuppliersPage.tsx
+++ b/frontend/src/pages/app/operations/suppliers/SuppliersPage.tsx
@@ -8,6 +8,7 @@ import { useSuppliers, useSupplierStatistics, useCreateSupplier, useUpdateSuppli
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useAuthUser } from '../../../../stores/auth.store';
import { useTranslation } from 'react-i18next';
+import { useTenantCurrency } from '../../../../hooks/useTenantCurrency';
import { statusColors } from '../../../../styles/colors';
import { DeleteSupplierModal, SupplierPriceListViewModal, PriceListModal } from '../../../../components/domain/suppliers';
import { useQueryClient } from '@tanstack/react-query';
@@ -35,6 +36,7 @@ const SuppliersPage: React.FC = () => {
const currentTenant = useCurrentTenant();
const user = useAuthUser();
const tenantId = currentTenant?.id || user?.tenant_id || '';
+ const { currencySymbol } = useTenantCurrency();
// API hooks
const {
@@ -299,7 +301,7 @@ const SuppliersPage: React.FC = () => {
primaryValueLabel="días entrega"
secondaryInfo={{
label: 'Pedido Min.',
- value: `€${formatters.compact(supplier.minimum_order_amount || 0)}`
+ value: `${currencySymbol}${formatters.compact(supplier.minimum_order_amount || 0)}`
}}
metadata={[
supplier.contact_person || 'Sin contacto',
diff --git a/frontend/src/pages/app/settings/bakery/BakerySettingsPage.tsx b/frontend/src/pages/app/settings/bakery/BakerySettingsPage.tsx
index 620cf71c..087c9d97 100644
--- a/frontend/src/pages/app/settings/bakery/BakerySettingsPage.tsx
+++ b/frontend/src/pages/app/settings/bakery/BakerySettingsPage.tsx
@@ -124,9 +124,9 @@ const BakerySettingsPage: React.FC = () => {
postalCode: currentTenant.postal_code || '',
country: currentTenant.country || '',
taxId: '',
- currency: 'EUR',
- timezone: 'Europe/Madrid',
- language: 'es'
+ currency: currentTenant.currency || 'EUR',
+ timezone: currentTenant.timezone || 'Europe/Madrid',
+ language: currentTenant.language || 'es'
});
setHasUnsavedChanges(false);
}
@@ -203,7 +203,11 @@ const BakerySettingsPage: React.FC = () => {
address: config.address,
city: config.city,
postal_code: config.postalCode,
- country: config.country
+ country: config.country,
+ // Regional/Localization settings
+ currency: config.currency,
+ timezone: config.timezone,
+ language: config.language
};
const updatedTenant = await updateTenantMutation.mutateAsync({
@@ -308,9 +312,9 @@ const BakerySettingsPage: React.FC = () => {
postalCode: currentTenant.postal_code || '',
country: currentTenant.country || '',
taxId: '',
- currency: 'EUR',
- timezone: 'Europe/Madrid',
- language: 'es'
+ currency: currentTenant.currency || 'EUR',
+ timezone: currentTenant.timezone || 'Europe/Madrid',
+ language: currentTenant.language || 'es'
});
}
if (settings) {
diff --git a/frontend/src/utils/currency.ts b/frontend/src/utils/currency.ts
index 07567e17..fc35d9fa 100644
--- a/frontend/src/utils/currency.ts
+++ b/frontend/src/utils/currency.ts
@@ -27,7 +27,15 @@ export const CURRENCY_CONFIG = {
},
} as const;
-type CurrencyCode = keyof typeof CURRENCY_CONFIG;
+export type CurrencyCode = keyof typeof CURRENCY_CONFIG;
+
+// Default currency for the application (Euro)
+export const DEFAULT_CURRENCY: CurrencyCode = 'EUR';
+
+// Get currency symbol
+export const getCurrencySymbol = (currencyCode: CurrencyCode = DEFAULT_CURRENCY): string => {
+ return CURRENCY_CONFIG[currencyCode]?.symbol || '€';
+};
// Format currency amount
export const formatCurrency = (
diff --git a/frontend/src/utils/smartActionHandlers.ts b/frontend/src/utils/smartActionHandlers.ts
index a7b9a811..d74f5846 100644
--- a/frontend/src/utils/smartActionHandlers.ts
+++ b/frontend/src/utils/smartActionHandlers.ts
@@ -7,6 +7,8 @@
import { useNavigate } from 'react-router-dom';
import { SmartAction as ImportedSmartAction, SmartActionType } from '../api/types/events';
+import { useTenantStore } from '../stores/tenant.store';
+import { getTenantCurrencySymbol } from '../hooks/useTenantCurrency';
// ============================================================
// Types (using imported types from events.ts)
@@ -168,7 +170,7 @@ export class SmartActionHandler {
body: JSON.stringify({
action: 'approve',
approved_by: 'current_user',
- notes: `Approved via alert action${amount ? ` (€${amount})` : ''}`,
+ notes: `Approved via alert action${amount ? ` (${getTenantCurrencySymbol(useTenantStore.getState().currentTenant?.currency)}${amount})` : ''}`,
}),
}
);
diff --git a/frontend/src/utils/validation.ts b/frontend/src/utils/validation.ts
index b9af5f65..4132197b 100644
--- a/frontend/src/utils/validation.ts
+++ b/frontend/src/utils/validation.ts
@@ -2,14 +2,49 @@
* Validation utilities for forms and data
*/
-// Email validation
+import { z } from 'zod';
+
+// =============================================================================
+// ZOD SCHEMAS - Centralized validation schemas for consistent validation
+// =============================================================================
+
+/**
+ * Email validation schema using Zod
+ * - Uses Zod's built-in email validator (RFC 5322 compliant)
+ * - Trims whitespace before validation
+ * - Provides consistent error messages in Spanish
+ */
+export const emailSchema = z
+ .string()
+ .trim()
+ .min(1, 'El email es requerido')
+ .email('Por favor, ingrese un email válido');
+
+/**
+ * Validates an email string using the centralized Zod schema
+ * @param email - The email string to validate
+ * @returns Object with isValid boolean and optional error message
+ */
+export const validateEmail = (email: string): { isValid: boolean; error?: string } => {
+ const result = emailSchema.safeParse(email);
+ if (result.success) {
+ return { isValid: true };
+ }
+ return { isValid: false, error: result.error.errors[0]?.message };
+};
+
+// =============================================================================
+// LEGACY VALIDATION FUNCTIONS - Kept for backward compatibility
+// =============================================================================
+
+// Email validation (legacy - use validateEmail or emailSchema instead)
export const isValidEmail = (email: string): boolean => {
if (!email || typeof email !== 'string') {
return false;
}
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
- return emailRegex.test(email.trim());
+ const result = emailSchema.safeParse(email);
+ return result.success;
};
// Spanish phone number validation
diff --git a/services/auth/app/services/user_service.py b/services/auth/app/services/user_service.py
index e33fb84f..d748d968 100644
--- a/services/auth/app/services/user_service.py
+++ b/services/auth/app/services/user_service.py
@@ -449,7 +449,41 @@ class EnhancedUserService:
new_role=new_role,
error=str(e))
raise DatabaseError(f"Failed to update role: {str(e)}")
-
+
+ async def update_user_field(
+ self,
+ user_id: str,
+ field_name: str,
+ field_value: Any
+ ) -> bool:
+ """Update a single field on a user record"""
+ try:
+ async with self.database_manager.get_session() as session:
+ user_repo = UserRepository(User, session)
+
+ # Update the specific field
+ updated_user = await user_repo.update(user_id, {field_name: field_value})
+ if not updated_user:
+ logger.error("User not found for field update",
+ user_id=user_id,
+ field_name=field_name)
+ return False
+
+ await session.commit()
+
+ logger.info("User field updated",
+ user_id=user_id,
+ field_name=field_name)
+
+ return True
+
+ except Exception as e:
+ logger.error("Failed to update user field",
+ user_id=user_id,
+ field_name=field_name,
+ error=str(e))
+ return False
+
async def get_user_activity(self, user_id: str) -> Dict[str, Any]:
"""Get user activity information using repository pattern"""
try:
diff --git a/services/tenant/app/models/tenants.py b/services/tenant/app/models/tenants.py
index 67bb6da4..fc2c439c 100644
--- a/services/tenant/app/models/tenants.py
+++ b/services/tenant/app/models/tenants.py
@@ -29,8 +29,10 @@ class Tenant(Base):
latitude = Column(Float)
longitude = Column(Float)
- # Timezone configuration for accurate scheduling
+ # Regional/Localization configuration
timezone = Column(String(50), default="Europe/Madrid", nullable=False)
+ currency = Column(String(3), default="EUR", nullable=False) # Currency code: EUR, USD, GBP
+ language = Column(String(5), default="es", nullable=False) # Language code: es, en, eu
# Contact info
phone = Column(String(20))
diff --git a/services/tenant/app/schemas/tenants.py b/services/tenant/app/schemas/tenants.py
index 659927a5..4078f921 100644
--- a/services/tenant/app/schemas/tenants.py
+++ b/services/tenant/app/schemas/tenants.py
@@ -68,6 +68,10 @@ class TenantResponse(BaseModel):
address: str
city: str
postal_code: str
+ # Regional/Localization settings
+ timezone: Optional[str] = "Europe/Madrid"
+ currency: Optional[str] = "EUR" # Currency code: EUR, USD, GBP
+ language: Optional[str] = "es" # Language code: es, en, eu
phone: Optional[str]
is_active: bool
subscription_plan: Optional[str] = None # Populated from subscription relationship or service
@@ -125,6 +129,10 @@ class TenantUpdate(BaseModel):
phone: Optional[str] = None
business_type: Optional[str] = None
business_model: Optional[str] = None
+ # Regional/Localization settings
+ timezone: Optional[str] = None
+ currency: Optional[str] = Field(None, pattern=r'^(EUR|USD|GBP)$') # Currency code
+ language: Optional[str] = Field(None, pattern=r'^(es|en|eu)$') # Language code
class TenantListResponse(BaseModel):
"""Response schema for listing tenants"""
diff --git a/services/tenant/migrations/versions/001_unified_initial_schema.py b/services/tenant/migrations/versions/001_unified_initial_schema.py
index d55ac289..6632b0e3 100644
--- a/services/tenant/migrations/versions/001_unified_initial_schema.py
+++ b/services/tenant/migrations/versions/001_unified_initial_schema.py
@@ -68,7 +68,10 @@ def upgrade() -> None:
sa.Column('postal_code', sa.String(length=10), nullable=False),
sa.Column('latitude', sa.Float(), nullable=True),
sa.Column('longitude', sa.Float(), nullable=True),
- sa.Column('timezone', sa.String(length=50), nullable=False),
+ # Regional/Localization configuration
+ sa.Column('timezone', sa.String(length=50), nullable=False, server_default='Europe/Madrid'),
+ sa.Column('currency', sa.String(length=3), nullable=False, server_default='EUR'), # Currency code: EUR, USD, GBP
+ sa.Column('language', sa.String(length=5), nullable=False, server_default='es'), # Language code: es, en, eu
sa.Column('phone', sa.String(length=20), nullable=True),
sa.Column('email', sa.String(length=255), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
diff --git a/services/training/app/services/training_service.py b/services/training/app/services/training_service.py
index 71fe6be0..58e706be 100644
--- a/services/training/app/services/training_service.py
+++ b/services/training/app/services/training_service.py
@@ -364,6 +364,10 @@ class EnhancedTrainingService:
job_id, results=json_safe_result
)
+ # CRITICAL: Commit the session to persist the completed status to database
+ # Without this commit, the status update is lost when the session closes
+ await session.commit()
+
logger.info("Enhanced training job completed successfully",
job_id=job_id,
models_created=len(stored_models))
@@ -380,7 +384,10 @@ class EnhancedTrainingService:
await self.training_log_repo.complete_training_log(
job_id, error_message=str(e)
)
-
+
+ # Commit the failure status to database
+ await session.commit()
+
error_result = {
"job_id": job_id,
"tenant_id": tenant_id,