362 lines
16 KiB
TypeScript
362 lines
16 KiB
TypeScript
'use client';
|
|
|
|
import React, { useEffect, useState } from 'react';
|
|
import Link from 'next/link';
|
|
import { useRouter, useParams } from 'next/navigation';
|
|
import { useMembers, Member } from '../../../../hooks/useMembers';
|
|
|
|
export default function MemberDetailsPage() {
|
|
const params = useParams();
|
|
const id = params.id as string;
|
|
const router = useRouter();
|
|
const { getMemberById, deleteMember, bulkAction } = useMembers();
|
|
|
|
const [member, setMember] = useState<Member | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [deleteConfirm, setDeleteConfirm] = useState(false);
|
|
const [actionLoading, setActionLoading] = useState(false);
|
|
|
|
// Fetch member data
|
|
useEffect(() => {
|
|
const fetchMember = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const data = await getMemberById(id);
|
|
setMember(data);
|
|
} catch (err) {
|
|
console.error('Error fetching member:', err);
|
|
setError('Failed to load member details. Please try again.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchMember();
|
|
}, [id, getMemberById]);
|
|
|
|
// Handle member deletion
|
|
const handleDelete = async () => {
|
|
if (!deleteConfirm) {
|
|
setDeleteConfirm(true);
|
|
return;
|
|
}
|
|
|
|
setActionLoading(true);
|
|
try {
|
|
const success = await deleteMember(id);
|
|
if (success) {
|
|
router.push('/admin/members');
|
|
} else {
|
|
setError('Failed to delete member. Please try again.');
|
|
}
|
|
} catch (err) {
|
|
console.error('Error deleting member:', err);
|
|
setError('Failed to delete member. Please try again.');
|
|
} finally {
|
|
setActionLoading(false);
|
|
setDeleteConfirm(false);
|
|
}
|
|
};
|
|
|
|
// Handle member renewal
|
|
const handleRenew = async () => {
|
|
setActionLoading(true);
|
|
try {
|
|
const result = await bulkAction('renew', [id]);
|
|
if (result) {
|
|
// Refresh member data
|
|
const updatedMember = await getMemberById(id);
|
|
setMember(updatedMember);
|
|
alert('Membership renewed successfully');
|
|
} else {
|
|
setError('Failed to renew membership. Please try again.');
|
|
}
|
|
} catch (err) {
|
|
console.error('Error renewing membership:', err);
|
|
setError('Failed to renew membership. Please try again.');
|
|
} finally {
|
|
setActionLoading(false);
|
|
}
|
|
};
|
|
|
|
// Format date for display
|
|
const formatDate = (dateString: string | null | undefined) => {
|
|
if (!dateString) return 'N/A';
|
|
|
|
const date = new Date(dateString);
|
|
return new Intl.DateTimeFormat('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
}).format(date);
|
|
};
|
|
|
|
// Get status badge classes
|
|
const getStatusBadgeClasses = (status: string) => {
|
|
switch (status) {
|
|
case 'active':
|
|
return 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-200';
|
|
case 'expired':
|
|
return 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-200';
|
|
case 'pending':
|
|
return 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-200';
|
|
default:
|
|
return 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300';
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex justify-center items-center h-64">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="bg-red-50 dark:bg-red-900 p-4 rounded-md">
|
|
<div className="flex">
|
|
<div className="flex-shrink-0">
|
|
<svg className="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
<div className="ml-3">
|
|
<h3 className="text-sm font-medium text-red-800 dark:text-red-200">
|
|
{error}
|
|
</h3>
|
|
<div className="mt-2">
|
|
<button
|
|
onClick={() => router.push('/admin/members')}
|
|
className="text-sm font-medium text-red-800 dark:text-red-200 underline"
|
|
>
|
|
Return to Members List
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!member) {
|
|
return (
|
|
<div className="bg-yellow-50 dark:bg-yellow-900 p-4 rounded-md">
|
|
<div className="flex">
|
|
<div className="flex-shrink-0">
|
|
<svg className="h-5 w-5 text-yellow-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
<div className="ml-3">
|
|
<h3 className="text-sm font-medium text-yellow-800 dark:text-yellow-200">
|
|
Member not found
|
|
</h3>
|
|
<div className="mt-2">
|
|
<button
|
|
onClick={() => router.push('/admin/members')}
|
|
className="text-sm font-medium text-yellow-800 dark:text-yellow-200 underline"
|
|
>
|
|
Return to Members List
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{/* Header */}
|
|
<div className="sm:flex sm:items-center sm:justify-between mb-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
{member.firstName} {member.lastName}
|
|
</h1>
|
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
Member since {formatDate(member.joinDate)}
|
|
</p>
|
|
</div>
|
|
<div className="mt-4 sm:mt-0 flex space-x-3">
|
|
<Link
|
|
href="/admin/members"
|
|
className="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 shadow-sm text-sm font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
|
|
>
|
|
<svg className="-ml-1 mr-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
Back to Members
|
|
</Link>
|
|
<Link
|
|
href={`/admin/members/${id}/edit`}
|
|
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
|
|
>
|
|
<svg className="-ml-1 mr-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
|
</svg>
|
|
Edit Member
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status and Quick Actions */}
|
|
<div className="card p-6 mb-6">
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
|
<div className="flex items-center mb-4 sm:mb-0">
|
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300 mr-2">Status:</span>
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusBadgeClasses(member.status)}`}>
|
|
{member.status.charAt(0).toUpperCase() + member.status.slice(1)}
|
|
</span>
|
|
</div>
|
|
<div className="flex space-x-3">
|
|
<button
|
|
onClick={handleRenew}
|
|
disabled={actionLoading}
|
|
className="inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
|
|
>
|
|
{actionLoading ? (
|
|
<>
|
|
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
Processing...
|
|
</>
|
|
) : (
|
|
<>Renew Membership</>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={handleDelete}
|
|
disabled={actionLoading}
|
|
className="inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
|
>
|
|
{deleteConfirm ? 'Confirm Delete' : 'Delete Member'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Member Details */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* Personal Information */}
|
|
<div className="card p-6">
|
|
<h2 className="text-lg font-semibold mb-4">Personal Information</h2>
|
|
<dl className="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">First Name</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 dark:text-white">{member.firstName}</dd>
|
|
</div>
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">Last Name</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 dark:text-white">{member.lastName}</dd>
|
|
</div>
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">Email</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 dark:text-white">{member.email}</dd>
|
|
</div>
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">Phone</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 dark:text-white">{member.phone || 'Not provided'}</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
{/* Membership Information */}
|
|
<div className="card p-6">
|
|
<h2 className="text-lg font-semibold mb-4">Membership Information</h2>
|
|
<dl className="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">Membership Type</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 dark:text-white capitalize">{member.membershipType}</dd>
|
|
</div>
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">Join Date</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 dark:text-white">{formatDate(member.joinDate)}</dd>
|
|
</div>
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">Expiration Date</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 dark:text-white">{formatDate(member.expirationDate)}</dd>
|
|
</div>
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">Last Renewal</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 dark:text-white">{formatDate(member.lastRenewalDate)}</dd>
|
|
</div>
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">Notification Preference</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 dark:text-white capitalize">{member.notificationPreference || 'Email'}</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
{/* Address */}
|
|
<div className="card p-6">
|
|
<h2 className="text-lg font-semibold mb-4">Address</h2>
|
|
{member.address && (Object.values(member.address).some(val => val)) ? (
|
|
<dl className="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
|
|
<div className="sm:col-span-2">
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">Street</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 dark:text-white">{member.address.street || 'Not provided'}</dd>
|
|
</div>
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">City</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 dark:text-white">{member.address.city || 'Not provided'}</dd>
|
|
</div>
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">State</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 dark:text-white">{member.address.state || 'Not provided'}</dd>
|
|
</div>
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">ZIP Code</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 dark:text-white">{member.address.zip || 'Not provided'}</dd>
|
|
</div>
|
|
</dl>
|
|
) : (
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">No address information provided.</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Board Member Information */}
|
|
<div className="card p-6">
|
|
<h2 className="text-lg font-semibold mb-4">Board Member Information</h2>
|
|
{member.boardMember ? (
|
|
<dl className="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">Board Position</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 dark:text-white">{member.boardPosition || 'Not specified'}</dd>
|
|
</div>
|
|
</dl>
|
|
) : (
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Not a board member.</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Emergency Contact */}
|
|
<div className="card p-6">
|
|
<h2 className="text-lg font-semibold mb-4">Emergency Contact</h2>
|
|
{member.emergencyContact && (Object.values(member.emergencyContact).some(val => val)) ? (
|
|
<dl className="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">Name</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 dark:text-white">{member.emergencyContact.name || 'Not provided'}</dd>
|
|
</div>
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">Relationship</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 dark:text-white">{member.emergencyContact.relationship || 'Not provided'}</dd>
|
|
</div>
|
|
<div className="sm:col-span-1">
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">Phone</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 dark:text-white">{member.emergencyContact.phone || 'Not provided'}</dd>
|
|
</div>
|
|
</dl>
|
|
) : (
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">No emergency contact information provided.</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|