Improve UI

This commit is contained in:
Urtzi Alfaro
2025-12-30 14:40:20 +01:00
parent e494ea8635
commit c07df124fb
71 changed files with 647 additions and 265 deletions

View File

@@ -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,
},
},
},

View File

@@ -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') {

View File

@@ -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;

View File

@@ -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<PerformanceChartProps> = ({
onOutletClick
}) => {
const { t } = useTranslation('dashboard');
const { currencySymbol } = useTenantCurrency();
// Get metric info
const getMetricInfo = () => {
@@ -38,14 +40,14 @@ const PerformanceChart: React.FC<PerformanceChartProps> = ({
return {
icon: <TrendingUp className="w-4 h-4" />,
label: t('enterprise.metrics.sales'),
unit: '€',
unit: currencySymbol,
format: (val: number) => val.toLocaleString('es-ES', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
};
case 'inventory_value':
return {
icon: <Package className="w-4 h-4" />,
label: t('enterprise.metrics.inventory_value'),
unit: '€',
unit: currencySymbol,
format: (val: number) => val.toLocaleString('es-ES', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
};
case 'order_frequency':

View File

@@ -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<DistributionTabProps> = ({ tenantId, selectedDate, onDateChange }) => {
const { t } = useTranslation('dashboard');
const { currencySymbol } = useTenantCurrency();
// Get distribution data
const {
@@ -317,7 +319,7 @@ const DistributionTab: React.FC<DistributionTabProps> = ({ tenantId, selectedDat
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-info)]">
{optimizationMetrics.fuelSaved.toFixed(2)}
{currencySymbol}{optimizationMetrics.fuelSaved.toFixed(2)}
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('enterprise.estimated_fuel_savings')}

View File

@@ -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<NetworkPerformanceTabProps> = ({ 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<NetworkPerformanceTabProps> = ({ tenantId,
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-success)]">
{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()}
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
@@ -266,8 +268,8 @@ const NetworkPerformanceTab: React.FC<NetworkPerformanceTabProps> = ({ 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<NetworkPerformanceTabProps> = ({ 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<NetworkPerformanceTabProps> = ({ 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') :

View File

@@ -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<PerformanceChartProps> = ({ data, metric, period }) => {
const { t } = useTranslation('dashboard');
const { currencySymbol } = useTenantCurrency();
// Prepare chart data
const chartData = {
@@ -76,7 +78,7 @@ export const PerformanceChart: React.FC<PerformanceChartProps> = ({ 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<PerformanceChartProps> = ({ data, metric
<td className="px-3 py-2">{item.rank}</td>
<td className="px-3 py-2 font-medium">{item.anonymized_name}</td>
<td className="px-3 py-2 text-right">
{metric === 'sales' ? `${item.metric_value.toFixed(2)}` : item.metric_value}
{metric === 'sales' ? `${currencySymbol}${item.metric_value.toFixed(2)}` : item.metric_value}
</td>
</tr>
))}

View File

@@ -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<string | null>(null);
const [processingId, setProcessingId] = useState<string | null>(null);
@@ -288,7 +290,7 @@ export function PendingPurchasesBlock({
</p>
<p className="text-lg font-bold text-[var(--text-primary)]">
{(po.total_amount || po.total || 0).toLocaleString(undefined, {
{currencySymbol}{(po.total_amount || po.total || 0).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}

View File

@@ -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')}
</div>
<div className="text-xl font-bold text-[var(--color-success-600)]">
{orchestrationSummary.estimatedSavingsEur.toLocaleString()}
{currencySymbol}{orchestrationSummary.estimatedSavingsEur.toLocaleString()}
</div>
</div>
)}
@@ -266,7 +268,7 @@ export function SystemStatusBlock({ data, loading }: SystemStatusBlockProps) {
</p>
{issue.business_impact?.financial_impact_eur && (
<p className="text-xs text-[var(--text-secondary)]">
{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()}
</p>
)}
</div>

View File

@@ -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<AnalyticsDashboardProps> = ({
onMetricsLoad,
onExport,
}) => {
const { currencySymbol } = useTenantCurrency();
const [selectedTimeRange, setSelectedTimeRange] = useState<TimeRange>(initialTimeRange);
const [customDateRange, setCustomDateRange] = useState<{ from: Date; to: Date } | null>(null);
const [bakeryMetrics, setBakeryMetrics] = useState<BakeryMetrics | null>(null);
@@ -319,7 +321,7 @@ export const AnalyticsDashboard: React.FC<AnalyticsDashboardProps> = ({
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{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<AnalyticsDashboardProps> = ({
{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<AnalyticsDashboardProps> = ({
{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<AnalyticsDashboardProps> = ({
</div>
<div className="text-right">
<p className="font-semibold text-[var(--color-success)]">
{channel.revenue.toLocaleString()}
{currencySymbol}{channel.revenue.toLocaleString()}
</p>
<p className="text-sm text-[var(--text-secondary)]">
Conv. {channel.conversion_rate.toFixed(1)}%
@@ -390,7 +392,7 @@ export const AnalyticsDashboard: React.FC<AnalyticsDashboardProps> = ({
</div>
<div className="text-right">
<p className="font-semibold text-[var(--color-success)]">
{product.revenue.toLocaleString()}
{currencySymbol}{product.revenue.toLocaleString()}
</p>
<p className="text-sm text-[var(--text-secondary)]">
Margen {product.profit_margin.toFixed(1)}%
@@ -444,7 +446,7 @@ export const AnalyticsDashboard: React.FC<AnalyticsDashboardProps> = ({
</div>
<div className="text-center">
<p className="text-2xl font-bold text-pink-600">
{bakeryMetrics.customer.customer_lifetime_value.toFixed(0)}
{currencySymbol}{bakeryMetrics.customer.customer_lifetime_value.toFixed(0)}
</p>
<p className="text-sm text-[var(--text-secondary)]">Valor Cliente</p>
</div>

View File

@@ -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<LoginFormProps> = ({
const validateForm = (): boolean => {
const newErrors: Partial<LoginCredentials> = {};
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) {

View File

@@ -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<RegisterFormProps> = ({
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<RegisterFormProps> = ({
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<RegisterFormProps> = ({
}
/>
<Input
type="email"
label={t('auth:register.email', 'Correo Electrónico')}
placeholder="tu.email@ejemplo.com"
value={formData.email}
onChange={handleInputChange('email')}
error={errors.email}
disabled={isLoading}
required
autoComplete="email"
leftIcon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207" />
</svg>
}
/>
<div className="relative">
<Input
type="email"
label={t('auth:register.email', 'Correo Electrónico')}
placeholder="tu.email@ejemplo.com"
value={formData.email}
onChange={handleInputChange('email')}
error={errors.email}
disabled={isLoading}
required
autoComplete="email"
className={
emailValidationStatus === 'valid' && formData.email
? 'border-color-success focus:border-color-success ring-color-success'
: emailValidationStatus === 'invalid' && formData.email
? 'border-color-error focus:border-color-error ring-color-error'
: ''
}
leftIcon={
emailValidationStatus === 'valid' && formData.email ? (
<svg className="w-5 h-5 text-color-success" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
) : emailValidationStatus === 'invalid' && formData.email ? (
<svg className="w-5 h-5 text-color-error" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
) : (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207" />
</svg>
)
}
/>
{/* Email Validation Status Message */}
{formData.email && (
<div className="mt-2 transition-all duration-300 ease-in-out">
{emailValidationStatus === 'valid' ? (
<div className="flex items-center space-x-2 text-color-success animate-fade-in">
<div className="flex-shrink-0 w-5 h-5 rounded-full bg-color-success/10 flex items-center justify-center">
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
<span className="text-sm font-medium">{t('auth:validation.email_valid', 'Email válido')}</span>
</div>
) : (
<div className="flex items-center space-x-2 text-color-error animate-fade-in">
<div className="flex-shrink-0 w-5 h-5 rounded-full bg-color-error/10 flex items-center justify-center">
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</div>
<span className="text-sm font-medium">{t('auth:validation.email_invalid', 'Por favor, ingrese un email válido')}</span>
</div>
)}
</div>
)}
</div>
<Input
type={showPassword ? 'text' : 'password'}
@@ -630,7 +683,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
<div className="flex justify-between items-center pt-3 border-t border-blue-200 dark:border-blue-800">
<span className="text-green-700 dark:text-green-400 font-medium">{t('auth:payment.trial_period', 'Período de prueba:')}</span>
<span className="font-bold text-green-700 dark:text-green-400">
{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')}
</span>
</div>
)}
@@ -642,7 +695,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
<p className="text-xs text-text-tertiary mt-2 text-center">
{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')
}
</p>
</div>

View File

@@ -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:')}
</span>{' '}
<span className="font-bold" style={{ color: 'var(--text-primary)' }}>
{financialImpactEur.toFixed(2)}
{currencySymbol}{financialImpactEur.toFixed(2)}
</span>
</div>
)}

View File

@@ -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<PendingPOApprovalsProps> = ({
const { t } = useTranslation(['dashboard']);
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
const { currencySymbol } = useTenantCurrency();
const [approvingPO, setApprovingPO] = useState<string | null>(null);
const [rejectingPO, setRejectingPO] = useState<string | null>(null);
@@ -145,10 +147,7 @@ const PendingPOApprovals: React.FC<PendingPOApprovalsProps> = ({
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) => {

View File

@@ -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<MaintenanceHistoryModalProps> = (
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<MaintenanceHistoryModalProps> = (
</div>
<div>
<span className="text-[var(--text-tertiary)]">{t('common:actions.cost', 'Coste')}:</span>
<span className="ml-1 font-medium">{record.cost.toFixed(2)}</span>
<span className="ml-1 font-medium">{currencySymbol}{record.cost.toFixed(2)}</span>
</div>
<div>
<span className="text-[var(--text-tertiary)]">{t('fields.downtime', 'Parada')}:</span>

View File

@@ -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<BatchAddIngredientsModalProps> =
}) => {
const { t } = useTranslation();
const createIngredient = useCreateIngredient();
const { currencySymbol } = useTenantCurrency();
const [rows, setRows] = useState<BatchIngredientRow[]>([
{ id: '1', name: '', category: 'Baking Ingredients', unit_of_measure: 'kg' },
@@ -269,7 +271,7 @@ export const BatchAddIngredientsModal: React.FC<BatchAddIngredientsModalProps> =
<th className="px-3 py-2 text-left text-xs font-semibold text-[var(--text-primary)]">Categoría *</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-[var(--text-primary)]">Unidad *</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-[var(--text-primary)]">Stock Inicial</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-[var(--text-primary)]">Costo ()</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-[var(--text-primary)]">Costo ({currencySymbol})</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-[var(--text-primary)] w-12"></th>
</tr>
</thead>

View File

@@ -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<QuickAddIngredientModalProps> = (
}) => {
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<QuickAddIngredientModalProps> = (
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
Costo por Unidad ()
Costo por Unidad ({currencySymbol})
</label>
<input
type="number"

View File

@@ -2,6 +2,7 @@ import React, { useState, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '../../../ui/Button';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useTenantCurrency } from '../../../../hooks/useTenantCurrency';
import { useCreateIngredient, useClassifyBatch, useAddStock } from '../../../../api/hooks/inventory';
import { useValidateImportFile, useImportSalesData } from '../../../../api/hooks/sales';
import { useSuppliers } from '../../../../api/hooks/suppliers';
@@ -54,6 +55,7 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
isFirstStep
}) => {
const { t } = useTranslation();
const { currencySymbol } = useTenantCurrency();
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [isValidating, setIsValidating] = useState(false);
const [validationResult, setValidationResult] = useState<ImportValidationResponse | null>(null);
@@ -658,7 +660,7 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
</p>
<div className="mt-2 flex items-center gap-4 text-xs text-[var(--text-secondary)]">
<span>Stock: {item.stock_quantity} {item.unit_of_measure}</span>
<span>Costo: {item.cost_per_unit.toFixed(2)}/{item.unit_of_measure}</span>
<span>Costo: {currencySymbol}{item.cost_per_unit.toFixed(2)}/{item.unit_of_measure}</span>
<span>Caducidad: {item.estimated_shelf_life_days} días</span>
</div>
{item.sales_data && (
@@ -962,7 +964,7 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Costo por Unidad ()
Costo por Unidad ({currencySymbol})
</label>
<input
type="number"
@@ -1170,7 +1172,7 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Costo por Unidad ()
Costo por Unidad ({currencySymbol})
</label>
<input
type="number"

View File

@@ -30,6 +30,7 @@ import { ProductType, ProductCategory } from '../../../api/types/inventory';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { useAuthUser } from '../../../stores/auth.store';
import { useTranslation } from 'react-i18next';
import { useTenantCurrency } from '../../../hooks/useTenantCurrency';
interface OrderFormModalProps {
isOpen: boolean;
@@ -47,6 +48,7 @@ export const OrderFormModal: React.FC<OrderFormModalProps> = ({
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<OrderFormModalProps> = ({
<option value="">Seleccionar producto...</option>
{finishedProducts.map(product => (
<option key={product.id} value={product.id}>
{product.name} - {(product.average_cost || product.standard_cost || 0).toFixed(2)}
{product.name} - {currencySymbol}{(product.average_cost || product.standard_cost || 0).toFixed(2)}
</option>
))}
</select>
@@ -362,7 +364,7 @@ export const OrderFormModal: React.FC<OrderFormModalProps> = ({
<div className="flex-1">
<h4 className="font-medium text-[var(--text-primary)]">{item.product_name}</h4>
<p className="text-sm text-[var(--text-secondary)]">
{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)}
</p>
</div>
<div className="flex items-center gap-2">

View File

@@ -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<POSCartProps> = ({
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<POSCartProps> = ({
</h4>
<div className="flex items-baseline gap-2 mt-1">
<span className="text-sm font-medium text-[var(--color-primary)]">
{item.price.toFixed(2)}
{currencySymbol}{item.price.toFixed(2)}
</span>
<span className="text-xs text-[var(--text-tertiary)]">c/u</span>
</div>
@@ -128,7 +131,7 @@ export const POSCart: React.FC<POSCartProps> = ({
{/* Item Subtotal */}
<div className="text-right">
<p className="text-base font-bold text-[var(--text-primary)]">
{(item.price * item.quantity).toFixed(2)}
{currencySymbol}{(item.price * item.quantity).toFixed(2)}
</p>
</div>
</div>
@@ -145,7 +148,7 @@ export const POSCart: React.FC<POSCartProps> = ({
<div className="flex justify-between items-center text-sm">
<span className="text-[var(--text-secondary)]">Subtotal:</span>
<span className="font-semibold text-[var(--text-primary)]">
{subtotal.toFixed(2)}
{currencySymbol}{subtotal.toFixed(2)}
</span>
</div>
@@ -153,7 +156,7 @@ export const POSCart: React.FC<POSCartProps> = ({
<div className="flex justify-between items-center text-sm">
<span className="text-[var(--text-secondary)]">IVA ({(taxRate * 100).toFixed(0)}%):</span>
<span className="font-semibold text-[var(--text-primary)]">
{tax.toFixed(2)}
{currencySymbol}{tax.toFixed(2)}
</span>
</div>
@@ -163,7 +166,7 @@ export const POSCart: React.FC<POSCartProps> = ({
<div className="flex justify-between items-center">
<span className="text-lg font-bold text-[var(--text-primary)]">TOTAL:</span>
<span className="text-2xl font-bold text-[var(--color-primary)]">
{total.toFixed(2)}
{currencySymbol}{total.toFixed(2)}
</span>
</div>
</div>

View File

@@ -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<POSPaymentProps> = ({
onProcessPayment,
disabled = false,
}) => {
const { currencySymbol } = useTenantCurrency();
const [paymentMethod, setPaymentMethod] = useState<'cash' | 'card' | 'transfer'>('cash');
const [cashReceived, setCashReceived] = useState('');
const [customerInfo, setCustomerInfo] = useState<CustomerInfo>({
@@ -193,7 +195,7 @@ export const POSPayment: React.FC<POSPaymentProps> = ({
<Input
type="number"
step="0.01"
placeholder="€0.00"
placeholder={`${currencySymbol}0.00`}
value={cashReceived}
onChange={(e) => setCashReceived(e.target.value)}
className="text-lg font-semibold"
@@ -214,7 +216,7 @@ export const POSPayment: React.FC<POSPaymentProps> = ({
Cambio:
</span>
<span className="text-2xl font-bold" style={{ color: 'var(--color-success-dark)' }}>
{change.toFixed(2)}
{currencySymbol}{change.toFixed(2)}
</span>
</div>
</Card>
@@ -230,7 +232,7 @@ export const POSPayment: React.FC<POSPaymentProps> = ({
}}
>
<p className="text-sm font-medium text-center" style={{ color: 'var(--color-warning-dark)' }}>
Efectivo insuficiente: falta {(total - parseFloat(cashReceived)).toFixed(2)}
Efectivo insuficiente: falta {currencySymbol}{(total - parseFloat(cashReceived)).toFixed(2)}
</p>
</Card>
)}
@@ -247,7 +249,7 @@ export const POSPayment: React.FC<POSPaymentProps> = ({
className="w-full text-lg font-bold py-6 shadow-lg hover:shadow-xl transition-all"
>
<Receipt className="w-6 h-6 mr-2" />
Procesar Venta - {total.toFixed(2)}
Procesar Venta - {currencySymbol}{total.toFixed(2)}
</Button>
</div>
);

View File

@@ -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<POSProductCardProps> = ({
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<POSProductCardProps> = ({
{/* Price - Large and prominent */}
<div className="flex items-baseline gap-2">
<span className="text-2xl sm:text-3xl font-bold text-[var(--color-primary)]">
{price.toFixed(2)}
{currencySymbol}{price.toFixed(2)}
</span>
<span className="text-sm text-[var(--text-tertiary)]">c/u</span>
</div>

View File

@@ -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<CreatePurchaseOrderModalProps> =
requirements,
onSuccess
}) => {
const { currencySymbol } = useTenantCurrency();
const [loading, setLoading] = useState(false);
const [selectedSupplier, setSelectedSupplier] = useState<string>('');
const [formData, setFormData] = useState<Record<string, any>>({});
@@ -317,7 +319,7 @@ export const CreatePurchaseOrderModal: React.FC<CreatePurchaseOrderModalProps> =
},
{
name: 'unit_price',
label: 'Precio Est. (€)',
label: `Precio Est. (${currencySymbol})`,
type: 'currency',
required: true
}
@@ -362,7 +364,7 @@ export const CreatePurchaseOrderModal: React.FC<CreatePurchaseOrderModalProps> =
},
{
name: 'unit_price',
label: 'Precio Unitario (€)',
label: `Precio Unitario (${currencySymbol})`,
type: 'currency',
required: true,
defaultValue: 0,

View File

@@ -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<ModifyPurchaseOrderModalProps> =
poId,
onSuccess
}) => {
const { currencySymbol } = useTenantCurrency();
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState<Record<string, any>>({});
@@ -228,7 +230,7 @@ export const ModifyPurchaseOrderModal: React.FC<ModifyPurchaseOrderModalProps> =
},
{
name: 'unit_price',
label: 'Precio Unitario (€)',
label: `Precio Unitario (${currencySymbol})`,
type: 'currency',
required: true,
placeholder: '0.00',

View File

@@ -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<UnifiedPurchaseOrderModalProps>
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<UnifiedPurchaseOrderModalProps>
</div>
<div className="text-right">
<p className="font-bold text-lg text-[var(--color-primary-600)]">
{itemTotal.toFixed(2)}
{currencySymbol}{itemTotal.toFixed(2)}
</p>
</div>
</div>
@@ -178,7 +180,7 @@ export const UnifiedPurchaseOrderModal: React.FC<UnifiedPurchaseOrderModalProps>
</div>
<div>
<p className="text-[var(--text-secondary)]">{t('unit_price')}</p>
<p className="font-medium text-[var(--text-primary)]">{unitPrice.toFixed(2)}</p>
<p className="font-medium text-[var(--text-primary)]">{currencySymbol}{unitPrice.toFixed(2)}</p>
</div>
</div>
{item.quality_requirements && (
@@ -198,7 +200,7 @@ export const UnifiedPurchaseOrderModal: React.FC<UnifiedPurchaseOrderModalProps>
})}
<div className="flex justify-between items-center pt-4 border-t-2 border-[var(--border-primary)]">
<span className="font-semibold text-lg text-[var(--text-primary)]">{t('total')}</span>
<span className="font-bold text-2xl text-[var(--color-primary-600)]">{totalAmount.toFixed(2)}</span>
<span className="font-bold text-2xl text-[var(--color-primary-600)]">{currencySymbol}{totalAmount.toFixed(2)}</span>
</div>
</div>
);
@@ -296,22 +298,22 @@ export const UnifiedPurchaseOrderModal: React.FC<UnifiedPurchaseOrderModalProps>
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<UnifiedPurchaseOrderModalProps>
</div>
<div className="text-right">
<p className="font-bold text-lg text-[var(--color-primary-600)]">
{itemTotal.toFixed(2)}
{currencySymbol}{itemTotal.toFixed(2)}
</p>
</div>
</div>
@@ -555,7 +557,7 @@ export const UnifiedPurchaseOrderModal: React.FC<UnifiedPurchaseOrderModalProps>
})}
<div className="flex justify-between items-center pt-4 border-t-2 border-[var(--border-primary)]">
<span className="font-semibold text-lg text-[var(--text-primary)]">{t('total')}</span>
<span className="font-bold text-2xl text-[var(--color-primary-600)]">{totalAmount.toFixed(2)}</span>
<span className="font-bold text-2xl text-[var(--color-primary-600)]">{currencySymbol}{totalAmount.toFixed(2)}</span>
</div>
</div>
);

View File

@@ -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 = () => {
</div>
<div className="text-center p-4 bg-[var(--color-success)]/10 rounded-lg border border-[var(--color-success)]/20 hover:border-[var(--color-success)]/40 transition-colors">
<div className="w-8 h-8 mx-auto bg-[var(--color-success)]/20 rounded-full flex items-center justify-center mb-2">
<span className="text-[var(--color-success)] font-bold text-sm"></span>
<span className="text-[var(--color-success)] font-bold text-sm">{currencySymbol}</span>
</div>
<p className="text-2xl font-bold text-[var(--color-success)]">{totalPotentialSavings}</p>
<p className="text-2xl font-bold text-[var(--color-success)]">{currencySymbol}{totalPotentialSavings}</p>
<p className="text-sm text-[var(--text-secondary)] font-medium">{t('ai.stats.potential_savings')}</p>
</div>
<div className="text-center p-4 bg-[var(--color-info)]/10 rounded-lg border border-[var(--color-info)]/20 hover:border-[var(--color-info)]/40 transition-colors">
@@ -371,7 +373,7 @@ export const AIInsightsWidget: React.FC = () => {
</p>
<p className="text-xs text-[var(--text-secondary)]">
{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')}`}
</p>
</div>
</div>

View File

@@ -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 = () => {
<div className="flex items-center justify-center space-x-2 mb-2">
<DollarSign className="w-5 h-5 text-[var(--color-primary)]" />
<span className="text-lg font-bold text-[var(--text-primary)]">
{averageCostPerUnit.toFixed(2)}
{currencySymbol}{averageCostPerUnit.toFixed(2)}
</span>
</div>
<p className="text-sm text-[var(--text-secondary)]">{t('cost.average_cost_per_unit')}</p>
@@ -171,7 +173,7 @@ export const CostPerUnitWidget: React.FC = () => {
<div className="flex items-center justify-center space-x-2 mb-2">
<TrendingUp className="w-5 h-5 text-[var(--color-primary)]" />
<span className="text-lg font-bold text-[var(--text-primary)]">
{totalCosts.toFixed(0)}
{currencySymbol}{totalCosts.toFixed(0)}
</span>
</div>
<p className="text-sm text-[var(--text-secondary)]">{t('cost.total_production_cost')}</p>
@@ -221,7 +223,7 @@ export const CostPerUnitWidget: React.FC = () => {
</span>
</div>
<span className="text-lg font-bold text-[var(--text-primary)]">
{item.costPerUnit.toFixed(2)}
{currencySymbol}{item.costPerUnit.toFixed(2)}
</span>
</div>
@@ -229,13 +231,13 @@ export const CostPerUnitWidget: React.FC = () => {
<div>
<p className="text-[var(--text-secondary)]">{t('cost.estimated')}</p>
<p className="font-semibold text-[var(--text-primary)]">
{item.estimatedCost.toFixed(2)}
{currencySymbol}{item.estimatedCost.toFixed(2)}
</p>
</div>
<div>
<p className="text-[var(--text-secondary)]">{t('cost.actual')}</p>
<p className="font-semibold text-[var(--text-primary)]">
{item.actualCost.toFixed(2)}
{currencySymbol}{item.actualCost.toFixed(2)}
</p>
</div>
<div>

View File

@@ -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 = () => {
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<div className="w-8 h-8 mx-auto bg-green-100 dark:bg-green-900/20 rounded-full flex items-center justify-center mb-2">
<span className="text-green-600 font-bold text-sm"></span>
<span className="text-green-600 font-bold text-sm">{currencySymbol}</span>
</div>
<p className="text-2xl font-bold text-[var(--text-primary)]">{totalCost}</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">{currencySymbol}{totalCost}</p>
<p className="text-sm text-[var(--text-secondary)]">{t('equipment.maintenance.total_cost')}</p>
</div>
</div>
@@ -234,7 +236,7 @@ export const MaintenanceScheduleWidget: React.FC = () => {
<div className="flex items-center space-x-4 text-xs text-[var(--text-secondary)]">
<span>{t('equipment.maintenance.scheduled')}: {formatDate(task.scheduledDate)}</span>
<span>{t('equipment.maintenance.duration')}: {task.estimatedDuration}h</span>
{task.cost && <span>{t('equipment.maintenance.cost')}: {task.cost}</span>}
{task.cost && <span>{t('equipment.maintenance.cost')}: {currencySymbol}{task.cost}</span>}
{task.technician && <span>{t('equipment.maintenance.technician')}: {task.technician}</span>}
</div>
</div>

View File

@@ -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 = () => {
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<div className="w-8 h-8 mx-auto bg-orange-100 dark:bg-orange-900/20 rounded-full flex items-center justify-center mb-2">
<span className="text-orange-600 font-bold text-sm"></span>
<span className="text-orange-600 font-bold text-sm">{currencySymbol}</span>
</div>
<p className="text-2xl font-bold text-[var(--text-primary)]">{totalEstimatedCost}</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">{currencySymbol}{totalEstimatedCost}</p>
<p className="text-sm text-[var(--text-secondary)]">{t('ai.predictive_maintenance.estimated_cost')}</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
@@ -365,7 +367,7 @@ export const PredictiveMaintenanceWidget: React.FC = () => {
<div className="flex items-center space-x-4 text-xs">
<span className="flex items-center space-x-1">
<span className="w-2 h-2 bg-orange-500 rounded-full"></span>
<span>{t('ai.predictive_maintenance.estimated_cost')}: {alert.estimatedCost}</span>
<span>{t('ai.predictive_maintenance.estimated_cost')}: {currencySymbol}{alert.estimatedCost}</span>
</span>
<span className="flex items-center space-x-1">
<span className="w-2 h-2 bg-red-500 rounded-full"></span>

View File

@@ -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 = () => {
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg text-center">
<div className="flex items-center justify-center space-x-2 mb-1">
<span className="text-2xl font-bold text-red-600">
{totalDefectCost.toFixed(0)}
{currencySymbol}{totalDefectCost.toFixed(0)}
</span>
</div>
<p className="text-sm text-[var(--text-secondary)]">{t('quality.estimated_cost')}</p>
@@ -229,7 +231,7 @@ export const TopDefectTypesWidget: React.FC = () => {
<div className="flex items-center space-x-3 text-xs text-[var(--text-secondary)]">
<span>{defect.count} {t('quality.incidents')}</span>
<span></span>
<span>{defect.estimatedCost.toFixed(2)} {t('quality.cost')}</span>
<span>{currencySymbol}{defect.estimatedCost.toFixed(2)} {t('quality.cost')}</span>
<span className={getTrendColor(defect.trend)}>
{getTrendIcon(defect.trend)} {t(`quality.trend.${defect.trend}`)}
</span>

View File

@@ -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 = () => {
<div className="flex items-center justify-center space-x-2 mb-1">
<TrendingDown className="w-5 h-5 text-[var(--color-success)]" />
<span className="text-2xl font-bold text-[var(--color-success)]">
{totalWasteCost.toFixed(0)}
{currencySymbol}{totalWasteCost.toFixed(0)}
</span>
</div>
<p className="text-xs text-[var(--text-secondary)] font-medium">{t('cost.waste_cost')}</p>
@@ -241,7 +243,7 @@ export const WasteDefectTrackerWidget: React.FC = () => {
{source.source}
</p>
<p className="text-xs text-[var(--text-secondary)]">
{source.count} {t('common.units')} {source.cost.toFixed(2)}
{source.count} {t('common.units')} {currencySymbol}{source.cost.toFixed(2)}
</p>
</div>
</div>

View File

@@ -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<CustomerInfoProps> = ({
allowEditing = true,
className = ''
}) => {
const { currencySymbol } = useTenantCurrency();
// State
const [customer, setCustomer] = useState<Customer | null>(null);
const [customerStats, setCustomerStats] = useState<CustomerStats | null>(null);
@@ -531,7 +534,7 @@ export const CustomerInfo: React.FC<CustomerInfoProps> = ({
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Total Gastado</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">
{customerStats.total_spent.toFixed(2)}
{currencySymbol}{customerStats.total_spent.toFixed(2)}
</p>
</div>
<div className="p-2 bg-[var(--color-info)]/10 rounded-lg">
@@ -563,7 +566,7 @@ export const CustomerInfo: React.FC<CustomerInfoProps> = ({
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Ticket Promedio</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">
{customerStats.average_order_value.toFixed(2)}
{currencySymbol}{customerStats.average_order_value.toFixed(2)}
</p>
</div>
<div className="p-2 bg-yellow-100 rounded-lg">
@@ -854,7 +857,7 @@ export const CustomerInfo: React.FC<CustomerInfoProps> = ({
<div className="flex items-center space-x-4">
<div className="text-right">
<p className="font-medium text-[var(--text-primary)]">{order.total.toFixed(2)}</p>
<p className="font-medium text-[var(--text-primary)]">{currencySymbol}{order.total.toFixed(2)}</p>
<p className="text-sm text-[var(--text-secondary)]">{order.items_count} artículos</p>
</div>
<Badge color={order.status === OrderStatus.DELIVERED ? 'green' : 'blue'} variant="soft">

View File

@@ -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<OrderFormProps> = ({
showPricing = true,
className = ''
}) => {
const { currencySymbol } = useTenantCurrency();
// Form data state
const [orderData, setOrderData] = useState<OrderFormData>({
customer: initialCustomer,
@@ -687,7 +689,7 @@ export const OrderForm: React.FC<OrderFormProps> = ({
<div className="flex-1">
<h4 className="font-medium text-[var(--text-primary)]">{item.product_name}</h4>
<p className="text-sm text-[var(--text-secondary)]">
{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)}
</p>
{item.special_instructions && (
<p className="text-sm text-[var(--color-primary)] mt-1">
@@ -844,10 +846,10 @@ export const OrderForm: React.FC<OrderFormProps> = ({
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<p className="text-sm text-yellow-800">
💡 <strong>Envío gratuito</strong> en pedidos superiores a 25.
Tu pedido: {orderData.subtotal.toFixed(2)}
💡 <strong>Envío gratuito</strong> en pedidos superiores a {currencySymbol}25.
Tu pedido: {currencySymbol}{orderData.subtotal.toFixed(2)}
{orderData.subtotal < 25 && (
<span> - Faltan {(25 - orderData.subtotal).toFixed(2)} para envío gratuito</span>
<span> - Faltan {currencySymbol}{(25 - orderData.subtotal).toFixed(2)} para envío gratuito</span>
)}
</p>
</div>
@@ -953,39 +955,39 @@ export const OrderForm: React.FC<OrderFormProps> = ({
<div className="space-y-3">
<div className="flex justify-between text-sm">
<span className="text-[var(--text-secondary)]">Subtotal</span>
<span className="font-medium">{orderData.subtotal.toFixed(2)}</span>
<span className="font-medium">{currencySymbol}{orderData.subtotal.toFixed(2)}</span>
</div>
{orderData.discount_amount > 0 && (
<div className="flex justify-between text-sm text-[var(--color-success)]">
<span>Descuento{orderData.discount_code && ` (${orderData.discount_code})`}</span>
<span>-{orderData.discount_amount.toFixed(2)}</span>
<span>-{currencySymbol}{orderData.discount_amount.toFixed(2)}</span>
</div>
)}
{orderData.delivery_fee > 0 && (
<div className="flex justify-between text-sm">
<span className="text-[var(--text-secondary)]">Gastos de envío</span>
<span className="font-medium">{orderData.delivery_fee.toFixed(2)}</span>
<span className="font-medium">{currencySymbol}{orderData.delivery_fee.toFixed(2)}</span>
</div>
)}
{orderData.loyalty_points_to_use > 0 && (
<div className="flex justify-between text-sm text-[var(--color-success)]">
<span>Puntos utilizados ({orderData.loyalty_points_to_use})</span>
<span>-{(orderData.loyalty_points_to_use * 0.01).toFixed(2)}</span>
<span>-{currencySymbol}{(orderData.loyalty_points_to_use * 0.01).toFixed(2)}</span>
</div>
)}
<div className="flex justify-between text-sm">
<span className="text-[var(--text-secondary)]">IVA ({(orderData.tax_rate * 100).toFixed(0)}%)</span>
<span className="font-medium">{orderData.tax_amount.toFixed(2)}</span>
<span className="font-medium">{currencySymbol}{orderData.tax_amount.toFixed(2)}</span>
</div>
<div className="border-t pt-3">
<div className="flex justify-between">
<span className="text-lg font-semibold text-[var(--text-primary)]">Total</span>
<span className="text-lg font-bold text-[var(--color-info)]">{orderData.total_amount.toFixed(2)}</span>
<span className="text-lg font-bold text-[var(--color-info)]">{currencySymbol}{orderData.total_amount.toFixed(2)}</span>
</div>
</div>
</div>
@@ -1025,7 +1027,7 @@ export const OrderForm: React.FC<OrderFormProps> = ({
</span>
</div>
<p className="text-xs text-[var(--text-tertiary)] mt-1">
Ahorro: {(orderData.loyalty_points_to_use * 0.01).toFixed(2)}
Ahorro: {currencySymbol}{(orderData.loyalty_points_to_use * 0.01).toFixed(2)}
</p>
</div>
)}
@@ -1135,7 +1137,7 @@ export const OrderForm: React.FC<OrderFormProps> = ({
{product.category}
</Badge>
<span className="text-lg font-semibold text-[var(--color-info)]">
{product.price.toFixed(2)}
{currencySymbol}{product.price.toFixed(2)}
</span>
</div>

View File

@@ -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<OrdersTableProps> = ({
initialFilters = {}
}) => {
const { t } = useTranslation(['sales']);
const { currencySymbol } = useTenantCurrency();
// Translation helper functions
const getStatusLabel = (status: OrderStatus) => {
@@ -316,10 +318,10 @@ export const OrdersTable: React.FC<OrdersTableProps> = ({
sortable: true,
render: (order: Order) => (
<div className="text-right">
<div className="font-semibold">{order.total_revenue.toFixed(2)}</div>
<div className="font-semibold">{currencySymbol}{order.total_revenue.toFixed(2)}</div>
{order.discount_applied > 0 && (
<div className="text-sm text-[var(--color-success)]">
-{order.discount_applied.toFixed(2)}
-{currencySymbol}{order.discount_applied.toFixed(2)}
</div>
)}
</div>
@@ -590,7 +592,7 @@ export const OrdersTable: React.FC<OrdersTableProps> = ({
...prev,
min_total: e.target.value ? parseFloat(e.target.value) : undefined
}))}
placeholder="€0.00"
placeholder={`${currencySymbol}0.00`}
/>
<Input
label="Total máximo"
@@ -601,7 +603,7 @@ export const OrdersTable: React.FC<OrdersTableProps> = ({
...prev,
max_total: e.target.value ? parseFloat(e.target.value) : undefined
}))}
placeholder="€999.99"
placeholder={`${currencySymbol}999.99`}
/>
</div>
</div>
@@ -781,8 +783,8 @@ export const OrdersTable: React.FC<OrdersTableProps> = ({
)}
</div>
<div className="text-right">
<div className="font-medium">{selectedOrder.unit_price.toFixed(2)} × {selectedOrder.quantity_sold}</div>
<div className="text-sm text-[var(--text-secondary)]">{selectedOrder.total_revenue.toFixed(2)}</div>
<div className="font-medium">{currencySymbol}{selectedOrder.unit_price.toFixed(2)} × {selectedOrder.quantity_sold}</div>
<div className="text-sm text-[var(--text-secondary)]">{currencySymbol}{selectedOrder.total_revenue.toFixed(2)}</div>
</div>
</div>
</div>
@@ -800,18 +802,18 @@ export const OrdersTable: React.FC<OrdersTableProps> = ({
<div className="border-t pt-4">
<div className="flex justify-between items-center text-lg font-semibold">
<span>Total del Pedido:</span>
<span>{selectedOrder.total_revenue.toFixed(2)}</span>
<span>{currencySymbol}{selectedOrder.total_revenue.toFixed(2)}</span>
</div>
{selectedOrder.discount_applied > 0 && (
<div className="flex justify-between items-center text-sm text-[var(--color-success)]">
<span>Descuento aplicado:</span>
<span>-{selectedOrder.discount_applied.toFixed(2)}</span>
<span>-{currencySymbol}{selectedOrder.discount_applied.toFixed(2)}</span>
</div>
)}
{selectedOrder.tax_amount > 0 && (
<div className="flex justify-between items-center text-sm text-[var(--text-secondary)]">
<span>IVA incluido:</span>
<span>{selectedOrder.tax_amount.toFixed(2)}</span>
<span>{currencySymbol}{selectedOrder.tax_amount.toFixed(2)}</span>
</div>
)}
</div>

View File

@@ -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<SalesChartProps> = ({
showExport = true,
className = ''
}) => {
const { currencySymbol } = useTenantCurrency();
// State
const [analytics, setAnalytics] = useState<ExtendedSalesAnalytics | null>(null);
const [loading, setLoading] = useState(false);
@@ -247,7 +249,7 @@ export const SalesChart: React.FC<SalesChartProps> = ({
),
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<SalesChartProps> = ({
),
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<SalesChartProps> = ({
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<SalesChartProps> = ({
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<SalesChartProps> = ({
fontSize="12"
fill={Colors.text}
>
{(minValue + range * (1 - ratio)).toLocaleString('es-ES', { maximumFractionDigits: 0 })}
{currencySymbol}{(minValue + range * (1 - ratio)).toLocaleString('es-ES', { maximumFractionDigits: 0 })}
</text>
</g>
);
@@ -558,7 +560,7 @@ export const SalesChart: React.FC<SalesChartProps> = ({
strokeWidth={2}
>
<title>
{chartData.labels[index]}: {dataset.data[index].toLocaleString('es-ES', { minimumFractionDigits: 2 })}
{chartData.labels[index]}: {currencySymbol}{dataset.data[index].toLocaleString('es-ES', { minimumFractionDigits: 2 })}
</title>
</circle>
))}
@@ -751,7 +753,7 @@ export const SalesChart: React.FC<SalesChartProps> = ({
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Ingresos Totales</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">
{summaryStats.totalRevenue.toLocaleString('es-ES', { minimumFractionDigits: 2 })}
{currencySymbol}{summaryStats.totalRevenue.toLocaleString('es-ES', { minimumFractionDigits: 2 })}
</p>
</div>
<div className={`flex items-center ${summaryStats.growthRate >= 0 ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'}`}>
@@ -777,7 +779,7 @@ export const SalesChart: React.FC<SalesChartProps> = ({
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Ticket Promedio</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">
{summaryStats.avgOrderValue.toLocaleString('es-ES', { minimumFractionDigits: 2 })}
{currencySymbol}{summaryStats.avgOrderValue.toLocaleString('es-ES', { minimumFractionDigits: 2 })}
</p>
</div>
</Card>

View File

@@ -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<SupplierProductManagerProps> = ({
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<SupplierProductManagerProps> = ({
{getProductName(priceList.inventory_product_id)}
</span>
<span className="text-[var(--text-secondary)] ml-2">
{Number(priceList.unit_price || 0).toFixed(2)}/{priceList.unit_of_measure}
{currencySymbol}{Number(priceList.unit_price || 0).toFixed(2)}/{priceList.unit_of_measure}
</span>
{priceList.minimum_order_quantity && priceList.minimum_order_quantity > 1 && (
<span className="text-xs text-[var(--text-secondary)] ml-2">
@@ -319,7 +321,7 @@ export const SupplierProductManager: React.FC<SupplierProductManagerProps> = ({
<div className="ml-6 mt-2 grid grid-cols-3 gap-2 p-2 bg-[var(--bg-primary)] rounded">
<div>
<label className="block text-xs font-medium text-[var(--text-primary)] mb-1">
{t('setup_wizard:suppliers.unit_price', 'Price')} () *
{t('setup_wizard:suppliers.unit_price', 'Price')} ({currencySymbol}) *
</label>
<input
type="number"

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { Truck } from 'lucide-react';
import type { WizardStepProps } from '../../../ui/WizardModal/WizardModal';
import type { SupplierCreate, PaymentTerms, DeliverySchedule } from '../../../../api/types/suppliers';
import { useTenantCurrency } from '../../../../hooks/useTenantCurrency';
interface SupplierDeliveryStepProps extends WizardStepProps {
supplierData: Partial<SupplierCreate>;
@@ -14,6 +15,8 @@ export const SupplierDeliveryStep: React.FC<SupplierDeliveryStepProps> = ({
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<SupplierDeliveryStepProps> = ({
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
Pedido Mínimo ()
Pedido Mínimo ({currencySymbol})
</label>
<input
type="number"

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { FileText, CheckCircle2, Users, Truck, Award } from 'lucide-react';
import type { WizardStepProps } from '../../../ui/WizardModal/WizardModal';
import type { SupplierCreate } from '../../../../api/types/suppliers';
import { useTenantCurrency } from '../../../../hooks/useTenantCurrency';
interface SupplierReviewStepProps extends WizardStepProps {
supplierData: Partial<SupplierCreate>;
@@ -14,6 +15,8 @@ export const SupplierReviewStep: React.FC<SupplierReviewStepProps> = ({
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<SupplierReviewStepProps> = ({
<Truck className="w-4 h-4 text-[var(--text-secondary)] mt-0.5 flex-shrink-0" />
<div>
<p className="text-xs text-[var(--text-tertiary)]">Pedido Mínimo</p>
<p className="text-sm font-medium text-[var(--text-primary)]">{supplierData.minimum_order_value}</p>
<p className="text-sm font-medium text-[var(--text-primary)]">{currencySymbol}{supplierData.minimum_order_value}</p>
</div>
</div>
)}

View File

@@ -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<SustainabilityWidgetProps> = ({
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<SustainabilityWidgetProps> = ({
{t('sustainability:financial.potential_savings', 'Potential Monthly Savings')}
</p>
<p className="text-2xl font-bold text-green-600 dark:text-green-400">
{data.financial_savings_eur.toFixed(2)}
{currencySymbol}{data.financial_savings_eur.toFixed(2)}
</p>
</div>
<TreeDeciduous className="w-10 h-10 text-green-600/30 dark:text-green-400/30" />

View File

@@ -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<WizardStepProps> = ({ dataRef, onDataChange }) => {
@@ -293,6 +294,7 @@ const OrderItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =>
const data = dataRef?.current || {};
const { t } = useTranslation('wizards');
const { currentTenant } = useTenant();
const { currencySymbol } = useTenantCurrency();
const [products, setProducts] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -452,7 +454,7 @@ const OrderItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =>
<option value="">{t('customerOrder.orderItems.selectProduct')}</option>
{products.map((product) => (
<option key={product.id} value={product.id}>
{product.name} - {(product.average_cost || product.last_purchase_price || 0).toFixed(2)} / {product.unit_of_measure}
{product.name} - {currencySymbol}{(product.average_cost || product.last_purchase_price || 0).toFixed(2)} / {product.unit_of_measure}
</option>
))}
</select>
@@ -502,7 +504,7 @@ const OrderItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =>
<div className="pt-2 border-t border-[var(--border-primary)] text-sm">
<span className="font-semibold text-[var(--text-primary)]">
{t('customerOrder.orderItems.subtotal')}: {item.subtotal.toFixed(2)}
{t('customerOrder.orderItems.subtotal')}: {currencySymbol}{item.subtotal.toFixed(2)}
</span>
</div>
</div>
@@ -515,7 +517,7 @@ const OrderItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =>
<div className="flex justify-between items-center">
<span className="text-lg font-semibold text-[var(--text-primary)]">{t('customerOrder.messages.orderTotal')}:</span>
<span className="text-2xl font-bold text-[var(--color-primary)]">
{calculateTotal().toFixed(2)}
{currencySymbol}{calculateTotal().toFixed(2)}
</span>
</div>
</div>
@@ -531,6 +533,7 @@ const OrderItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =>
const DeliveryPaymentStep: React.FC<WizardStepProps> = ({ 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<WizardStepProps> = ({ dataRef, onDataChange
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">{t('customerOrder.messages.total')}:</span>
<span className="font-semibold text-lg text-[var(--color-primary)]">
{data.totalAmount?.toFixed(2) || '0.00'}
{currencySymbol}{data.totalAmount?.toFixed(2) || '0.00'}
</span>
</div>
</div>

View File

@@ -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<WizardStepProps> = ({ dataRef, onDataChange }) => {
@@ -197,24 +198,31 @@ const BasicInfoStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =>
<>
<option value="">{t('inventory.ingredientCategories.select')}</option>
<option value="flour">{t('inventory.ingredientCategories.flour')}</option>
<option value="yeast">{t('inventory.ingredientCategories.yeast')}</option>
<option value="dairy">{t('inventory.ingredientCategories.dairy')}</option>
<option value="eggs">{t('inventory.ingredientCategories.eggs')}</option>
<option value="sugar">{t('inventory.ingredientCategories.sugar')}</option>
<option value="fats">{t('inventory.ingredientCategories.fats')}</option>
<option value="sweeteners">{t('inventory.ingredientCategories.sweeteners')}</option>
<option value="additives">{t('inventory.ingredientCategories.additives')}</option>
<option value="fruits">{t('inventory.ingredientCategories.fruits')}</option>
<option value="nuts">{t('inventory.ingredientCategories.nuts')}</option>
<option value="salt">{t('inventory.ingredientCategories.salt')}</option>
<option value="spices">{t('inventory.ingredientCategories.spices')}</option>
<option value="leavening">{t('inventory.ingredientCategories.leavening')}</option>
<option value="additives">{t('inventory.ingredientCategories.additives')}</option>
<option value="packaging">{t('inventory.ingredientCategories.packaging')}</option>
<option value="cleaning">{t('inventory.ingredientCategories.cleaning')}</option>
<option value="other">{t('inventory.ingredientCategories.other')}</option>
</>
) : (
<>
<option value="">{t('inventory.productCategories.select')}</option>
<option value="bread">{t('inventory.productCategories.bread')}</option>
<option value="pastry">{t('inventory.productCategories.pastry')}</option>
<option value="cake">{t('inventory.productCategories.cake')}</option>
<option value="croissants">{t('inventory.productCategories.croissants')}</option>
<option value="pastries">{t('inventory.productCategories.pastries')}</option>
<option value="cakes">{t('inventory.productCategories.cakes')}</option>
<option value="cookies">{t('inventory.productCategories.cookies')}</option>
<option value="specialty">{t('inventory.productCategories.specialty')}</option>
<option value="muffins">{t('inventory.productCategories.muffins')}</option>
<option value="sandwiches">{t('inventory.productCategories.sandwiches')}</option>
<option value="seasonal">{t('inventory.productCategories.seasonal')}</option>
<option value="beverages">{t('inventory.productCategories.beverages')}</option>
<option value="other_products">{t('inventory.productCategories.other_products')}</option>
</>
)}
</select>
@@ -310,6 +318,7 @@ const BasicInfoStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =>
const StockConfigStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
const data = dataRef?.current || {};
const { t } = useTranslation('wizards');
const { currencySymbol } = useTenantCurrency();
const [lots, setLots] = useState<any[]>(data.initialLots || []);
const handleFieldChange = (field: string, value: any) => {
@@ -381,7 +390,7 @@ const StockConfigStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
</div>
<div>
<span className="text-[var(--text-tertiary)] block mb-1">{t('inventory.stockConfig.totalValue')}</span>
<span className="font-medium text-green-600">${totalValue.toFixed(2)}</span>
<span className="font-medium text-green-600">{currencySymbol}{totalValue.toFixed(2)}</span>
</div>
</div>
</div>
@@ -454,7 +463,7 @@ const StockConfigStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
{/* Unit Cost */}
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
{t('inventory.stockConfig.unitCost')}
{t('inventory.stockConfig.unitCost')} ({currencySymbol})
</label>
<input
type="number"
@@ -513,7 +522,7 @@ const StockConfigStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
{lot.quantity && lot.unitCost && (
<div className="text-xs text-[var(--text-tertiary)] pt-2 border-t border-[var(--border-secondary)]">
{t('inventory.stockConfig.lotValue')} <span className="font-semibold text-green-600">
${(parseFloat(lot.quantity) * parseFloat(lot.unitCost)).toFixed(2)}
{currencySymbol}{(parseFloat(lot.quantity) * parseFloat(lot.unitCost)).toFixed(2)}
</span>
</div>
)}

View File

@@ -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<WizardStepProps> = ({ dataRef, onDataChange }) => {
@@ -157,6 +158,7 @@ const AddItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
const data = dataRef?.current || {};
const { t } = useTranslation(['wizards', 'procurement']);
const { currentTenant } = useTenant();
const { currencySymbol } = useTenantCurrency();
const [supplierProductIds, setSupplierProductIds] = useState<string[]>([]);
const [isLoadingSupplierProducts, setIsLoadingSupplierProducts] = useState(false);
@@ -338,7 +340,7 @@ const AddItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
<option value="">{t('purchaseOrder.orderItems.selectIngredient')}</option>
{ingredientsData.map((product: any) => (
<option key={product.id} value={product.id}>
{product.name} - {(product.last_purchase_price || product.average_cost || 0).toFixed(2)} /{' '}
{product.name} - {currencySymbol}{(product.last_purchase_price || product.average_cost || 0).toFixed(2)} /{' '}
{product.unit_of_measure}
</option>
))}
@@ -393,7 +395,7 @@ const AddItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
<div className="pt-2 border-t border-[var(--border-primary)] text-sm">
<span className="font-semibold text-[var(--text-primary)]">
{t('purchaseOrder.orderItems.subtotal')}: {item.subtotal.toFixed(2)}
{t('purchaseOrder.orderItems.subtotal')}: {currencySymbol}{item.subtotal.toFixed(2)}
</span>
</div>
</div>
@@ -405,7 +407,7 @@ const AddItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
<div className="p-4 bg-gradient-to-r from-[var(--color-primary)]/5 to-[var(--color-primary)]/10 rounded-lg border-2 border-[var(--color-primary)]/20">
<div className="flex justify-between items-center">
<span className="text-lg font-semibold text-[var(--text-primary)]">{t('purchaseOrder.orderItems.total')}:</span>
<span className="text-2xl font-bold text-[var(--color-primary)]">{calculateTotal().toFixed(2)}</span>
<span className="text-2xl font-bold text-[var(--color-primary)]">{currencySymbol}{calculateTotal().toFixed(2)}</span>
</div>
</div>
)}
@@ -537,6 +539,7 @@ const OrderDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
const ReviewSubmitStep: React.FC<WizardStepProps> = ({ 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<WizardStepProps> = ({ dataRef, onDataChange })
<div className="flex-1">
<p className="font-medium text-[var(--text-primary)]">{item.product_name || t('purchaseOrder.review.productNoName')}</p>
<p className="text-sm text-[var(--text-secondary)]">
{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)}
</p>
</div>
<div className="text-right">
<p className="font-semibold text-[var(--text-primary)]">{item.subtotal.toFixed(2)}</p>
<p className="font-semibold text-[var(--text-primary)]">{currencySymbol}{item.subtotal.toFixed(2)}</p>
</div>
</div>
))}
@@ -645,29 +648,29 @@ const ReviewSubmitStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.subtotal')}:</span>
<span className="font-medium">{calculateSubtotal().toFixed(2)}</span>
<span className="font-medium">{currencySymbol}{calculateSubtotal().toFixed(2)}</span>
</div>
{(data.tax_amount || 0) > 0 && (
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.taxes')}:</span>
<span className="font-medium">{(data.tax_amount || 0).toFixed(2)}</span>
<span className="font-medium">{currencySymbol}{(data.tax_amount || 0).toFixed(2)}</span>
</div>
)}
{(data.shipping_cost || 0) > 0 && (
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.shipping')}:</span>
<span className="font-medium">{(data.shipping_cost || 0).toFixed(2)}</span>
<span className="font-medium">{currencySymbol}{(data.shipping_cost || 0).toFixed(2)}</span>
</div>
)}
{(data.discount_amount || 0) > 0 && (
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.discount')}:</span>
<span className="font-medium text-green-600">-{(data.discount_amount || 0).toFixed(2)}</span>
<span className="font-medium text-green-600">-{currencySymbol}{(data.discount_amount || 0).toFixed(2)}</span>
</div>
)}
<div className="pt-2 border-t-2 border-[var(--color-primary)]/30 flex justify-between">
<span className="text-lg font-semibold text-[var(--text-primary)]">{t('purchaseOrder.review.total')}:</span>
<span className="text-2xl font-bold text-[var(--color-primary)]">{calculateTotal().toFixed(2)}</span>
<span className="text-2xl font-bold text-[var(--color-primary)]">{currencySymbol}{calculateTotal().toFixed(2)}</span>
</div>
</div>
</div>

View File

@@ -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<WizardStepProps> = ({ dataRef, onDataChange, onN
const data = dataRef?.current || {};
const { t } = useTranslation('wizards');
const { currentTenant } = useTenant();
const { currencySymbol } = useTenantCurrency();
const [products, setProducts] = useState<any[]>([]);
const [loadingProducts, setLoadingProducts] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -351,7 +353,7 @@ const ManualEntryStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onN
<option value="">{t('salesEntry.manualEntry.products.selectProduct')}</option>
{products.map((product: any) => (
<option key={product.id} value={product.id}>
{product.name} - {(product.average_cost || product.last_purchase_price || 0).toFixed(2)}
{product.name} - {currencySymbol}{(product.average_cost || product.last_purchase_price || 0).toFixed(2)}
</option>
))}
</select>
@@ -383,7 +385,7 @@ const ManualEntryStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onN
/>
</div>
<div className="col-span-3 sm:col-span-2 text-sm font-semibold text-[var(--text-primary)]">
{item.subtotal.toFixed(2)}
{currencySymbol}{item.subtotal.toFixed(2)}
</div>
<div className="col-span-1 sm:col-span-1 flex justify-end">
<button
@@ -403,7 +405,7 @@ const ManualEntryStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onN
{(data.salesItems || []).length > 0 && (
<div className="pt-3 border-t border-[var(--border-primary)] text-right">
<span className="text-lg font-bold text-[var(--text-primary)]">
{t('salesEntry.manualEntry.products.total')} {calculateTotal().toFixed(2)}
{t('salesEntry.manualEntry.products.total')} {currencySymbol}{calculateTotal().toFixed(2)}
</span>
</div>
)}
@@ -673,6 +675,7 @@ const FileUploadStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNe
const ReviewStep: React.FC<WizardStepProps> = ({ dataRef }) => {
const data = dataRef?.current || {};
const { t } = useTranslation('wizards');
const { currencySymbol } = useTenantCurrency();
const isManual = data.entryMethod === 'manual';
const isUpload = data.entryMethod === 'upload';
@@ -725,12 +728,12 @@ const ReviewStep: React.FC<WizardStepProps> = ({ dataRef }) => {
<div className="flex-1">
<p className="font-medium text-[var(--text-primary)]">{item.product}</p>
<p className="text-sm text-[var(--text-secondary)]">
{item.quantity} × {item.unitPrice.toFixed(2)}
{item.quantity} × {currencySymbol}{item.unitPrice.toFixed(2)}
</p>
</div>
<div className="text-right">
<p className="font-semibold text-[var(--text-primary)]">
{item.subtotal.toFixed(2)}
{currencySymbol}{item.subtotal.toFixed(2)}
</p>
</div>
</div>
@@ -743,7 +746,7 @@ const ReviewStep: React.FC<WizardStepProps> = ({ dataRef }) => {
<div className="flex justify-between items-center">
<span className="text-lg font-semibold text-[var(--text-primary)]">{t('salesEntry.review.fields.total')}</span>
<span className="text-2xl font-bold text-[var(--color-primary)]">
{data.totalAmount?.toFixed(2)}
{currencySymbol}{data.totalAmount?.toFixed(2)}
</span>
</div>
</div>

View File

@@ -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<ListFieldRendererProps> = ({ 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<ListFieldRendererProps> = ({ field, value, onC
{listConfig.showSubtotals && (
<div className="pt-2 border-t border-[var(--border-primary)] text-sm text-[var(--text-secondary)]">
<span className="font-medium">Subtotal: {calculateSubtotal(item).toFixed(2)}</span>
<span className="font-medium">Subtotal: {currencySymbol}{calculateSubtotal(item).toFixed(2)}</span>
</div>
)}
</div>
@@ -185,7 +187,7 @@ const ListFieldRenderer: React.FC<ListFieldRendererProps> = ({ field, value, onC
{listConfig.showSubtotals && value.length > 0 && (
<div className="pt-3 border-t border-[var(--border-primary)] text-right">
<span className="text-lg font-semibold text-[var(--text-primary)]">
Total: {calculateTotal().toFixed(2)}
Total: {currencySymbol}{calculateTotal().toFixed(2)}
</span>
</div>
)}

View File

@@ -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<AdvancedOptionsSectionProps> = ({
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<AdvancedOptionsSectionProps> = ({
{isExpanded ? (
<>
<ChevronUp className="w-5 h-5" />
Hide {title}
{t('common.hide')} {title}
</>
) : (
<>
<ChevronDown className="w-5 h-5" />
Show {title}
{t('common.show')} {title}
</>
)}
</button>

View File

@@ -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 && (
<div className="flex items-center gap-1 px-2 py-1 rounded-md bg-warning/10 text-warning text-xs">
<DollarSign className="w-3 h-3" />
<span>{alert.business_impact.financial_impact_eur.toFixed(0)} en riesgo</span>
<span>{currencySymbol}{alert.business_impact.financial_impact_eur.toFixed(0)} en riesgo</span>
</div>
)}
{alert.urgency?.hours_until_consequence && (

View File

@@ -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,

View File

@@ -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<SSEProviderProps> = ({ 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<SSEProviderProps> = ({ 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`;
}

View File

@@ -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
* <label>Costo Unitario ({currencySymbol})</label>
*
* // Format a value
* <span>{format(123.45)}</span> // "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;

View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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<EnterpriseDashboardPageProps> = ({ 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<EnterpriseDashboardPageProps> = ({ tenan
style={{ borderColor: 'var(--border-primary)' }}>
<div>
<span className="text-[var(--color-info)]">Network Average Sales:</span>
<span className="ml-2 font-semibold text-[var(--text-primary)]">{enterpriseState.networkMetrics.averageSales.toLocaleString()}</span>
<span className="ml-2 font-semibold text-[var(--text-primary)]">{currencySymbol}{enterpriseState.networkMetrics.averageSales.toLocaleString()}</span>
</div>
<div>
<span className="text-[var(--color-info)]">Total Outlets:</span>
@@ -323,7 +325,7 @@ const EnterpriseDashboardPage: React.FC<EnterpriseDashboardPageProps> = ({ tenan
</div>
<div>
<span className="text-[var(--color-info)]">Network Total:</span>
<span className="ml-2 font-semibold text-[var(--text-primary)]">{enterpriseState.networkMetrics.totalSales.toLocaleString()}</span>
<span className="ml-2 font-semibold text-[var(--text-primary)]">{currencySymbol}{enterpriseState.networkMetrics.totalSales.toLocaleString()}</span>
</div>
</div>
)}

View File

@@ -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}
</td>
<td className="py-4 px-6 text-sm text-right font-bold text-[var(--text-primary)]">
{formatters.currency(plan.total_estimated_cost)}
{currencySymbol}{formatters.currency(plan.total_estimated_cost, '')}
</td>
</tr>
))}
@@ -378,7 +380,7 @@ const ProcurementAnalyticsPage: React.FC = () => {
<div>
<span className="text-xs text-[var(--text-tertiary)] uppercase tracking-wide">Costo Total Estimado</span>
<div className="text-3xl font-bold text-[var(--text-primary)] mt-1">
{formatters.currency(dashboard?.summary?.total_estimated_cost || 0)}
{currencySymbol}{formatters.currency(dashboard?.summary?.total_estimated_cost || 0, '')}
</div>
</div>
<DollarSign className="h-12 w-12 text-[var(--color-info)] opacity-20" />
@@ -387,7 +389,7 @@ const ProcurementAnalyticsPage: React.FC = () => {
<div>
<span className="text-xs text-[var(--text-tertiary)] uppercase tracking-wide">Costo Total Aprobado</span>
<div className="text-3xl font-bold text-[var(--text-primary)] mt-1">
{formatters.currency(dashboard?.summary?.total_approved_cost || 0)}
{currencySymbol}{formatters.currency(dashboard?.summary?.total_approved_cost || 0, '')}
</div>
</div>
<DollarSign className="h-12 w-12 text-[var(--color-success)] opacity-20" />
@@ -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, '')}
</div>
</div>
<TrendingUp className={`h-12 w-12 opacity-20 ${
@@ -419,7 +421,7 @@ const ProcurementAnalyticsPage: React.FC = () => {
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-[var(--text-primary)]">{category.name}</span>
<span className="text-sm font-bold text-[var(--text-primary)]">
{formatters.currency(category.amount)}
{currencySymbol}{formatters.currency(category.amount, '')}
</span>
</div>
<div className="relative">

View File

@@ -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<TimePeriod>('week');
const [activeTab, setActiveTab] = useState('overview');
@@ -515,13 +517,13 @@ const PerformanceAnalyticsPage: React.FC = () => {
<div className="text-center p-6 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-sm text-[var(--text-secondary)] mb-2">Ingresos Totales</p>
<p className="text-3xl font-bold text-green-600">
{costRevenue.total_revenue.toLocaleString('es-ES')}
{currencySymbol}{costRevenue.total_revenue.toLocaleString('es-ES')}
</p>
</div>
<div className="text-center p-6 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-sm text-[var(--text-secondary)] mb-2">Costos Estimados</p>
<p className="text-3xl font-bold text-red-600">
{costRevenue.estimated_costs.toLocaleString('es-ES')}
{currencySymbol}{costRevenue.estimated_costs.toLocaleString('es-ES')}
</p>
</div>
</div>

View File

@@ -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 = () => {
</div>
{program.funding_eur && program.funding_eur > 0 && (
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('sustainability:grant.funding', 'Financiación')}: {program.funding_eur.toLocaleString()}
{t('sustainability:grant.funding', 'Financiación')}: {currencySymbol}{program.funding_eur.toLocaleString()}
</p>
)}
</div>
@@ -569,10 +571,10 @@ const SustainabilityPage: React.FC = () => {
{t('sustainability:financial.waste_cost', 'Coste de Residuos')}
</p>
<p className="text-2xl font-bold text-red-600">
{metrics.financial_impact.waste_cost_eur.toFixed(2)}
{currencySymbol}{metrics.financial_impact.waste_cost_eur.toFixed(2)}
</p>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{metrics.financial_impact.cost_per_kg.toFixed(2)}/kg
{currencySymbol}{metrics.financial_impact.cost_per_kg.toFixed(2)}/kg
</p>
</div>
@@ -581,7 +583,7 @@ const SustainabilityPage: React.FC = () => {
{t('sustainability:financial.monthly_savings', 'Ahorro Mensual')}
</p>
<p className="text-2xl font-bold text-green-600 dark:text-green-400">
{metrics.financial_impact.potential_monthly_savings.toFixed(2)}
{currencySymbol}{metrics.financial_impact.potential_monthly_savings.toFixed(2)}
</p>
<p className="text-xs text-green-600/80 dark:text-green-400/80 mt-1">
{t('sustainability:financial.from_reduction', 'Por reducción')}
@@ -593,7 +595,7 @@ const SustainabilityPage: React.FC = () => {
{t('sustainability:financial.annual_projection', 'Proyección Anual')}
</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">
{metrics.financial_impact.annual_projection.toFixed(2)}
{currencySymbol}{metrics.financial_impact.annual_projection.toFixed(2)}
</p>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('sustainability:financial.estimated', 'Estimado')}
@@ -605,7 +607,7 @@ const SustainabilityPage: React.FC = () => {
{t('sustainability:financial.roi', 'ROI de IA')}
</p>
<p className="text-2xl font-bold text-blue-600 dark:text-blue-400">
{(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)}
</p>
<p className="text-xs text-blue-600/80 dark:text-blue-400/80 mt-1">
{t('sustainability:financial.ai_savings', 'Ahorrado por IA')}

View File

@@ -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}`,

View File

@@ -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<string | null>(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');

View File

@@ -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<PurchaseOrderStatus | ''>('');
@@ -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}`,

View File

@@ -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',

View File

@@ -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',

View File

@@ -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) {

View File

@@ -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 = (

View File

@@ -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})` : ''}`,
}),
}
);

View File

@@ -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

View File

@@ -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:

View File

@@ -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))

View File

@@ -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"""

View File

@@ -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),

View File

@@ -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,