WellNuo/admin/components/AddBeneficiaryModal.tsx
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

175 lines
4.7 KiB
TypeScript

'use client';
import React, { useState, useEffect, useRef } from 'react';
import { Button } from './ui/Button';
import { Input } from './ui/Input';
import { ErrorMessage } from './ui/ErrorMessage';
import { createBeneficiary } from '../lib/api';
interface AddBeneficiaryModalProps {
isOpen: boolean;
onClose: () => void;
onSuccess: (beneficiary: { id: number; name: string }) => void;
}
export function AddBeneficiaryModal({
isOpen,
onClose,
onSuccess,
}: AddBeneficiaryModalProps) {
const [name, setName] = useState('');
const [address, setAddress] = useState('');
const [phone, setPhone] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const nameInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isOpen) {
setName('');
setAddress('');
setPhone('');
setError(null);
setTimeout(() => {
nameInputRef.current?.focus();
}, 100);
}
}, [isOpen]);
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen && !isLoading) {
onClose();
}
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [isOpen, isLoading, onClose]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
const trimmedName = name.trim();
if (!trimmedName) {
setError('Please enter a name for the beneficiary');
return;
}
setIsLoading(true);
try {
const result = await createBeneficiary({
name: trimmedName,
address: address.trim() || undefined,
phone: phone.trim() || undefined,
});
onSuccess({ id: result.id, name: trimmedName });
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create beneficiary');
} finally {
setIsLoading(false);
}
};
const handleBackdropClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget && !isLoading) {
onClose();
}
};
if (!isOpen) return null;
return (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
onClick={handleBackdropClick}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h2 id="modal-title" className="text-lg font-semibold text-textPrimary">
Add New Beneficiary
</h2>
<p className="text-sm text-textSecondary mt-1">
Enter details for the person you want to care for
</p>
</div>
<form onSubmit={handleSubmit}>
<div className="px-6 py-4 space-y-4">
{error && (
<ErrorMessage
message={error}
onDismiss={() => setError(null)}
/>
)}
<Input
ref={nameInputRef}
id="beneficiary-name"
label="Name *"
value={name}
onChange={(e) => {
setName(e.target.value);
setError(null);
}}
placeholder="e.g., Grandma Julia"
disabled={isLoading}
fullWidth
autoComplete="off"
/>
<Input
id="beneficiary-address"
label="Address (optional)"
value={address}
onChange={(e) => setAddress(e.target.value)}
placeholder="123 Main St, City, State"
disabled={isLoading}
fullWidth
autoComplete="off"
/>
<Input
id="beneficiary-phone"
label="Phone (optional)"
type="tel"
value={phone}
onChange={(e) => setPhone(e.target.value)}
placeholder="+1 (555) 123-4567"
disabled={isLoading}
fullWidth
autoComplete="off"
/>
</div>
<div className="px-6 py-4 bg-gray-50 border-t border-gray-200 flex justify-end gap-3">
<Button
type="button"
variant="ghost"
onClick={onClose}
disabled={isLoading}
>
Cancel
</Button>
<Button
type="submit"
variant="primary"
loading={isLoading}
>
Add Beneficiary
</Button>
</div>
</form>
</div>
</div>
);
}