Sergei d530695b8b Add Add Beneficiary Flow for web admin
- 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
2026-02-01 08:32:59 -08:00

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)',
},
};