"use client"; import { useState, useEffect, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { ClientForm } from "@/components/clients/client-form"; import { ClientDetailDialog } from "@/components/clients/client-detail-dialog"; import { AssignMembershipDialog } from "@/components/memberships/assign-membership-dialog"; import { StatCardSkeleton } from "@/components/dashboard/stat-card"; import { cn, formatDate } from "@/lib/utils"; import { Users, CreditCard, AlertTriangle, UserX, Search, Eye, Phone, Mail, Calendar, Plus, ChevronLeft, ChevronRight, X, } from "lucide-react"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- interface Client { id: string; firstName: string; lastName: string; email: string | null; phone: string | null; avatar?: string | null; level: string | null; notes: string | null; isActive: boolean; createdAt: string; memberships?: Array<{ id: string; status: string; startDate: string; endDate: string; remainingHours: number | null; plan: { id: string; name: string; price: number | string; durationMonths: number; courtHours: number | null; discountPercent: number | string | null; }; }>; _count?: { bookings: number; }; stats?: { totalBookings: number; totalSpent: number; balance: number; }; lastBookingDate?: string | null; } interface ClientsResponse { data: Client[]; pagination: { total: number; limit: number; offset: number; hasMore: boolean; }; } interface MembershipPlan { id: string; name: string; price: number | string; durationMonths: number; courtHours: number | null; discountPercent: number | string | null; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- type MembershipStatus = "active" | "expiring" | "expired" | "none"; function getMembershipStatus(client: Client): MembershipStatus { const membership = client.memberships?.[0]; if (!membership) return "none"; if (membership.status !== "ACTIVE") { // Check if it truly expired vs just inactive const end = new Date(membership.endDate); if (end < new Date()) return "expired"; return "none"; } const end = new Date(membership.endDate); const now = new Date(); if (end < now) return "expired"; const daysUntilExpiry = Math.ceil( (end.getTime() - now.getTime()) / (1000 * 60 * 60 * 24) ); if (daysUntilExpiry <= 30) return "expiring"; return "active"; } function getStatusBadge(status: MembershipStatus) { switch (status) { case "active": return { label: "Active", className: "bg-green-100 text-green-700 border-green-300", }; case "expiring": return { label: "Expiring Soon", className: "bg-amber-100 text-amber-700 border-amber-300", }; case "expired": return { label: "Expired", className: "bg-red-100 text-red-700 border-red-300", }; case "none": default: return { label: "None", className: "bg-gray-100 text-gray-600 border-gray-200", }; } } function getInitials(firstName: string, lastName: string) { return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase(); } function formatShortDate(dateStr: string): string { const d = new Date(dateStr); return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", }); } // --------------------------------------------------------------------------- // Filter options // --------------------------------------------------------------------------- const FILTER_OPTIONS = [ { value: "all", label: "All" }, { value: "active", label: "Active Members" }, { value: "expiring", label: "Expiring Soon" }, { value: "expired", label: "Expired" }, { value: "none", label: "No Membership" }, ] as const; type FilterValue = (typeof FILTER_OPTIONS)[number]["value"]; const ITEMS_PER_PAGE = 10; // --------------------------------------------------------------------------- // Page Component // --------------------------------------------------------------------------- export default function ClientsPage() { // Data state const [clients, setClients] = useState([]); const [loadingClients, setLoadingClients] = useState(true); const [searchQuery, setSearchQuery] = useState(""); const [filterValue, setFilterValue] = useState("all"); const [currentPage, setCurrentPage] = useState(1); const [totalClients, setTotalClients] = useState(0); // Stats state const [allClientsForStats, setAllClientsForStats] = useState([]); const [loadingStats, setLoadingStats] = useState(true); // Modal state const [showCreateForm, setShowCreateForm] = useState(false); const [editingClient, setEditingClient] = useState(null); const [selectedClient, setSelectedClient] = useState(null); const [showAssignMembership, setShowAssignMembership] = useState(false); const [formLoading, setFormLoading] = useState(false); // Membership plans for assignment dialog const [membershipPlans, setMembershipPlans] = useState([]); const [error, setError] = useState(null); // --------------------------------------------------------------------------- // Derived stats // --------------------------------------------------------------------------- const stats = useMemo(() => { const total = allClientsForStats.length; let activeMemberships = 0; let expiringThisMonth = 0; let noMembership = 0; for (const c of allClientsForStats) { const status = getMembershipStatus(c); if (status === "active") activeMemberships++; if (status === "expiring") expiringThisMonth++; if (status === "none") noMembership++; } return { total, activeMemberships, expiringThisMonth, noMembership }; }, [allClientsForStats]); // --------------------------------------------------------------------------- // Fetchers // --------------------------------------------------------------------------- const fetchClients = useCallback(async () => { setLoadingClients(true); try { const params = new URLSearchParams(); if (searchQuery) params.append("search", searchQuery); params.append("limit", ITEMS_PER_PAGE.toString()); params.append("offset", ((currentPage - 1) * ITEMS_PER_PAGE).toString()); const response = await fetch(`/api/clients?${params.toString()}`); if (!response.ok) throw new Error("Error loading clients"); const data: ClientsResponse = await response.json(); // Apply membership filter client-side let filtered = data.data; if (filterValue !== "all") { filtered = data.data.filter( (c) => getMembershipStatus(c) === filterValue ); } setClients(filtered); setTotalClients(data.pagination.total); } catch (err) { console.error("Error fetching clients:", err); setError(err instanceof Error ? err.message : "Unknown error"); } finally { setLoadingClients(false); } }, [searchQuery, currentPage, filterValue]); const fetchStats = useCallback(async () => { setLoadingStats(true); try { const response = await fetch("/api/clients?limit=1000"); if (!response.ok) throw new Error("Error loading statistics"); const data: ClientsResponse = await response.json(); setAllClientsForStats(data.data); } catch (err) { console.error("Error fetching stats:", err); } finally { setLoadingStats(false); } }, []); const fetchMembershipPlans = useCallback(async () => { try { const response = await fetch("/api/membership-plans"); if (!response.ok) throw new Error("Error loading plans"); const data = await response.json(); setMembershipPlans( data.filter( (p: MembershipPlan & { isActive?: boolean }) => p.isActive !== false ) ); } catch (err) { console.error("Error fetching membership plans:", err); } }, []); const fetchClientDetails = async (clientId: string) => { try { const response = await fetch(`/api/clients/${clientId}`); if (!response.ok) throw new Error("Error loading client details"); const data = await response.json(); setSelectedClient(data); } catch (err) { console.error("Error fetching client details:", err); setError(err instanceof Error ? err.message : "Unknown error"); } }; // --------------------------------------------------------------------------- // Effects // --------------------------------------------------------------------------- useEffect(() => { fetchClients(); fetchStats(); fetchMembershipPlans(); }, [fetchClients, fetchStats, fetchMembershipPlans]); // Debounce search const [debouncedSearch, setDebouncedSearch] = useState(searchQuery); useEffect(() => { const timer = setTimeout(() => { setDebouncedSearch(searchQuery); }, 300); return () => clearTimeout(timer); }, [searchQuery]); useEffect(() => { setCurrentPage(1); }, [debouncedSearch, filterValue]); // --------------------------------------------------------------------------- // Handlers // --------------------------------------------------------------------------- const handleCreateClient = async (data: { firstName: string; lastName: string; email: string; phone: string; avatar?: string; }) => { setFormLoading(true); try { const response = await fetch("/api/clients", { 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 creating client"); } setShowCreateForm(false); await Promise.all([fetchClients(), fetchStats()]); } catch (err) { throw err; } finally { setFormLoading(false); } }; const handleUpdateClient = async (data: { firstName: string; lastName: string; email: string; phone: string; avatar?: string; }) => { if (!editingClient) return; setFormLoading(true); try { const response = await fetch(`/api/clients/${editingClient.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || "Error updating client"); } setEditingClient(null); await fetchClients(); if (selectedClient?.id === editingClient.id) { await fetchClientDetails(editingClient.id); } } catch (err) { throw err; } finally { setFormLoading(false); } }; const handleDeleteClient = async (client: Client) => { if ( !confirm( `Are you sure you want to deactivate ${client.firstName} ${client.lastName}?` ) ) { return; } try { const response = await fetch(`/api/clients/${client.id}`, { method: "DELETE", }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || "Error deactivating client"); } await Promise.all([fetchClients(), fetchStats()]); } catch (err) { console.error("Error deleting client:", err); setError(err instanceof Error ? err.message : "Unknown error"); } }; const handleAssignMembership = async (data: { clientId: string; planId: string; startDate: string; endDate: string; }) => { setFormLoading(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 assigning membership"); } setShowAssignMembership(false); await Promise.all([fetchClients(), fetchStats()]); if (selectedClient) { await fetchClientDetails(selectedClient.id); } } catch (err) { throw err; } finally { setFormLoading(false); } }; const handleRowClick = (client: Client) => { fetchClientDetails(client.id); }; // Pagination const totalPages = Math.ceil(totalClients / ITEMS_PER_PAGE); // --------------------------------------------------------------------------- // Render: Stat Card (inline for CRM style) // --------------------------------------------------------------------------- function CRMStatCard({ title, value, icon, iconBg, iconColor, }: { title: string; value: number; icon: React.ReactNode; iconBg: string; iconColor: string; }) { return (
{icon}

{title}

{value}

); } // --------------------------------------------------------------------------- // Render: Table skeleton // --------------------------------------------------------------------------- function TableSkeleton() { return ( <> {[...Array(5)].map((_, i) => (
))} ); } // --------------------------------------------------------------------------- // Render // --------------------------------------------------------------------------- return (
{/* Header */}

Clients

Manage your club members and memberships

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

{error}

)} {/* Stats Row */}
{loadingStats ? ( <> ) : ( <> } iconBg="bg-primary-100" iconColor="text-primary-600" /> } iconBg="bg-green-100" iconColor="text-green-600" /> } iconBg="bg-amber-100" iconColor="text-amber-600" /> } iconBg="bg-gray-100" iconColor="text-gray-500" /> )}
{/* Filters */}
{/* Search */}
setSearchQuery(e.target.value)} className="pl-9 w-full" />
{/* Membership Filter Dropdown */}
{/* Clients Table */}
{loadingClients ? ( ) : clients.length === 0 ? ( ) : ( clients.map((client) => { const membership = client.memberships?.[0]; const mStatus = getMembershipStatus(client); const badge = getStatusBadge(mStatus); // Determine last visit from lastBookingDate or fallback const lastVisit = client.lastBookingDate || null; return ( handleRowClick(client)} > {/* Name */} {/* Contact */} {/* Membership Plan */} {/* Status Badge */} {/* Expires */} {/* Last Visit */} {/* Actions */} ); }) )}
Name Contact Membership Status Expires Last Visit Actions

No clients found

{searchQuery || filterValue !== "all" ? "Try adjusting your search or filters" : "Add your first client to get started"}

{client.avatar ? ( {`${client.firstName} ) : (
{getInitials(client.firstName, client.lastName)}
)} {client.firstName} {client.lastName}
{client.phone && (
{client.phone}
)} {client.email && (
{client.email}
)} {!client.phone && !client.email && ( )}
{membership && membership.status === "ACTIVE" ? membership.plan.name : membership && mStatus === "expired" ? membership.plan.name : "None"} {badge.label} {membership && (membership.status === "ACTIVE" || mStatus === "expired") ? (
{formatShortDate(membership.endDate)}
) : ( )}
{lastVisit ? ( formatShortDate(lastVisit) ) : ( Never )}
e.stopPropagation()} >
{/* Pagination */} {totalPages > 1 && (
Page {currentPage} of {totalPages}
)}
{/* Create Client Form Modal */} {showCreateForm && ( setShowCreateForm(false)} isLoading={formLoading} mode="create" /> )} {/* Edit Client Form Modal */} {editingClient && ( setEditingClient(null)} isLoading={formLoading} mode="edit" /> )} {/* Client Detail Dialog */} {selectedClient && !editingClient && ( setSelectedClient(null)} onEdit={() => { setEditingClient(selectedClient); }} onAssignMembership={() => { setShowAssignMembership(true); }} /> )} {/* Assign Membership Dialog */} {showAssignMembership && selectedClient && ( setShowAssignMembership(false)} onAssign={handleAssignMembership} isLoading={formLoading} /> )}
); }