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 = (
= ({ 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 = ({
= ({
= ({ 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 = ({ {finishedProducts.map(product => ( ))} @@ -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 = ({
; @@ -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 = ({
; @@ -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 }) => {products.map((product) => ( ))} @@ -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 }) => <> + + - - - - + - + + + + ) : ( <> - - + + + - + + + + + )} @@ -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 */}
= ({ 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 }) => { {ingredientsData.map((product: any) => ( ))} @@ -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 {products.map((product: any) => ( ))} @@ -383,7 +385,7 @@ const ManualEntryStep: React.FC = ({ dataRef, onDataChange, onN />
- €{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 + * + * + * // 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,