Add POI feature and imporve the overall backend implementation

This commit is contained in:
Urtzi Alfaro
2025-11-12 15:34:10 +01:00
parent e8096cd979
commit 5783c7ed05
173 changed files with 16862 additions and 9078 deletions

View File

@@ -15,7 +15,7 @@
* - Trust-building (explain system reasoning)
*/
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { RefreshCw, ExternalLink, Plus, Sparkles } from 'lucide-react';
import { useTenant } from '../../stores/tenant.store';
@@ -29,6 +29,7 @@ import {
useStartProductionBatch,
usePauseProductionBatch,
} from '../../api/hooks/newDashboard';
import { useRejectPurchaseOrder } from '../../api/hooks/purchase-orders';
import { HealthStatusCard } from '../../components/dashboard/HealthStatusCard';
import { ActionQueueCard } from '../../components/dashboard/ActionQueueCard';
import { OrchestrationSummaryCard } from '../../components/dashboard/OrchestrationSummaryCard';
@@ -36,11 +37,15 @@ import { ProductionTimelineCard } from '../../components/dashboard/ProductionTim
import { InsightsGrid } from '../../components/dashboard/InsightsGrid';
import { UnifiedAddWizard } from '../../components/domain/unified-wizard';
import type { ItemType } from '../../components/domain/unified-wizard';
import { useDemoTour, shouldStartTour, clearTourStartPending } from '../../features/demo-onboarding';
import { DemoBanner } from '../../components/layout/DemoBanner/DemoBanner';
export function NewDashboardPage() {
const navigate = useNavigate();
const { currentTenant } = useTenant();
const tenantId = currentTenant?.id || '';
const { startTour } = useDemoTour();
const isDemoMode = localStorage.getItem('demo_mode') === 'true';
// Unified Add Wizard state
const [isAddWizardOpen, setIsAddWizardOpen] = useState(false);
@@ -79,6 +84,7 @@ export function NewDashboardPage() {
// Mutations
const approvePO = useApprovePurchaseOrder();
const rejectPO = useRejectPurchaseOrder();
const startBatch = useStartProductionBatch();
const pauseBatch = usePauseProductionBatch();
@@ -94,6 +100,17 @@ export function NewDashboardPage() {
}
};
const handleReject = async (actionId: string, reason: string) => {
try {
await rejectPO.mutateAsync({ tenantId, poId: actionId, reason });
// Refetch to update UI
refetchActionQueue();
refetchHealth();
} catch (error) {
console.error('Error rejecting PO:', error);
}
};
const handleViewDetails = (actionId: string) => {
// Navigate to appropriate detail page based on action type
navigate(`/app/operations/procurement`);
@@ -137,8 +154,43 @@ export function NewDashboardPage() {
handleRefreshAll();
};
// Demo tour auto-start logic
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'));
// 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()');
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);
}
}, [isDemoMode, startTour]);
return (
<div className="min-h-screen pb-20 md:pb-8" style={{ backgroundColor: 'var(--bg-secondary)' }}>
{/* Demo Banner */}
{isDemoMode && <DemoBanner />}
{/* Mobile-optimized container */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{/* Header */}
@@ -183,30 +235,40 @@ export function NewDashboardPage() {
{/* Main Dashboard Layout */}
<div className="space-y-6">
{/* SECTION 1: Bakery Health Status */}
<HealthStatusCard healthStatus={healthStatus} loading={healthLoading} />
<div data-tour="dashboard-stats">
<HealthStatusCard healthStatus={healthStatus} loading={healthLoading} />
</div>
{/* SECTION 2: What Needs Your Attention (Action Queue) */}
<ActionQueueCard
actionQueue={actionQueue}
loading={actionQueueLoading}
onApprove={handleApprove}
onViewDetails={handleViewDetails}
onModify={handleModify}
/>
<div data-tour="pending-po-approvals">
<ActionQueueCard
actionQueue={actionQueue}
loading={actionQueueLoading}
onApprove={handleApprove}
onReject={handleReject}
onViewDetails={handleViewDetails}
onModify={handleModify}
tenantId={tenantId}
/>
</div>
{/* SECTION 3: What the System Did for You (Orchestration Summary) */}
<OrchestrationSummaryCard
summary={orchestrationSummary}
loading={orchestrationLoading}
/>
<div data-tour="real-time-alerts">
<OrchestrationSummaryCard
summary={orchestrationSummary}
loading={orchestrationLoading}
/>
</div>
{/* SECTION 4: Today's Production Timeline */}
<ProductionTimelineCard
timeline={productionTimeline}
loading={timelineLoading}
onStart={handleStartBatch}
onPause={handlePauseBatch}
/>
<div data-tour="today-production">
<ProductionTimelineCard
timeline={productionTimeline}
loading={timelineLoading}
onStart={handleStartBatch}
onPause={handlePauseBatch}
/>
</div>
{/* SECTION 5: Quick Insights Grid */}
<div>

View File

@@ -28,6 +28,75 @@ const SustainabilityPage: React.FC = () => {
// Date range state (default to last 30 days)
const [dateRange, setDateRange] = useState<{ start?: string; end?: string }>({});
// CSV Export function
const exportToCSV = () => {
if (!metrics) return;
const timestamp = new Date().toISOString().split('T')[0];
const filename = `sustainability_report_${currentTenant?.name || 'bakery'}_${timestamp}.csv`;
// Build CSV content
const csvRows = [
['Sustainability Report'],
['Generated:', new Date().toLocaleString()],
['Bakery:', currentTenant?.name || 'N/A'],
['Period:', `${metrics.period.start_date} to ${metrics.period.end_date}`],
[],
['WASTE METRICS'],
['Total Waste (kg)', metrics.waste_metrics.total_waste_kg.toFixed(2)],
['Production Waste (kg)', metrics.waste_metrics.production_waste_kg.toFixed(2)],
['Inventory Waste (kg)', metrics.waste_metrics.inventory_waste_kg.toFixed(2)],
['Waste Percentage (%)', metrics.waste_metrics.waste_percentage.toFixed(2)],
[],
['SDG 12.3 COMPLIANCE'],
['Status', metrics.sdg_compliance.sdg_12_3.status_label],
['Reduction Achieved (%)', metrics.sdg_compliance.sdg_12_3.reduction_achieved.toFixed(2)],
['Progress to Target (%)', metrics.sdg_compliance.sdg_12_3.progress_to_target.toFixed(2)],
['Target (%)', metrics.sdg_compliance.sdg_12_3.target_percentage],
[],
['ENVIRONMENTAL IMPACT'],
['CO2 Emissions (kg)', metrics.environmental_impact.co2_emissions.kg.toFixed(2)],
['CO2 Emissions (tons)', metrics.environmental_impact.co2_emissions.tons.toFixed(4)],
['Trees to Offset', metrics.environmental_impact.co2_emissions.trees_to_offset.toFixed(1)],
['Equivalent Car KM', metrics.environmental_impact.co2_emissions.car_km_equivalent.toFixed(0)],
['Water Footprint (liters)', metrics.environmental_impact.water_footprint.liters.toFixed(0)],
['Water Footprint (m³)', metrics.environmental_impact.water_footprint.cubic_meters.toFixed(2)],
['Equivalent Showers', metrics.environmental_impact.water_footprint.shower_equivalent.toFixed(0)],
['Land Use (m²)', metrics.environmental_impact.land_use.square_meters.toFixed(2)],
['Land Use (hectares)', metrics.environmental_impact.land_use.hectares.toFixed(4)],
[],
['FINANCIAL IMPACT'],
['Waste Cost (EUR)', metrics.financial_impact.waste_cost_eur.toFixed(2)],
['Potential Monthly Savings (EUR)', metrics.financial_impact.potential_monthly_savings.toFixed(2)],
['ROI on Prevention (%)', metrics.financial_impact.roi_on_waste_prevention.toFixed(2)],
[],
['AVOIDED WASTE (AI PREDICTIONS)'],
['Waste Avoided (kg)', metrics.avoided_waste.total_waste_avoided_kg.toFixed(2)],
['Cost Savings (EUR)', metrics.avoided_waste.cost_savings_eur.toFixed(2)],
['CO2 Avoided (kg)', metrics.avoided_waste.environmental_impact_avoided.co2_kg.toFixed(2)],
['Water Saved (liters)', metrics.avoided_waste.environmental_impact_avoided.water_liters.toFixed(0)],
[],
['GRANT READINESS'],
['Certification Ready', metrics.sdg_compliance.certification_ready ? 'Yes' : 'No'],
['Eligible Programs', Object.values(metrics.grant_readiness.grant_programs).filter(p => p.eligible).length],
['Recommended Applications', metrics.grant_readiness.recommended_applications.join(', ')]
];
// Convert to CSV string
const csvContent = csvRows.map(row => row.join(',')).join('\n');
// Create download
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// Fetch sustainability metrics
const {
data: metrics,
@@ -226,12 +295,10 @@ const SustainabilityPage: React.FC = () => {
id: "export-report",
label: t('sustainability:actions.export_report', 'Exportar Informe'),
icon: Download,
onClick: () => {
// TODO: Implement export
console.log('Export sustainability report');
},
onClick: exportToCSV,
variant: "outline",
size: "sm"
size: "sm",
disabled: !metrics
}
]}
/>

View File

@@ -17,9 +17,9 @@ import type { ItemType } from '../../../../components/domain/unified-wizard';
// Import AddStockModal separately since we need it for adding batches
import AddStockModal from '../../../../components/domain/inventory/AddStockModal';
import { useIngredients, useStockAnalytics, useStockMovements, useStockByIngredient, useCreateIngredient, useSoftDeleteIngredient, useHardDeleteIngredient, useAddStock, useConsumeStock, useUpdateIngredient, useUpdateStock, useTransformationsByIngredient } from '../../../../api/hooks/inventory';
import { useIngredients, useStockAnalytics, useStockMovements, useStockByIngredient, useCreateIngredient, useSoftDeleteIngredient, useHardDeleteIngredient, useAddStock, useConsumeStock, useUpdateIngredient, useUpdateStock, useCreateStockMovement, useTransformationsByIngredient } from '../../../../api/hooks/inventory';
import { useTenantId } from '../../../../hooks/useTenantId';
import { IngredientResponse, StockCreate, StockMovementCreate, IngredientCreate } from '../../../../api/types/inventory';
import { IngredientResponse, StockCreate, StockMovementCreate, StockMovementType, IngredientCreate } from '../../../../api/types/inventory';
import { subscriptionService } from '../../../../api/services/subscription';
import { useQueryClient } from '@tanstack/react-query';
@@ -55,6 +55,7 @@ const InventoryPage: React.FC = () => {
const consumeStockMutation = useConsumeStock();
const updateIngredientMutation = useUpdateIngredient();
const updateStockMutation = useUpdateStock();
const createStockMovementMutation = useCreateStockMovement();
// API Data
const {
@@ -795,8 +796,36 @@ const InventoryPage: React.FC = () => {
});
}}
onMarkAsWaste={async (batchId) => {
// TODO: Implement mark as waste functionality
console.log('Mark as waste:', batchId);
if (!tenantId || !batches) return;
try {
// Find the batch to get its details
const batch = batches.find(b => b.id === batchId);
if (!batch) {
console.error('Batch not found:', batchId);
return;
}
// Create a waste movement for the entire current quantity
const movementData: StockMovementCreate = {
ingredient_id: selectedItem!.id,
stock_id: batchId,
movement_type: StockMovementType.WASTE,
quantity: batch.current_quantity,
unit_cost: batch.unit_cost || undefined,
notes: `Batch marked as waste. Reason: ${batch.is_expired ? 'Expired' : 'Damaged/Spoiled'}`
};
await createStockMovementMutation.mutateAsync({
tenantId,
movementData
});
// Refresh the batches list
queryClient.invalidateQueries({ queryKey: ['stock', 'byIngredient', tenantId, selectedItem!.id] });
} catch (error) {
console.error('Failed to mark batch as waste:', error);
}
}}
waitForRefetch={true}
isRefetching={isRefetchingBatches}

View File

@@ -21,7 +21,7 @@ import {
CustomerType,
CustomerSegment
} from '../../../../api/types/orders';
import { useOrders, useCustomers, useOrdersDashboard, useCreateOrder, useCreateCustomer, useUpdateCustomer } from '../../../../api/hooks/orders';
import { useOrders, useCustomers, useOrdersDashboard, useCreateOrder, useUpdateOrder, useCreateCustomer, useUpdateCustomer } from '../../../../api/hooks/orders';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useAuthUser } from '../../../../stores/auth.store';
import { OrderFormModal } from '../../../../components/domain/orders';
@@ -76,6 +76,7 @@ const OrdersPage: React.FC = () => {
// Mutations
const createOrderMutation = useCreateOrder();
const updateOrderMutation = useUpdateOrder();
const createCustomerMutation = useCreateCustomer();
const updateCustomerMutation = useUpdateCustomer();
@@ -634,11 +635,36 @@ const OrdersPage: React.FC = () => {
sections={sections}
showDefaultActions={true}
onSave={async () => {
// TODO: Implement order update functionality
// Note: The backend only has updateOrderStatus, not a general update endpoint
// For now, orders can be updated via status changes using useUpdateOrderStatus
console.log('Saving order:', selectedOrder);
console.warn('Order update not yet implemented - only status updates are supported via useUpdateOrderStatus');
if (!selectedOrder || !tenantId) return;
try {
// Build the update payload from the selectedOrder state
const updateData = {
status: selectedOrder.status,
priority: selectedOrder.priority,
requested_delivery_date: selectedOrder.requested_delivery_date,
delivery_method: selectedOrder.delivery_method,
delivery_address: selectedOrder.delivery_address,
delivery_instructions: selectedOrder.delivery_instructions,
delivery_window_start: selectedOrder.delivery_window_start,
delivery_window_end: selectedOrder.delivery_window_end,
payment_method: selectedOrder.payment_method,
payment_status: selectedOrder.payment_status,
special_instructions: selectedOrder.special_instructions,
custom_requirements: selectedOrder.custom_requirements,
allergen_warnings: selectedOrder.allergen_warnings,
};
await updateOrderMutation.mutateAsync({
tenantId: tenantId,
orderId: selectedOrder.id,
data: updateData
});
setShowForm(false);
} catch (error) {
console.error('Failed to update order:', error);
}
}}
onFieldChange={(sectionIndex, fieldIndex, value) => {
const newOrder = { ...selectedOrder };

View File

@@ -11,6 +11,7 @@ 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';
import { salesService } from '../../../../api/services/sales';
import { bakeryColors } from '../../../../styles/colors';
// Import new POS components
@@ -18,6 +19,7 @@ import { POSProductCard } from '../../../../components/domain/pos/POSProductCard
import { POSCart } from '../../../../components/domain/pos/POSCart';
import { POSPayment } from '../../../../components/domain/pos/POSPayment';
import { CreatePOSConfigModal } from '../../../../components/domain/pos/CreatePOSConfigModal';
import { POSSyncStatus } from '../../../../components/domain/pos/POSSyncStatus';
interface CartItem {
id: string;
@@ -752,17 +754,37 @@ const POSPage: React.FC = () => {
const tax = subtotal * taxRate;
const total = subtotal + tax;
const processPayment = (paymentData: any) => {
const processPayment = async (paymentData: any) => {
if (cart.length === 0) return;
console.log('Processing payment:', {
cart,
...paymentData,
total,
});
try {
// Create sales records for each item in the cart
const saleDate = new Date().toISOString().split('T')[0];
setCart([]);
showToast.success('Venta procesada exitosamente');
for (const item of cart) {
const salesData = {
inventory_product_id: item.id, // Product ID for inventory tracking
product_name: item.name,
product_category: 'finished_product',
quantity_sold: item.quantity,
unit_price: item.price,
total_amount: item.price * item.quantity,
sale_date: saleDate,
sales_channel: 'pos',
source: 'manual_pos',
payment_method: paymentData.method || 'cash',
notes: paymentData.notes || 'Venta desde POS manual',
};
await salesService.createSalesRecord(tenantId, salesData);
}
setCart([]);
showToast.success(`Venta procesada exitosamente: €${total.toFixed(2)}`);
} catch (error: any) {
console.error('Error processing payment:', error);
showToast.error(error.response?.data?.detail || 'Error al procesar la venta');
}
};
// Loading and error states
@@ -1030,6 +1052,11 @@ const POSPage: React.FC = () => {
{posData.configurations.length > 0 && (
<TransactionsSection tenantId={tenantId} />
)}
{/* POS to Sales Sync Status - Only show if there are configurations */}
{posData.configurations.length > 0 && (
<POSSyncStatus tenantId={tenantId} />
)}
</div>
)}

View File

@@ -135,78 +135,19 @@ const ProductionPage: React.FC = () => {
// The QualityCheckModal should be enhanced to handle stage-specific checks
};
// Helper function to generate mock process stage data for the selected batch
const generateMockProcessStageData = (batch: ProductionBatchResponse) => {
// Mock data based on batch status - this would come from the API in real implementation
const mockProcessStage = {
current: batch.status === ProductionStatusEnum.PENDING ? 'mixing' as const :
batch.status === ProductionStatusEnum.IN_PROGRESS ? 'baking' as const :
batch.status === ProductionStatusEnum.QUALITY_CHECK ? 'cooling' as const :
'finishing' as const,
history: batch.status !== ProductionStatusEnum.PENDING ? [
{ stage: 'mixing' as const, timestamp: batch.actual_start_time || batch.planned_start_time, duration: 30 },
...(batch.status === ProductionStatusEnum.IN_PROGRESS || batch.status === ProductionStatusEnum.QUALITY_CHECK || batch.status === ProductionStatusEnum.COMPLETED ? [
{ stage: 'proofing' as const, timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), duration: 90 },
{ stage: 'shaping' as const, timestamp: new Date(Date.now() - 1 * 60 * 60 * 1000).toISOString(), duration: 15 }
] : []),
...(batch.status === ProductionStatusEnum.QUALITY_CHECK || batch.status === ProductionStatusEnum.COMPLETED ? [
{ stage: 'baking' as const, timestamp: new Date(Date.now() - 30 * 60 * 1000).toISOString(), duration: 45 }
] : [])
] : [],
pendingQualityChecks: batch.status === ProductionStatusEnum.IN_PROGRESS ? [
{
id: 'qc1',
name: 'Control de temperatura interna',
stage: 'baking' as const,
isRequired: true,
isCritical: true,
status: 'pending' as const,
checkType: 'temperature' as const
}
] : batch.status === ProductionStatusEnum.QUALITY_CHECK ? [
{
id: 'qc2',
name: 'Inspección visual final',
stage: 'cooling' as const,
isRequired: true,
isCritical: false,
status: 'pending' as const,
checkType: 'visual' as const
}
] : [],
completedQualityChecks: batch.status === ProductionStatusEnum.COMPLETED ? [
{
id: 'qc1',
name: 'Control de temperatura interna',
stage: 'baking' as const,
isRequired: true,
isCritical: true,
status: 'completed' as const,
checkType: 'temperature' as const
},
{
id: 'qc2',
name: 'Inspección visual final',
stage: 'cooling' as const,
isRequired: true,
isCritical: false,
status: 'completed' as const,
checkType: 'visual' as const
}
] : batch.status === ProductionStatusEnum.IN_PROGRESS ? [
{
id: 'qc3',
name: 'Verificación de masa',
stage: 'mixing' as const,
isRequired: true,
isCritical: false,
status: 'completed' as const,
checkType: 'visual' as const
}
] : []
// Helper function to get process stage data from the batch (now from real backend data)
const getProcessStageData = (batch: ProductionBatchResponse) => {
// Backend now provides these fields in the API response:
// - current_process_stage
// - process_stage_history
// - pending_quality_checks
// - completed_quality_checks
return {
current: batch.current_process_stage || 'mixing',
history: batch.process_stage_history || [],
pendingQualityChecks: batch.pending_quality_checks || [],
completedQualityChecks: batch.completed_quality_checks || []
};
return mockProcessStage;
};
@@ -576,7 +517,7 @@ const ProductionPage: React.FC = () => {
label: '',
value: (
<CompactProcessStageTracker
processStage={generateMockProcessStageData(selectedBatch)}
processStage={getProcessStageData(selectedBatch)}
onAdvanceStage={(currentStage) => handleStageAdvance(selectedBatch.id, currentStage)}
onQualityCheck={(checkId) => {
setShowQualityModal(true);

View File

@@ -1,8 +1,19 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { PublicLayout } from '../../components/layout';
import { Button } from '../../components/ui';
import {
Button,
TableOfContents,
ProgressBar,
FloatingCTA,
ScrollReveal,
SavingsCalculator,
StepTimeline,
AnimatedCounter,
TOCSection,
TimelineStep
} from '../../components/ui';
import { getDemoUrl } from '../../utils/navigation';
import {
Clock,
@@ -35,6 +46,110 @@ import {
const FeaturesPage: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
// Automatic System Timeline Steps
const automaticSystemSteps: TimelineStep[] = [
{
id: 'step1',
number: 1,
title: t('features:automatic.step1.title', 'Revisa Todo Tu Inventario'),
color: 'blue',
items: [
t('features:automatic.step1.item1', 'Cuenta cada kilo de harina, cada litro de leche'),
t('features:automatic.step1.item2', 'Comprueba fechas de caducidad'),
t('features:automatic.step1.item3', 'Ve qué llega hoy de proveedores'),
],
},
{
id: 'step2',
number: 2,
title: t('features:automatic.step2.title', 'Predice Ventas de Hoy'),
color: 'purple',
items: [
t('features:automatic.step2.item1', 'Analiza el día (lunes lluvioso, fiesta local, colegio cerrado)'),
t('features:automatic.step2.item2', 'Compara con días similares del pasado'),
t('features:automatic.step2.item3', 'Te dice: "Hoy venderás 80 croissants, 120 barras, 50 magdalenas"'),
],
},
{
id: 'step3',
number: 3,
title: t('features:automatic.step3.title', 'Planifica Qué Hacer'),
color: 'green',
items: [
t('features:automatic.step3.item1', 'Calcula exactamente cuánto hornear'),
t('features:automatic.step3.item2', 'Te da una lista lista para ejecutar'),
t('features:automatic.step3.item3', '"Haz 80 croissants (no 100), usa 5kg mantequilla, 3kg harina..."'),
],
},
{
id: 'step4',
number: 4,
title: t('features:automatic.step4.title', 'Gestiona Inventario Inteligentemente'),
color: 'amber',
items: [
t('features:automatic.step4.projection_title', 'Proyecta 7 días hacia adelante:'),
t('features:automatic.step4.solution', '"Pide 50kg hoy, llega en 3 días, problema resuelto"'),
],
},
{
id: 'step5',
number: 5,
title: t('features:automatic.step5.title', 'Crea Pedidos a Proveedores'),
color: 'red',
items: [
t('features:automatic.step5.item1', 'Sabe que Proveedor A tarda 3 días, Proveedor B tarda 5'),
t('features:automatic.step5.item2', 'Calcula cuándo pedir para que llegue justo a tiempo'),
t('features:automatic.step5.item3', 'Prepara pedidos listos para aprobar con 1 clic'),
],
},
{
id: 'step6',
number: 6,
title: t('features:automatic.step6.title', 'Previene Desperdicios'),
color: 'teal',
items: [
t('features:automatic.step6.item1', '"Tienes leche que caduca en 5 días"'),
t('features:automatic.step6.item2', '"Solo usarás 15L en 5 días"'),
t('features:automatic.step6.item3', '"No pidas más de 15L, se desperdiciará"'),
],
},
];
// Table of Contents sections
const tocSections: TOCSection[] = [
{
id: 'automatic-system',
label: t('features:toc.automatic', 'Sistema Automático'),
icon: <Clock className="w-4 h-4" />
},
{
id: 'local-intelligence',
label: t('features:toc.local', 'Inteligencia Local'),
icon: <MapPin className="w-4 h-4" />
},
{
id: 'demand-forecasting',
label: t('features:toc.forecasting', 'Predicción de Demanda'),
icon: <Target className="w-4 h-4" />
},
{
id: 'waste-reduction',
label: t('features:toc.waste', 'Reducción de Desperdicios'),
icon: <Recycle className="w-4 h-4" />
},
{
id: 'sustainability',
label: t('features:toc.sustainability', 'Sostenibilidad'),
icon: <Leaf className="w-4 h-4" />
},
{
id: 'business-models',
label: t('features:toc.business', 'Modelos de Negocio'),
icon: <Store className="w-4 h-4" />
},
];
return (
<PublicLayout
@@ -47,226 +162,71 @@ const FeaturesPage: React.FC = () => {
variant: "default"
}}
>
{/* Progress Bar */}
<ProgressBar position="top" height={3} />
{/* Floating CTA */}
<FloatingCTA
text={t('features:cta.button', 'Solicitar Demo')}
onClick={() => navigate(getDemoUrl())}
icon={<ArrowRight className="w-4 h-4" />}
position="bottom-right"
showAfterScroll={500}
dismissible
/>
{/* Main Content with Sidebar */}
<div className="flex gap-8">
{/* Sidebar - Table of Contents */}
<aside className="hidden lg:block w-64 flex-shrink-0">
<TableOfContents sections={tocSections} />
</aside>
{/* Main Content */}
<div className="flex-1 min-w-0">
{/* Hero Section */}
<section className="bg-gradient-to-br from-[var(--bg-primary)] via-[var(--bg-secondary)] to-[var(--color-primary)]/5 py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center max-w-4xl mx-auto">
<h1 className="text-4xl lg:text-6xl font-extrabold text-[var(--text-primary)] mb-6">
{t('features:hero.title', 'Cómo Bakery-IA Trabaja Para Ti Cada Día')}
</h1>
<p className="text-xl text-[var(--text-secondary)] leading-relaxed">
{t('features:hero.subtitle', 'Todas las funcionalidades explicadas en lenguaje sencillo para dueños de panaderías')}
</p>
</div>
<ScrollReveal variant="fadeUp" delay={0.1}>
<div className="text-center max-w-4xl mx-auto">
<h1 className="text-4xl lg:text-6xl font-extrabold text-[var(--text-primary)] mb-6">
{t('features:hero.title', 'Cómo Bakery-IA Trabaja Para Ti Cada Día')}
</h1>
<p className="text-xl text-[var(--text-secondary)] leading-relaxed">
{t('features:hero.subtitle', 'Todas las funcionalidades explicadas en lenguaje sencillo para dueños de panaderías')}
</p>
</div>
</ScrollReveal>
</div>
</section>
{/* Feature 1: Automatic Daily System - THE KILLER FEATURE */}
<section className="py-20 bg-[var(--bg-primary)]">
<section id="automatic-system" className="py-20 bg-[var(--bg-primary)] scroll-mt-24">
<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 bg-[var(--color-primary)]/10 text-[var(--color-primary)] px-4 py-2 rounded-full text-sm font-medium mb-6">
<Clock className="w-4 h-4" />
<span>{t('features:automatic.badge', 'La Funcionalidad Estrella')}</span>
</div>
<h2 className="text-3xl lg:text-5xl font-extrabold text-[var(--text-primary)] mb-6">
{t('features:automatic.title', 'Tu Asistente Personal Que Nunca Duerme')}
</h2>
<p className="text-xl text-[var(--text-secondary)] max-w-3xl mx-auto">
{t('features:automatic.intro', 'Imagina contratar un ayudante súper organizado que llega a las 5:30 AM (antes que tú) y hace todo esto AUTOMÁTICAMENTE:')}
</p>
</div>
<div className="max-w-5xl mx-auto space-y-8">
{/* Step 1 */}
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-2xl p-8 border-2 border-blue-200 dark:border-blue-800">
<div className="flex gap-6 items-start">
<div className="w-16 h-16 bg-blue-600 rounded-full flex items-center justify-center text-white text-2xl font-bold flex-shrink-0">
1
</div>
<div>
<h3 className="text-2xl font-bold text-[var(--text-primary)] mb-3">
{t('features:automatic.step1.title', 'Revisa Todo Tu Inventario')}
</h3>
<ul className="space-y-2 text-[var(--text-secondary)]">
<li className="flex items-start gap-2">
<CheckCircle2 className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" />
<span>{t('features:automatic.step1.item1', 'Cuenta cada kilo de harina, cada litro de leche')}</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" />
<span>{t('features:automatic.step1.item2', 'Comprueba fechas de caducidad')}</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" />
<span>{t('features:automatic.step1.item3', 'Ve qué llega hoy de proveedores')}</span>
</li>
</ul>
</div>
<ScrollReveal variant="fadeUp" delay={0.1}>
<div className="text-center mb-16">
<div className="inline-flex items-center gap-2 bg-[var(--color-primary)]/10 text-[var(--color-primary)] px-4 py-2 rounded-full text-sm font-medium mb-6">
<Clock className="w-4 h-4" />
<span>{t('features:automatic.badge', 'La Funcionalidad Estrella')}</span>
</div>
<h2 className="text-3xl lg:text-5xl font-extrabold text-[var(--text-primary)] mb-6">
{t('features:automatic.title', 'Tu Asistente Personal Que Nunca Duerme')}
</h2>
<p className="text-xl text-[var(--text-secondary)] max-w-3xl mx-auto">
{t('features:automatic.intro', 'Imagina contratar un ayudante súper organizado que llega a las 5:30 AM (antes que tú) y hace todo esto AUTOMÁTICAMENTE:')}
</p>
</div>
</ScrollReveal>
{/* Step 2 */}
<div className="bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 rounded-2xl p-8 border-2 border-purple-200 dark:border-purple-800">
<div className="flex gap-6 items-start">
<div className="w-16 h-16 bg-purple-600 rounded-full flex items-center justify-center text-white text-2xl font-bold flex-shrink-0">
2
</div>
<div>
<h3 className="text-2xl font-bold text-[var(--text-primary)] mb-3">
{t('features:automatic.step2.title', 'Predice Ventas de Hoy')}
</h3>
<ul className="space-y-2 text-[var(--text-secondary)]">
<li className="flex items-start gap-2">
<CheckCircle2 className="w-5 h-5 text-purple-600 mt-0.5 flex-shrink-0" />
<span>{t('features:automatic.step2.item1', 'Analiza el día (lunes lluvioso, fiesta local, colegio cerrado)')}</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 className="w-5 h-5 text-purple-600 mt-0.5 flex-shrink-0" />
<span>{t('features:automatic.step2.item2', 'Compara con días similares del pasado')}</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 className="w-5 h-5 text-purple-600 mt-0.5 flex-shrink-0" />
<span>{t('features:automatic.step2.item3', 'Te dice: "Hoy venderás 80 croissants, 120 barras, 50 magdalenas"')}</span>
</li>
</ul>
</div>
</div>
</div>
{/* Step 3 */}
<div className="bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 rounded-2xl p-8 border-2 border-green-200 dark:border-green-800">
<div className="flex gap-6 items-start">
<div className="w-16 h-16 bg-green-600 rounded-full flex items-center justify-center text-white text-2xl font-bold flex-shrink-0">
3
</div>
<div>
<h3 className="text-2xl font-bold text-[var(--text-primary)] mb-3">
{t('features:automatic.step3.title', 'Planifica Qué Hacer')}
</h3>
<ul className="space-y-2 text-[var(--text-secondary)]">
<li className="flex items-start gap-2">
<CheckCircle2 className="w-5 h-5 text-green-600 mt-0.5 flex-shrink-0" />
<span>{t('features:automatic.step3.item1', 'Calcula exactamente cuánto hornear')}</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 className="w-5 h-5 text-green-600 mt-0.5 flex-shrink-0" />
<span>{t('features:automatic.step3.item2', 'Te da una lista lista para ejecutar')}</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 className="w-5 h-5 text-green-600 mt-0.5 flex-shrink-0" />
<span>{t('features:automatic.step3.item3', '"Haz 80 croissants (no 100), usa 5kg mantequilla, 3kg harina..."')}</span>
</li>
</ul>
</div>
</div>
</div>
{/* Step 4 - Inventory Management */}
<div className="bg-gradient-to-r from-amber-50 to-orange-50 dark:from-amber-900/20 dark:to-orange-900/20 rounded-2xl p-8 border-2 border-amber-200 dark:border-amber-800">
<div className="flex gap-6 items-start">
<div className="w-16 h-16 bg-amber-600 rounded-full flex items-center justify-center text-white text-2xl font-bold flex-shrink-0">
4
</div>
<div>
<h3 className="text-2xl font-bold text-[var(--text-primary)] mb-3">
{t('features:automatic.step4.title', 'Gestiona Inventario Inteligentemente')}
</h3>
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 mb-4 border border-amber-300 dark:border-amber-700">
<p className="text-sm font-medium text-[var(--text-secondary)] mb-2">{t('features:automatic.step4.projection_title', 'Proyecta 7 días hacia adelante:')}</p>
<div className="space-y-1 text-sm">
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">{t('features:automatic.step4.day1', 'Hoy: 50kg harina')}</span>
<span className="text-green-600"></span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">{t('features:automatic.step4.day2', 'Mañana: 42kg')}</span>
<span className="text-green-600"></span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">{t('features:automatic.step4.day3', 'Pasado: 30kg')}</span>
<span className="text-amber-600"></span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">{t('features:automatic.step4.day4', 'Día 4: 15kg')}</span>
<span className="text-red-600">🚨</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--text-secondary)]">{t('features:automatic.step4.day5', 'Día 5: Te quedarías sin harina')}</span>
<span className="text-red-600"></span>
</div>
</div>
</div>
<div className="bg-green-100 dark:bg-green-900/30 rounded-lg p-4 border-l-4 border-green-600">
<p className="font-bold text-green-900 dark:text-green-100 mb-2">
{t('features:automatic.step4.solution_title', 'SOLUCIÓN AUTOMÁTICA:')}
</p>
<p className="text-green-800 dark:text-green-200">
{t('features:automatic.step4.solution', '"Pide 50kg hoy, llega en 3 días, problema resuelto"')}
</p>
</div>
</div>
</div>
</div>
{/* Step 5 - Purchase Orders */}
<div className="bg-gradient-to-r from-red-50 to-rose-50 dark:from-red-900/20 dark:to-rose-900/20 rounded-2xl p-8 border-2 border-red-200 dark:border-red-800">
<div className="flex gap-6 items-start">
<div className="w-16 h-16 bg-red-600 rounded-full flex items-center justify-center text-white text-2xl font-bold flex-shrink-0">
5
</div>
<div>
<h3 className="text-2xl font-bold text-[var(--text-primary)] mb-3">
{t('features:automatic.step5.title', 'Crea Pedidos a Proveedores')}
</h3>
<ul className="space-y-2 text-[var(--text-secondary)]">
<li className="flex items-start gap-2">
<CheckCircle2 className="w-5 h-5 text-red-600 mt-0.5 flex-shrink-0" />
<span>{t('features:automatic.step5.item1', 'Sabe que Proveedor A tarda 3 días, Proveedor B tarda 5')}</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 className="w-5 h-5 text-red-600 mt-0.5 flex-shrink-0" />
<span>{t('features:automatic.step5.item2', 'Calcula cuándo pedir para que llegue justo a tiempo')}</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 className="w-5 h-5 text-red-600 mt-0.5 flex-shrink-0" />
<span>{t('features:automatic.step5.item3', 'Prepara pedidos listos para aprobar con 1 clic')}</span>
</li>
</ul>
</div>
</div>
</div>
{/* Step 6 - Waste Prevention */}
<div className="bg-gradient-to-r from-teal-50 to-cyan-50 dark:from-teal-900/20 dark:to-cyan-900/20 rounded-2xl p-8 border-2 border-teal-200 dark:border-teal-800">
<div className="flex gap-6 items-start">
<div className="w-16 h-16 bg-teal-600 rounded-full flex items-center justify-center text-white text-2xl font-bold flex-shrink-0">
6
</div>
<div>
<h3 className="text-2xl font-bold text-[var(--text-primary)] mb-3">
{t('features:automatic.step6.title', 'Previene Desperdicios')}
</h3>
<div className="space-y-3 text-[var(--text-secondary)]">
<p className="font-medium">{t('features:automatic.step6.perishables', 'Ingredientes perecederos (leche, nata, huevos):')}</p>
<ul className="space-y-2">
<li className="flex items-start gap-2">
<CheckCircle2 className="w-5 h-5 text-teal-600 mt-0.5 flex-shrink-0" />
<span>{t('features:automatic.step6.item1', '"Tienes leche que caduca en 5 días"')}</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 className="w-5 h-5 text-teal-600 mt-0.5 flex-shrink-0" />
<span>{t('features:automatic.step6.item2', '"Solo usarás 15L en 5 días"')}</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 className="w-5 h-5 text-teal-600 mt-0.5 flex-shrink-0" />
<span>{t('features:automatic.step6.item3', '"No pidas más de 15L, se desperdiciará"')}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
<ScrollReveal variant="fadeUp" delay={0.2}>
<div className="max-w-5xl mx-auto">
{/* Automatic System Timeline */}
<StepTimeline
steps={automaticSystemSteps}
orientation="vertical"
showConnector
animated
/>
{/* Morning Result */}
<div className="bg-gradient-to-r from-[var(--color-primary)] to-orange-600 rounded-2xl p-8 text-white">
@@ -328,29 +288,33 @@ const FeaturesPage: React.FC = () => {
</div>
</div>
</div>
</div>
</div>
</ScrollReveal>
</div>
</section>
{/* Feature 2: Local Intelligence */}
<section className="py-20 bg-[var(--bg-secondary)]">
<section id="local-intelligence" className="py-20 bg-[var(--bg-secondary)] scroll-mt-24">
<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 bg-[var(--color-primary)]/10 text-[var(--color-primary)] px-4 py-2 rounded-full text-sm font-medium mb-6">
<MapPin className="w-4 h-4" />
<span>{t('features:local.badge', 'Tu Ventaja Competitiva')}</span>
<ScrollReveal variant="fadeUp" delay={0.1}>
<div className="text-center mb-16">
<div className="inline-flex items-center gap-2 bg-[var(--color-primary)]/10 text-[var(--color-primary)] px-4 py-2 rounded-full text-sm font-medium mb-6">
<MapPin className="w-4 h-4" />
<span>{t('features:local.badge', 'Tu Ventaja Competitiva')}</span>
</div>
<h2 className="text-3xl lg:text-5xl font-extrabold text-[var(--text-primary)] mb-6">
{t('features:local.title', 'Tu Panadería Es Única. La IA También.')}
</h2>
<p className="text-xl text-[var(--text-secondary)] max-w-3xl mx-auto">
{t('features:local.intro', 'Las IA genéricas saben que es lunes. La TUYA sabe que:')}
</p>
</div>
<h2 className="text-3xl lg:text-5xl font-extrabold text-[var(--text-primary)] mb-6">
{t('features:local.title', 'Tu Panadería Es Única. La IA También.')}
</h2>
<p className="text-xl text-[var(--text-secondary)] max-w-3xl mx-auto">
{t('features:local.intro', 'Las IA genéricas saben que es lunes. La TUYA sabe que:')}
</p>
</div>
</ScrollReveal>
<div className="grid md:grid-cols-2 gap-8 max-w-6xl mx-auto">
{/* Schools */}
<div className="bg-[var(--bg-primary)] rounded-2xl p-6 border border-[var(--border-primary)] hover:shadow-xl transition-all">
<ScrollReveal variant="fadeUp" delay={0.1}>
<div className="bg-[var(--bg-primary)] rounded-2xl p-6 border border-[var(--border-primary)] hover:shadow-xl transition-all">
<div className="w-12 h-12 bg-blue-500/10 rounded-xl flex items-center justify-center mb-4">
<School className="w-6 h-6 text-blue-600" />
</div>
@@ -371,10 +335,12 @@ const FeaturesPage: React.FC = () => {
<span>{t('features:local.schools.item3', '"Los lunes a las 8:30 hay pico (padres tras dejar niños)"')}</span>
</li>
</ul>
</div>
</div>
</ScrollReveal>
{/* Offices */}
<div className="bg-[var(--bg-primary)] rounded-2xl p-6 border border-[var(--border-primary)] hover:shadow-xl transition-all">
<ScrollReveal variant="fadeUp" delay={0.15}>
<div className="bg-[var(--bg-primary)] rounded-2xl p-6 border border-[var(--border-primary)] hover:shadow-xl transition-all">
<div className="w-12 h-12 bg-purple-500/10 rounded-xl flex items-center justify-center mb-4">
<Building2 className="w-6 h-6 text-purple-600" />
</div>
@@ -395,10 +361,12 @@ const FeaturesPage: React.FC = () => {
<span>{t('features:local.offices.item3', '"Hora punta: 13:00-14:00 (bocadillos)"')}</span>
</li>
</ul>
</div>
</div>
</ScrollReveal>
{/* Gyms */}
<div className="bg-[var(--bg-primary)] rounded-2xl p-6 border border-[var(--border-primary)] hover:shadow-xl transition-all">
<ScrollReveal variant="fadeUp" delay={0.2}>
<div className="bg-[var(--bg-primary)] rounded-2xl p-6 border border-[var(--border-primary)] hover:shadow-xl transition-all">
<div className="w-12 h-12 bg-green-500/10 rounded-xl flex items-center justify-center mb-4">
<Dumbbell className="w-6 h-6 text-green-600" />
</div>
@@ -419,10 +387,12 @@ const FeaturesPage: React.FC = () => {
<span>{t('features:local.gyms.item3', '"Pico a las 7:00 AM y 19:00 PM"')}</span>
</li>
</ul>
</div>
</div>
</ScrollReveal>
{/* Competition */}
<div className="bg-[var(--bg-primary)] rounded-2xl p-6 border border-[var(--border-primary)] hover:shadow-xl transition-all">
<ScrollReveal variant="fadeUp" delay={0.25}>
<div className="bg-[var(--bg-primary)] rounded-2xl p-6 border border-[var(--border-primary)] hover:shadow-xl transition-all">
<div className="w-12 h-12 bg-amber-500/10 rounded-xl flex items-center justify-center mb-4">
<ShoppingBag className="w-6 h-6 text-amber-600" />
</div>
@@ -443,10 +413,12 @@ const FeaturesPage: React.FC = () => {
<span>{t('features:local.competition.item3', '"Oportunidad: Diferénciate con especialidades"')}</span>
</li>
</ul>
</div>
</div>
</ScrollReveal>
{/* Weather */}
<div className="bg-[var(--bg-primary)] rounded-2xl p-6 border border-[var(--border-primary)] hover:shadow-xl transition-all">
<ScrollReveal variant="fadeUp" delay={0.3}>
<div className="bg-[var(--bg-primary)] rounded-2xl p-6 border border-[var(--border-primary)] hover:shadow-xl transition-all">
<div className="w-12 h-12 bg-sky-500/10 rounded-xl flex items-center justify-center mb-4">
<Cloud className="w-6 h-6 text-sky-600" />
</div>
@@ -467,10 +439,12 @@ const FeaturesPage: React.FC = () => {
<span>{t('features:local.weather.item3', '"Calor → +30% productos frescos"')}</span>
</li>
</ul>
</div>
</div>
</ScrollReveal>
{/* Events */}
<div className="bg-[var(--bg-primary)] rounded-2xl p-6 border border-[var(--border-primary)] hover:shadow-xl transition-all">
<ScrollReveal variant="fadeUp" delay={0.35}>
<div className="bg-[var(--bg-primary)] rounded-2xl p-6 border border-[var(--border-primary)] hover:shadow-xl transition-all">
<div className="w-12 h-12 bg-pink-500/10 rounded-xl flex items-center justify-center mb-4">
<PartyPopper className="w-6 h-6 text-pink-600" />
</div>
@@ -491,11 +465,13 @@ const FeaturesPage: React.FC = () => {
<span>{t('features:local.events.item3', '"Partido importante → pico de ventas pre-evento"')}</span>
</li>
</ul>
</div>
</div>
</ScrollReveal>
</div>
{/* Why it matters */}
<div className="mt-12 max-w-4xl mx-auto bg-gradient-to-r from-[var(--color-primary)] to-orange-600 rounded-2xl p-8 text-white">
<ScrollReveal variant="fadeUp" delay={0.4}>
<div className="mt-12 max-w-4xl mx-auto bg-gradient-to-r from-[var(--color-primary)] to-orange-600 rounded-2xl p-8 text-white">
<h3 className="text-2xl font-bold mb-4 text-center">
{t('features:local.why_matters.title', 'Por qué importa:')}
</h3>
@@ -510,23 +486,26 @@ const FeaturesPage: React.FC = () => {
</div>
</div>
<p className="text-center mt-6 text-xl font-bold">
{t('features:local.accuracy', 'Precisión: 92% (vs 60-70% de sistemas genéricos)')}
Precisión: <AnimatedCounter value={92} suffix="%" className="inline text-[var(--color-primary)]" /> (vs 60-70% de sistemas genéricos)
</p>
</div>
</div>
</ScrollReveal>
</div>
</section>
{/* Feature 3: Demand Forecasting */}
<section className="py-20 bg-[var(--bg-primary)]">
<section id="demand-forecasting" className="py-20 bg-[var(--bg-primary)] scroll-mt-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)] mb-4">
{t('features:forecasting.title', 'Sabe Cuánto Venderás Mañana (92% de Precisión)')}
</h2>
<p className="text-xl text-[var(--text-secondary)] max-w-3xl mx-auto">
{t('features:forecasting.subtitle', 'No es magia. Es matemáticas con tus datos.')}
</p>
</div>
<ScrollReveal variant="fadeUp" delay={0.1}>
<div className="text-center mb-12">
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)] mb-4">
Sabe Cuánto Venderás Mañana (<AnimatedCounter value={92} suffix="%" className="inline text-[var(--color-primary)]" /> de Precisión)
</h2>
<p className="text-xl text-[var(--text-secondary)] max-w-3xl mx-auto">
{t('features:forecasting.subtitle', 'No es magia. Es matemáticas con tus datos.')}
</p>
</div>
</ScrollReveal>
<div className="max-w-5xl mx-auto">
<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-200 dark:border-blue-800 mb-8">
@@ -591,17 +570,20 @@ const FeaturesPage: React.FC = () => {
</section>
{/* Feature 4: Reduce Waste = Save Money */}
<section className="py-20 bg-[var(--bg-secondary)]">
<section id="waste-reduction" className="py-20 bg-[var(--bg-secondary)] scroll-mt-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)] mb-4">
{t('features:waste.title', 'Menos Pan en la Basura, Más Dinero en Tu Bolsillo')}
</h2>
</div>
<ScrollReveal variant="fadeUp" delay={0.1}>
<div className="text-center mb-12">
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)] mb-4">
{t('features:waste.title', 'Menos Pan en la Basura, Más Dinero en Tu Bolsillo')}
</h2>
</div>
</ScrollReveal>
<div className="max-w-5xl mx-auto grid md:grid-cols-2 gap-8">
{/* Before */}
<div className="bg-red-50 dark:bg-red-900/20 rounded-2xl p-8 border-2 border-red-200 dark:border-red-800">
<ScrollReveal variant="fadeLeft" delay={0.2}>
<div className="bg-red-50 dark:bg-red-900/20 rounded-2xl p-8 border-2 border-red-200 dark:border-red-800">
<div className="w-12 h-12 bg-red-500/20 rounded-xl flex items-center justify-center mb-4">
<AlertTriangle className="w-6 h-6 text-red-600" />
</div>
@@ -615,10 +597,12 @@ const FeaturesPage: React.FC = () => {
<li className="font-bold text-red-700 dark:text-red-400">{t('features:waste.before.monthly', 'Al mes: €100 × 30 = €3,000 perdidos')}</li>
<li className="font-bold text-red-900 dark:text-red-300 text-lg">{t('features:waste.before.yearly', 'Al año: €36,000 tirados a la basura')}</li>
</ul>
</div>
</div>
</ScrollReveal>
{/* After */}
<div className="bg-green-50 dark:bg-green-900/20 rounded-2xl p-8 border-2 border-green-200 dark:border-green-800">
<ScrollReveal variant="fadeRight" delay={0.2}>
<div className="bg-green-50 dark:bg-green-900/20 rounded-2xl p-8 border-2 border-green-200 dark:border-green-800">
<div className="w-12 h-12 bg-green-500/20 rounded-xl flex items-center justify-center mb-4">
<TrendingUp className="w-6 h-6 text-green-600" />
</div>
@@ -630,29 +614,45 @@ const FeaturesPage: React.FC = () => {
<li>{t('features:waste.after.item2', 'Desperdicio: 5 × €2 = €10/día')}</li>
<li className="font-bold text-green-700 dark:text-green-400">{t('features:waste.after.monthly', 'Al mes: €300')}</li>
<li className="font-bold text-green-900 dark:text-green-300 text-xl bg-green-100 dark:bg-green-900/40 p-3 rounded-lg">
{t('features:waste.after.savings', 'AHORRO: €2,700/mes (€32,400/año)')}
AHORRO: <AnimatedCounter value={2700} prefix="€" className="inline" decimals={0} />/mes (<AnimatedCounter value={32400} prefix="€" className="inline" decimals={0} />/año)
</li>
</ul>
<p className="mt-4 text-sm font-medium text-green-700 dark:text-green-400">
{t('features:waste.after.roi', 'Recuperas la inversión en semana 1.')}
</p>
</div>
</div>
</ScrollReveal>
</div>
{/* Interactive Savings Calculator */}
<div className="mt-12 max-w-4xl mx-auto">
<ScrollReveal variant="scaleUp" delay={0.2}>
<SavingsCalculator
defaultWaste={50}
pricePerUnit={2}
wasteReduction={80}
unitName="barras"
currency="€"
/>
</ScrollReveal>
</div>
</div>
</section>
{/* Feature 5: Sustainability + Grants */}
<section className="py-20 bg-[var(--bg-primary)]">
<section id="sustainability" className="py-20 bg-[var(--bg-primary)] scroll-mt-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<div className="inline-flex items-center gap-2 bg-green-500/10 text-green-600 dark:text-green-400 px-4 py-2 rounded-full text-sm font-medium mb-6">
<Leaf className="w-4 h-4" />
<span>{t('features:sustainability.badge', 'Funcionalidad del Sistema')}</span>
<ScrollReveal variant="fadeUp" delay={0.1}>
<div className="text-center mb-12">
<div className="inline-flex items-center gap-2 bg-green-500/10 text-green-600 dark:text-green-400 px-4 py-2 rounded-full text-sm font-medium mb-6">
<Leaf className="w-4 h-4" />
<span>{t('features:sustainability.badge', 'Funcionalidad del Sistema')}</span>
</div>
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)] mb-4">
{t('features:sustainability.title', 'Impacto Ambiental y Sostenibilidad')}
</h2>
</div>
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)] mb-4">
{t('features:sustainability.title', 'Impacto Ambiental y Sostenibilidad')}
</h2>
</div>
</ScrollReveal>
{/* UN SDG Compliance */}
<div className="max-w-5xl mx-auto mb-12">
@@ -813,7 +813,7 @@ const FeaturesPage: React.FC = () => {
</section>
{/* Feature 6: Business Models */}
<section className="py-20 bg-[var(--bg-secondary)]">
<section id="business-models" className="py-20 bg-[var(--bg-secondary)] scroll-mt-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)] mb-4">
@@ -896,6 +896,8 @@ const FeaturesPage: React.FC = () => {
</Link>
</div>
</section>
</div>
</div>
</PublicLayout>
);
};

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Button } from '../../components/ui';
import { Button, ScrollReveal, FloatingCTA, AnimatedCounter } from '../../components/ui';
import { PublicLayout } from '../../components/layout';
import { PricingSection } from '../../components/subscription';
import { getRegisterUrl, getDemoUrl } from '../../utils/navigation';
@@ -26,6 +26,7 @@ import {
const LandingPage: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
return (
<PublicLayout
@@ -38,26 +39,42 @@ const LandingPage: React.FC = () => {
variant: "default"
}}
>
{/* Floating CTA - appears after scrolling */}
<FloatingCTA
text={t('landing:hero.cta_primary', 'Únete al Programa Piloto')}
onClick={() => navigate(getRegisterUrl())}
icon={<ArrowRight className="w-4 h-4" />}
position="bottom-right"
showAfterScroll={600}
dismissible
/>
{/* Hero Section */}
<section className="relative py-20 lg:py-32 bg-gradient-to-br from-[var(--bg-primary)] via-[var(--bg-secondary)] to-[var(--color-primary)]/5">
<section className="relative py-20 lg:py-32 bg-gradient-to-br from-[var(--bg-primary)] via-[var(--bg-secondary)] to-[var(--color-primary)]/5 overflow-hidden">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
{/* Pre-headline */}
<div className="mb-4">
<p className="text-sm md:text-base font-medium text-[var(--text-tertiary)]">
{t('landing:hero.pre_headline', 'Para Panaderías que Pierden €500-2,000/Mes en Desperdicios')}
</p>
</div>
{/* Scarcity Badge */}
<div className="mb-6 inline-block">
<div className="bg-gradient-to-r from-amber-50 via-orange-50 to-amber-50 dark:from-amber-900/20 dark:via-orange-900/20 dark:to-amber-900/20 border-2 border-amber-400 dark:border-amber-500 rounded-full px-6 py-3 shadow-lg">
<div className="mb-8 inline-block">
<div className="bg-gradient-to-r from-amber-50 via-orange-50 to-amber-50 dark:from-amber-900/20 dark:via-orange-900/20 dark:to-amber-900/20 border-2 border-amber-400 dark:border-amber-500 rounded-full px-6 py-3 shadow-lg hover:shadow-xl transition-shadow">
<p className="text-sm font-bold text-amber-700 dark:text-amber-300">
🔥 {t('landing:hero.scarcity', 'Solo 12 plazas restantes de 20 • 3 meses GRATIS')}
</p>
</div>
</div>
<h1 className="text-4xl tracking-tight font-extrabold text-[var(--text-primary)] sm:text-5xl lg:text-7xl">
<span className="block">{t('landing:hero.title_line1', 'Aumenta Ganancias,')}</span>
<span className="block text-[var(--color-primary)]">{t('landing:hero.title_line2', 'Reduce Desperdicios')}</span>
<h1 className="text-4xl tracking-tight font-extrabold text-[var(--text-primary)] sm:text-5xl lg:text-7xl leading-tight">
<span className="block">{t('landing:hero.title_option_a_line1', 'Ahorra €500-2,000 al Mes')}</span>
<span className="block text-[var(--color-primary)] mt-2">{t('landing:hero.title_option_a_line2', 'Produciendo Exactamente Lo Que Venderás')}</span>
</h1>
<p className="mt-6 max-w-3xl mx-auto text-lg text-[var(--text-secondary)] sm:text-xl">
{t('landing:hero.subtitle', 'IA que predice demanda con datos de tu zona para que produzcas exactamente lo que vas a vender. Reduce desperdicios, mejora márgenes y ahorra tiempo.')}
<p className="mt-8 max-w-3xl mx-auto text-lg text-[var(--text-secondary)] sm:text-xl leading-relaxed">
{t('landing:hero.subtitle_option_a', 'La primera IA que conoce tu barrio: colegios cerca, clima local, tu competencia, eventos. Sistema automático cada mañana. Listo a las 6 AM.')}
</p>
{/* CTA Buttons */}
@@ -75,24 +92,48 @@ const LandingPage: React.FC = () => {
</Link>
</div>
{/* Social Proof - New */}
<div className="mt-12 max-w-3xl mx-auto">
<div className="grid md:grid-cols-3 gap-6 text-left">
<div className="flex items-start gap-3 bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm p-4 rounded-xl shadow-sm border border-[var(--border-primary)]">
<CheckCircle2 className="w-5 h-5 text-green-600 dark:text-green-400 mt-0.5 flex-shrink-0" />
<span className="text-sm font-medium text-[var(--text-secondary)]">
<AnimatedCounter value={20} className="inline font-bold" /> panaderías ya ahorran <AnimatedCounter value={1500} prefix="€" className="inline font-bold" />/mes de promedio
</span>
</div>
<div className="flex items-start gap-3 bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm p-4 rounded-xl shadow-sm border border-[var(--border-primary)]">
<Target className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
<span className="text-sm font-medium text-[var(--text-secondary)]">
Predicciones <AnimatedCounter value={92} suffix="%" className="inline font-bold" /> precisas (vs 60% sistemas genéricos)
</span>
</div>
<div className="flex items-start gap-3 bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm p-4 rounded-xl shadow-sm border border-[var(--border-primary)]">
<Clock className="w-5 h-5 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0" />
<span className="text-sm font-medium text-[var(--text-secondary)]">
{t('landing:hero.social_proof.setup', 'Configuración en 15 minutos')}
</span>
</div>
</div>
</div>
{/* Trust Badges */}
<div className="mt-12 flex flex-wrap items-center justify-center gap-x-8 gap-y-4 text-sm">
<div className="flex items-center bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm px-4 py-2 rounded-full shadow-sm border border-[var(--border-primary)]">
<CheckCircle2 className="w-4 h-4 text-amber-600 dark:text-amber-400 mr-2" />
<span className="font-medium text-[var(--text-secondary)]">
{t('landing:hero.trust.card', 'Tarjeta requerida')}
<div className="mt-8 flex flex-wrap items-center justify-center gap-x-8 gap-y-4 text-sm">
<div className="flex items-center">
<CheckCircle2 className="w-4 h-4 text-green-600 dark:text-green-400 mr-2" />
<span className="font-medium text-[var(--text-tertiary)]">
{t('landing:hero.trust.no_cc', '3 meses gratis')}
</span>
</div>
<div className="flex items-center bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm px-4 py-2 rounded-full shadow-sm border border-[var(--border-primary)]">
<div className="flex items-center">
<Clock className="w-4 h-4 text-blue-600 dark:text-blue-400 mr-2" />
<span className="font-medium text-[var(--text-secondary)]">
{t('landing:hero.trust.quick', '3 meses gratis')}
<span className="font-medium text-[var(--text-tertiary)]">
{t('landing:hero.trust.quick', 'Configuración en 15 min')}
</span>
</div>
<div className="flex items-center bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm px-4 py-2 rounded-full shadow-sm border border-[var(--border-primary)]">
<Zap className="w-4 h-4 text-green-600 dark:text-green-400 mr-2" />
<span className="font-medium text-[var(--text-secondary)]">
{t('landing:hero.trust.setup', 'Configuración en 15 min')}
<div className="flex items-center">
<Shield className="w-4 h-4 text-purple-600 dark:text-purple-400 mr-2" />
<span className="font-medium text-[var(--text-tertiary)]">
{t('landing:hero.trust.card', 'Tarjeta requerida')}
</span>
</div>
</div>
@@ -105,10 +146,11 @@ const LandingPage: React.FC = () => {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid md:grid-cols-2 gap-12">
{/* Problems */}
<div>
<h2 className="text-3xl font-bold text-[var(--text-primary)] mb-8">
{t('landing:problems.title', '❌ Los Problemas Que Enfrentas')}
</h2>
<ScrollReveal variant="fadeRight" delay={0.1}>
<div>
<h2 className="text-3xl font-bold text-[var(--text-primary)] mb-8">
{t('landing:problems.title', '❌ Los Problemas Que Enfrentas')}
</h2>
<div className="space-y-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 bg-red-100 dark:bg-red-900/20 rounded-lg flex items-center justify-center flex-shrink-0">
@@ -150,13 +192,15 @@ const LandingPage: React.FC = () => {
</div>
</div>
</div>
</div>
</div>
</ScrollReveal>
{/* Solutions */}
<div>
<h2 className="text-3xl font-bold text-[var(--text-primary)] mb-8">
{t('landing:solutions.title', '✅ La Solución Con IA')}
</h2>
<ScrollReveal variant="fadeLeft" delay={0.2}>
<div>
<h2 className="text-3xl font-bold text-[var(--text-primary)] mb-8">
{t('landing:solutions.title', '✅ La Solución Con IA')}
</h2>
<div className="space-y-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 bg-green-100 dark:bg-green-900/20 rounded-lg flex items-center justify-center flex-shrink-0">
@@ -198,7 +242,8 @@ const LandingPage: React.FC = () => {
</div>
</div>
</div>
</div>
</div>
</ScrollReveal>
</div>
</div>
</section>
@@ -279,7 +324,7 @@ const LandingPage: React.FC = () => {
<div className="mt-6 bg-gradient-to-r from-[var(--color-primary)]/10 to-orange-500/10 rounded-lg p-4 border-l-4 border-[var(--color-primary)]">
<p className="font-bold text-[var(--text-primary)]">
{t('landing:pillar1.accuracy', '🎯 Precisión: 92% (vs 60-70% de sistemas genéricos)')}
🎯 Precisión: <AnimatedCounter value={92} suffix="%" className="inline text-[var(--color-primary)]" /> (vs 60-70% de sistemas genéricos)
</p>
</div>
</div>