- Add AddBeneficiaryModal component with form fields for name, address, phone - Add createBeneficiary and updateBeneficiary API methods in admin/lib/api.js - Update Beneficiaries page with Add button and modal integration - Add comprehensive tests for AddBeneficiaryModal (15 test cases) Features: - Modal with form validation (name required) - Keyboard support (Escape to close, Enter to submit) - Click outside to dismiss - Loading state during submission - Error handling and display - Form resets on modal reopen - Automatic redirect to beneficiary detail after creation
246 lines
6.2 KiB
JavaScript
246 lines
6.2 KiB
JavaScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import AdminLayout from '../../components/AdminLayout';
|
|
import { AddBeneficiaryModal } from '../../components/AddBeneficiaryModal';
|
|
import { Button } from '../../components/ui/Button';
|
|
import { getBeneficiaries } from '../../lib/api';
|
|
|
|
export default function BeneficiariesPage() {
|
|
const router = useRouter();
|
|
const [beneficiaries, setBeneficiaries] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
loadBeneficiaries();
|
|
}, []);
|
|
|
|
const loadBeneficiaries = async () => {
|
|
try {
|
|
const data = await getBeneficiaries();
|
|
setBeneficiaries(data.beneficiaries || []);
|
|
} catch (err) {
|
|
console.error('Failed to load beneficiaries:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleAddSuccess = (newBeneficiary) => {
|
|
loadBeneficiaries();
|
|
router.push(`/admin/beneficiaries/${newBeneficiary.id}`);
|
|
};
|
|
|
|
const handleCardClick = (id) => {
|
|
router.push(`/admin/beneficiaries/${id}`);
|
|
};
|
|
|
|
return (
|
|
<AdminLayout>
|
|
<div style={styles.header}>
|
|
<div>
|
|
<h1 style={styles.title}>Beneficiaries</h1>
|
|
<p style={styles.subtitle}>Users being monitored (elderly relatives)</p>
|
|
</div>
|
|
<Button onClick={() => setIsModalOpen(true)}>
|
|
+ Add Beneficiary
|
|
</Button>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<p style={styles.loading}>Loading...</p>
|
|
) : beneficiaries.length === 0 ? (
|
|
<div style={styles.empty}>
|
|
<div style={styles.emptyIcon}>
|
|
<svg
|
|
width="48"
|
|
height="48"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="1.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
|
<circle cx="9" cy="7" r="4" />
|
|
<line x1="19" y1="8" x2="19" y2="14" />
|
|
<line x1="22" y1="11" x2="16" y2="11" />
|
|
</svg>
|
|
</div>
|
|
<p style={styles.emptyTitle}>No beneficiaries yet</p>
|
|
<p style={styles.emptyHint}>
|
|
Click the button above to add your first beneficiary
|
|
</p>
|
|
<div style={styles.emptyAction}>
|
|
<Button onClick={() => setIsModalOpen(true)}>
|
|
Add Your First Beneficiary
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div style={styles.grid}>
|
|
{beneficiaries.map((ben) => (
|
|
<div
|
|
key={ben.id}
|
|
style={styles.card}
|
|
onClick={() => handleCardClick(ben.id)}
|
|
role="button"
|
|
tabIndex={0}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
handleCardClick(ben.id);
|
|
}
|
|
}}
|
|
>
|
|
<div style={styles.avatar}>
|
|
{getInitials(ben.first_name, ben.last_name)}
|
|
</div>
|
|
<div style={styles.cardContent}>
|
|
<h3 style={styles.name}>
|
|
{ben.first_name || ben.last_name
|
|
? `${ben.first_name || ''} ${ben.last_name || ''}`.trim()
|
|
: 'Unknown'}
|
|
</h3>
|
|
<p style={styles.email}>{ben.email}</p>
|
|
|
|
{(ben.address_city || ben.address_country) && (
|
|
<p style={styles.location}>
|
|
{[ben.address_city, ben.address_country].filter(Boolean).join(', ')}
|
|
</p>
|
|
)}
|
|
|
|
{ben.phone && (
|
|
<p style={styles.phone}>{ben.phone}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div style={styles.stats}>
|
|
<span>Total: {beneficiaries.length} beneficiaries</span>
|
|
</div>
|
|
|
|
<AddBeneficiaryModal
|
|
isOpen={isModalOpen}
|
|
onClose={() => setIsModalOpen(false)}
|
|
onSuccess={handleAddSuccess}
|
|
/>
|
|
</AdminLayout>
|
|
);
|
|
}
|
|
|
|
function getInitials(first, last) {
|
|
const f = first?.[0] || '';
|
|
const l = last?.[0] || '';
|
|
return (f + l).toUpperCase() || '?';
|
|
}
|
|
|
|
const styles = {
|
|
header: {
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'flex-start',
|
|
marginBottom: '24px',
|
|
},
|
|
title: {
|
|
fontSize: '24px',
|
|
fontWeight: '600',
|
|
marginBottom: '4px',
|
|
},
|
|
subtitle: {
|
|
fontSize: '14px',
|
|
color: 'var(--text-muted)',
|
|
},
|
|
loading: {
|
|
color: 'var(--text-muted)',
|
|
},
|
|
empty: {
|
|
background: 'white',
|
|
padding: '48px',
|
|
borderRadius: '12px',
|
|
textAlign: 'center',
|
|
},
|
|
emptyIcon: {
|
|
color: 'var(--text-muted)',
|
|
marginBottom: '16px',
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
},
|
|
emptyTitle: {
|
|
fontSize: '18px',
|
|
fontWeight: '600',
|
|
marginBottom: '8px',
|
|
},
|
|
emptyHint: {
|
|
fontSize: '14px',
|
|
color: 'var(--text-muted)',
|
|
marginBottom: '24px',
|
|
},
|
|
emptyAction: {
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
},
|
|
grid: {
|
|
display: 'grid',
|
|
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
|
|
gap: '16px',
|
|
},
|
|
card: {
|
|
background: 'white',
|
|
borderRadius: '12px',
|
|
padding: '20px',
|
|
display: 'flex',
|
|
gap: '16px',
|
|
cursor: 'pointer',
|
|
transition: 'box-shadow 0.2s, transform 0.2s',
|
|
},
|
|
avatar: {
|
|
width: '48px',
|
|
height: '48px',
|
|
borderRadius: '50%',
|
|
background: 'var(--surface)',
|
|
color: 'var(--primary)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
fontSize: '16px',
|
|
fontWeight: '600',
|
|
flexShrink: 0,
|
|
},
|
|
cardContent: {
|
|
flex: 1,
|
|
minWidth: 0,
|
|
},
|
|
name: {
|
|
fontSize: '16px',
|
|
fontWeight: '600',
|
|
marginBottom: '4px',
|
|
},
|
|
email: {
|
|
fontSize: '13px',
|
|
color: 'var(--text-secondary)',
|
|
marginBottom: '8px',
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
},
|
|
location: {
|
|
fontSize: '13px',
|
|
color: 'var(--text-muted)',
|
|
},
|
|
phone: {
|
|
fontSize: '13px',
|
|
color: 'var(--text-muted)',
|
|
marginTop: '4px',
|
|
},
|
|
stats: {
|
|
marginTop: '24px',
|
|
fontSize: '13px',
|
|
color: 'var(--text-muted)',
|
|
},
|
|
};
|