/** * PlanDetailPanel Component * * Detail view for a selected plan or add-on, shown in the main panel * of the master-detail layout. */ import React, { useState } from 'react'; import { Package, Pencil, Copy, Trash2, DollarSign, Users, Check, AlertTriangle, ExternalLink, ChevronDown, ChevronRight, Plus, Archive, } from 'lucide-react'; import { Badge, Alert, Modal, ModalFooter } from '../../components/ui'; import { usePlanVersionSubscribers, useDeletePlan, useDeletePlanVersion, useMarkVersionLegacy, useForceUpdatePlanVersion, formatCentsToDollars, type PlanWithVersions, type PlanVersion, type AddOnProduct, } from '../../hooks/useBillingAdmin'; import { useCurrentUser } from '../../hooks/useAuth'; // ============================================================================= // Types // ============================================================================= interface PlanDetailPanelProps { plan: PlanWithVersions | null; addon: AddOnProduct | null; onEdit: () => void; onDuplicate: () => void; onCreateVersion: () => void; onEditVersion: (version: PlanVersion) => void; } // ============================================================================= // Component // ============================================================================= export const PlanDetailPanel: React.FC = ({ plan, addon, onEdit, onDuplicate, onCreateVersion, onEditVersion, }) => { const [expandedSections, setExpandedSections] = useState>( new Set(['overview', 'pricing', 'features']) ); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [deleteConfirmText, setDeleteConfirmText] = useState(''); const [showForcePushModal, setShowForcePushModal] = useState(false); const [forcePushConfirmText, setForcePushConfirmText] = useState(''); const [forcePushError, setForcePushError] = useState(null); const [forcePushSuccess, setForcePushSuccess] = useState(null); const { data: currentUser } = useCurrentUser(); const isSuperuser = currentUser?.is_superuser ?? false; const deletePlanMutation = useDeletePlan(); const deleteVersionMutation = useDeletePlanVersion(); const markLegacyMutation = useMarkVersionLegacy(); const forceUpdateMutation = useForceUpdatePlanVersion(); if (!plan && !addon) { return (

Select a plan or add-on from the catalog

); } const toggleSection = (section: string) => { setExpandedSections((prev) => { const next = new Set(prev); if (next.has(section)) { next.delete(section); } else { next.add(section); } return next; }); }; const activeVersion = plan?.active_version; const handleDelete = async () => { if (!plan) return; const expectedText = `DELETE ${plan.code}`; if (deleteConfirmText !== expectedText) { return; } try { await deletePlanMutation.mutateAsync(plan.id); setShowDeleteConfirm(false); setDeleteConfirmText(''); } catch (error) { console.error('Failed to delete plan:', error); } }; const handleForcePush = async () => { if (!plan || !activeVersion) return; const expectedText = `FORCE PUSH ${plan.code}`; if (forcePushConfirmText !== expectedText) { setForcePushError('Please type the confirmation text exactly.'); return; } setForcePushError(null); try { const result = await forceUpdateMutation.mutateAsync({ id: activeVersion.id, confirm: true, // Pass current version data to ensure it's updated in place name: activeVersion.name, }); if ('version' in result) { setForcePushSuccess( `Successfully pushed changes to ${result.affected_count} subscriber(s).` ); setTimeout(() => { setShowForcePushModal(false); setForcePushConfirmText(''); setForcePushSuccess(null); }, 2000); } } catch (error: any) { const errorMessage = error.response?.data?.detail || error.message || 'Failed to force push'; setForcePushError(errorMessage); } }; // Render Plan Detail if (plan) { return (
{/* Header */}

{plan.name}

{plan.code} {!plan.is_active && Inactive}
{plan.description && (

{plan.description}

)} {activeVersion && (
{activeVersion.price_monthly_cents === 0 ? 'Free' : `$${formatCentsToDollars(activeVersion.price_monthly_cents)}/mo`} {plan.total_subscribers} subscriber{plan.total_subscribers !== 1 ? 's' : ''} Version {activeVersion.version}
)}
{/* Overview Section */} toggleSection('overview')} >

{plan.code}

{plan.display_order}

{plan.is_active ? 'Active' : 'Inactive'}

{plan.total_subscribers}

{/* Pricing Section */} {activeVersion && ( toggleSection('pricing')} >

${formatCentsToDollars(activeVersion.price_monthly_cents)}

${formatCentsToDollars(activeVersion.price_yearly_cents)}

{activeVersion.price_yearly_cents > 0 && (

${(activeVersion.price_yearly_cents / 12 / 100).toFixed(2)}/mo

)}

{activeVersion.trial_days} days

)} {/* Transaction Fees Section */} {activeVersion && ( toggleSection('fees')} >

{activeVersion.transaction_fee_percent}%

${(activeVersion.transaction_fee_fixed_cents / 100).toFixed(2)}

)} {/* Features Section */} {activeVersion && activeVersion.features.length > 0 && ( toggleSection('features')} >
{activeVersion.features.map((f) => (
{f.feature.name} {f.int_value !== null && ( {f.int_value === 0 ? 'Unlimited' : f.int_value} )}
))}
)} {/* Stripe Section */} {activeVersion && (activeVersion.stripe_product_id || activeVersion.stripe_price_id_monthly || activeVersion.stripe_price_id_yearly) && ( toggleSection('stripe')} >
{activeVersion.stripe_product_id && (
Product: {activeVersion.stripe_product_id}
)} {activeVersion.stripe_price_id_monthly && (
Monthly Price: {activeVersion.stripe_price_id_monthly}
)} {activeVersion.stripe_price_id_yearly && (
Yearly Price: {activeVersion.stripe_price_id_yearly}
)} {!activeVersion.stripe_product_id && ( )}
)} {/* Versions Section */} toggleSection('versions')} >
{plan.versions.map((version) => ( onEditVersion(version)} onMarkLegacy={() => markLegacyMutation.mutate(version.id)} onDelete={() => deleteVersionMutation.mutate(version.id)} /> ))}
{/* Danger Zone */} toggleSection('danger')} variant="danger" >
{/* Force Push to Subscribers - Superuser Only */} {isSuperuser && activeVersion && plan.total_subscribers > 0 && (

Force Push Changes to All Subscribers

This will modify the current plan version in place, immediately affecting all {plan.total_subscribers} active subscriber(s). This bypasses grandfathering and cannot be undone. Changes to pricing, features, and limits will take effect immediately.

)} {/* Delete Plan */}

Deleting a plan is permanent and cannot be undone. Plans with active subscribers cannot be deleted.

{plan.total_subscribers > 0 ? ( ) : ( )}
{/* Delete Confirmation Modal */} {showDeleteConfirm && ( { setShowDeleteConfirm(false); setDeleteConfirmText(''); }} title="Delete Plan" size="sm" >

To confirm, type DELETE {plan.code} below:

setDeleteConfirmText(e.target.value)} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white" placeholder={`DELETE ${plan.code}`} />
{ setShowDeleteConfirm(false); setDeleteConfirmText(''); }} submitText="Delete Plan" submitVariant="danger" isDisabled={deleteConfirmText !== `DELETE ${plan.code}`} isLoading={deletePlanMutation.isPending} onSubmit={handleDelete} />
)} {/* Force Push Confirmation Modal */} {showForcePushModal && activeVersion && ( { setShowForcePushModal(false); setForcePushConfirmText(''); setForcePushError(null); setForcePushSuccess(null); }} title="Force Push to All Subscribers" size="md" >
{forcePushSuccess ? ( ) : ( <> DANGER: This action affects paying customers!
  • All {plan.total_subscribers} subscriber(s) will be affected immediately
  • Changes to pricing will apply to future billing cycles
  • Feature and limit changes take effect immediately
  • This bypasses grandfathering protection
  • This action cannot be undone
} />

Current version: v{activeVersion.version} - {activeVersion.name}

Price: ${formatCentsToDollars(activeVersion.price_monthly_cents)}/mo

{forcePushError && ( )}

To confirm this dangerous action, type FORCE PUSH {plan.code} below:

setForcePushConfirmText(e.target.value)} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white" placeholder={`FORCE PUSH ${plan.code}`} /> )}
{!forcePushSuccess && ( { setShowForcePushModal(false); setForcePushConfirmText(''); setForcePushError(null); }} submitText="Force Push Changes" submitVariant="danger" isDisabled={forcePushConfirmText !== `FORCE PUSH ${plan.code}`} isLoading={forceUpdateMutation.isPending} onSubmit={handleForcePush} /> )} )} ); } // Render Add-on Detail if (addon) { return (

{addon.name}

{addon.code} {!addon.is_active && Inactive}
{addon.description && (

{addon.description}

)}

${formatCentsToDollars(addon.price_monthly_cents)}

${formatCentsToDollars(addon.price_one_time_cents)}

); } return null; }; // ============================================================================= // Collapsible Section // ============================================================================= interface CollapsibleSectionProps { title: string; isExpanded: boolean; onToggle: () => void; children: React.ReactNode; variant?: 'default' | 'danger'; } const CollapsibleSection: React.FC = ({ title, isExpanded, onToggle, children, variant = 'default', }) => { return (
{isExpanded && (
{children}
)}
); }; // ============================================================================= // Version Row // ============================================================================= interface VersionRowProps { version: PlanVersion; isActive: boolean; onEdit: () => void; onMarkLegacy: () => void; onDelete: () => void; } const VersionRow: React.FC = ({ version, isActive, onEdit, onMarkLegacy, onDelete, }) => { return (
v{version.version} {version.name} {version.is_legacy && ( Legacy )} {!version.is_public && !version.is_legacy && ( Hidden )} {version.subscriber_count} subscriber{version.subscriber_count !== 1 ? 's' : ''}
{!version.is_legacy && version.subscriber_count === 0 && ( )} {version.subscriber_count === 0 && ( )}
); };