Improve UI
This commit is contained in:
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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') :
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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,
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
92
frontend/src/hooks/useTenantCurrency.ts
Normal file
92
frontend/src/hooks/useTenantCurrency.ts
Normal 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;
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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})` : ''}`,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user