Improve public pages
This commit is contained in:
@@ -73,7 +73,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
image: rabbitmq:3.12-management-alpine
|
image: rabbitmq:4.0-management-alpine
|
||||||
container_name: bakery-rabbitmq
|
container_name: bakery-rabbitmq
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# ONLY use environment substitution from .env
|
# ONLY use environment substitution from .env
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export interface UserRegistration {
|
|||||||
subscription_plan?: string | null; // Default: "starter", options: starter, professional, enterprise
|
subscription_plan?: string | null; // Default: "starter", options: starter, professional, enterprise
|
||||||
use_trial?: boolean | null; // Default: false - Whether to use trial period
|
use_trial?: boolean | null; // Default: false - Whether to use trial period
|
||||||
payment_method_id?: string | null; // Stripe payment method ID
|
payment_method_id?: string | null; // Stripe payment method ID
|
||||||
|
coupon_code?: string | null; // Promotional coupon code for discounts/trial extensions
|
||||||
// GDPR Consent fields
|
// GDPR Consent fields
|
||||||
terms_accepted?: boolean; // Default: true - Accept terms of service
|
terms_accepted?: boolean; // Default: true - Accept terms of service
|
||||||
privacy_accepted?: boolean; // Default: true - Accept privacy policy
|
privacy_accepted?: boolean; // Default: true - Accept privacy policy
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Button, Card, CardBody } from '../ui';
|
|
||||||
import { PublicLayout } from '../layout';
|
|
||||||
import { AlertCircle, RefreshCw, Home, HelpCircle } from 'lucide-react';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
error: string;
|
|
||||||
details?: Array<{ service: string; error_message: string }>;
|
|
||||||
onRetry: () => void;
|
|
||||||
isRetrying?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DemoErrorScreen: React.FC<Props> = ({
|
|
||||||
error,
|
|
||||||
details,
|
|
||||||
onRetry,
|
|
||||||
isRetrying = false,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<PublicLayout
|
|
||||||
variant="centered"
|
|
||||||
headerProps={{
|
|
||||||
showThemeToggle: true,
|
|
||||||
showAuthButtons: false,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="max-w-2xl mx-auto p-8">
|
|
||||||
<Card className="shadow-xl">
|
|
||||||
<CardBody className="p-8 text-center">
|
|
||||||
<div className="flex justify-center mb-4">
|
|
||||||
<AlertCircle className="w-20 h-20 text-[var(--color-error)]" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 className="text-3xl font-bold text-[var(--color-error)] mb-3">
|
|
||||||
Error en la Configuración del Demo
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p className="text-[var(--text-secondary)] mb-6 text-lg">
|
|
||||||
{error}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{details && details.length > 0 && (
|
|
||||||
<div className="mb-6 p-4 bg-[var(--color-error)]/10 border border-[var(--color-error)] rounded-lg text-left">
|
|
||||||
<h3 className="text-sm font-semibold text-[var(--text-primary)] mb-3">
|
|
||||||
Detalles del error:
|
|
||||||
</h3>
|
|
||||||
<ul className="space-y-2">
|
|
||||||
{details.map((detail, idx) => (
|
|
||||||
<li key={idx} className="text-sm">
|
|
||||||
<span className="font-medium text-[var(--text-primary)]">
|
|
||||||
{detail.service}:
|
|
||||||
</span>{' '}
|
|
||||||
<span className="text-[var(--text-secondary)]">
|
|
||||||
{detail.error_message}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 max-w-md mx-auto mt-6">
|
|
||||||
<Button
|
|
||||||
onClick={onRetry}
|
|
||||||
disabled={isRetrying}
|
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<RefreshCw className={`w-5 h-5 mr-2 ${isRetrying ? 'animate-spin' : ''}`} />
|
|
||||||
{isRetrying ? 'Reintentando...' : 'Reintentar Configuración'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => window.location.href = '/demo'}
|
|
||||||
variant="secondary"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<Home className="w-5 h-5 mr-2" />
|
|
||||||
Volver a la Página Demo
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => window.location.href = '/contact'}
|
|
||||||
variant="ghost"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<HelpCircle className="w-5 h-5 mr-2" />
|
|
||||||
Contactar Soporte
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6">
|
|
||||||
<p className="text-xs text-[var(--text-tertiary)]">
|
|
||||||
Si el problema persiste, por favor contacta a nuestro equipo de soporte.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</PublicLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Badge, ProgressBar } from '../ui';
|
|
||||||
import { CheckCircle, XCircle, Loader2, Clock } from 'lucide-react';
|
|
||||||
import { ServiceProgress } from '@/api/services/demo';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
progress: Record<string, ServiceProgress>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SERVICE_LABELS: Record<string, string> = {
|
|
||||||
tenant: 'Tenant Virtual',
|
|
||||||
inventory: 'Inventario y Productos',
|
|
||||||
recipes: 'Recetas',
|
|
||||||
sales: 'Historial de Ventas',
|
|
||||||
orders: 'Pedidos de Clientes',
|
|
||||||
suppliers: 'Proveedores',
|
|
||||||
production: 'Producción',
|
|
||||||
forecasting: 'Pronósticos',
|
|
||||||
};
|
|
||||||
|
|
||||||
const SERVICE_DESCRIPTIONS: Record<string, string> = {
|
|
||||||
tenant: 'Creando tu entorno demo aislado',
|
|
||||||
inventory: 'Cargando ingredientes, recetas y datos de stock',
|
|
||||||
recipes: 'Configurando recetas y fórmulas',
|
|
||||||
sales: 'Importando registros de ventas históricas',
|
|
||||||
orders: 'Configurando pedidos de clientes',
|
|
||||||
suppliers: 'Importando datos de proveedores',
|
|
||||||
production: 'Configurando lotes de producción',
|
|
||||||
forecasting: 'Preparando datos de pronósticos',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DemoProgressIndicator: React.FC<Props> = ({ progress }) => {
|
|
||||||
return (
|
|
||||||
<div className="mt-4 space-y-3">
|
|
||||||
{Object.entries(progress).map(([serviceName, serviceProgress]) => (
|
|
||||||
<div
|
|
||||||
key={serviceName}
|
|
||||||
className={`
|
|
||||||
p-4 rounded-lg border-2 transition-all
|
|
||||||
${
|
|
||||||
serviceProgress.status === 'completed'
|
|
||||||
? 'bg-green-50 dark:bg-green-900/20 border-green-500'
|
|
||||||
: serviceProgress.status === 'failed'
|
|
||||||
? 'bg-red-50 dark:bg-red-900/20 border-red-500'
|
|
||||||
: serviceProgress.status === 'in_progress'
|
|
||||||
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-500'
|
|
||||||
: 'bg-gray-50 dark:bg-gray-800/20 border-gray-300 dark:border-gray-700'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-3 flex-1">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
{getStatusIcon(serviceProgress.status)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3 className="text-sm font-semibold text-[var(--text-primary)] truncate">
|
|
||||||
{SERVICE_LABELS[serviceName] || serviceName}
|
|
||||||
</h3>
|
|
||||||
<p className="text-xs text-[var(--text-secondary)] truncate">
|
|
||||||
{SERVICE_DESCRIPTIONS[serviceName] || 'Procesando...'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Badge variant={getStatusVariant(serviceProgress.status)}>
|
|
||||||
{getStatusLabel(serviceProgress.status)}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{serviceProgress.status === 'in_progress' && (
|
|
||||||
<div className="my-2">
|
|
||||||
<ProgressBar value={50} variant="primary" className="animate-pulse" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{serviceProgress.records_cloned > 0 && (
|
|
||||||
<p className="text-xs text-[var(--text-secondary)] mt-2">
|
|
||||||
✓ {serviceProgress.records_cloned} registros clonados
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{serviceProgress.error && (
|
|
||||||
<p className="text-xs text-[var(--color-error)] mt-2">
|
|
||||||
Error: {serviceProgress.error}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function getStatusIcon(status: ServiceProgress['status']): React.ReactNode {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed':
|
|
||||||
return <CheckCircle className="w-5 h-5 text-green-500" />;
|
|
||||||
case 'failed':
|
|
||||||
return <XCircle className="w-5 h-5 text-red-500" />;
|
|
||||||
case 'in_progress':
|
|
||||||
return <Loader2 className="w-5 h-5 text-blue-500 animate-spin" />;
|
|
||||||
default:
|
|
||||||
return <Clock className="w-5 h-5 text-gray-400" />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusLabel(status: ServiceProgress['status']): string {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed':
|
|
||||||
return 'Completado';
|
|
||||||
case 'failed':
|
|
||||||
return 'Fallido';
|
|
||||||
case 'in_progress':
|
|
||||||
return 'En Progreso';
|
|
||||||
default:
|
|
||||||
return 'Pendiente';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusVariant(
|
|
||||||
status: ServiceProgress['status']
|
|
||||||
): 'success' | 'error' | 'info' | 'default' {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed':
|
|
||||||
return 'success';
|
|
||||||
case 'failed':
|
|
||||||
return 'error';
|
|
||||||
case 'in_progress':
|
|
||||||
return 'info';
|
|
||||||
default:
|
|
||||||
return 'default';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
76
frontend/src/components/domain/auth/PilotBanner.tsx
Normal file
76
frontend/src/components/domain/auth/PilotBanner.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* Pilot Banner Component
|
||||||
|
* Displays when pilot=true URL parameter is detected
|
||||||
|
* Shows 3-month free trial information and PILOT2025 coupon code
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import { Star, Check, Award } from 'lucide-react';
|
||||||
|
import { Card } from '../../ui';
|
||||||
|
|
||||||
|
interface PilotBannerProps {
|
||||||
|
couponCode: string;
|
||||||
|
trialMonths: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PilotBanner: React.FC<PilotBannerProps> = ({
|
||||||
|
couponCode,
|
||||||
|
trialMonths,
|
||||||
|
className = ''
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Card className={`p-6 border-2 border-amber-500 dark:border-amber-600 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 ${className}`}>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
{/* Icon */}
|
||||||
|
<div className="flex-shrink-0 w-12 h-12 rounded-full bg-gradient-to-br from-amber-500 to-orange-600 flex items-center justify-center shadow-lg">
|
||||||
|
<Award className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Star className="w-5 h-5 text-amber-600 dark:text-amber-400 fill-amber-600 dark:fill-amber-400" />
|
||||||
|
<h3 className="text-lg font-bold text-amber-900 dark:text-amber-100">
|
||||||
|
¡Programa Piloto Activado!
|
||||||
|
</h3>
|
||||||
|
<Star className="w-5 h-5 text-amber-600 dark:text-amber-400 fill-amber-600 dark:fill-amber-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-amber-800 dark:text-amber-200 mb-3">
|
||||||
|
Has sido seleccionado para nuestro programa piloto exclusivo.
|
||||||
|
Disfruta de <strong>{trialMonths} meses completamente gratis</strong> como uno de nuestros primeros 20 clientes.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<div className="flex items-center gap-2 bg-white/60 dark:bg-gray-800/60 px-3 py-1.5 rounded-lg border border-amber-300 dark:border-amber-700">
|
||||||
|
<Check className="w-4 h-4 text-green-600 dark:text-green-400" />
|
||||||
|
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||||
|
Cupón: <span className="font-mono font-bold text-amber-700 dark:text-amber-300">{couponCode}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 bg-white/60 dark:bg-gray-800/60 px-3 py-1.5 rounded-lg border border-amber-300 dark:border-amber-700">
|
||||||
|
<Check className="w-4 h-4 text-green-600 dark:text-green-400" />
|
||||||
|
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||||
|
{trialMonths} meses gratis
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 bg-white/60 dark:bg-gray-800/60 px-3 py-1.5 rounded-lg border border-amber-300 dark:border-amber-700">
|
||||||
|
<Check className="w-4 h-4 text-green-600 dark:text-green-400" />
|
||||||
|
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||||
|
40% descuento de por vida
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-amber-700 dark:text-amber-300 mt-3 italic">
|
||||||
|
El cupón se aplicará automáticamente durante el registro. Tarjeta requerida para validación, sin cargo inmediato.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PilotBanner;
|
||||||
@@ -6,9 +6,11 @@ import { useAuthActions, useAuthLoading, useAuthError } from '../../../stores/au
|
|||||||
import { useToast } from '../../../hooks/ui/useToast';
|
import { useToast } from '../../../hooks/ui/useToast';
|
||||||
import { SubscriptionSelection } from './SubscriptionSelection';
|
import { SubscriptionSelection } from './SubscriptionSelection';
|
||||||
import PaymentForm from './PaymentForm';
|
import PaymentForm from './PaymentForm';
|
||||||
|
import PilotBanner from './PilotBanner';
|
||||||
import { loadStripe } from '@stripe/stripe-js';
|
import { loadStripe } from '@stripe/stripe-js';
|
||||||
import { Elements } from '@stripe/react-stripe-js';
|
import { Elements } from '@stripe/react-stripe-js';
|
||||||
import { CheckCircle } from 'lucide-react';
|
import { CheckCircle } from 'lucide-react';
|
||||||
|
import { usePilotDetection } from '../../../hooks/usePilotDetection';
|
||||||
|
|
||||||
// Initialize Stripe - In production, use environment variable
|
// Initialize Stripe - In production, use environment variable
|
||||||
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || 'pk_test_51234567890123456789012345678901234567890123456789012345678901234567890123456789012345');
|
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || 'pk_test_51234567890123456789012345678901234567890123456789012345678901234567890123456789012345');
|
||||||
@@ -57,10 +59,13 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|||||||
const error = useAuthError();
|
const error = useAuthError();
|
||||||
const { success: showSuccessToast, error: showErrorToast } = useToast();
|
const { success: showSuccessToast, error: showErrorToast } = useToast();
|
||||||
|
|
||||||
|
// Detect pilot program participation
|
||||||
|
const { isPilot, couponCode, trialMonths } = usePilotDetection();
|
||||||
|
|
||||||
// Multi-step form state
|
// Multi-step form state
|
||||||
const [currentStep, setCurrentStep] = useState<RegistrationStep>('basic_info');
|
const [currentStep, setCurrentStep] = useState<RegistrationStep>('basic_info');
|
||||||
const [selectedPlan, setSelectedPlan] = useState<string>('starter');
|
const [selectedPlan, setSelectedPlan] = useState<string>('starter');
|
||||||
const [useTrial, setUseTrial] = useState<boolean>(false);
|
const [useTrial, setUseTrial] = useState<boolean>(isPilot); // Auto-enable trial for pilot customers
|
||||||
const [bypassPayment, setBypassPayment] = useState<boolean>(false);
|
const [bypassPayment, setBypassPayment] = useState<boolean>(false);
|
||||||
|
|
||||||
// Helper function to determine password match status
|
// Helper function to determine password match status
|
||||||
@@ -139,6 +144,8 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|||||||
subscription_plan: selectedPlan,
|
subscription_plan: selectedPlan,
|
||||||
use_trial: useTrial,
|
use_trial: useTrial,
|
||||||
payment_method_id: paymentMethodId,
|
payment_method_id: paymentMethodId,
|
||||||
|
// Include coupon code if pilot customer
|
||||||
|
coupon_code: isPilot ? couponCode : undefined,
|
||||||
// Include consent data
|
// Include consent data
|
||||||
terms_accepted: formData.acceptTerms,
|
terms_accepted: formData.acceptTerms,
|
||||||
privacy_accepted: formData.acceptTerms,
|
privacy_accepted: formData.acceptTerms,
|
||||||
@@ -148,7 +155,11 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|||||||
|
|
||||||
await register(registrationData);
|
await register(registrationData);
|
||||||
|
|
||||||
showSuccessToast(t('auth:register.registering', '¡Bienvenido! Tu cuenta ha sido creada correctamente.'), {
|
const successMessage = isPilot
|
||||||
|
? '¡Bienvenido al programa piloto! Tu cuenta ha sido creada con 3 meses gratis.'
|
||||||
|
: '¡Bienvenido! Tu cuenta ha sido creada correctamente.';
|
||||||
|
|
||||||
|
showSuccessToast(t('auth:register.registering', successMessage), {
|
||||||
title: t('auth:alerts.success_create', 'Cuenta creada exitosamente')
|
title: t('auth:alerts.success_create', 'Cuenta creada exitosamente')
|
||||||
});
|
});
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
@@ -157,7 +168,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|||||||
title: t('auth:alerts.error_create', 'Error al crear la cuenta')
|
title: t('auth:alerts.error_create', 'Error al crear la cuenta')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePaymentSuccess = () => {
|
const handlePaymentSuccess = () => {
|
||||||
handleRegistrationSubmit(); // In a real app, you would pass the payment method ID
|
handleRegistrationSubmit(); // In a real app, you would pass the payment method ID
|
||||||
@@ -486,10 +497,14 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isPilot && couponCode && (
|
||||||
|
<PilotBanner couponCode={couponCode} trialMonths={trialMonths} />
|
||||||
|
)}
|
||||||
|
|
||||||
<SubscriptionSelection
|
<SubscriptionSelection
|
||||||
selectedPlan={selectedPlan}
|
selectedPlan={selectedPlan}
|
||||||
onPlanSelect={setSelectedPlan}
|
onPlanSelect={setSelectedPlan}
|
||||||
showTrialOption={true}
|
showTrialOption={!isPilot}
|
||||||
onTrialSelect={setUseTrial}
|
onTrialSelect={setUseTrial}
|
||||||
trialSelected={useTrial}
|
trialSelected={useTrial}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -143,8 +143,8 @@ export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> = ({
|
|||||||
return translations[feature] || feature.replace(/_/g, ' ');
|
return translations[feature] || feature.replace(/_/g, ' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get trial days from the selected plan (default to 14 if not available)
|
// Get trial days from the selected plan (default to 90 for pilot customers)
|
||||||
const trialDays = availablePlans.plans[selectedPlan]?.trial_days || 14;
|
const trialDays = 90; // 3 months for pilot customers
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`space-y-4 ${className}`}>
|
<div className={`space-y-4 ${className}`}>
|
||||||
@@ -160,7 +160,7 @@ export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> = ({
|
|||||||
{t('auth:subscription.trial_title', 'Prueba gratuita')}
|
{t('auth:subscription.trial_title', 'Prueba gratuita')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-text-secondary">
|
<p className="text-sm text-text-secondary">
|
||||||
{t('auth:subscription.trial_description', `Obtén ${trialDays} días de prueba gratuita - sin tarjeta de crédito requerida`)}
|
{t('auth:subscription.trial_description', `Obtén 3 meses de prueba gratuita - tarjeta requerida para validación`)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,7 +218,7 @@ export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> = ({
|
|||||||
{metadata.trial_days > 0 && (
|
{metadata.trial_days > 0 && (
|
||||||
<Badge variant="success" className="text-xs px-2 py-0.5">
|
<Badge variant="success" className="text-xs px-2 py-0.5">
|
||||||
<Zap className="w-3 h-3 mr-1" />
|
<Zap className="w-3 h-3 mr-1" />
|
||||||
{metadata.trial_days} días gratis
|
3 meses gratis
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -194,10 +194,9 @@ export const Footer = forwardRef<FooterRef, FooterProps>(({
|
|||||||
id: 'company',
|
id: 'company',
|
||||||
title: t('common:footer.sections.company', 'Empresa'),
|
title: t('common:footer.sections.company', 'Empresa'),
|
||||||
links: [
|
links: [
|
||||||
{ id: 'about', label: t('common:footer.links.about', 'Acerca de'), href: '/about', external: true },
|
{ id: 'about', label: t('common:footer.links.about', 'Acerca de'), href: '/about' },
|
||||||
{ id: 'blog', label: t('common:footer.links.blog', 'Blog'), href: 'https://blog.panaderia-ia.com', external: true },
|
{ id: 'blog', label: t('common:footer.links.blog', 'Blog'), href: '/blog' },
|
||||||
{ id: 'careers', label: t('common:footer.links.careers', 'Carreras'), href: 'https://careers.panaderia-ia.com', external: true },
|
{ id: 'careers', label: t('common:footer.links.careers', 'Carreras'), href: '/careers' },
|
||||||
{ id: 'press', label: t('common:footer.links.press', 'Prensa'), href: '/press', external: true },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, ThemeToggle } from '../../ui';
|
import { Button, ThemeToggle } from '../../ui';
|
||||||
import { CompactLanguageSelector } from '../../ui/LanguageSelector';
|
import { CompactLanguageSelector } from '../../ui/LanguageSelector';
|
||||||
|
|
||||||
@@ -62,6 +63,7 @@ export const PublicHeader = forwardRef<PublicHeaderRef, PublicHeaderProps>(({
|
|||||||
navigationItems = [],
|
navigationItems = [],
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const headerRef = React.useRef<HTMLDivElement>(null);
|
const headerRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Default navigation items
|
// Default navigation items
|
||||||
@@ -180,7 +182,7 @@ export const PublicHeader = forwardRef<PublicHeaderRef, PublicHeaderProps>(({
|
|||||||
size="md"
|
size="md"
|
||||||
className="hidden sm:inline-flex font-medium hover:bg-[var(--bg-secondary)] transition-all duration-200"
|
className="hidden sm:inline-flex font-medium hover:bg-[var(--bg-secondary)] transition-all duration-200"
|
||||||
>
|
>
|
||||||
Iniciar Sesión
|
{t('common:header.login')}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/register">
|
<Link to="/register">
|
||||||
@@ -188,8 +190,8 @@ export const PublicHeader = forwardRef<PublicHeaderRef, PublicHeaderProps>(({
|
|||||||
size="md"
|
size="md"
|
||||||
className="bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] hover:opacity-90 text-white font-semibold shadow-lg hover:shadow-xl transition-all duration-200 px-6"
|
className="bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] hover:opacity-90 text-white font-semibold shadow-lg hover:shadow-xl transition-all duration-200 px-6"
|
||||||
>
|
>
|
||||||
<span className="hidden sm:inline">Comenzar Gratis</span>
|
<span className="hidden sm:inline">{t('common:header.start_free')}</span>
|
||||||
<span className="sm:hidden">Registro</span>
|
<span className="sm:hidden">{t('common:header.register')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,7 +212,7 @@ export const PublicHeader = forwardRef<PublicHeaderRef, PublicHeaderProps>(({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="p-2"
|
className="p-2"
|
||||||
aria-label="Abrir menú de navegación"
|
aria-label={t('common:header.open_menu')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// TODO: Implement mobile menu
|
// TODO: Implement mobile menu
|
||||||
console.log('Mobile menu toggle');
|
console.log('Mobile menu toggle');
|
||||||
@@ -237,7 +239,7 @@ export const PublicHeader = forwardRef<PublicHeaderRef, PublicHeaderProps>(({
|
|||||||
{showLanguageSelector && (
|
{showLanguageSelector && (
|
||||||
<div className="py-2 border-b border-[var(--border-primary)] sm:hidden">
|
<div className="py-2 border-b border-[var(--border-primary)] sm:hidden">
|
||||||
<div className="text-sm font-medium text-[var(--text-secondary)] mb-2">
|
<div className="text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
Idioma
|
{t('common:header.language')}
|
||||||
</div>
|
</div>
|
||||||
<CompactLanguageSelector className="w-full" />
|
<CompactLanguageSelector className="w-full" />
|
||||||
</div>
|
</div>
|
||||||
@@ -252,7 +254,7 @@ export const PublicHeader = forwardRef<PublicHeaderRef, PublicHeaderProps>(({
|
|||||||
size="md"
|
size="md"
|
||||||
className="w-full font-medium border border-[var(--border-primary)] hover:bg-[var(--bg-secondary)]"
|
className="w-full font-medium border border-[var(--border-primary)] hover:bg-[var(--bg-secondary)]"
|
||||||
>
|
>
|
||||||
Iniciar Sesión
|
{t('common:header.login')}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/register">
|
<Link to="/register">
|
||||||
@@ -260,7 +262,7 @@ export const PublicHeader = forwardRef<PublicHeaderRef, PublicHeaderProps>(({
|
|||||||
size="md"
|
size="md"
|
||||||
className="w-full bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] hover:opacity-90 text-white font-semibold shadow-lg"
|
className="w-full bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] hover:opacity-90 text-white font-semibold shadow-lg"
|
||||||
>
|
>
|
||||||
Comenzar Gratis
|
{t('common:header.start_free')}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ export const PricingSection: React.FC = () => {
|
|||||||
<div className={`mt-2 px-3 py-1 text-sm font-medium rounded-full inline-block ${
|
<div className={`mt-2 px-3 py-1 text-sm font-medium rounded-full inline-block ${
|
||||||
isPopular ? 'bg-white/20 text-white' : 'bg-[var(--color-success)]/10 text-[var(--color-success)]'
|
isPopular ? 'bg-white/20 text-white' : 'bg-[var(--color-success)]/10 text-[var(--color-success)]'
|
||||||
}`}>
|
}`}>
|
||||||
{plan.trial_days} días gratis
|
3 meses gratis
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -346,7 +346,7 @@ export const PricingSection: React.FC = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<p className={`text-xs text-center mt-3 ${isPopular ? 'text-white/70' : 'text-[var(--text-secondary)]'}`}>
|
<p className={`text-xs text-center mt-3 ${isPopular ? 'text-white/70' : 'text-[var(--text-secondary)]'}`}>
|
||||||
{plan.trial_days} días gratis • Sin tarjeta requerida
|
3 meses gratis • Tarjeta requerida para validación
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
32
frontend/src/hooks/usePilotDetection.ts
Normal file
32
frontend/src/hooks/usePilotDetection.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Custom hook to detect pilot program participation via URL parameter
|
||||||
|
* Checks for ?pilot=true in URL and provides pilot status and coupon code
|
||||||
|
*/
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface PilotDetectionResult {
|
||||||
|
isPilot: boolean;
|
||||||
|
couponCode: string | null;
|
||||||
|
trialMonths: number;
|
||||||
|
trialDays: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePilotDetection = (): PilotDetectionResult => {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const pilotInfo = useMemo(() => {
|
||||||
|
const searchParams = new URLSearchParams(location.search);
|
||||||
|
const pilotParam = searchParams.get('pilot');
|
||||||
|
const isPilot = pilotParam === 'true';
|
||||||
|
|
||||||
|
return {
|
||||||
|
isPilot,
|
||||||
|
couponCode: isPilot ? 'PILOT2025' : null,
|
||||||
|
trialMonths: isPilot ? 3 : 0,
|
||||||
|
trialDays: isPilot ? 90 : 14,
|
||||||
|
};
|
||||||
|
}, [location.search]);
|
||||||
|
|
||||||
|
return pilotInfo;
|
||||||
|
};
|
||||||
@@ -335,5 +335,25 @@
|
|||||||
"positive_number": "Must be a positive number",
|
"positive_number": "Must be a positive number",
|
||||||
"negative_not_allowed": "Negative numbers are not allowed"
|
"negative_not_allowed": "Negative numbers are not allowed"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cookie": {
|
||||||
|
"banner_title": "We Value Your Privacy",
|
||||||
|
"minimized_message": "We use cookies to enhance your experience.",
|
||||||
|
"show_details": "Show Details",
|
||||||
|
"banner_description": "We use cookies and similar technologies to provide, protect, and improve our services. Some cookies are essential for the site to function, while others help us understand how you use our services and provide personalized features.",
|
||||||
|
"banner_description_2": "By clicking \"Accept All\", you consent to our use of all cookies. You can customize your preferences or accept only essential cookies.",
|
||||||
|
"learn_more": "Learn more about cookies",
|
||||||
|
"privacy_policy": "Privacy Policy",
|
||||||
|
"minimize": "Minimize",
|
||||||
|
"accept_all": "Accept All",
|
||||||
|
"accept_essential": "Essential Only",
|
||||||
|
"customize": "Customize"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"login": "Login",
|
||||||
|
"start_free": "Start Free",
|
||||||
|
"register": "Sign Up",
|
||||||
|
"language": "Language",
|
||||||
|
"open_menu": "Open navigation menu"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,125 +3,246 @@
|
|||||||
"features": "Features",
|
"features": "Features",
|
||||||
"benefits": "Benefits",
|
"benefits": "Benefits",
|
||||||
"pricing": "Pricing",
|
"pricing": "Pricing",
|
||||||
"testimonials": "Testimonials"
|
"faq": "FAQ"
|
||||||
},
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"badge": "Advanced AI for Bakeries",
|
"badge": "Advanced AI for Bakeries",
|
||||||
"title_line1": "Revolutionize your",
|
"badge_sustainability": "Food Waste Reduction",
|
||||||
"title_line2": "Bakery with AI",
|
"title_line1": "Increase Profits,",
|
||||||
"subtitle": "Automatically optimize your production, reduce waste by up to 35%, predict demand with 92% accuracy and increase your sales with artificial intelligence.",
|
"title_line2": "Reduce Waste",
|
||||||
"cta_primary": "Start Free 14-Day Trial",
|
"subtitle": "AI platform for bakeries that want to produce exactly what they'll sell. Reduce food waste, improve margins, and save time on planning. 100% Spanish, your data is yours.",
|
||||||
"cta_secondary": "Watch Live Demo",
|
"pilot_banner": {
|
||||||
|
"title": "Pilot Launch!",
|
||||||
|
"offer": "3 MONTHS FREE",
|
||||||
|
"description": "for early pilot participants"
|
||||||
|
},
|
||||||
|
"cta_primary": "Start Free Now",
|
||||||
|
"cta_secondary": "Watch Demo",
|
||||||
"features": {
|
"features": {
|
||||||
"no_credit_card": "No credit card required",
|
"card_required": "Card required • 3 months free",
|
||||||
"quick_setup": "5-minute setup",
|
"no_credit_card": "Card required • 3 months free",
|
||||||
"support_24_7": "24/7 support in English"
|
"quick_setup": "Ready in minutes, not hours",
|
||||||
|
"support_24_7": "Personalized support in English"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pilot": {
|
||||||
|
"banner": {
|
||||||
|
"active": "Pilot Program Activated",
|
||||||
|
"trial_applied": "3 months free automatically applied",
|
||||||
|
"coupon_code": "Code",
|
||||||
|
"description": "You've been selected for our exclusive pilot program"
|
||||||
|
},
|
||||||
|
"badge": "Pilot Program - Limited Spots",
|
||||||
|
"title": "Looking for 20 Pioneer Bakeries",
|
||||||
|
"subtitle": "We're selecting the first 20 bakeries to join our exclusive pilot program. In exchange for your feedback, you get <strong>3 months free + lifetime preferential pricing</strong>.",
|
||||||
|
"benefits": {
|
||||||
|
"founders_beta": {
|
||||||
|
"title": "Founders Beta",
|
||||||
|
"description": "Lifetime access with 40% discount"
|
||||||
|
},
|
||||||
|
"influence_product": {
|
||||||
|
"title": "Influence the Product",
|
||||||
|
"description": "Your needs shape the platform"
|
||||||
|
},
|
||||||
|
"premium_support": {
|
||||||
|
"title": "Premium Support",
|
||||||
|
"description": "Direct attention from founding team"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"features": {
|
"features": {
|
||||||
"title": "Everything you need to optimize your bakery",
|
"title_main": "Fight Food Waste",
|
||||||
"subtitle": "Powerful tools designed specifically for modern bakeries",
|
"title_accent": "with Artificial Intelligence",
|
||||||
"ai_forecasting": {
|
"badge": "Cutting-Edge AI Technology",
|
||||||
"title": "AI Demand Forecasting",
|
"subtitle": "High-tech system that uses advanced AI algorithms to optimize your production, reduce food waste, and keep your data 100% secure and under your control.",
|
||||||
"description": "Advanced algorithms predict which products you'll need each day with 92% accuracy"
|
"ai_prediction": {
|
||||||
},
|
"title": "Advanced Prediction AI",
|
||||||
"production_optimization": {
|
"description": "State-of-the-art machine learning algorithms analyze historical patterns, weather, events, and trends to predict demand with surgical precision.",
|
||||||
"title": "Production Optimization",
|
"features": {
|
||||||
"description": "Automatically plan baking schedules and staff management for maximum efficiency"
|
"accuracy": "92% accuracy in predictions",
|
||||||
|
"learning": "Continuous and adaptive learning",
|
||||||
|
"realtime": "Real-time predictive analytics"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"waste_reduction": {
|
"waste_reduction": {
|
||||||
"title": "Waste Reduction",
|
"title": "Waste Reduction",
|
||||||
"description": "Reduce waste by up to 35% with precise predictions and intelligent inventory management"
|
"description": "Contribute to the environment and reduce costs by eliminating up to 35% of food waste through optimized and intelligent production.",
|
||||||
|
"features": {
|
||||||
|
"reduction": "Up to 35% less waste",
|
||||||
|
"savings": "Average savings of €800/month",
|
||||||
|
"eligible": "Eligible for EU grants"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"real_time_analytics": {
|
"data_ownership": {
|
||||||
"title": "Real-Time Analytics",
|
"title": "Your Data, Your Property",
|
||||||
"description": "Intuitive dashboard with sales, production and profitability metrics updated instantly"
|
"description": "Total privacy and security. Your operational data, suppliers, and analytics remain 100% under your control. Never shared, never sold.",
|
||||||
|
"features": {
|
||||||
|
"ownership": "100% data ownership",
|
||||||
|
"privacy": "Full privacy control",
|
||||||
|
"gdpr": "Guaranteed GDPR compliance"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"inventory_management": {
|
"smart_inventory": {
|
||||||
"title": "Inventory Management",
|
"title": "Smart Inventory",
|
||||||
"description": "Automatic stock control with smart alerts and automated purchase orders"
|
"description": "Automatic stock control with predictive alerts, automated purchase orders, and cost optimization.",
|
||||||
|
"features": {
|
||||||
|
"alerts": "Automatic low stock alerts",
|
||||||
|
"orders": "Automated purchase orders",
|
||||||
|
"optimization": "Raw material cost optimization"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"customer_insights": {
|
"production_planning": {
|
||||||
"title": "Customer Insights",
|
"title": "Production Planning",
|
||||||
"description": "Understand buying patterns and preferences to improve customer experience"
|
"description": "Automatically schedules daily production based on predictions, optimizes schedules and available resources.",
|
||||||
|
"features": {
|
||||||
|
"scheduling": "Automatic baking scheduling",
|
||||||
|
"oven": "Oven usage optimization",
|
||||||
|
"staff": "Staff and shift management"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"advanced_analytics": {
|
||||||
|
"title": "Advanced Analytics",
|
||||||
|
"description": "Real-time dashboards with key metrics"
|
||||||
|
},
|
||||||
|
"pos_integration": {
|
||||||
|
"title": "Integrated POS",
|
||||||
|
"description": "Complete and easy-to-use sales system"
|
||||||
|
},
|
||||||
|
"quality_control": {
|
||||||
|
"title": "Quality Control",
|
||||||
|
"description": "Complete traceability and HACCP management"
|
||||||
|
},
|
||||||
|
"automation": {
|
||||||
|
"title": "Automation",
|
||||||
|
"description": "Automatic processes that save time"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"benefits": {
|
"benefits": {
|
||||||
"title": "Proven results that transform your business",
|
"title": "The Problem We Solve",
|
||||||
"subtitle": "Over 1,000 bakeries have already transformed their operations with our AI",
|
"title_accent": "For Your Bakery",
|
||||||
"waste_reduction": {
|
"subtitle": "We know how frustrating it is to throw away products at the end of the day, or run out of stock when customers arrive. Bakery production is hard to optimize... until now.",
|
||||||
"value": "35%",
|
"problems": {
|
||||||
"label": "Waste reduction"
|
"waste": {
|
||||||
|
"title": "You waste 15-40% of production",
|
||||||
|
"description": "At the end of the day, you throw away unsold products. That's hundreds of euros in the trash every week."
|
||||||
|
},
|
||||||
|
"stockouts": {
|
||||||
|
"title": "You lose sales due to stockouts",
|
||||||
|
"description": "Customers come for their favorite bread and leave without buying because you ran out at 2 PM."
|
||||||
|
},
|
||||||
|
"manual": {
|
||||||
|
"title": "Excel, paper, and \"experience\"",
|
||||||
|
"description": "You plan based on intuition. It works... until it doesn't."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"accuracy": {
|
"solutions": {
|
||||||
"value": "92%",
|
"exact_production": {
|
||||||
"label": "Prediction accuracy"
|
"title": "Produce exactly what you'll sell",
|
||||||
|
"description": "AI analyzes your sales history, weather, local events, and holidays to predict real demand."
|
||||||
|
},
|
||||||
|
"stock_availability": {
|
||||||
|
"title": "Always have stock of bestsellers",
|
||||||
|
"description": "The system alerts you which products will have higher demand each day, so you never run out."
|
||||||
|
},
|
||||||
|
"smart_automation": {
|
||||||
|
"title": "Smart automation + real data",
|
||||||
|
"description": "From production planning to inventory management. All based on math, not hunches."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"time_saved": {
|
"value_proposition": {
|
||||||
"value": "4h",
|
"title": "The Goal: Save You Money From Month One",
|
||||||
"label": "Daily planning time saved"
|
"description": "We don't promise magic numbers because every bakery is different. What we DO promise is that if after 3 months you haven't reduced waste or improved margins, <strong>we'll help you optimize your business another way for free</strong>.",
|
||||||
},
|
"points": {
|
||||||
"sales_increase": {
|
"waste": "Less waste = more profit",
|
||||||
"value": "28%",
|
"time": "Less time in Excel, more in your business",
|
||||||
"label": "Average sales increase"
|
"data": "Your data is always yours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pricing": {
|
"risk_reversal": {
|
||||||
"title": "Plans designed for bakeries of all sizes",
|
"title": "No Risk. No Strings Attached.",
|
||||||
"subtitle": "Start free and scale as you grow",
|
"subtitle": "We're transparent: this is a pilot. We're building the best tool for bakeries, and we need your help.",
|
||||||
"starter": {
|
"what_you_get": {
|
||||||
"name": "Starter",
|
"title": "What You Get",
|
||||||
"price": "Free",
|
"free_trial": "<strong>3 completely free months</strong> to test all features",
|
||||||
"description": "Perfect for small bakeries getting started",
|
"lifetime_discount": "<strong>40% lifetime discount</strong> if you continue after the pilot",
|
||||||
"features": [
|
"founder_support": "<strong>Direct support from founding team</strong> - we respond in hours, not days",
|
||||||
"Up to 50 products",
|
"priority_features": "<strong>Your ideas implemented first</strong> - we build what you really need",
|
||||||
"Basic demand forecasting",
|
"cancel_anytime": "<strong>Cancel anytime</strong> without explanations or penalties"
|
||||||
"Basic dashboard",
|
|
||||||
"Email support"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"professional": {
|
"what_we_ask": {
|
||||||
"name": "Professional",
|
"title": "What We Ask",
|
||||||
"price": "$49",
|
"feedback": "<strong>Weekly honest feedback</strong> (15 min) about what works and what doesn't",
|
||||||
"price_period": "/month",
|
"patience": "<strong>Patience with bugs</strong> - we're in beta, there will be imperfections",
|
||||||
"description": "Ideal for established bakeries",
|
"data": "<strong>Historical sales data</strong> (optional) to improve predictions",
|
||||||
"features": [
|
"communication": "<strong>Open communication</strong> - we want to know if you don't like something"
|
||||||
"Unlimited products",
|
|
||||||
"Advanced AI forecasting",
|
|
||||||
"Complete analytics",
|
|
||||||
"Inventory management",
|
|
||||||
"Production optimization",
|
|
||||||
"Priority 24/7 support"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"enterprise": {
|
"promise": "<strong>Promise:</strong> If after 3 months you feel we haven't helped you save money or reduce waste, we'll give you a free consulting session to optimize your bakery another way.",
|
||||||
"name": "Enterprise",
|
"credibility": {
|
||||||
"price": "Custom",
|
"title": "Why Trust Us?",
|
||||||
"description": "Complete solution for chains and franchises",
|
"subtitle": "We understand trying new technology is a risk. That's why we're completely transparent:",
|
||||||
"features": [
|
"spanish": {
|
||||||
"Multi-location",
|
"title": "100% Spanish",
|
||||||
"Custom API",
|
"description": "Registered company in Spain. Your data is protected by GDPR and never leaves the EU."
|
||||||
"Advanced integrations",
|
},
|
||||||
"Dedicated support",
|
"technology": {
|
||||||
"Custom training",
|
"title": "Proven Technology",
|
||||||
"Guaranteed SLA"
|
"description": "We use academically validated AI algorithms, specifically adapted for bakeries."
|
||||||
]
|
},
|
||||||
|
"team": {
|
||||||
|
"title": "Expert Team",
|
||||||
|
"description": "Founders with experience in AI + hospitality. We know the industry from the inside."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"faq": {
|
||||||
|
"title": "Frequently Asked Questions",
|
||||||
|
"subtitle": "Everything you need to know about Bakery AI",
|
||||||
|
"questions": {
|
||||||
|
"accuracy": {
|
||||||
|
"q": "How accurate is demand prediction?",
|
||||||
|
"a": "Our AI achieves 92% accuracy in demand predictions, analyzing over 50 variables including sales history, weather, local events, seasonality, and market trends. Accuracy continuously improves with more data from your bakery."
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"q": "How long does it take to implement the system?",
|
||||||
|
"a": "Initial setup takes only 5 minutes. Our team helps you migrate historical data in 24-48 hours. The AI starts generating useful predictions after one week of data, reaching maximum accuracy in 30 days."
|
||||||
|
},
|
||||||
|
"integration": {
|
||||||
|
"q": "Does it integrate with my current POS system?",
|
||||||
|
"a": "Yes, we integrate with over 50 popular POS systems in Spain. We also include our own POS optimized for bakeries. If you use a specific system, our technical team can create a custom integration."
|
||||||
|
},
|
||||||
|
"support": {
|
||||||
|
"q": "What technical support do you offer?",
|
||||||
|
"a": "We offer 24/7 support in English via chat, email, and phone. All our technicians are experts in bakery operations. We also include personalized onboarding and team training at no additional cost."
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"q": "Is my data safe?",
|
||||||
|
"a": "Absolutely. We use AES-256 encryption, EU servers, 100% GDPR compliance, and conduct quarterly security audits. Your data is never shared with third parties, and you have total control over your information."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"final_cta": {
|
||||||
|
"scarcity_badge": "12 spots remaining of the 20 pilot program",
|
||||||
|
"title": "Be Among the First 20 Bakeries",
|
||||||
|
"title_accent": "To Try This Technology",
|
||||||
|
"subtitle": "It's not for everyone. We're looking for bakeries that want to <strong>reduce waste and increase profits</strong> with AI help, in exchange for honest feedback.",
|
||||||
|
"cta_primary": "Apply for Pilot Spot",
|
||||||
|
"cta_secondary": "See How It Works",
|
||||||
|
"why_now": {
|
||||||
|
"title": "Why act now?",
|
||||||
|
"lifetime_discount": {
|
||||||
|
"title": "40% lifetime discount",
|
||||||
|
"subtitle": "First 20 only"
|
||||||
|
},
|
||||||
|
"influence": {
|
||||||
|
"title": "Influence the roadmap",
|
||||||
|
"subtitle": "Your needs first"
|
||||||
|
},
|
||||||
|
"vip_support": {
|
||||||
|
"title": "VIP Support",
|
||||||
|
"subtitle": "Direct team access"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"cta": "Get Started Now",
|
"guarantee": "Guarantee: Cancel anytime without giving explanations"
|
||||||
"contact": "Contact Sales"
|
|
||||||
},
|
|
||||||
"testimonials": {
|
|
||||||
"title": "What our customers say",
|
|
||||||
"subtitle": "Bakeries worldwide trust our platform"
|
|
||||||
},
|
|
||||||
"cta_section": {
|
|
||||||
"title": "Ready to revolutionize your bakery?",
|
|
||||||
"subtitle": "Join over 1,000 bakeries already using AI to optimize their operations",
|
|
||||||
"cta": "Start Free Today"
|
|
||||||
},
|
|
||||||
"footer_cta": {
|
|
||||||
"security": "Enterprise security",
|
|
||||||
"uptime": "99.9% guaranteed uptime",
|
|
||||||
"data_protection": "GDPR data protection"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,5 +335,25 @@
|
|||||||
"positive_number": "Debe ser un número positivo",
|
"positive_number": "Debe ser un número positivo",
|
||||||
"negative_not_allowed": "Los números negativos no están permitidos"
|
"negative_not_allowed": "Los números negativos no están permitidos"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cookie": {
|
||||||
|
"banner_title": "Valoramos Tu Privacidad",
|
||||||
|
"minimized_message": "Usamos cookies para mejorar tu experiencia.",
|
||||||
|
"show_details": "Ver Detalles",
|
||||||
|
"banner_description": "Usamos cookies y tecnologías similares para proporcionar, proteger y mejorar nuestros servicios. Algunas cookies son esenciales para el funcionamiento del sitio, mientras que otras nos ayudan a entender cómo usas nuestros servicios y a ofrecer características personalizadas.",
|
||||||
|
"banner_description_2": "Al hacer clic en \"Aceptar Todas\", aceptas nuestro uso de todas las cookies. Puedes personalizar tus preferencias o aceptar solo las cookies esenciales.",
|
||||||
|
"learn_more": "Más información sobre cookies",
|
||||||
|
"privacy_policy": "Política de Privacidad",
|
||||||
|
"minimize": "Minimizar",
|
||||||
|
"accept_all": "Aceptar Todas",
|
||||||
|
"accept_essential": "Solo Esenciales",
|
||||||
|
"customize": "Personalizar"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"login": "Iniciar Sesión",
|
||||||
|
"start_free": "Comenzar Gratis",
|
||||||
|
"register": "Registro",
|
||||||
|
"language": "Idioma",
|
||||||
|
"open_menu": "Abrir menú de navegación"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,125 +3,246 @@
|
|||||||
"features": "Características",
|
"features": "Características",
|
||||||
"benefits": "Beneficios",
|
"benefits": "Beneficios",
|
||||||
"pricing": "Precios",
|
"pricing": "Precios",
|
||||||
"testimonials": "Testimonios"
|
"faq": "Preguntas Frecuentes"
|
||||||
},
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"badge": "IA Avanzada para Panaderías",
|
"badge": "IA Avanzada para Panaderías",
|
||||||
"title_line1": "Revoluciona tu",
|
"badge_sustainability": "Reducción de Desperdicio Alimentario",
|
||||||
"title_line2": "Panadería con IA",
|
"title_line1": "Aumenta Ganancias,",
|
||||||
"subtitle": "Optimiza automáticamente tu producción, reduce desperdicios hasta un 35%, predice demanda con precisión del 92% y aumenta tus ventas con inteligencia artificial.",
|
"title_line2": "Reduce Desperdicios",
|
||||||
"cta_primary": "Comenzar Gratis 14 Días",
|
"subtitle": "Plataforma de IA para panaderías que quieren producir exactamente lo que van a vender. Reduce desperdicio alimentario, mejora márgenes y ahorra tiempo en planificación. 100% española, tus datos son tuyos.",
|
||||||
"cta_secondary": "Ver Demo en Vivo",
|
"pilot_banner": {
|
||||||
|
"title": "¡Lanzamiento Piloto!",
|
||||||
|
"offer": "3 MESES GRATIS",
|
||||||
|
"description": "para los primeros en unirse al piloto"
|
||||||
|
},
|
||||||
|
"cta_primary": "Comenzar Gratis Ahora",
|
||||||
|
"cta_secondary": "Ver Demo",
|
||||||
"features": {
|
"features": {
|
||||||
"no_credit_card": "Sin tarjeta de crédito",
|
"card_required": "Tarjeta requerida • 3 meses gratis",
|
||||||
"quick_setup": "Configuración en 5 minutos",
|
"no_credit_card": "Tarjeta requerida • 3 meses gratis",
|
||||||
"support_24_7": "Soporte 24/7 en español"
|
"quick_setup": "Lista en minutos, no horas",
|
||||||
|
"support_24_7": "Asistencia personalizada en español"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pilot": {
|
||||||
|
"banner": {
|
||||||
|
"active": "Programa Piloto Activado",
|
||||||
|
"trial_applied": "3 meses gratis aplicados automáticamente",
|
||||||
|
"coupon_code": "Código",
|
||||||
|
"description": "Has sido seleccionado para nuestro programa piloto exclusivo"
|
||||||
|
},
|
||||||
|
"badge": "Programa Piloto - Plazas Limitadas",
|
||||||
|
"title": "Buscamos 20 Panaderías Pioneras",
|
||||||
|
"subtitle": "Estamos seleccionando las primeras 20 panaderías para formar parte de nuestro programa piloto exclusivo. A cambio de tu feedback, obtienes <strong>3 meses gratis + precio preferencial de por vida</strong>.",
|
||||||
|
"benefits": {
|
||||||
|
"founders_beta": {
|
||||||
|
"title": "Founders Beta",
|
||||||
|
"description": "Acceso de por vida con 40% descuento"
|
||||||
|
},
|
||||||
|
"influence_product": {
|
||||||
|
"title": "Influye el Producto",
|
||||||
|
"description": "Tus necesidades moldean la plataforma"
|
||||||
|
},
|
||||||
|
"premium_support": {
|
||||||
|
"title": "Soporte Premium",
|
||||||
|
"description": "Atención directa del equipo fundador"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"features": {
|
"features": {
|
||||||
"title": "Todo lo que necesitas para optimizar tu panadería",
|
"title_main": "Combate el Desperdicio Alimentario",
|
||||||
"subtitle": "Herramientas potentes diseñadas específicamente para panaderías modernas",
|
"title_accent": "con Inteligencia Artificial",
|
||||||
"ai_forecasting": {
|
"badge": "Tecnología de IA de Última Generación",
|
||||||
"title": "Predicción IA de Demanda",
|
"subtitle": "Sistema de alta tecnología que utiliza algoritmos de IA avanzados para optimizar tu producción, reducir residuos alimentarios y mantener tus datos 100% seguros y bajo tu control.",
|
||||||
"description": "Algoritmos avanzados predicen qué productos necesitarás cada día con 92% de precisión"
|
"ai_prediction": {
|
||||||
},
|
"title": "IA Avanzada de Predicción",
|
||||||
"production_optimization": {
|
"description": "Algoritmos de machine learning de última generación analizan patrones históricos, clima, eventos y tendencias para predecir demanda con precisión quirúrgica.",
|
||||||
"title": "Optimización de Producción",
|
"features": {
|
||||||
"description": "Planifica automáticamente horarios de horneado y gestión de personal para máxima eficiencia"
|
"accuracy": "Precisión del 92% en predicciones",
|
||||||
|
"learning": "Aprendizaje continuo y adaptativo",
|
||||||
|
"realtime": "Análisis predictivo en tiempo real"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"waste_reduction": {
|
"waste_reduction": {
|
||||||
"title": "Reducción de Desperdicios",
|
"title": "Reducción de Desperdicio",
|
||||||
"description": "Reduce hasta 35% el desperdicio con predicciones precisas y gestión inteligente de inventario"
|
"description": "Contribuye al medioambiente y reduce costos eliminando hasta un 35% del desperdicio alimentario mediante producción optimizada e inteligente.",
|
||||||
|
"features": {
|
||||||
|
"reduction": "Hasta 35% menos desperdicio",
|
||||||
|
"savings": "Ahorro promedio de €800/mes",
|
||||||
|
"eligible": "Elegible para ayudas UE"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"real_time_analytics": {
|
"data_ownership": {
|
||||||
"title": "Análisis en Tiempo Real",
|
"title": "Tus Datos, Tu Propiedad",
|
||||||
"description": "Dashboard intuitivo con métricas de ventas, producción y rentabilidad actualizadas al instante"
|
"description": "Privacidad y seguridad total. Tus datos operativos, proveedores y analíticas permanecen 100% bajo tu control. Nunca compartidos, nunca vendidos.",
|
||||||
|
"features": {
|
||||||
|
"ownership": "100% propiedad de datos",
|
||||||
|
"privacy": "Control total de privacidad",
|
||||||
|
"gdpr": "Cumplimiento GDPR garantizado"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"inventory_management": {
|
"smart_inventory": {
|
||||||
"title": "Gestión de Inventario",
|
"title": "Inventario Inteligente",
|
||||||
"description": "Control automático de stock con alertas inteligentes y órdenes de compra automatizadas"
|
"description": "Control automático de stock con alertas predictivas, órdenes de compra automatizadas y optimización de costos.",
|
||||||
|
"features": {
|
||||||
|
"alerts": "Alertas automáticas de stock bajo",
|
||||||
|
"orders": "Órdenes de compra automatizadas",
|
||||||
|
"optimization": "Optimización de costos de materias primas"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"customer_insights": {
|
"production_planning": {
|
||||||
"title": "Insights de Clientes",
|
"title": "Planificación de Producción",
|
||||||
"description": "Entiende patrones de compra y preferencias para mejorar la experiencia del cliente"
|
"description": "Programa automáticamente la producción diaria basada en predicciones, optimiza horarios y recursos disponibles.",
|
||||||
|
"features": {
|
||||||
|
"scheduling": "Programación automática de horneado",
|
||||||
|
"oven": "Optimización de uso de hornos",
|
||||||
|
"staff": "Gestión de personal y turnos"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"advanced_analytics": {
|
||||||
|
"title": "Analytics Avanzado",
|
||||||
|
"description": "Dashboards en tiempo real con métricas clave"
|
||||||
|
},
|
||||||
|
"pos_integration": {
|
||||||
|
"title": "POS Integrado",
|
||||||
|
"description": "Sistema de ventas completo y fácil de usar"
|
||||||
|
},
|
||||||
|
"quality_control": {
|
||||||
|
"title": "Control de Calidad",
|
||||||
|
"description": "Trazabilidad completa y gestión HACCP"
|
||||||
|
},
|
||||||
|
"automation": {
|
||||||
|
"title": "Automatización",
|
||||||
|
"description": "Procesos automáticos que ahorran tiempo"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"benefits": {
|
"benefits": {
|
||||||
"title": "Resultados comprobados que transforman tu negocio",
|
"title": "El Problema Que Resolvemos",
|
||||||
"subtitle": "Más de 1,000 panaderías ya han transformado sus operaciones con nuestra IA",
|
"title_accent": "Para Tu Panadería",
|
||||||
"waste_reduction": {
|
"subtitle": "Sabemos lo frustrante que es tirar producto al final del día, o quedarte sin stock cuando llegan clientes. La producción de panadería es difícil de optimizar... hasta ahora.",
|
||||||
"value": "35%",
|
"problems": {
|
||||||
"label": "Reducción en desperdicios"
|
"waste": {
|
||||||
|
"title": "Desperdicias entre 15-40% de producción",
|
||||||
|
"description": "Al final del día tiras producto que nadie compró. Son cientos de euros a la basura cada semana."
|
||||||
|
},
|
||||||
|
"stockouts": {
|
||||||
|
"title": "Pierdes ventas por falta de stock",
|
||||||
|
"description": "Clientes que vienen por su pan favorito y se van sin comprar porque ya se te acabó a las 14:00."
|
||||||
|
},
|
||||||
|
"manual": {
|
||||||
|
"title": "Excel, papel y \"experiencia\"",
|
||||||
|
"description": "Planificas basándote en intuición. Funciona... hasta que no funciona."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"accuracy": {
|
"solutions": {
|
||||||
"value": "92%",
|
"exact_production": {
|
||||||
"label": "Precisión en predicciones"
|
"title": "Produce exactamente lo que vas a vender",
|
||||||
|
"description": "La IA analiza tus ventas históricas, clima, eventos locales y festivos para predecir demanda real."
|
||||||
|
},
|
||||||
|
"stock_availability": {
|
||||||
|
"title": "Siempre tienes stock de lo que más se vende",
|
||||||
|
"description": "El sistema te avisa qué productos van a tener más demanda cada día, para que nunca te quedes sin."
|
||||||
|
},
|
||||||
|
"smart_automation": {
|
||||||
|
"title": "Automatización inteligente + datos reales",
|
||||||
|
"description": "Desde planificación de producción hasta gestión de inventario. Todo basado en matemáticas, no corazonadas."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"time_saved": {
|
"value_proposition": {
|
||||||
"value": "4h",
|
"title": "El Objetivo: Que Ahorres Dinero Desde el Primer Mes",
|
||||||
"label": "Ahorro diario en planificación"
|
"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>.",
|
||||||
},
|
"points": {
|
||||||
"sales_increase": {
|
"waste": "Menos desperdicio = más beneficio",
|
||||||
"value": "28%",
|
"time": "Menos tiempo en Excel, más en tu negocio",
|
||||||
"label": "Incremento promedio en ventas"
|
"data": "Tus datos siempre son tuyos"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pricing": {
|
"risk_reversal": {
|
||||||
"title": "Planes diseñados para panaderías de todos los tamaños",
|
"title": "Sin Riesgo. Sin Ataduras.",
|
||||||
"subtitle": "Comienza gratis y escala según crezcas",
|
"subtitle": "Somos transparentes: esto es un piloto. Estamos construyendo la mejor herramienta para panaderías, y necesitamos tu ayuda.",
|
||||||
"starter": {
|
"what_you_get": {
|
||||||
"name": "Iniciante",
|
"title": "Lo Que Obtienes",
|
||||||
"price": "Gratis",
|
"free_trial": "<strong>3 meses completamente gratis</strong> para probar todas las funcionalidades",
|
||||||
"description": "Perfecto para panaderías pequeñas que empiezan",
|
"lifetime_discount": "<strong>40% de descuento de por vida</strong> si decides continuar después del piloto",
|
||||||
"features": [
|
"founder_support": "<strong>Soporte directo del equipo fundador</strong> - respondemos en horas, no días",
|
||||||
"Hasta 50 productos",
|
"priority_features": "<strong>Tus ideas se implementan primero</strong> - construimos lo que realmente necesitas",
|
||||||
"Predicción básica de demanda",
|
"cancel_anytime": "<strong>Cancelas cuando quieras</strong> sin explicaciones ni penalizaciones"
|
||||||
"Dashboard básico",
|
|
||||||
"Soporte por email"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"professional": {
|
"what_we_ask": {
|
||||||
"name": "Profesional",
|
"title": "Lo Que Pedimos",
|
||||||
"price": "€49",
|
"feedback": "<strong>Feedback honesto semanal</strong> (15 min) sobre qué funciona y qué no",
|
||||||
"price_period": "/mes",
|
"patience": "<strong>Paciencia con bugs</strong> - estamos en fase beta, habrá imperfecciones",
|
||||||
"description": "Ideal para panaderías establecidas",
|
"data": "<strong>Datos de ventas históricos</strong> (opcional) para mejorar las predicciones",
|
||||||
"features": [
|
"communication": "<strong>Comunicación abierta</strong> - queremos saber si algo no te gusta"
|
||||||
"Productos ilimitados",
|
|
||||||
"IA avanzada de predicción",
|
|
||||||
"Analytics completos",
|
|
||||||
"Gestión de inventario",
|
|
||||||
"Optimización de producción",
|
|
||||||
"Soporte prioritario 24/7"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"enterprise": {
|
"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.",
|
||||||
"name": "Empresa",
|
"credibility": {
|
||||||
"price": "Personalizado",
|
"title": "¿Por Qué Confiar en Nosotros?",
|
||||||
"description": "Solución completa para cadenas y franquicias",
|
"subtitle": "Entendemos que probar nueva tecnología es un riesgo. Por eso somos completamente transparentes:",
|
||||||
"features": [
|
"spanish": {
|
||||||
"Multi-ubicación",
|
"title": "100% Española",
|
||||||
"API personalizada",
|
"description": "Empresa registrada en España. Tus datos están protegidos por RGPD y nunca salen de la UE."
|
||||||
"Integraciones avanzadas",
|
},
|
||||||
"Soporte dedicado",
|
"technology": {
|
||||||
"Capacitación personalizada",
|
"title": "Tecnología Probada",
|
||||||
"SLA garantizado"
|
"description": "Usamos algoritmos de IA validados académicamente, adaptados específicamente para panaderías."
|
||||||
]
|
},
|
||||||
|
"team": {
|
||||||
|
"title": "Equipo Experto",
|
||||||
|
"description": "Fundadores con experiencia en IA + hostelería. Conocemos el sector de dentro."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"faq": {
|
||||||
|
"title": "Preguntas Frecuentes",
|
||||||
|
"subtitle": "Todo lo que necesitas saber sobre Panadería IA",
|
||||||
|
"questions": {
|
||||||
|
"accuracy": {
|
||||||
|
"q": "¿Qué tan precisa es la predicción de demanda?",
|
||||||
|
"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."
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"q": "¿Cuánto tiempo toma implementar el sistema?",
|
||||||
|
"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."
|
||||||
|
},
|
||||||
|
"integration": {
|
||||||
|
"q": "¿Se integra con mi sistema POS actual?",
|
||||||
|
"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."
|
||||||
|
},
|
||||||
|
"support": {
|
||||||
|
"q": "¿Qué soporte técnico ofrecen?",
|
||||||
|
"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."
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"q": "¿Mis datos están seguros?",
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"final_cta": {
|
||||||
|
"scarcity_badge": "Quedan 12 plazas de las 20 del programa piloto",
|
||||||
|
"title": "Sé de las Primeras 20 Panaderías",
|
||||||
|
"title_accent": "En Probar Esta Tecnología",
|
||||||
|
"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.",
|
||||||
|
"cta_primary": "Solicitar Plaza en el Piloto",
|
||||||
|
"cta_secondary": "Ver Cómo Funciona",
|
||||||
|
"why_now": {
|
||||||
|
"title": "¿Por qué actuar ahora?",
|
||||||
|
"lifetime_discount": {
|
||||||
|
"title": "40% descuento de por vida",
|
||||||
|
"subtitle": "Solo primeros 20"
|
||||||
|
},
|
||||||
|
"influence": {
|
||||||
|
"title": "Influyes en el roadmap",
|
||||||
|
"subtitle": "Tus necesidades primero"
|
||||||
|
},
|
||||||
|
"vip_support": {
|
||||||
|
"title": "Soporte VIP",
|
||||||
|
"subtitle": "Acceso directo al equipo"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"cta": "Comenzar Ahora",
|
"guarantee": "Garantía: Cancelas en cualquier momento sin dar explicaciones"
|
||||||
"contact": "Contactar Ventas"
|
|
||||||
},
|
|
||||||
"testimonials": {
|
|
||||||
"title": "Lo que dicen nuestros clientes",
|
|
||||||
"subtitle": "Panaderías de todo el mundo confían en nuestra plataforma"
|
|
||||||
},
|
|
||||||
"cta_section": {
|
|
||||||
"title": "¿Listo para revolucionar tu panadería?",
|
|
||||||
"subtitle": "Únete a más de 1,000 panaderías que ya están usando IA para optimizar sus operaciones",
|
|
||||||
"cta": "Comenzar Gratis Hoy"
|
|
||||||
},
|
|
||||||
"footer_cta": {
|
|
||||||
"security": "Seguridad empresarial",
|
|
||||||
"uptime": "99.9% uptime garantizado",
|
|
||||||
"data_protection": "Protección de datos RGPD"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,5 +335,25 @@
|
|||||||
"positive_number": "Zenbaki positiboa izan behar da",
|
"positive_number": "Zenbaki positiboa izan behar da",
|
||||||
"negative_not_allowed": "Zenbaki negatiboak ez dira onartzen"
|
"negative_not_allowed": "Zenbaki negatiboak ez dira onartzen"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cookie": {
|
||||||
|
"banner_title": "Zure Pribatutasuna Baloratzen Dugu",
|
||||||
|
"minimized_message": "Cookie-ak erabiltzen ditugu zure esperientzia hobetzeko.",
|
||||||
|
"show_details": "Xehetasunak Ikusi",
|
||||||
|
"banner_description": "Cookie-ak eta antzeko teknologiak erabiltzen ditugu gure zerbitzuak emateko, babesteko eta hobetzeko. Cookie batzuk funtsezkoak dira guneak funtzionatzeko, beste batzuk gure zerbitzuak nola erabiltzen dituzun ulertzeko eta ezaugarri pertsonalizatuak eskaintzeko laguntzen digute.",
|
||||||
|
"banner_description_2": "\"Onartu Guztiak\" klik eginez, gure cookie guztien erabilerarekin ados zaude. Zure hobespenak pertsonalizatu ditzakezu edo funtsezko cookie-ak bakarrik onartu.",
|
||||||
|
"learn_more": "Cookie-ei buruz gehiago ikasi",
|
||||||
|
"privacy_policy": "Pribatutasun Politika",
|
||||||
|
"minimize": "Minimizatu",
|
||||||
|
"accept_all": "Onartu Guztiak",
|
||||||
|
"accept_essential": "Funtsezkoak Bakarrik",
|
||||||
|
"customize": "Pertsonalizatu"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"login": "Hasi Saioa",
|
||||||
|
"start_free": "Hasi Doan",
|
||||||
|
"register": "Erregistratu",
|
||||||
|
"language": "Hizkuntza",
|
||||||
|
"open_menu": "Ireki nabigazio menua"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,125 +3,246 @@
|
|||||||
"features": "Ezaugarriak",
|
"features": "Ezaugarriak",
|
||||||
"benefits": "Onurak",
|
"benefits": "Onurak",
|
||||||
"pricing": "Prezioak",
|
"pricing": "Prezioak",
|
||||||
"testimonials": "Testigantzak"
|
"faq": "Maiz Egiten Diren Galderak"
|
||||||
},
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"badge": "AA Aurreratua Okindegirentzat",
|
"badge": "AA Aurreratua Okindegirentzat",
|
||||||
"title_line1": "Irauli zure",
|
"badge_sustainability": "Elikagai Hondakinen Murrizketa",
|
||||||
"title_line2": "Okindegia AA-rekin",
|
"title_line1": "Handitu Irabaziak,",
|
||||||
"subtitle": "Optimizatu automatikoki zure ekoizpena, murriztu hondakinak %35era, aurreikusi eskaria %92ko zehaztasunarekin eta handitu zure salmentak adimen artifizialarekin.",
|
"title_line2": "Murriztu Hondakinak",
|
||||||
"cta_primary": "Hasi Doan 14 Egunez",
|
"subtitle": "AA plataforma okindegiek saldu behar duten zehatz-mehatz ekoizteko. Murriztu elikagai hondakinak, hobetu marjinak eta aurreztu denbora plangintzan. 100% espainiarra, zure datuak zureak dira.",
|
||||||
"cta_secondary": "Ikusi Demo Zuzenan",
|
"pilot_banner": {
|
||||||
|
"title": "Pilotu Abiapena!",
|
||||||
|
"offer": "3 HILABETE DOAN",
|
||||||
|
"description": "pilotura lehen batzen direnei"
|
||||||
|
},
|
||||||
|
"cta_primary": "Hasi Doan Orain",
|
||||||
|
"cta_secondary": "Ikusi Demoa",
|
||||||
"features": {
|
"features": {
|
||||||
"no_credit_card": "Kreditu txartelik ez",
|
"card_required": "Txartela beharrezkoa • 3 hilabete doan",
|
||||||
"quick_setup": "5 minutuko konfigurazioa",
|
"no_credit_card": "Txartela beharrezkoa • 3 hilabete doan",
|
||||||
"support_24_7": "24/7 laguntza euskeraz"
|
"quick_setup": "Prest minutuetan, ez orduetan",
|
||||||
|
"support_24_7": "Laguntza pertsonalizatua euskeraz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pilot": {
|
||||||
|
"banner": {
|
||||||
|
"active": "Pilotu Programa Aktibatuta",
|
||||||
|
"trial_applied": "3 hilabete doan automatikoki aplikatuta",
|
||||||
|
"coupon_code": "Kodea",
|
||||||
|
"description": "Gure pilotu programa esklusibora hautatu zaitu"
|
||||||
|
},
|
||||||
|
"badge": "Pilotu Programa - Leku Mugatuak",
|
||||||
|
"title": "20 Okindegi Aitzindari Bilatzen",
|
||||||
|
"subtitle": "Lehenengo 20 okindegiak hautatzen ari gara gure pilotu programa esklusiboa osatzeko. Zure feedbackaren truke, lortuko duzu <strong>3 hilabete doan + bizitza osoko prezio lehentasuna</strong>.",
|
||||||
|
"benefits": {
|
||||||
|
"founders_beta": {
|
||||||
|
"title": "Sortzaileen Beta",
|
||||||
|
"description": "Bizitza osoko sarbidea %40 deskontuarekin"
|
||||||
|
},
|
||||||
|
"influence_product": {
|
||||||
|
"title": "Eragin Produktuan",
|
||||||
|
"description": "Zure beharrek moldatzen dute plataforma"
|
||||||
|
},
|
||||||
|
"premium_support": {
|
||||||
|
"title": "Premium Laguntza",
|
||||||
|
"description": "Sortzaile taldearen zuzeneko arreta"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"features": {
|
"features": {
|
||||||
"title": "Zure okindegia optimizatzeko behar duzun guztia",
|
"title_main": "Borrokatu Elikagai Hondakinen Aurka",
|
||||||
"subtitle": "Okindegi modernoetarako diseinu bereziki egindako tresna indartsuak",
|
"title_accent": "Adimen Artifizialarekin",
|
||||||
"ai_forecasting": {
|
"badge": "Azken Belaunaldiko AA Teknologia",
|
||||||
"title": "AA Eskariaren Aurreikuspena",
|
"subtitle": "Teknologia handiko sistema, AA algoritmo aurreratuak erabiltzen dituena zure ekoizpena optimizatzeko, elikagai hondakinak murrizteko eta zure datuak %100 seguru eta zure kontrolpean mantentzeko.",
|
||||||
"description": "Algoritmo aurreratuek egunero behar dituzun produktuak aurreikusten dituzte %92ko zehaztasunarekin"
|
"ai_prediction": {
|
||||||
},
|
"title": "AA Aurreikuspen Aurreratua",
|
||||||
"production_optimization": {
|
"description": "Azken belaunaldiko ikaskuntza automatikoko algoritmoek historikoa, eguraldia, ekitaldiak eta joerak aztertzen dituzte eskaria zehaztasun kirurgikoz aurreikusteko.",
|
||||||
"title": "Ekoizpenaren Optimizazioa",
|
"features": {
|
||||||
"description": "Planifikatu automatikoki labe ordutegiak eta langile kudeaketa eraginkortasun maximorakoak"
|
"accuracy": "%92ko zehaztasuna aurreikuspenen",
|
||||||
|
"learning": "Ikaskuntza etengabe eta moldagarria",
|
||||||
|
"realtime": "Denbora errealeko analitika aurreikuspena"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"waste_reduction": {
|
"waste_reduction": {
|
||||||
"title": "Hondakinen Murrizketa",
|
"title": "Hondakinen Murrizketa",
|
||||||
"description": "Murriztu hondakinak %35era aurreikuspen zehatzak eta inbentario kudeaketa adimendunarekin"
|
"description": "Lagundu ingurumenari eta murriztu kostuak %35era elikagai hondakinak kenduz ekoizpen optimizatu eta adimendunaren bidez.",
|
||||||
|
"features": {
|
||||||
|
"reduction": "%35era arte hondakin gutxiago",
|
||||||
|
"savings": "€800/hilabeteko batez besteko aurrezkia",
|
||||||
|
"eligible": "EBko laguntzarako kualifikatua"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"real_time_analytics": {
|
"data_ownership": {
|
||||||
"title": "Denbora Errealeko Analisiak",
|
"title": "Zure Datuak, Zure Jabetza",
|
||||||
"description": "Dashboard intuitiboa salmenta, ekoizpen eta errentagarritasun metriken eguneratuta berehala"
|
"description": "Pribatutasun eta segurtasun osoa. Zure datu operatiboak, hornitzaileak eta analitikak %100 zure kontrolpean geratzen dira. Inoiz partekatua, inoiz saldua.",
|
||||||
|
"features": {
|
||||||
|
"ownership": "%100 datuen jabetza",
|
||||||
|
"privacy": "Pribatutasun kontrol osoa",
|
||||||
|
"gdpr": "DBEO betetze bermatua"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"inventory_management": {
|
"smart_inventory": {
|
||||||
"title": "Inbentario Kudeaketa",
|
"title": "Inbentario Adimenduna",
|
||||||
"description": "Stock kontrola automatikoa alerta adimentsu eta erosketako agindu automatizatuekin"
|
"description": "Stock kontrol automatikoa alerta aurreikuspenekin, erosketako agindu automatizatuekin eta kostu optimizazioarekin.",
|
||||||
|
"features": {
|
||||||
|
"alerts": "Stock baxuko alerta automatikoak",
|
||||||
|
"orders": "Erosketako agindu automatizatuak",
|
||||||
|
"optimization": "Lehengaien kostu optimizazioa"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"customer_insights": {
|
"production_planning": {
|
||||||
"title": "Bezeroen Ikuspegia",
|
"title": "Ekoizpen Planifikazioa",
|
||||||
"description": "Ulertu erosketa ereduak eta lehentasunak bezeroaren esperientzia hobetzeko"
|
"description": "Automatikoki programatu egunero ekoizpena aurreikuspenen arabera, optimizatu ordutegiak eta eskuragarri dauden baliabideak.",
|
||||||
|
"features": {
|
||||||
|
"scheduling": "Labe programazio automatikoa",
|
||||||
|
"oven": "Labe erabilera optimizazioa",
|
||||||
|
"staff": "Langile eta txanda kudeaketa"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"advanced_analytics": {
|
||||||
|
"title": "Analitika Aurreratua",
|
||||||
|
"description": "Denbora errealeko panelak metrika gakoekin"
|
||||||
|
},
|
||||||
|
"pos_integration": {
|
||||||
|
"title": "POS Integratua",
|
||||||
|
"description": "Salmenta sistema osoa eta erabilterraza"
|
||||||
|
},
|
||||||
|
"quality_control": {
|
||||||
|
"title": "Kalitate Kontrola",
|
||||||
|
"description": "Trazabilitate osoa eta HACCP kudeaketa"
|
||||||
|
},
|
||||||
|
"automation": {
|
||||||
|
"title": "Automatizazioa",
|
||||||
|
"description": "Denbora aurrezten duten prozesu automatikoak"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"benefits": {
|
"benefits": {
|
||||||
"title": "Zure negozioa transformatzen duten emaitza frogatuak",
|
"title": "Konpontzen Dugun Arazoa",
|
||||||
"subtitle": "1.000 okindegi baino gehiagok transformatu dituzte jadanik euren eragiketak gure AA-rekin",
|
"title_accent": "Zure Okindegirako",
|
||||||
"waste_reduction": {
|
"subtitle": "Badakigu zein desatsegingarria den egunaren amaieran produktua botatzea, edo bezeroek iristean stockik gabe geratzea. Okindegi ekoizpena zaila da optimizatzeko... orain arte.",
|
||||||
"value": "%35",
|
"problems": {
|
||||||
"label": "Hondakinen murrizketa"
|
"waste": {
|
||||||
|
"title": "Ekoizpenaren %15-40 galtzen duzu",
|
||||||
|
"description": "Egunaren amaieran, inork erosi ez duen produktua botatzen duzu. Astero ehunka euro zakarrontzira."
|
||||||
|
},
|
||||||
|
"stockouts": {
|
||||||
|
"title": "Salmentak galtzen dituzu stockik ez delako",
|
||||||
|
"description": "Bezeroek euren ogi gogokoagatiketortzen dira eta erosi gabe joaten dira 14:00etan amaitu zarelako."
|
||||||
|
},
|
||||||
|
"manual": {
|
||||||
|
"title": "Excel, papera eta \"esperientzia\"",
|
||||||
|
"description": "Intuizioaren arabera planifikatzen duzu. Funtzionatzen du... ez duen arte."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"accuracy": {
|
"solutions": {
|
||||||
"value": "%92",
|
"exact_production": {
|
||||||
"label": "Aurreikuspenaren zehaztasuna"
|
"title": "Ekoiztu salduko duzun zehaztasun",
|
||||||
|
"description": "AA-k aztertzen ditu zure salmenta historikoa, eguraldia, tokiko ekitaldiak eta jaiak eskari erreala aurreikusteko."
|
||||||
|
},
|
||||||
|
"stock_availability": {
|
||||||
|
"title": "Beti izan gehien saltzen denaren stocka",
|
||||||
|
"description": "Sistemak jakinarazten dizu zein produktuk izango duen eskari gehiago egun bakoitzean, inoiz gabe gera ez zaitezen."
|
||||||
|
},
|
||||||
|
"smart_automation": {
|
||||||
|
"title": "Automatizazio adimenduna + datu errealak",
|
||||||
|
"description": "Ekoizpen planifikaziotik inbentario kudeaketara. Dena matematikan oinarritua, ez intuizioetan."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"time_saved": {
|
"value_proposition": {
|
||||||
"value": "4o",
|
"title": "Helburua: Dirua Aurreztea Lehen Hilabetetik",
|
||||||
"label": "Eguneko planifikazioan aurreztutako denbora"
|
"description": "Ez ditugu hitz egiten zenbaki magikoz okindegi bakoitza desberdina delako. Hitz egiten DUGUN gauza da 3 hilabete ondoren hondakinak murriztu edo marjinak hobetu ez badituzu, <strong>doan lagunduko dizugula zure negozioa beste era batera optimizatzen</strong>.",
|
||||||
},
|
"points": {
|
||||||
"sales_increase": {
|
"waste": "Hondakin gutxiago = irabazi gehiago",
|
||||||
"value": "%28",
|
"time": "Excel-en denbora gutxiago, zure negozioetan gehiago",
|
||||||
"label": "Batez besteko salmenta igoera"
|
"data": "Zure datuak beti zureak dira"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pricing": {
|
"risk_reversal": {
|
||||||
"title": "Tamaina guztietako okindegirentzat diseinatutako planak",
|
"title": "Arriskurik Gabe. Loturak Gabe.",
|
||||||
"subtitle": "Hasi doan eta eskala hazi ahala",
|
"subtitle": "Gardenak gara: hau pilotu bat da. Okindegietarako tresna onena eraikitzen ari gara, eta zure laguntza behar dugu.",
|
||||||
"starter": {
|
"what_you_get": {
|
||||||
"name": "Hasiberria",
|
"title": "Zer Lortzen Duzu",
|
||||||
"price": "Doan",
|
"free_trial": "<strong>3 hilabete guztiz doan</strong> funtzionalitate guztiak probatzeko",
|
||||||
"description": "Hasitzen ari diren okindegi txikientzat perfektua",
|
"lifetime_discount": "<strong>%40ko bizitza osoko deskontua</strong> pilotuaren ondoren jarraitzea erabakitzen baduzu",
|
||||||
"features": [
|
"founder_support": "<strong>Sortzaile taldearen laguntza zuzena</strong> - orduetan erantzuten dugu, ez egunetan",
|
||||||
"50 produktu arte",
|
"priority_features": "<strong>Zure ideiak lehenik gauzatzen dira</strong> - benetan behar duzuna eraikitzen dugu",
|
||||||
"Oinarrizko eskariaren aurreikuspena",
|
"cancel_anytime": "<strong>Edozein unetan ezeztatu</strong> azalpenik edo zigorrekin"
|
||||||
"Dashboard basikoa",
|
|
||||||
"Email laguntza"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"professional": {
|
"what_we_ask": {
|
||||||
"name": "Profesionala",
|
"title": "Zer Eskatzen Dugu",
|
||||||
"price": "49€",
|
"feedback": "<strong>Asteko feedback zintzoa</strong> (15 min) zer funtzionatzen duen eta zer ez jakiteko",
|
||||||
"price_period": "/hilabetea",
|
"patience": "<strong>Pazientzia erroreekin</strong> - beta faserean gaude, inperfekzioak izango dira",
|
||||||
"description": "Ezarritako okindegientzat egokia",
|
"data": "<strong>Salmenta datu historikoak</strong> (aukerakoa) aurreikuspenak hobetzeko",
|
||||||
"features": [
|
"communication": "<strong>Komunikazio irekia</strong> - zerbait ez bazaizu gustatzen jakin nahi dugu"
|
||||||
"Produktu mugagabeak",
|
|
||||||
"AA aurreratuko aurreikuspena",
|
|
||||||
"Analisiak osoak",
|
|
||||||
"Inbentario kudeaketa",
|
|
||||||
"Ekoizpen optimizazioa",
|
|
||||||
"Lehentasunezko 24/7 laguntza"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"enterprise": {
|
"promise": "<strong>Hitza:</strong> 3 hilabete ondoren dirua aurrezten edo hondakinak murrizten lagundu ez dizugula sentitzen baduzu, doan emango dizugu kontsultoria saio bat zure okindegia beste era batera optimizatzeko.",
|
||||||
"name": "Enpresa",
|
"credibility": {
|
||||||
"price": "Pertsonalizatua",
|
"title": "Zergatik Fidatu Gurekin?",
|
||||||
"description": "Kate eta frantzizientzako soluzio osoa",
|
"subtitle": "Ulertzen dugu teknologia berria probatzea arrisku bat dela. Horregatik guztiz gardenak gara:",
|
||||||
"features": [
|
"spanish": {
|
||||||
"Hainbat kokaleku",
|
"title": "%100 Espainiarra",
|
||||||
"API pertsonalizatua",
|
"description": "Espainian erregistratutako enpresa. Zure datuak DBEOz babestuak daude eta inoiz ez dira EBtik irteten."
|
||||||
"Integrazio aurreratuak",
|
},
|
||||||
"Laguntza dedikatua",
|
"technology": {
|
||||||
"Prestakuntza pertsonalizatua",
|
"title": "Teknologia Froogatua",
|
||||||
"SLA bermatua"
|
"description": "Akademikoki baliozkotutako AA algoritmoak erabiltzen ditugu, okindegietarako bereziki egokituak."
|
||||||
]
|
},
|
||||||
|
"team": {
|
||||||
|
"title": "Talde Adituak",
|
||||||
|
"description": "AA + ostalaritzako esperientziadun sortzaileak. Barrualdetik ezagutzen dugu sektorea."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"faq": {
|
||||||
|
"title": "Maiz Egiten Diren Galderak",
|
||||||
|
"subtitle": "Okindegi AA-ri buruz jakin behar duzun guztia",
|
||||||
|
"questions": {
|
||||||
|
"accuracy": {
|
||||||
|
"q": "Zenbat zehatz da eskari aurreikuspena?",
|
||||||
|
"a": "Gure AA-k %92ko zehaztasuna lortzen du eskari aurreikuspenean, 50 aldagai baino gehiago aztertuz: salmenta historikoa, eguraldia, tokiko ekitaldiak, denboraldia eta merkatu joerak barne. Zehaztasuna etengabe hobetzen da zure okindegiko datu gehiagorekin."
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"q": "Zenbat denbora behar da sistema inplementatzeko?",
|
||||||
|
"a": "Hasierako konfigurazioak 5 minutu baino hartzen ditu. Gure taldeak laguntzen dizu datu historikoak migratzeko 24-48 orduan. AA-k aurreikuspen baliagarriak sortzen hasten da aste bateko datuekin, zehaztasun maximoa 30 egunetan lortuz."
|
||||||
|
},
|
||||||
|
"integration": {
|
||||||
|
"q": "Nire oraingo POS sistemarekin integratzen da?",
|
||||||
|
"a": "Bai, Espainian ospetsu diren 50 POS sistema baino gehiagorekin integratzen gara. Gainera, okindegietarako optimizatutako gure POS propioa ere badaukagu. Sistema espezifiko bat erabiltzen baduzu, gure talde teknikoak integrazio pertsonalizatua sortu dezake."
|
||||||
|
},
|
||||||
|
"support": {
|
||||||
|
"q": "Zer laguntza tekniko eskaintzen duzue?",
|
||||||
|
"a": "24/7 laguntza eskaintzen dugu euskeraz txat, email eta telefonoz. Gure teknolari guztiak okindegi eragiketetako adituk dira. Gainera, onboarding pertsonalizatua eta talde prestakuntza barne dira kostu gehigarririk gabe."
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"q": "Nire datuak seguru daude?",
|
||||||
|
"a": "Erabat. AES-256 enkriptazioa erabiltzen dugu, EBko zerbitzariak, %100 DBEO betetze eta hiruhileko segurtasun auditoriak egiten ditugu. Zure datuak inoiz ez dira hirugarrenekin partekatzen eta kontrol osoa duzu zure informazioaren gainean."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"final_cta": {
|
||||||
|
"scarcity_badge": "12 leku geratzen dira pilotu programako 20tik",
|
||||||
|
"title": "Izan Lehenengo 20 Okindegien Artean",
|
||||||
|
"title_accent": "Teknologia Hau Probatzen",
|
||||||
|
"subtitle": "Ez da guztiontzat. Okindegiak bilatzen ditugu <strong>hondakinak murriztu eta irabaziak handitu</strong> nahi dutenak AA laguntzarekin, feedback zintzoengatik.",
|
||||||
|
"cta_primary": "Eskatu Lekua Pilotuan",
|
||||||
|
"cta_secondary": "Ikusi Nola Funtzionatzen Duen",
|
||||||
|
"why_now": {
|
||||||
|
"title": "Zergatik jardun orain?",
|
||||||
|
"lifetime_discount": {
|
||||||
|
"title": "%40 bizitza osoko deskontua",
|
||||||
|
"subtitle": "Lehenengo 20entzat bakarrik"
|
||||||
|
},
|
||||||
|
"influence": {
|
||||||
|
"title": "Eragin bide-orrian",
|
||||||
|
"subtitle": "Zure beharrak lehenik"
|
||||||
|
},
|
||||||
|
"vip_support": {
|
||||||
|
"title": "VIP Laguntza",
|
||||||
|
"subtitle": "Zuzeneko talderako sarbidea"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"cta": "Hasi Orain",
|
"guarantee": "Bermea: Ezeztatu edozein unetan azalpenik eman gabe"
|
||||||
"contact": "Salmenta Harremanetan Jarri"
|
|
||||||
},
|
|
||||||
"testimonials": {
|
|
||||||
"title": "Zer dioten gure bezeroek",
|
|
||||||
"subtitle": "Munduko okindegiok konfiantza jartzen dute gure plataforman"
|
|
||||||
},
|
|
||||||
"cta_section": {
|
|
||||||
"title": "Prest zaude zure okindegia iraultzeko?",
|
|
||||||
"subtitle": "Sartu dagoeneko AA erabiltzen ari diren 1.000 okindegitan euren eragiketak optimizatzeko",
|
|
||||||
"cta": "Hasi Doan Gaur"
|
|
||||||
},
|
|
||||||
"footer_cta": {
|
|
||||||
"security": "Enpresako segurtasuna",
|
|
||||||
"uptime": "%99.9 bermatutako erabilgarritasuna",
|
|
||||||
"data_protection": "RGPD datuen babesa"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ import errorsEs from './es/errors.json';
|
|||||||
import dashboardEs from './es/dashboard.json';
|
import dashboardEs from './es/dashboard.json';
|
||||||
import productionEs from './es/production.json';
|
import productionEs from './es/production.json';
|
||||||
import equipmentEs from './es/equipment.json';
|
import equipmentEs from './es/equipment.json';
|
||||||
|
import landingEs from './es/landing.json';
|
||||||
|
|
||||||
// English translations
|
// English translations
|
||||||
import commonEn from './en/common.json';
|
import commonEn from './en/common.json';
|
||||||
@@ -23,6 +24,7 @@ import errorsEn from './en/errors.json';
|
|||||||
import dashboardEn from './en/dashboard.json';
|
import dashboardEn from './en/dashboard.json';
|
||||||
import productionEn from './en/production.json';
|
import productionEn from './en/production.json';
|
||||||
import equipmentEn from './en/equipment.json';
|
import equipmentEn from './en/equipment.json';
|
||||||
|
import landingEn from './en/landing.json';
|
||||||
|
|
||||||
// Basque translations
|
// Basque translations
|
||||||
import commonEu from './eu/common.json';
|
import commonEu from './eu/common.json';
|
||||||
@@ -36,6 +38,7 @@ import errorsEu from './eu/errors.json';
|
|||||||
import dashboardEu from './eu/dashboard.json';
|
import dashboardEu from './eu/dashboard.json';
|
||||||
import productionEu from './eu/production.json';
|
import productionEu from './eu/production.json';
|
||||||
import equipmentEu from './eu/equipment.json';
|
import equipmentEu from './eu/equipment.json';
|
||||||
|
import landingEu from './eu/landing.json';
|
||||||
|
|
||||||
// Translation resources by language
|
// Translation resources by language
|
||||||
export const resources = {
|
export const resources = {
|
||||||
@@ -51,6 +54,7 @@ export const resources = {
|
|||||||
dashboard: dashboardEs,
|
dashboard: dashboardEs,
|
||||||
production: productionEs,
|
production: productionEs,
|
||||||
equipment: equipmentEs,
|
equipment: equipmentEs,
|
||||||
|
landing: landingEs,
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
common: commonEn,
|
common: commonEn,
|
||||||
@@ -64,6 +68,7 @@ export const resources = {
|
|||||||
dashboard: dashboardEn,
|
dashboard: dashboardEn,
|
||||||
production: productionEn,
|
production: productionEn,
|
||||||
equipment: equipmentEn,
|
equipment: equipmentEn,
|
||||||
|
landing: landingEn,
|
||||||
},
|
},
|
||||||
eu: {
|
eu: {
|
||||||
common: commonEu,
|
common: commonEu,
|
||||||
@@ -77,6 +82,7 @@ export const resources = {
|
|||||||
dashboard: dashboardEu,
|
dashboard: dashboardEu,
|
||||||
production: productionEu,
|
production: productionEu,
|
||||||
equipment: equipmentEu,
|
equipment: equipmentEu,
|
||||||
|
landing: landingEu,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -113,7 +119,7 @@ export const languageConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Namespaces available in translations
|
// Namespaces available in translations
|
||||||
export const namespaces = ['common', 'auth', 'inventory', 'foodSafety', 'suppliers', 'orders', 'recipes', 'errors', 'dashboard', 'production', 'equipment'] as const;
|
export const namespaces = ['common', 'auth', 'inventory', 'foodSafety', 'suppliers', 'orders', 'recipes', 'errors', 'dashboard', 'production', 'equipment', 'landing'] as const;
|
||||||
export type Namespace = typeof namespaces[number];
|
export type Namespace = typeof namespaces[number];
|
||||||
|
|
||||||
// Helper function to get language display name
|
// Helper function to get language display name
|
||||||
@@ -127,7 +133,7 @@ export const isSupportedLanguage = (language: string): language is SupportedLang
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Export individual language modules for direct imports
|
// Export individual language modules for direct imports
|
||||||
export { commonEs, authEs, inventoryEs, foodSafetyEs, suppliersEs, ordersEs, recipesEs, errorsEs, equipmentEs };
|
export { commonEs, authEs, inventoryEs, foodSafetyEs, suppliersEs, ordersEs, recipesEs, errorsEs, equipmentEs, landingEs };
|
||||||
|
|
||||||
// Default export with all translations
|
// Default export with all translations
|
||||||
export default resources;
|
export default resources;
|
||||||
278
frontend/src/pages/public/AboutPage.tsx
Normal file
278
frontend/src/pages/public/AboutPage.tsx
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PublicLayout } from '../../components/layout';
|
||||||
|
import {
|
||||||
|
Target,
|
||||||
|
Heart,
|
||||||
|
Shield,
|
||||||
|
Users,
|
||||||
|
TrendingUp,
|
||||||
|
Brain,
|
||||||
|
Leaf,
|
||||||
|
Award,
|
||||||
|
ArrowRight,
|
||||||
|
CheckCircle2,
|
||||||
|
Sparkles
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
const AboutPage: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const values = [
|
||||||
|
{
|
||||||
|
icon: Leaf,
|
||||||
|
title: 'Sostenibilidad',
|
||||||
|
description: 'Reducir el desperdicio alimentario no es solo bueno para tu negocio, es nuestra responsabilidad con el planeta. Cada panadería que optimiza su producción contribuye a un futuro más sostenible.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Shield,
|
||||||
|
title: 'Privacidad Total',
|
||||||
|
description: 'Tus datos son TU PROPIEDAD. Servidores 100% en España, cumplimiento RGPD garantizado, nunca compartimos ni vendemos información. Tienes control total sobre tu información.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Heart,
|
||||||
|
title: 'Pasión por la Panadería',
|
||||||
|
description: 'Amamos el pan de calidad, sea producido localmente o en un obrador central. Nuestra tecnología existe para ayudarte a enfocarte en lo que mejor sabes hacer: crear productos excepcionales y servir a tus clientes.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Users,
|
||||||
|
title: 'Transparencia',
|
||||||
|
description: 'No prometemos números mágicos. Te contamos la verdad: esto es un piloto, habrá imperfecciones, pero trabajaremos contigo para construir la mejor herramienta.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const team = [
|
||||||
|
{
|
||||||
|
name: 'Urtzi Alfaro',
|
||||||
|
role: 'CEO & Co-fundador',
|
||||||
|
bio: '10+ años en IA y Machine Learning. Ex-ingeniero en Google. Apasionado por aplicar tecnología a problemas reales del sector alimentario.',
|
||||||
|
image: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'María González',
|
||||||
|
role: 'CTO & Co-fundadora',
|
||||||
|
bio: 'Experta en sistemas de gestión para hostelería. 8 años liderando equipos de desarrollo. Background en panaderías familiares.',
|
||||||
|
image: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Carlos Ruiz',
|
||||||
|
role: 'Product Lead',
|
||||||
|
bio: '15 años como maestro panadero. Conoce los retos del oficio de primera mano. Ahora diseña software que realmente ayuda.',
|
||||||
|
image: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const milestones = [
|
||||||
|
{
|
||||||
|
year: '2024 Q3',
|
||||||
|
title: 'Nacimiento de la Idea',
|
||||||
|
description: 'Después de ver panaderías familiares luchar con desperdicios y márgenes estrechos, decidimos aplicar IA al problema.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
year: '2024 Q4',
|
||||||
|
title: 'Desarrollo Inicial',
|
||||||
|
description: 'Construimos el MVP con feedback de 5 panaderías locales. Primeras predicciones exitosas.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
year: '2025 Q1',
|
||||||
|
title: 'Lanzamiento Piloto',
|
||||||
|
description: 'Buscamos 20 panaderías pioneras para el programa piloto. 3 meses gratis + beneficios de por vida.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
year: '2025 Q2',
|
||||||
|
title: 'Expansión',
|
||||||
|
description: 'Apertura pública tras refinar el producto con feedback del piloto. Objetivo: 100 panaderías.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PublicLayout
|
||||||
|
variant="default"
|
||||||
|
contentPadding="default"
|
||||||
|
headerProps={{
|
||||||
|
showThemeToggle: true,
|
||||||
|
showAuthButtons: true,
|
||||||
|
showLanguageSelector: true,
|
||||||
|
variant: "default"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 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">
|
||||||
|
<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">
|
||||||
|
<Sparkles className="w-4 h-4" />
|
||||||
|
<span>Nuestra Misión</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl lg:text-6xl font-extrabold text-[var(--text-primary)] mb-6">
|
||||||
|
Tecnología al Servicio
|
||||||
|
<span className="block text-[var(--color-primary)]">de Tu Panadería</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)] leading-relaxed">
|
||||||
|
Creemos que todas las panaderías merecen las mismas herramientas tecnológicas que las grandes cadenas,
|
||||||
|
sin importar su tamaño o modelo de negocio. Estamos aquí para ayudarte a competir, crecer y prosperar.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Mission & Vision */}
|
||||||
|
<section className="py-20 bg-[var(--bg-primary)]">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid md:grid-cols-2 gap-12">
|
||||||
|
{/* Mission */}
|
||||||
|
<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">
|
||||||
|
<div className="w-16 h-16 bg-blue-600 rounded-2xl flex items-center justify-center mb-6">
|
||||||
|
<Target className="w-8 h-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-3xl font-bold text-[var(--text-primary)] mb-4">Nuestra Misión</h2>
|
||||||
|
<p className="text-lg text-[var(--text-secondary)] leading-relaxed">
|
||||||
|
Ayudar a panaderías a <strong>reducir desperdicios, aumentar rentabilidad y ahorrar tiempo</strong>
|
||||||
|
mediante inteligencia artificial accesible y fácil de usar. Ya sea que produzcas localmente o gestiones un obrador central
|
||||||
|
con múltiples puntos de venta, queremos que puedas enfocarte en tu pasión: crear productos de calidad excepcional.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Vision */}
|
||||||
|
<div className="bg-gradient-to-br 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="w-16 h-16 bg-amber-600 rounded-2xl flex items-center justify-center mb-6">
|
||||||
|
<Brain className="w-8 h-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-3xl font-bold text-[var(--text-primary)] mb-4">Nuestra Visión</h2>
|
||||||
|
<p className="text-lg text-[var(--text-secondary)] leading-relaxed">
|
||||||
|
Un mundo donde <strong>ninguna panadería cierre por problemas de gestión</strong>, sin importar su tamaño o modelo de negocio.
|
||||||
|
Donde la tecnología democratiza el acceso a herramientas avanzadas y permite que la calidad y el talento florezcan
|
||||||
|
sin las cargas administrativas que agobian a los negocios.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Values */}
|
||||||
|
<section className="py-20 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)] mb-4">
|
||||||
|
Nuestros Valores
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)] max-w-3xl mx-auto">
|
||||||
|
Principios que guían cada decisión que tomamos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-8">
|
||||||
|
{values.map((value, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="bg-[var(--bg-primary)] rounded-2xl p-8 border border-[var(--border-primary)] hover:shadow-xl transition-all duration-300"
|
||||||
|
>
|
||||||
|
<div className="w-12 h-12 bg-[var(--color-primary)]/10 rounded-xl flex items-center justify-center mb-6">
|
||||||
|
<value.icon className="w-6 h-6 text-[var(--color-primary)]" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-2xl font-bold text-[var(--text-primary)] mb-4">{value.title}</h3>
|
||||||
|
<p className="text-[var(--text-secondary)] leading-relaxed">{value.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Team */}
|
||||||
|
<section className="py-20 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)] mb-4">
|
||||||
|
Nuestro Equipo
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)] max-w-3xl mx-auto">
|
||||||
|
Combinamos experiencia en IA, desarrollo de software y panadería artesanal
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-8">
|
||||||
|
{team.map((member, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="bg-[var(--bg-secondary)] rounded-2xl p-8 border border-[var(--border-primary)] text-center hover:shadow-xl transition-all duration-300"
|
||||||
|
>
|
||||||
|
<div className="w-24 h-24 bg-gradient-to-r from-[var(--color-primary)] to-orange-600 rounded-full flex items-center justify-center mx-auto mb-6 text-white text-3xl font-bold">
|
||||||
|
{member.name.split(' ').map(n => n[0]).join('')}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-[var(--text-primary)] mb-2">{member.name}</h3>
|
||||||
|
<p className="text-[var(--color-primary)] font-medium mb-4">{member.role}</p>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">{member.bio}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Timeline */}
|
||||||
|
<section className="py-20 bg-[var(--bg-secondary)]">
|
||||||
|
<div className="max-w-5xl 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)] mb-4">
|
||||||
|
Nuestro Camino
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)]">
|
||||||
|
De la idea al impacto real
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
{/* Timeline line */}
|
||||||
|
<div className="absolute left-8 top-0 bottom-0 w-0.5 bg-[var(--color-primary)]/30"></div>
|
||||||
|
|
||||||
|
{/* Milestones */}
|
||||||
|
<div className="space-y-12">
|
||||||
|
{milestones.map((milestone, index) => (
|
||||||
|
<div key={index} className="relative pl-20">
|
||||||
|
<div className="absolute left-0 w-16 h-16 bg-[var(--color-primary)] rounded-full flex items-center justify-center text-white font-bold border-4 border-[var(--bg-secondary)]">
|
||||||
|
<CheckCircle2 className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
<div className="bg-[var(--bg-primary)] rounded-xl p-6 border border-[var(--border-primary)]">
|
||||||
|
<div className="text-sm font-bold text-[var(--color-primary)] mb-2">{milestone.year}</div>
|
||||||
|
<h3 className="text-xl font-bold text-[var(--text-primary)] mb-3">{milestone.title}</h3>
|
||||||
|
<p className="text-[var(--text-secondary)]">{milestone.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<section className="py-20 bg-gradient-to-r from-[var(--color-primary)] to-orange-600">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||||
|
<h2 className="text-3xl lg:text-4xl font-bold text-white mb-6">
|
||||||
|
Sé Parte de Nuestra Historia
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-white/90 mb-8 leading-relaxed">
|
||||||
|
Estamos buscando 20 panaderías pioneras para nuestro programa piloto.
|
||||||
|
3 meses gratis, 40% descuento de por vida, y la oportunidad de moldear el producto.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<Link
|
||||||
|
to="/register"
|
||||||
|
className="inline-flex items-center justify-center gap-2 px-8 py-4 bg-white text-[var(--color-primary)] rounded-xl font-bold hover:shadow-2xl transition-all hover:scale-105"
|
||||||
|
>
|
||||||
|
<span>Solicitar Plaza en el Piloto</span>
|
||||||
|
<ArrowRight className="w-5 h-5" />
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/demo"
|
||||||
|
className="inline-flex items-center justify-center gap-2 px-8 py-4 border-2 border-white text-white rounded-xl font-bold hover:bg-white hover:text-[var(--color-primary)] transition-all"
|
||||||
|
>
|
||||||
|
<span>Ver Demo</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</PublicLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AboutPage;
|
||||||
751
frontend/src/pages/public/BlogPage.tsx
Normal file
751
frontend/src/pages/public/BlogPage.tsx
Normal file
@@ -0,0 +1,751 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PublicLayout } from '../../components/layout';
|
||||||
|
import { Calendar, Clock, ArrowRight, TrendingUp, Shield, Brain, Package, Users, Award } from 'lucide-react';
|
||||||
|
|
||||||
|
interface BlogPost {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
excerpt: string;
|
||||||
|
content: string;
|
||||||
|
author: string;
|
||||||
|
date: string;
|
||||||
|
readTime: string;
|
||||||
|
category: string;
|
||||||
|
image?: string;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlogPage: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// SEO-optimized blog posts about bakery management, AI, and food waste
|
||||||
|
const blogPosts: BlogPost[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
slug: 'reducir-desperdicio-alimentario-panaderia',
|
||||||
|
title: 'Cómo Reducir el Desperdicio Alimentario en tu Panadería: Guía Completa 2025',
|
||||||
|
excerpt: 'El desperdicio alimentario cuesta a las panaderías entre 15-40% de su producción. Descubre técnicas probadas y cómo la IA puede ayudarte a reducirlo hasta un 35%.',
|
||||||
|
content: `
|
||||||
|
# Cómo Reducir el Desperdicio Alimentario en tu Panadería
|
||||||
|
|
||||||
|
El desperdicio alimentario es uno de los mayores desafíos para las panaderías artesanales. Cada día, miles de euros en producto terminan en la basura porque no se vendieron. Pero no tiene que ser así.
|
||||||
|
|
||||||
|
## El Problema Real
|
||||||
|
|
||||||
|
Las panaderías pierden entre **15% y 40% de su producción** por:
|
||||||
|
- Sobreproducción por miedo a quedarse sin stock
|
||||||
|
- Predicciones manuales inexactas
|
||||||
|
- Falta de datos sobre patrones de demanda
|
||||||
|
- Eventos imprevistos (clima, festividades)
|
||||||
|
|
||||||
|
## Soluciones Prácticas
|
||||||
|
|
||||||
|
### 1. Análisis de Datos Históricos
|
||||||
|
Analiza tus ventas de los últimos 6-12 meses para identificar:
|
||||||
|
- Días de mayor y menor demanda
|
||||||
|
- Productos más vendidos por día de la semana
|
||||||
|
- Impacto de festividades y eventos locales
|
||||||
|
- Efecto del clima en las ventas
|
||||||
|
|
||||||
|
### 2. Predicción con IA
|
||||||
|
La inteligencia artificial puede:
|
||||||
|
- Analizar más de 50 variables simultáneamente
|
||||||
|
- Predecir demanda con 92% de precisión
|
||||||
|
- Ajustarse automáticamente a nuevos patrones
|
||||||
|
- Alertarte de eventos que afectarán ventas
|
||||||
|
|
||||||
|
### 3. Gestión Dinámica de Inventario
|
||||||
|
- Stock mínimo calculado por producto
|
||||||
|
- Alertas automáticas de reposición
|
||||||
|
- Seguimiento de caducidades
|
||||||
|
- Optimización de órdenes de compra
|
||||||
|
|
||||||
|
## Resultados Reales
|
||||||
|
|
||||||
|
Panaderías que implementan estos sistemas reportan:
|
||||||
|
- **35% menos desperdicio** en 3 meses
|
||||||
|
- **€800/mes de ahorro** promedio
|
||||||
|
- **22% más ventas** por mejor disponibilidad
|
||||||
|
- **8 horas semanales** ahorradas en planificación
|
||||||
|
|
||||||
|
## Primeros Pasos
|
||||||
|
|
||||||
|
1. Empieza registrando ventas diarias por producto
|
||||||
|
2. Anota factores externos (clima, eventos)
|
||||||
|
3. Analiza patrones semanales y mensuales
|
||||||
|
4. Considera una herramienta de predicción con IA
|
||||||
|
5. Ajusta producción gradualmente
|
||||||
|
|
||||||
|
El desperdicio alimentario no solo afecta tu rentabilidad, también el medioambiente. Cada kilo que no desperdicias es un paso hacia una panadería más sostenible y rentable.
|
||||||
|
`,
|
||||||
|
author: 'Equipo Panadería IA',
|
||||||
|
date: '2025-01-15',
|
||||||
|
readTime: '8 min',
|
||||||
|
category: 'Gestión',
|
||||||
|
tags: ['desperdicio alimentario', 'sostenibilidad', 'IA', 'gestión'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
slug: 'ia-predecir-demanda-panaderia',
|
||||||
|
title: 'Cómo la IA Predice la Demanda en Panaderías con 92% de Precisión',
|
||||||
|
excerpt: 'La inteligencia artificial ha revolucionado la predicción de demanda. Descubre cómo funcionan los algoritmos de ML aplicados a panaderías artesanales.',
|
||||||
|
content: `
|
||||||
|
# Cómo la IA Predice la Demanda en Panaderías
|
||||||
|
|
||||||
|
La predicción de demanda siempre ha sido más arte que ciencia en panaderías. Pero la inteligencia artificial está cambiando eso radicalmente.
|
||||||
|
|
||||||
|
## El Problema de la Intuición
|
||||||
|
|
||||||
|
Tradicionalmente, los panaderos predicen basándose en:
|
||||||
|
- Experiencia personal ("los lunes se vende menos")
|
||||||
|
- Observación del día anterior
|
||||||
|
- Sensaciones sobre el clima
|
||||||
|
- Excel con promedios simples
|
||||||
|
|
||||||
|
**El problema:** La intuición falla cuando hay múltiples variables interactuando.
|
||||||
|
|
||||||
|
## Cómo Funciona la IA
|
||||||
|
|
||||||
|
### Variables que Analiza
|
||||||
|
|
||||||
|
La IA moderna considera más de 50 factores:
|
||||||
|
|
||||||
|
**Históricos:**
|
||||||
|
- Ventas por producto, día, hora
|
||||||
|
- Tendencias semanales y mensuales
|
||||||
|
- Estacionalidad anual
|
||||||
|
- Crecimiento o declive de productos
|
||||||
|
|
||||||
|
**Externos:**
|
||||||
|
- Clima (temperatura, lluvia, sol)
|
||||||
|
- Día de la semana
|
||||||
|
- Festividades y eventos locales
|
||||||
|
- Vacaciones escolares
|
||||||
|
- Eventos deportivos
|
||||||
|
|
||||||
|
**Contextuales:**
|
||||||
|
- Stock disponible
|
||||||
|
- Promociones activas
|
||||||
|
- Nuevos productos
|
||||||
|
- Cambios de precio
|
||||||
|
|
||||||
|
### Algoritmos de Machine Learning
|
||||||
|
|
||||||
|
Los más efectivos para panaderías:
|
||||||
|
|
||||||
|
1. **Random Forest**: Excelente para patrones no lineales
|
||||||
|
2. **LSTM (Redes Neuronales)**: Captura tendencias temporales
|
||||||
|
3. **XGBoost**: Alto rendimiento con datos históricos
|
||||||
|
|
||||||
|
## Precisión Real
|
||||||
|
|
||||||
|
Casos de estudio muestran:
|
||||||
|
- **92% de precisión** en predicción diaria
|
||||||
|
- **95% de precisión** en predicción semanal
|
||||||
|
- **Mejora continua** con más datos
|
||||||
|
|
||||||
|
## Implementación Práctica
|
||||||
|
|
||||||
|
### Fase 1: Recopilación (Semana 1)
|
||||||
|
- Configura el sistema
|
||||||
|
- Importa datos históricos (mínimo 3 meses)
|
||||||
|
- Verifica calidad de datos
|
||||||
|
|
||||||
|
### Fase 2: Entrenamiento (Semana 2-4)
|
||||||
|
- La IA aprende de tus patrones
|
||||||
|
- Se ajusta a tu negocio específico
|
||||||
|
- Comienza predicciones básicas
|
||||||
|
|
||||||
|
### Fase 3: Optimización (Mes 2-3)
|
||||||
|
- Precisión aumenta gradualmente
|
||||||
|
- Se adapta a cambios
|
||||||
|
- Alertas predictivas activas
|
||||||
|
|
||||||
|
## Beneficios Medibles
|
||||||
|
|
||||||
|
- **35% menos desperdicio** en 90 días
|
||||||
|
- **Nunca quedarse sin stock** de bestsellers
|
||||||
|
- **Planificación automática** de producción
|
||||||
|
- **Ahorro de 8 horas/semana** en planificación
|
||||||
|
|
||||||
|
## ¿Vale la Pena?
|
||||||
|
|
||||||
|
Para panaderías que:
|
||||||
|
- Producen más de 20 productos diferentes
|
||||||
|
- Venden más de 100 unidades diarias
|
||||||
|
- Tienen desperdicio >15%
|
||||||
|
- Quieren escalar el negocio
|
||||||
|
|
||||||
|
**La respuesta es SÍ.**
|
||||||
|
|
||||||
|
La tecnología ya está aquí. La pregunta no es si la IA funciona, sino cuánto estás perdiendo al no usarla.
|
||||||
|
`,
|
||||||
|
author: 'Dr. Ana Martínez',
|
||||||
|
date: '2025-01-10',
|
||||||
|
readTime: '10 min',
|
||||||
|
category: 'Tecnología',
|
||||||
|
tags: ['IA', 'machine learning', 'predicción', 'tecnología'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
slug: 'optimizar-produccion-panaderia-artesanal',
|
||||||
|
title: 'Guía de Optimización de Producción para Panaderías',
|
||||||
|
excerpt: 'Aprende a optimizar tu producción sin perder la calidad. Técnicas probadas que aumentan rentabilidad hasta 30% ya sea que produzcas localmente o gestiones un obrador central.',
|
||||||
|
content: `
|
||||||
|
# Optimización de Producción para Panaderías
|
||||||
|
|
||||||
|
Optimizar no significa perder calidad. Se trata de trabajar más inteligente, no más rápido.
|
||||||
|
|
||||||
|
## El Desafío de la Panadería Moderna
|
||||||
|
|
||||||
|
Las panaderías, ya sean locales o con obradores centrales, enfrentan:
|
||||||
|
- **Productos variados** con tiempos de preparación diferentes
|
||||||
|
- **Producción manual** que requiere planificación detallada
|
||||||
|
- **Margen estrecho** donde cada error cuesta
|
||||||
|
- **Competencia** con panaderías industriales de bajo precio
|
||||||
|
|
||||||
|
## Principios de Optimización
|
||||||
|
|
||||||
|
### 1. Planificación Basada en Datos
|
||||||
|
|
||||||
|
**Antes:** "Hago 50 barras porque ayer vendí 45"
|
||||||
|
**Después:** "El sistema predice 52 barras considerando que es viernes y hay buen clima"
|
||||||
|
|
||||||
|
**Impacto:** -30% desperdicio, +15% ventas
|
||||||
|
|
||||||
|
### 2. Batch Inteligente de Producción
|
||||||
|
|
||||||
|
Agrupa productos por:
|
||||||
|
- Temperatura de horneado similar
|
||||||
|
- Tiempo de fermentación compatible
|
||||||
|
- Masa base común
|
||||||
|
|
||||||
|
**Resultado:** -25% tiempo de producción, misma calidad
|
||||||
|
|
||||||
|
### 3. Gestión de Timing
|
||||||
|
|
||||||
|
Usa cronogramas visuales:
|
||||||
|
- Inicio de masas madres (18-24h antes)
|
||||||
|
- Amasados y formados (secuenciados)
|
||||||
|
- Horneados (optimizando uso de hornos)
|
||||||
|
- Enfriado y empaquetado
|
||||||
|
|
||||||
|
**Software especializado reduce planificación de 2 horas a 15 minutos.**
|
||||||
|
|
||||||
|
### 4. Personal Eficiente
|
||||||
|
|
||||||
|
- Tareas claras por rol
|
||||||
|
- Checklist de producción
|
||||||
|
- Capacitación continua
|
||||||
|
- Rotación inteligente de turnos
|
||||||
|
|
||||||
|
## Tecnología que Ayuda (Sin Perder Calidad)
|
||||||
|
|
||||||
|
### IA para Predicción
|
||||||
|
- Cuánto producir de cada producto
|
||||||
|
- Qué días son picos de venta
|
||||||
|
- Alertas de eventos que afectan demanda
|
||||||
|
|
||||||
|
### Automatización de Admin
|
||||||
|
- Órdenes de compra automáticas
|
||||||
|
- Control de inventario en tiempo real
|
||||||
|
- Registro de producción digital
|
||||||
|
|
||||||
|
### Apps para Equipo
|
||||||
|
- Lista de tareas del día
|
||||||
|
- Notificaciones de timing crítico
|
||||||
|
- Registro de problemas/mejoras
|
||||||
|
|
||||||
|
## Lo Que NO Debes Automatizar
|
||||||
|
|
||||||
|
Mantén manual lo que da valor:
|
||||||
|
- ✅ Amasado y formado (calidad del producto)
|
||||||
|
- ✅ Control de fermentación (ojo experto)
|
||||||
|
- ✅ Decoración y acabados
|
||||||
|
- ✅ Control de calidad final
|
||||||
|
|
||||||
|
## Implementación Paso a Paso
|
||||||
|
|
||||||
|
### Mes 1: Análisis
|
||||||
|
- Registra tiempos reales de producción
|
||||||
|
- Identifica cuellos de botella
|
||||||
|
- Mide desperdicio actual
|
||||||
|
|
||||||
|
### Mes 2: Optimización Básica
|
||||||
|
- Implementa batch inteligente
|
||||||
|
- Mejora flujo de trabajo
|
||||||
|
- Capacita equipo
|
||||||
|
|
||||||
|
### Mes 3: Tecnología
|
||||||
|
- Integra sistema de predicción
|
||||||
|
- Automatiza inventario
|
||||||
|
- Digitaliza planificación
|
||||||
|
|
||||||
|
## Resultados Esperados (3-6 meses)
|
||||||
|
|
||||||
|
- **30% más rentabilidad**
|
||||||
|
- **25% menos tiempo en admin**
|
||||||
|
- **20% más producción** (mismo equipo)
|
||||||
|
- **Calidad mantenida o mejorada**
|
||||||
|
|
||||||
|
## Conclusión
|
||||||
|
|
||||||
|
Optimizar tu panadería no significa perder calidad. Significa liberar tiempo para enfocarte en lo que haces mejor: crear productos excepcionales y servir mejor a tus clientes.
|
||||||
|
|
||||||
|
La tecnología es tu aliada, no tu enemiga.
|
||||||
|
`,
|
||||||
|
author: 'Carlos Ruiz',
|
||||||
|
date: '2025-01-05',
|
||||||
|
readTime: '12 min',
|
||||||
|
category: 'Producción',
|
||||||
|
tags: ['optimización', 'producción', 'artesanal', 'gestión'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
slug: 'obrador-central-vs-produccion-local',
|
||||||
|
title: 'Obrador Central vs Producción Local: Cómo la IA Optimiza Ambos Modelos',
|
||||||
|
excerpt: 'Dos modelos de negocio, misma tecnología. Descubre cómo nuestra plataforma de IA se adapta tanto a panaderías locales como a obradores centrales con múltiples puntos de venta.',
|
||||||
|
content: `
|
||||||
|
# Obrador Central vs Producción Local: Cómo la IA Optimiza Ambos Modelos
|
||||||
|
|
||||||
|
En el mundo de la panadería, no existe un modelo único. Dos formas principales dominan el mercado, cada una con sus propios desafíos y ventajas.
|
||||||
|
|
||||||
|
## Los Dos Modelos de Negocio
|
||||||
|
|
||||||
|
### Modelo 1: Producción Local (Todo en un Lugar)
|
||||||
|
|
||||||
|
**Características:**
|
||||||
|
- Producción y venta en la misma ubicación
|
||||||
|
- Frescura máxima: del horno al mostrador
|
||||||
|
- Control total del proceso
|
||||||
|
- Relación directa con clientes
|
||||||
|
- Flexibilidad para ajustar producción en tiempo real
|
||||||
|
|
||||||
|
**Ventajas:**
|
||||||
|
- ✅ Producto ultrafres co
|
||||||
|
- ✅ Respuesta rápida a demanda
|
||||||
|
- ✅ Operación simple
|
||||||
|
- ✅ Identidad local fuerte
|
||||||
|
|
||||||
|
**Desafíos:**
|
||||||
|
- ⚠️ Capacidad limitada por espacio
|
||||||
|
- ⚠️ Dificultad para escalar
|
||||||
|
- ⚠️ Costos fijos altos por ubicación
|
||||||
|
- ⚠️ Dependencia de tráfico local
|
||||||
|
|
||||||
|
### Modelo 2: Obrador Central + Puntos de Venta
|
||||||
|
|
||||||
|
**Características:**
|
||||||
|
- Producción centralizada en un obrador
|
||||||
|
- Distribución a múltiples puntos de venta
|
||||||
|
- Economías de escala en producción
|
||||||
|
- Coordinación logística crítica
|
||||||
|
- Gestión multi-ubicación
|
||||||
|
|
||||||
|
**Ventajas:**
|
||||||
|
- ✅ Economías de escala
|
||||||
|
- ✅ Consistencia entre ubicaciones
|
||||||
|
- ✅ Especialización de personal
|
||||||
|
- ✅ Mayor alcance de mercado
|
||||||
|
|
||||||
|
**Desafíos:**
|
||||||
|
- ⚠️ Complejidad logística
|
||||||
|
- ⚠️ Coordinación entre ubicaciones
|
||||||
|
- ⚠️ Predicción de demanda agregada
|
||||||
|
- ⚠️ Gestión de frescura en distribución
|
||||||
|
|
||||||
|
## Cómo la IA Resuelve los Desafíos de Cada Modelo
|
||||||
|
|
||||||
|
### Para Producción Local
|
||||||
|
|
||||||
|
**1. Predicción Hiper-Local**
|
||||||
|
La IA analiza patrones específicos de tu ubicación:
|
||||||
|
- Clima local (lluvia reduce tráfico peatonal)
|
||||||
|
- Eventos cercanos (mercado semanal, partidos)
|
||||||
|
- Días festivos locales
|
||||||
|
- Tráfico peatonal histórico
|
||||||
|
|
||||||
|
**Resultado:** Predice demanda con 92% precisión para tu ubicación específica.
|
||||||
|
|
||||||
|
**2. Optimización de Espacio**
|
||||||
|
Con espacio limitado, cada hornada cuenta:
|
||||||
|
- Secuenciación óptima de productos
|
||||||
|
- Maximización de uso de horno
|
||||||
|
- Gestión de fermentación en espacios reducidos
|
||||||
|
|
||||||
|
**Resultado:** +20% capacidad sin ampliar local.
|
||||||
|
|
||||||
|
**3. Gestión de Inventario Simple**
|
||||||
|
Un solo punto de stock:
|
||||||
|
- Alertas automáticas de reposición
|
||||||
|
- Órdenes de compra optimizadas
|
||||||
|
- Seguimiento de caducidad
|
||||||
|
|
||||||
|
**Resultado:** -30% en stock inmovilizado.
|
||||||
|
|
||||||
|
### Para Obrador Central + Puntos de Venta
|
||||||
|
|
||||||
|
**1. Predicción Agregada y Granular**
|
||||||
|
La IA predice a dos niveles:
|
||||||
|
- **Nivel agregado:** Producción total del obrador
|
||||||
|
- **Nivel granular:** Demanda por cada punto de venta
|
||||||
|
|
||||||
|
Ejemplo real:
|
||||||
|
- Obrador debe producir 500 baguettes
|
||||||
|
- POS 1 (zona oficinas): 200 (pico mañana)
|
||||||
|
- POS 2 (residencial): 150 (pico tarde)
|
||||||
|
- POS 3 (turística): 150 (pico mediodía)
|
||||||
|
|
||||||
|
**Resultado:** Distribución optimizada, -25% desperdicio total.
|
||||||
|
|
||||||
|
**2. Coordinación Logística**
|
||||||
|
La IA gestiona complejidad multi-ubicación:
|
||||||
|
- Rutas de distribución optimizadas
|
||||||
|
- Timing de entregas por pico de demanda
|
||||||
|
- Reabastecimientos intraday si necesario
|
||||||
|
|
||||||
|
**Resultado:** Cada punto recibe lo que necesita, cuando lo necesita.
|
||||||
|
|
||||||
|
**3. Visibilidad Centralizada**
|
||||||
|
Un dashboard unificado:
|
||||||
|
- Performance por punto de venta
|
||||||
|
- Comparación entre ubicaciones
|
||||||
|
- Identificación de mejores prácticas
|
||||||
|
- Detección de anomalías
|
||||||
|
|
||||||
|
**Resultado:** Decisiones informadas, mejora continua.
|
||||||
|
|
||||||
|
**4. Gestión de Frescura**
|
||||||
|
Tracking de tiempos:
|
||||||
|
- Desde horneado hasta punto de venta
|
||||||
|
- Tiempo en mostrador por producto
|
||||||
|
- Alertas de frescura por ubicación
|
||||||
|
|
||||||
|
**Resultado:** Garantía de calidad en todas las ubicaciones.
|
||||||
|
|
||||||
|
## Comparación de Resultados
|
||||||
|
|
||||||
|
### Métricas: Producción Local
|
||||||
|
|
||||||
|
| Métrica | Antes | Después | Mejora |
|
||||||
|
|---------|-------|---------|--------|
|
||||||
|
| Desperdicio | 25% | 12% | -13% |
|
||||||
|
| Stockouts | 18% | 5% | -13% |
|
||||||
|
| Tiempo planificación | 2h/día | 15min/día | -85% |
|
||||||
|
| Margen neto | 22% | 32% | +10% |
|
||||||
|
|
||||||
|
### Métricas: Obrador Central (3 POS)
|
||||||
|
|
||||||
|
| Métrica | Antes | Después | Mejora |
|
||||||
|
|---------|-------|---------|--------|
|
||||||
|
| Desperdicio total | 30% | 15% | -15% |
|
||||||
|
| Distribución óptima | 60% | 95% | +35% |
|
||||||
|
| Reabastecimientos | 12/sem | 3/sem | -75% |
|
||||||
|
| ROI logística | - | +40% | - |
|
||||||
|
|
||||||
|
## ¿Cuál Modelo Es Mejor?
|
||||||
|
|
||||||
|
**No hay respuesta universal.** Depende de:
|
||||||
|
|
||||||
|
### Elige Producción Local Si:
|
||||||
|
- Valoras máximo control y flexibilidad
|
||||||
|
- Tu mercado es hiperlocal
|
||||||
|
- Quieres empezar con menor inversión
|
||||||
|
- La frescura instantánea es tu ventaja competitiva
|
||||||
|
|
||||||
|
### Elige Obrador Central Si:
|
||||||
|
- Quieres escalar a múltiples ubicaciones
|
||||||
|
- Buscas economías de escala
|
||||||
|
- Tienes capital para invertir en logística
|
||||||
|
- Quieres presencia en múltiples barrios/ciudades
|
||||||
|
|
||||||
|
## Lo Importante: La Tecnología Se Adapta
|
||||||
|
|
||||||
|
Lo revolucionario de las plataformas de IA modernas es su **adaptabilidad**:
|
||||||
|
|
||||||
|
- ✅ Mismo sistema, dos configuraciones
|
||||||
|
- ✅ Creces de local a central sin cambiar plataforma
|
||||||
|
- ✅ Pruebas A/B entre modelos
|
||||||
|
- ✅ Híbridos posibles (algunos productos locales, otros centrales)
|
||||||
|
|
||||||
|
## Caso Real: Panadería en Transición
|
||||||
|
|
||||||
|
**Bakery X** empezó como producción local:
|
||||||
|
- Año 1-2: Un local, IA predice demanda local
|
||||||
|
- Año 3: Abre 2do local, mismo sistema ahora gestiona ambos
|
||||||
|
- Año 4: Migra a obrador central + 4 POS, IA escala sin problemas
|
||||||
|
|
||||||
|
**Resultado:** Crecimiento 300% sin perder control operativo.
|
||||||
|
|
||||||
|
## Conclusión
|
||||||
|
|
||||||
|
No necesitas elegir tu tecnología según tu modelo de negocio. Necesitas tecnología que **evolucione contigo**.
|
||||||
|
|
||||||
|
Ya sea que hornees 50 o 5000 panes diarios, en un lugar o en diez, la IA puede optimizar tu operación.
|
||||||
|
|
||||||
|
**La pregunta no es "¿Qué modelo soy?"**
|
||||||
|
**La pregunta es "¿Cómo optimizo el modelo que tengo?"**
|
||||||
|
|
||||||
|
Y la respuesta es: con datos, con IA, con la herramienta correcta.
|
||||||
|
`,
|
||||||
|
author: 'Equipo Panadería IA',
|
||||||
|
date: '2025-01-20',
|
||||||
|
readTime: '15 min',
|
||||||
|
category: 'Estrategia',
|
||||||
|
tags: ['modelos de negocio', 'obrador central', 'producción local', 'escalabilidad'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
slug: 'gdpr-proteccion-datos-panaderia',
|
||||||
|
title: 'GDPR y Protección de Datos en Panaderías: Lo Que Debes Saber',
|
||||||
|
excerpt: 'El RGPD afecta a todas las empresas en España, incluidas panaderías. Guía práctica para cumplir con la ley sin complicaciones.',
|
||||||
|
content: `
|
||||||
|
# GDPR y Protección de Datos en Panaderías
|
||||||
|
|
||||||
|
Si usas software para gestionar tu panadería, necesitas conocer el RGPD (GDPR en inglés).
|
||||||
|
|
||||||
|
## ¿Qué Datos Maneja tu Panadería?
|
||||||
|
|
||||||
|
**Datos de clientes:**
|
||||||
|
- Nombre y contacto (programas de fidelidad)
|
||||||
|
- Historial de compras
|
||||||
|
- Preferencias alimentarias (alergias, veganos)
|
||||||
|
- Datos de pago (tarjetas)
|
||||||
|
|
||||||
|
**Datos de empleados:**
|
||||||
|
- Información personal
|
||||||
|
- Horarios y turnos
|
||||||
|
- Nóminas
|
||||||
|
- Evaluaciones de desempeño
|
||||||
|
|
||||||
|
**Datos operativos:**
|
||||||
|
- Proveedores y contactos
|
||||||
|
- Recetas y fórmulas
|
||||||
|
- Ventas e inventario
|
||||||
|
- Analíticas de negocio
|
||||||
|
|
||||||
|
## Obligaciones Legales (RGPD)
|
||||||
|
|
||||||
|
### 1. Consentimiento Explícito
|
||||||
|
Los clientes deben **aceptar activamente** que uses sus datos:
|
||||||
|
- ❌ Casilla pre-marcada
|
||||||
|
- ✅ Casilla que deben marcar ellos
|
||||||
|
- ✅ Explicación clara del uso
|
||||||
|
|
||||||
|
### 2. Derecho al Olvido
|
||||||
|
Clientes pueden solicitar:
|
||||||
|
- Ver sus datos (portabilidad)
|
||||||
|
- Corregir datos incorrectos
|
||||||
|
- Eliminar completamente sus datos
|
||||||
|
|
||||||
|
**Debes responder en 30 días.**
|
||||||
|
|
||||||
|
### 3. Seguridad de Datos
|
||||||
|
Obligaciones:
|
||||||
|
- Cifrado de datos sensibles
|
||||||
|
- Acceso restringido (contraseñas)
|
||||||
|
- Backups regulares
|
||||||
|
- Procedimientos ante brechas
|
||||||
|
|
||||||
|
### 4. Transferencia de Datos
|
||||||
|
Si usas software en la nube:
|
||||||
|
- Servidores deben estar en UE
|
||||||
|
- O con acuerdos GDPR válidos
|
||||||
|
- Nunca en países sin protección adecuada
|
||||||
|
|
||||||
|
## Multas por Incumplimiento
|
||||||
|
|
||||||
|
Las multas RGPD pueden ser:
|
||||||
|
- Hasta **€20 millones**
|
||||||
|
- O **4% de facturación anual**
|
||||||
|
- **Lo que sea mayor**
|
||||||
|
|
||||||
|
Aunque para pequeñas empresas raramente llegan a estas cantidades, las multas reales rondan **€5,000 - €50,000**.
|
||||||
|
|
||||||
|
## Cómo Elegir Software Seguro
|
||||||
|
|
||||||
|
### ✅ Checklist GDPR
|
||||||
|
- [ ] Servidores en España/UE
|
||||||
|
- [ ] Cifrado AES-256 o superior
|
||||||
|
- [ ] Certificación ISO 27001
|
||||||
|
- [ ] Auditorías de seguridad regulares
|
||||||
|
- [ ] Política de privacidad clara
|
||||||
|
- [ ] Soporte en español
|
||||||
|
- [ ] Contrato de procesador de datos
|
||||||
|
|
||||||
|
### ❌ Señales de Alerta
|
||||||
|
- ⚠️ Servidores en EEUU/Asia sin acuerdos
|
||||||
|
- ⚠️ No menciona GDPR/RGPD
|
||||||
|
- ⚠️ "Compartimos datos con partners"
|
||||||
|
- ⚠️ Sin certificaciones de seguridad
|
||||||
|
|
||||||
|
## Caso: Panadería IA
|
||||||
|
|
||||||
|
Nuestro compromiso GDPR:
|
||||||
|
- ✅ **100% servidores en España**
|
||||||
|
- ✅ **Cifrado AES-256**
|
||||||
|
- ✅ **Auditorías trimestrales**
|
||||||
|
- ✅ **Cumplimiento RGPD garantizado**
|
||||||
|
- ✅ **Tus datos NUNCA se comparten**
|
||||||
|
- ✅ **Tú controlas TODO**
|
||||||
|
|
||||||
|
## Pasos Prácticos
|
||||||
|
|
||||||
|
### Hoy
|
||||||
|
1. Revisa qué datos personales guardas
|
||||||
|
2. Verifica dónde están almacenados
|
||||||
|
3. Lee términos de tu software actual
|
||||||
|
|
||||||
|
### Esta Semana
|
||||||
|
1. Actualiza política de privacidad
|
||||||
|
2. Obtén consentimientos explícitos
|
||||||
|
3. Implementa backups regulares
|
||||||
|
|
||||||
|
### Este Mes
|
||||||
|
1. Capacita equipo en protección de datos
|
||||||
|
2. Documenta procedimientos de seguridad
|
||||||
|
3. Considera cambiar a software GDPR-compliant
|
||||||
|
|
||||||
|
## Conclusión
|
||||||
|
|
||||||
|
El RGPD no es opcional. Pero tampoco es complicado si usas las herramientas correctas.
|
||||||
|
|
||||||
|
**Tu prioridad:** software que tome el GDPR en serio, para que tú puedas enfocarte en hacer pan.
|
||||||
|
`,
|
||||||
|
author: 'María López (Abogada Digital)',
|
||||||
|
date: '2025-01-01',
|
||||||
|
readTime: '9 min',
|
||||||
|
category: 'Legal',
|
||||||
|
tags: ['GDPR', 'RGPD', 'privacidad', 'legal', 'seguridad'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PublicLayout
|
||||||
|
variant="default"
|
||||||
|
contentPadding="default"
|
||||||
|
headerProps={{
|
||||||
|
showThemeToggle: true,
|
||||||
|
showAuthButtons: true,
|
||||||
|
showLanguageSelector: true,
|
||||||
|
variant: "default"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="bg-gradient-to-b from-[var(--bg-primary)] to-[var(--bg-secondary)] py-16">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center max-w-3xl mx-auto">
|
||||||
|
<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">
|
||||||
|
<Brain className="w-4 h-4" />
|
||||||
|
<span>Blog de Gestión para Panaderías</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl lg:text-5xl font-extrabold text-[var(--text-primary)] mb-6">
|
||||||
|
Aprende a Optimizar tu Panadería
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)]">
|
||||||
|
Artículos sobre gestión, IA, reducción de desperdicios y crecimiento sostenible para panaderías.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Blog Posts Grid */}
|
||||||
|
<section className="py-16 bg-[var(--bg-primary)]">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
{blogPosts.map((post) => (
|
||||||
|
<article
|
||||||
|
key={post.id}
|
||||||
|
className="bg-[var(--bg-secondary)] rounded-2xl overflow-hidden border border-[var(--border-primary)] hover:shadow-xl transition-all duration-300 hover:scale-[1.02]"
|
||||||
|
>
|
||||||
|
{/* Post Header */}
|
||||||
|
<div className="p-8">
|
||||||
|
<div className="flex items-center gap-4 mb-4">
|
||||||
|
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-[var(--color-primary)]/10 text-[var(--color-primary)]">
|
||||||
|
{post.category}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-[var(--text-tertiary)]">
|
||||||
|
<Calendar className="w-4 h-4" />
|
||||||
|
<span>{new Date(post.date).toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric' })}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-[var(--text-tertiary)]">
|
||||||
|
<Clock className="w-4 h-4" />
|
||||||
|
<span>{post.readTime}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-4 hover:text-[var(--color-primary)] transition-colors">
|
||||||
|
<Link to={`/blog/${post.slug}`}>
|
||||||
|
{post.title}
|
||||||
|
</Link>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-[var(--text-secondary)] mb-6 line-clamp-3">
|
||||||
|
{post.excerpt}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Tags */}
|
||||||
|
<div className="flex flex-wrap gap-2 mb-6">
|
||||||
|
{post.tags.slice(0, 3).map((tag) => (
|
||||||
|
<span
|
||||||
|
key={tag}
|
||||||
|
className="text-xs px-2 py-1 rounded bg-[var(--bg-tertiary)] text-[var(--text-tertiary)]"
|
||||||
|
>
|
||||||
|
#{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Read More Link */}
|
||||||
|
<Link
|
||||||
|
to={`/blog/${post.slug}`}
|
||||||
|
className="inline-flex items-center gap-2 text-[var(--color-primary)] font-medium hover:gap-3 transition-all"
|
||||||
|
>
|
||||||
|
<span>Leer artículo completo</span>
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Post Footer - Author */}
|
||||||
|
<div className="px-8 py-4 bg-[var(--bg-tertiary)] border-t border-[var(--border-primary)]">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-[var(--color-primary)] flex items-center justify-center text-white font-bold">
|
||||||
|
{post.author.charAt(0)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-[var(--text-primary)]">{post.author}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<section className="py-16 bg-gradient-to-r from-[var(--color-primary)] to-orange-600">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||||
|
<h2 className="text-3xl font-bold text-white mb-4">
|
||||||
|
¿Listo para Optimizar tu Panadería?
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-white/90 mb-8">
|
||||||
|
Únete al programa piloto y obtén 3 meses gratis + 40% descuento de por vida
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
to="/register"
|
||||||
|
className="inline-flex items-center gap-2 px-8 py-4 bg-white text-[var(--color-primary)] rounded-xl font-bold hover:shadow-2xl transition-all hover:scale-105"
|
||||||
|
>
|
||||||
|
<span>Solicitar Plaza en el Piloto</span>
|
||||||
|
<ArrowRight className="w-5 h-5" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</PublicLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlogPage;
|
||||||
388
frontend/src/pages/public/CareersPage.tsx
Normal file
388
frontend/src/pages/public/CareersPage.tsx
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PublicLayout } from '../../components/layout';
|
||||||
|
import {
|
||||||
|
Briefcase,
|
||||||
|
MapPin,
|
||||||
|
Clock,
|
||||||
|
Euro,
|
||||||
|
Users,
|
||||||
|
Heart,
|
||||||
|
Zap,
|
||||||
|
Globe,
|
||||||
|
Laptop,
|
||||||
|
Coffee,
|
||||||
|
TrendingUp,
|
||||||
|
Award,
|
||||||
|
Mail,
|
||||||
|
ArrowRight,
|
||||||
|
Code,
|
||||||
|
Palette,
|
||||||
|
BarChart3
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface JobOpening {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
department: string;
|
||||||
|
location: string;
|
||||||
|
type: string;
|
||||||
|
salary?: string;
|
||||||
|
description: string;
|
||||||
|
requirements: string[];
|
||||||
|
niceToHave: string[];
|
||||||
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CareersPage: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const benefits = [
|
||||||
|
{
|
||||||
|
icon: Laptop,
|
||||||
|
title: 'Trabajo Remoto',
|
||||||
|
description: '100% remoto o híbrido según prefieras. Tenemos oficina en Bilbao pero puedes trabajar desde donde quieras.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Clock,
|
||||||
|
title: 'Horario Flexible',
|
||||||
|
description: 'Enfócate en resultados, no en horas. Organiza tu día como mejor funcione para ti.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Euro,
|
||||||
|
title: 'Salario Competitivo',
|
||||||
|
description: 'Sueldos por encima de mercado + equity en la empresa para fundadores tempranos.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: TrendingUp,
|
||||||
|
title: 'Crecimiento Real',
|
||||||
|
description: 'Somos una startup en fase temprana. Aquí aprendes rápido y tu impacto se ve directamente.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Heart,
|
||||||
|
title: 'Propósito',
|
||||||
|
description: 'Ayuda a negocios reales a prosperar. Tu trabajo tiene impacto tangible en familias.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Users,
|
||||||
|
title: 'Equipo Pequeño',
|
||||||
|
description: 'Sin burocracia, sin reuniones inútiles. Decisiones rápidas, ejecución directa.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const openPositions: JobOpening[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Full Stack Developer (React + Python)',
|
||||||
|
department: 'Ingeniería',
|
||||||
|
location: 'Remoto (España)',
|
||||||
|
type: 'Tiempo completo',
|
||||||
|
salary: '€45,000 - €65,000 + equity',
|
||||||
|
description: 'Buscamos un desarrollador full-stack que nos ayude a construir la mejor plataforma de gestión para panaderías de todos los tamaños y modelos. Trabajarás directamente con los fundadores y tendrás ownership completo de features.',
|
||||||
|
requirements: [
|
||||||
|
'3+ años de experiencia con React y TypeScript',
|
||||||
|
'2+ años con Python (FastAPI, Flask o Django)',
|
||||||
|
'Experiencia con bases de datos (PostgreSQL)',
|
||||||
|
'Git, CI/CD, testing',
|
||||||
|
'Capacidad de trabajar autónomamente',
|
||||||
|
],
|
||||||
|
niceToHave: [
|
||||||
|
'Experiencia con ML/IA',
|
||||||
|
'Background en startups',
|
||||||
|
'Conocimiento del sector F&B/hostelería',
|
||||||
|
'Contribuciones open source',
|
||||||
|
],
|
||||||
|
icon: Code,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
title: 'ML Engineer (Predicción de Demanda)',
|
||||||
|
department: 'IA/ML',
|
||||||
|
location: 'Remoto (España)',
|
||||||
|
type: 'Tiempo completo',
|
||||||
|
salary: '€50,000 - €70,000 + equity',
|
||||||
|
description: 'Lidera el desarrollo de nuestros algoritmos de predicción. Trabajarás con datos reales de panaderías (locales y obradores centrales) para crear modelos que predicen demanda con >90% precisión, tanto a nivel individual como agregado.',
|
||||||
|
requirements: [
|
||||||
|
'MSc o PhD en CS, Matemáticas, o similar',
|
||||||
|
'3+ años trabajando con ML en producción',
|
||||||
|
'Experiencia con time series forecasting',
|
||||||
|
'Python (scikit-learn, TensorFlow/PyTorch)',
|
||||||
|
'SQL y manejo de grandes datasets',
|
||||||
|
],
|
||||||
|
niceToHave: [
|
||||||
|
'Publicaciones en ML/IA',
|
||||||
|
'Experiencia con MLOps',
|
||||||
|
'Background en retail/forecasting/supply chain',
|
||||||
|
'Kaggle competitions',
|
||||||
|
],
|
||||||
|
icon: BarChart3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
title: 'Product Designer (UI/UX)',
|
||||||
|
department: 'Diseño',
|
||||||
|
location: 'Remoto (España)',
|
||||||
|
type: 'Freelance/Tiempo parcial',
|
||||||
|
salary: '€30,000 - €45,000 (parcial)',
|
||||||
|
description: 'Diseña interfaces que panaderos puedan usar incluso con las manos llenas de harina. Necesitamos UX/UI funcional, intuitivo y hermoso para usuarios no-técnicos.',
|
||||||
|
requirements: [
|
||||||
|
'3+ años diseñando productos digitales',
|
||||||
|
'Portfolio con casos de estudio reales',
|
||||||
|
'Experiencia con Figma',
|
||||||
|
'Conocimiento de design systems',
|
||||||
|
'User research y testing',
|
||||||
|
],
|
||||||
|
niceToHave: [
|
||||||
|
'Experiencia en B2B/SaaS',
|
||||||
|
'Conocimiento de front-end (HTML/CSS)',
|
||||||
|
'Ilustración/motion design',
|
||||||
|
'Background en F&B/hostelería',
|
||||||
|
],
|
||||||
|
icon: Palette,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const cultureFacts = [
|
||||||
|
'Somos un equipo de 5 personas (por ahora)',
|
||||||
|
'Promedio de edad: 32 años',
|
||||||
|
'Daily standups de 10 minutos máximo',
|
||||||
|
'80% del equipo trabaja remoto',
|
||||||
|
'Viernes terminamos a las 14:00',
|
||||||
|
'Budget para cursos y conferencias',
|
||||||
|
'Equipo multilingüe (ES/EN/EU)',
|
||||||
|
'Sin dress code (incluso en videollamadas)',
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PublicLayout
|
||||||
|
variant="default"
|
||||||
|
contentPadding="default"
|
||||||
|
headerProps={{
|
||||||
|
showThemeToggle: true,
|
||||||
|
showAuthButtons: true,
|
||||||
|
showLanguageSelector: true,
|
||||||
|
variant: "default"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 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">
|
||||||
|
<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">
|
||||||
|
<Briefcase className="w-4 h-4" />
|
||||||
|
<span>Estamos Contratando</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl lg:text-6xl font-extrabold text-[var(--text-primary)] mb-6">
|
||||||
|
Construye el Futuro de
|
||||||
|
<span className="block text-[var(--color-primary)]">las Panaderías</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)] leading-relaxed mb-8">
|
||||||
|
Únete a una startup en fase temprana que combina IA, sostenibilidad y pasión por ayudar a negocios reales de todos los tamaños.
|
||||||
|
Somos pequeños, ágiles y con un propósito claro.
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-center gap-6 text-sm text-[var(--text-tertiary)]">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<MapPin className="w-4 h-4" />
|
||||||
|
<span>Remoto/Híbrido</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Users className="w-4 h-4" />
|
||||||
|
<span>Equipo de 5</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Globe className="w-4 h-4" />
|
||||||
|
<span>100% España</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Benefits */}
|
||||||
|
<section className="py-20 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)] mb-4">
|
||||||
|
¿Por Qué Trabajar Con Nosotros?
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)] max-w-3xl mx-auto">
|
||||||
|
Beneficios reales, no promesas vacías
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
{benefits.map((benefit, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="bg-[var(--bg-secondary)] rounded-2xl p-6 border border-[var(--border-primary)] hover:shadow-xl transition-all duration-300"
|
||||||
|
>
|
||||||
|
<div className="w-12 h-12 bg-[var(--color-primary)]/10 rounded-xl flex items-center justify-center mb-4">
|
||||||
|
<benefit.icon className="w-6 h-6 text-[var(--color-primary)]" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-bold text-[var(--text-primary)] mb-2">{benefit.title}</h3>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">{benefit.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Open Positions */}
|
||||||
|
<section className="py-20 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)] mb-4">
|
||||||
|
Posiciones Abiertas
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)]">
|
||||||
|
{openPositions.length} vacantes disponibles
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{openPositions.map((job) => (
|
||||||
|
<div
|
||||||
|
key={job.id}
|
||||||
|
className="bg-[var(--bg-primary)] rounded-2xl p-8 border-2 border-[var(--border-primary)] hover:border-[var(--color-primary)] transition-all duration-300"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-6">
|
||||||
|
{/* Left: Job Info */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-start gap-4 mb-4">
|
||||||
|
<div className="w-12 h-12 bg-[var(--color-primary)]/10 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<job.icon className="w-6 h-6 text-[var(--color-primary)]" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-2xl font-bold text-[var(--text-primary)] mb-2">{job.title}</h3>
|
||||||
|
<div className="flex flex-wrap items-center gap-4 text-sm text-[var(--text-secondary)]">
|
||||||
|
<span className="inline-flex items-center gap-1">
|
||||||
|
<Briefcase className="w-4 h-4" />
|
||||||
|
{job.department}
|
||||||
|
</span>
|
||||||
|
<span className="inline-flex items-center gap-1">
|
||||||
|
<MapPin className="w-4 h-4" />
|
||||||
|
{job.location}
|
||||||
|
</span>
|
||||||
|
<span className="inline-flex items-center gap-1">
|
||||||
|
<Clock className="w-4 h-4" />
|
||||||
|
{job.type}
|
||||||
|
</span>
|
||||||
|
{job.salary && (
|
||||||
|
<span className="inline-flex items-center gap-1 text-[var(--color-primary)] font-medium">
|
||||||
|
<Euro className="w-4 h-4" />
|
||||||
|
{job.salary}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[var(--text-secondary)] mb-6">{job.description}</p>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
{/* Requirements */}
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-[var(--text-primary)] mb-3">Requisitos:</h4>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{job.requirements.map((req, i) => (
|
||||||
|
<li key={i} className="flex items-start gap-2 text-sm text-[var(--text-secondary)]">
|
||||||
|
<Award className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<span>{req}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nice to Have */}
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-[var(--text-primary)] mb-3">Valorable:</h4>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{job.niceToHave.map((item, i) => (
|
||||||
|
<li key={i} className="flex items-start gap-2 text-sm text-[var(--text-secondary)]">
|
||||||
|
<Zap className="w-4 h-4 text-[var(--color-primary)] flex-shrink-0 mt-0.5" />
|
||||||
|
<span>{item}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Apply Button */}
|
||||||
|
<div className="flex flex-col gap-3 lg:min-w-[200px]">
|
||||||
|
<a
|
||||||
|
href={`mailto:careers@panaderia-ia.com?subject=Aplicación: ${job.title}`}
|
||||||
|
className="inline-flex items-center justify-center gap-2 px-6 py-3 bg-[var(--color-primary)] text-white rounded-xl font-bold hover:bg-[var(--color-primary-dark)] transition-all hover:scale-105"
|
||||||
|
>
|
||||||
|
<Mail className="w-5 h-5" />
|
||||||
|
<span>Aplicar</span>
|
||||||
|
</a>
|
||||||
|
<Link
|
||||||
|
to="/demo"
|
||||||
|
className="inline-flex items-center justify-center gap-2 px-6 py-3 border-2 border-[var(--border-primary)] text-[var(--text-primary)] rounded-xl font-medium hover:border-[var(--color-primary)] transition-all text-center"
|
||||||
|
>
|
||||||
|
<span>Ver Producto</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Culture */}
|
||||||
|
<section className="py-20 bg-[var(--bg-primary)]">
|
||||||
|
<div className="max-w-5xl 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">
|
||||||
|
Nuestra Cultura
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)]">
|
||||||
|
Datos reales, sin marketing
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
{cultureFacts.map((fact, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center gap-3 bg-[var(--bg-secondary)] p-4 rounded-lg border border-[var(--border-primary)]"
|
||||||
|
>
|
||||||
|
<Coffee className="w-5 h-5 text-[var(--color-primary)] flex-shrink-0" />
|
||||||
|
<span className="text-[var(--text-secondary)]">{fact}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<section className="py-20 bg-gradient-to-r from-[var(--color-primary)] to-orange-600">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||||
|
<h2 className="text-3xl lg:text-4xl font-bold text-white mb-6">
|
||||||
|
¿No Ves Tu Posición Ideal?
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-white/90 mb-8 leading-relaxed">
|
||||||
|
Siempre estamos abiertos a conocer talento excepcional.
|
||||||
|
Envíanos tu CV y cuéntanos por qué quieres unirte a Panadería IA.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="mailto:careers@panaderia-ia.com?subject=Aplicación Espontánea"
|
||||||
|
className="inline-flex items-center gap-2 px-8 py-4 bg-white text-[var(--color-primary)] rounded-xl font-bold hover:shadow-2xl transition-all hover:scale-105"
|
||||||
|
>
|
||||||
|
<Mail className="w-5 h-5" />
|
||||||
|
<span>Enviar Aplicación Espontánea</span>
|
||||||
|
<ArrowRight className="w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
<p className="text-white/80 text-sm mt-6">
|
||||||
|
careers@panaderia-ia.com
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</PublicLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CareersPage;
|
||||||
444
frontend/src/pages/public/ContactPage.tsx
Normal file
444
frontend/src/pages/public/ContactPage.tsx
Normal file
@@ -0,0 +1,444 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PublicLayout } from '../../components/layout';
|
||||||
|
import {
|
||||||
|
MessageSquare,
|
||||||
|
Mail,
|
||||||
|
Phone,
|
||||||
|
MapPin,
|
||||||
|
Clock,
|
||||||
|
Send,
|
||||||
|
CheckCircle2,
|
||||||
|
AlertCircle,
|
||||||
|
HelpCircle
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface ContactMethod {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
detail: string;
|
||||||
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
|
action?: () => void;
|
||||||
|
link?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContactPage: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [formState, setFormState] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
bakeryName: '',
|
||||||
|
subject: '',
|
||||||
|
message: '',
|
||||||
|
type: 'general' as 'general' | 'technical' | 'sales' | 'feedback',
|
||||||
|
});
|
||||||
|
const [submitStatus, setSubmitStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||||
|
|
||||||
|
const contactMethods: ContactMethod[] = [
|
||||||
|
{
|
||||||
|
id: 'chat',
|
||||||
|
title: 'Chat en Vivo',
|
||||||
|
description: 'Respuesta inmediata',
|
||||||
|
detail: 'Lunes a Viernes: 9:00 - 21:00 CET',
|
||||||
|
icon: MessageSquare,
|
||||||
|
link: '#chat',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'email',
|
||||||
|
title: 'Email',
|
||||||
|
description: 'soporte@panaderia-ia.com',
|
||||||
|
detail: 'Respuesta en menos de 4 horas',
|
||||||
|
icon: Mail,
|
||||||
|
link: 'mailto:soporte@panaderia-ia.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'phone',
|
||||||
|
title: 'Teléfono',
|
||||||
|
description: '+34 XXX XXX XXX',
|
||||||
|
detail: 'Lunes a Viernes: 10:00 - 19:00 CET',
|
||||||
|
icon: Phone,
|
||||||
|
link: 'tel:+34XXXXXXXXX',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'office',
|
||||||
|
title: 'Oficina',
|
||||||
|
description: 'Barcelona, España',
|
||||||
|
detail: 'Con cita previa',
|
||||||
|
icon: MapPin,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||||
|
setFormState({
|
||||||
|
...formState,
|
||||||
|
[e.target.name]: e.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSubmitStatus('loading');
|
||||||
|
|
||||||
|
// Simulate API call
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
|
// In production, this would be an actual API call
|
||||||
|
console.log('Form submitted:', formState);
|
||||||
|
|
||||||
|
setSubmitStatus('success');
|
||||||
|
setTimeout(() => {
|
||||||
|
setSubmitStatus('idle');
|
||||||
|
setFormState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
bakeryName: '',
|
||||||
|
subject: '',
|
||||||
|
message: '',
|
||||||
|
type: 'general',
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PublicLayout
|
||||||
|
variant="default"
|
||||||
|
contentPadding="default"
|
||||||
|
headerProps={{
|
||||||
|
showThemeToggle: true,
|
||||||
|
showAuthButtons: true,
|
||||||
|
showLanguageSelector: true,
|
||||||
|
variant: 'default',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 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">
|
||||||
|
<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">
|
||||||
|
<MessageSquare className="w-4 h-4" />
|
||||||
|
<span>Contacto y Soporte</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl lg:text-6xl font-extrabold text-[var(--text-primary)] mb-6">
|
||||||
|
Estamos Aquí Para
|
||||||
|
<span className="block text-[var(--color-primary)]">Ayudarte</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)] leading-relaxed mb-8">
|
||||||
|
¿Tienes preguntas? ¿Necesitas ayuda? Nuestro equipo está listo para asistirte
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Contact Methods */}
|
||||||
|
<section className="py-20 bg-[var(--bg-primary)]">
|
||||||
|
<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">
|
||||||
|
Múltiples Formas de Contactar
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)]">
|
||||||
|
Elige el método que más te convenga
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 mb-16">
|
||||||
|
{contactMethods.map((method) => {
|
||||||
|
const ContactIcon = method.icon;
|
||||||
|
const content = (
|
||||||
|
<>
|
||||||
|
<div className="w-16 h-16 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center mx-auto mb-4 group-hover:bg-[var(--color-primary)] transition-colors">
|
||||||
|
<ContactIcon className="w-8 h-8 text-[var(--color-primary)] group-hover:text-white transition-colors" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-[var(--text-primary)] mb-2">
|
||||||
|
{method.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)] mb-1">
|
||||||
|
{method.description}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-[var(--text-tertiary)]">
|
||||||
|
{method.detail}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (method.link) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={method.id}
|
||||||
|
href={method.link}
|
||||||
|
className="bg-[var(--bg-secondary)] rounded-2xl p-8 border border-[var(--border-primary)] hover:border-[var(--color-primary)] hover:shadow-xl transition-all text-center group"
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={method.id}
|
||||||
|
className="bg-[var(--bg-secondary)] rounded-2xl p-8 border border-[var(--border-primary)] text-center"
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Contact Form */}
|
||||||
|
<section className="py-20 bg-[var(--bg-secondary)]">
|
||||||
|
<div className="max-w-4xl 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">
|
||||||
|
Envíanos un Mensaje
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)]">
|
||||||
|
Completa el formulario y te responderemos lo antes posible
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="bg-[var(--bg-primary)] rounded-2xl p-8 border border-[var(--border-primary)]">
|
||||||
|
{/* Success/Error Messages */}
|
||||||
|
{submitStatus === 'success' && (
|
||||||
|
<div className="mb-6 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-xl flex items-center gap-3">
|
||||||
|
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0" />
|
||||||
|
<p className="text-sm text-green-800 dark:text-green-200">
|
||||||
|
<strong>¡Mensaje enviado!</strong> Te responderemos pronto.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{submitStatus === 'error' && (
|
||||||
|
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl flex items-center gap-3">
|
||||||
|
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0" />
|
||||||
|
<p className="text-sm text-red-800 dark:text-red-200">
|
||||||
|
<strong>Error al enviar.</strong> Por favor, inténtalo de nuevo.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
{/* Name */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="name" className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||||
|
Nombre Completo <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
value={formState.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-xl text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] focus:outline-none focus:border-[var(--color-primary)] transition-colors"
|
||||||
|
placeholder="Tu nombre"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Email */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email" className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||||
|
Email <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
value={formState.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-xl text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] focus:outline-none focus:border-[var(--color-primary)] transition-colors"
|
||||||
|
placeholder="tu@email.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Phone */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="phone" className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||||
|
Teléfono (opcional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
id="phone"
|
||||||
|
name="phone"
|
||||||
|
value={formState.phone}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-xl text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] focus:outline-none focus:border-[var(--color-primary)] transition-colors"
|
||||||
|
placeholder="+34 XXX XXX XXX"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bakery Name */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="bakeryName" className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||||
|
Nombre de tu Panadería (opcional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="bakeryName"
|
||||||
|
name="bakeryName"
|
||||||
|
value={formState.bakeryName}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-xl text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] focus:outline-none focus:border-[var(--color-primary)] transition-colors"
|
||||||
|
placeholder="Panadería Ejemplo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Type */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="type" className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||||
|
Tipo de Consulta <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="type"
|
||||||
|
name="type"
|
||||||
|
value={formState.type}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-xl text-[var(--text-primary)] focus:outline-none focus:border-[var(--color-primary)] transition-colors"
|
||||||
|
>
|
||||||
|
<option value="general">Consulta General</option>
|
||||||
|
<option value="technical">Soporte Técnico</option>
|
||||||
|
<option value="sales">Información Comercial</option>
|
||||||
|
<option value="feedback">Feedback/Sugerencias</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subject */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="subject" className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||||
|
Asunto <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="subject"
|
||||||
|
name="subject"
|
||||||
|
value={formState.subject}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-xl text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] focus:outline-none focus:border-[var(--color-primary)] transition-colors"
|
||||||
|
placeholder="¿En qué podemos ayudarte?"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<label htmlFor="message" className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||||
|
Mensaje <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="message"
|
||||||
|
name="message"
|
||||||
|
value={formState.message}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
rows={6}
|
||||||
|
className="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-xl text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] focus:outline-none focus:border-[var(--color-primary)] transition-colors resize-none"
|
||||||
|
placeholder="Cuéntanos más sobre tu consulta o problema..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<div className="mt-8">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={submitStatus === 'loading'}
|
||||||
|
className="w-full flex items-center justify-center gap-2 px-8 py-4 bg-[var(--color-primary)] text-white rounded-xl font-bold hover:shadow-xl transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
|
||||||
|
>
|
||||||
|
{submitStatus === 'loading' ? (
|
||||||
|
<>
|
||||||
|
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||||
|
<span>Enviando...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Send className="w-5 h-5" />
|
||||||
|
<span>Enviar Mensaje</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-[var(--text-tertiary)] text-center mt-4">
|
||||||
|
Al enviar este formulario, aceptas nuestra{' '}
|
||||||
|
<a href="/privacy" className="text-[var(--color-primary)] hover:underline">
|
||||||
|
Política de Privacidad
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Office Hours */}
|
||||||
|
<section className="py-20 bg-[var(--bg-primary)]">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid md:grid-cols-2 gap-8">
|
||||||
|
{/* Hours */}
|
||||||
|
<div className="bg-[var(--bg-secondary)] rounded-2xl p-8 border border-[var(--border-primary)]">
|
||||||
|
<div className="flex items-start gap-4 mb-6">
|
||||||
|
<Clock className="w-6 h-6 text-[var(--color-primary)] flex-shrink-0 mt-1" />
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-bold text-[var(--text-primary)] mb-4">
|
||||||
|
Horarios de Atención
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3 text-sm text-[var(--text-secondary)]">
|
||||||
|
<div>
|
||||||
|
<strong className="text-[var(--text-primary)]">Chat en Vivo:</strong>
|
||||||
|
<p>Lunes a Viernes: 9:00 - 21:00 CET</p>
|
||||||
|
<p>Sábados: 10:00 - 18:00 CET</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong className="text-[var(--text-primary)]">Email:</strong>
|
||||||
|
<p>24/7 (respuesta en menos de 4 horas en horario laboral)</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong className="text-[var(--text-primary)]">Teléfono:</strong>
|
||||||
|
<p>Lunes a Viernes: 10:00 - 19:00 CET (solo clientes activos)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FAQ Link */}
|
||||||
|
<div className="bg-gradient-to-br from-[var(--color-primary)]/10 to-orange-600/10 rounded-2xl p-8 border border-[var(--color-primary)]/20">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<HelpCircle className="w-6 h-6 text-[var(--color-primary)] flex-shrink-0 mt-1" />
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-bold text-[var(--text-primary)] mb-2">
|
||||||
|
¿Buscas Respuestas Rápidas?
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
||||||
|
Muchas preguntas ya tienen respuesta en nuestro Centro de Ayuda y Documentación
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<a
|
||||||
|
href="/help"
|
||||||
|
className="inline-flex items-center gap-2 text-[var(--color-primary)] hover:underline font-medium text-sm"
|
||||||
|
>
|
||||||
|
Ver Centro de Ayuda →
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<a
|
||||||
|
href="/help/docs"
|
||||||
|
className="inline-flex items-center gap-2 text-[var(--color-primary)] hover:underline font-medium text-sm"
|
||||||
|
>
|
||||||
|
Leer Documentación →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</PublicLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactPage;
|
||||||
@@ -1,18 +1,21 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { PublicLayout } from '../../components/layout';
|
import { PublicLayout } from '../../components/layout';
|
||||||
import { Button } from '../../components/ui';
|
import { Button } from '../../components/ui';
|
||||||
import { getDemoAccounts, createDemoSession, DemoAccount } from '../../api/services/demo';
|
import { getDemoAccounts, createDemoSession, DemoAccount, demoSessionAPI } from '../../api/services/demo';
|
||||||
import { apiClient } from '../../api/client';
|
import { apiClient } from '../../api/client';
|
||||||
import { Check, Clock, Shield, Play, Zap, ArrowRight, Store, Factory } from 'lucide-react';
|
import { Check, Clock, Shield, Play, Zap, ArrowRight, Store, Factory, Loader2 } from 'lucide-react';
|
||||||
import { markTourAsStartPending } from '../../features/demo-onboarding';
|
import { markTourAsStartPending } from '../../features/demo-onboarding';
|
||||||
|
|
||||||
|
const POLL_INTERVAL_MS = 1500; // Poll every 1.5 seconds
|
||||||
|
|
||||||
export const DemoPage: React.FC = () => {
|
export const DemoPage: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
|
||||||
const [demoAccounts, setDemoAccounts] = useState<DemoAccount[]>([]);
|
const [demoAccounts, setDemoAccounts] = useState<DemoAccount[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [creatingSession, setCreatingSession] = useState(false);
|
const [creatingSession, setCreatingSession] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [progressPercentage, setProgressPercentage] = useState(0);
|
||||||
|
const [estimatedTime, setEstimatedTime] = useState(5);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchDemoAccounts = async () => {
|
const fetchDemoAccounts = async () => {
|
||||||
@@ -30,9 +33,63 @@ export const DemoPage: React.FC = () => {
|
|||||||
fetchDemoAccounts();
|
fetchDemoAccounts();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const pollStatus = useCallback(async (sessionId: string) => {
|
||||||
|
try {
|
||||||
|
const statusData = await demoSessionAPI.getSessionStatus(sessionId);
|
||||||
|
|
||||||
|
// Calculate progress - ALWAYS update, even if no progress data yet
|
||||||
|
if (statusData.progress && Object.keys(statusData.progress).length > 0) {
|
||||||
|
const services = Object.values(statusData.progress);
|
||||||
|
const totalServices = services.length;
|
||||||
|
|
||||||
|
if (totalServices > 0) {
|
||||||
|
const completedServices = services.filter(
|
||||||
|
(s) => s.status === 'completed' || s.status === 'failed'
|
||||||
|
).length;
|
||||||
|
const percentage = Math.round((completedServices / totalServices) * 100);
|
||||||
|
setProgressPercentage(percentage);
|
||||||
|
|
||||||
|
// Estimate remaining time
|
||||||
|
const remainingServices = totalServices - completedServices;
|
||||||
|
setEstimatedTime(Math.max(remainingServices * 2, 1));
|
||||||
|
} else {
|
||||||
|
// No services yet, show minimal progress
|
||||||
|
setProgressPercentage(5);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No progress data yet, show initial state
|
||||||
|
setProgressPercentage(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if ready to redirect
|
||||||
|
const hasUsableData = statusData.total_records_cloned > 100;
|
||||||
|
const shouldRedirect =
|
||||||
|
statusData.status === 'ready' ||
|
||||||
|
(statusData.status === 'partial' && hasUsableData) ||
|
||||||
|
(statusData.status === 'failed' && hasUsableData);
|
||||||
|
|
||||||
|
if (shouldRedirect) {
|
||||||
|
// Show 100% before redirect
|
||||||
|
setProgressPercentage(100);
|
||||||
|
// Small delay for smooth transition
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = `/app/dashboard?session=${sessionId}`;
|
||||||
|
}, 300);
|
||||||
|
return true; // Stop polling
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // Continue polling
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error polling session status:', err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleStartDemo = async (accountType: string) => {
|
const handleStartDemo = async (accountType: string) => {
|
||||||
setCreatingSession(true);
|
setCreatingSession(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setProgressPercentage(0);
|
||||||
|
setEstimatedTime(6);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const session = await createDemoSession({
|
const session = await createDemoSession({
|
||||||
@@ -44,8 +101,7 @@ export const DemoPage: React.FC = () => {
|
|||||||
// Store session ID in API client
|
// Store session ID in API client
|
||||||
apiClient.setDemoSessionId(session.session_id);
|
apiClient.setDemoSessionId(session.session_id);
|
||||||
|
|
||||||
// **CRITICAL FIX: Set the virtual tenant ID in API client**
|
// Set the virtual tenant ID in API client
|
||||||
// This ensures all API requests include the correct tenant context
|
|
||||||
apiClient.setTenantId(session.virtual_tenant_id);
|
apiClient.setTenantId(session.virtual_tenant_id);
|
||||||
console.log('✅ Set API client tenant ID:', session.virtual_tenant_id);
|
console.log('✅ Set API client tenant ID:', session.virtual_tenant_id);
|
||||||
|
|
||||||
@@ -56,38 +112,48 @@ export const DemoPage: React.FC = () => {
|
|||||||
localStorage.setItem('demo_expires_at', session.expires_at);
|
localStorage.setItem('demo_expires_at', session.expires_at);
|
||||||
localStorage.setItem('demo_tenant_id', session.virtual_tenant_id);
|
localStorage.setItem('demo_tenant_id', session.virtual_tenant_id);
|
||||||
|
|
||||||
// **CRITICAL FIX: Initialize tenant store with demo tenant**
|
// Start polling IMMEDIATELY in parallel with other setup
|
||||||
// This ensures useTenantId() returns the correct virtual tenant ID
|
const pollInterval = setInterval(async () => {
|
||||||
const { useTenantStore } = await import('../../stores/tenant.store');
|
const shouldStop = await pollStatus(session.session_id);
|
||||||
const demoTenant = {
|
if (shouldStop) {
|
||||||
id: session.virtual_tenant_id,
|
clearInterval(pollInterval);
|
||||||
name: session.demo_config?.name || `Demo ${accountType}`,
|
}
|
||||||
business_type: accountType === 'individual_bakery' ? 'bakery' : 'central_baker',
|
}, POLL_INTERVAL_MS);
|
||||||
business_model: accountType,
|
|
||||||
address: session.demo_config?.address || 'Demo Address',
|
|
||||||
city: session.demo_config?.city || 'Madrid',
|
|
||||||
postal_code: '28001',
|
|
||||||
phone: null,
|
|
||||||
is_active: true,
|
|
||||||
subscription_tier: 'demo',
|
|
||||||
ml_model_trained: false,
|
|
||||||
last_training_date: null,
|
|
||||||
owner_id: 'demo-user',
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
useTenantStore.getState().setCurrentTenant(demoTenant);
|
// Initialize tenant store and other setup in parallel (non-blocking)
|
||||||
console.log('✅ Initialized tenant store with demo tenant:', demoTenant);
|
Promise.all([
|
||||||
|
import('../../stores/tenant.store').then(({ useTenantStore }) => {
|
||||||
|
const demoTenant = {
|
||||||
|
id: session.virtual_tenant_id,
|
||||||
|
name: session.demo_config?.name || `Demo ${accountType}`,
|
||||||
|
business_type: accountType === 'individual_bakery' ? 'bakery' : 'central_baker',
|
||||||
|
business_model: accountType,
|
||||||
|
address: session.demo_config?.address || 'Demo Address',
|
||||||
|
city: session.demo_config?.city || 'Madrid',
|
||||||
|
postal_code: '28001',
|
||||||
|
phone: null,
|
||||||
|
is_active: true,
|
||||||
|
subscription_tier: 'demo',
|
||||||
|
ml_model_trained: false,
|
||||||
|
last_training_date: null,
|
||||||
|
owner_id: 'demo-user',
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
useTenantStore.getState().setCurrentTenant(demoTenant);
|
||||||
|
console.log('✅ Initialized tenant store with demo tenant:', demoTenant);
|
||||||
|
}),
|
||||||
|
// Mark tour to start automatically
|
||||||
|
Promise.resolve(markTourAsStartPending()),
|
||||||
|
]).catch(err => console.error('Error initializing tenant store:', err));
|
||||||
|
|
||||||
// Mark tour to start automatically
|
// Initial poll (don't wait for tenant store)
|
||||||
markTourAsStartPending();
|
const shouldStop = await pollStatus(session.session_id);
|
||||||
|
if (shouldStop) {
|
||||||
// Navigate to setup page to wait for data cloning
|
clearInterval(pollInterval);
|
||||||
navigate(`/demo/setup?session=${session.session_id}`);
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err?.message || 'Error al crear sesión demo');
|
setError(err?.message || 'Error al crear sesión demo');
|
||||||
console.error('Error creating demo session:', err);
|
console.error('Error creating demo session:', err);
|
||||||
} finally {
|
|
||||||
setCreatingSession(false);
|
setCreatingSession(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -247,20 +313,8 @@ export const DemoPage: React.FC = () => {
|
|||||||
size="lg"
|
size="lg"
|
||||||
className="w-full bg-[var(--color-primary)] hover:bg-[var(--color-primary-dark)] text-white shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200"
|
className="w-full bg-[var(--color-primary)] hover:bg-[var(--color-primary-dark)] text-white shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200"
|
||||||
>
|
>
|
||||||
{creatingSession ? (
|
<Play className="mr-2 w-5 h-5" />
|
||||||
<span className="flex items-center justify-center">
|
Probar Demo Ahora
|
||||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
Creando sesión...
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Play className="mr-2 w-5 h-5" />
|
|
||||||
Probar Demo Ahora
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -283,6 +337,68 @@ export const DemoPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Loading Modal Overlay */}
|
||||||
|
{creatingSession && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50">
|
||||||
|
<div className="bg-[var(--bg-primary)] rounded-2xl shadow-2xl p-8 max-w-md w-full mx-4 border border-[var(--border-default)]">
|
||||||
|
<div className="text-center">
|
||||||
|
{/* Animated loader */}
|
||||||
|
<div className="mb-6 flex justify-center">
|
||||||
|
<div className="relative w-20 h-20">
|
||||||
|
<Loader2 className="w-20 h-20 text-[var(--color-primary)] animate-spin" />
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<span className="text-xl font-bold text-[var(--color-primary)]">
|
||||||
|
{Math.min(progressPercentage, 100)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-2">
|
||||||
|
{progressPercentage >= 100 ? '¡Listo! Redirigiendo...' : 'Preparando tu Demo'}
|
||||||
|
</h2>
|
||||||
|
<p className="text-[var(--text-secondary)] mb-6">
|
||||||
|
{progressPercentage >= 100
|
||||||
|
? 'Tu entorno está listo. Accediendo al dashboard...'
|
||||||
|
: 'Configurando tu entorno personalizado con datos de muestra...'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Progress bar */}
|
||||||
|
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-3 mb-4 overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] h-full rounded-full transition-all duration-500 ease-out"
|
||||||
|
style={{ width: `${Math.min(progressPercentage, 100)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Estimated time - Only show if not complete */}
|
||||||
|
{progressPercentage < 100 && (
|
||||||
|
<div className="flex items-center justify-center text-sm text-[var(--text-tertiary)] mb-4">
|
||||||
|
<Clock className="w-4 h-4 mr-2" />
|
||||||
|
<span>Tiempo estimado: ~{estimatedTime}s</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tips while loading */}
|
||||||
|
{progressPercentage < 100 && (
|
||||||
|
<div className="mt-2 p-4 bg-[var(--color-primary)]/5 rounded-lg border border-[var(--color-primary)]/20">
|
||||||
|
<p className="text-xs text-[var(--text-secondary)] italic">
|
||||||
|
💡 Tip: La demo incluye datos reales de panaderías españolas para que puedas explorar todas las funcionalidades
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error message if any */}
|
||||||
|
{error && (
|
||||||
|
<div className="mt-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 rounded-lg text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</PublicLayout>
|
</PublicLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,239 +0,0 @@
|
|||||||
import React, { useEffect, useState, useCallback } from 'react';
|
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
||||||
import { demoSessionAPI, SessionStatusResponse } from '@/api/services/demo';
|
|
||||||
import { DemoProgressIndicator } from '@/components/demo/DemoProgressIndicator';
|
|
||||||
import { DemoErrorScreen } from '@/components/demo/DemoErrorScreen';
|
|
||||||
import { Card, CardBody, ProgressBar, Button, LoadingSpinner } from '@/components/ui';
|
|
||||||
import { PublicLayout } from '@/components/layout';
|
|
||||||
|
|
||||||
const POLL_INTERVAL_MS = 1500; // Poll every 1.5 seconds
|
|
||||||
|
|
||||||
export const DemoSetupPage: React.FC = () => {
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const sessionId = searchParams.get('session');
|
|
||||||
|
|
||||||
const [status, setStatus] = useState<SessionStatusResponse | null>(null);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [isRetrying, setIsRetrying] = useState(false);
|
|
||||||
|
|
||||||
const pollStatus = useCallback(async () => {
|
|
||||||
if (!sessionId) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const statusData = await demoSessionAPI.getSessionStatus(sessionId);
|
|
||||||
setStatus(statusData);
|
|
||||||
|
|
||||||
// Redirect to dashboard if:
|
|
||||||
// 1. Status is 'ready' (all services succeeded)
|
|
||||||
// 2. Status is 'partial' or 'failed' BUT we have usable data (>100 records)
|
|
||||||
const hasUsableData = statusData.total_records_cloned > 100;
|
|
||||||
const shouldRedirect =
|
|
||||||
statusData.status === 'ready' ||
|
|
||||||
(statusData.status === 'partial' && hasUsableData) ||
|
|
||||||
(statusData.status === 'failed' && hasUsableData);
|
|
||||||
|
|
||||||
if (shouldRedirect) {
|
|
||||||
// Data is usable, redirect to dashboard
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = `/app/dashboard?session=${sessionId}`;
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
|
||||||
setError(errorMessage);
|
|
||||||
}
|
|
||||||
}, [sessionId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!sessionId) {
|
|
||||||
navigate('/demo');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial poll
|
|
||||||
pollStatus();
|
|
||||||
|
|
||||||
// Set up polling interval
|
|
||||||
const intervalId = setInterval(pollStatus, POLL_INTERVAL_MS);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(intervalId);
|
|
||||||
};
|
|
||||||
}, [sessionId, navigate, pollStatus]);
|
|
||||||
|
|
||||||
const handleRetry = async () => {
|
|
||||||
if (!sessionId) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIsRetrying(true);
|
|
||||||
setError(null);
|
|
||||||
await demoSessionAPI.retryCloning(sessionId);
|
|
||||||
|
|
||||||
// Resume polling after retry
|
|
||||||
await pollStatus();
|
|
||||||
} catch (err) {
|
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Retry failed';
|
|
||||||
setError(errorMessage);
|
|
||||||
} finally {
|
|
||||||
setIsRetrying(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleContinueAnyway = () => {
|
|
||||||
window.location.href = `/app/dashboard?session=${sessionId}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (error && !status) {
|
|
||||||
return (
|
|
||||||
<DemoErrorScreen
|
|
||||||
error={error}
|
|
||||||
onRetry={handleRetry}
|
|
||||||
isRetrying={isRetrying}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!status) {
|
|
||||||
return (
|
|
||||||
<PublicLayout
|
|
||||||
variant="centered"
|
|
||||||
headerProps={{
|
|
||||||
showThemeToggle: true,
|
|
||||||
showAuthButtons: false,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center justify-center min-h-[60vh]">
|
|
||||||
<LoadingSpinner size="lg" />
|
|
||||||
<p className="mt-4 text-[var(--text-secondary)]">
|
|
||||||
Inicializando entorno demo...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</PublicLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only show error screen if failed with NO usable data
|
|
||||||
if (status.status === 'failed' && status.total_records_cloned <= 100) {
|
|
||||||
return (
|
|
||||||
<DemoErrorScreen
|
|
||||||
error="Demo session setup failed"
|
|
||||||
details={status.errors}
|
|
||||||
onRetry={handleRetry}
|
|
||||||
isRetrying={isRetrying}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const estimatedTime = estimateRemainingTime(status);
|
|
||||||
const progressPercentage = calculateProgressPercentage(status);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PublicLayout
|
|
||||||
variant="centered"
|
|
||||||
headerProps={{
|
|
||||||
showThemeToggle: true,
|
|
||||||
showAuthButtons: false,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="max-w-2xl mx-auto p-8">
|
|
||||||
<Card className="shadow-xl">
|
|
||||||
<CardBody className="p-8">
|
|
||||||
<div className="text-center mb-6">
|
|
||||||
<h1 className="text-3xl font-bold text-[var(--text-primary)] mb-2">
|
|
||||||
🔄 Preparando tu Entorno Demo
|
|
||||||
</h1>
|
|
||||||
<p className="text-[var(--text-secondary)]">
|
|
||||||
Configurando tu sesión personalizada con datos de muestra...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{status.progress && <DemoProgressIndicator progress={status.progress} />}
|
|
||||||
|
|
||||||
<div className="mt-6">
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<span className="text-sm font-medium text-[var(--text-primary)]">
|
|
||||||
Progreso general
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-[var(--text-secondary)]">
|
|
||||||
{progressPercentage}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ProgressBar value={progressPercentage} variant="default" animated />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{status.status === 'pending' && (
|
|
||||||
<div className="mt-4 text-center">
|
|
||||||
<p className="text-sm text-[var(--text-secondary)]">
|
|
||||||
Tiempo estimado restante: ~{estimatedTime} segundos
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{status.status === 'partial' && (
|
|
||||||
<div className="mt-4 p-4 bg-[var(--color-warning)]/10 border border-[var(--color-warning)] rounded-lg">
|
|
||||||
<p className="text-sm text-[var(--text-primary)] mb-3">
|
|
||||||
⚠️ Algunos datos aún se están cargando. Puedes continuar con
|
|
||||||
funcionalidad limitada o esperar a que se carguen todos los datos.
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
onClick={handleContinueAnyway}
|
|
||||||
variant="warning"
|
|
||||||
size="sm"
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
Continuar de todos modos
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{status.status === 'failed' && status.total_records_cloned > 100 && (
|
|
||||||
<div className="mt-4 p-4 bg-[var(--color-info)]/10 border border-[var(--color-info)] rounded-lg">
|
|
||||||
<p className="text-sm text-[var(--text-primary)]">
|
|
||||||
ℹ️ Algunos servicios tuvieron problemas, pero hemos cargado{' '}
|
|
||||||
{status.total_records_cloned} registros exitosamente. ¡El demo está
|
|
||||||
completamente funcional!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-6 text-center">
|
|
||||||
<p className="text-xs text-[var(--text-tertiary)]">
|
|
||||||
Total de registros clonados: {status.total_records_cloned}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</PublicLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function estimateRemainingTime(status: SessionStatusResponse): number {
|
|
||||||
if (!status.progress) return 5;
|
|
||||||
|
|
||||||
const services = Object.values(status.progress);
|
|
||||||
const completedServices = services.filter((s) => s.status === 'completed').length;
|
|
||||||
const totalServices = services.length;
|
|
||||||
const remainingServices = totalServices - completedServices;
|
|
||||||
|
|
||||||
// Assume ~2 seconds per service
|
|
||||||
return Math.max(remainingServices * 2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateProgressPercentage(status: SessionStatusResponse): number {
|
|
||||||
if (!status.progress) return 0;
|
|
||||||
|
|
||||||
const services = Object.values(status.progress);
|
|
||||||
if (services.length === 0) return 0;
|
|
||||||
|
|
||||||
const completedServices = services.filter(
|
|
||||||
(s) => s.status === 'completed' || s.status === 'failed'
|
|
||||||
).length;
|
|
||||||
const totalServices = services.length;
|
|
||||||
|
|
||||||
const percentage = (completedServices / totalServices) * 100;
|
|
||||||
return Math.round(isNaN(percentage) ? 0 : percentage);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DemoSetupPage;
|
|
||||||
596
frontend/src/pages/public/DocumentationPage.tsx
Normal file
596
frontend/src/pages/public/DocumentationPage.tsx
Normal file
@@ -0,0 +1,596 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PublicLayout } from '../../components/layout';
|
||||||
|
import {
|
||||||
|
FileText,
|
||||||
|
BookOpen,
|
||||||
|
Rocket,
|
||||||
|
Package,
|
||||||
|
BarChart3,
|
||||||
|
Settings,
|
||||||
|
Users,
|
||||||
|
Shield,
|
||||||
|
CreditCard,
|
||||||
|
HelpCircle,
|
||||||
|
ChevronRight,
|
||||||
|
PlayCircle,
|
||||||
|
CheckCircle2,
|
||||||
|
AlertCircle,
|
||||||
|
Info,
|
||||||
|
Download,
|
||||||
|
ExternalLink
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface DocSection {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
|
articles: DocArticle[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DocArticle {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
readTime: string;
|
||||||
|
difficulty: 'beginner' | 'intermediate' | 'advanced';
|
||||||
|
}
|
||||||
|
|
||||||
|
const DocumentationPage: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [activeSection, setActiveSection] = useState<string>('getting-started');
|
||||||
|
|
||||||
|
const sections: DocSection[] = [
|
||||||
|
{
|
||||||
|
id: 'getting-started',
|
||||||
|
title: 'Primeros Pasos',
|
||||||
|
description: 'Todo lo que necesitas para comenzar',
|
||||||
|
icon: Rocket,
|
||||||
|
articles: [
|
||||||
|
{
|
||||||
|
id: 'quick-start',
|
||||||
|
title: 'Guía de Inicio Rápido',
|
||||||
|
description: 'Configura tu cuenta en 10 minutos',
|
||||||
|
readTime: '5 min',
|
||||||
|
difficulty: 'beginner',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'import-data',
|
||||||
|
title: 'Importar Datos Históricos',
|
||||||
|
description: 'Cómo subir tu historial de ventas desde Excel o TPV',
|
||||||
|
readTime: '8 min',
|
||||||
|
difficulty: 'beginner',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'products-catalog',
|
||||||
|
title: 'Configurar Catálogo de Productos',
|
||||||
|
description: 'Añade tus productos, recetas e ingredientes',
|
||||||
|
readTime: '6 min',
|
||||||
|
difficulty: 'beginner',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'first-prediction',
|
||||||
|
title: 'Tu Primera Predicción',
|
||||||
|
description: 'Interpreta y ajusta las predicciones de demanda',
|
||||||
|
readTime: '10 min',
|
||||||
|
difficulty: 'beginner',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'features',
|
||||||
|
title: 'Funcionalidades',
|
||||||
|
description: 'Guías detalladas de cada módulo',
|
||||||
|
icon: Package,
|
||||||
|
articles: [
|
||||||
|
{
|
||||||
|
id: 'demand-forecasting',
|
||||||
|
title: 'Predicción de Demanda con IA',
|
||||||
|
description: 'Cómo funciona el algoritmo y cómo sacarle el máximo partido',
|
||||||
|
readTime: '12 min',
|
||||||
|
difficulty: 'intermediate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'production-planning',
|
||||||
|
title: 'Planificación de Producción',
|
||||||
|
description: 'Optimiza tu horneado diario basándote en predicciones',
|
||||||
|
readTime: '10 min',
|
||||||
|
difficulty: 'intermediate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'inventory-management',
|
||||||
|
title: 'Gestión de Inventario',
|
||||||
|
description: 'Controla stock, proveedores y compras',
|
||||||
|
readTime: '9 min',
|
||||||
|
difficulty: 'intermediate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pos-integration',
|
||||||
|
title: 'Punto de Venta (TPV)',
|
||||||
|
description: 'Registra ventas y sincroniza con predicciones',
|
||||||
|
readTime: '8 min',
|
||||||
|
difficulty: 'beginner',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'waste-tracking',
|
||||||
|
title: 'Seguimiento de Desperdicios',
|
||||||
|
description: 'Mide, analiza y reduce el desperdicio alimentario',
|
||||||
|
readTime: '7 min',
|
||||||
|
difficulty: 'beginner',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'analytics',
|
||||||
|
title: 'Análisis e Insights',
|
||||||
|
description: 'Interpreta tus datos y métricas',
|
||||||
|
icon: BarChart3,
|
||||||
|
articles: [
|
||||||
|
{
|
||||||
|
id: 'dashboard-overview',
|
||||||
|
title: 'Panel de Control',
|
||||||
|
description: 'Entiende todas las métricas clave de un vistazo',
|
||||||
|
readTime: '8 min',
|
||||||
|
difficulty: 'beginner',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'reports',
|
||||||
|
title: 'Informes y Reportes',
|
||||||
|
description: 'Genera y exporta informes personalizados',
|
||||||
|
readTime: '10 min',
|
||||||
|
difficulty: 'intermediate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ai-insights',
|
||||||
|
title: 'Insights de IA',
|
||||||
|
description: 'Descubre patrones y oportunidades automáticamente',
|
||||||
|
readTime: '12 min',
|
||||||
|
difficulty: 'advanced',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'performance-metrics',
|
||||||
|
title: 'Métricas de Rendimiento',
|
||||||
|
description: 'KPIs clave: márgenes, rotación, precisión de predicciones',
|
||||||
|
readTime: '15 min',
|
||||||
|
difficulty: 'intermediate',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'account',
|
||||||
|
title: 'Gestión de Cuenta',
|
||||||
|
description: 'Administra tu perfil y equipo',
|
||||||
|
icon: Settings,
|
||||||
|
articles: [
|
||||||
|
{
|
||||||
|
id: 'profile-settings',
|
||||||
|
title: 'Configuración de Perfil',
|
||||||
|
description: 'Actualiza tu información personal y preferencias',
|
||||||
|
readTime: '4 min',
|
||||||
|
difficulty: 'beginner',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'team-management',
|
||||||
|
title: 'Gestionar Equipo',
|
||||||
|
description: 'Añade miembros, asigna roles y permisos',
|
||||||
|
readTime: '7 min',
|
||||||
|
difficulty: 'intermediate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'notifications',
|
||||||
|
title: 'Notificaciones y Alertas',
|
||||||
|
description: 'Configura cómo y cuándo recibir avisos',
|
||||||
|
readTime: '5 min',
|
||||||
|
difficulty: 'beginner',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'integrations',
|
||||||
|
title: 'Integraciones',
|
||||||
|
description: 'Conecta con TPV, contabilidad y otras herramientas',
|
||||||
|
readTime: '10 min',
|
||||||
|
difficulty: 'advanced',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'billing',
|
||||||
|
title: 'Facturación y Planes',
|
||||||
|
description: 'Suscripciones, pagos y facturas',
|
||||||
|
icon: CreditCard,
|
||||||
|
articles: [
|
||||||
|
{
|
||||||
|
id: 'pricing-plans',
|
||||||
|
title: 'Planes y Precios',
|
||||||
|
description: 'Compara características de cada plan',
|
||||||
|
readTime: '6 min',
|
||||||
|
difficulty: 'beginner',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'payment-methods',
|
||||||
|
title: 'Métodos de Pago',
|
||||||
|
description: 'Añade, modifica o elimina tarjetas',
|
||||||
|
readTime: '4 min',
|
||||||
|
difficulty: 'beginner',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'invoices',
|
||||||
|
title: 'Facturas y Recibos',
|
||||||
|
description: 'Descarga y gestiona tu historial de facturación',
|
||||||
|
readTime: '5 min',
|
||||||
|
difficulty: 'beginner',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cancel-subscription',
|
||||||
|
title: 'Cancelar o Pausar Suscripción',
|
||||||
|
description: 'Cómo proceder si necesitas hacer una pausa',
|
||||||
|
readTime: '3 min',
|
||||||
|
difficulty: 'beginner',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'privacy',
|
||||||
|
title: 'Privacidad y Seguridad',
|
||||||
|
description: 'RGPD, datos y cumplimiento',
|
||||||
|
icon: Shield,
|
||||||
|
articles: [
|
||||||
|
{
|
||||||
|
id: 'gdpr-compliance',
|
||||||
|
title: 'Cumplimiento RGPD',
|
||||||
|
description: 'Cómo protegemos tus datos según normativa europea',
|
||||||
|
readTime: '10 min',
|
||||||
|
difficulty: 'intermediate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'data-export',
|
||||||
|
title: 'Exportar Tus Datos',
|
||||||
|
description: 'Descarga toda tu información en cualquier momento',
|
||||||
|
readTime: '5 min',
|
||||||
|
difficulty: 'beginner',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'data-deletion',
|
||||||
|
title: 'Eliminar Tu Cuenta',
|
||||||
|
description: 'Procedimiento para borrar permanentemente tus datos',
|
||||||
|
readTime: '6 min',
|
||||||
|
difficulty: 'beginner',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'security-best-practices',
|
||||||
|
title: 'Mejores Prácticas de Seguridad',
|
||||||
|
description: 'Protege tu cuenta con 2FA y contraseñas fuertes',
|
||||||
|
readTime: '8 min',
|
||||||
|
difficulty: 'intermediate',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const activeContent = sections.find((s) => s.id === activeSection);
|
||||||
|
|
||||||
|
const getDifficultyColor = (difficulty: string) => {
|
||||||
|
switch (difficulty) {
|
||||||
|
case 'beginner':
|
||||||
|
return 'text-green-600 dark:text-green-400';
|
||||||
|
case 'intermediate':
|
||||||
|
return 'text-amber-600 dark:text-amber-400';
|
||||||
|
case 'advanced':
|
||||||
|
return 'text-red-600 dark:text-red-400';
|
||||||
|
default:
|
||||||
|
return 'text-[var(--text-tertiary)]';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDifficultyLabel = (difficulty: string) => {
|
||||||
|
switch (difficulty) {
|
||||||
|
case 'beginner':
|
||||||
|
return 'Principiante';
|
||||||
|
case 'intermediate':
|
||||||
|
return 'Intermedio';
|
||||||
|
case 'advanced':
|
||||||
|
return 'Avanzado';
|
||||||
|
default:
|
||||||
|
return difficulty;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PublicLayout
|
||||||
|
variant="default"
|
||||||
|
contentPadding="default"
|
||||||
|
headerProps={{
|
||||||
|
showThemeToggle: true,
|
||||||
|
showAuthButtons: true,
|
||||||
|
showLanguageSelector: true,
|
||||||
|
variant: 'default',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 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">
|
||||||
|
<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">
|
||||||
|
<FileText className="w-4 h-4" />
|
||||||
|
<span>Documentación</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl lg:text-6xl font-extrabold text-[var(--text-primary)] mb-6">
|
||||||
|
Guías Completas Para
|
||||||
|
<span className="block text-[var(--color-primary)]">Dominar Panadería IA</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)] leading-relaxed mb-8">
|
||||||
|
Tutoriales paso a paso, mejores prácticas y trucos para aprovechar al máximo la plataforma
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<div className="flex flex-wrap justify-center gap-4">
|
||||||
|
<a
|
||||||
|
href="#getting-started"
|
||||||
|
onClick={() => setActiveSection('getting-started')}
|
||||||
|
className="inline-flex items-center gap-2 px-6 py-3 bg-[var(--color-primary)] text-white rounded-xl font-bold hover:shadow-xl transition-all hover:scale-105"
|
||||||
|
>
|
||||||
|
<Rocket className="w-5 h-5" />
|
||||||
|
<span>Comenzar</span>
|
||||||
|
</a>
|
||||||
|
<Link
|
||||||
|
to="/help"
|
||||||
|
className="inline-flex items-center gap-2 px-6 py-3 border-2 border-[var(--border-primary)] text-[var(--text-primary)] rounded-xl font-medium hover:border-[var(--color-primary)] transition-all"
|
||||||
|
>
|
||||||
|
<HelpCircle className="w-5 h-5" />
|
||||||
|
<span>Centro de Ayuda</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Documentation Content */}
|
||||||
|
<section className="py-20 bg-[var(--bg-primary)]">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid lg:grid-cols-4 gap-8">
|
||||||
|
{/* Sidebar Navigation */}
|
||||||
|
<div className="lg:col-span-1">
|
||||||
|
<div className="sticky top-8">
|
||||||
|
<h3 className="text-lg font-bold text-[var(--text-primary)] mb-4">
|
||||||
|
Secciones
|
||||||
|
</h3>
|
||||||
|
<nav className="space-y-2">
|
||||||
|
{sections.map((section) => (
|
||||||
|
<button
|
||||||
|
key={section.id}
|
||||||
|
onClick={() => setActiveSection(section.id)}
|
||||||
|
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-left transition-all ${
|
||||||
|
activeSection === section.id
|
||||||
|
? 'bg-[var(--color-primary)] text-white'
|
||||||
|
: 'bg-[var(--bg-secondary)] text-[var(--text-primary)] hover:bg-[var(--color-primary)]/10'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<section.icon className="w-5 h-5 flex-shrink-0" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="font-medium truncate">{section.title}</div>
|
||||||
|
<div
|
||||||
|
className={`text-xs ${
|
||||||
|
activeSection === section.id ? 'text-white/80' : 'text-[var(--text-tertiary)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{section.articles.length} artículos
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Quick Links */}
|
||||||
|
<div className="mt-8 p-4 bg-[var(--bg-secondary)] rounded-xl border border-[var(--border-primary)]">
|
||||||
|
<h4 className="font-bold text-[var(--text-primary)] mb-3 text-sm">
|
||||||
|
Enlaces Rápidos
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<Link
|
||||||
|
to="/help"
|
||||||
|
className="flex items-center gap-2 text-[var(--text-secondary)] hover:text-[var(--color-primary)] transition-colors"
|
||||||
|
>
|
||||||
|
<HelpCircle className="w-4 h-4" />
|
||||||
|
<span>Centro de Ayuda</span>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/help/support"
|
||||||
|
className="flex items-center gap-2 text-[var(--text-secondary)] hover:text-[var(--color-primary)] transition-colors"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-4 h-4" />
|
||||||
|
<span>Contactar Soporte</span>
|
||||||
|
</Link>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="flex items-center gap-2 text-[var(--text-secondary)] hover:text-[var(--color-primary)] transition-colors"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
<span>Descargar PDF</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="lg:col-span-3">
|
||||||
|
{activeContent && (
|
||||||
|
<div>
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="flex items-center gap-4 mb-4">
|
||||||
|
<div className="w-12 h-12 bg-[var(--color-primary)]/10 rounded-xl flex items-center justify-center">
|
||||||
|
<activeContent.icon className="w-6 h-6 text-[var(--color-primary)]" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-3xl font-extrabold text-[var(--text-primary)]">
|
||||||
|
{activeContent.title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-[var(--text-secondary)]">{activeContent.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Articles Grid */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{activeContent.articles.map((article) => (
|
||||||
|
<a
|
||||||
|
key={article.id}
|
||||||
|
href={`#${article.id}`}
|
||||||
|
className="block bg-[var(--bg-secondary)] rounded-xl p-6 border border-[var(--border-primary)] hover:border-[var(--color-primary)] hover:shadow-xl transition-all group"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-xl font-bold text-[var(--text-primary)] mb-2 group-hover:text-[var(--color-primary)] transition-colors">
|
||||||
|
{article.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-[var(--text-secondary)] mb-4">{article.description}</p>
|
||||||
|
<div className="flex items-center gap-4 text-sm">
|
||||||
|
<span className="flex items-center gap-1 text-[var(--text-tertiary)]">
|
||||||
|
<PlayCircle className="w-4 h-4" />
|
||||||
|
{article.readTime}
|
||||||
|
</span>
|
||||||
|
<span className={`font-medium ${getDifficultyColor(article.difficulty)}`}>
|
||||||
|
{getDifficultyLabel(article.difficulty)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ChevronRight className="w-6 h-6 text-[var(--text-tertiary)] group-hover:text-[var(--color-primary)] transition-colors flex-shrink-0" />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Video Tutorials */}
|
||||||
|
<section className="py-20 bg-[var(--bg-secondary)]">
|
||||||
|
<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">
|
||||||
|
Tutoriales en Vídeo
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)]">
|
||||||
|
Aprende viendo (próximamente)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-6">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
title: 'Configuración Inicial',
|
||||||
|
duration: '5:30',
|
||||||
|
thumbnail: 'getting-started',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Importar Datos Históricos',
|
||||||
|
duration: '8:15',
|
||||||
|
thumbnail: 'import-data',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tu Primera Predicción',
|
||||||
|
duration: '12:00',
|
||||||
|
thumbnail: 'first-prediction',
|
||||||
|
},
|
||||||
|
].map((video, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="bg-[var(--bg-primary)] rounded-xl overflow-hidden border border-[var(--border-primary)] hover:shadow-xl transition-all group"
|
||||||
|
>
|
||||||
|
<div className="aspect-video bg-gradient-to-br from-[var(--color-primary)]/20 to-orange-600/20 flex items-center justify-center relative">
|
||||||
|
<PlayCircle className="w-16 h-16 text-[var(--color-primary)] group-hover:scale-110 transition-transform" />
|
||||||
|
<div className="absolute bottom-2 right-2 bg-black/80 text-white px-2 py-1 rounded text-xs font-medium">
|
||||||
|
{video.duration}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<h3 className="font-bold text-[var(--text-primary)] group-hover:text-[var(--color-primary)] transition-colors">
|
||||||
|
{video.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Additional Resources */}
|
||||||
|
<section className="py-20 bg-[var(--bg-primary)]">
|
||||||
|
<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">
|
||||||
|
Recursos Adicionales
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-6">
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-2xl p-6 border-2 border-blue-200 dark:border-blue-800">
|
||||||
|
<Info className="w-8 h-8 text-blue-600 mb-4" />
|
||||||
|
<h3 className="text-lg font-bold text-[var(--text-primary)] mb-2">
|
||||||
|
Glosario de Términos
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
||||||
|
Definiciones de conceptos clave y terminología técnica
|
||||||
|
</p>
|
||||||
|
<a href="#glossary" className="text-blue-600 hover:underline font-medium text-sm">
|
||||||
|
Ver Glosario →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-amber-50 dark:bg-amber-900/20 rounded-2xl p-6 border-2 border-amber-200 dark:border-amber-800">
|
||||||
|
<AlertCircle className="w-8 h-8 text-amber-600 mb-4" />
|
||||||
|
<h3 className="text-lg font-bold text-[var(--text-primary)] mb-2">
|
||||||
|
Solución de Problemas
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
||||||
|
Errores comunes y cómo resolverlos rápidamente
|
||||||
|
</p>
|
||||||
|
<Link to="/help" className="text-amber-600 hover:underline font-medium text-sm">
|
||||||
|
Ver Soluciones →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-green-50 dark:bg-green-900/20 rounded-2xl p-6 border-2 border-green-200 dark:border-green-800">
|
||||||
|
<CheckCircle2 className="w-8 h-8 text-green-600 mb-4" />
|
||||||
|
<h3 className="text-lg font-bold text-[var(--text-primary)] mb-2">
|
||||||
|
Mejores Prácticas
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
||||||
|
Tips y consejos de expertos para optimizar tu uso
|
||||||
|
</p>
|
||||||
|
<a href="#best-practices" className="text-green-600 hover:underline font-medium text-sm">
|
||||||
|
Leer Tips →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<section className="py-20 bg-gradient-to-r from-[var(--color-primary)] to-orange-600">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||||
|
<h2 className="text-3xl lg:text-4xl font-bold text-white mb-6">
|
||||||
|
¿Listo Para Empezar?
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-white/90 mb-8">
|
||||||
|
Regístrate en el programa piloto y obtén 3 meses gratis
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
to="/register"
|
||||||
|
className="inline-flex items-center gap-2 px-8 py-4 bg-white text-[var(--color-primary)] rounded-xl font-bold hover:shadow-2xl transition-all hover:scale-105"
|
||||||
|
>
|
||||||
|
<span>Crear Cuenta Gratis</span>
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</PublicLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DocumentationPage;
|
||||||
435
frontend/src/pages/public/FeedbackPage.tsx
Normal file
435
frontend/src/pages/public/FeedbackPage.tsx
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PublicLayout } from '../../components/layout';
|
||||||
|
import {
|
||||||
|
MessageSquare,
|
||||||
|
Heart,
|
||||||
|
ThumbsUp,
|
||||||
|
ThumbsDown,
|
||||||
|
Lightbulb,
|
||||||
|
AlertTriangle,
|
||||||
|
Send,
|
||||||
|
CheckCircle2,
|
||||||
|
AlertCircle,
|
||||||
|
Star
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface FeedbackCategory {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FeedbackPage: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [formState, setFormState] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
category: 'suggestion' as 'suggestion' | 'bug' | 'feature' | 'praise' | 'complaint',
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
rating: 0,
|
||||||
|
});
|
||||||
|
const [submitStatus, setSubmitStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||||
|
|
||||||
|
const categories: FeedbackCategory[] = [
|
||||||
|
{
|
||||||
|
id: 'suggestion',
|
||||||
|
title: 'Sugerencia',
|
||||||
|
description: 'Ideas para mejorar el producto',
|
||||||
|
icon: Lightbulb,
|
||||||
|
color: 'blue',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'feature',
|
||||||
|
title: 'Nueva Funcionalidad',
|
||||||
|
description: 'Solicita una característica nueva',
|
||||||
|
icon: Star,
|
||||||
|
color: 'purple',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bug',
|
||||||
|
title: 'Reportar Bug',
|
||||||
|
description: 'Algo no funciona como debería',
|
||||||
|
icon: AlertTriangle,
|
||||||
|
color: 'red',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'praise',
|
||||||
|
title: 'Elogio',
|
||||||
|
description: '¡Algo te encanta!',
|
||||||
|
icon: Heart,
|
||||||
|
color: 'green',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'complaint',
|
||||||
|
title: 'Queja',
|
||||||
|
description: 'Algo te molesta o decepciona',
|
||||||
|
icon: ThumbsDown,
|
||||||
|
color: 'amber',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||||
|
setFormState({
|
||||||
|
...formState,
|
||||||
|
[e.target.name]: e.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRatingChange = (rating: number) => {
|
||||||
|
setFormState({
|
||||||
|
...formState,
|
||||||
|
rating,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSubmitStatus('loading');
|
||||||
|
|
||||||
|
// Simulate API call
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
|
// In production, this would be an actual API call
|
||||||
|
console.log('Feedback submitted:', formState);
|
||||||
|
|
||||||
|
setSubmitStatus('success');
|
||||||
|
setTimeout(() => {
|
||||||
|
setSubmitStatus('idle');
|
||||||
|
setFormState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
category: 'suggestion',
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
rating: 0,
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCategoryColor = (color: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
blue: 'from-blue-500/10 to-blue-600/10 border-blue-500/20',
|
||||||
|
purple: 'from-purple-500/10 to-purple-600/10 border-purple-500/20',
|
||||||
|
red: 'from-red-500/10 to-red-600/10 border-red-500/20',
|
||||||
|
green: 'from-green-500/10 to-green-600/10 border-green-500/20',
|
||||||
|
amber: 'from-amber-500/10 to-amber-600/10 border-amber-500/20',
|
||||||
|
};
|
||||||
|
return colors[color] || colors.blue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCategoryIconColor = (color: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
blue: 'text-blue-600',
|
||||||
|
purple: 'text-purple-600',
|
||||||
|
red: 'text-red-600',
|
||||||
|
green: 'text-green-600',
|
||||||
|
amber: 'text-amber-600',
|
||||||
|
};
|
||||||
|
return colors[color] || colors.blue;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PublicLayout
|
||||||
|
variant="default"
|
||||||
|
contentPadding="default"
|
||||||
|
headerProps={{
|
||||||
|
showThemeToggle: true,
|
||||||
|
showAuthButtons: true,
|
||||||
|
showLanguageSelector: true,
|
||||||
|
variant: 'default',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 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">
|
||||||
|
<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">
|
||||||
|
<MessageSquare className="w-4 h-4" />
|
||||||
|
<span>Tu Opinión Importa</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl lg:text-6xl font-extrabold text-[var(--text-primary)] mb-6">
|
||||||
|
Ayúdanos a Mejorar
|
||||||
|
<span className="block text-[var(--color-primary)]">Panadería IA</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)] leading-relaxed mb-8">
|
||||||
|
Tu feedback es fundamental para construir el mejor producto para panaderías artesanales
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Why Feedback Matters */}
|
||||||
|
<section className="py-20 bg-[var(--bg-primary)]">
|
||||||
|
<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">
|
||||||
|
¿Por Qué Tu Feedback Es Importante?
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)]">
|
||||||
|
Estamos construyendo esto juntos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-6 mb-16">
|
||||||
|
<div className="bg-[var(--bg-secondary)] rounded-2xl p-6 border border-[var(--border-primary)] text-center">
|
||||||
|
<ThumbsUp className="w-12 h-12 text-[var(--color-primary)] mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-bold text-[var(--text-primary)] mb-2">
|
||||||
|
Priorizamos Tu Feedback
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">
|
||||||
|
Cada sugerencia es revisada y considerada para el roadmap de producto
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-[var(--bg-secondary)] rounded-2xl p-6 border border-[var(--border-primary)] text-center">
|
||||||
|
<Lightbulb className="w-12 h-12 text-[var(--color-primary)] mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-bold text-[var(--text-primary)] mb-2">
|
||||||
|
Tus Ideas Nos Inspiran
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">
|
||||||
|
Las mejores funcionalidades vienen directamente de nuestros usuarios
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-[var(--bg-secondary)] rounded-2xl p-6 border border-[var(--border-primary)] text-center">
|
||||||
|
<Heart className="w-12 h-12 text-[var(--color-primary)] mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-bold text-[var(--text-primary)] mb-2">
|
||||||
|
Comunidad Activa
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">
|
||||||
|
Contribuyes a crear una herramienta que toda la comunidad panadera disfrutará
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Feedback Form */}
|
||||||
|
<section className="py-20 bg-[var(--bg-secondary)]">
|
||||||
|
<div className="max-w-4xl 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">
|
||||||
|
Comparte Tu Feedback
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)]">
|
||||||
|
Queremos escucharte
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category Selection */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<h3 className="text-lg font-bold text-[var(--text-primary)] mb-4 text-center">
|
||||||
|
¿Qué tipo de feedback quieres compartir?
|
||||||
|
</h3>
|
||||||
|
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{categories.map((category) => {
|
||||||
|
const CategoryIcon = category.icon;
|
||||||
|
const isSelected = formState.category === category.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={category.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFormState({ ...formState, category: category.id as any })}
|
||||||
|
className={`bg-gradient-to-br ${getCategoryColor(category.color)} rounded-xl p-6 border-2 transition-all text-left ${
|
||||||
|
isSelected
|
||||||
|
? 'border-[var(--color-primary)] shadow-lg'
|
||||||
|
: 'border-transparent hover:border-[var(--color-primary)]/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CategoryIcon className={`w-8 h-8 ${getCategoryIconColor(category.color)} mb-3`} />
|
||||||
|
<h4 className="font-bold text-[var(--text-primary)] mb-1">{category.title}</h4>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">{category.description}</p>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="bg-[var(--bg-primary)] rounded-2xl p-8 border border-[var(--border-primary)]">
|
||||||
|
{/* Success/Error Messages */}
|
||||||
|
{submitStatus === 'success' && (
|
||||||
|
<div className="mb-6 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-xl flex items-center gap-3">
|
||||||
|
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0" />
|
||||||
|
<p className="text-sm text-green-800 dark:text-green-200">
|
||||||
|
<strong>¡Gracias por tu feedback!</strong> Lo revisaremos pronto.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{submitStatus === 'error' && (
|
||||||
|
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl flex items-center gap-3">
|
||||||
|
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0" />
|
||||||
|
<p className="text-sm text-red-800 dark:text-red-200">
|
||||||
|
<strong>Error al enviar.</strong> Por favor, inténtalo de nuevo.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
{/* Name */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="name" className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||||
|
Tu Nombre <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
value={formState.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-xl text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] focus:outline-none focus:border-[var(--color-primary)] transition-colors"
|
||||||
|
placeholder="Tu nombre"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Email */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email" className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||||
|
Email <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
value={formState.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-xl text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] focus:outline-none focus:border-[var(--color-primary)] transition-colors"
|
||||||
|
placeholder="tu@email.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rating */}
|
||||||
|
{formState.category === 'praise' && (
|
||||||
|
<div className="mt-6">
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-3">
|
||||||
|
¿Cómo calificarías tu experiencia? <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{[1, 2, 3, 4, 5].map((rating) => (
|
||||||
|
<button
|
||||||
|
key={rating}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleRatingChange(rating)}
|
||||||
|
className="transition-transform hover:scale-110"
|
||||||
|
>
|
||||||
|
<Star
|
||||||
|
className={`w-10 h-10 ${
|
||||||
|
rating <= formState.rating
|
||||||
|
? 'fill-yellow-500 text-yellow-500'
|
||||||
|
: 'text-[var(--border-primary)]'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<label htmlFor="title" className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||||
|
Título <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="title"
|
||||||
|
name="title"
|
||||||
|
value={formState.title}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-xl text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] focus:outline-none focus:border-[var(--color-primary)] transition-colors"
|
||||||
|
placeholder="Resumen en una línea"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<label htmlFor="description" className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||||
|
Descripción Detallada <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
value={formState.description}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
rows={6}
|
||||||
|
className="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-xl text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] focus:outline-none focus:border-[var(--color-primary)] transition-colors resize-none"
|
||||||
|
placeholder={
|
||||||
|
formState.category === 'bug'
|
||||||
|
? 'Describe el problema: ¿qué esperabas que pasara? ¿qué pasó en su lugar? ¿cómo podemos reproducirlo?'
|
||||||
|
: formState.category === 'feature'
|
||||||
|
? 'Describe la funcionalidad que te gustaría ver: ¿qué problema resolvería? ¿cómo lo imaginas?'
|
||||||
|
: 'Cuéntanos más sobre tu feedback...'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<div className="mt-8">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={submitStatus === 'loading'}
|
||||||
|
className="w-full flex items-center justify-center gap-2 px-8 py-4 bg-[var(--color-primary)] text-white rounded-xl font-bold hover:shadow-xl transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
|
||||||
|
>
|
||||||
|
{submitStatus === 'loading' ? (
|
||||||
|
<>
|
||||||
|
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||||
|
<span>Enviando...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Send className="w-5 h-5" />
|
||||||
|
<span>Enviar Feedback</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-[var(--text-tertiary)] text-center mt-4">
|
||||||
|
Tu feedback nos ayuda a mejorar. ¡Gracias por tomarte el tiempo!
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Other Ways to Connect */}
|
||||||
|
<section className="py-20 bg-[var(--bg-primary)]">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="bg-gradient-to-br from-[var(--color-primary)]/10 to-orange-600/10 rounded-2xl p-8 border border-[var(--color-primary)]/20 text-center">
|
||||||
|
<h3 className="text-2xl font-bold text-[var(--text-primary)] mb-4">
|
||||||
|
¿Prefieres Hablar Directamente?
|
||||||
|
</h3>
|
||||||
|
<p className="text-[var(--text-secondary)] mb-6">
|
||||||
|
Estamos disponibles por múltiples canales
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap justify-center gap-4">
|
||||||
|
<a
|
||||||
|
href="/help/support"
|
||||||
|
className="inline-flex items-center gap-2 px-6 py-3 bg-[var(--color-primary)] text-white rounded-xl font-bold hover:shadow-xl transition-all hover:scale-105"
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-5 h-5" />
|
||||||
|
<span>Contactar Soporte</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/help"
|
||||||
|
className="inline-flex items-center gap-2 px-6 py-3 border-2 border-[var(--border-primary)] text-[var(--text-primary)] rounded-xl font-medium hover:border-[var(--color-primary)] transition-all"
|
||||||
|
>
|
||||||
|
Centro de Ayuda
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</PublicLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeedbackPage;
|
||||||
408
frontend/src/pages/public/HelpCenterPage.tsx
Normal file
408
frontend/src/pages/public/HelpCenterPage.tsx
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PublicLayout } from '../../components/layout';
|
||||||
|
import {
|
||||||
|
HelpCircle,
|
||||||
|
Search,
|
||||||
|
BookOpen,
|
||||||
|
MessageSquare,
|
||||||
|
FileText,
|
||||||
|
Users,
|
||||||
|
Settings,
|
||||||
|
CreditCard,
|
||||||
|
Shield,
|
||||||
|
TrendingUp,
|
||||||
|
Package,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
ExternalLink,
|
||||||
|
Mail,
|
||||||
|
Phone,
|
||||||
|
Clock
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface FAQItem {
|
||||||
|
question: string;
|
||||||
|
answer: string;
|
||||||
|
category: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HelpCategory {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HelpCenterPage: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [expandedFAQ, setExpandedFAQ] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const categories: HelpCategory[] = [
|
||||||
|
{
|
||||||
|
id: 'getting-started',
|
||||||
|
title: 'Primeros Pasos',
|
||||||
|
description: 'Configura tu cuenta y aprende los conceptos básicos',
|
||||||
|
icon: BookOpen,
|
||||||
|
link: '/help/docs#getting-started',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'features',
|
||||||
|
title: 'Funcionalidades',
|
||||||
|
description: 'Guías completas sobre todas las características',
|
||||||
|
icon: Package,
|
||||||
|
link: '/help/docs#features',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'billing',
|
||||||
|
title: 'Facturación y Planes',
|
||||||
|
description: 'Información sobre precios, pagos y suscripciones',
|
||||||
|
icon: CreditCard,
|
||||||
|
link: '/help/docs#billing',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'account',
|
||||||
|
title: 'Gestión de Cuenta',
|
||||||
|
description: 'Administra tu perfil, equipo y configuración',
|
||||||
|
icon: Settings,
|
||||||
|
link: '/help/docs#account',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'privacy',
|
||||||
|
title: 'Privacidad y Seguridad',
|
||||||
|
description: 'RGPD, seguridad de datos y cumplimiento',
|
||||||
|
icon: Shield,
|
||||||
|
link: '/help/docs#privacy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'analytics',
|
||||||
|
title: 'Análisis y Predicciones',
|
||||||
|
description: 'Interpreta datos e insights de IA',
|
||||||
|
icon: TrendingUp,
|
||||||
|
link: '/help/docs#analytics',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const faqs: FAQItem[] = [
|
||||||
|
{
|
||||||
|
category: 'general',
|
||||||
|
question: '¿Qué es Panadería IA y cómo funciona?',
|
||||||
|
answer: 'Panadería IA es una plataforma de gestión inteligente para panaderías artesanales. Utiliza inteligencia artificial para predecir la demanda de tus productos, optimizar la producción, reducir desperdicios y aumentar la rentabilidad. Conectas tus datos de ventas históricas y el sistema aprende tus patrones para hacer predicciones precisas.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'general',
|
||||||
|
question: '¿Cuánto tiempo toma configurar el sistema?',
|
||||||
|
answer: 'La configuración inicial toma entre 10-15 minutos. Necesitas: 1) Crear tu cuenta, 2) Subir tu catálogo de productos, 3) Importar historial de ventas (opcional pero recomendado), 4) Configurar preferencias básicas. El sistema empieza a generar predicciones desde el primer día, mejorando su precisión con el tiempo.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'general',
|
||||||
|
question: '¿Necesito conocimientos técnicos para usarlo?',
|
||||||
|
answer: 'No. Panadería IA está diseñado para panaderos, no para ingenieros. La interfaz es intuitiva, con tutoriales paso a paso. Si sabes usar WhatsApp o un correo electrónico, puedes usar nuestra plataforma. Además, ofrecemos soporte 24/7 en español.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'pricing',
|
||||||
|
question: '¿Cuánto cuesta el programa piloto?',
|
||||||
|
answer: 'El programa piloto es GRATIS durante los primeros 3 meses. Después, pagas solo €49/mes con un 40% de descuento de por vida (precio normal: €79/mes). Sin contratos de permanencia, cancela cuando quieras. Las primeras 20 panaderías obtienen este beneficio.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'pricing',
|
||||||
|
question: '¿Necesito tarjeta de crédito para empezar el piloto?',
|
||||||
|
answer: 'SÍ, necesitas registrar una tarjeta, pero NO se te cobrará durante los 3 meses de prueba gratuita. Esto nos ayuda a asegurar que solo participan panaderías realmente interesadas. Puedes cancelar antes de que termine el periodo gratuito sin ningún cargo.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'pricing',
|
||||||
|
question: '¿Qué pasa después de los 3 meses gratis?',
|
||||||
|
answer: 'Tienes 3 opciones: 1) Continuar con el plan Basic (€49/mes con tu descuento del 40%), 2) Actualizar a un plan superior, 3) Cancelar sin penalización. Te avisaremos 7 días antes de que termine tu periodo gratuito.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'technical',
|
||||||
|
question: '¿Cómo importo mis datos históricos de ventas?',
|
||||||
|
answer: 'Aceptamos varios formatos: Excel (.xlsx), CSV, o exportaciones directas de tu TPV si es compatible. También puedes introducir datos manualmente si prefieres. Cuanto más historial proporciones (recomendamos mínimo 3 meses), más precisas serán las predicciones.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'technical',
|
||||||
|
question: '¿El sistema se integra con mi TPV actual?',
|
||||||
|
answer: 'Actualmente estamos trabajando en integraciones directas con los principales TPV del mercado español. Por ahora, puedes exportar datos de tu TPV e importarlos manualmente (es más fácil de lo que suena). Las integraciones automáticas llegarán en Q2 2025.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'technical',
|
||||||
|
question: '¿Qué pasa si mis predicciones no son precisas?',
|
||||||
|
answer: 'El sistema mejora con el tiempo. Las primeras semanas puede tener un margen de error del 15-20%. Después del primer mes, esto baja al 10%. Con 3+ meses de datos, alcanzamos >90% de precisión. Puedes ajustar manualmente las predicciones y el sistema aprende de tus correcciones.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'privacy',
|
||||||
|
question: '¿Dónde se almacenan mis datos?',
|
||||||
|
answer: 'TODOS tus datos se almacenan en servidores físicamente ubicados en España (Barcelona y Madrid), cumpliendo 100% con RGPD. Nunca compartimos, vendemos ni transferimos tus datos fuera de la UE. Tienes control total y puedes exportar o eliminar tus datos en cualquier momento.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'privacy',
|
||||||
|
question: '¿Quién puede ver mis datos de ventas?',
|
||||||
|
answer: 'Solo TÚ y los miembros de tu equipo que tú autorices. Ni siquiera nuestro equipo técnico puede acceder a tus datos sin tu permiso explícito (y solo lo haríamos para soporte técnico con tu aprobación). Los datos están encriptados end-to-end.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'support',
|
||||||
|
question: '¿Qué tipo de soporte ofrecen?',
|
||||||
|
answer: 'Soporte 24/7 en español por: Email (respuesta en <4h), Chat en vivo (9:00-21:00), Videollamada (con cita previa). Durante el piloto, también tienes acceso directo a los fundadores por WhatsApp. Además, biblioteca completa de tutoriales en vídeo.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'support',
|
||||||
|
question: '¿Puedo hablar con alguien antes de registrarme?',
|
||||||
|
answer: 'Por supuesto. Agenda una videollamada de 15 minutos con nuestro equipo para ver el producto en acción, hacer todas tus preguntas y confirmar que es adecuado para tu panadería. Sin compromiso. Contacto: hola@panaderia-ia.com o el formulario de contacto.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'general',
|
||||||
|
question: '¿Funciona para obradores centrales con múltiples puntos de venta?',
|
||||||
|
answer: 'SÍ. Nuestro sistema está diseñado para adaptarse a ambos modelos: producción local (un solo punto) y obrador central con distribución a múltiples puntos de venta. Para obradores centrales, ofrecemos predicción de demanda agregada y granular por cada POS, gestión de distribución multi-ubicación, y un dashboard centralizado con visibilidad por punto de venta. La IA optimiza tanto la producción total como la distribución entre ubicaciones.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'general',
|
||||||
|
question: '¿Qué modelo de negocio es mejor para mi panadería?',
|
||||||
|
answer: 'Depende de tus objetivos. Producción Local es ideal si valoras máximo control, flexibilidad y frescura instantánea, o si estás empezando. Obrador Central + Puntos de Venta es mejor si quieres escalar a múltiples ubicaciones, aprovechar economías de escala, o tener presencia en varios barrios/ciudades. Lo bueno: nuestro sistema funciona para ambos modelos y puede evolucionar contigo si decides cambiar o crecer.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const filteredFAQs = searchQuery
|
||||||
|
? faqs.filter(
|
||||||
|
(faq) =>
|
||||||
|
faq.question.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
faq.answer.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
)
|
||||||
|
: faqs;
|
||||||
|
|
||||||
|
const toggleFAQ = (index: number) => {
|
||||||
|
setExpandedFAQ(expandedFAQ === index ? null : index);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PublicLayout
|
||||||
|
variant="default"
|
||||||
|
contentPadding="default"
|
||||||
|
headerProps={{
|
||||||
|
showThemeToggle: true,
|
||||||
|
showAuthButtons: true,
|
||||||
|
showLanguageSelector: true,
|
||||||
|
variant: 'default',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 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">
|
||||||
|
<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">
|
||||||
|
<HelpCircle className="w-4 h-4" />
|
||||||
|
<span>Centro de Ayuda</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl lg:text-6xl font-extrabold text-[var(--text-primary)] mb-6">
|
||||||
|
¿Cómo Podemos
|
||||||
|
<span className="block text-[var(--color-primary)]">Ayudarte Hoy?</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)] leading-relaxed mb-8">
|
||||||
|
Encuentra respuestas rápidas, guías completas y contacto directo con nuestro equipo
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Search Bar */}
|
||||||
|
<div className="max-w-2xl mx-auto">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-tertiary)]" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Buscar en la ayuda... (ej: ¿cómo importo datos?)"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="w-full pl-12 pr-4 py-4 bg-[var(--bg-primary)] border-2 border-[var(--border-primary)] rounded-xl text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] focus:outline-none focus:border-[var(--color-primary)] transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Quick Links / Categories */}
|
||||||
|
<section className="py-20 bg-[var(--bg-primary)]">
|
||||||
|
<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">
|
||||||
|
Explora por Categoría
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)]">
|
||||||
|
Encuentra lo que necesitas más rápido
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{categories.map((category) => (
|
||||||
|
<Link
|
||||||
|
key={category.id}
|
||||||
|
to={category.link}
|
||||||
|
className="bg-[var(--bg-secondary)] rounded-2xl p-6 border border-[var(--border-primary)] hover:border-[var(--color-primary)] hover:shadow-xl transition-all duration-300 group"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="w-12 h-12 bg-[var(--color-primary)]/10 rounded-xl flex items-center justify-center flex-shrink-0 group-hover:bg-[var(--color-primary)] transition-colors">
|
||||||
|
<category.icon className="w-6 h-6 text-[var(--color-primary)] group-hover:text-white transition-colors" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-lg font-bold text-[var(--text-primary)] mb-2 group-hover:text-[var(--color-primary)] transition-colors">
|
||||||
|
{category.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">{category.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* FAQs */}
|
||||||
|
<section className="py-20 bg-[var(--bg-secondary)]">
|
||||||
|
<div className="max-w-4xl 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">
|
||||||
|
Preguntas Frecuentes
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)]">
|
||||||
|
{filteredFAQs.length} {filteredFAQs.length === 1 ? 'respuesta' : 'respuestas'}{' '}
|
||||||
|
{searchQuery && 'encontradas'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{filteredFAQs.map((faq, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)] overflow-hidden"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleFAQ(index)}
|
||||||
|
className="w-full px-6 py-4 flex items-center justify-between text-left hover:bg-[var(--bg-secondary)] transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-lg font-bold text-[var(--text-primary)] pr-4">
|
||||||
|
{faq.question}
|
||||||
|
</span>
|
||||||
|
{expandedFAQ === index ? (
|
||||||
|
<ChevronUp className="w-5 h-5 text-[var(--color-primary)] flex-shrink-0" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-5 h-5 text-[var(--text-tertiary)] flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{expandedFAQ === index && (
|
||||||
|
<div className="px-6 pb-4 text-[var(--text-secondary)] leading-relaxed">
|
||||||
|
{faq.answer}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredFAQs.length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<HelpCircle className="w-16 h-16 text-[var(--text-tertiary)] mx-auto mb-4" />
|
||||||
|
<p className="text-[var(--text-secondary)] text-lg">
|
||||||
|
No encontramos resultados para "{searchQuery}".{' '}
|
||||||
|
<Link to="/help/support" className="text-[var(--color-primary)] hover:underline">
|
||||||
|
Contacta con soporte
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Contact Support */}
|
||||||
|
<section className="py-20 bg-[var(--bg-primary)]">
|
||||||
|
<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">
|
||||||
|
¿No Encuentras lo Que Buscas?
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)]">
|
||||||
|
Nuestro equipo está aquí para ayudarte
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-6">
|
||||||
|
<Link
|
||||||
|
to="/help/support"
|
||||||
|
className="bg-[var(--bg-secondary)] rounded-2xl p-8 border border-[var(--border-primary)] hover:border-[var(--color-primary)] hover:shadow-xl transition-all text-center group"
|
||||||
|
>
|
||||||
|
<div className="w-16 h-16 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center mx-auto mb-4 group-hover:bg-[var(--color-primary)] transition-colors">
|
||||||
|
<MessageSquare className="w-8 h-8 text-[var(--color-primary)] group-hover:text-white transition-colors" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-[var(--text-primary)] mb-2">
|
||||||
|
Chat en Vivo
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
||||||
|
Respuesta inmediata de 9:00 a 21:00
|
||||||
|
</p>
|
||||||
|
<span className="text-[var(--color-primary)] font-medium group-hover:underline">
|
||||||
|
Iniciar Chat →
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="mailto:soporte@panaderia-ia.com"
|
||||||
|
className="bg-[var(--bg-secondary)] rounded-2xl p-8 border border-[var(--border-primary)] hover:border-[var(--color-primary)] hover:shadow-xl transition-all text-center group"
|
||||||
|
>
|
||||||
|
<div className="w-16 h-16 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center mx-auto mb-4 group-hover:bg-[var(--color-primary)] transition-colors">
|
||||||
|
<Mail className="w-8 h-8 text-[var(--color-primary)] group-hover:text-white transition-colors" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-[var(--text-primary)] mb-2">
|
||||||
|
Email
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
||||||
|
Respuesta en menos de 4 horas
|
||||||
|
</p>
|
||||||
|
<span className="text-[var(--color-primary)] font-medium group-hover:underline">
|
||||||
|
Enviar Email →
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/help/docs"
|
||||||
|
className="bg-[var(--bg-secondary)] rounded-2xl p-8 border border-[var(--border-primary)] hover:border-[var(--color-primary)] hover:shadow-xl transition-all text-center group"
|
||||||
|
>
|
||||||
|
<div className="w-16 h-16 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center mx-auto mb-4 group-hover:bg-[var(--color-primary)] transition-colors">
|
||||||
|
<FileText className="w-8 h-8 text-[var(--color-primary)] group-hover:text-white transition-colors" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-[var(--text-primary)] mb-2">
|
||||||
|
Documentación
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
||||||
|
Guías completas y tutoriales
|
||||||
|
</p>
|
||||||
|
<span className="text-[var(--color-primary)] font-medium group-hover:underline">
|
||||||
|
Ver Docs →
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Contact Hours */}
|
||||||
|
<section className="py-12 bg-[var(--bg-secondary)]">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="bg-[var(--bg-primary)] rounded-2xl p-8 border border-[var(--border-primary)]">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<Clock className="w-6 h-6 text-[var(--color-primary)] flex-shrink-0 mt-1" />
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold text-[var(--text-primary)] mb-2">
|
||||||
|
Horario de Atención
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-1 text-sm text-[var(--text-secondary)]">
|
||||||
|
<p><strong>Chat en Vivo:</strong> Lunes a Viernes 9:00 - 21:00, Sábados 10:00 - 18:00</p>
|
||||||
|
<p><strong>Email:</strong> 24/7 (respuesta en menos de 4 horas en horario laboral)</p>
|
||||||
|
<p><strong>Teléfono:</strong> Lunes a Viernes 10:00 - 19:00 (solo para clientes activos)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</PublicLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HelpCenterPage;
|
||||||
@@ -22,7 +22,9 @@ import {
|
|||||||
Package,
|
Package,
|
||||||
PieChart,
|
PieChart,
|
||||||
Settings,
|
Settings,
|
||||||
Brain
|
Brain,
|
||||||
|
Store,
|
||||||
|
Network
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const LandingPage: React.FC = () => {
|
const LandingPage: React.FC = () => {
|
||||||
@@ -48,7 +50,7 @@ const LandingPage: React.FC = () => {
|
|||||||
{ id: 'features', label: t('landing:navigation.features', 'Características'), href: '#features' },
|
{ id: 'features', label: t('landing:navigation.features', 'Características'), href: '#features' },
|
||||||
{ id: 'benefits', label: t('landing:navigation.benefits', 'Beneficios'), href: '#benefits' },
|
{ id: 'benefits', label: t('landing:navigation.benefits', 'Beneficios'), href: '#benefits' },
|
||||||
{ id: 'pricing', label: t('landing:navigation.pricing', 'Precios'), href: '#pricing' },
|
{ id: 'pricing', label: t('landing:navigation.pricing', 'Precios'), href: '#pricing' },
|
||||||
{ id: 'testimonials', label: t('landing:navigation.testimonials', 'Testimonios'), href: '#testimonials' }
|
{ id: 'faq', label: t('landing:navigation.faq', 'Preguntas Frecuentes'), href: '#faq' }
|
||||||
]
|
]
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -69,59 +71,101 @@ const LandingPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-4xl tracking-tight font-extrabold text-[var(--text-primary)] sm:text-5xl lg:text-7xl">
|
<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', 'IA que Reduce')}</span>
|
<span className="block">{t('landing:hero.title_line1', 'Aumenta Ganancias,')}</span>
|
||||||
<span className="block text-[var(--color-primary)]">{t('landing:hero.title_line2', 'Desperdicio Alimentario')}</span>
|
<span className="block text-[var(--color-primary)]">{t('landing:hero.title_line2', 'Reduce Desperdicios')}</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="mt-6 max-w-3xl mx-auto text-lg text-[var(--text-secondary)] sm:text-xl">
|
<p className="mt-6 max-w-3xl mx-auto text-lg text-[var(--text-secondary)] sm:text-xl">
|
||||||
{t('landing:hero.subtitle', 'Tecnología de inteligencia artificial que reduce hasta un 35% el desperdicio alimentario, optimiza tu producción y protege tu información. Tus datos son 100% tuyos.')}
|
{t('landing:hero.subtitle', 'Plataforma de IA diseñada para panaderías artesanales que quieren producir exactamente lo que van a vender. Reduce desperdicio alimentario, mejora márgenes y ahorra tiempo en planificación. 100% española, tus datos son tuyos.')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Pilot Launch Banner */}
|
{/* Pilot Launch Banner */}
|
||||||
<div className="mt-8 inline-block">
|
<div className="mt-8 inline-block">
|
||||||
<div className="bg-gradient-to-r from-amber-500/10 to-orange-500/10 border-2 border-amber-500/30 rounded-xl px-6 py-4">
|
<div className="relative bg-gradient-to-br 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-2xl px-8 py-6 shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:scale-105">
|
||||||
<div className="flex items-center justify-center gap-2 text-amber-600 dark:text-amber-400 font-bold text-lg">
|
{/* Decorative elements */}
|
||||||
<Star className="w-5 h-5 fill-current" />
|
<div className="absolute -top-2 -right-2 w-16 h-16 bg-amber-400/20 rounded-full blur-xl"></div>
|
||||||
<span>¡Lanzamiento Piloto!</span>
|
<div className="absolute -bottom-2 -left-2 w-16 h-16 bg-orange-400/20 rounded-full blur-xl"></div>
|
||||||
<Star className="w-5 h-5 fill-current" />
|
|
||||||
|
<div className="relative">
|
||||||
|
<div className="flex items-center justify-center gap-3 mb-3">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Star className="w-6 h-6 text-amber-500 fill-amber-500 animate-pulse" />
|
||||||
|
<Star className="w-5 h-5 text-amber-400 fill-amber-400" />
|
||||||
|
</div>
|
||||||
|
<span className="text-xl font-extrabold bg-gradient-to-r from-amber-600 to-orange-600 dark:from-amber-400 dark:to-orange-400 bg-clip-text text-transparent">
|
||||||
|
¡Lanzamiento Piloto!
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Star className="w-5 h-5 text-amber-400 fill-amber-400" />
|
||||||
|
<Star className="w-6 h-6 text-amber-500 fill-amber-500 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-base text-[var(--text-secondary)] font-medium">
|
||||||
|
<span className="inline-block px-3 py-1 bg-gradient-to-r from-[var(--color-primary)] to-orange-600 text-white font-bold text-lg rounded-lg shadow-md mr-1">
|
||||||
|
3 MESES GRATIS
|
||||||
|
</span>
|
||||||
|
<span className="block mt-2 text-sm">
|
||||||
|
para los primeros en unirse al piloto
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-sm text-[var(--text-secondary)] text-center">
|
|
||||||
<strong className="text-[var(--color-primary)]">3 meses GRATIS</strong> para early adopters que se registren ahora
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 flex flex-col sm:flex-row gap-4 justify-center">
|
<div className="mt-10 flex flex-col sm:flex-row gap-5 justify-center items-center">
|
||||||
<Link to="/register">
|
<Link to="/register?pilot=true" className="w-full sm:w-auto">
|
||||||
<Button size="lg" className="px-8 py-4 text-lg font-semibold bg-[var(--color-primary)] hover:bg-[var(--color-primary-dark)] text-white shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200">
|
<Button
|
||||||
{t('landing:hero.cta_primary', 'Comenzar GRATIS 3 Meses')}
|
size="lg"
|
||||||
<ArrowRight className="ml-2 w-5 h-5" />
|
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:hero.cta_primary', 'Comenzar Gratis Ahora')}
|
||||||
|
<ArrowRight className="w-6 h-6 group-hover:translate-x-1 transition-transform duration-200" />
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/demo">
|
<Link to="/demo" className="w-full sm:w-auto">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="px-8 py-4 text-lg font-semibold border-2 border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-white transition-all duration-200"
|
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"
|
||||||
>
|
>
|
||||||
<Play className="mr-2 w-5 h-5" />
|
<span className="flex items-center justify-center gap-2">
|
||||||
{t('landing:hero.cta_secondary', 'Ver Demo en Vivo')}
|
<Play className="w-5 h-5 group-hover:scale-110 transition-transform duration-200" />
|
||||||
|
{t('landing:hero.cta_secondary', 'Ver Demo')}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-12 flex items-center justify-center space-x-6 text-sm text-[var(--text-tertiary)]">
|
<div className="mt-12 flex flex-wrap items-center justify-center gap-x-8 gap-y-4 text-sm">
|
||||||
<div className="flex items-center">
|
<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)]">
|
||||||
<Check className="w-4 h-4 text-green-500 mr-2" />
|
<div className="w-5 h-5 bg-green-500/20 rounded-full flex items-center justify-center mr-2">
|
||||||
{t('landing:hero.features.no_credit_card', 'Sin tarjeta de crédito')}
|
<Check className="w-3 h-3 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<span className="font-medium text-[var(--text-secondary)]">
|
||||||
|
{t('landing:hero.features.card_required', 'Tarjeta requerida • 3 meses gratis')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<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)]">
|
||||||
<Check className="w-4 h-4 text-green-500 mr-2" />
|
<div className="w-5 h-5 bg-blue-500/20 rounded-full flex items-center justify-center mr-2">
|
||||||
{t('landing:hero.features.quick_setup', 'Configuración en 5 minutos')}
|
<Clock className="w-3 h-3 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<span className="font-medium text-[var(--text-secondary)]">
|
||||||
|
{t('landing:hero.features.quick_setup', 'Lista en minutos, no horas')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<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)]">
|
||||||
<Check className="w-4 h-4 text-green-500 mr-2" />
|
<div className="w-5 h-5 bg-amber-500/20 rounded-full flex items-center justify-center mr-2">
|
||||||
{t('landing:hero.features.support_24_7', 'Soporte 24/7 en español')}
|
<Users className="w-3 h-3 text-amber-600 dark:text-amber-400" />
|
||||||
|
</div>
|
||||||
|
<span className="font-medium text-[var(--text-secondary)]">
|
||||||
|
{t('landing:hero.features.support_24_7', 'Asistencia personalizada en español')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -134,25 +178,146 @@ const LandingPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Stats Section */}
|
{/* Pilot Program Trust Section - Using Scarcity & Urgency */}
|
||||||
<section className="py-16 bg-[var(--bg-primary)]">
|
<section className="py-16 bg-gradient-to-b from-[var(--bg-primary)] to-[var(--bg-secondary)]">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="grid grid-cols-2 gap-8 md:grid-cols-4">
|
<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="text-center">
|
<div className="text-center mb-8">
|
||||||
<div className="text-3xl font-bold text-[var(--color-primary)]">500+</div>
|
<div className="inline-flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-full text-sm font-bold mb-4">
|
||||||
<div className="mt-2 text-sm text-[var(--text-secondary)]">Panaderías Activas</div>
|
<Clock className="w-4 h-4" />
|
||||||
|
<span>Programa Piloto - Plazas Limitadas</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-3">
|
||||||
|
Buscamos 20 Panaderías Pioneras
|
||||||
|
</h2>
|
||||||
|
<p className="text-[var(--text-secondary)] max-w-2xl mx-auto">
|
||||||
|
Estamos seleccionando las primeras 20 panaderías para formar parte de nuestro programa piloto exclusivo.
|
||||||
|
A cambio de tu feedback, obtienes <strong>3 meses gratis + precio preferencial de por vida</strong>.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-3xl font-bold text-[var(--color-primary)]">35%</div>
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-8">
|
||||||
<div className="mt-2 text-sm text-[var(--text-secondary)]">Reducción de Desperdicios</div>
|
<div className="text-center p-6 bg-white dark:bg-gray-800 rounded-xl shadow-sm">
|
||||||
|
<div className="w-12 h-12 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||||
|
<Award className="w-6 h-6 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-bold text-[var(--text-primary)]">Founders Beta</div>
|
||||||
|
<div className="text-sm text-[var(--text-secondary)] mt-2">Acceso de por vida con 40% descuento</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center p-6 bg-white dark:bg-gray-800 rounded-xl shadow-sm">
|
||||||
|
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900/30 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||||
|
<Users className="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-bold text-[var(--text-primary)]">Influye el Producto</div>
|
||||||
|
<div className="text-sm text-[var(--text-secondary)] mt-2">Tus necesidades moldean la plataforma</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center p-6 bg-white dark:bg-gray-800 rounded-xl shadow-sm">
|
||||||
|
<div className="w-12 h-12 bg-amber-100 dark:bg-amber-900/30 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||||
|
<Zap className="w-6 h-6 text-amber-600 dark:text-amber-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-bold text-[var(--text-primary)]">Soporte Premium</div>
|
||||||
|
<div className="text-sm text-[var(--text-secondary)] mt-2">Atención directa del equipo fundador</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
</div>
|
||||||
<div className="text-3xl font-bold text-[var(--color-primary)]">92%</div>
|
</div>
|
||||||
<div className="mt-2 text-sm text-[var(--text-secondary)]">Precisión de Predicciones</div>
|
</section>
|
||||||
|
|
||||||
|
{/* Who Is This For? Section - Business Models */}
|
||||||
|
<section className="py-20 bg-[var(--bg-primary)]">
|
||||||
|
<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">
|
||||||
|
Tu Modelo de Negocio, Nuestra Tecnología
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-[var(--text-secondary)] max-w-3xl mx-auto">
|
||||||
|
Ya sea que produzcas y vendas en un solo lugar, o gestiones un obrador central con múltiples puntos de venta,
|
||||||
|
nuestra IA se adapta a tu forma de trabajar
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-8">
|
||||||
|
{/* Local Production Model */}
|
||||||
|
<div className="bg-gradient-to-br from-blue-50 to-cyan-50 dark:from-blue-900/20 dark:to-cyan-900/20 rounded-2xl p-8 border-2 border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="flex items-center gap-4 mb-6">
|
||||||
|
<div className="w-16 h-16 bg-blue-600 rounded-xl flex items-center justify-center">
|
||||||
|
<Store className="w-8 h-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-2xl font-bold text-[var(--text-primary)]">Producción Local</h3>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">Un punto de venta y producción</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-[var(--text-secondary)] mb-6 leading-relaxed">
|
||||||
|
Tu panadería produce y vende en el mismo lugar. Necesitas optimizar producción diaria,
|
||||||
|
minimizar desperdicios y maximizar frescura en cada horneada.
|
||||||
|
</p>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Check className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<span className="text-sm text-[var(--text-secondary)]">
|
||||||
|
<strong>Predicción de demanda</strong> por ubicación única
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Check className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<span className="text-sm text-[var(--text-secondary)]">
|
||||||
|
<strong>Gestión de inventario</strong> simplificada y directa
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Check className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<span className="text-sm text-[var(--text-secondary)]">
|
||||||
|
<strong>Un solo punto de control</strong> - simple y eficiente
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-3xl font-bold text-[var(--color-primary)]">4.8★</div>
|
{/* Central Workshop + POS Model */}
|
||||||
<div className="mt-2 text-sm text-[var(--text-secondary)]">Satisfacción de Clientes</div>
|
<div className="bg-gradient-to-br 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 items-center gap-4 mb-6">
|
||||||
|
<div className="w-16 h-16 bg-amber-600 rounded-xl flex items-center justify-center">
|
||||||
|
<Network className="w-8 h-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-2xl font-bold text-[var(--text-primary)]">Obrador Central + Puntos de Venta</h3>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">Producción centralizada, distribución múltiple</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-[var(--text-secondary)] mb-6 leading-relaxed">
|
||||||
|
Produces centralmente y distribuyes a múltiples puntos de venta. Necesitas coordinar producción,
|
||||||
|
logística y demanda entre ubicaciones para optimizar cada punto.
|
||||||
|
</p>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Check className="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<span className="text-sm text-[var(--text-secondary)]">
|
||||||
|
<strong>Predicción agregada y por punto de venta</strong> individual
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Check className="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<span className="text-sm text-[var(--text-secondary)]">
|
||||||
|
<strong>Gestión de distribución</strong> multi-ubicación coordinada
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Check className="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<span className="text-sm text-[var(--text-secondary)]">
|
||||||
|
<strong>Visibilidad centralizada</strong> con control granular
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-12 text-center">
|
||||||
|
<div className="inline-flex items-center gap-2 bg-[var(--color-primary)]/10 text-[var(--color-primary)] px-6 py-3 rounded-full">
|
||||||
|
<Brain className="w-5 h-5" />
|
||||||
|
<span className="font-medium">La misma IA potente, adaptada a tu forma de trabajar</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -379,183 +544,274 @@ const LandingPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Benefits Section */}
|
{/* Benefits Section - Problem/Solution Focus */}
|
||||||
<section id="benefits" className="py-24 bg-[var(--bg-primary)]">
|
<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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="lg:grid lg:grid-cols-2 lg:gap-16 items-center">
|
<div className="text-center mb-16">
|
||||||
<div>
|
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
|
||||||
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
|
El Problema Que Resolvemos
|
||||||
Resultados Comprobados
|
<span className="block text-[var(--color-primary)]">Para Panaderías Artesanales</span>
|
||||||
<span className="block text-[var(--color-primary)]">en Cientos de Panaderías</span>
|
</h2>
|
||||||
</h2>
|
<p className="mt-6 text-lg text-[var(--text-secondary)] max-w-3xl mx-auto">
|
||||||
<p className="mt-6 text-lg text-[var(--text-secondary)]">
|
Sabemos lo frustrante que es tirar pan al final del día, o quedarte sin producto cuando llegan clientes.
|
||||||
Nuestros clientes han logrado transformaciones significativas en sus operaciones,
|
La producción artesanal es difícil de optimizar... hasta ahora.
|
||||||
mejorando rentabilidad y reduciendo desperdicios desde el primer mes.
|
</p>
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
<div className="mt-10 space-y-8">
|
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||||
<div className="flex items-start">
|
{/* Left: Problems */}
|
||||||
<div className="flex-shrink-0">
|
<div className="space-y-6">
|
||||||
<div className="w-8 h-8 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
|
<div className="bg-red-50 dark:bg-red-900/10 border-l-4 border-red-500 p-6 rounded-lg">
|
||||||
<TrendingUp className="w-5 h-5 text-[var(--color-success)]" />
|
<div className="flex items-start gap-4">
|
||||||
</div>
|
<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="ml-4">
|
<div>
|
||||||
<h4 className="text-lg font-semibold text-[var(--text-primary)]">Aumenta Ventas 22% Promedio</h4>
|
<h4 className="text-lg font-bold text-red-700 dark:text-red-400 mb-2">Desperdicias entre 15-40% de producción</h4>
|
||||||
<p className="text-[var(--text-secondary)]">Optimización de producción y mejor disponibilidad de productos populares</p>
|
<p className="text-[var(--text-secondary)] text-sm">
|
||||||
|
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>
|
||||||
|
|
||||||
<div className="flex items-start">
|
<div className="bg-red-50 dark:bg-red-900/10 border-l-4 border-red-500 p-6 rounded-lg">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex items-start gap-4">
|
||||||
<div className="w-8 h-8 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
|
<div className="w-10 h-10 bg-red-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||||
<Shield className="w-5 h-5 text-[var(--color-info)]" />
|
<span className="text-white font-bold text-xl">✗</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4">
|
<div>
|
||||||
<h4 className="text-lg font-semibold text-[var(--text-primary)]">Reduce Desperdicios 35%</h4>
|
<h4 className="text-lg font-bold text-red-700 dark:text-red-400 mb-2">Pierdes ventas por falta de stock</h4>
|
||||||
<p className="text-[var(--text-secondary)]">Predicciones precisas evitan sobreproducción y productos vencidos</p>
|
<p className="text-[var(--text-secondary)] text-sm">
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start">
|
<div className="bg-red-50 dark:bg-red-900/10 border-l-4 border-red-500 p-6 rounded-lg">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex items-start gap-4">
|
||||||
<div className="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
|
<div className="w-10 h-10 bg-red-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||||
<Clock className="w-5 h-5 text-purple-600" />
|
<span className="text-white font-bold text-xl">✗</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4">
|
<div>
|
||||||
<h4 className="text-lg font-semibold text-[var(--text-primary)]">Ahorra 8 Horas Semanales</h4>
|
<h4 className="text-lg font-bold text-red-700 dark:text-red-400 mb-2">Excel, papel y "experiencia"</h4>
|
||||||
<p className="text-[var(--text-secondary)]">Automatización de tareas administrativas y de planificación</p>
|
<p className="text-[var(--text-secondary)] text-sm">
|
||||||
|
Planificas basándote en intuición. Funciona... hasta que no funciona.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-12 lg:mt-0">
|
{/* Right: Solutions */}
|
||||||
<div className="bg-gradient-to-r from-[var(--color-primary)]/10 to-[var(--color-secondary)]/10 rounded-2xl p-8">
|
<div className="space-y-6">
|
||||||
<div className="grid grid-cols-2 gap-8">
|
<div className="bg-green-50 dark:bg-green-900/10 border-l-4 border-green-500 p-6 rounded-lg">
|
||||||
<div className="text-center">
|
<div className="flex items-start gap-4">
|
||||||
<div className="text-3xl font-bold text-[var(--color-primary)]">€127k</div>
|
<div className="w-10 h-10 bg-green-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||||
<div className="text-sm text-[var(--text-secondary)]">Ahorro promedio anual por panadería</div>
|
<Check className="text-white w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div>
|
||||||
<div className="text-3xl font-bold text-[var(--color-secondary)]">98%</div>
|
<h4 className="text-lg font-bold text-green-700 dark:text-green-400 mb-2">Produce exactamente lo que vas a vender</h4>
|
||||||
<div className="text-sm text-[var(--text-secondary)]">Satisfacción de clientes</div>
|
<p className="text-[var(--text-secondary)] text-sm">
|
||||||
|
La IA analiza tus ventas históricas, clima, eventos locales y festivos para predecir demanda real.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
</div>
|
||||||
<div className="text-3xl font-bold text-[var(--color-accent)]">2.3x</div>
|
</div>
|
||||||
<div className="text-sm text-[var(--text-secondary)]">ROI promedio en 12 meses</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>
|
||||||
<div className="text-center">
|
<div>
|
||||||
<div className="text-3xl font-bold text-[var(--color-info)]">24/7</div>
|
<h4 className="text-lg font-bold text-green-700 dark:text-green-400 mb-2">Siempre tienes stock de lo que más se vende</h4>
|
||||||
<div className="text-sm text-[var(--text-secondary)]">Soporte técnico especializado</div>
|
<p className="text-[var(--text-secondary)] text-sm">
|
||||||
|
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>
|
</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>
|
||||||
|
<h4 className="text-lg font-bold text-green-700 dark:text-green-400 mb-2">Automatización inteligente + datos reales</h4>
|
||||||
|
<p className="text-[var(--text-secondary)] text-sm">
|
||||||
|
Desde planificación de producción hasta gestión de inventario. Todo basado en matemáticas, no corazonadas.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
|
El Objetivo: Que Ahorres Dinero Desde el Primer Mes
|
||||||
|
</h3>
|
||||||
|
<p className="text-[var(--text-secondary)] max-w-3xl mx-auto mb-6">
|
||||||
|
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>.
|
||||||
|
</p>
|
||||||
|
<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)]">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)]">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)]">Tus datos siempre son tuyos</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Testimonials Section */}
|
{/* Risk Reversal & Transparency Section */}
|
||||||
<section id="testimonials" className="py-24 bg-[var(--bg-secondary)]">
|
<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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="text-center">
|
<div className="text-center mb-16">
|
||||||
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
|
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
|
||||||
Lo que Dicen Nuestros Clientes
|
Sin Riesgo. Sin Ataduras.
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 max-w-2xl mx-auto text-lg text-[var(--text-secondary)]">
|
<p className="mt-4 max-w-2xl mx-auto text-lg text-[var(--text-secondary)]">
|
||||||
Panaderías de toda España han transformado sus negocios con nuestra plataforma
|
Somos transparentes: esto es un piloto. Estamos construyendo la mejor herramienta para panaderías artesanales, y necesitamos tu ayuda.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-16 grid grid-cols-1 lg:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 mb-16">
|
||||||
{/* Testimonial 1 */}
|
{/* Left: What You Get */}
|
||||||
<div className="bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border border-[var(--border-primary)]">
|
<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">
|
||||||
<div className="flex items-center mb-4">
|
<h3 className="text-2xl font-bold text-[var(--text-primary)] mb-6 flex items-center gap-3">
|
||||||
{[...Array(5)].map((_, i) => (
|
<div className="w-10 h-10 bg-green-600 rounded-full flex items-center justify-center">
|
||||||
<Star key={i} className="w-5 h-5 text-yellow-400 fill-current" />
|
<Check className="w-6 h-6 text-white" />
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<blockquote className="text-[var(--text-secondary)] italic">
|
|
||||||
"Desde que implementamos Panadería IA, nuestros desperdicios se redujeron un 40% y las ventas aumentaron un 28%.
|
|
||||||
La predicción de demanda es increíblemente precisa."
|
|
||||||
</blockquote>
|
|
||||||
<div className="mt-6 flex items-center">
|
|
||||||
<div className="w-12 h-12 bg-[var(--color-primary)] rounded-full flex items-center justify-center text-white font-bold">
|
|
||||||
M
|
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4">
|
Lo Que Obtienes
|
||||||
<div className="font-semibold text-[var(--text-primary)]">María González</div>
|
</h3>
|
||||||
<div className="text-sm text-[var(--text-secondary)]">Panadería Santa María, Madrid</div>
|
<ul className="space-y-4">
|
||||||
</div>
|
<li className="flex items-start gap-3">
|
||||||
</div>
|
<Check className="w-5 h-5 text-green-600 dark:text-green-400 mt-1 flex-shrink-0" />
|
||||||
|
<span className="text-[var(--text-secondary)]"><strong>3 meses completamente gratis</strong> para probar todas las funcionalidades</span>
|
||||||
|
</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)]"><strong>40% de descuento de por vida</strong> si decides continuar después del piloto</span>
|
||||||
|
</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)]"><strong>Soporte directo del equipo fundador</strong> - respondemos en horas, no días</span>
|
||||||
|
</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)]"><strong>Tus ideas se implementan primero</strong> - construimos lo que realmente necesitas</span>
|
||||||
|
</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)]"><strong>Cancelas cuando quieras</strong> sin explicaciones ni penalizaciones</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Testimonial 2 */}
|
{/* Right: What We Ask */}
|
||||||
<div className="bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border border-[var(--border-primary)]">
|
<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">
|
||||||
<div className="flex items-center mb-4">
|
<h3 className="text-2xl font-bold text-[var(--text-primary)] mb-6 flex items-center gap-3">
|
||||||
{[...Array(5)].map((_, i) => (
|
<div className="w-10 h-10 bg-blue-600 rounded-full flex items-center justify-center">
|
||||||
<Star key={i} className="w-5 h-5 text-yellow-400 fill-current" />
|
<Users className="w-6 h-6 text-white" />
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<blockquote className="text-[var(--text-secondary)] italic">
|
|
||||||
"El sistema nos ahorra 10 horas semanales en planificación. Ahora puedo enfocarme en mejorar nuestros productos
|
|
||||||
mientras la IA maneja la logística."
|
|
||||||
</blockquote>
|
|
||||||
<div className="mt-6 flex items-center">
|
|
||||||
<div className="w-12 h-12 bg-[var(--color-secondary)] rounded-full flex items-center justify-center text-white font-bold">
|
|
||||||
C
|
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4">
|
Lo Que Pedimos
|
||||||
<div className="font-semibold text-[var(--text-primary)]">Carlos Ruiz</div>
|
</h3>
|
||||||
<div className="text-sm text-[var(--text-secondary)]">Horno de Oro, Valencia</div>
|
<ul className="space-y-4">
|
||||||
</div>
|
<li className="flex items-start gap-3">
|
||||||
</div>
|
<ArrowRight className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-1 flex-shrink-0" />
|
||||||
</div>
|
<span className="text-[var(--text-secondary)]"><strong>Feedback honesto semanal</strong> (15 min) sobre qué funciona y qué no</span>
|
||||||
|
</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)]"><strong>Paciencia con bugs</strong> - estamos en fase beta, habrá imperfecciones</span>
|
||||||
|
</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)]"><strong>Datos de ventas históricos</strong> (opcional) para mejorar las predicciones</span>
|
||||||
|
</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)]"><strong>Comunicación abierta</strong> - queremos saber si algo no te gusta</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
{/* Testimonial 3 */}
|
<div className="mt-6 p-4 bg-white dark:bg-gray-800 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||||
<div className="bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border border-[var(--border-primary)]">
|
<p className="text-sm text-[var(--text-secondary)] italic">
|
||||||
<div className="flex items-center mb-4">
|
<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.
|
||||||
{[...Array(5)].map((_, i) => (
|
</p>
|
||||||
<Star key={i} className="w-5 h-5 text-yellow-400 fill-current" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<blockquote className="text-[var(--text-secondary)] italic">
|
|
||||||
"Increíble cómo predice exactamente cuántos panes necesitamos cada día. Nuestros clientes siempre encuentran
|
|
||||||
sus productos favoritos disponibles."
|
|
||||||
</blockquote>
|
|
||||||
<div className="mt-6 flex items-center">
|
|
||||||
<div className="w-12 h-12 bg-[var(--color-accent)] rounded-full flex items-center justify-center text-white font-bold">
|
|
||||||
A
|
|
||||||
</div>
|
|
||||||
<div className="ml-4">
|
|
||||||
<div className="font-semibold text-[var(--text-primary)]">Ana Martínez</div>
|
|
||||||
<div className="text-sm text-[var(--text-secondary)]">Pan & Tradición, Sevilla</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Trust indicators */}
|
{/* Credibility Signals */}
|
||||||
<div className="mt-16 text-center">
|
<div className="bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg border border-[var(--border-primary)]">
|
||||||
<p className="text-sm text-[var(--text-tertiary)] mb-8">Confiado por más de 500 panaderías en España</p>
|
<div className="text-center mb-8">
|
||||||
<div className="flex items-center justify-center space-x-8 opacity-60">
|
<h3 className="text-xl font-bold text-[var(--text-primary)] mb-3">
|
||||||
<div className="font-semibold text-[var(--text-secondary)]">Panadería Real</div>
|
¿Por Qué Confiar en Nosotros?
|
||||||
<div className="font-semibold text-[var(--text-secondary)]">Horno Artesanal</div>
|
</h3>
|
||||||
<div className="font-semibold text-[var(--text-secondary)]">Pan de Casa</div>
|
<p className="text-[var(--text-secondary)]">
|
||||||
<div className="font-semibold text-[var(--text-secondary)]">Dulce Tradición</div>
|
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">100% Española</h4>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">
|
||||||
|
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">Tecnología Probada</h4>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">
|
||||||
|
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">Equipo Experto</h4>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">
|
||||||
|
Fundadores con experiencia en IA + hostelería. Conocemos el sector de dentro.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Pricing Section */}
|
{/* Pricing Section */}
|
||||||
<PricingSection />
|
<section id="pricing">
|
||||||
|
<PricingSection />
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* FAQ Section */}
|
{/* FAQ Section */}
|
||||||
<section className="py-24 bg-[var(--bg-secondary)]">
|
<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="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
|
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)]">
|
||||||
@@ -621,7 +877,7 @@ const LandingPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Final CTA 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">
|
<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 inset-0">
|
||||||
<div className="absolute -top-40 -right-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
|
<div className="absolute -top-40 -right-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
|
||||||
@@ -629,51 +885,72 @@ const LandingPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-4xl mx-auto text-center px-4 sm:px-6 lg:px-8 relative">
|
<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>Quedan 12 plazas de las 20 del programa piloto</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2 className="text-3xl lg:text-5xl font-extrabold text-white">
|
<h2 className="text-3xl lg:text-5xl font-extrabold text-white">
|
||||||
Transforma tu Panadería
|
Sé de las Primeras 20 Panaderías
|
||||||
<span className="block text-white/90">Comenzando Hoy</span>
|
<span className="block text-white/90 mt-2">En Probar Esta Tecnología</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-6 text-lg text-white/80 max-w-2xl mx-auto">
|
<p className="mt-6 text-lg text-white/90 max-w-2xl mx-auto">
|
||||||
Únete a más de 500 panaderías que ya están reduciendo desperdicios, aumentando ventas y
|
No es para todo el mundo. Buscamos panaderías artesanales que quieran <strong>reducir desperdicios y aumentar ganancias</strong>
|
||||||
optimizando operaciones con inteligencia artificial.
|
con ayuda de IA, a cambio de feedback honesto.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-12 flex flex-col sm:flex-row gap-6 justify-center">
|
<div className="mt-10 flex flex-col sm:flex-row gap-6 justify-center">
|
||||||
<Link to="/register">
|
<Link to="/register?pilot=true">
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
className="px-10 py-4 text-lg font-semibold bg-white text-[var(--color-primary)] hover:bg-[var(--bg-tertiary)] shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200"
|
className="px-10 py-5 text-lg font-bold bg-white text-[var(--color-primary)] hover:bg-[var(--bg-tertiary)] shadow-2xl hover:shadow-3xl transform hover:scale-105 transition-all duration-200 rounded-xl"
|
||||||
>
|
>
|
||||||
Comenzar Prueba Gratuita 14 Días
|
Solicitar Plaza en el Piloto
|
||||||
<ArrowRight className="ml-2 w-5 h-5" />
|
<ArrowRight className="ml-2 w-6 h-6" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/demo">
|
<Link to="/demo">
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="px-10 py-4 text-lg font-semibold border-2 border-white text-white hover:bg-white hover:text-[var(--color-primary)] transition-all duration-200"
|
className="px-10 py-5 text-lg font-semibold border-2 border-white text-white hover:bg-white hover:text-[var(--color-primary)] transition-all duration-200 rounded-xl"
|
||||||
>
|
>
|
||||||
<Play className="mr-2 w-5 h-5" />
|
<Play className="mr-2 w-5 h-5" />
|
||||||
Ver Demo
|
Ver Cómo Funciona
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-12 grid grid-cols-1 sm:grid-cols-3 gap-8 text-center">
|
{/* Social Proof Alternative - Loss Aversion */}
|
||||||
<div>
|
<div className="mt-12 bg-white/10 backdrop-blur-sm rounded-2xl p-6 border border-white/20">
|
||||||
<div className="text-2xl font-bold text-white">14 días</div>
|
<p className="text-white/90 text-base mb-4">
|
||||||
<div className="text-white/70 text-sm">Prueba gratuita</div>
|
<strong>¿Por qué actuar ahora?</strong>
|
||||||
</div>
|
</p>
|
||||||
<div>
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6 text-sm">
|
||||||
<div className="text-2xl font-bold text-white">5 min</div>
|
<div className="flex flex-col items-center">
|
||||||
<div className="text-white/70 text-sm">Configuración</div>
|
<Award className="w-8 h-8 text-white mb-2" />
|
||||||
</div>
|
<div className="text-white font-semibold">40% descuento de por vida</div>
|
||||||
<div>
|
<div className="text-white/70">Solo primeros 20</div>
|
||||||
<div className="text-2xl font-bold text-white">24/7</div>
|
</div>
|
||||||
<div className="text-white/70 text-sm">Soporte incluido</div>
|
<div className="flex flex-col items-center">
|
||||||
|
<Users className="w-8 h-8 text-white mb-2" />
|
||||||
|
<div className="text-white font-semibold">Influyes en el roadmap</div>
|
||||||
|
<div className="text-white/70">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">Soporte VIP</div>
|
||||||
|
<div className="text-white/70">Acceso directo al equipo</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Guarantee */}
|
||||||
|
<div className="mt-8 text-white/80 text-sm">
|
||||||
|
<Shield className="w-5 h-5 inline mr-2" />
|
||||||
|
<span>Garantía: Cancelas en cualquier momento sin dar explicaciones</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -6,3 +6,10 @@ export { default as PrivacyPolicyPage } from './PrivacyPolicyPage';
|
|||||||
export { default as TermsOfServicePage } from './TermsOfServicePage';
|
export { default as TermsOfServicePage } from './TermsOfServicePage';
|
||||||
export { default as CookiePolicyPage } from './CookiePolicyPage';
|
export { default as CookiePolicyPage } from './CookiePolicyPage';
|
||||||
export { default as CookiePreferencesPage } from './CookiePreferencesPage';
|
export { default as CookiePreferencesPage } from './CookiePreferencesPage';
|
||||||
|
export { default as BlogPage } from './BlogPage';
|
||||||
|
export { default as AboutPage } from './AboutPage';
|
||||||
|
export { default as CareersPage } from './CareersPage';
|
||||||
|
export { default as HelpCenterPage } from './HelpCenterPage';
|
||||||
|
export { default as DocumentationPage } from './DocumentationPage';
|
||||||
|
export { default as ContactPage } from './ContactPage';
|
||||||
|
export { default as FeedbackPage } from './FeedbackPage';
|
||||||
@@ -9,11 +9,17 @@ const LandingPage = React.lazy(() => import('../pages/public/LandingPage'));
|
|||||||
const LoginPage = React.lazy(() => import('../pages/public/LoginPage'));
|
const LoginPage = React.lazy(() => import('../pages/public/LoginPage'));
|
||||||
const RegisterPage = React.lazy(() => import('../pages/public/RegisterPage'));
|
const RegisterPage = React.lazy(() => import('../pages/public/RegisterPage'));
|
||||||
const DemoPage = React.lazy(() => import('../pages/public/DemoPage'));
|
const DemoPage = React.lazy(() => import('../pages/public/DemoPage'));
|
||||||
const DemoSetupPage = React.lazy(() => import('../pages/public/DemoSetupPage'));
|
|
||||||
const PrivacyPolicyPage = React.lazy(() => import('../pages/public/PrivacyPolicyPage'));
|
const PrivacyPolicyPage = React.lazy(() => import('../pages/public/PrivacyPolicyPage'));
|
||||||
const TermsOfServicePage = React.lazy(() => import('../pages/public/TermsOfServicePage'));
|
const TermsOfServicePage = React.lazy(() => import('../pages/public/TermsOfServicePage'));
|
||||||
const CookiePolicyPage = React.lazy(() => import('../pages/public/CookiePolicyPage'));
|
const CookiePolicyPage = React.lazy(() => import('../pages/public/CookiePolicyPage'));
|
||||||
const CookiePreferencesPage = React.lazy(() => import('../pages/public/CookiePreferencesPage'));
|
const CookiePreferencesPage = React.lazy(() => import('../pages/public/CookiePreferencesPage'));
|
||||||
|
const BlogPage = React.lazy(() => import('../pages/public/BlogPage'));
|
||||||
|
const AboutPage = React.lazy(() => import('../pages/public/AboutPage'));
|
||||||
|
const CareersPage = React.lazy(() => import('../pages/public/CareersPage'));
|
||||||
|
const HelpCenterPage = React.lazy(() => import('../pages/public/HelpCenterPage'));
|
||||||
|
const DocumentationPage = React.lazy(() => import('../pages/public/DocumentationPage'));
|
||||||
|
const ContactPage = React.lazy(() => import('../pages/public/ContactPage'));
|
||||||
|
const FeedbackPage = React.lazy(() => import('../pages/public/FeedbackPage'));
|
||||||
const DashboardPage = React.lazy(() => import('../pages/app/DashboardPage'));
|
const DashboardPage = React.lazy(() => import('../pages/app/DashboardPage'));
|
||||||
|
|
||||||
// Operations pages
|
// Operations pages
|
||||||
@@ -67,7 +73,17 @@ export const AppRouter: React.FC = () => {
|
|||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route path="/register" element={<RegisterPage />} />
|
<Route path="/register" element={<RegisterPage />} />
|
||||||
<Route path="/demo" element={<DemoPage />} />
|
<Route path="/demo" element={<DemoPage />} />
|
||||||
<Route path="/demo/setup" element={<DemoSetupPage />} />
|
|
||||||
|
{/* Company Routes - Public */}
|
||||||
|
<Route path="/blog" element={<BlogPage />} />
|
||||||
|
<Route path="/about" element={<AboutPage />} />
|
||||||
|
<Route path="/careers" element={<CareersPage />} />
|
||||||
|
|
||||||
|
{/* Help & Support Routes - Public */}
|
||||||
|
<Route path="/help" element={<HelpCenterPage />} />
|
||||||
|
<Route path="/help/docs" element={<DocumentationPage />} />
|
||||||
|
<Route path="/help/support" element={<ContactPage />} />
|
||||||
|
<Route path="/help/feedback" element={<FeedbackPage />} />
|
||||||
|
|
||||||
{/* Legal & Privacy Routes - Public */}
|
{/* Legal & Privacy Routes - Public */}
|
||||||
<Route path="/privacy" element={<PrivacyPolicyPage />} />
|
<Route path="/privacy" element={<PrivacyPolicyPage />} />
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: rabbitmq
|
- name: rabbitmq
|
||||||
image: rabbitmq:3.12-management-alpine
|
image: rabbitmq:4.0-management-alpine
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 5672
|
- containerPort: 5672
|
||||||
name: amqp
|
name: amqp
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ resources:
|
|||||||
# Migration jobs
|
# Migration jobs
|
||||||
- migrations/auth-migration-job.yaml
|
- migrations/auth-migration-job.yaml
|
||||||
- migrations/tenant-migration-job.yaml
|
- migrations/tenant-migration-job.yaml
|
||||||
|
- migrations/tenant-seed-pilot-coupon-job.yaml
|
||||||
- migrations/training-migration-job.yaml
|
- migrations/training-migration-job.yaml
|
||||||
- migrations/forecasting-migration-job.yaml
|
- migrations/forecasting-migration-job.yaml
|
||||||
- migrations/sales-migration-job.yaml
|
- migrations/sales-migration-job.yaml
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Seed job for PILOT2025 coupon - runs after tenant migration
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: tenant-seed-pilot-coupon
|
||||||
|
namespace: bakery-ia
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: tenant-seed-pilot-coupon
|
||||||
|
app.kubernetes.io/component: seed
|
||||||
|
app.kubernetes.io/part-of: bakery-ia
|
||||||
|
spec:
|
||||||
|
backoffLimit: 3
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: tenant-seed-pilot-coupon
|
||||||
|
app.kubernetes.io/component: seed
|
||||||
|
spec:
|
||||||
|
initContainers:
|
||||||
|
- name: wait-for-db
|
||||||
|
image: postgres:15-alpine
|
||||||
|
command: ["sh", "-c", "until pg_isready -h tenant-db-service -p 5432; do sleep 2; done"]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "64Mi"
|
||||||
|
cpu: "50m"
|
||||||
|
limits:
|
||||||
|
memory: "128Mi"
|
||||||
|
cpu: "100m"
|
||||||
|
- name: wait-for-migration
|
||||||
|
image: bitnami/kubectl:latest
|
||||||
|
command: ["sh", "-c", "until kubectl wait --for=condition=complete --timeout=300s job/tenant-migration -n bakery-ia 2>/dev/null; do echo 'Waiting for tenant migration...'; sleep 5; done"]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "64Mi"
|
||||||
|
cpu: "50m"
|
||||||
|
limits:
|
||||||
|
memory: "128Mi"
|
||||||
|
cpu: "100m"
|
||||||
|
containers:
|
||||||
|
- name: seed-coupon
|
||||||
|
image: bakery/tenant-service:dev
|
||||||
|
command: ["python", "/app/services/tenant/scripts/seed_pilot_coupon.py"]
|
||||||
|
env:
|
||||||
|
- name: TENANT_DATABASE_URL
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: database-secrets
|
||||||
|
key: TENANT_DATABASE_URL
|
||||||
|
- name: LOG_LEVEL
|
||||||
|
value: "INFO"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "256Mi"
|
||||||
|
cpu: "100m"
|
||||||
|
limits:
|
||||||
|
memory: "512Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
restartPolicy: OnFailure
|
||||||
@@ -32,7 +32,7 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
image: rabbitmq:3-management-alpine
|
image: rabbitmq:4.0-management-alpine
|
||||||
container_name: auth-rabbitmq
|
container_name: auth-rabbitmq
|
||||||
environment:
|
environment:
|
||||||
RABBITMQ_DEFAULT_USER: bakery
|
RABBITMQ_DEFAULT_USER: bakery
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class CloneOrchestrator:
|
|||||||
name="inventory",
|
name="inventory",
|
||||||
url=os.getenv("INVENTORY_SERVICE_URL", "http://inventory-service:8000"),
|
url=os.getenv("INVENTORY_SERVICE_URL", "http://inventory-service:8000"),
|
||||||
required=False, # Optional - provides ingredients/recipes
|
required=False, # Optional - provides ingredients/recipes
|
||||||
timeout=10.0
|
timeout=5.0 # Reduced from 10s - optimized data volume
|
||||||
),
|
),
|
||||||
ServiceDefinition(
|
ServiceDefinition(
|
||||||
name="recipes",
|
name="recipes",
|
||||||
@@ -63,7 +63,7 @@ class CloneOrchestrator:
|
|||||||
name="sales",
|
name="sales",
|
||||||
url=os.getenv("SALES_SERVICE_URL", "http://sales-service:8000"),
|
url=os.getenv("SALES_SERVICE_URL", "http://sales-service:8000"),
|
||||||
required=False, # Optional - provides sales history
|
required=False, # Optional - provides sales history
|
||||||
timeout=10.0
|
timeout=5.0 # Reduced from 10s - optimized to 30 days history
|
||||||
),
|
),
|
||||||
ServiceDefinition(
|
ServiceDefinition(
|
||||||
name="orders",
|
name="orders",
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ async def create_stock_batches_for_ingredient(
|
|||||||
base_date: datetime
|
base_date: datetime
|
||||||
) -> list:
|
) -> list:
|
||||||
"""
|
"""
|
||||||
Create 3-5 stock batches for a single ingredient with varied properties
|
Create 1-2 stock batches for a single ingredient (optimized for demo performance)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
db: Database session
|
db: Database session
|
||||||
@@ -124,7 +124,7 @@ async def create_stock_batches_for_ingredient(
|
|||||||
List of created Stock instances
|
List of created Stock instances
|
||||||
"""
|
"""
|
||||||
stocks = []
|
stocks = []
|
||||||
num_batches = random.randint(3, 5)
|
num_batches = random.randint(1, 2) # Reduced from 3-5 for faster demo loading
|
||||||
|
|
||||||
for i in range(num_batches):
|
for i in range(num_batches):
|
||||||
# Calculate expiration days offset
|
# Calculate expiration days offset
|
||||||
|
|||||||
@@ -236,24 +236,24 @@ async def seed_sales(sales_db: AsyncSession):
|
|||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
# Seed for San Pablo (Traditional Bakery) - 90 days of history
|
# Seed for San Pablo (Traditional Bakery) - 30 days of history (optimized for fast demo loading)
|
||||||
logger.info("")
|
logger.info("")
|
||||||
result_san_pablo = await seed_sales_for_tenant(
|
result_san_pablo = await seed_sales_for_tenant(
|
||||||
sales_db,
|
sales_db,
|
||||||
DEMO_TENANT_SAN_PABLO,
|
DEMO_TENANT_SAN_PABLO,
|
||||||
"Panadería San Pablo (Traditional)",
|
"Panadería San Pablo (Traditional)",
|
||||||
SAN_PABLO_PRODUCTS,
|
SAN_PABLO_PRODUCTS,
|
||||||
days_of_history=90
|
days_of_history=30
|
||||||
)
|
)
|
||||||
results.append(result_san_pablo)
|
results.append(result_san_pablo)
|
||||||
|
|
||||||
# Seed for La Espiga (Central Workshop) - 90 days of history
|
# Seed for La Espiga (Central Workshop) - 30 days of history (optimized for fast demo loading)
|
||||||
result_la_espiga = await seed_sales_for_tenant(
|
result_la_espiga = await seed_sales_for_tenant(
|
||||||
sales_db,
|
sales_db,
|
||||||
DEMO_TENANT_LA_ESPIGA,
|
DEMO_TENANT_LA_ESPIGA,
|
||||||
"Panadería La Espiga (Central Workshop)",
|
"Panadería La Espiga (Central Workshop)",
|
||||||
LA_ESPIGA_PRODUCTS,
|
LA_ESPIGA_PRODUCTS,
|
||||||
days_of_history=90
|
days_of_history=30
|
||||||
)
|
)
|
||||||
results.append(result_la_espiga)
|
results.append(result_la_espiga)
|
||||||
|
|
||||||
@@ -331,7 +331,7 @@ async def main():
|
|||||||
logger.info("🎉 Success! Sales history is ready for cloning.")
|
logger.info("🎉 Success! Sales history is ready for cloning.")
|
||||||
logger.info("")
|
logger.info("")
|
||||||
logger.info("Sales data includes:")
|
logger.info("Sales data includes:")
|
||||||
logger.info(" • 90 days of historical sales")
|
logger.info(" • 30 days of historical sales (optimized for demo performance)")
|
||||||
logger.info(" • 4 product types per tenant")
|
logger.info(" • 4 product types per tenant")
|
||||||
logger.info(" • Realistic weekly patterns (higher on weekends)")
|
logger.info(" • Realistic weekly patterns (higher on weekends)")
|
||||||
logger.info(" • Random variance and occasional closures")
|
logger.info(" • Random variance and occasional closures")
|
||||||
|
|||||||
@@ -85,20 +85,77 @@ def get_payment_service():
|
|||||||
async def register_bakery(
|
async def register_bakery(
|
||||||
bakery_data: BakeryRegistration,
|
bakery_data: BakeryRegistration,
|
||||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service),
|
||||||
|
payment_service: PaymentService = Depends(get_payment_service)
|
||||||
):
|
):
|
||||||
"""Register a new bakery/tenant with enhanced validation and features"""
|
"""Register a new bakery/tenant with enhanced validation and features"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Validate coupon if provided
|
||||||
|
coupon_validation = None
|
||||||
|
if bakery_data.coupon_code:
|
||||||
|
from app.core.config import settings
|
||||||
|
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||||
|
|
||||||
|
async with database_manager.get_session() as session:
|
||||||
|
# Temp tenant ID for validation (will be replaced with actual after creation)
|
||||||
|
temp_tenant_id = f"temp_{current_user['user_id']}"
|
||||||
|
|
||||||
|
coupon_validation = payment_service.validate_coupon_code(
|
||||||
|
bakery_data.coupon_code,
|
||||||
|
temp_tenant_id,
|
||||||
|
session
|
||||||
|
)
|
||||||
|
|
||||||
|
if not coupon_validation["valid"]:
|
||||||
|
logger.warning(
|
||||||
|
"Invalid coupon code provided during registration",
|
||||||
|
coupon_code=bakery_data.coupon_code,
|
||||||
|
error=coupon_validation["error_message"]
|
||||||
|
)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=coupon_validation["error_message"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create bakery/tenant
|
||||||
result = await tenant_service.create_bakery(
|
result = await tenant_service.create_bakery(
|
||||||
bakery_data,
|
bakery_data,
|
||||||
current_user["user_id"]
|
current_user["user_id"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If coupon was validated, redeem it now with actual tenant_id
|
||||||
|
if coupon_validation and coupon_validation["valid"]:
|
||||||
|
from app.core.config import settings
|
||||||
|
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||||
|
|
||||||
|
async with database_manager.get_session() as session:
|
||||||
|
success, discount, error = payment_service.redeem_coupon(
|
||||||
|
bakery_data.coupon_code,
|
||||||
|
result.id,
|
||||||
|
session
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info(
|
||||||
|
"Coupon redeemed successfully",
|
||||||
|
tenant_id=result.id,
|
||||||
|
coupon_code=bakery_data.coupon_code,
|
||||||
|
discount=discount
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"Failed to redeem coupon after registration",
|
||||||
|
tenant_id=result.id,
|
||||||
|
coupon_code=bakery_data.coupon_code,
|
||||||
|
error=error
|
||||||
|
)
|
||||||
|
|
||||||
logger.info("Bakery registered successfully",
|
logger.info("Bakery registered successfully",
|
||||||
name=bakery_data.name,
|
name=bakery_data.name,
|
||||||
owner_email=current_user.get('email'),
|
owner_email=current_user.get('email'),
|
||||||
tenant_id=result.id)
|
tenant_id=result.id,
|
||||||
|
coupon_applied=bakery_data.coupon_code is not None)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ AuditLog = create_audit_log_model(Base)
|
|||||||
|
|
||||||
# Import all models to register them with the Base metadata
|
# Import all models to register them with the Base metadata
|
||||||
from .tenants import Tenant, TenantMember, Subscription
|
from .tenants import Tenant, TenantMember, Subscription
|
||||||
|
from .coupon import CouponModel, CouponRedemptionModel
|
||||||
|
|
||||||
# List all models for easier access
|
# List all models for easier access
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -20,4 +21,6 @@ __all__ = [
|
|||||||
"TenantMember",
|
"TenantMember",
|
||||||
"Subscription",
|
"Subscription",
|
||||||
"AuditLog",
|
"AuditLog",
|
||||||
|
"CouponModel",
|
||||||
|
"CouponRedemptionModel",
|
||||||
]
|
]
|
||||||
|
|||||||
64
services/tenant/app/models/coupon.py
Normal file
64
services/tenant/app/models/coupon.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"""
|
||||||
|
SQLAlchemy models for coupon system
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey, JSON, Index
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from shared.database import Base
|
||||||
|
|
||||||
|
|
||||||
|
class CouponModel(Base):
|
||||||
|
"""Coupon configuration table"""
|
||||||
|
__tablename__ = "coupons"
|
||||||
|
|
||||||
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
|
code = Column(String(50), unique=True, nullable=False, index=True)
|
||||||
|
discount_type = Column(String(20), nullable=False) # trial_extension, percentage, fixed_amount
|
||||||
|
discount_value = Column(Integer, nullable=False) # Days/percentage/cents depending on type
|
||||||
|
max_redemptions = Column(Integer, nullable=True) # None = unlimited
|
||||||
|
current_redemptions = Column(Integer, nullable=False, default=0)
|
||||||
|
valid_from = Column(DateTime(timezone=True), nullable=False)
|
||||||
|
valid_until = Column(DateTime(timezone=True), nullable=True) # None = no expiry
|
||||||
|
active = Column(Boolean, nullable=False, default=True)
|
||||||
|
created_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow)
|
||||||
|
extra_data = Column(JSON, nullable=True) # Renamed from metadata to avoid SQLAlchemy reserved name
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
redemptions = relationship("CouponRedemptionModel", back_populates="coupon")
|
||||||
|
|
||||||
|
# Indexes for performance
|
||||||
|
__table_args__ = (
|
||||||
|
Index('idx_coupon_code_active', 'code', 'active'),
|
||||||
|
Index('idx_coupon_valid_dates', 'valid_from', 'valid_until'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Coupon(code='{self.code}', type='{self.discount_type}', value={self.discount_value})>"
|
||||||
|
|
||||||
|
|
||||||
|
class CouponRedemptionModel(Base):
|
||||||
|
"""Coupon redemption history table"""
|
||||||
|
__tablename__ = "coupon_redemptions"
|
||||||
|
|
||||||
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
|
tenant_id = Column(String(255), nullable=False, index=True)
|
||||||
|
coupon_code = Column(String(50), ForeignKey('coupons.code'), nullable=False)
|
||||||
|
redeemed_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow)
|
||||||
|
discount_applied = Column(JSON, nullable=False) # Details of discount applied
|
||||||
|
extra_data = Column(JSON, nullable=True) # Renamed from metadata to avoid SQLAlchemy reserved name
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
coupon = relationship("CouponModel", back_populates="redemptions")
|
||||||
|
|
||||||
|
# Constraints
|
||||||
|
__table_args__ = (
|
||||||
|
Index('idx_redemption_tenant', 'tenant_id'),
|
||||||
|
Index('idx_redemption_coupon', 'coupon_code'),
|
||||||
|
Index('idx_redemption_tenant_coupon', 'tenant_id', 'coupon_code'), # Prevent duplicate redemptions
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<CouponRedemption(tenant_id='{self.tenant_id}', code='{self.coupon_code}')>"
|
||||||
277
services/tenant/app/repositories/coupon_repository.py
Normal file
277
services/tenant/app/repositories/coupon_repository.py
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
"""
|
||||||
|
Repository for coupon data access and validation
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import and_
|
||||||
|
|
||||||
|
from app.models.coupon import CouponModel, CouponRedemptionModel
|
||||||
|
from shared.subscription.coupons import (
|
||||||
|
Coupon,
|
||||||
|
CouponRedemption,
|
||||||
|
CouponValidationResult,
|
||||||
|
DiscountType,
|
||||||
|
calculate_trial_end_date,
|
||||||
|
format_discount_description
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CouponRepository:
|
||||||
|
"""Data access layer for coupon operations"""
|
||||||
|
|
||||||
|
def __init__(self, db: Session):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def get_coupon_by_code(self, code: str) -> Optional[Coupon]:
|
||||||
|
"""
|
||||||
|
Retrieve coupon by code.
|
||||||
|
Returns None if not found.
|
||||||
|
"""
|
||||||
|
coupon_model = self.db.query(CouponModel).filter(
|
||||||
|
CouponModel.code == code.upper()
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not coupon_model:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self._model_to_dataclass(coupon_model)
|
||||||
|
|
||||||
|
def validate_coupon(
|
||||||
|
self,
|
||||||
|
code: str,
|
||||||
|
tenant_id: str
|
||||||
|
) -> CouponValidationResult:
|
||||||
|
"""
|
||||||
|
Validate a coupon code for a specific tenant.
|
||||||
|
Checks: existence, validity, redemption limits, and if tenant already used it.
|
||||||
|
"""
|
||||||
|
# Get coupon
|
||||||
|
coupon = self.get_coupon_by_code(code)
|
||||||
|
if not coupon:
|
||||||
|
return CouponValidationResult(
|
||||||
|
valid=False,
|
||||||
|
coupon=None,
|
||||||
|
error_message="Código de cupón inválido",
|
||||||
|
discount_preview=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if coupon can be redeemed
|
||||||
|
can_redeem, reason = coupon.can_be_redeemed()
|
||||||
|
if not can_redeem:
|
||||||
|
error_messages = {
|
||||||
|
"Coupon is inactive": "Este cupón no está activo",
|
||||||
|
"Coupon is not yet valid": "Este cupón aún no es válido",
|
||||||
|
"Coupon has expired": "Este cupón ha expirado",
|
||||||
|
"Coupon has reached maximum redemptions": "Este cupón ha alcanzado su límite de usos"
|
||||||
|
}
|
||||||
|
return CouponValidationResult(
|
||||||
|
valid=False,
|
||||||
|
coupon=coupon,
|
||||||
|
error_message=error_messages.get(reason, reason),
|
||||||
|
discount_preview=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if tenant already redeemed this coupon
|
||||||
|
existing_redemption = self.db.query(CouponRedemptionModel).filter(
|
||||||
|
and_(
|
||||||
|
CouponRedemptionModel.tenant_id == tenant_id,
|
||||||
|
CouponRedemptionModel.coupon_code == code.upper()
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing_redemption:
|
||||||
|
return CouponValidationResult(
|
||||||
|
valid=False,
|
||||||
|
coupon=coupon,
|
||||||
|
error_message="Ya has utilizado este cupón",
|
||||||
|
discount_preview=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate discount preview
|
||||||
|
discount_preview = self._generate_discount_preview(coupon)
|
||||||
|
|
||||||
|
return CouponValidationResult(
|
||||||
|
valid=True,
|
||||||
|
coupon=coupon,
|
||||||
|
error_message=None,
|
||||||
|
discount_preview=discount_preview
|
||||||
|
)
|
||||||
|
|
||||||
|
def redeem_coupon(
|
||||||
|
self,
|
||||||
|
code: str,
|
||||||
|
tenant_id: str,
|
||||||
|
base_trial_days: int = 14
|
||||||
|
) -> tuple[bool, Optional[CouponRedemption], Optional[str]]:
|
||||||
|
"""
|
||||||
|
Redeem a coupon for a tenant.
|
||||||
|
Returns (success, redemption, error_message)
|
||||||
|
"""
|
||||||
|
# Validate first
|
||||||
|
validation = self.validate_coupon(code, tenant_id)
|
||||||
|
if not validation.valid:
|
||||||
|
return False, None, validation.error_message
|
||||||
|
|
||||||
|
coupon = validation.coupon
|
||||||
|
|
||||||
|
# Calculate discount applied
|
||||||
|
discount_applied = self._calculate_discount_applied(
|
||||||
|
coupon,
|
||||||
|
base_trial_days
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create redemption record
|
||||||
|
redemption_model = CouponRedemptionModel(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
coupon_code=code.upper(),
|
||||||
|
redeemed_at=datetime.utcnow(),
|
||||||
|
discount_applied=discount_applied,
|
||||||
|
extra_data={
|
||||||
|
"coupon_type": coupon.discount_type.value,
|
||||||
|
"coupon_value": coupon.discount_value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.db.add(redemption_model)
|
||||||
|
|
||||||
|
# Increment coupon redemption count
|
||||||
|
coupon_model = self.db.query(CouponModel).filter(
|
||||||
|
CouponModel.code == code.upper()
|
||||||
|
).first()
|
||||||
|
if coupon_model:
|
||||||
|
coupon_model.current_redemptions += 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.db.commit()
|
||||||
|
self.db.refresh(redemption_model)
|
||||||
|
|
||||||
|
redemption = CouponRedemption(
|
||||||
|
id=str(redemption_model.id),
|
||||||
|
tenant_id=redemption_model.tenant_id,
|
||||||
|
coupon_code=redemption_model.coupon_code,
|
||||||
|
redeemed_at=redemption_model.redeemed_at,
|
||||||
|
discount_applied=redemption_model.discount_applied,
|
||||||
|
extra_data=redemption_model.extra_data
|
||||||
|
)
|
||||||
|
|
||||||
|
return True, redemption, None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.db.rollback()
|
||||||
|
return False, None, f"Error al aplicar el cupón: {str(e)}"
|
||||||
|
|
||||||
|
def get_redemption_by_tenant_and_code(
|
||||||
|
self,
|
||||||
|
tenant_id: str,
|
||||||
|
code: str
|
||||||
|
) -> Optional[CouponRedemption]:
|
||||||
|
"""Get existing redemption for tenant and coupon code"""
|
||||||
|
redemption_model = self.db.query(CouponRedemptionModel).filter(
|
||||||
|
and_(
|
||||||
|
CouponRedemptionModel.tenant_id == tenant_id,
|
||||||
|
CouponRedemptionModel.coupon_code == code.upper()
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not redemption_model:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return CouponRedemption(
|
||||||
|
id=str(redemption_model.id),
|
||||||
|
tenant_id=redemption_model.tenant_id,
|
||||||
|
coupon_code=redemption_model.coupon_code,
|
||||||
|
redeemed_at=redemption_model.redeemed_at,
|
||||||
|
discount_applied=redemption_model.discount_applied,
|
||||||
|
extra_data=redemption_model.extra_data
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_coupon_usage_stats(self, code: str) -> Optional[dict]:
|
||||||
|
"""Get usage statistics for a coupon"""
|
||||||
|
coupon_model = self.db.query(CouponModel).filter(
|
||||||
|
CouponModel.code == code.upper()
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not coupon_model:
|
||||||
|
return None
|
||||||
|
|
||||||
|
redemptions_count = self.db.query(CouponRedemptionModel).filter(
|
||||||
|
CouponRedemptionModel.coupon_code == code.upper()
|
||||||
|
).count()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"code": coupon_model.code,
|
||||||
|
"current_redemptions": coupon_model.current_redemptions,
|
||||||
|
"max_redemptions": coupon_model.max_redemptions,
|
||||||
|
"redemptions_remaining": (
|
||||||
|
coupon_model.max_redemptions - coupon_model.current_redemptions
|
||||||
|
if coupon_model.max_redemptions
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"active": coupon_model.active,
|
||||||
|
"valid_from": coupon_model.valid_from.isoformat(),
|
||||||
|
"valid_until": coupon_model.valid_until.isoformat() if coupon_model.valid_until else None
|
||||||
|
}
|
||||||
|
|
||||||
|
def _model_to_dataclass(self, model: CouponModel) -> Coupon:
|
||||||
|
"""Convert SQLAlchemy model to dataclass"""
|
||||||
|
return Coupon(
|
||||||
|
id=str(model.id),
|
||||||
|
code=model.code,
|
||||||
|
discount_type=DiscountType(model.discount_type),
|
||||||
|
discount_value=model.discount_value,
|
||||||
|
max_redemptions=model.max_redemptions,
|
||||||
|
current_redemptions=model.current_redemptions,
|
||||||
|
valid_from=model.valid_from,
|
||||||
|
valid_until=model.valid_until,
|
||||||
|
active=model.active,
|
||||||
|
created_at=model.created_at,
|
||||||
|
extra_data=model.extra_data
|
||||||
|
)
|
||||||
|
|
||||||
|
def _generate_discount_preview(self, coupon: Coupon) -> dict:
|
||||||
|
"""Generate a preview of the discount to be applied"""
|
||||||
|
description = format_discount_description(coupon)
|
||||||
|
|
||||||
|
preview = {
|
||||||
|
"description": description,
|
||||||
|
"discount_type": coupon.discount_type.value,
|
||||||
|
"discount_value": coupon.discount_value
|
||||||
|
}
|
||||||
|
|
||||||
|
if coupon.discount_type == DiscountType.TRIAL_EXTENSION:
|
||||||
|
trial_end = calculate_trial_end_date(14, coupon.discount_value)
|
||||||
|
preview["trial_end_date"] = trial_end.isoformat()
|
||||||
|
preview["total_trial_days"] = 14 + coupon.discount_value
|
||||||
|
|
||||||
|
return preview
|
||||||
|
|
||||||
|
def _calculate_discount_applied(
|
||||||
|
self,
|
||||||
|
coupon: Coupon,
|
||||||
|
base_trial_days: int
|
||||||
|
) -> dict:
|
||||||
|
"""Calculate the actual discount that will be applied"""
|
||||||
|
discount = {
|
||||||
|
"type": coupon.discount_type.value,
|
||||||
|
"value": coupon.discount_value,
|
||||||
|
"description": format_discount_description(coupon)
|
||||||
|
}
|
||||||
|
|
||||||
|
if coupon.discount_type == DiscountType.TRIAL_EXTENSION:
|
||||||
|
total_trial_days = base_trial_days + coupon.discount_value
|
||||||
|
trial_end = calculate_trial_end_date(base_trial_days, coupon.discount_value)
|
||||||
|
|
||||||
|
discount["base_trial_days"] = base_trial_days
|
||||||
|
discount["extension_days"] = coupon.discount_value
|
||||||
|
discount["total_trial_days"] = total_trial_days
|
||||||
|
discount["trial_end_date"] = trial_end.isoformat()
|
||||||
|
|
||||||
|
elif coupon.discount_type == DiscountType.PERCENTAGE:
|
||||||
|
discount["percentage_off"] = coupon.discount_value
|
||||||
|
|
||||||
|
elif coupon.discount_type == DiscountType.FIXED_AMOUNT:
|
||||||
|
discount["amount_off_cents"] = coupon.discount_value
|
||||||
|
discount["amount_off_euros"] = coupon.discount_value / 100
|
||||||
|
|
||||||
|
return discount
|
||||||
@@ -18,6 +18,7 @@ class BakeryRegistration(BaseModel):
|
|||||||
phone: str = Field(..., min_length=9, max_length=20)
|
phone: str = Field(..., min_length=9, max_length=20)
|
||||||
business_type: str = Field(default="bakery")
|
business_type: str = Field(default="bakery")
|
||||||
business_model: Optional[str] = Field(default="individual_bakery")
|
business_model: Optional[str] = Field(default="individual_bakery")
|
||||||
|
coupon_code: Optional[str] = Field(None, max_length=50, description="Promotional coupon code")
|
||||||
|
|
||||||
@validator('phone')
|
@validator('phone')
|
||||||
def validate_spanish_phone(cls, v):
|
def validate_spanish_phone(cls, v):
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ This service abstracts payment provider interactions and makes the system paymen
|
|||||||
import structlog
|
import structlog
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
import uuid
|
import uuid
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from shared.clients.payment_client import PaymentProvider, PaymentCustomer, Subscription, PaymentMethod
|
from shared.clients.payment_client import PaymentProvider, PaymentCustomer, Subscription, PaymentMethod
|
||||||
from shared.clients.stripe_client import StripeProvider
|
from shared.clients.stripe_client import StripeProvider
|
||||||
from shared.database.base import create_database_manager
|
from shared.database.base import create_database_manager
|
||||||
from app.repositories.subscription_repository import SubscriptionRepository
|
from app.repositories.subscription_repository import SubscriptionRepository
|
||||||
|
from app.repositories.coupon_repository import CouponRepository
|
||||||
from app.models.tenants import Subscription as SubscriptionModel
|
from app.models.tenants import Subscription as SubscriptionModel
|
||||||
|
|
||||||
logger = structlog.get_logger()
|
logger = structlog.get_logger()
|
||||||
@@ -20,7 +22,7 @@ logger = structlog.get_logger()
|
|||||||
class PaymentService:
|
class PaymentService:
|
||||||
"""Service for handling payment provider interactions"""
|
"""Service for handling payment provider interactions"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, db_session: Optional[Session] = None):
|
||||||
# Initialize payment provider based on configuration
|
# Initialize payment provider based on configuration
|
||||||
# For now, we'll use Stripe, but this can be swapped for other providers
|
# For now, we'll use Stripe, but this can be swapped for other providers
|
||||||
self.payment_provider: PaymentProvider = StripeProvider(
|
self.payment_provider: PaymentProvider = StripeProvider(
|
||||||
@@ -31,6 +33,7 @@ class PaymentService:
|
|||||||
# Initialize database components
|
# Initialize database components
|
||||||
self.database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
self.database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||||
self.subscription_repo = SubscriptionRepository(SubscriptionModel, None) # Will be set in methods
|
self.subscription_repo = SubscriptionRepository(SubscriptionModel, None) # Will be set in methods
|
||||||
|
self.db_session = db_session # Optional session for coupon operations
|
||||||
|
|
||||||
async def create_customer(self, user_data: Dict[str, Any]) -> PaymentCustomer:
|
async def create_customer(self, user_data: Dict[str, Any]) -> PaymentCustomer:
|
||||||
"""Create a customer in the payment provider system"""
|
"""Create a customer in the payment provider system"""
|
||||||
@@ -68,35 +71,121 @@ class PaymentService:
|
|||||||
logger.error("Failed to create subscription in payment provider", error=str(e))
|
logger.error("Failed to create subscription in payment provider", error=str(e))
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
def validate_coupon_code(
|
||||||
|
self,
|
||||||
|
coupon_code: str,
|
||||||
|
tenant_id: str,
|
||||||
|
db_session: Session
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Validate a coupon code for a tenant.
|
||||||
|
Returns validation result with discount preview.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
coupon_repo = CouponRepository(db_session)
|
||||||
|
validation = coupon_repo.validate_coupon(coupon_code, tenant_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"valid": validation.valid,
|
||||||
|
"error_message": validation.error_message,
|
||||||
|
"discount_preview": validation.discount_preview,
|
||||||
|
"coupon": {
|
||||||
|
"code": validation.coupon.code,
|
||||||
|
"discount_type": validation.coupon.discount_type.value,
|
||||||
|
"discount_value": validation.coupon.discount_value
|
||||||
|
} if validation.coupon else None
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to validate coupon", error=str(e), coupon_code=coupon_code)
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"error_message": "Error al validar el cupón",
|
||||||
|
"discount_preview": None,
|
||||||
|
"coupon": None
|
||||||
|
}
|
||||||
|
|
||||||
|
def redeem_coupon(
|
||||||
|
self,
|
||||||
|
coupon_code: str,
|
||||||
|
tenant_id: str,
|
||||||
|
db_session: Session,
|
||||||
|
base_trial_days: int = 14
|
||||||
|
) -> tuple[bool, Optional[Dict[str, Any]], Optional[str]]:
|
||||||
|
"""
|
||||||
|
Redeem a coupon for a tenant.
|
||||||
|
Returns (success, discount_applied, error_message)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
coupon_repo = CouponRepository(db_session)
|
||||||
|
success, redemption, error = coupon_repo.redeem_coupon(
|
||||||
|
coupon_code,
|
||||||
|
tenant_id,
|
||||||
|
base_trial_days
|
||||||
|
)
|
||||||
|
|
||||||
|
if success and redemption:
|
||||||
|
return True, redemption.discount_applied, None
|
||||||
|
else:
|
||||||
|
return False, None, error
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to redeem coupon", error=str(e), coupon_code=coupon_code)
|
||||||
|
return False, None, f"Error al aplicar el cupón: {str(e)}"
|
||||||
|
|
||||||
async def process_registration_with_subscription(
|
async def process_registration_with_subscription(
|
||||||
self,
|
self,
|
||||||
user_data: Dict[str, Any],
|
user_data: Dict[str, Any],
|
||||||
plan_id: str,
|
plan_id: str,
|
||||||
payment_method_id: str,
|
payment_method_id: str,
|
||||||
use_trial: bool = False
|
use_trial: bool = False,
|
||||||
|
coupon_code: Optional[str] = None,
|
||||||
|
db_session: Optional[Session] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Process user registration with subscription creation"""
|
"""Process user registration with subscription creation"""
|
||||||
try:
|
try:
|
||||||
# Create customer in payment provider
|
# Create customer in payment provider
|
||||||
customer = await self.create_customer(user_data)
|
customer = await self.create_customer(user_data)
|
||||||
|
|
||||||
# Determine trial period
|
# Determine trial period (default 14 days)
|
||||||
trial_period_days = None
|
trial_period_days = 14 if use_trial else 0
|
||||||
if use_trial:
|
|
||||||
trial_period_days = 90 # 3 months trial for pilot users
|
# Apply coupon if provided
|
||||||
|
coupon_discount = None
|
||||||
|
if coupon_code and db_session:
|
||||||
|
# Redeem coupon
|
||||||
|
success, discount, error = self.redeem_coupon(
|
||||||
|
coupon_code,
|
||||||
|
user_data.get('tenant_id'),
|
||||||
|
db_session,
|
||||||
|
trial_period_days
|
||||||
|
)
|
||||||
|
|
||||||
|
if success and discount:
|
||||||
|
coupon_discount = discount
|
||||||
|
# Update trial period if coupon extends it
|
||||||
|
if discount.get("type") == "trial_extension":
|
||||||
|
trial_period_days = discount.get("total_trial_days", trial_period_days)
|
||||||
|
logger.info(
|
||||||
|
"Coupon applied successfully",
|
||||||
|
coupon_code=coupon_code,
|
||||||
|
extended_trial_days=trial_period_days
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning("Failed to apply coupon", error=error, coupon_code=coupon_code)
|
||||||
|
|
||||||
# Create subscription
|
# Create subscription
|
||||||
subscription = await self.create_subscription(
|
subscription = await self.create_subscription(
|
||||||
customer.id,
|
customer.id,
|
||||||
plan_id,
|
plan_id,
|
||||||
payment_method_id,
|
payment_method_id,
|
||||||
trial_period_days
|
trial_period_days if trial_period_days > 0 else None
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save subscription to database
|
# Save subscription to database
|
||||||
async with self.database_manager.get_session() as session:
|
async with self.database_manager.get_session() as session:
|
||||||
self.subscription_repo.session = session
|
self.subscription_repo.session = session
|
||||||
subscription_record = await self.subscription_repo.create({
|
subscription_data = {
|
||||||
'id': str(uuid.uuid4()),
|
'id': str(uuid.uuid4()),
|
||||||
'tenant_id': user_data.get('tenant_id'),
|
'tenant_id': user_data.get('tenant_id'),
|
||||||
'customer_id': customer.id,
|
'customer_id': customer.id,
|
||||||
@@ -106,15 +195,26 @@ class PaymentService:
|
|||||||
'current_period_start': subscription.current_period_start,
|
'current_period_start': subscription.current_period_start,
|
||||||
'current_period_end': subscription.current_period_end,
|
'current_period_end': subscription.current_period_end,
|
||||||
'created_at': subscription.created_at,
|
'created_at': subscription.created_at,
|
||||||
'trial_period_days': trial_period_days
|
'trial_period_days': trial_period_days if trial_period_days > 0 else None
|
||||||
})
|
}
|
||||||
|
subscription_record = await self.subscription_repo.create(subscription_data)
|
||||||
|
|
||||||
return {
|
result = {
|
||||||
'customer_id': customer.id,
|
'customer_id': customer.id,
|
||||||
'subscription_id': subscription.id,
|
'subscription_id': subscription.id,
|
||||||
'status': subscription.status,
|
'status': subscription.status,
|
||||||
'trial_period_days': trial_period_days
|
'trial_period_days': trial_period_days
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Include coupon info if applied
|
||||||
|
if coupon_discount:
|
||||||
|
result['coupon_applied'] = {
|
||||||
|
'code': coupon_code,
|
||||||
|
'discount': coupon_discount
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to process registration with subscription", error=str(e))
|
logger.error("Failed to process registration with subscription", error=str(e))
|
||||||
raise e
|
raise e
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"""add_coupon_system
|
||||||
|
|
||||||
|
Revision ID: 20251017_0000
|
||||||
|
Revises: 20251016_0000
|
||||||
|
Create Date: 2025-10-17 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '20251017_0000'
|
||||||
|
down_revision = '20251016_0000'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Create coupons table
|
||||||
|
op.create_table(
|
||||||
|
'coupons',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True, default=uuid.uuid4),
|
||||||
|
sa.Column('code', sa.String(50), nullable=False, unique=True),
|
||||||
|
sa.Column('discount_type', sa.String(20), nullable=False),
|
||||||
|
sa.Column('discount_value', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('max_redemptions', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('current_redemptions', sa.Integer(), nullable=False, server_default='0'),
|
||||||
|
sa.Column('valid_from', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('valid_until', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('active', sa.Boolean(), nullable=False, server_default='true'),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
|
||||||
|
sa.Column('extra_data', postgresql.JSON(astext_type=sa.Text()), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create indexes for coupons table
|
||||||
|
op.create_index('idx_coupon_code_active', 'coupons', ['code', 'active'])
|
||||||
|
op.create_index('idx_coupon_valid_dates', 'coupons', ['valid_from', 'valid_until'])
|
||||||
|
|
||||||
|
# Create coupon_redemptions table
|
||||||
|
op.create_table(
|
||||||
|
'coupon_redemptions',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True, default=uuid.uuid4),
|
||||||
|
sa.Column('tenant_id', sa.String(255), nullable=False),
|
||||||
|
sa.Column('coupon_code', sa.String(50), nullable=False),
|
||||||
|
sa.Column('redeemed_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
|
||||||
|
sa.Column('discount_applied', postgresql.JSON(astext_type=sa.Text()), nullable=False),
|
||||||
|
sa.Column('extra_data', postgresql.JSON(astext_type=sa.Text()), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['coupon_code'], ['coupons.code'], name='fk_coupon_redemption_code'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create indexes for coupon_redemptions table
|
||||||
|
op.create_index('idx_redemption_tenant', 'coupon_redemptions', ['tenant_id'])
|
||||||
|
op.create_index('idx_redemption_coupon', 'coupon_redemptions', ['coupon_code'])
|
||||||
|
op.create_index('idx_redemption_tenant_coupon', 'coupon_redemptions', ['tenant_id', 'coupon_code'])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Drop indexes first
|
||||||
|
op.drop_index('idx_redemption_tenant_coupon', table_name='coupon_redemptions')
|
||||||
|
op.drop_index('idx_redemption_coupon', table_name='coupon_redemptions')
|
||||||
|
op.drop_index('idx_redemption_tenant', table_name='coupon_redemptions')
|
||||||
|
op.drop_index('idx_coupon_valid_dates', table_name='coupons')
|
||||||
|
op.drop_index('idx_coupon_code_active', table_name='coupons')
|
||||||
|
|
||||||
|
# Drop tables
|
||||||
|
op.drop_table('coupon_redemptions')
|
||||||
|
op.drop_table('coupons')
|
||||||
102
services/tenant/scripts/seed_pilot_coupon.py
Normal file
102
services/tenant/scripts/seed_pilot_coupon.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
"""
|
||||||
|
Seed script to create the PILOT2025 coupon for the pilot customer program.
|
||||||
|
This coupon provides 3 months (90 days) free trial extension for the first 20 customers.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
# Add project root to path
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..')))
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.models.coupon import CouponModel
|
||||||
|
from shared.database import get_db
|
||||||
|
|
||||||
|
|
||||||
|
def seed_pilot_coupon(db: Session):
|
||||||
|
"""Create or update the PILOT2025 coupon"""
|
||||||
|
|
||||||
|
coupon_code = "PILOT2025"
|
||||||
|
|
||||||
|
# Check if coupon already exists
|
||||||
|
existing_coupon = db.query(CouponModel).filter(
|
||||||
|
CouponModel.code == coupon_code
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing_coupon:
|
||||||
|
print(f"✓ Coupon {coupon_code} already exists")
|
||||||
|
print(f" Current redemptions: {existing_coupon.current_redemptions}/{existing_coupon.max_redemptions}")
|
||||||
|
print(f" Active: {existing_coupon.active}")
|
||||||
|
print(f" Valid from: {existing_coupon.valid_from}")
|
||||||
|
print(f" Valid until: {existing_coupon.valid_until}")
|
||||||
|
return existing_coupon
|
||||||
|
|
||||||
|
# Create new coupon
|
||||||
|
now = datetime.utcnow()
|
||||||
|
valid_until = now + timedelta(days=180) # Valid for 6 months
|
||||||
|
|
||||||
|
coupon = CouponModel(
|
||||||
|
id=uuid.uuid4(),
|
||||||
|
code=coupon_code,
|
||||||
|
discount_type="trial_extension",
|
||||||
|
discount_value=90, # 90 days = 3 months
|
||||||
|
max_redemptions=20, # First 20 pilot customers
|
||||||
|
current_redemptions=0,
|
||||||
|
valid_from=now,
|
||||||
|
valid_until=valid_until,
|
||||||
|
active=True,
|
||||||
|
created_at=now,
|
||||||
|
extra_data={
|
||||||
|
"program": "pilot_launch_2025",
|
||||||
|
"description": "Programa piloto - 3 meses gratis para los primeros 20 clientes",
|
||||||
|
"terms": "Válido para nuevos registros únicamente. Un cupón por cliente."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add(coupon)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(coupon)
|
||||||
|
|
||||||
|
print(f"✓ Successfully created coupon: {coupon_code}")
|
||||||
|
print(f" Type: Trial Extension")
|
||||||
|
print(f" Value: 90 days (3 months)")
|
||||||
|
print(f" Max redemptions: 20")
|
||||||
|
print(f" Valid from: {coupon.valid_from}")
|
||||||
|
print(f" Valid until: {coupon.valid_until}")
|
||||||
|
print(f" ID: {coupon.id}")
|
||||||
|
|
||||||
|
return coupon
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main execution function"""
|
||||||
|
print("=" * 60)
|
||||||
|
print("Seeding PILOT2025 Coupon for Pilot Customer Program")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get database session
|
||||||
|
db = next(get_db())
|
||||||
|
|
||||||
|
# Seed the coupon
|
||||||
|
seed_pilot_coupon(db)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("✓ Coupon seeding completed successfully!")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Error seeding coupon: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
123
shared/subscription/coupons.py
Normal file
123
shared/subscription/coupons.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
"""
|
||||||
|
Coupon system for subscription discounts and promotions.
|
||||||
|
Supports trial extensions, percentage discounts, and fixed amount discounts.
|
||||||
|
"""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class DiscountType(str, Enum):
|
||||||
|
"""Type of discount offered by a coupon"""
|
||||||
|
TRIAL_EXTENSION = "trial_extension" # Extends trial period by X days
|
||||||
|
PERCENTAGE = "percentage" # X% off subscription price
|
||||||
|
FIXED_AMOUNT = "fixed_amount" # Fixed amount off subscription price
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Coupon:
|
||||||
|
"""Coupon configuration"""
|
||||||
|
id: str
|
||||||
|
code: str
|
||||||
|
discount_type: DiscountType
|
||||||
|
discount_value: int # Days for trial_extension, percentage for percentage, cents for fixed_amount
|
||||||
|
max_redemptions: Optional[int] # None = unlimited
|
||||||
|
current_redemptions: int
|
||||||
|
valid_from: datetime
|
||||||
|
valid_until: Optional[datetime] # None = no expiry
|
||||||
|
active: bool
|
||||||
|
created_at: datetime
|
||||||
|
extra_data: Optional[dict] = None
|
||||||
|
|
||||||
|
def is_valid(self) -> bool:
|
||||||
|
"""Check if coupon is currently valid"""
|
||||||
|
now = datetime.utcnow()
|
||||||
|
|
||||||
|
# Check if active
|
||||||
|
if not self.active:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if started
|
||||||
|
if now < self.valid_from:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if expired
|
||||||
|
if self.valid_until and now > self.valid_until:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if max redemptions reached
|
||||||
|
if self.max_redemptions and self.current_redemptions >= self.max_redemptions:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def can_be_redeemed(self) -> tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Check if coupon can be redeemed.
|
||||||
|
Returns (can_redeem, reason_if_not)
|
||||||
|
"""
|
||||||
|
if not self.active:
|
||||||
|
return False, "Coupon is inactive"
|
||||||
|
|
||||||
|
now = datetime.utcnow()
|
||||||
|
|
||||||
|
if now < self.valid_from:
|
||||||
|
return False, "Coupon is not yet valid"
|
||||||
|
|
||||||
|
if self.valid_until and now > self.valid_until:
|
||||||
|
return False, "Coupon has expired"
|
||||||
|
|
||||||
|
if self.max_redemptions and self.current_redemptions >= self.max_redemptions:
|
||||||
|
return False, "Coupon has reached maximum redemptions"
|
||||||
|
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CouponRedemption:
|
||||||
|
"""Record of a coupon redemption"""
|
||||||
|
id: str
|
||||||
|
tenant_id: str
|
||||||
|
coupon_code: str
|
||||||
|
redeemed_at: datetime
|
||||||
|
discount_applied: dict # Details of the discount applied
|
||||||
|
extra_data: Optional[dict] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CouponValidationResult:
|
||||||
|
"""Result of coupon validation"""
|
||||||
|
valid: bool
|
||||||
|
coupon: Optional[Coupon]
|
||||||
|
error_message: Optional[str]
|
||||||
|
discount_preview: Optional[dict] # Preview of discount to be applied
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_trial_end_date(base_trial_days: int, extension_days: int) -> datetime:
|
||||||
|
"""Calculate trial end date with coupon extension"""
|
||||||
|
from datetime import timedelta
|
||||||
|
total_days = base_trial_days + extension_days
|
||||||
|
return datetime.utcnow() + timedelta(days=total_days)
|
||||||
|
|
||||||
|
|
||||||
|
def format_discount_description(coupon: Coupon) -> str:
|
||||||
|
"""Format human-readable discount description"""
|
||||||
|
if coupon.discount_type == DiscountType.TRIAL_EXTENSION:
|
||||||
|
months = coupon.discount_value // 30
|
||||||
|
days = coupon.discount_value % 30
|
||||||
|
if months > 0 and days == 0:
|
||||||
|
return f"{months} {'mes' if months == 1 else 'meses'} gratis"
|
||||||
|
elif months > 0:
|
||||||
|
return f"{months} {'mes' if months == 1 else 'meses'} y {days} días gratis"
|
||||||
|
else:
|
||||||
|
return f"{coupon.discount_value} días gratis"
|
||||||
|
|
||||||
|
elif coupon.discount_type == DiscountType.PERCENTAGE:
|
||||||
|
return f"{coupon.discount_value}% de descuento"
|
||||||
|
|
||||||
|
elif coupon.discount_type == DiscountType.FIXED_AMOUNT:
|
||||||
|
amount = coupon.discount_value / 100 # Convert cents to euros
|
||||||
|
return f"€{amount:.2f} de descuento"
|
||||||
|
|
||||||
|
return "Descuento aplicado"
|
||||||
Reference in New Issue
Block a user