"use client"; import { useState, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { PlanCard } from "@/components/memberships/plan-card"; import { PlanForm } from "@/components/memberships/plan-form"; import { MembershipTable } from "@/components/memberships/membership-table"; import { AssignMembershipDialog } from "@/components/memberships/assign-membership-dialog"; import { cn } from "@/lib/utils"; interface MembershipPlan { id: string; name: string; description: string | null; price: number | string; durationMonths: number; courtHours: number | null; discountPercent: number | string | null; benefits: string[] | null; isActive: boolean; subscriberCount: number; benefitsSummary?: { freeHours: number; bookingDiscount: number; extraBenefits: string[]; }; } interface Membership { id: string; startDate: string; endDate: string; status: "ACTIVE" | "EXPIRED" | "CANCELLED" | "SUSPENDED"; remainingHours: number | null; autoRenew: boolean; isExpiring?: boolean; daysUntilExpiry?: number | null; plan: { id: string; name: string; price: number | string; durationMonths: number; courtHours: number | null; }; client: { id: string; firstName: string; lastName: string; email: string | null; phone: string | null; }; benefitsSummary?: { freeHours: number; hoursRemaining: number; }; } interface MembershipsResponse { data: Membership[]; pagination: { total: number; limit: number; offset: number; hasMore: boolean; }; } const statusFilters = [ { value: "", label: "Todos" }, { value: "ACTIVE", label: "Activas" }, { value: "EXPIRED", label: "Expiradas" }, { value: "CANCELLED", label: "Canceladas" }, ]; export default function MembershipsPage() { // Plans state const [plans, setPlans] = useState([]); const [loadingPlans, setLoadingPlans] = useState(true); const [showPlanForm, setShowPlanForm] = useState(false); const [editingPlan, setEditingPlan] = useState(null); const [planFormLoading, setPlanFormLoading] = useState(false); // Memberships state const [memberships, setMemberships] = useState([]); const [loadingMemberships, setLoadingMemberships] = useState(true); const [statusFilter, setStatusFilter] = useState(""); const [planFilter, setPlanFilter] = useState(""); const [searchQuery, setSearchQuery] = useState(""); const [showAssignDialog, setShowAssignDialog] = useState(false); const [assignLoading, setAssignLoading] = useState(false); // Stats const [stats, setStats] = useState({ totalActive: 0, expiringSoon: 0, }); const [error, setError] = useState(null); // Fetch plans const fetchPlans = useCallback(async () => { setLoadingPlans(true); try { const response = await fetch("/api/membership-plans?includeInactive=true"); if (!response.ok) throw new Error("Error al cargar planes"); const data = await response.json(); setPlans(data); } catch (err) { console.error("Error fetching plans:", err); setError(err instanceof Error ? err.message : "Error desconocido"); } finally { setLoadingPlans(false); } }, []); // Fetch memberships const fetchMemberships = useCallback(async () => { setLoadingMemberships(true); try { const params = new URLSearchParams(); if (statusFilter) params.append("status", statusFilter); if (planFilter) params.append("planId", planFilter); if (searchQuery) params.append("search", searchQuery); const response = await fetch(`/api/memberships?${params.toString()}`); if (!response.ok) throw new Error("Error al cargar membresias"); const data: MembershipsResponse = await response.json(); setMemberships(data.data); // Calculate stats const active = data.data.filter(m => m.status === "ACTIVE"); const expiring = data.data.filter(m => m.isExpiring); setStats({ totalActive: active.length, expiringSoon: expiring.length, }); } catch (err) { console.error("Error fetching memberships:", err); setError(err instanceof Error ? err.message : "Error desconocido"); } finally { setLoadingMemberships(false); } }, [statusFilter, planFilter, searchQuery]); useEffect(() => { fetchPlans(); }, [fetchPlans]); useEffect(() => { fetchMemberships(); }, [fetchMemberships]); // Debounce search const [debouncedSearch, setDebouncedSearch] = useState(searchQuery); useEffect(() => { const timer = setTimeout(() => { setDebouncedSearch(searchQuery); }, 300); return () => clearTimeout(timer); }, [searchQuery]); useEffect(() => { if (debouncedSearch !== undefined) { fetchMemberships(); } }, [debouncedSearch]); // Handle plan creation/update const handlePlanSubmit = async (data: { name: string; description: string; price: number; durationMonths: number; freeHours: number; bookingDiscount: number; storeDiscount: number; extraBenefits: string; }) => { setPlanFormLoading(true); try { const extraBenefitsArray = data.extraBenefits .split("\n") .map(b => b.trim()) .filter(b => b.length > 0); const payload = { name: data.name, description: data.description || undefined, price: data.price, durationMonths: data.durationMonths, freeHours: data.freeHours || undefined, bookingDiscount: data.bookingDiscount || undefined, storeDiscount: data.storeDiscount || undefined, extraBenefits: extraBenefitsArray.length > 0 ? extraBenefitsArray : undefined, }; const url = editingPlan ? `/api/membership-plans/${editingPlan.id}` : "/api/membership-plans"; const method = editingPlan ? "PUT" : "POST"; const response = await fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || "Error al guardar plan"); } setShowPlanForm(false); setEditingPlan(null); await fetchPlans(); } catch (err) { throw err; } finally { setPlanFormLoading(false); } }; // Handle plan deletion const handleDeletePlan = async (plan: MembershipPlan) => { if (!confirm(`¿Estas seguro de eliminar el plan "${plan.name}"? Esta accion lo desactivara.`)) { return; } try { const response = await fetch(`/api/membership-plans/${plan.id}`, { method: "DELETE", }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || "Error al eliminar plan"); } await fetchPlans(); } catch (err) { console.error("Error deleting plan:", err); setError(err instanceof Error ? err.message : "Error desconocido"); } }; // Handle membership assignment const handleAssignMembership = async (data: { clientId: string; planId: string; startDate: string; endDate: string; }) => { setAssignLoading(true); try { const response = await fetch("/api/memberships", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || "Error al asignar membresia"); } setShowAssignDialog(false); await Promise.all([fetchMemberships(), fetchPlans()]); } catch (err) { throw err; } finally { setAssignLoading(false); } }; // Handle membership renewal const handleRenewMembership = async (membership: Membership) => { // For renewal, we create a new membership with the same plan const startDate = new Date(); const endDate = new Date(); endDate.setMonth(endDate.getMonth() + membership.plan.durationMonths); try { const response = await fetch("/api/memberships", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId: membership.client.id, planId: membership.plan.id, startDate: startDate.toISOString(), endDate: endDate.toISOString(), }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || "Error al renovar membresia"); } await Promise.all([fetchMemberships(), fetchPlans()]); } catch (err) { console.error("Error renewing membership:", err); setError(err instanceof Error ? err.message : "Error desconocido"); } }; // Handle membership cancellation const handleCancelMembership = async (membership: Membership) => { if (!confirm(`¿Estas seguro de cancelar la membresia de ${membership.client.firstName} ${membership.client.lastName}?`)) { return; } try { const response = await fetch(`/api/memberships/${membership.id}`, { method: "DELETE", }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || "Error al cancelar membresia"); } await Promise.all([fetchMemberships(), fetchPlans()]); } catch (err) { console.error("Error cancelling membership:", err); setError(err instanceof Error ? err.message : "Error desconocido"); } }; // Filter active plans for assignment dialog const activePlans = plans.filter(p => p.isActive); return (
{/* Header */}

Membresias

Gestiona planes y membresias de tus clientes

{/* Error Message */} {error && (

{error}

)} {/* Stats Cards */}

Membresias Activas

{stats.totalActive}

0 && "border-yellow-300 bg-yellow-50")}>
0 ? "bg-yellow-100" : "bg-primary-100" )}> 0 ? "text-yellow-600" : "text-primary-600" )} fill="none" stroke="currentColor" viewBox="0 0 24 24">

Por Expirar

{stats.expiringSoon}

Planes Activos

{activePlans.length}

Total Suscriptores

{plans.reduce((sum, p) => sum + p.subscriberCount, 0)}

{/* Plans Section */}

Planes de Membresia

{loadingPlans ? (

Cargando planes...

) : plans.length === 0 ? (

No hay planes

Crea tu primer plan de membresia

) : (
{plans.map((plan) => ( { setEditingPlan(p); setShowPlanForm(true); }} onDelete={handleDeletePlan} /> ))}
)}
{/* Memberships Section */}

Membresias

{/* Filters */}
{/* Search */}
setSearchQuery(e.target.value)} className="w-full" />
{/* Plan Filter */}
{/* Status Filter */}
{statusFilters.map((filter) => ( ))}
{/* Memberships Table */}
{/* Plan Form Modal */} {showPlanForm && (
{ setShowPlanForm(false); setEditingPlan(null); }} isLoading={planFormLoading} mode={editingPlan ? "edit" : "create"} />
)} {/* Assign Membership Dialog */} {showAssignDialog && ( setShowAssignDialog(false)} onAssign={handleAssignMembership} isLoading={assignLoading} /> )}
); }