Improve the frontend 3
This commit is contained in:
@@ -13,10 +13,11 @@ import { useDemoTour, shouldStartTour, clearTourStartPending } from '../../featu
|
||||
import { useDashboardStats } from '../../api/hooks/dashboard';
|
||||
import { usePurchaseOrder, useApprovePurchaseOrder, useRejectPurchaseOrder } from '../../api/hooks/purchase-orders';
|
||||
import { useBatchDetails, useUpdateBatchStatus } from '../../api/hooks/production';
|
||||
import { useRunDailyWorkflow } from '../../api';
|
||||
import { ProductionStatusEnum } from '../../api';
|
||||
import {
|
||||
AlertTriangle,
|
||||
Clock,
|
||||
Clock,
|
||||
Euro,
|
||||
Package,
|
||||
FileText,
|
||||
@@ -28,9 +29,10 @@ import {
|
||||
Factory,
|
||||
Timer,
|
||||
TrendingDown,
|
||||
Leaf
|
||||
Leaf,
|
||||
Play
|
||||
} from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { showToast } from '../../utils/toast';
|
||||
|
||||
const DashboardPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -76,18 +78,43 @@ const DashboardPage: React.FC = () => {
|
||||
const approvePOMutation = useApprovePurchaseOrder();
|
||||
const rejectPOMutation = useRejectPurchaseOrder();
|
||||
const updateBatchStatusMutation = useUpdateBatchStatus();
|
||||
const orchestratorMutation = useRunDailyWorkflow();
|
||||
|
||||
const handleRunOrchestrator = async () => {
|
||||
try {
|
||||
await orchestratorMutation.mutateAsync(currentTenant?.id || '');
|
||||
showToast.success('Flujo de planificación ejecutado exitosamente');
|
||||
} catch (error) {
|
||||
console.error('Error running orchestrator:', error);
|
||||
showToast.error('Error al ejecutar flujo de planificación');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log('[Dashboard] Demo mode:', isDemoMode);
|
||||
console.log('[Dashboard] Should start tour:', shouldStartTour());
|
||||
console.log('[Dashboard] SessionStorage demo_tour_should_start:', sessionStorage.getItem('demo_tour_should_start'));
|
||||
console.log('[Dashboard] SessionStorage demo_tour_start_step:', sessionStorage.getItem('demo_tour_start_step'));
|
||||
|
||||
if (isDemoMode && shouldStartTour()) {
|
||||
// Check if there's a tour intent from redirection (higher priority)
|
||||
const shouldStartFromRedirect = sessionStorage.getItem('demo_tour_should_start') === 'true';
|
||||
const redirectStartStep = parseInt(sessionStorage.getItem('demo_tour_start_step') || '0', 10);
|
||||
|
||||
if (isDemoMode && (shouldStartTour() || shouldStartFromRedirect)) {
|
||||
console.log('[Dashboard] Starting tour in 1.5s...');
|
||||
const timer = setTimeout(() => {
|
||||
console.log('[Dashboard] Executing startTour()');
|
||||
startTour();
|
||||
clearTourStartPending();
|
||||
if (shouldStartFromRedirect) {
|
||||
// Start tour from the specific step that was intended
|
||||
startTour(redirectStartStep);
|
||||
// Clear the redirect intent
|
||||
sessionStorage.removeItem('demo_tour_should_start');
|
||||
sessionStorage.removeItem('demo_tour_start_step');
|
||||
} else {
|
||||
// Start tour normally (from beginning or resume)
|
||||
startTour();
|
||||
clearTourStartPending();
|
||||
}
|
||||
}, 1500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
@@ -114,10 +141,10 @@ const DashboardPage: React.FC = () => {
|
||||
batchId,
|
||||
statusUpdate: { status: ProductionStatusEnum.IN_PROGRESS }
|
||||
});
|
||||
toast.success('Lote iniciado');
|
||||
showToast.success('Lote iniciado');
|
||||
} catch (error) {
|
||||
console.error('Error starting batch:', error);
|
||||
toast.error('Error al iniciar lote');
|
||||
showToast.error('Error al iniciar lote');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -128,10 +155,10 @@ const DashboardPage: React.FC = () => {
|
||||
batchId,
|
||||
statusUpdate: { status: ProductionStatusEnum.ON_HOLD }
|
||||
});
|
||||
toast.success('Lote pausado');
|
||||
showToast.success('Lote pausado');
|
||||
} catch (error) {
|
||||
console.error('Error pausing batch:', error);
|
||||
toast.error('Error al pausar lote');
|
||||
showToast.error('Error al pausar lote');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -147,10 +174,10 @@ const DashboardPage: React.FC = () => {
|
||||
poId,
|
||||
notes: 'Aprobado desde el dashboard'
|
||||
});
|
||||
toast.success('Orden aprobada');
|
||||
showToast.success('Orden aprobada');
|
||||
} catch (error) {
|
||||
console.error('Error approving PO:', error);
|
||||
toast.error('Error al aprobar orden');
|
||||
showToast.error('Error al aprobar orden');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -161,10 +188,10 @@ const DashboardPage: React.FC = () => {
|
||||
poId,
|
||||
reason: 'Rechazado desde el dashboard'
|
||||
});
|
||||
toast.success('Orden rechazada');
|
||||
showToast.success('Orden rechazada');
|
||||
} catch (error) {
|
||||
console.error('Error rejecting PO:', error);
|
||||
toast.error('Error al rechazar orden');
|
||||
showToast.error('Error al rechazar orden');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -355,6 +382,18 @@ const DashboardPage: React.FC = () => {
|
||||
<PageHeader
|
||||
title={t('dashboard:title', 'Dashboard')}
|
||||
description={t('dashboard:subtitle', 'Overview of your bakery operations')}
|
||||
actions={[
|
||||
{
|
||||
id: 'run-orchestrator',
|
||||
label: orchestratorMutation.isPending ? 'Ejecutando...' : 'Ejecutar Planificación Diaria',
|
||||
icon: Play,
|
||||
onClick: handleRunOrchestrator,
|
||||
variant: 'primary', // Primary button for visibility
|
||||
size: 'sm',
|
||||
disabled: orchestratorMutation.isPending,
|
||||
loading: orchestratorMutation.isPending
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Critical Metrics using StatsGrid */}
|
||||
@@ -447,12 +486,12 @@ const DashboardPage: React.FC = () => {
|
||||
poId: poDetails.id,
|
||||
notes: 'Aprobado desde el dashboard'
|
||||
});
|
||||
toast.success('Orden aprobada');
|
||||
showToast.success('Orden aprobada');
|
||||
setShowPOModal(false);
|
||||
setSelectedPOId(null);
|
||||
} catch (error) {
|
||||
console.error('Error approving PO:', error);
|
||||
toast.error('Error al aprobar orden');
|
||||
showToast.error('Error al aprobar orden');
|
||||
}
|
||||
},
|
||||
variant: 'primary' as const,
|
||||
@@ -467,12 +506,12 @@ const DashboardPage: React.FC = () => {
|
||||
poId: poDetails.id,
|
||||
reason: 'Rechazado desde el dashboard'
|
||||
});
|
||||
toast.success('Orden rechazada');
|
||||
showToast.success('Orden rechazada');
|
||||
setShowPOModal(false);
|
||||
setSelectedPOId(null);
|
||||
} catch (error) {
|
||||
console.error('Error rejecting PO:', error);
|
||||
toast.error('Error al rechazar orden');
|
||||
showToast.error('Error al rechazar orden');
|
||||
}
|
||||
},
|
||||
variant: 'outline' as const,
|
||||
@@ -521,12 +560,12 @@ const DashboardPage: React.FC = () => {
|
||||
batchId: batchDetails.id,
|
||||
statusUpdate: { status: ProductionStatusEnum.IN_PROGRESS }
|
||||
});
|
||||
toast.success('Lote iniciado');
|
||||
showToast.success('Lote iniciado');
|
||||
setShowBatchModal(false);
|
||||
setSelectedBatchId(null);
|
||||
} catch (error) {
|
||||
console.error('Error starting batch:', error);
|
||||
toast.error('Error al iniciar lote');
|
||||
showToast.error('Error al iniciar lote');
|
||||
}
|
||||
},
|
||||
variant: 'primary' as const,
|
||||
@@ -542,12 +581,12 @@ const DashboardPage: React.FC = () => {
|
||||
batchId: batchDetails.id,
|
||||
statusUpdate: { status: ProductionStatusEnum.ON_HOLD }
|
||||
});
|
||||
toast.success('Lote pausado');
|
||||
showToast.success('Lote pausado');
|
||||
setShowBatchModal(false);
|
||||
setSelectedBatchId(null);
|
||||
} catch (error) {
|
||||
console.error('Error pausing batch:', error);
|
||||
toast.error('Error al pausar lote');
|
||||
showToast.error('Error al pausar lote');
|
||||
}
|
||||
},
|
||||
variant: 'outline' as const,
|
||||
@@ -561,4 +600,4 @@ const DashboardPage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardPage;
|
||||
export default DashboardPage;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
||||
import { Settings, Save, RotateCcw, AlertCircle, Loader } from 'lucide-react';
|
||||
import { Button, Card } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
import { useSettings, useUpdateSettings } from '../../../../api/hooks/settings';
|
||||
import { useCurrentTenant } from '../../../../stores/tenant.store';
|
||||
import type {
|
||||
@@ -13,6 +13,10 @@ import type {
|
||||
SupplierSettings,
|
||||
POSSettings,
|
||||
OrderSettings,
|
||||
ReplenishmentSettings,
|
||||
SafetyStockSettings,
|
||||
MOQSettings,
|
||||
SupplierSelectionSettings,
|
||||
} from '../../../../api/types/settings';
|
||||
import ProcurementSettingsCard from './cards/ProcurementSettingsCard';
|
||||
import InventorySettingsCard from './cards/InventorySettingsCard';
|
||||
@@ -20,9 +24,13 @@ import ProductionSettingsCard from './cards/ProductionSettingsCard';
|
||||
import SupplierSettingsCard from './cards/SupplierSettingsCard';
|
||||
import POSSettingsCard from './cards/POSSettingsCard';
|
||||
import OrderSettingsCard from './cards/OrderSettingsCard';
|
||||
import ReplenishmentSettingsCard from './cards/ReplenishmentSettingsCard';
|
||||
import SafetyStockSettingsCard from './cards/SafetyStockSettingsCard';
|
||||
import MOQSettingsCard from './cards/MOQSettingsCard';
|
||||
import SupplierSelectionSettingsCard from './cards/SupplierSelectionSettingsCard';
|
||||
|
||||
const AjustesPage: React.FC = () => {
|
||||
const { addToast } = useToast();
|
||||
|
||||
const currentTenant = useCurrentTenant();
|
||||
const tenantId = currentTenant?.id || '';
|
||||
|
||||
@@ -52,6 +60,10 @@ const AjustesPage: React.FC = () => {
|
||||
const [supplierSettings, setSupplierSettings] = useState<SupplierSettings | null>(null);
|
||||
const [posSettings, setPosSettings] = useState<POSSettings | null>(null);
|
||||
const [orderSettings, setOrderSettings] = useState<OrderSettings | null>(null);
|
||||
const [replenishmentSettings, setReplenishmentSettings] = useState<ReplenishmentSettings | null>(null);
|
||||
const [safetyStockSettings, setSafetyStockSettings] = useState<SafetyStockSettings | null>(null);
|
||||
const [moqSettings, setMoqSettings] = useState<MOQSettings | null>(null);
|
||||
const [supplierSelectionSettings, setSupplierSelectionSettings] = useState<SupplierSelectionSettings | null>(null);
|
||||
|
||||
// Load settings into local state when data is fetched
|
||||
React.useEffect(() => {
|
||||
@@ -62,13 +74,18 @@ const AjustesPage: React.FC = () => {
|
||||
setSupplierSettings(settings.supplier_settings);
|
||||
setPosSettings(settings.pos_settings);
|
||||
setOrderSettings(settings.order_settings);
|
||||
setReplenishmentSettings(settings.replenishment_settings);
|
||||
setSafetyStockSettings(settings.safety_stock_settings);
|
||||
setMoqSettings(settings.moq_settings);
|
||||
setSupplierSelectionSettings(settings.supplier_selection_settings);
|
||||
setHasUnsavedChanges(false);
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
const handleSaveAll = async () => {
|
||||
if (!tenantId || !procurementSettings || !inventorySettings || !productionSettings ||
|
||||
!supplierSettings || !posSettings || !orderSettings) {
|
||||
!supplierSettings || !posSettings || !orderSettings || !replenishmentSettings ||
|
||||
!safetyStockSettings || !moqSettings || !supplierSelectionSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,14 +101,18 @@ const AjustesPage: React.FC = () => {
|
||||
supplier_settings: supplierSettings,
|
||||
pos_settings: posSettings,
|
||||
order_settings: orderSettings,
|
||||
replenishment_settings: replenishmentSettings,
|
||||
safety_stock_settings: safetyStockSettings,
|
||||
moq_settings: moqSettings,
|
||||
supplier_selection_settings: supplierSelectionSettings,
|
||||
},
|
||||
});
|
||||
|
||||
setHasUnsavedChanges(false);
|
||||
addToast('Ajustes guardados correctamente', { type: 'success' });
|
||||
showToast.success('Ajustes guardados correctamente');
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
|
||||
addToast(`Error al guardar ajustes: ${errorMessage}`, { type: 'error' });
|
||||
showToast.error(`Error al guardar ajustes: ${errorMessage}`);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -105,6 +126,10 @@ const AjustesPage: React.FC = () => {
|
||||
setSupplierSettings(settings.supplier_settings);
|
||||
setPosSettings(settings.pos_settings);
|
||||
setOrderSettings(settings.order_settings);
|
||||
setReplenishmentSettings(settings.replenishment_settings);
|
||||
setSafetyStockSettings(settings.safety_stock_settings);
|
||||
setMoqSettings(settings.moq_settings);
|
||||
setSupplierSelectionSettings(settings.supplier_selection_settings);
|
||||
setHasUnsavedChanges(false);
|
||||
}
|
||||
};
|
||||
@@ -256,6 +281,54 @@ const AjustesPage: React.FC = () => {
|
||||
disabled={isSaving}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Replenishment Settings */}
|
||||
{replenishmentSettings && (
|
||||
<ReplenishmentSettingsCard
|
||||
settings={replenishmentSettings}
|
||||
onChange={(newSettings) => {
|
||||
setReplenishmentSettings(newSettings);
|
||||
handleCategoryChange('replenishment');
|
||||
}}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Safety Stock Settings */}
|
||||
{safetyStockSettings && (
|
||||
<SafetyStockSettingsCard
|
||||
settings={safetyStockSettings}
|
||||
onChange={(newSettings) => {
|
||||
setSafetyStockSettings(newSettings);
|
||||
handleCategoryChange('safety_stock');
|
||||
}}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* MOQ Settings */}
|
||||
{moqSettings && (
|
||||
<MOQSettingsCard
|
||||
settings={moqSettings}
|
||||
onChange={(newSettings) => {
|
||||
setMoqSettings(newSettings);
|
||||
handleCategoryChange('moq');
|
||||
}}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Supplier Selection Settings */}
|
||||
{supplierSelectionSettings && (
|
||||
<SupplierSelectionSettingsCard
|
||||
settings={supplierSelectionSettings}
|
||||
onChange={(newSettings) => {
|
||||
setSupplierSelectionSettings(newSettings);
|
||||
handleCategoryChange('supplier_selection');
|
||||
}}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Floating Save Banner */}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
import { Card } from '@components/ui';
|
||||
import { MOQSettings } from '@services/types/settings';
|
||||
import { Input } from '@components/ui/Input';
|
||||
|
||||
interface MOQSettingsCardProps {
|
||||
settings: MOQSettings;
|
||||
onChange: (settings: MOQSettings) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const MOQSettingsCard: React.FC<MOQSettingsCardProps> = ({
|
||||
settings,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const handleNumberChange = (field: keyof MOQSettings, value: string) => {
|
||||
const numValue = value === '' ? 0 : Number(value);
|
||||
onChange({
|
||||
...settings,
|
||||
[field]: numValue,
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleChange = (field: keyof MOQSettings, value: boolean) => {
|
||||
onChange({
|
||||
...settings,
|
||||
[field]: value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">
|
||||
Configuración de MOQ (Cantidad Mínima de Pedido)
|
||||
</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Consolidation Window Days */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Días de Ventana de Consolidación (1-30)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
max="30"
|
||||
value={settings.consolidation_window_days}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('consolidation_window_days', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Min Batch Size */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Tamaño Mínimo de Lote (0.1-1000)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0.1"
|
||||
max="1000"
|
||||
step="0.1"
|
||||
value={settings.min_batch_size}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('min_batch_size', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Max Batch Size */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Tamaño Máximo de Lote (1-10000)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
max="10000"
|
||||
step="1"
|
||||
value={settings.max_batch_size}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('max_batch_size', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toggle Options */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="allow_early_ordering"
|
||||
checked={settings.allow_early_ordering}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleToggleChange('allow_early_ordering', e.target.checked)}
|
||||
disabled={disabled}
|
||||
className="rounded border-[var(--border-primary)]"
|
||||
/>
|
||||
<label htmlFor="allow_early_ordering" className="text-sm text-[var(--text-secondary)]">
|
||||
Permitir Pedido Anticipado
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="enable_batch_optimization"
|
||||
checked={settings.enable_batch_optimization}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleToggleChange('enable_batch_optimization', e.target.checked)}
|
||||
disabled={disabled}
|
||||
className="rounded border-[var(--border-primary)]"
|
||||
/>
|
||||
<label htmlFor="enable_batch_optimization" className="text-sm text-[var(--text-secondary)]">
|
||||
Habilitar Optimización de Lotes
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default MOQSettingsCard;
|
||||
@@ -0,0 +1,158 @@
|
||||
import React from 'react';
|
||||
import { Card } from '@components/ui';
|
||||
import { ReplenishmentSettings } from '@services/types/settings';
|
||||
import { Slider } from '@components/ui/Slider';
|
||||
import { Input } from '@components/ui/Input';
|
||||
|
||||
interface ReplenishmentSettingsCardProps {
|
||||
settings: ReplenishmentSettings;
|
||||
onChange: (settings: ReplenishmentSettings) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ReplenishmentSettingsCard: React.FC<ReplenishmentSettingsCardProps> = ({
|
||||
settings,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const handleNumberChange = (field: keyof ReplenishmentSettings, value: string) => {
|
||||
const numValue = value === '' ? 0 : Number(value);
|
||||
onChange({
|
||||
...settings,
|
||||
[field]: numValue,
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleChange = (field: keyof ReplenishmentSettings, value: boolean) => {
|
||||
onChange({
|
||||
...settings,
|
||||
[field]: value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">
|
||||
Planeamiento de Reposición
|
||||
</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Projection Horizon Days */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Días de Proyección (1-30)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
max="30"
|
||||
value={settings.projection_horizon_days}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('projection_horizon_days', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Service Level */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Nivel de Servicio ({(settings.service_level * 100).toFixed(0)}%)
|
||||
</label>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={[settings.service_level]}
|
||||
onValueChange={([value]: number[]) => handleNumberChange('service_level', value.toString())}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Buffer Days */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Días de Buffer (0-14)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="14"
|
||||
value={settings.buffer_days}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('buffer_days', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Demand Forecast Days */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Días de Previsión de Demanda (1-90)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
max="90"
|
||||
value={settings.demand_forecast_days}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('demand_forecast_days', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Min Order Quantity */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Cantidad Mínima de Pedido (0.1-1000)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0.1"
|
||||
max="100"
|
||||
step="0.1"
|
||||
value={settings.min_order_quantity}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('min_order_quantity', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Max Order Quantity */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Cantidad Máxima de Pedido (1-1000)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
max="10000"
|
||||
step="1"
|
||||
value={settings.max_order_quantity}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('max_order_quantity', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enable Auto Replenishment Toggle */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="enable_auto_replenishment"
|
||||
checked={settings.enable_auto_replenishment}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleToggleChange('enable_auto_replenishment', e.target.checked)}
|
||||
disabled={disabled}
|
||||
className="rounded border-[var(--border-primary)]"
|
||||
/>
|
||||
<label htmlFor="enable_auto_replenishment" className="text-sm text-[var(--text-secondary)]">
|
||||
Habilitar Reposición Automática
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReplenishmentSettingsCard;
|
||||
@@ -0,0 +1,130 @@
|
||||
import React from 'react';
|
||||
import { Card } from '@components/ui';
|
||||
import { SafetyStockSettings } from '@services/types/settings';
|
||||
import { Slider } from '@components/ui/Slider';
|
||||
import { Input } from '@components/ui/Input';
|
||||
import { Select } from '@components/ui/Select';
|
||||
|
||||
interface SafetyStockSettingsCardProps {
|
||||
settings: SafetyStockSettings;
|
||||
onChange: (settings: SafetyStockSettings) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const SafetyStockSettingsCard: React.FC<SafetyStockSettingsCardProps> = ({
|
||||
settings,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const handleNumberChange = (field: keyof SafetyStockSettings, value: string) => {
|
||||
const numValue = value === '' ? 0 : Number(value);
|
||||
onChange({
|
||||
...settings,
|
||||
[field]: numValue,
|
||||
});
|
||||
};
|
||||
|
||||
const handleStringChange = (field: keyof SafetyStockSettings, value: any) => {
|
||||
const stringValue = typeof value === 'object' && value !== null ? value[0] : value;
|
||||
onChange({
|
||||
...settings,
|
||||
[field]: stringValue.toString(),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">
|
||||
Configuración de Stock de Seguridad
|
||||
</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Service Level */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Nivel de Servicio ({(settings.service_level * 100).toFixed(0)}%)
|
||||
</label>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={[settings.service_level]}
|
||||
onValueChange={([value]: number[]) => handleNumberChange('service_level', value.toString())}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Method */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Método de Cálculo
|
||||
</label>
|
||||
<Select
|
||||
value={settings.method}
|
||||
onChange={(value) => handleStringChange('method', value)}
|
||||
disabled={disabled}
|
||||
options={[
|
||||
{ value: 'statistical', label: 'Estadístico (Z×σ×√L)' },
|
||||
{ value: 'fixed_percentage', label: 'Porcentaje Fijo (20%)' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Min Safety Stock */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Stock de Seguridad Mínimo (0-1000)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1000"
|
||||
step="0.1"
|
||||
value={settings.min_safety_stock}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('min_safety_stock', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Max Safety Stock */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Stock de Seguridad Máximo (0-1000)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1000"
|
||||
step="0.1"
|
||||
value={settings.max_safety_stock}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('max_safety_stock', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Reorder Point Calculation */}
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Método de Punto de Reorden
|
||||
</label>
|
||||
<Select
|
||||
value={settings.reorder_point_calculation}
|
||||
onChange={(value) => handleStringChange('reorder_point_calculation', value)}
|
||||
disabled={disabled}
|
||||
options={[
|
||||
{ value: 'safety_stock_plus_lead_time_demand', label: 'Stock de Seguridad + Demanda de Tiempo de Entrega' },
|
||||
{ value: 'safety_stock_only', label: 'Solo Stock de Seguridad' },
|
||||
{ value: 'fixed_quantity', label: 'Cantidad Fija' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SafetyStockSettingsCard;
|
||||
@@ -0,0 +1,152 @@
|
||||
import React from 'react';
|
||||
import { Card } from '@components/ui';
|
||||
import { SupplierSelectionSettings } from '@services/types/settings';
|
||||
import { Slider } from '@components/ui/Slider';
|
||||
import { Input } from '@components/ui/Input';
|
||||
|
||||
interface SupplierSelectionSettingsCardProps {
|
||||
settings: SupplierSelectionSettings;
|
||||
onChange: (settings: SupplierSelectionSettings) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const SupplierSelectionSettingsCard: React.FC<SupplierSelectionSettingsCardProps> = ({
|
||||
settings,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const handleNumberChange = (field: keyof SupplierSelectionSettings, value: string) => {
|
||||
const numValue = value === '' ? 0 : Number(value);
|
||||
onChange({
|
||||
...settings,
|
||||
[field]: numValue,
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleChange = (field: keyof SupplierSelectionSettings, value: boolean) => {
|
||||
onChange({
|
||||
...settings,
|
||||
[field]: value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">
|
||||
Configuración de Selección de Proveedores
|
||||
</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Price Weight */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Peso del Precio ({(settings.price_weight * 100).toFixed(0)}%)
|
||||
</label>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={[settings.price_weight]}
|
||||
onValueChange={([value]: number[]) => handleNumberChange('price_weight', value.toString())}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Lead Time Weight */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Peso del Tiempo de Entrega ({(settings.lead_time_weight * 100).toFixed(0)}%)
|
||||
</label>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={[settings.lead_time_weight]}
|
||||
onValueChange={([value]: number[]) => handleNumberChange('lead_time_weight', value.toString())}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Quality Weight */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Peso de la Calidad ({(settings.quality_weight * 100).toFixed(0)}%)
|
||||
</label>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={[settings.quality_weight]}
|
||||
onValueChange={([value]: number[]) => handleNumberChange('quality_weight', value.toString())}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Reliability Weight */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Peso de la Confiabilidad ({(settings.reliability_weight * 100).toFixed(0)}%)
|
||||
</label>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={[settings.reliability_weight]}
|
||||
onValueChange={([value]: number[]) => handleNumberChange('reliability_weight', value.toString())}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Diversification Threshold */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Umbral de Diversificación (0-1000)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1000"
|
||||
value={settings.diversification_threshold}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('diversification_threshold', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Max Single Percentage */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Máximo % para Proveedor Único ({(settings.max_single_percentage * 100).toFixed(0)}%)
|
||||
</label>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={[settings.max_single_percentage]}
|
||||
onValueChange={([value]: number[]) => handleNumberChange('max_single_percentage', value.toString())}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enable Supplier Score Optimization Toggle */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="enable_supplier_score_optimization"
|
||||
checked={settings.enable_supplier_score_optimization}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleToggleChange('enable_supplier_score_optimization', e.target.checked)}
|
||||
disabled={disabled}
|
||||
className="rounded border-[var(--border-primary)]"
|
||||
/>
|
||||
<label htmlFor="enable_supplier_score_optimization" className="text-sm text-[var(--text-secondary)]">
|
||||
Habilitar Optimización por Puntuación de Proveedores
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupplierSelectionSettingsCard;
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useMemo } from 'react';
|
||||
import { Brain, TrendingUp, AlertCircle, Play, RotateCcw, Eye, Loader, CheckCircle } from 'lucide-react';
|
||||
import { Button, Badge, Modal, Table, Select, StatsGrid, StatusCard, SearchAndFilter, type FilterConfig, Card, EmptyState } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
import { useCurrentTenant } from '../../../../stores/tenant.store';
|
||||
import { useIngredients } from '../../../../api/hooks/inventory';
|
||||
import {
|
||||
@@ -40,7 +40,7 @@ interface ModelStatus {
|
||||
}
|
||||
|
||||
const ModelsConfigPage: React.FC = () => {
|
||||
const { addToast } = useToast();
|
||||
|
||||
const currentTenant = useCurrentTenant();
|
||||
const tenantId = currentTenant?.id || '';
|
||||
|
||||
@@ -160,10 +160,10 @@ const ModelsConfigPage: React.FC = () => {
|
||||
request: trainingSettings
|
||||
});
|
||||
|
||||
addToast(`Entrenamiento iniciado para ${selectedIngredient.name}`, { type: 'success' });
|
||||
showToast.success(`Entrenamiento iniciado para ${selectedIngredient.name}`);
|
||||
setShowTrainingModal(false);
|
||||
} catch (error) {
|
||||
addToast('Error al iniciar el entrenamiento', { type: 'error' });
|
||||
showToast.error('Error al iniciar el entrenamiento');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -206,12 +206,12 @@ const ModelsConfigPage: React.FC = () => {
|
||||
request: settings
|
||||
});
|
||||
|
||||
addToast(`Reentrenamiento iniciado para ${selectedIngredient.name}`, { type: 'success' });
|
||||
showToast.success(`Reentrenamiento iniciado para ${selectedIngredient.name}`);
|
||||
setShowRetrainModal(false);
|
||||
setSelectedIngredient(null);
|
||||
setSelectedModel(null);
|
||||
} catch (error) {
|
||||
addToast('Error al reentrenar el modelo', { type: 'error' });
|
||||
showToast.error('Error al reentrenar el modelo');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
||||
import { useIngredients } from '../../../../api/hooks/inventory';
|
||||
import { useTenantId } from '../../../../hooks/useTenantId';
|
||||
import { ProductType, ProductCategory, IngredientResponse } from '../../../../api/types/inventory';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
import { usePOSConfigurationData, usePOSConfigurationManager, usePOSTransactions, usePOSTransactionsDashboard, usePOSTransaction } from '../../../../api/hooks/pos';
|
||||
import { POSConfiguration } from '../../../../api/types/pos';
|
||||
import { posService } from '../../../../api/services/pos';
|
||||
@@ -546,7 +546,7 @@ const POSPage: React.FC = () => {
|
||||
const [testingConnection, setTestingConnection] = useState<string | null>(null);
|
||||
|
||||
const tenantId = useTenantId();
|
||||
const { addToast } = useToast();
|
||||
|
||||
|
||||
// POS Configuration hooks
|
||||
const posData = usePOSConfigurationData(tenantId);
|
||||
@@ -674,12 +674,12 @@ const POSPage: React.FC = () => {
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
addToast('Conexión exitosa', { type: 'success' });
|
||||
showToast.success('Conexión exitosa');
|
||||
} else {
|
||||
addToast(`Error en la conexión: ${response.message || 'Error desconocido'}`, { type: 'error' });
|
||||
showToast.error(`Error en la conexión: ${response.message || 'Error desconocido'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
addToast('Error al probar la conexión', { type: 'error' });
|
||||
showToast.error('Error al probar la conexión');
|
||||
} finally {
|
||||
setTestingConnection(null);
|
||||
}
|
||||
@@ -695,10 +695,10 @@ const POSPage: React.FC = () => {
|
||||
tenant_id: tenantId,
|
||||
config_id: configId,
|
||||
});
|
||||
addToast('Configuración eliminada correctamente', { type: 'success' });
|
||||
showToast.success('Configuración eliminada correctamente');
|
||||
loadPosConfigurations();
|
||||
} catch (error) {
|
||||
addToast('Error al eliminar la configuración', { type: 'error' });
|
||||
showToast.error('Error al eliminar la configuración');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -762,7 +762,7 @@ const POSPage: React.FC = () => {
|
||||
});
|
||||
|
||||
setCart([]);
|
||||
addToast('Venta procesada exitosamente', { type: 'success' });
|
||||
showToast.success('Venta procesada exitosamente');
|
||||
};
|
||||
|
||||
// Loading and error states
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useTriggerDailyScheduler } from '../../../../api';
|
||||
import type { PurchaseOrderStatus, PurchaseOrderPriority, PurchaseOrderDetail } from '../../../../api/services/purchase_orders';
|
||||
import { useTenantStore } from '../../../../stores/tenant.store';
|
||||
import { useUserById } from '../../../../api/hooks/user';
|
||||
import toast from 'react-hot-toast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
|
||||
const ProcurementPage: React.FC = () => {
|
||||
// State
|
||||
@@ -59,7 +59,6 @@ const ProcurementPage: React.FC = () => {
|
||||
const approvePOMutation = useApprovePurchaseOrder();
|
||||
const rejectPOMutation = useRejectPurchaseOrder();
|
||||
const updatePOMutation = useUpdatePurchaseOrder();
|
||||
const triggerSchedulerMutation = useTriggerDailyScheduler();
|
||||
|
||||
// Filter POs
|
||||
const filteredPOs = useMemo(() => {
|
||||
@@ -129,11 +128,11 @@ const ProcurementPage: React.FC = () => {
|
||||
poId: po.id,
|
||||
data: { status: 'SENT_TO_SUPPLIER' }
|
||||
});
|
||||
toast.success('Orden enviada al proveedor');
|
||||
showToast.success('Orden enviada al proveedor');
|
||||
refetchPOs();
|
||||
} catch (error) {
|
||||
console.error('Error sending PO to supplier:', error);
|
||||
toast.error('Error al enviar orden al proveedor');
|
||||
showToast.error('Error al enviar orden al proveedor');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -144,11 +143,11 @@ const ProcurementPage: React.FC = () => {
|
||||
poId: po.id,
|
||||
data: { status: 'CONFIRMED' }
|
||||
});
|
||||
toast.success('Orden confirmada');
|
||||
showToast.success('Orden confirmada');
|
||||
refetchPOs();
|
||||
} catch (error) {
|
||||
console.error('Error confirming PO:', error);
|
||||
toast.error('Error al confirmar orden');
|
||||
showToast.error('Error al confirmar orden');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,10 +161,10 @@ const ProcurementPage: React.FC = () => {
|
||||
poId: selectedPOId,
|
||||
notes: approvalNotes || undefined
|
||||
});
|
||||
toast.success('Orden aprobada exitosamente');
|
||||
showToast.success('Orden aprobada exitosamente');
|
||||
} else {
|
||||
if (!approvalNotes.trim()) {
|
||||
toast.error('Debes proporcionar una razón para rechazar');
|
||||
showToast.error('Debes proporcionar una razón para rechazar');
|
||||
return;
|
||||
}
|
||||
await rejectPOMutation.mutateAsync({
|
||||
@@ -173,7 +172,7 @@ const ProcurementPage: React.FC = () => {
|
||||
poId: selectedPOId,
|
||||
reason: approvalNotes
|
||||
});
|
||||
toast.success('Orden rechazada');
|
||||
showToast.success('Orden rechazada');
|
||||
}
|
||||
setShowApprovalModal(false);
|
||||
setShowDetailsModal(false);
|
||||
@@ -181,18 +180,18 @@ const ProcurementPage: React.FC = () => {
|
||||
refetchPOs();
|
||||
} catch (error) {
|
||||
console.error('Error in approval action:', error);
|
||||
toast.error('Error al procesar aprobación');
|
||||
showToast.error('Error al procesar aprobación');
|
||||
}
|
||||
};
|
||||
|
||||
const handleTriggerScheduler = async () => {
|
||||
try {
|
||||
await triggerSchedulerMutation.mutateAsync(tenantId);
|
||||
toast.success('Scheduler ejecutado exitosamente');
|
||||
showToast.success('Scheduler ejecutado exitosamente');
|
||||
refetchPOs();
|
||||
} catch (error) {
|
||||
console.error('Error triggering scheduler:', error);
|
||||
toast.error('Error al ejecutar scheduler');
|
||||
showToast.error('Error al ejecutar scheduler');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -715,16 +714,6 @@ const ProcurementPage: React.FC = () => {
|
||||
title="Órdenes de Compra"
|
||||
description="Gestiona órdenes de compra y aprovisionamiento"
|
||||
actions={[
|
||||
{
|
||||
id: 'trigger-scheduler',
|
||||
label: triggerSchedulerMutation.isPending ? 'Ejecutando...' : 'Ejecutar Scheduler',
|
||||
icon: Play,
|
||||
onClick: handleTriggerScheduler,
|
||||
variant: 'outline',
|
||||
size: 'sm',
|
||||
disabled: triggerSchedulerMutation.isPending,
|
||||
loading: triggerSchedulerMutation.isPending
|
||||
},
|
||||
{
|
||||
id: 'create-po',
|
||||
label: 'Nueva Orden',
|
||||
@@ -857,7 +846,7 @@ const ProcurementPage: React.FC = () => {
|
||||
onSuccess={() => {
|
||||
setShowCreatePOModal(false);
|
||||
refetchPOs();
|
||||
toast.success('Orden de compra creada exitosamente');
|
||||
showToast.success('Orden de compra creada exitosamente');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from '../../../../api';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ProcessStage } from '../../../../api/types/qualityTemplates';
|
||||
import toast from 'react-hot-toast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
|
||||
const ProductionPage: React.FC = () => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -58,7 +58,6 @@ const ProductionPage: React.FC = () => {
|
||||
// Mutations
|
||||
const createBatchMutation = useCreateProductionBatch();
|
||||
const updateBatchStatusMutation = useUpdateBatchStatus();
|
||||
const triggerSchedulerMutation = useTriggerProductionScheduler();
|
||||
|
||||
// Handlers
|
||||
const handleCreateBatch = async (batchData: ProductionBatchCreate) => {
|
||||
@@ -76,10 +75,10 @@ const ProductionPage: React.FC = () => {
|
||||
const handleTriggerScheduler = async () => {
|
||||
try {
|
||||
await triggerSchedulerMutation.mutateAsync(tenantId);
|
||||
toast.success('Scheduler ejecutado exitosamente');
|
||||
showToast.success('Scheduler ejecutado exitosamente');
|
||||
} catch (error) {
|
||||
console.error('Error triggering scheduler:', error);
|
||||
toast.error('Error al ejecutar scheduler');
|
||||
showToast.error('Error al ejecutar scheduler');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -300,16 +299,6 @@ const ProductionPage: React.FC = () => {
|
||||
title="Gestión de Producción"
|
||||
description="Planifica y controla la producción diaria de tu panadería"
|
||||
actions={[
|
||||
{
|
||||
id: 'trigger-scheduler',
|
||||
label: triggerSchedulerMutation.isPending ? 'Ejecutando...' : 'Ejecutar Scheduler',
|
||||
icon: Play,
|
||||
onClick: handleTriggerScheduler,
|
||||
variant: 'outline',
|
||||
size: 'sm',
|
||||
disabled: triggerSchedulerMutation.isPending,
|
||||
loading: triggerSchedulerMutation.isPending
|
||||
},
|
||||
{
|
||||
id: 'create-batch',
|
||||
label: 'Nueva Orden de Producción',
|
||||
@@ -731,4 +720,4 @@ const ProductionPage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductionPage;
|
||||
export default ProductionPage;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
||||
import { Store, MapPin, Clock, Phone, Mail, Globe, Save, X, Edit3, Zap, Plus, Settings, Trash2, Wifi, WifiOff, AlertCircle, CheckCircle, Loader, Eye, EyeOff, Info } from 'lucide-react';
|
||||
import { Button, Card, Input, Select, Modal, Badge, Tabs } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
import { usePOSConfigurationData, usePOSConfigurationManager } from '../../../../api/hooks/pos';
|
||||
import { POSConfiguration, POSProviderConfig } from '../../../../api/types/pos';
|
||||
import { posService } from '../../../../api/services/pos';
|
||||
@@ -38,7 +38,7 @@ interface BusinessHours {
|
||||
|
||||
|
||||
const BakeryConfigPage: React.FC = () => {
|
||||
const { addToast } = useToast();
|
||||
|
||||
const currentTenant = useCurrentTenant();
|
||||
const { loadUserTenants, setCurrentTenant } = useTenantActions();
|
||||
const tenantId = currentTenant?.id || '';
|
||||
@@ -287,9 +287,9 @@ const BakeryConfigPage: React.FC = () => {
|
||||
}
|
||||
|
||||
setHasUnsavedChanges(false);
|
||||
addToast('Configuración actualizada correctamente', { type: 'success' });
|
||||
showToast.success('Configuración actualizada correctamente');
|
||||
} catch (error) {
|
||||
addToast(`Error al actualizar: ${error instanceof Error ? error.message : 'Error desconocido'}`, { type: 'error' });
|
||||
showToast.error(`Error al actualizar: ${error instanceof Error ? error.message : 'Error desconocido'}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -364,7 +364,7 @@ const BakeryConfigPage: React.FC = () => {
|
||||
.map(field => field.label);
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
addToast(`Campos requeridos: ${missingFields.join(', ')}`, 'error');
|
||||
showToast.error(`Campos requeridos: ${missingFields.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -375,7 +375,7 @@ const BakeryConfigPage: React.FC = () => {
|
||||
config_id: selectedPosConfig.id,
|
||||
...posFormData,
|
||||
});
|
||||
addToast('Configuración actualizada correctamente', 'success');
|
||||
showToast.success('Configuración actualizada correctamente');
|
||||
setShowEditPosModal(false);
|
||||
loadPosConfigurations();
|
||||
} else {
|
||||
@@ -384,12 +384,12 @@ const BakeryConfigPage: React.FC = () => {
|
||||
tenant_id: tenantId,
|
||||
...posFormData,
|
||||
});
|
||||
addToast('Configuración creada correctamente', 'success');
|
||||
showToast.success('Configuración creada correctamente');
|
||||
setShowAddPosModal(false);
|
||||
loadPosConfigurations();
|
||||
}
|
||||
} catch (error) {
|
||||
addToast('Error al guardar la configuración', 'error');
|
||||
showToast.error('Error al guardar la configuración');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -402,12 +402,12 @@ const BakeryConfigPage: React.FC = () => {
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
addToast('Conexión exitosa', 'success');
|
||||
showToast.success('Conexión exitosa');
|
||||
} else {
|
||||
addToast(`Error en la conexión: ${response.message || 'Error desconocido'}`, 'error');
|
||||
showToast.error(`Error en la conexión: ${response.message || 'Error desconocido'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
addToast('Error al probar la conexión', 'error');
|
||||
showToast.error('Error al probar la conexión');
|
||||
} finally {
|
||||
setTestingConnection(null);
|
||||
}
|
||||
@@ -423,10 +423,10 @@ const BakeryConfigPage: React.FC = () => {
|
||||
tenant_id: tenantId,
|
||||
config_id: configId,
|
||||
});
|
||||
addToast('Configuración eliminada correctamente', 'success');
|
||||
showToast.success('Configuración eliminada correctamente');
|
||||
loadPosConfigurations();
|
||||
} catch (error) {
|
||||
addToast('Error al eliminar la configuración', 'error');
|
||||
showToast.error('Error al eliminar la configuración');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1116,4 +1116,4 @@ const BakeryConfigPage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default BakeryConfigPage;
|
||||
export default BakeryConfigPage;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Store, MapPin, Clock, Settings as SettingsIcon, Save, X, AlertCircle, L
|
||||
import { Button, Card, Input, Select } from '../../../../components/ui';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../../../components/ui/Tabs';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
import { useUpdateTenant } from '../../../../api/hooks/tenant';
|
||||
import { useCurrentTenant, useTenantActions } from '../../../../stores/tenant.store';
|
||||
import { useSettings, useUpdateSettings } from '../../../../api/hooks/settings';
|
||||
@@ -49,7 +49,7 @@ interface BusinessHours {
|
||||
|
||||
const BakerySettingsPage: React.FC = () => {
|
||||
const { t } = useTranslation('settings');
|
||||
const { addToast } = useToast();
|
||||
|
||||
const currentTenant = useCurrentTenant();
|
||||
const { loadUserTenants, setCurrentTenant } = useTenantActions();
|
||||
const tenantId = currentTenant?.id || '';
|
||||
@@ -221,10 +221,10 @@ const BakerySettingsPage: React.FC = () => {
|
||||
}
|
||||
|
||||
setHasUnsavedChanges(false);
|
||||
addToast(t('bakery.save_success'), { type: 'success' });
|
||||
showToast.success(t('bakery.save_success'));
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : t('common.error');
|
||||
addToast(`${t('bakery.save_error')}: ${errorMessage}`, { type: 'error' });
|
||||
showToast.error(`${t('bakery.save_error')}: ${errorMessage}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -252,10 +252,10 @@ const BakerySettingsPage: React.FC = () => {
|
||||
});
|
||||
|
||||
setHasUnsavedChanges(false);
|
||||
addToast(t('bakery.save_success'), { type: 'success' });
|
||||
showToast.success(t('bakery.save_success'));
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : t('common.error');
|
||||
addToast(`${t('bakery.save_error')}: ${errorMessage}`, { type: 'error' });
|
||||
showToast.error(`${t('bakery.save_error')}: ${errorMessage}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
Sun,
|
||||
Settings
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
|
||||
// Backend-aligned preference types
|
||||
export interface NotificationPreferences {
|
||||
@@ -75,7 +75,7 @@ const CommunicationPreferences: React.FC<CommunicationPreferencesProps> = ({
|
||||
onReset,
|
||||
hasChanges
|
||||
}) => {
|
||||
const { addToast } = useToast();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [preferences, setPreferences] = useState<NotificationPreferences>({
|
||||
@@ -161,9 +161,9 @@ const CommunicationPreferences: React.FC<CommunicationPreferencesProps> = ({
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await onSave(preferences);
|
||||
addToast('Preferencias guardadas correctamente', 'success');
|
||||
showToast.success('Preferencias guardadas correctamente');
|
||||
} catch (error) {
|
||||
addToast('Error al guardar las preferencias', 'error');
|
||||
showToast.error('Error al guardar las preferencias');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -700,4 +700,4 @@ const CommunicationPreferences: React.FC<CommunicationPreferencesProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default CommunicationPreferences;
|
||||
export default CommunicationPreferences;
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import { Button, Card, Avatar, Input, Select } from '../../../../components/ui';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../../../components/ui/Tabs';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
import { useAuthUser, useAuthActions } from '../../../../stores/auth.store';
|
||||
import { useAuthProfile, useUpdateProfile, useChangePassword } from '../../../../api/hooks/auth';
|
||||
import { useCurrentTenant } from '../../../../stores';
|
||||
@@ -49,7 +49,7 @@ interface PasswordData {
|
||||
const NewProfileSettingsPage: React.FC = () => {
|
||||
const { t } = useTranslation('settings');
|
||||
const navigate = useNavigate();
|
||||
const { addToast } = useToast();
|
||||
|
||||
const user = useAuthUser();
|
||||
const { logout } = useAuthActions();
|
||||
const currentTenant = useCurrentTenant();
|
||||
@@ -169,9 +169,9 @@ const NewProfileSettingsPage: React.FC = () => {
|
||||
await updateProfileMutation.mutateAsync(profileData);
|
||||
|
||||
setIsEditing(false);
|
||||
addToast(t('profile.save_changes'), { type: 'success' });
|
||||
showToast.success(t('profile.save_changes'));
|
||||
} catch (error) {
|
||||
addToast(t('common.error'), { type: 'error' });
|
||||
showToast.error(t('common.error'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -191,9 +191,9 @@ const NewProfileSettingsPage: React.FC = () => {
|
||||
|
||||
setShowPasswordForm(false);
|
||||
setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
||||
addToast(t('profile.password.change_success'), { type: 'success' });
|
||||
showToast.success(t('profile.password.change_success'));
|
||||
} catch (error) {
|
||||
addToast(t('profile.password.change_error'), { type: 'error' });
|
||||
showToast.error(t('profile.password.change_error'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -246,9 +246,9 @@ const NewProfileSettingsPage: React.FC = () => {
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
addToast(t('profile.privacy.export_success'), { type: 'success' });
|
||||
showToast.success(t('profile.privacy.export_success'));
|
||||
} catch (err) {
|
||||
addToast(t('profile.privacy.export_error'), { type: 'error' });
|
||||
showToast.error(t('profile.privacy.export_error'));
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
@@ -256,12 +256,12 @@ const NewProfileSettingsPage: React.FC = () => {
|
||||
|
||||
const handleAccountDeletion = async () => {
|
||||
if (deleteConfirmEmail.toLowerCase() !== user?.email?.toLowerCase()) {
|
||||
addToast(t('common.error'), { type: 'error' });
|
||||
showToast.error(t('common.error'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!deletePassword) {
|
||||
addToast(t('common.error'), { type: 'error' });
|
||||
showToast.error(t('common.error'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -270,14 +270,14 @@ const NewProfileSettingsPage: React.FC = () => {
|
||||
const { authService } = await import('../../../../api');
|
||||
await authService.deleteAccount(deleteConfirmEmail, deletePassword, deleteReason);
|
||||
|
||||
addToast(t('common.success'), { type: 'success' });
|
||||
showToast.success(t('common.success'));
|
||||
|
||||
setTimeout(() => {
|
||||
logout();
|
||||
navigate('/');
|
||||
}, 2000);
|
||||
} catch (err: any) {
|
||||
addToast(err.message || t('common.error'), { type: 'error' });
|
||||
showToast.error(err.message || t('common.error'));
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button, Card, Avatar, Input, Select, Tabs, Badge, Modal } from '../../.
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { useAuthUser } from '../../../../stores/auth.store';
|
||||
import { useCurrentTenant } from '../../../../stores';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
import { useAuthProfile, useUpdateProfile, useChangePassword } from '../../../../api/hooks/auth';
|
||||
import { subscriptionService, type UsageSummary, type AvailablePlans } from '../../../../api';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -30,7 +30,7 @@ interface PasswordData {
|
||||
const ProfilePage: React.FC = () => {
|
||||
const user = useAuthUser();
|
||||
const { t } = useTranslation(['settings', 'auth']);
|
||||
const { addToast } = useToast();
|
||||
|
||||
|
||||
const { data: profile, isLoading: profileLoading, error: profileError } = useAuthProfile();
|
||||
const updateProfileMutation = useUpdateProfile();
|
||||
@@ -176,9 +176,9 @@ const ProfilePage: React.FC = () => {
|
||||
await updateProfileMutation.mutateAsync(profileData);
|
||||
|
||||
setIsEditing(false);
|
||||
addToast('Perfil actualizado correctamente', 'success');
|
||||
showToast.success('Perfil actualizado correctamente');
|
||||
} catch (error) {
|
||||
addToast('No se pudo actualizar tu perfil', 'error');
|
||||
showToast.error('No se pudo actualizar tu perfil');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -198,9 +198,9 @@ const ProfilePage: React.FC = () => {
|
||||
|
||||
setShowPasswordForm(false);
|
||||
setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
||||
addToast('Contraseña actualizada correctamente', 'success');
|
||||
showToast.success('Contraseña actualizada correctamente');
|
||||
} catch (error) {
|
||||
addToast('No se pudo cambiar tu contraseña', 'error');
|
||||
showToast.error('No se pudo cambiar tu contraseña');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -269,7 +269,7 @@ const ProfilePage: React.FC = () => {
|
||||
const tenantId = currentTenant?.id || user?.tenant_id;
|
||||
|
||||
if (!tenantId) {
|
||||
addToast('No se encontró información del tenant', 'error');
|
||||
showToast.error('No se encontró información del tenant');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ const ProfilePage: React.FC = () => {
|
||||
setAvailablePlans(plans);
|
||||
} catch (error) {
|
||||
console.error('Error loading subscription data:', error);
|
||||
addToast("No se pudo cargar la información de suscripción", 'error');
|
||||
showToast.error("No se pudo cargar la información de suscripción");
|
||||
} finally {
|
||||
setSubscriptionLoading(false);
|
||||
}
|
||||
@@ -299,7 +299,7 @@ const ProfilePage: React.FC = () => {
|
||||
const tenantId = currentTenant?.id || user?.tenant_id;
|
||||
|
||||
if (!tenantId || !selectedPlan) {
|
||||
addToast('Información de tenant no disponible', 'error');
|
||||
showToast.error('Información de tenant no disponible');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -312,24 +312,24 @@ const ProfilePage: React.FC = () => {
|
||||
);
|
||||
|
||||
if (!validation.can_upgrade) {
|
||||
addToast(validation.reason || 'No se puede actualizar el plan', 'error');
|
||||
return;
|
||||
showToast.error(validation.reason || 'No se puede actualizar el plan');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await subscriptionService.upgradePlan(tenantId, selectedPlan);
|
||||
|
||||
if (result.success) {
|
||||
addToast(result.message, 'success');
|
||||
showToast.success(result.message);
|
||||
|
||||
await loadSubscriptionData();
|
||||
setUpgradeDialogOpen(false);
|
||||
setSelectedPlan('');
|
||||
} else {
|
||||
addToast('Error al cambiar el plan', 'error');
|
||||
showToast.error('Error al cambiar el plan');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error upgrading plan:', error);
|
||||
addToast('Error al procesar el cambio de plan', 'error');
|
||||
showToast.error('Error al procesar el cambio de plan');
|
||||
} finally {
|
||||
setUpgrading(false);
|
||||
}
|
||||
@@ -953,4 +953,4 @@ const ProfilePage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePage;
|
||||
export default ProfilePage;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DialogModal } from '../../../../components/ui/DialogModal/DialogModal';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { useAuthUser } from '../../../../stores/auth.store';
|
||||
import { useCurrentTenant } from '../../../../stores';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
import { subscriptionService, type UsageSummary, type AvailablePlans } from '../../../../api';
|
||||
import { useSubscriptionEvents } from '../../../../contexts/SubscriptionEventsContext';
|
||||
import { SubscriptionPricingCards } from '../../../../components/subscription/SubscriptionPricingCards';
|
||||
@@ -13,7 +13,6 @@ import { SubscriptionPricingCards } from '../../../../components/subscription/Su
|
||||
const SubscriptionPage: React.FC = () => {
|
||||
const user = useAuthUser();
|
||||
const currentTenant = useCurrentTenant();
|
||||
const { addToast } = useToast();
|
||||
const { notifySubscriptionChanged } = useSubscriptionEvents();
|
||||
|
||||
const [usageSummary, setUsageSummary] = useState<UsageSummary | null>(null);
|
||||
@@ -36,7 +35,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
const tenantId = currentTenant?.id || user?.tenant_id;
|
||||
|
||||
if (!tenantId) {
|
||||
addToast('No se encontró información del tenant', { type: 'error' });
|
||||
showToast.error('No se encontró información del tenant');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -120,7 +119,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
setAvailablePlans(plans);
|
||||
} catch (error) {
|
||||
console.error('Error loading subscription data:', error);
|
||||
addToast("No se pudo cargar la información de suscripción", { type: 'error' });
|
||||
showToast.error("No se pudo cargar la información de suscripción");
|
||||
} finally {
|
||||
setSubscriptionLoading(false);
|
||||
}
|
||||
@@ -135,7 +134,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
const tenantId = currentTenant?.id || user?.tenant_id;
|
||||
|
||||
if (!tenantId || !selectedPlan) {
|
||||
addToast('Información de tenant no disponible', { type: 'error' });
|
||||
showToast.error('Información de tenant no disponible');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -148,14 +147,17 @@ const SubscriptionPage: React.FC = () => {
|
||||
);
|
||||
|
||||
if (!validation.can_upgrade) {
|
||||
addToast(validation.reason || 'No se puede actualizar el plan', { type: 'error' });
|
||||
showToast.error(validation.reason || 'No se puede actualizar el plan');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await subscriptionService.upgradePlan(tenantId, selectedPlan);
|
||||
|
||||
if (result.success) {
|
||||
addToast(result.message, { type: 'success' });
|
||||
showToast.success(result.message);
|
||||
|
||||
// Invalidate cache to ensure fresh data on next fetch
|
||||
subscriptionService.invalidateCache();
|
||||
|
||||
// Broadcast subscription change event to refresh sidebar and other components
|
||||
notifySubscriptionChanged();
|
||||
@@ -164,11 +166,11 @@ const SubscriptionPage: React.FC = () => {
|
||||
setUpgradeDialogOpen(false);
|
||||
setSelectedPlan('');
|
||||
} else {
|
||||
addToast('Error al cambiar el plan', { type: 'error' });
|
||||
showToast.error('Error al cambiar el plan');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error upgrading plan:', error);
|
||||
addToast('Error al procesar el cambio de plan', { type: 'error' });
|
||||
showToast.error('Error al procesar el cambio de plan');
|
||||
} finally {
|
||||
setUpgrading(false);
|
||||
}
|
||||
@@ -182,7 +184,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
const tenantId = currentTenant?.id || user?.tenant_id;
|
||||
|
||||
if (!tenantId) {
|
||||
addToast('Información de tenant no disponible', { type: 'error' });
|
||||
showToast.error('Información de tenant no disponible');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -199,9 +201,8 @@ const SubscriptionPage: React.FC = () => {
|
||||
day: 'numeric'
|
||||
});
|
||||
|
||||
addToast(
|
||||
`Suscripción cancelada. Acceso de solo lectura a partir del ${effectiveDate} (${daysRemaining} días restantes)`,
|
||||
{ type: 'success' }
|
||||
showToast.success(
|
||||
`Suscripción cancelada. Acceso de solo lectura a partir del ${effectiveDate} (${daysRemaining} días restantes)`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -209,7 +210,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
setCancellationDialogOpen(false);
|
||||
} catch (error) {
|
||||
console.error('Error cancelling subscription:', error);
|
||||
addToast('Error al cancelar la suscripción', { type: 'error' });
|
||||
showToast.error('Error al cancelar la suscripción');
|
||||
} finally {
|
||||
setCancelling(false);
|
||||
}
|
||||
@@ -219,7 +220,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
const tenantId = currentTenant?.id || user?.tenant_id;
|
||||
|
||||
if (!tenantId) {
|
||||
addToast('No se encontró información del tenant', { type: 'error' });
|
||||
showToast.error('No se encontró información del tenant');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -236,7 +237,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Error loading invoices:', error);
|
||||
addToast('Error al cargar las facturas', { type: 'error' });
|
||||
showToast.error('Error al cargar las facturas');
|
||||
} finally {
|
||||
setInvoicesLoading(false);
|
||||
}
|
||||
@@ -245,7 +246,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
const handleDownloadInvoice = (invoiceId: string) => {
|
||||
// In a real implementation, this would download the actual invoice
|
||||
console.log(`Downloading invoice: ${invoiceId}`);
|
||||
addToast(`Descargando factura ${invoiceId}`, { type: 'info' });
|
||||
showToast.info(`Descargando factura ${invoiceId}`);
|
||||
};
|
||||
|
||||
const ProgressBar: React.FC<{ value: number; className?: string }> = ({ value, className = '' }) => {
|
||||
@@ -389,7 +390,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
<ProgressBar value={usageSummary.usage.users.usage_percentage} />
|
||||
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
||||
<span>{usageSummary.usage.users.usage_percentage}% utilizado</span>
|
||||
<span className="font-medium">{usageSummary.usage.users.unlimited ? 'Ilimitado' : `${usageSummary.usage.users.limit - usageSummary.usage.users.current} restantes`}</span>
|
||||
<span className="font-medium">{usageSummary.usage.users.unlimited ? 'Ilimitado' : `${(usageSummary.usage.users.limit ?? 0) - usageSummary.usage.users.current} restantes`}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -410,7 +411,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
<ProgressBar value={usageSummary.usage.locations.usage_percentage} />
|
||||
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
||||
<span>{usageSummary.usage.locations.usage_percentage}% utilizado</span>
|
||||
<span className="font-medium">{usageSummary.usage.locations.unlimited ? 'Ilimitado' : `${usageSummary.usage.locations.limit - usageSummary.usage.locations.current} restantes`}</span>
|
||||
<span className="font-medium">{usageSummary.usage.locations.unlimited ? 'Ilimitado' : `${(usageSummary.usage.locations.limit ?? 0) - usageSummary.usage.locations.current} restantes`}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -437,7 +438,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
<ProgressBar value={usageSummary.usage.products.usage_percentage} />
|
||||
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
||||
<span>{usageSummary.usage.products.usage_percentage}% utilizado</span>
|
||||
<span className="font-medium">{usageSummary.usage.products.unlimited ? 'Ilimitado' : `${usageSummary.usage.products.limit - usageSummary.usage.products.current} restantes`}</span>
|
||||
<span className="font-medium">{usageSummary.usage.products.unlimited ? 'Ilimitado' : `${(usageSummary.usage.products.limit ?? 0) - usageSummary.usage.products.current} restantes`}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -458,7 +459,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
<ProgressBar value={usageSummary.usage.recipes.usage_percentage} />
|
||||
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
||||
<span>{usageSummary.usage.recipes.usage_percentage}% utilizado</span>
|
||||
<span className="font-medium">{usageSummary.usage.recipes.unlimited ? 'Ilimitado' : `${usageSummary.usage.recipes.limit - usageSummary.usage.recipes.current} restantes`}</span>
|
||||
<span className="font-medium">{usageSummary.usage.recipes.unlimited ? 'Ilimitado' : `${(usageSummary.usage.recipes.limit ?? 0) - usageSummary.usage.recipes.current} restantes`}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -479,7 +480,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
<ProgressBar value={usageSummary.usage.suppliers.usage_percentage} />
|
||||
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
||||
<span>{usageSummary.usage.suppliers.usage_percentage}% utilizado</span>
|
||||
<span className="font-medium">{usageSummary.usage.suppliers.unlimited ? 'Ilimitado' : `${usageSummary.usage.suppliers.limit - usageSummary.usage.suppliers.current} restantes`}</span>
|
||||
<span className="font-medium">{usageSummary.usage.suppliers.unlimited ? 'Ilimitado' : `${(usageSummary.usage.suppliers.limit ?? 0) - usageSummary.usage.suppliers.current} restantes`}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -506,7 +507,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
<ProgressBar value={usageSummary.usage.training_jobs_today.usage_percentage} />
|
||||
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
||||
<span>{usageSummary.usage.training_jobs_today.usage_percentage}% utilizado</span>
|
||||
<span className="font-medium">{usageSummary.usage.training_jobs_today.unlimited ? 'Ilimitado' : `${usageSummary.usage.training_jobs_today.limit - usageSummary.usage.training_jobs_today.current} restantes`}</span>
|
||||
<span className="font-medium">{usageSummary.usage.training_jobs_today.unlimited ? 'Ilimitado' : `${(usageSummary.usage.training_jobs_today.limit ?? 0) - usageSummary.usage.training_jobs_today.current} restantes`}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -527,7 +528,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
<ProgressBar value={usageSummary.usage.forecasts_today.usage_percentage} />
|
||||
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
||||
<span>{usageSummary.usage.forecasts_today.usage_percentage}% utilizado</span>
|
||||
<span className="font-medium">{usageSummary.usage.forecasts_today.unlimited ? 'Ilimitado' : `${usageSummary.usage.forecasts_today.limit - usageSummary.usage.forecasts_today.current} restantes`}</span>
|
||||
<span className="font-medium">{usageSummary.usage.forecasts_today.unlimited ? 'Ilimitado' : `${(usageSummary.usage.forecasts_today.limit ?? 0) - usageSummary.usage.forecasts_today.current} restantes`}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -554,7 +555,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
<ProgressBar value={usageSummary.usage.api_calls_this_hour.usage_percentage} />
|
||||
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
||||
<span>{usageSummary.usage.api_calls_this_hour.usage_percentage}% utilizado</span>
|
||||
<span className="font-medium">{usageSummary.usage.api_calls_this_hour.unlimited ? 'Ilimitado' : `${usageSummary.usage.api_calls_this_hour.limit - usageSummary.usage.api_calls_this_hour.current} restantes`}</span>
|
||||
<span className="font-medium">{usageSummary.usage.api_calls_this_hour.unlimited ? 'Ilimitado' : `${(usageSummary.usage.api_calls_this_hour.limit ?? 0) - usageSummary.usage.api_calls_this_hour.current} restantes`}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -575,7 +576,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
<ProgressBar value={usageSummary.usage.file_storage_used_gb.usage_percentage} />
|
||||
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
||||
<span>{usageSummary.usage.file_storage_used_gb.usage_percentage}% utilizado</span>
|
||||
<span className="font-medium">{usageSummary.usage.file_storage_used_gb.unlimited ? 'Ilimitado' : `${(usageSummary.usage.file_storage_used_gb.limit - usageSummary.usage.file_storage_used_gb.current).toFixed(2)} GB restantes`}</span>
|
||||
<span className="font-medium">{usageSummary.usage.file_storage_used_gb.unlimited ? 'Ilimitado' : `${((usageSummary.usage.file_storage_used_gb.limit ?? 0) - usageSummary.usage.file_storage_used_gb.current).toFixed(2)} GB restantes`}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,13 +9,13 @@ import { useUserActivity } from '../../../../api/hooks/user';
|
||||
import { userService } from '../../../../api/services/user';
|
||||
import { useAuthUser } from '../../../../stores/auth.store';
|
||||
import { useCurrentTenant, useCurrentTenantAccess } from '../../../../stores/tenant.store';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
import { TENANT_ROLES, type TenantRole } from '../../../../types/roles';
|
||||
import { subscriptionService } from '../../../../api/services/subscription';
|
||||
|
||||
const TeamPage: React.FC = () => {
|
||||
const { t } = useTranslation(['settings']);
|
||||
const { addToast } = useToast();
|
||||
|
||||
const currentUser = useAuthUser();
|
||||
const currentTenant = useCurrentTenant();
|
||||
const currentTenantAccess = useCurrentTenantAccess();
|
||||
@@ -310,7 +310,7 @@ const TeamPage: React.FC = () => {
|
||||
setShowActivityModal(true);
|
||||
} catch (error) {
|
||||
console.error('Error fetching user activity:', error);
|
||||
addToast('Error al cargar la actividad del usuario', { type: 'error' });
|
||||
showToast.error('Error al cargar la actividad del usuario');
|
||||
} finally {
|
||||
setActivityLoading(false);
|
||||
}
|
||||
@@ -359,9 +359,9 @@ const TeamPage: React.FC = () => {
|
||||
memberUserId,
|
||||
});
|
||||
|
||||
addToast('Miembro removido exitosamente', { type: 'success' });
|
||||
showToast.success('Miembro removido exitosamente');
|
||||
} catch (error) {
|
||||
addToast('Error al remover miembro', { type: 'error' });
|
||||
showToast.error('Error al remover miembro');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -375,9 +375,9 @@ const TeamPage: React.FC = () => {
|
||||
newRole,
|
||||
});
|
||||
|
||||
addToast('Rol actualizado exitosamente', { type: 'success' });
|
||||
showToast.success('Rol actualizado exitosamente');
|
||||
} catch (error) {
|
||||
addToast('Error al actualizar rol', { type: 'error' });
|
||||
showToast.error('Error al actualizar rol');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -556,7 +556,7 @@ const TeamPage: React.FC = () => {
|
||||
if (!usageCheck.allowed) {
|
||||
const errorMessage = usageCheck.message ||
|
||||
`Has alcanzado el límite de ${usageCheck.limit} usuarios para tu plan. Actualiza tu suscripción para agregar más miembros.`;
|
||||
addToast(errorMessage, { type: 'error' });
|
||||
showToast.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
@@ -579,14 +579,14 @@ const TeamPage: React.FC = () => {
|
||||
timezone: 'Europe/Madrid'
|
||||
}
|
||||
});
|
||||
addToast('Usuario creado y agregado exitosamente', { type: 'success' });
|
||||
showToast.success('Usuario creado y agregado exitosamente');
|
||||
} else {
|
||||
await addMemberMutation.mutateAsync({
|
||||
tenantId,
|
||||
userId: userData.userId!,
|
||||
role,
|
||||
});
|
||||
addToast('Miembro agregado exitosamente', { type: 'success' });
|
||||
showToast.success('Miembro agregado exitosamente');
|
||||
}
|
||||
|
||||
setShowAddForm(false);
|
||||
@@ -597,9 +597,8 @@ const TeamPage: React.FC = () => {
|
||||
// Limit error already toasted above
|
||||
throw error;
|
||||
}
|
||||
addToast(
|
||||
userData.createUser ? 'Error al crear usuario' : 'Error al agregar miembro',
|
||||
{ type: 'error' }
|
||||
showToast.error(
|
||||
userData.createUser ? 'Error al crear usuario' : 'Error al agregar miembro'
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ import {
|
||||
getCookieCategories,
|
||||
CookiePreferences
|
||||
} from '../../components/ui/CookieConsent';
|
||||
import { useToast } from '../../hooks/ui/useToast';
|
||||
import { showToast } from '../../utils/toast';
|
||||
|
||||
export const CookiePreferencesPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { success } = useToast();
|
||||
|
||||
|
||||
const [preferences, setPreferences] = useState<CookiePreferences>({
|
||||
essential: true,
|
||||
@@ -48,7 +48,7 @@ export const CookiePreferencesPage: React.FC = () => {
|
||||
};
|
||||
|
||||
saveCookieConsent(updatedPreferences);
|
||||
success(
|
||||
showToast.success(
|
||||
t('common:cookie.preferences_saved', 'Your cookie preferences have been saved successfully.'),
|
||||
{ title: t('common:cookie.success', 'Preferences Saved') }
|
||||
);
|
||||
@@ -66,7 +66,7 @@ export const CookiePreferencesPage: React.FC = () => {
|
||||
|
||||
saveCookieConsent(allEnabled);
|
||||
setPreferences(allEnabled);
|
||||
success(
|
||||
showToast.success(
|
||||
t('common:cookie.all_accepted', 'All cookies have been accepted.'),
|
||||
{ title: t('common:cookie.success', 'Preferences Saved') }
|
||||
);
|
||||
@@ -84,7 +84,7 @@ export const CookiePreferencesPage: React.FC = () => {
|
||||
|
||||
saveCookieConsent(essentialOnly);
|
||||
setPreferences(essentialOnly);
|
||||
success(
|
||||
showToast.success(
|
||||
t('common:cookie.only_essential', 'Only essential cookies are enabled.'),
|
||||
{ title: t('common:cookie.success', 'Preferences Saved') }
|
||||
);
|
||||
|
||||
@@ -32,7 +32,9 @@ import {
|
||||
Target,
|
||||
CheckCircle2,
|
||||
Sparkles,
|
||||
Recycle
|
||||
Recycle,
|
||||
MapPin,
|
||||
Globe
|
||||
} from 'lucide-react';
|
||||
|
||||
const LandingPage: React.FC = () => {
|
||||
@@ -56,6 +58,7 @@ const LandingPage: React.FC = () => {
|
||||
variant: "default",
|
||||
navigationItems: [
|
||||
{ id: 'features', label: t('landing:navigation.features', 'Características'), href: '#features' },
|
||||
{ id: 'local', label: t('landing:navigation.local', 'Datos Locales'), href: '#local' },
|
||||
{ id: 'benefits', label: t('landing:navigation.benefits', 'Beneficios'), href: '#benefits' },
|
||||
{ id: 'pricing', label: t('landing:navigation.pricing', 'Precios'), href: '#pricing' },
|
||||
{ id: 'faq', label: t('landing:navigation.faq', 'Preguntas Frecuentes'), href: '#faq' }
|
||||
@@ -76,6 +79,10 @@ const LandingPage: React.FC = () => {
|
||||
<Shield className="w-4 h-4 mr-2" />
|
||||
{t('landing:hero.badge_sustainability', 'Reducción de Desperdicio Alimentario')}
|
||||
</span>
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-500/10 text-blue-600 dark:text-blue-400">
|
||||
<MapPin className="w-4 h-4 mr-2" />
|
||||
{t('landing:hero.badge_local', 'Datos Hiperlocales Españoles')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl tracking-tight font-extrabold text-[var(--text-primary)] sm:text-5xl lg:text-7xl">
|
||||
@@ -178,7 +185,7 @@ const LandingPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Background decoration */}
|
||||
<div className="absolute top-0 left-0 right-0 h-full overflow-hidden -z-10">
|
||||
<div className="absolute -top-40 -right-40 w-80 h-80 bg-[var(--color-primary)]/5 rounded-full blur-3xl"></div>
|
||||
@@ -581,6 +588,128 @@ const LandingPage: React.FC = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Hyper-Local Spanish Intelligence Section */}
|
||||
<section id="local" className="py-24 bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-full text-sm font-semibold mb-6">
|
||||
<MapPin className="w-4 h-4" />
|
||||
{t('landing:local.badge', 'Datos Hiperlocales Españoles')}
|
||||
</div>
|
||||
<h2 className="text-3xl lg:text-5xl font-extrabold text-[var(--text-primary)] mb-6">
|
||||
{t('landing:local.title_main', 'Inteligencia Hiperlocal')}
|
||||
<span className="block text-[var(--color-primary)]">{t('landing:local.title_accent', 'para España')}</span>
|
||||
</h2>
|
||||
<p className="text-lg text-[var(--text-secondary)] max-w-3xl mx-auto">
|
||||
{t('landing:local.subtitle', 'Nuestra IA está entrenada con datos hiperlocales españoles: información meteorológica AEMET, datos históricos de tráfico congestionado, y eventos culturales específicos de cada región. Comenzamos en Madrid, pero estamos preparados para tu ciudad con la misma precisión local.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8 mb-16">
|
||||
{/* Weather Data */}
|
||||
<div className="bg-white dark:bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border-2 border-blue-200 dark:border-blue-800 hover:border-blue-400 dark:hover:border-blue-600 transition-all duration-300">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-2xl flex items-center justify-center mb-6 mx-auto">
|
||||
<Droplets className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-[var(--text-primary)] mb-4 text-center">{t('landing:local.weather.title', 'Datos Meteorológicos AEMET')}</h3>
|
||||
<p className="text-[var(--text-secondary)] text-center mb-6">
|
||||
{t('landing:local.weather.description', 'Precisión meteorológica local con datos AEMET para predicciones hiperlocales que entienden las microclimas de tu ciudad.')}
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">{t('landing:local.weather.features.aemet', 'Integración directa con AEMET')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">{t('landing:local.weather.features.microclimate', 'Datos de microclima por ciudad')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">{t('landing:local.weather.features.local', 'Adaptado a cada región española')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Traffic Data */}
|
||||
<div className="bg-white dark:bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border-2 border-purple-200 dark:border-purple-800 hover:border-purple-40 dark:hover:border-purple-600 transition-all duration-300">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-purple-500 to-pink-600 rounded-2xl flex items-center justify-center mb-6 mx-auto">
|
||||
<Globe className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-[var(--text-primary)] mb-4 text-center">{t('landing:local.traffic.title', 'Datos de Tráfico Históricos')}</h3>
|
||||
<p className="text-[var(--text-secondary)] text-center mb-6">
|
||||
{t('landing:local.traffic.description', 'Análisis de patrones de tráfico congestionado en ciudades españolas para entender mejor los flujos de clientes y demanda.')}
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">{t('landing:local.traffic.features.historical', 'Datos históricos de tráfico')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">{t('landing:local.traffic.features.patterns', 'Patrones de movilidad por ciudad')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">{t('landing:local.traffic.features.local', 'Adaptado a cada ciudad española')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Events Data */}
|
||||
<div className="bg-white dark:bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border-2 border-amber-200 dark:border-amber-800 hover:border-amber-400 dark:hover:border-amber-600 transition-all duration-300">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-amber-500 to-orange-600 rounded-2xl flex items-center justify-center mb-6 mx-auto">
|
||||
<Calendar className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-[var(--text-primary)] mb-4 text-center">{t('landing:local.events.title', 'Eventos y Festividades')}</h3>
|
||||
<p className="text-[var(--text-secondary)] text-center mb-6">
|
||||
{t('landing:local.events.description', 'Integración de festividades locales, nacionales y eventos culturales específicos de cada región para predicciones más precisas.')}
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">{t('landing:local.events.features.local_holidays', 'Festivos locales y autonómicos')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">{t('landing:local.events.features.cultural', 'Eventos culturales regionales')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">{t('landing:local.events.features.scalable', 'Listo para cualquier ciudad española')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Spanish Cities Ready */}
|
||||
<div className="bg-gradient-to-r from-[var(--color-primary)]/10 to-orange-500/10 rounded-2xl p-10 border-2 border-[var(--color-primary)]/30">
|
||||
<div className="text-center">
|
||||
<h3 className="text-2xl font-bold text-[var(--text-primary)] mb-4">
|
||||
{t('landing:local.scalability.title', 'Construido para España, Listo para Tu Ciudad')}
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)] max-w-3xl mx-auto mb-6">
|
||||
{t('landing:local.scalability.description', 'Aunque comenzamos en Madrid, nuestra arquitectura está diseñada para escalar a cualquier ciudad española manteniendo la misma precisión hiperlocal.')}
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-6 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="w-5 h-5 text-[var(--color-primary)]" />
|
||||
<span className="text-[var(--text-secondary)]">{t('landing:local.scalability.madrid', 'Madrid (Lanzamiento)')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="w-5 h-5 text-[var(--color-success)]" />
|
||||
<span className="text-[var(--text-secondary)]">{t('landing:local.scalability.scalable', 'Listo para otras ciudades')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="w-5 h-5 text-blue-600" />
|
||||
<span className="text-[var(--text-secondary)]">{t('landing:local.scalability.national', 'Arquitectura nacional')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Sustainability & SDG Compliance Section */}
|
||||
<section className="py-24 bg-gradient-to-b from-green-50 to-white dark:from-green-950/20 dark:to-[var(--bg-secondary)]">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
@@ -608,7 +737,7 @@ const LandingPage: React.FC = () => {
|
||||
<TreeDeciduous className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-4xl font-bold text-green-600 dark:text-green-400 mb-2">855 kg</div>
|
||||
<div className="text-4xl font-bold text-green-600 dark:text-green-400 mb-2">85 kg</div>
|
||||
<div className="text-sm font-semibold text-[var(--text-primary)] mb-2">{t('landing:sustainability.metrics.co2_avoided', 'CO₂ Avoided Monthly')}</div>
|
||||
<div className="text-xs text-[var(--text-secondary)]">{t('landing:sustainability.metrics.co2_equivalent', 'Equivalent to 43 trees planted')}</div>
|
||||
</div>
|
||||
@@ -671,34 +800,34 @@ const LandingPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 w-full">
|
||||
<div className="bg-white dark:bg-[var(--bg-primary)] rounded-xl p-6 shadow-lg">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<span className="text-sm font-semibold text-[var(--text-primary)]">{t('landing:sustainability.sdg.progress_label', 'Progress to Target')}</span>
|
||||
<div className="bg-white dark:bg-[var(--bg-primary)] rounded-xl p-6 shadow-lg">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<span className="text-sm font-semibold text-[var(--text-primary)]">{t('landing:sustainability.sdg.progress_label', 'Progress to Target')}</span>
|
||||
<span className="text-2xl font-bold text-green-600">65%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-6 overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-green-500 to-emerald-500 h-6 rounded-full flex items-center justify-end pr-3" style={{ width: '65%' }}>
|
||||
<TrendingUp className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-6 overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-green-500 to-emerald-500 h-6 rounded-full flex items-center justify-end pr-3" style={{ width: '65%' }}>
|
||||
<TrendingUp className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<div className="text-xs text-[var(--text-secondary)] mb-1">{t('landing:sustainability.sdg.baseline', 'Baseline')}</div>
|
||||
<div className="text-lg font-bold text-[var(--text-primary)]">25%</div>
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<div className="text-xs text-[var(--text-secondary)] mb-1">{t('landing:sustainability.sdg.baseline', 'Baseline')}</div>
|
||||
<div className="text-lg font-bold text-[var(--text-primary)]">25%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-[var(--text-secondary)] mb-1">{t('landing:sustainability.sdg.current', 'Current')}</div>
|
||||
<div>
|
||||
<div className="text-xs text-[var(--text-secondary)] mb-1">{t('landing:sustainability.sdg.current', 'Current')}</div>
|
||||
<div className="text-lg font-bold text-green-600">16.25%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-[var(--text-secondary)] mb-1">{t('landing:sustainability.sdg.target', 'Target 2030')}</div>
|
||||
<div className="text-lg font-bold text-[var(--text-primary)]">12.5%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-[var(--text-secondary)] mb-1">{t('landing:sustainability.sdg.target', 'Target 2030')}</div>
|
||||
<div className="text-lg font-bold text-[var(--text-primary)]">12.5%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grant Programs Grid */}
|
||||
<div className="mt-16 grid md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-6">
|
||||
@@ -784,408 +913,408 @@ const LandingPage: React.FC = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Benefits Section - Problem/Solution Focus */}
|
||||
<section id="benefits" className="py-24 bg-[var(--bg-primary)]">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
|
||||
{t('landing:benefits.title', 'El Problema Que Resolvemos')}
|
||||
<span className="block text-[var(--color-primary)]">{t('landing:benefits.title_accent', 'Para Panaderías')}</span>
|
||||
</h2>
|
||||
<p className="mt-6 text-lg text-[var(--text-secondary)] max-w-3xl mx-auto">
|
||||
{t('landing:benefits.subtitle', 'Sabemos lo frustrante que es tirar pan al final del día, o quedarte sin producto cuando llegan clientes. La producción artesanal es difícil de optimizar... hasta ahora.')}
|
||||
</p>
|
||||
</div>
|
||||
{/* Benefits Section - Problem/Solution Focus */}
|
||||
<section id="benefits" className="py-24 bg-[var(--bg-primary)]">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
|
||||
{t('landing:benefits.title', 'El Problema Que Resolvemos')}
|
||||
<span className="block text-[var(--color-primary)]">{t('landing:benefits.title_accent', 'Para Panaderías')}</span>
|
||||
</h2>
|
||||
<p className="mt-6 text-lg text-[var(--text-secondary)] max-w-3xl mx-auto">
|
||||
{t('landing:benefits.subtitle', 'Sabemos lo frustrante que es tirar pan al final del día, o quedarte sin producto cuando llegan clientes. La producción artesanal es difícil de optimizar... hasta ahora.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||
{/* Left: Problems */}
|
||||
<div className="space-y-6">
|
||||
<div className="bg-red-50 dark:bg-red-900/10 border-l-4 border-red-500 p-6 rounded-lg">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 bg-red-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-white font-bold text-xl">✗</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-bold text-red-700 dark:text-red-400 mb-2">{t('landing:benefits.problems.waste.title', 'Desperdicias entre 15-40% de producción')}</h4>
|
||||
<p className="text-[var(--text-secondary)] text-sm">
|
||||
{t('landing:benefits.problems.waste.description', 'Al final del día tiras producto que nadie compró. Son cientos de euros a la basura cada semana.')}
|
||||
</p>
|
||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||
{/* Left: Problems */}
|
||||
<div className="space-y-6">
|
||||
<div className="bg-red-50 dark:bg-red-900/10 border-l-4 border-red-500 p-6 rounded-lg">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 bg-red-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-white font-bold text-xl">✗</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-bold text-red-700 dark:text-red-400 mb-2">{t('landing:benefits.problems.waste.title', 'Desperdicias entre 15-40% de producción')}</h4>
|
||||
<p className="text-[var(--text-secondary)] text-sm">
|
||||
{t('landing:benefits.problems.waste.description', 'Al final del día tiras producto que nadie compró. Son cientos de euros a la basura cada semana.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-50 dark:bg-red-900/10 border-l-4 border-red-500 p-6 rounded-lg">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 bg-red-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-white font-bold text-xl">✗</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="bg-red-50 dark:bg-red-900/10 border-l-4 border-red-500 p-6 rounded-lg">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 bg-red-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-white font-bold text-xl">✗</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-bold text-red-700 dark:text-red-400 mb-2">{t('landing:benefits.problems.stockouts.title', 'Pierdes ventas por falta de stock')}</h4>
|
||||
<p className="text-[var(--text-secondary)] text-sm">
|
||||
{t('landing:benefits.problems.stockouts.description', 'Clientes que vienen por su pan favorito y se van sin comprar porque ya se te acabó a las 14:00.')}
|
||||
</p>
|
||||
<p className="text-[var(--text-secondary)] text-sm">
|
||||
{t('landing:benefits.problems.stockouts.description', 'Clientes que vienen por su pan favorito y se van sin comprar porque ya se te acabó a las 14:00.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-50 dark:bg-red-900/10 border-l-4 border-red-500 p-6 rounded-lg">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 bg-red-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-white font-bold text-xl">✗</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-bold text-red-700 dark:text-red-400 mb-2">{t('landing:benefits.problems.manual.title', 'Excel, papel y "experiencia"')}</h4>
|
||||
<p className="text-[var(--text-secondary)] text-sm">
|
||||
{t('landing:benefits.problems.manual.description', 'Planificas basándote en intuición. Funciona... hasta que no funciona.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-50 dark:bg-red-900/10 border-l-4 border-red-500 p-6 rounded-lg">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 bg-red-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-white font-bold text-xl">✗</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-bold text-red-700 dark:text-red-400 mb-2">{t('landing:benefits.problems.manual.title', 'Excel, papel y "experiencia"')}</h4>
|
||||
<p className="text-[var(--text-secondary)] text-sm">
|
||||
{t('landing:benefits.problems.manual.description', 'Planificas basándote en intuición. Funciona... hasta que no funciona.')}
|
||||
</p>
|
||||
{/* Right: Solutions */}
|
||||
<div className="space-y-6">
|
||||
<div className="bg-green-50 dark:bg-green-900/10 border-l-4 border-green-500 p-6 rounded-lg">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 bg-green-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<Check className="text-white w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-bold text-green-700 dark:text-green-400 mb-2">{t('landing:benefits.solutions.exact_production.title', 'Produce exactamente lo que vas a vender')}</h4>
|
||||
<p className="text-[var(--text-secondary)] text-sm">
|
||||
{t('landing:benefits.solutions.exact_production.description', 'La IA analiza tus ventas históricas, clima, eventos locales y festivos para predecir demanda real.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Solutions */}
|
||||
<div className="space-y-6">
|
||||
<div className="bg-green-50 dark:bg-green-900/10 border-l-4 border-green-500 p-6 rounded-lg">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="bg-green-50 dark:bg-green-900/10 border-l-4 border-green-500 p-6 rounded-lg">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 bg-green-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<Check className="text-white w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-bold text-green-700 dark:text-green-400 mb-2">{t('landing:benefits.solutions.exact_production.title', 'Produce exactamente lo que vas a vender')}</h4>
|
||||
<p className="text-[var(--text-secondary)] text-sm">
|
||||
{t('landing:benefits.solutions.exact_production.description', 'La IA analiza tus ventas históricas, clima, eventos locales y festivos para predecir demanda real.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 dark:bg-green-900/10 border-l-4 border-green-500 p-6 rounded-lg">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 bg-green-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<Check className="text-white w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<Check className="text-white w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-bold text-green-700 dark:text-green-400 mb-2">{t('landing:benefits.solutions.stock_availability.title', 'Siempre tienes stock de lo que más se vende')}</h4>
|
||||
<p className="text-[var(--text-secondary)] text-sm">
|
||||
{t('landing:benefits.solutions.stock_availability.description', 'El sistema te avisa qué productos van a tener más demanda cada día, para que nunca te quedes sin.')}
|
||||
</p>
|
||||
<p className="text-[var(--text-secondary)] text-sm">
|
||||
{t('landing:benefits.solutions.stock_availability.description', 'El sistema te avisa qué productos van a tener más demanda cada día, para que nunca te quedes sin.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 dark:bg-green-900/10 border-l-4 border-green-500 p-6 rounded-lg">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="bg-green-50 dark:bg-green-900/10 border-l-4 border-green-500 p-6 rounded-lg">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 bg-green-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<Check className="text-white w-6 h-6" />
|
||||
<Check className="text-white w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-bold text-green-700 dark:text-green-400 mb-2">{t('landing:benefits.solutions.smart_automation.title', 'Automatización inteligente + datos reales')}</h4>
|
||||
<p className="text-[var(--text-secondary)] text-sm">
|
||||
{t('landing:benefits.solutions.smart_automation.description', 'Desde planificación de producción hasta gestión de inventario. Todo basado en matemáticas, no corazonadas.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-bold text-green-700 dark:text-green-400 mb-2">{t('landing:benefits.solutions.smart_automation.title', 'Automatización inteligente + datos reales')}</h4>
|
||||
<p className="text-[var(--text-secondary)] text-sm">
|
||||
{t('landing:benefits.solutions.smart_automation.description', 'Desde planificación de producción hasta gestión de inventario. Todo basado en matemáticas, no corazonadas.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Value Proposition Summary */}
|
||||
<div className="mt-16 bg-gradient-to-r from-[var(--color-primary)]/10 to-orange-500/10 rounded-2xl p-8 border-2 border-[var(--color-primary)]/30">
|
||||
<div className="text-center">
|
||||
<h3 className="text-2xl font-bold text-[var(--text-primary)] mb-4">
|
||||
{t('landing:benefits.value_proposition.title', 'El Objetivo: Que Ahorres Dinero Desde el Primer Mes')}
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)] max-w-3xl mx-auto mb-6" dangerouslySetInnerHTML={{ __html: t('landing:benefits.value_proposition.description', 'No prometemos números mágicos porque cada panadería es diferente. Lo que SÍ prometemos es que si después de 3 meses no has reducido desperdicios o mejorado tus márgenes, <strong>te ayudamos gratis a optimizar tu negocio de otra forma</strong>.') }} />
|
||||
<div className="flex flex-wrap justify-center gap-6 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5 text-[var(--color-success)]" />
|
||||
<span className="text-[var(--text-secondary)]">{t('landing:benefits.value_proposition.points.waste', 'Menos desperdicio = más beneficio')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-5 h-5 text-blue-600" />
|
||||
<span className="text-[var(--text-secondary)]">{t('landing:benefits.value_proposition.points.time', 'Menos tiempo en Excel, más en tu negocio')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-5 h-5 text-purple-600" />
|
||||
<span className="text-[var(--text-secondary)]">{t('landing:benefits.value_proposition.points.data', 'Tus datos siempre son tuyos')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Value Proposition Summary */}
|
||||
<div className="mt-16 bg-gradient-to-r from-[var(--color-primary)]/10 to-orange-500/10 rounded-2xl p-8 border-2 border-[var(--color-primary)]/30">
|
||||
<div className="text-center">
|
||||
<h3 className="text-2xl font-bold text-[var(--text-primary)] mb-4">
|
||||
{t('landing:benefits.value_proposition.title', 'El Objetivo: Que Ahorres Dinero Desde el Primer Mes')}
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)] max-w-3xl mx-auto mb-6" dangerouslySetInnerHTML={{ __html: t('landing:benefits.value_proposition.description', 'No prometemos números mágicos porque cada panadería es diferente. Lo que SÍ prometemos es que si después de 3 meses no has reducido desperdicios o mejorado tus márgenes, <strong>te ayudamos gratis a optimizar tu negocio de otra forma</strong>.') }} />
|
||||
<div className="flex flex-wrap justify-center gap-6 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5 text-[var(--color-success)]" />
|
||||
<span className="text-[var(--text-secondary)]">{t('landing:benefits.value_proposition.points.waste', 'Menos desperdicio = más beneficio')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-5 h-5 text-blue-600" />
|
||||
<span className="text-[var(--text-secondary)]">{t('landing:benefits.value_proposition.points.time', 'Menos tiempo en Excel, más en tu negocio')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-5 h-5 text-purple-600" />
|
||||
<span className="text-[var(--text-secondary)]">{t('landing:benefits.value_proposition.points.data', 'Tus datos siempre son tuyos')}</span>
|
||||
</div>
|
||||
{/* Risk Reversal & Transparency Section */}
|
||||
<section id="testimonials" className="py-24 bg-[var(--bg-secondary)]">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
|
||||
{t('landing:risk_reversal.title', 'Sin Riesgo. Sin Ataduras.')}
|
||||
</h2>
|
||||
<p className="mt-4 max-w-2xl mx-auto text-lg text-[var(--text-secondary)]">
|
||||
{t('landing:risk_reversal.subtitle', 'Somos transparentes: esto es un piloto. Estamos construyendo la mejor herramienta para panaderías, y necesitamos tu ayuda.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 mb-16">
|
||||
{/* Left: What You Get */}
|
||||
<div className="bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 rounded-2xl p-8 border-2 border-green-300 dark:border-green-700">
|
||||
<h3 className="text-2xl font-bold text-[var(--text-primary)] mb-6 flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-green-600 rounded-full flex items-center justify-center">
|
||||
<Check className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
{t('landing:risk_reversal.what_you_get.title', 'Lo Que Obtienes')}
|
||||
</h3>
|
||||
<ul className="space-y-4">
|
||||
<li className="flex items-start gap-3">
|
||||
<Check className="w-5 h-5 text-green-600 dark:text-green-400 mt-1 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:risk_reversal.what_you_get.free_trial', '<strong>3 meses completamente gratis</strong> para probar todas las funcionalidades') }} />
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<Check className="w-5 h-5 text-green-600 dark:text-green-400 mt-1 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:risk_reversal.what_you_get.lifetime_discount', '<strong>20% de descuento de por vida</strong> si decides continuar después del piloto') }} />
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<Check className="w-5 h-5 text-green-600 dark:text-green-400 mt-1 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:risk_reversal.what_you_get.founder_support', '<strong>Soporte directo del equipo fundador</strong> - respondemos en horas, no días') }} />
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<Check className="w-5 h-5 text-green-600 dark:text-green-400 mt-1 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:risk_reversal.what_you_get.priority_features', '<strong>Tus ideas se implementan primero</strong> - construimos lo que realmente necesitas') }} />
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<Check className="w-5 h-5 text-green-600 dark:text-green-400 mt-1 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:risk_reversal.what_you_get.cancel_anytime', '<strong>Cancelas cuando quieras</strong> sin explicaciones ni penalizaciones') }} />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Risk Reversal & Transparency Section */}
|
||||
<section id="testimonials" className="py-24 bg-[var(--bg-secondary)]">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
|
||||
{t('landing:risk_reversal.title', 'Sin Riesgo. Sin Ataduras.')}
|
||||
</h2>
|
||||
<p className="mt-4 max-w-2xl mx-auto text-lg text-[var(--text-secondary)]">
|
||||
{t('landing:risk_reversal.subtitle', 'Somos transparentes: esto es un piloto. Estamos construyendo la mejor herramienta para panaderías, y necesitamos tu ayuda.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 mb-16">
|
||||
{/* Left: What You Get */}
|
||||
<div className="bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 rounded-2xl p-8 border-2 border-green-300 dark:border-green-700">
|
||||
<h3 className="text-2xl font-bold text-[var(--text-primary)] mb-6 flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-green-600 rounded-full flex items-center justify-center">
|
||||
<Check className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
{t('landing:risk_reversal.what_you_get.title', 'Lo Que Obtienes')}
|
||||
</h3>
|
||||
<ul className="space-y-4">
|
||||
<li className="flex items-start gap-3">
|
||||
<Check className="w-5 h-5 text-green-600 dark:text-green-400 mt-1 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:risk_reversal.what_you_get.free_trial', '<strong>3 meses completamente gratis</strong> para probar todas las funcionalidades') }} />
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<Check className="w-5 h-5 text-green-600 dark:text-green-400 mt-1 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:risk_reversal.what_you_get.lifetime_discount', '<strong>20% de descuento de por vida</strong> si decides continuar después del piloto') }} />
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<Check className="w-5 h-5 text-green-600 dark:text-green-400 mt-1 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:risk_reversal.what_you_get.founder_support', '<strong>Soporte directo del equipo fundador</strong> - respondemos en horas, no días') }} />
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<Check className="w-5 h-5 text-green-600 dark:text-green-400 mt-1 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:risk_reversal.what_you_get.priority_features', '<strong>Tus ideas se implementan primero</strong> - construimos lo que realmente necesitas') }} />
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<Check className="w-5 h-5 text-green-600 dark:text-green-400 mt-1 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:risk_reversal.what_you_get.cancel_anytime', '<strong>Cancelas cuando quieras</strong> sin explicaciones ni penalizaciones') }} />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Right: What We Ask */}
|
||||
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-2xl p-8 border-2 border-blue-300 dark:border-blue-700">
|
||||
<h3 className="text-2xl font-bold text-[var(--text-primary)] mb-6 flex items-center gap-3">
|
||||
{/* Right: What We Ask */}
|
||||
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-2xl p-8 border-2 border-blue-300 dark:border-blue-700">
|
||||
<h3 className="text-2xl font-bold text-[var(--text-primary)] mb-6 flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-blue-600 rounded-full flex items-center justify-center">
|
||||
<Users className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
{t('landing:risk_reversal.what_we_ask.title', 'Lo Que Pedimos')}
|
||||
</h3>
|
||||
<ul className="space-y-4">
|
||||
<li className="flex items-start gap-3">
|
||||
<ArrowRight className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-1 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:risk_reversal.what_we_ask.feedback', '<strong>Feedback honesto semanal</strong> (15 min) sobre qué funciona y qué no') }} />
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<ArrowRight className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-1 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:risk_reversal.what_we_ask.patience', '<strong>Paciencia con bugs</strong> - estamos en fase beta, habrá imperfecciones') }} />
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<ArrowRight className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-1 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:risk_reversal.what_we_ask.data', '<strong>Datos de ventas históricos</strong> (opcional) para mejorar las predicciones') }} />
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<ArrowRight className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-1 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:risk_reversal.what_we_ask.communication', '<strong>Comunicación abierta</strong> - queremos saber si algo no te gusta') }} />
|
||||
</li>
|
||||
</ul>
|
||||
<Users className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
{t('landing:risk_reversal.what_we_ask.title', 'Lo Que Pedimos')}
|
||||
</h3>
|
||||
<ul className="space-y-4">
|
||||
<li className="flex items-start gap-3">
|
||||
<ArrowRight className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-1 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:risk_reversal.what_we_ask.feedback', '<strong>Feedback honesto semanal</strong> (15 min) sobre qué funciona y qué no') }} />
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<ArrowRight className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-1 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:risk_reversal.what_we_ask.patience', '<strong>Paciencia con bugs</strong> - estamos en fase beta, habrá imperfecciones') }} />
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<ArrowRight className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-1 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:risk_reversal.what_we_ask.data', '<strong>Datos de ventas históricos</strong> (opcional) para mejorar las predicciones') }} />
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<ArrowRight className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-1 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]" dangerouslySetInnerHTML={{ __html: t('landing:risk_reversal.what_we_ask.communication', '<strong>Comunicación abierta</strong> - queremos saber si algo no te gusta') }} />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="mt-6 p-4 bg-white dark:bg-gray-800 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<p className="text-sm text-[var(--text-secondary)] italic" dangerouslySetInnerHTML={{ __html: t('landing:risk_reversal.promise', '<strong>Promesa:</strong> Si después de 3 meses sientes que no te ayudamos a ahorrar dinero o reducir desperdicios, te damos una sesión gratuita de consultoría para optimizar tu panadería de otra forma.') }} />
|
||||
<div className="mt-6 p-4 bg-white dark:bg-gray-800 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<p className="text-sm text-[var(--text-secondary)] italic" dangerouslySetInnerHTML={{ __html: t('landing:risk_reversal.promise', '<strong>Promesa:</strong> Si después de 3 meses sientes que no te ayudamos a ahorrar dinero o reducir desperdicios, te damos una sesión gratuita de consultoría para optimizar tu panadería de otra forma.') }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Credibility Signals */}
|
||||
<div className="bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border border-[var(--border-primary)]">
|
||||
<div className="text-center mb-8">
|
||||
<h3 className="text-xl font-bold text-[var(--text-primary)] mb-3">
|
||||
{t('landing:risk_reversal.credibility.title', '¿Por Qué Confiar en Nosotros?')}
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
{t('landing:risk_reversal.credibility.subtitle', 'Entendemos que probar nueva tecnología es un riesgo. Por eso somos completamente transparentes:')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-purple-100 dark:bg-purple-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Shield className="w-8 h-8 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-[var(--text-primary)] mb-2">{t('landing:risk_reversal.credibility.spanish.title', '100% Española')}</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('landing:risk_reversal.credibility.spanish.description', 'Empresa registrada en España. Tus datos están protegidos por RGPD y nunca salen de la UE.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-orange-100 dark:bg-orange-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Brain className="w-8 h-8 text-orange-600 dark:text-orange-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-[var(--text-primary)] mb-2">{t('landing:risk_reversal.credibility.technology.title', 'Tecnología Probada')}</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('landing:risk_reversal.credibility.technology.description', 'Usamos algoritmos de IA validados académicamente, adaptados específicamente para panaderías.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-teal-100 dark:bg-teal-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Award className="w-8 h-8 text-teal-600 dark:text-teal-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-[var(--text-primary)] mb-2">{t('landing:risk_reversal.credibility.team.title', 'Equipo Experto')}</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('landing:risk_reversal.credibility.team.description', 'Fundadores con experiencia en proyectos de alto valor tecnológico + proyectos internacionales.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Credibility Signals */}
|
||||
<div className="bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border border-[var(--border-primary)]">
|
||||
<div className="text-center mb-8">
|
||||
<h3 className="text-xl font-bold text-[var(--text-primary)] mb-3">
|
||||
{t('landing:risk_reversal.credibility.title', '¿Por Qué Confiar en Nosotros?')}
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
{t('landing:risk_reversal.credibility.subtitle', 'Entendemos que probar nueva tecnología es un riesgo. Por eso somos completamente transparentes:')}
|
||||
{/* Pricing Section */}
|
||||
<section id="pricing">
|
||||
<PricingSection />
|
||||
</section>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<section id="faq" className="py-24 bg-[var(--bg-secondary)]">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
|
||||
{t('landing:faq.title', 'Preguntas Frecuentes')}
|
||||
</h2>
|
||||
<p className="mt-4 text-lg text-[var(--text-secondary)]">
|
||||
{t('landing:faq.subtitle', 'Todo lo que necesitas saber sobre Panadería IA')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-purple-100 dark:bg-purple-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Shield className="w-8 h-8 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-[var(--text-primary)] mb-2">{t('landing:risk_reversal.credibility.spanish.title', '100% Española')}</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('landing:risk_reversal.credibility.spanish.description', 'Empresa registrada en España. Tus datos están protegidos por RGPD y nunca salen de la UE.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-orange-100 dark:bg-orange-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Brain className="w-8 h-8 text-orange-600 dark:text-orange-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-[var(--text-primary)] mb-2">{t('landing:risk_reversal.credibility.technology.title', 'Tecnología Probada')}</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('landing:risk_reversal.credibility.technology.description', 'Usamos algoritmos de IA validados académicamente, adaptados específicamente para panaderías.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-teal-100 dark:bg-teal-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Award className="w-8 h-8 text-teal-600 dark:text-teal-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-[var(--text-primary)] mb-2">{t('landing:risk_reversal.credibility.team.title', 'Equipo Experto')}</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('landing:risk_reversal.credibility.team.description', 'Fundadores con experiencia en proyectos de alto valor tecnológico + proyectos internacionales.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Pricing Section */}
|
||||
<section id="pricing">
|
||||
<PricingSection />
|
||||
</section>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<section id="faq" className="py-24 bg-[var(--bg-secondary)]">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
|
||||
{t('landing:faq.title', 'Preguntas Frecuentes')}
|
||||
</h2>
|
||||
<p className="mt-4 text-lg text-[var(--text-secondary)]">
|
||||
{t('landing:faq.subtitle', 'Todo lo que necesitas saber sobre Panadería IA')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-16 space-y-8">
|
||||
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
<div className="mt-16 space-y-8">
|
||||
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{t('landing:faq.questions.accuracy.q', '¿Qué tan precisa es la predicción de demanda?')}
|
||||
</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
{t('landing:faq.questions.accuracy.a', 'Nuestra IA alcanza una precisión del 92% en predicciones de demanda, analizando más de 50 variables incluyendo histórico de ventas, clima, eventos locales, estacionalidad y tendencias de mercado. La precisión mejora continuamente con más datos de tu panadería.')}
|
||||
</p>
|
||||
</div>
|
||||
</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
{t('landing:faq.questions.accuracy.a', 'Nuestra IA alcanza una precisión del 92% en predicciones de demanda, analizando más de 50 variables incluyendo histórico de ventas, clima, eventos locales, estacionalidad y tendencias de mercado. La precisión mejora continuamente con más datos de tu panadería.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{t('landing:faq.questions.implementation.q', '¿Cuánto tiempo toma implementar el sistema?')}
|
||||
</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
{t('landing:faq.questions.implementation.a', 'La configuración inicial toma solo 5 minutos. Nuestro equipo te ayuda a migrar tus datos históricos en 24-48 horas. La IA comienza a generar predicciones útiles después de una semana de datos, alcanzando máxima precisión en 30 días.')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{t('landing:faq.questions.implementation.q', '¿Cuánto tiempo toma implementar el sistema?')}
|
||||
</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
{t('landing:faq.questions.implementation.a', 'La configuración inicial toma solo 5 minutos. Nuestro equipo te ayuda a migrar tus datos históricos en 24-48 horas. La IA comienza a generar predicciones útiles después de una semana de datos, alcanzando máxima precisión en 30 días.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{t('landing:faq.questions.integration.q', '¿Se integra con mi sistema POS actual?')}
|
||||
</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
{t('landing:faq.questions.integration.a', 'Sí, nos integramos con más de 50 sistemas POS populares en España. También incluimos nuestro propio POS optimizado para panaderías. Si usas un sistema específico, nuestro equipo técnico puede crear una integración personalizada.')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{t('landing:faq.questions.integration.q', '¿Se integra con mi sistema POS actual?')}
|
||||
</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
{t('landing:faq.questions.integration.a', 'Sí, nos integramos con más de 50 sistemas POS populares en España. También incluimos nuestro propio POS optimizado para panaderías. Si usas un sistema específico, nuestro equipo técnico puede crear una integración personalizada.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{t('landing:faq.questions.support.q', '¿Qué soporte técnico ofrecen?')}
|
||||
</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
{t('landing:faq.questions.support.a', 'Ofrecemos soporte 24/7 en español por chat, email y teléfono. Todos nuestros técnicos son expertos en operaciones de panadería. Además, incluimos onboarding personalizado y training para tu equipo sin costo adicional.')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{t('landing:faq.questions.support.q', '¿Qué soporte técnico ofrecen?')}
|
||||
</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
{t('landing:faq.questions.support.a', 'Ofrecemos soporte 24/7 en español por chat, email y teléfono. Todos nuestros técnicos son expertos en operaciones de panadería. Además, incluimos onboarding personalizado y training para tu equipo sin costo adicional.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{t('landing:faq.questions.security.q', '¿Mis datos están seguros?')}
|
||||
</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
{t('landing:faq.questions.security.a', 'Absolutamente. Utilizamos cifrado AES-256, servidores en la UE, cumplimos 100% con RGPD y realizamos auditorías de seguridad trimestrales. Tus datos nunca se comparten con terceros y tienes control total sobre tu información.')}
|
||||
</p>
|
||||
<div className="bg-[var(--bg-primary)] rounded-xl p-8 border border-[var(--border-primary)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{t('landing:faq.questions.security.q', '¿Mis datos están seguros?')}
|
||||
</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
{t('landing:faq.questions.security.a', 'Absolutamente. Utilizamos cifrado AES-256, servidores en la UE, cumplimos 100% con RGPD y realizamos auditorías de seguridad trimestrales. Tus datos nunca se comparten con terceros y tienes control total sobre tu información.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{/* Final CTA Section - With Urgency & Scarcity */}
|
||||
<section className="py-24 bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] relative overflow-hidden">
|
||||
<div className="absolute inset-0">
|
||||
<div className="absolute -top-40 -right-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
|
||||
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-4xl mx-auto text-center px-4 sm:px-6 lg:px-8 relative">
|
||||
{/* Scarcity Badge */}
|
||||
<div className="inline-flex items-center gap-2 bg-red-600 text-white px-6 py-3 rounded-full text-sm font-bold mb-6 shadow-lg animate-pulse">
|
||||
<Clock className="w-5 h-5" />
|
||||
<span>{t('landing:final_cta.scarcity_badge', 'Quedan 12 plazas de las 20 del programa piloto')}</span>
|
||||
{/* Final CTA Section - With Urgency & Scarcity */}
|
||||
<section className="py-24 bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] relative overflow-hidden">
|
||||
<div className="absolute inset-0">
|
||||
<div className="absolute -top-40 -right-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
|
||||
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-3xl lg:text-5xl font-extrabold text-white">
|
||||
{t('landing:final_cta.title', 'Sé de las Primeras 20 Panaderías')}
|
||||
<span className="block text-white/90 mt-2">{t('landing:final_cta.title_accent', 'En Probar Esta Tecnología')}</span>
|
||||
</h2>
|
||||
<p className="mt-6 text-lg text-white/90 max-w-2xl mx-auto" dangerouslySetInnerHTML={{ __html: t('landing:final_cta.subtitle', 'No es para todo el mundo. Buscamos panaderías que quieran <strong>reducir desperdicios y aumentar ganancias</strong> con ayuda de IA, a cambio de feedback honesto.') }} />
|
||||
<div className="max-w-4xl mx-auto text-center px-4 sm:px-6 lg:px-8 relative">
|
||||
{/* Scarcity Badge */}
|
||||
<div className="inline-flex items-center gap-2 bg-red-600 text-white px-6 py-3 rounded-full text-sm font-bold mb-6 shadow-lg animate-pulse">
|
||||
<Clock className="w-5 h-5" />
|
||||
<span>{t('landing:final_cta.scarcity_badge', 'Quedan 12 plazas de las 20 del programa piloto')}</span>
|
||||
</div>
|
||||
|
||||
<h2 className="text-3xl lg:text-5xl font-extrabold text-white">
|
||||
{t('landing:final_cta.title', 'Sé de las Primeras 20 Panaderías')}
|
||||
<span className="block text-white/90 mt-2">{t('landing:final_cta.title_accent', 'En Probar Esta Tecnología')}</span>
|
||||
</h2>
|
||||
<p className="mt-6 text-lg text-white/90 max-w-2xl mx-auto" dangerouslySetInnerHTML={{ __html: t('landing:final_cta.subtitle', 'No es para todo el mundo. Buscamos panaderías que quieran <strong>reducir desperdicios y aumentar ganancias</strong> con ayuda de IA, a cambio de feedback honesto.') }} />
|
||||
|
||||
<div className="mt-10 flex flex-col sm:flex-row gap-6 justify-center">
|
||||
<Link to={getRegisterUrl()} className="w-full sm:w-auto">
|
||||
<Button
|
||||
size="lg"
|
||||
className="w-full sm:w-auto group relative px-10 py-5 text-lg font-bold bg-gradient-to-r from-[var(--color-primary)] to-orange-600 hover:from-[var(--color-primary-dark)] hover:to-orange-700 text-white shadow-2xl hover:shadow-3xl transform hover:scale-105 transition-all duration-300 rounded-xl overflow-hidden"
|
||||
>
|
||||
<span className="absolute inset-0 w-full h-full bg-gradient-to-r from-white/0 via-white/20 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700"></span>
|
||||
<span className="relative flex items-center justify-center gap-2">
|
||||
{t('landing:final_cta.cta_primary', 'Solicitar Plaza en el Piloto')}
|
||||
<ArrowRight className="w-6 h-6 group-hover:translate-x-1 transition-transform duration-200" />
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to={getDemoUrl()} className="w-full sm:w-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
<Link to={getRegisterUrl()} className="w-full sm:w-auto">
|
||||
<Button
|
||||
size="lg"
|
||||
className="w-full sm:w-auto group relative px-10 py-5 text-lg font-bold bg-gradient-to-r from-[var(--color-primary)] to-orange-600 hover:from-[var(--color-primary-dark)] hover:to-orange-700 text-white shadow-2xl hover:shadow-3xl transform hover:scale-105 transition-all duration-300 rounded-xl overflow-hidden"
|
||||
>
|
||||
<span className="absolute inset-0 w-full h-full bg-gradient-to-r from-white/0 via-white/20 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700"></span>
|
||||
<span className="relative flex items-center justify-center gap-2">
|
||||
{t('landing:final_cta.cta_primary', 'Solicitar Plaza en el Piloto')}
|
||||
<ArrowRight className="w-6 h-6 group-hover:translate-x-1 transition-transform duration-200" />
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to={getDemoUrl()} className="w-full sm:w-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="w-full sm:w-auto group px-10 py-5 text-lg font-semibold border-3 border-[var(--color-primary)] text-[var(--text-primary)] hover:bg-[var(--color-primary)] hover:text-white hover:border-[var(--color-primary-dark)] shadow-lg hover:shadow-xl transition-all duration-300 rounded-xl backdrop-blur-sm bg-white/50 dark:bg-gray-800/50"
|
||||
>
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<Play className="w-5 h-5 group-hover:scale-110 transition-transform duration-200" />
|
||||
{t('landing:hero.cta_secondary', 'Ver Demo')}
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
>
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<Play className="w-5 h-5 group-hover:scale-110 transition-transform duration-200" />
|
||||
{t('landing:hero.cta_secondary', 'Ver Demo')}
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Social Proof Alternative - Loss Aversion */}
|
||||
<div className="mt-12 bg-white/10 backdrop-blur-sm rounded-2xl p-6 border border-white/20">
|
||||
<p className="text-white/90 text-base mb-4">
|
||||
<strong>{t('landing:final_cta.why_now.title', '¿Por qué actuar ahora?')}</strong>
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6 text-sm">
|
||||
<div className="flex flex-col items-center">
|
||||
<Award className="w-8 h-8 text-white mb-2" />
|
||||
<div className="text-white font-semibold">{t('landing:final_cta.why_now.lifetime_discount.title', '20% descuento de por vida')}</div>
|
||||
<div className="text-white/70">{t('landing:final_cta.why_now.lifetime_discount.subtitle', 'Solo primeros 20')}</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<Users className="w-8 h-8 text-white mb-2" />
|
||||
<div className="text-white font-semibold">{t('landing:final_cta.why_now.influence.title', 'Influyes en el roadmap')}</div>
|
||||
<div className="text-white/70">{t('landing:final_cta.why_now.influence.subtitle', 'Tus necesidades primero')}</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<Zap className="w-8 h-8 text-white mb-2" />
|
||||
<div className="text-white font-semibold">{t('landing:final_cta.why_now.vip_support.title', 'Soporte VIP')}</div>
|
||||
<div className="text-white/70">{t('landing:final_cta.why_now.vip_support.subtitle', 'Acceso directo al equipo')}</div>
|
||||
{/* Social Proof Alternative - Loss Aversion */}
|
||||
<div className="mt-12 bg-white/10 backdrop-blur-sm rounded-2xl p-6 border border-white/20">
|
||||
<p className="text-white/90 text-base mb-4">
|
||||
<strong>{t('landing:final_cta.why_now.title', '¿Por qué actuar ahora?')}</strong>
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6 text-sm">
|
||||
<div className="flex flex-col items-center">
|
||||
<Award className="w-8 h-8 text-white mb-2" />
|
||||
<div className="text-white font-semibold">{t('landing:final_cta.why_now.lifetime_discount.title', '20% descuento de por vida')}</div>
|
||||
<div className="text-white/70">{t('landing:final_cta.why_now.lifetime_discount.subtitle', 'Solo primeros 20')}</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<Users className="w-8 h-8 text-white mb-2" />
|
||||
<div className="text-white font-semibold">{t('landing:final_cta.why_now.influence.title', 'Influyes en el roadmap')}</div>
|
||||
<div className="text-white/70">{t('landing:final_cta.why_now.influence.subtitle', 'Tus necesidades primero')}</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<Zap className="w-8 h-8 text-white mb-2" />
|
||||
<div className="text-white font-semibold">{t('landing:final_cta.why_now.vip_support.title', 'Soporte VIP')}</div>
|
||||
<div className="text-white/70">{t('landing:final_cta.why_now.vip_support.subtitle', 'Acceso directo al equipo')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Guarantee */}
|
||||
<div className="mt-8 text-white/80 text-sm">
|
||||
<Shield className="w-5 h-5 inline mr-2" />
|
||||
<span>{t('landing:final_cta.guarantee', 'Garantía: Cancelas en cualquier momento sin dar explicaciones')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Guarantee */}
|
||||
<div className="mt-8 text-white/80 text-sm">
|
||||
<Shield className="w-5 h-5 inline mr-2" />
|
||||
<span>{t('landing:final_cta.guarantee', 'Garantía: Cancelas en cualquier momento sin dar explicaciones')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</PublicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
</PublicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default LandingPage;
|
||||
export default LandingPage;
|
||||
|
||||
Reference in New Issue
Block a user