You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

199 lines
6.7 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { listPlans, deletePlan, getEntitlements } from '@/src/lib/api/facility-admin';
import type { MembershipPlan, PlanTemplate, PlanEntitlements } from '@/src/types/facility-admin';
import PlanCard from '@/src/components/plans/PlanCard';
import PlanFormModal from '@/src/components/plans/PlanFormModal';
import PlanListSkeleton from '@/src/components/plans/PlanListSkeleton';
import TemplatePicker from '@/src/components/plans/TemplatePicker';
import EntitlementsConfigModal from '@/src/components/plans/EntitlementsConfigModal';
interface MembershipPlansComponentProps {
facilityId: number;
}
export default function MembershipPlansComponent({ facilityId }: MembershipPlansComponentProps) {
const [plans, setPlans] = useState<MembershipPlan[]>([]);
const [entitlementsMap, setEntitlementsMap] = useState<Record<number, PlanEntitlements>>({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [templatePickerOpen, setTemplatePickerOpen] = useState(false);
const [createModalOpen, setCreateModalOpen] = useState(false);
const [selectedTemplate, setSelectedTemplate] = useState<PlanTemplate | null>(null);
const [editingPlan, setEditingPlan] = useState<MembershipPlan | null>(null);
const [configuringPlan, setConfiguringPlan] = useState<MembershipPlan | null>(null);
useEffect(() => {
fetchPlans();
}, [facilityId]);
async function fetchPlans() {
setLoading(true);
setError(null);
const result = await listPlans(facilityId, { include_inactive: false });
if (result.success) {
setPlans(result.data);
// Fetch entitlements for all plans in parallel
await fetchAllEntitlements(result.data);
} else {
setError(result.error.detail || 'Failed to load plans');
}
setLoading(false);
}
async function fetchAllEntitlements(plansList: MembershipPlan[]) {
const entitlementPromises = plansList.map(async (plan) => {
const result = await getEntitlements(facilityId, plan.facility_membership_plan_id);
if (result.success) {
return { planId: plan.facility_membership_plan_id, entitlements: result.data };
}
return null;
});
const results = await Promise.all(entitlementPromises);
const newEntitlementsMap: Record<number, PlanEntitlements> = {};
for (const result of results) {
if (result) {
newEntitlementsMap[result.planId] = result.entitlements;
}
}
setEntitlementsMap(newEntitlementsMap);
}
async function handleDelete(planId: number, planName: string) {
if (!confirm(`Are you sure you want to deactivate "${planName}"?`)) return;
const result = await deletePlan(facilityId, planId);
if (result.success) {
fetchPlans();
} else {
alert(result.error.detail || 'Failed to delete plan');
}
}
function handleCreateClick() {
// Open template picker first
setTemplatePickerOpen(true);
}
function handleTemplateSelect(template: PlanTemplate | null) {
setSelectedTemplate(template);
setTemplatePickerOpen(false);
setCreateModalOpen(true);
}
function handleCreateModalClose() {
setCreateModalOpen(false);
setSelectedTemplate(null);
}
return (
<div>
{/* Header */}
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-2xl font-bold text-slate-900">Membership Plans</h1>
<p className="text-slate-600 mt-1">Create and manage subscription plans for your facility</p>
</div>
<button
onClick={handleCreateClick}
className="bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white px-6 py-3 rounded-lg font-semibold shadow-md transition-all duration-200"
>
+ Create Plan
</button>
</div>
{/* Error Message */}
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6">
{error}
</div>
)}
{/* Plans Grid */}
{loading ? (
<PlanListSkeleton count={3} />
) : plans.length === 0 ? (
<div className="text-center py-16">
<div className="text-6xl mb-4">📋</div>
<h2 className="text-2xl font-semibold text-slate-900 mb-2">No membership plans yet</h2>
<p className="text-slate-600 mb-6">Create your first plan to get started</p>
<button
onClick={handleCreateClick}
className="bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white px-6 py-3 rounded-lg font-semibold shadow-md transition-all duration-200"
>
+ Create Your First Plan
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{plans.map(plan => (
<PlanCard
key={plan.facility_membership_plan_id}
plan={plan}
entitlements={entitlementsMap[plan.facility_membership_plan_id]}
onEdit={() => setEditingPlan(plan)}
onDelete={() => handleDelete(plan.facility_membership_plan_id, plan.name)}
onConfigureEntitlements={() => setConfiguringPlan(plan)}
/>
))}
</div>
)}
{/* Template Picker Modal */}
<TemplatePicker
isOpen={templatePickerOpen}
onClose={() => setTemplatePickerOpen(false)}
onSelect={handleTemplateSelect}
/>
{/* Create Modal */}
{createModalOpen && (
<PlanFormModal
isOpen={createModalOpen}
onClose={handleCreateModalClose}
facilityId={facilityId}
initialValues={selectedTemplate ? {
name: selectedTemplate.name,
billing_period: selectedTemplate.billing_period,
price_cents: selectedTemplate.suggested_price_cents,
sport_id: selectedTemplate.sport_id,
is_active: true
} : undefined}
templateName={selectedTemplate?.name}
onSuccess={fetchPlans}
/>
)}
{/* Edit Modal */}
{editingPlan && (
<PlanFormModal
isOpen={!!editingPlan}
onClose={() => setEditingPlan(null)}
facilityId={facilityId}
plan={editingPlan}
onSuccess={fetchPlans}
/>
)}
{/* Entitlements Config Modal */}
{configuringPlan && (
<EntitlementsConfigModal
isOpen={!!configuringPlan}
onClose={() => setConfiguringPlan(null)}
facilityId={facilityId}
plan={configuringPlan}
onSuccess={fetchPlans}
/>
)}
</div>
);
}