Fix issues
This commit is contained in:
@@ -8,6 +8,7 @@ import { SubscriptionSelection } from './SubscriptionSelection';
|
|||||||
import PaymentForm from './PaymentForm';
|
import PaymentForm from './PaymentForm';
|
||||||
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';
|
||||||
|
|
||||||
// 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');
|
||||||
@@ -168,48 +169,69 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Render step indicator
|
// Render step indicator
|
||||||
const renderStepIndicator = () => (
|
const renderStepIndicator = () => {
|
||||||
<div className="flex justify-center mb-6">
|
const steps = [
|
||||||
<div className="flex items-center space-x-4">
|
{ key: 'basic_info', label: 'Información', number: 1 },
|
||||||
<div className={`flex items-center justify-center w-8 h-8 rounded-full ${
|
{ key: 'subscription', label: 'Plan', number: 2 },
|
||||||
currentStep === 'basic_info' ? 'bg-color-primary text-white' :
|
{ key: 'payment', label: 'Pago', number: 3 }
|
||||||
currentStep === 'subscription' || currentStep === 'payment' ? 'bg-color-success text-white' : 'bg-bg-secondary text-text-secondary'
|
];
|
||||||
}`}>
|
|
||||||
1
|
const getStepIndex = (step: RegistrationStep) => {
|
||||||
</div>
|
return steps.findIndex(s => s.key === step);
|
||||||
<div className="h-1 w-16 bg-border-primary"></div>
|
};
|
||||||
<div className={`flex items-center justify-center w-8 h-8 rounded-full ${
|
|
||||||
currentStep === 'subscription' ? 'bg-color-primary text-white' :
|
const currentIndex = getStepIndex(currentStep);
|
||||||
currentStep === 'payment' ? 'bg-color-success text-white' : 'bg-bg-secondary text-text-secondary'
|
|
||||||
}`}>
|
return (
|
||||||
2
|
<div className="mb-6 sm:mb-8">
|
||||||
</div>
|
<div className="flex justify-center items-center">
|
||||||
<div className="h-1 w-16 bg-border-primary"></div>
|
{steps.map((step, index) => (
|
||||||
<div className={`flex items-center justify-center w-8 h-8 rounded-full ${
|
<React.Fragment key={step.key}>
|
||||||
currentStep === 'payment' ? 'bg-color-primary text-white' : 'bg-bg-secondary text-text-secondary'
|
<div className="flex flex-col items-center">
|
||||||
}`}>
|
<div className={`flex items-center justify-center w-8 h-8 sm:w-10 sm:h-10 rounded-full font-semibold text-sm transition-all duration-200 ${
|
||||||
3
|
index < currentIndex
|
||||||
|
? 'bg-color-success text-white'
|
||||||
|
: index === currentIndex
|
||||||
|
? 'bg-color-primary text-white ring-4 ring-color-primary/20'
|
||||||
|
: 'bg-bg-secondary text-text-secondary'
|
||||||
|
}`}>
|
||||||
|
{index < currentIndex ? (
|
||||||
|
<CheckCircle className="w-5 h-5" />
|
||||||
|
) : (
|
||||||
|
step.number
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className={`mt-1 sm:mt-2 text-[10px] sm:text-xs font-medium hidden sm:block ${
|
||||||
|
index <= currentIndex ? 'text-text-primary' : 'text-text-secondary'
|
||||||
|
}`}>
|
||||||
|
{step.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{index < steps.length - 1 && (
|
||||||
|
<div className={`h-0.5 w-8 sm:w-16 mx-2 sm:mx-4 transition-all duration-200 ${
|
||||||
|
index < currentIndex ? 'bg-color-success' : 'bg-border-primary'
|
||||||
|
}`} />
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
// Render current step
|
// Render current step
|
||||||
const renderCurrentStep = () => {
|
const renderCurrentStep = () => {
|
||||||
switch (currentStep) {
|
switch (currentStep) {
|
||||||
case 'basic_info':
|
case 'basic_info':
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-4 sm:space-y-6">
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-4 sm:mb-6">
|
||||||
<h1 className="text-3xl font-bold text-text-primary mb-2">
|
<h1 className="text-2xl sm:text-3xl font-bold text-text-primary mb-2">
|
||||||
{t('auth:register.title', 'Crear Cuenta')}
|
{t('auth:register.title', 'Crear Cuenta')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-text-secondary text-lg">
|
<p className="text-text-secondary text-sm sm:text-base">
|
||||||
{t('auth:register.subtitle', 'Únete y comienza hoy mismo')}
|
{t('auth:register.subtitle', 'Únete y comienza hoy mismo')}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-text-tertiary mt-2">
|
|
||||||
Paso 1 de 3: Información Básica
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={(e) => { e.preventDefault(); handleNextStep(); }} className="space-y-6">
|
<form onSubmit={(e) => { e.preventDefault(); handleNextStep(); }} className="space-y-6">
|
||||||
@@ -396,14 +418,13 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between pt-4">
|
<div className="flex justify-end pt-4">
|
||||||
<div></div> {/* Spacer for alignment */}
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={isLoading || !validatePassword(formData.password) || !formData.acceptTerms || passwordMatchStatus !== 'match'}
|
disabled={isLoading || !validatePassword(formData.password) || !formData.acceptTerms || passwordMatchStatus !== 'match'}
|
||||||
className="w-48"
|
className="w-full sm:w-48"
|
||||||
>
|
>
|
||||||
Siguiente
|
Siguiente
|
||||||
</Button>
|
</Button>
|
||||||
@@ -415,16 +436,13 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|||||||
case 'subscription':
|
case 'subscription':
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="text-center mb-8">
|
<div className="text-center">
|
||||||
<h1 className="text-3xl font-bold text-text-primary mb-2">
|
<h1 className="text-2xl sm:text-3xl font-bold text-text-primary mb-2">
|
||||||
{t('auth:subscription.select_plan', 'Selecciona tu plan')}
|
{t('auth:subscription.select_plan', 'Selecciona tu plan')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-text-secondary text-lg">
|
<p className="text-text-secondary text-sm sm:text-base">
|
||||||
{t('auth:subscription.choose_plan', 'Elige el plan que mejor se adapte a tu negocio')}
|
{t('auth:subscription.choose_plan', 'Elige el plan que mejor se adapte a tu negocio')}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-text-tertiary mt-2">
|
|
||||||
Paso 2 de 3: Plan de Suscripción
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SubscriptionSelection
|
<SubscriptionSelection
|
||||||
@@ -435,13 +453,13 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|||||||
trialSelected={useTrial}
|
trialSelected={useTrial}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex justify-between pt-4">
|
<div className="flex flex-col sm:flex-row justify-between gap-3 sm:gap-4 pt-2 border-t border-border-primary">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="lg"
|
size="lg"
|
||||||
onClick={handlePreviousStep}
|
onClick={handlePreviousStep}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="w-48"
|
className="w-full sm:w-48 order-2 sm:order-1"
|
||||||
>
|
>
|
||||||
Anterior
|
Anterior
|
||||||
</Button>
|
</Button>
|
||||||
@@ -450,7 +468,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|||||||
size="lg"
|
size="lg"
|
||||||
onClick={handleNextStep}
|
onClick={handleNextStep}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="w-48"
|
className="w-full sm:w-48 order-1 sm:order-2"
|
||||||
>
|
>
|
||||||
Siguiente
|
Siguiente
|
||||||
</Button>
|
</Button>
|
||||||
@@ -460,17 +478,14 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|||||||
|
|
||||||
case 'payment':
|
case 'payment':
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-4 sm:space-y-6">
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-4 sm:mb-6">
|
||||||
<h1 className="text-3xl font-bold text-text-primary mb-2">
|
<h1 className="text-2xl sm:text-3xl font-bold text-text-primary mb-2">
|
||||||
{t('auth:payment.payment_info', 'Información de Pago')}
|
{t('auth:payment.payment_info', 'Información de Pago')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-text-secondary text-lg">
|
<p className="text-text-secondary text-xs sm:text-sm">
|
||||||
{t('auth:payment.secure_payment', 'Tu información de pago está protegida con encriptación de extremo a extremo')}
|
{t('auth:payment.secure_payment', 'Tu información de pago está protegida con encriptación de extremo a extremo')}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-text-tertiary mt-2">
|
|
||||||
Paso 3 de 3: Procesamiento de Pago
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Elements stripe={stripePromise}>
|
<Elements stripe={stripePromise}>
|
||||||
@@ -482,13 +497,13 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|||||||
/>
|
/>
|
||||||
</Elements>
|
</Elements>
|
||||||
|
|
||||||
<div className="flex justify-between pt-4">
|
<div className="flex justify-start pt-4">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="lg"
|
size="lg"
|
||||||
onClick={handlePreviousStep}
|
onClick={handlePreviousStep}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="w-48"
|
className="w-full sm:w-48"
|
||||||
>
|
>
|
||||||
Anterior
|
Anterior
|
||||||
</Button>
|
</Button>
|
||||||
@@ -499,7 +514,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={`p-8 w-full max-w-3xl ${className || ''}`} role="main">
|
<Card className={`p-4 sm:p-6 lg:p-8 w-full max-w-6xl ${className || ''}`} role="main">
|
||||||
{renderStepIndicator()}
|
{renderStepIndicator()}
|
||||||
{renderCurrentStep()}
|
{renderCurrentStep()}
|
||||||
|
|
||||||
|
|||||||
@@ -55,25 +55,16 @@ export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`space-y-6 ${className}`}>
|
<div className={`space-y-4 ${className}`}>
|
||||||
<div className="text-center mb-8">
|
|
||||||
<h2 className="text-2xl font-bold text-text-primary mb-2">
|
|
||||||
{t('auth:subscription.select_plan', 'Selecciona tu plan')}
|
|
||||||
</h2>
|
|
||||||
<p className="text-text-secondary">
|
|
||||||
{t('auth:subscription.choose_plan', 'Elige el plan que mejor se adapte a tu negocio')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showTrialOption && (
|
{showTrialOption && (
|
||||||
<Card className="p-4 mb-6 bg-blue-50 border-blue-200">
|
<Card className="p-4 border-2 border-color-primary/30 bg-bg-primary">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3 flex-1">
|
||||||
<div className="p-2 bg-blue-100 rounded-lg">
|
<div className="p-2.5 bg-color-primary/10 rounded-lg flex-shrink-0">
|
||||||
<Star className="w-5 h-5 text-blue-600" />
|
<Star className="w-5 h-5 text-color-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="font-semibold text-text-primary">
|
<h3 className="font-semibold text-text-primary text-base">
|
||||||
{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">
|
||||||
@@ -83,8 +74,9 @@ export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant={trialSelected ? "primary" : "outline"}
|
variant={trialSelected ? "primary" : "outline"}
|
||||||
size="sm"
|
size="md"
|
||||||
onClick={handleTrialToggle}
|
onClick={handleTrialToggle}
|
||||||
|
className="w-full sm:w-auto flex-shrink-0 min-w-[100px]"
|
||||||
>
|
>
|
||||||
{trialSelected
|
{trialSelected
|
||||||
? t('auth:subscription.trial_active', 'Activo')
|
? t('auth:subscription.trial_active', 'Activo')
|
||||||
@@ -94,140 +86,142 @@ export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> = ({
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="space-y-3">
|
||||||
{Object.entries(availablePlans.plans).map(([planKey, plan]) => {
|
{Object.entries(availablePlans.plans).map(([planKey, plan]) => {
|
||||||
const isSelected = selectedPlan === planKey;
|
const isSelected = selectedPlan === planKey;
|
||||||
const getPlanColor = () => {
|
|
||||||
switch (planKey) {
|
|
||||||
case 'starter': return 'border-blue-500/30 bg-blue-500/5';
|
|
||||||
case 'professional': return 'border-purple-500/30 bg-purple-500/5';
|
|
||||||
case 'enterprise': return 'border-amber-500/30 bg-amber-500/5';
|
|
||||||
default: return 'border-border-primary bg-bg-secondary';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={planKey}
|
key={planKey}
|
||||||
className={`relative p-6 cursor-pointer transition-all duration-200 hover:shadow-lg ${
|
className={`relative p-5 cursor-pointer transition-all duration-200 border-2 ${
|
||||||
getPlanColor()
|
isSelected
|
||||||
} ${isSelected ? 'ring-2 ring-color-primary' : ''}`}
|
? 'border-color-primary bg-color-primary/5 shadow-lg'
|
||||||
|
: 'border-border-primary bg-bg-primary hover:border-color-primary/40 hover:shadow-md'
|
||||||
|
}`}
|
||||||
onClick={() => onPlanSelect(planKey)}
|
onClick={() => onPlanSelect(planKey)}
|
||||||
>
|
>
|
||||||
|
{/* Popular Badge */}
|
||||||
{plan.popular && (
|
{plan.popular && (
|
||||||
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
|
<div className="absolute -top-2.5 right-4 z-10">
|
||||||
<Badge variant="primary" className="px-3 py-1">
|
<Badge variant="primary" className="px-3 py-1 text-xs font-semibold flex items-center gap-1.5 shadow-md">
|
||||||
<Star className="w-3 h-3 mr-1" />
|
<Star className="w-3.5 h-3.5 fill-current" />
|
||||||
{t('auth:subscription.popular', 'Más Popular')}
|
{t('auth:subscription.popular', 'Más Popular')}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="text-center mb-6">
|
{/* Horizontal Layout */}
|
||||||
<h4 className="text-xl font-bold text-text-primary mb-2">{plan.name}</h4>
|
<div className="flex flex-col md:flex-row gap-6 items-start">
|
||||||
<div className="text-3xl font-bold text-color-primary mb-1">
|
{/* Left Section: Plan Info & Pricing */}
|
||||||
{subscriptionService.formatPrice(plan.monthly_price)}
|
<div className="flex-shrink-0 md:w-52">
|
||||||
<span className="text-lg text-text-secondary">/mes</span>
|
<h4 className="text-2xl font-bold text-text-primary mb-2">{plan.name}</h4>
|
||||||
</div>
|
<div className="flex items-baseline gap-1 mb-3">
|
||||||
<p className="text-sm text-text-secondary">{plan.description}</p>
|
<span className="text-4xl font-bold text-color-primary">
|
||||||
</div>
|
{subscriptionService.formatPrice(plan.monthly_price)}
|
||||||
|
</span>
|
||||||
|
<span className="text-base text-text-secondary font-medium">/mes</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-text-secondary leading-relaxed mb-4">{plan.description}</p>
|
||||||
|
|
||||||
<div className="space-y-3 mb-6">
|
{/* Plan Limits */}
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="space-y-2.5 pt-2 border-t border-border-primary/50">
|
||||||
<Users className="w-4 h-4 text-color-primary" />
|
<div className="flex items-center gap-2.5 text-sm text-text-primary">
|
||||||
<span>{plan.max_users === -1 ? 'Usuarios ilimitados' : `${plan.max_users} usuarios`}</span>
|
<Users className="w-4 h-4 text-color-primary flex-shrink-0" />
|
||||||
|
<span className="font-medium">{plan.max_users === -1 ? 'Usuarios ilimitados' : `${plan.max_users} usuarios`}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2.5 text-sm text-text-primary">
|
||||||
|
<MapPin className="w-4 h-4 text-color-primary flex-shrink-0" />
|
||||||
|
<span className="font-medium">{plan.max_locations === -1 ? 'Ubicaciones ilimitadas' : `${plan.max_locations} ubicación${plan.max_locations > 1 ? 'es' : ''}`}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2.5 text-sm text-text-primary">
|
||||||
|
<Package className="w-4 h-4 text-color-primary flex-shrink-0" />
|
||||||
|
<span className="font-medium">{plan.max_products === -1 ? 'Productos ilimitados' : `${plan.max_products} productos`}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-sm">
|
|
||||||
<MapPin className="w-4 h-4 text-color-primary" />
|
{/* Divider */}
|
||||||
<span>{plan.max_locations === -1 ? 'Ubicaciones ilimitadas' : `${plan.max_locations} ubicación${plan.max_locations > 1 ? 'es' : ''}`}</span>
|
<div className="hidden md:block w-px self-stretch bg-border-primary/50"></div>
|
||||||
|
|
||||||
|
{/* Right Section: Features */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<TrendingUp className="w-5 h-5 text-color-primary flex-shrink-0" />
|
||||||
|
<h5 className="text-base font-bold text-text-primary">
|
||||||
|
{t('auth:subscription.features', 'Funcionalidades Incluidas')}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-x-6 gap-y-2.5">
|
||||||
|
{(() => {
|
||||||
|
const getPlanFeatures = (planKey: string) => {
|
||||||
|
switch (planKey) {
|
||||||
|
case 'starter':
|
||||||
|
return [
|
||||||
|
'Panel de Control Básico',
|
||||||
|
'Gestión de Inventario',
|
||||||
|
'Gestión de Pedidos',
|
||||||
|
'Gestión de Proveedores',
|
||||||
|
'Punto de Venta Básico'
|
||||||
|
];
|
||||||
|
case 'professional':
|
||||||
|
return [
|
||||||
|
'Todo lo de Starter',
|
||||||
|
'Panel Avanzado',
|
||||||
|
'Analytics de Ventas',
|
||||||
|
'Pronósticos con IA',
|
||||||
|
'Optimización de Producción'
|
||||||
|
];
|
||||||
|
case 'enterprise':
|
||||||
|
return [
|
||||||
|
'Todo lo de Professional',
|
||||||
|
'Insights Predictivos IA',
|
||||||
|
'Analytics Multi-ubicación',
|
||||||
|
'Integración ERP',
|
||||||
|
'Soporte 24/7 Prioritario',
|
||||||
|
'API Personalizada'
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return getPlanFeatures(planKey).map((feature, index) => (
|
||||||
|
<div key={index} className="flex items-start gap-2.5 text-sm">
|
||||||
|
<CheckCircle className="w-4 h-4 text-color-success flex-shrink-0 mt-0.5" />
|
||||||
|
<span className="text-text-primary leading-snug">{feature}</span>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-sm">
|
|
||||||
<Package className="w-4 h-4 text-color-primary" />
|
{/* Action Button */}
|
||||||
<span>{plan.max_products === -1 ? 'Productos ilimitados' : `${plan.max_products} productos`}</span>
|
<div className="flex-shrink-0 md:w-28 flex items-center justify-stretch md:justify-end pt-4 md:pt-0 w-full md:w-auto border-t md:border-t-0 md:border-l border-border-primary/50 md:pl-6">
|
||||||
|
<Button
|
||||||
|
variant={isSelected ? "primary" : "outline"}
|
||||||
|
className="w-full md:w-auto md:min-w-[100px]"
|
||||||
|
size="lg"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onPlanSelect(planKey);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSelected ? (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="w-5 h-5 md:mr-2" />
|
||||||
|
<span className="hidden md:inline">Seleccionado</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="md:hidden">Seleccionar</span>
|
||||||
|
<span className="hidden md:inline">Elegir Plan</span>
|
||||||
|
<ArrowRight className="w-5 h-5 ml-2" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Features Section */}
|
|
||||||
<div className="border-t border-border-color pt-4 mb-6">
|
|
||||||
<h5 className="text-sm font-semibold text-text-primary mb-3 flex items-center">
|
|
||||||
<TrendingUp className="w-4 h-4 mr-2 text-color-primary" />
|
|
||||||
{t('auth:subscription.features', 'Funcionalidades Incluidas')}
|
|
||||||
</h5>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{(() => {
|
|
||||||
const getPlanFeatures = (planKey: string) => {
|
|
||||||
switch (planKey) {
|
|
||||||
case 'starter':
|
|
||||||
return [
|
|
||||||
'✓ Panel de Control Básico',
|
|
||||||
'✓ Gestión de Inventario',
|
|
||||||
'✓ Gestión de Pedidos',
|
|
||||||
'✓ Gestión de Proveedores',
|
|
||||||
'✓ Punto de Venta Básico',
|
|
||||||
'✗ Analytics Avanzados',
|
|
||||||
'✗ Pronósticos IA',
|
|
||||||
'✗ Insights Predictivos'
|
|
||||||
];
|
|
||||||
case 'professional':
|
|
||||||
return [
|
|
||||||
'✓ Panel de Control Avanzado',
|
|
||||||
'✓ Gestión de Inventario Completa',
|
|
||||||
'✓ Analytics de Ventas',
|
|
||||||
'✓ Pronósticos con IA (92% precisión)',
|
|
||||||
'✓ Análisis de Rendimiento',
|
|
||||||
'✓ Optimización de Producción',
|
|
||||||
'✓ Integración POS',
|
|
||||||
'✗ Insights Predictivos Avanzados'
|
|
||||||
];
|
|
||||||
case 'enterprise':
|
|
||||||
return [
|
|
||||||
'✓ Todas las funcionalidades Professional',
|
|
||||||
'✓ Insights Predictivos con IA',
|
|
||||||
'✓ Analytics Multi-ubicación',
|
|
||||||
'✓ Integración ERP',
|
|
||||||
'✓ API Personalizada',
|
|
||||||
'✓ Gestor de Cuenta Dedicado',
|
|
||||||
'✓ Soporte 24/7 Prioritario',
|
|
||||||
'✓ Demo Personalizada'
|
|
||||||
];
|
|
||||||
default:
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return getPlanFeatures(planKey).map((feature, index) => (
|
|
||||||
<div key={index} className={`text-xs flex items-center gap-2 ${
|
|
||||||
feature.startsWith('✓')
|
|
||||||
? 'text-green-600'
|
|
||||||
: 'text-text-secondary opacity-60'
|
|
||||||
}`}>
|
|
||||||
<span>{feature}</span>
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant={isSelected ? "primary" : plan.popular ? "primary" : "outline"}
|
|
||||||
className="w-full"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onPlanSelect(planKey);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isSelected ? (
|
|
||||||
<>
|
|
||||||
<CheckCircle className="w-4 h-4 mr-2" />
|
|
||||||
{t('auth:subscription.selected', 'Seleccionado')}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{t('auth:subscription.select', 'Seleccionar Plan')}
|
|
||||||
<ArrowRight className="w-4 h-4 ml-2" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -82,12 +82,7 @@ spec:
|
|||||||
name: pos-integration-secrets
|
name: pos-integration-secrets
|
||||||
- secretRef:
|
- secretRef:
|
||||||
name: whatsapp-secrets
|
name: whatsapp-secrets
|
||||||
env:
|
|
||||||
- name: TRAINING_PERSISTENCE_PATH
|
|
||||||
value: "/app/training_state"
|
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: training-state
|
|
||||||
mountPath: /app/training_state
|
|
||||||
- name: tmp-storage
|
- name: tmp-storage
|
||||||
mountPath: /tmp
|
mountPath: /tmp
|
||||||
resources:
|
resources:
|
||||||
@@ -114,9 +109,6 @@ spec:
|
|||||||
periodSeconds: 15
|
periodSeconds: 15
|
||||||
failureThreshold: 5
|
failureThreshold: 5
|
||||||
volumes:
|
volumes:
|
||||||
- name: training-state
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: training-state-pvc
|
|
||||||
- name: tmp-storage
|
- name: tmp-storage
|
||||||
emptyDir:
|
emptyDir:
|
||||||
sizeLimit: 2Gi
|
sizeLimit: 2Gi
|
||||||
@@ -140,20 +132,3 @@ spec:
|
|||||||
selector:
|
selector:
|
||||||
app.kubernetes.io/name: training-service
|
app.kubernetes.io/name: training-service
|
||||||
app.kubernetes.io/component: microservice
|
app.kubernetes.io/component: microservice
|
||||||
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: training-state-pvc
|
|
||||||
namespace: bakery-ia
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: training-service
|
|
||||||
app.kubernetes.io/component: storage
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 5Gi
|
|
||||||
storageClassName: standard
|
|
||||||
|
|||||||
@@ -311,85 +311,6 @@ patches:
|
|||||||
kind: Deployment
|
kind: Deployment
|
||||||
name: external-service
|
name: external-service
|
||||||
patch: |-
|
patch: |-
|
||||||
- op: add
|
|
||||||
path: /spec/template/spec/initContainers
|
|
||||||
value:
|
|
||||||
- name: wait-for-external-db
|
|
||||||
image: postgres:13-alpine
|
|
||||||
command:
|
|
||||||
- sh
|
|
||||||
- -c
|
|
||||||
- |
|
|
||||||
until pg_isready -h $EXTERNAL_DB_HOST -p $EXTERNAL_DB_PORT -U $EXTERNAL_DB_USER; do
|
|
||||||
echo "Waiting for external database..."
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
echo "External database is ready!"
|
|
||||||
env:
|
|
||||||
- name: EXTERNAL_DB_HOST
|
|
||||||
valueFrom:
|
|
||||||
configMapKeyRef:
|
|
||||||
name: bakery-config
|
|
||||||
key: EXTERNAL_DB_HOST
|
|
||||||
- name: EXTERNAL_DB_PORT
|
|
||||||
valueFrom:
|
|
||||||
configMapKeyRef:
|
|
||||||
name: bakery-config
|
|
||||||
key: DB_PORT
|
|
||||||
- name: EXTERNAL_DB_USER
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: database-secrets
|
|
||||||
key: EXTERNAL_DB_USER
|
|
||||||
- name: PGPASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: database-secrets
|
|
||||||
key: EXTERNAL_DB_PASSWORD
|
|
||||||
- name: wait-for-rabbitmq
|
|
||||||
image: busybox:1.35
|
|
||||||
command:
|
|
||||||
- sh
|
|
||||||
- -c
|
|
||||||
- |
|
|
||||||
until nc -z $RABBITMQ_HOST $RABBITMQ_PORT; do
|
|
||||||
echo "Waiting for RabbitMQ..."
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
echo "RabbitMQ is ready!"
|
|
||||||
env:
|
|
||||||
- name: RABBITMQ_HOST
|
|
||||||
valueFrom:
|
|
||||||
configMapKeyRef:
|
|
||||||
name: bakery-config
|
|
||||||
key: RABBITMQ_HOST
|
|
||||||
- name: RABBITMQ_PORT
|
|
||||||
valueFrom:
|
|
||||||
configMapKeyRef:
|
|
||||||
name: bakery-config
|
|
||||||
key: RABBITMQ_PORT
|
|
||||||
- name: wait-for-redis
|
|
||||||
image: redis:7-alpine
|
|
||||||
command:
|
|
||||||
- sh
|
|
||||||
- -c
|
|
||||||
- |
|
|
||||||
until redis-cli -h $REDIS_HOST -p $REDIS_PORT ping; do
|
|
||||||
echo "Waiting for Redis..."
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
echo "Redis is ready!"
|
|
||||||
env:
|
|
||||||
- name: REDIS_HOST
|
|
||||||
valueFrom:
|
|
||||||
configMapKeyRef:
|
|
||||||
name: bakery-config
|
|
||||||
key: REDIS_HOST
|
|
||||||
- name: REDIS_PORT
|
|
||||||
valueFrom:
|
|
||||||
configMapKeyRef:
|
|
||||||
name: bakery-config
|
|
||||||
key: REDIS_PORT
|
|
||||||
- op: replace
|
- op: replace
|
||||||
path: /spec/template/spec/containers/0/resources
|
path: /spec/template/spec/containers/0/resources
|
||||||
value:
|
value:
|
||||||
|
|||||||
@@ -85,34 +85,34 @@ class NotificationService(StandardFastAPIService):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Define custom health checks for notification service components
|
# Define custom health checks for notification service components
|
||||||
async def check_email_service():
|
#async def check_email_service():
|
||||||
"""Check email service health - service is ready even if credentials are invalid"""
|
# """Check email service health - service is ready even if credentials are invalid"""
|
||||||
try:
|
# try:
|
||||||
if not self.email_service:
|
# if not self.email_service:
|
||||||
return False
|
# return False
|
||||||
# Service is considered healthy if it's initialized, even if credentials fail
|
# # Service is considered healthy if it's initialized, even if credentials fail
|
||||||
# This allows the pod to be ready while external services may have config issues
|
# # This allows the pod to be ready while external services may have config issues
|
||||||
await self.email_service.health_check()
|
# await self.email_service.health_check()
|
||||||
return True
|
# return True
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
# Log but don't fail readiness - email service config issues shouldn't block the pod
|
# Log but don't fail readiness - email service config issues shouldn't block the pod
|
||||||
self.logger.error("Email service health check failed", error=str(e))
|
# self.logger.error("Email service health check failed", error=str(e))
|
||||||
# Return True to indicate service is ready (initialized) even if credentials are wrong
|
# Return True to indicate service is ready (initialized) even if credentials are wrong
|
||||||
return True
|
# return True
|
||||||
|
|
||||||
async def check_whatsapp_service():
|
#async def check_whatsapp_service():
|
||||||
"""Check WhatsApp service health - service is ready even if credentials are invalid"""
|
# """Check WhatsApp service health - service is ready even if credentials are invalid"""
|
||||||
try:
|
# try:
|
||||||
if not self.whatsapp_service:
|
# if not self.whatsapp_service:
|
||||||
return False
|
# return False
|
||||||
# Service is considered healthy if it's initialized, even if credentials fail
|
# Service is considered healthy if it's initialized, even if credentials fail
|
||||||
await self.whatsapp_service.health_check()
|
# await self.whatsapp_service.health_check()
|
||||||
return True
|
# return True
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
# Log but don't fail readiness - WhatsApp config issues shouldn't block the pod
|
# Log but don't fail readiness - WhatsApp config issues shouldn't block the pod
|
||||||
self.logger.error("WhatsApp service health check failed", error=str(e))
|
# self.logger.error("WhatsApp service health check failed", error=str(e))
|
||||||
# Return True to indicate service is ready (initialized) even if credentials are wrong
|
# Return True to indicate service is ready (initialized) even if credentials are wrong
|
||||||
return True
|
# return True
|
||||||
|
|
||||||
async def check_sse_service():
|
async def check_sse_service():
|
||||||
"""Check SSE service health"""
|
"""Check SSE service health"""
|
||||||
@@ -125,14 +125,14 @@ class NotificationService(StandardFastAPIService):
|
|||||||
self.logger.error("SSE service health check failed", error=str(e))
|
self.logger.error("SSE service health check failed", error=str(e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def check_messaging():
|
#async def check_messaging():
|
||||||
"""Check messaging service health"""
|
# """Check messaging service health"""
|
||||||
try:
|
# try:
|
||||||
from app.services.messaging import notification_publisher
|
# from app.services.messaging import notification_publisher
|
||||||
return bool(notification_publisher and notification_publisher.connected)
|
# return bool(notification_publisher and notification_publisher.connected)
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
self.logger.error("Messaging health check failed", error=str(e))
|
# self.logger.error("Messaging health check failed", error=str(e))
|
||||||
return False
|
# return False
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
service_name="notification-service",
|
service_name="notification-service",
|
||||||
@@ -145,10 +145,10 @@ class NotificationService(StandardFastAPIService):
|
|||||||
database_manager=database_manager,
|
database_manager=database_manager,
|
||||||
expected_tables=notification_expected_tables,
|
expected_tables=notification_expected_tables,
|
||||||
custom_health_checks={
|
custom_health_checks={
|
||||||
"email_service": check_email_service,
|
# "email_service": check_email_service,
|
||||||
"whatsapp_service": check_whatsapp_service,
|
# "whatsapp_service": check_whatsapp_service,
|
||||||
"sse_service": check_sse_service,
|
"sse_service": check_sse_service,
|
||||||
"messaging": check_messaging
|
# "messaging": check_messaging
|
||||||
},
|
},
|
||||||
enable_messaging=True,
|
enable_messaging=True,
|
||||||
custom_metrics=notification_custom_metrics
|
custom_metrics=notification_custom_metrics
|
||||||
|
|||||||
Reference in New Issue
Block a user