Add subcription feature 9

This commit is contained in:
Urtzi Alfaro
2026-01-16 20:25:45 +01:00
parent fa7b62bd6c
commit 3a7d57ef90
19 changed files with 1833 additions and 985 deletions

View File

@@ -64,6 +64,7 @@ interface CommunicationPreferencesProps {
onSave: (preferences: NotificationPreferences) => Promise<void>;
onReset: () => void;
hasChanges: boolean;
onPreferencesChange?: (preferences: NotificationPreferences) => void;
}
const CommunicationPreferences: React.FC<CommunicationPreferencesProps> = ({
@@ -144,10 +145,16 @@ const CommunicationPreferences: React.FC<CommunicationPreferencesProps> = ({
];
const handlePreferenceChange = (key: keyof NotificationPreferences, value: any) => {
setPreferences(prev => ({
...prev,
const newPreferences = {
...preferences,
[key]: value
}));
};
setPreferences(newPreferences);
// Notify parent component about changes
if (onPreferencesChange) {
onPreferencesChange(newPreferences);
}
};
const handleContactChange = (field: 'email' | 'phone', value: string) => {

View File

@@ -335,6 +335,9 @@ const SubscriptionPageRedesign: React.FC = () => {
exp_year?: number;
} | null>(null);
// Billing cycle state (monthly/yearly)
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly');
// Section visibility states
const [showUsage, setShowUsage] = useState(false);
const [showBilling, setShowBilling] = useState(false);
@@ -439,6 +442,10 @@ const SubscriptionPageRedesign: React.FC = () => {
setUsageSummary(usage);
setAvailablePlans(plans);
// Initialize billing cycle from current subscription
if (usage.billing_cycle) {
setBillingCycle(usage.billing_cycle);
}
} catch (error) {
console.error('Error loading subscription data:', error);
showToast.error(
@@ -470,6 +477,7 @@ const SubscriptionPageRedesign: React.FC = () => {
try {
setUpgrading(true);
// Step 1: Validate the upgrade
const validation = await subscriptionService.validatePlanUpgrade(
tenantId,
selectedPlan
@@ -480,21 +488,46 @@ const SubscriptionPageRedesign: React.FC = () => {
return;
}
const result = await subscriptionService.upgradePlan(tenantId, selectedPlan);
// Step 2: Execute the upgrade (uses current billing cycle)
const result = await subscriptionService.upgradePlan(
tenantId,
selectedPlan,
billingCycle // Pass the currently selected billing cycle
);
if (result.success) {
showToast.success(result.message);
// Show appropriate success message based on trial status
if (result.is_trialing && result.trial_preserved) {
showToast.success(
`¡Plan actualizado a ${result.new_plan}! Tu período de prueba continúa hasta ${
result.trial_ends_at ? new Date(result.trial_ends_at).toLocaleDateString('es-ES') : 'su fecha original'
}. Al finalizar, se aplicará el precio del nuevo plan.`
);
} else {
showToast.success(result.message || `¡Plan actualizado exitosamente a ${result.new_plan}!`);
}
// Step 3: Invalidate caches to refresh UI
subscriptionService.invalidateCache();
// Step 4: Refresh subscription data from server
if (result.requires_token_refresh) {
// Force a fresh fetch of subscription data
await subscriptionService.getUsageSummary(tenantId).catch(() => {});
}
// Step 5: Invalidate React Query caches
await queryClient.invalidateQueries({ queryKey: ['subscription-usage'] });
await queryClient.invalidateQueries({ queryKey: ['tenant'] });
await queryClient.invalidateQueries({ queryKey: ['subscription'] });
// Step 6: Notify other components about the change
notifySubscriptionChanged();
// Step 7: Reload subscription data for this page
await loadSubscriptionData();
// Step 8: Close dialog and reset state
setUpgradeDialogOpen(false);
setSelectedPlan('');
} else {
@@ -502,7 +535,8 @@ const SubscriptionPageRedesign: React.FC = () => {
}
} catch (error) {
console.error('Error upgrading plan:', error);
showToast.error('Error al procesar el cambio de plan');
const errorMessage = error instanceof Error ? error.message : 'Error al procesar el cambio de plan';
showToast.error(errorMessage);
} finally {
setUpgrading(false);
}
@@ -647,9 +681,9 @@ const SubscriptionPageRedesign: React.FC = () => {
// Determine if this is a pilot subscription based on characteristics
// Pilot subscriptions have extended trial periods (typically 90 days from PILOT2025 coupon)
// compared to regular trials (typically 14 days), so we check for longer trial periods
const isPilotSubscription = usageSummary.status === 'trialing' &&
const isPilotSubscription = (usageSummary.status === 'trialing' ||
(usageSummary.trial_ends_at && new Date(usageSummary.trial_ends_at) > new Date())) &&
usageSummary.trial_ends_at &&
new Date(usageSummary.trial_ends_at) > new Date() &&
// Check if trial period is longer than typical trial (e.g., > 60 days indicates pilot)
(new Date(usageSummary.trial_ends_at).getTime() - new Date().getTime()) > (60 * 24 * 60 * 60 * 1000); // 60+ days
@@ -726,6 +760,10 @@ const SubscriptionPageRedesign: React.FC = () => {
onToggle={() => setShowUsage(!showUsage)}
showAlert={hasHighUsageMetrics}
>
{/* Spacing between content blocks */}
<div className="h-6"></div>
<div className="space-y-6">
{/* Team & Organization */}
<div>
@@ -886,6 +924,10 @@ const SubscriptionPageRedesign: React.FC = () => {
isOpen={showBilling}
onToggle={() => setShowBilling(!showBilling)}
>
{/* Spacing between content blocks */}
<div className="h-6"></div>
<div className="space-y-6">
{/* Payment Method */}
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-secondary)]">
@@ -931,8 +973,21 @@ const SubscriptionPageRedesign: React.FC = () => {
<div className="p-4 bg-[var(--bg-secondary)] rounded-full">
<Download className="w-8 h-8 text-[var(--text-tertiary)]" />
</div>
<p className="text-[var(--text-secondary)]">No hay facturas disponibles</p>
<p className="text-sm text-[var(--text-tertiary)]">Las facturas aparecerán aquí una vez realizados los pagos</p>
{(usageSummary?.status === 'trialing' ||
(usageSummary?.trial_ends_at && new Date(usageSummary.trial_ends_at) > new Date())) ? (
<>
<p className="text-[var(--text-secondary)]">Periodo de prueba activo</p>
<p className="text-sm text-[var(--text-tertiary)]">
No hay facturas durante el periodo de prueba. Las facturas aparecerán aquí
una vez finalice el periodo de prueba el {usageSummary?.trial_ends_at ? new Date(usageSummary.trial_ends_at).toLocaleDateString('es-ES') : 'próximamente'}.
</p>
</>
) : (
<>
<p className="text-[var(--text-secondary)]">No hay facturas disponibles</p>
<p className="text-sm text-[var(--text-tertiary)]">Las facturas aparecerán aquí una vez realizados los pagos</p>
</>
)}
</div>
</div>
) : (
@@ -951,21 +1006,32 @@ const SubscriptionPageRedesign: React.FC = () => {
{Array.isArray(invoices) && invoices.slice(0, 5).map((invoice) => (
<tr key={invoice.id} className="border-b border-[var(--border-color)] hover:bg-[var(--bg-secondary)] transition-colors">
<td className="py-3 px-4 text-[var(--text-primary)] font-medium">
{new Date(invoice.date).toLocaleDateString('es-ES', {
{invoice.created ? new Date(invoice.created * 1000).toLocaleDateString('es-ES', {
day: '2-digit',
month: 'short',
year: 'numeric'
})}
}) : 'N/A'}
</td>
<td className="py-3 px-4 text-[var(--text-primary)]">
{invoice.description || 'Suscripción'}
{invoice.trial ? (
<span className="flex items-center gap-1">
<span className="text-green-500"></span>
Periodo de prueba
</span>
) : (invoice.description || 'Suscripción')}
</td>
<td className="py-3 px-4 text-[var(--text-primary)] font-semibold text-right">
{subscriptionService.formatPrice(invoice.amount)}
{invoice.amount_due !== undefined ? (
invoice.trial ? (
<span className="text-green-500">0,00 </span>
) : (
subscriptionService.formatPrice(invoice.amount_due)
)
) : 'N/A'}
</td>
<td className="py-3 px-4 text-center">
<Badge variant={invoice.status === 'paid' ? 'success' : invoice.status === 'open' ? 'warning' : 'default'}>
{invoice.status === 'paid' ? 'Pagada' : invoice.status === 'open' ? 'Pendiente' : invoice.status}
<Badge variant={invoice.trial ? 'success' : invoice.status === 'paid' ? 'success' : invoice.status === 'open' ? 'warning' : 'default'}>
{invoice.trial ? 'Prueba' : invoice.status === 'paid' ? 'Pagada' : invoice.status === 'open' ? 'Pendiente' : invoice.status}
</Badge>
</td>
<td className="py-3 px-4 text-center">
@@ -977,7 +1043,7 @@ const SubscriptionPageRedesign: React.FC = () => {
className="flex items-center gap-2 mx-auto"
>
<Download className="w-4 h-4" />
{invoice.invoice_pdf ? 'PDF' : 'Ver'}
{invoice.invoice_pdf ? 'PDF' : invoice.hosted_invoice_url ? 'Ver' : 'Descargar'}
</Button>
</td>
</tr>
@@ -995,23 +1061,7 @@ const SubscriptionPageRedesign: React.FC = () => {
)}
</div>
{/* Support Contact */}
<div className="p-4 bg-blue-500/5 border border-blue-500/20 rounded-lg">
<div className="flex items-start gap-3">
<Activity className="w-5 h-5 text-blue-500 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm text-[var(--text-primary)] font-medium mb-1">
¿Preguntas sobre tu factura?
</p>
<p className="text-sm text-[var(--text-secondary)]">
Contacta a nuestro equipo de soporte en{' '}
<a href="mailto:support@bakery-ia.com" className="text-blue-500 hover:text-white hover:bg-blue-500 px-2 py-0.5 rounded transition-all duration-200 no-underline">
support@bakery-ia.com
</a>
</p>
</div>
</div>
</div>
</div>
</CollapsibleSection>
@@ -1022,6 +1072,10 @@ const SubscriptionPageRedesign: React.FC = () => {
isOpen={showPlans}
onToggle={() => setShowPlans(!showPlans)}
>
{/* Spacing between content blocks */}
<div className="h-6"></div>
<div className="space-y-6">
{/* Available Plans */}
<div>