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
This commit is contained in:
parent
bda883d34d
commit
d530695b8b
@ -1,12 +1,17 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
import AdminLayout from '../../components/AdminLayout';
|
import AdminLayout from '../../components/AdminLayout';
|
||||||
|
import { AddBeneficiaryModal } from '../../components/AddBeneficiaryModal';
|
||||||
|
import { Button } from '../../components/ui/Button';
|
||||||
import { getBeneficiaries } from '../../lib/api';
|
import { getBeneficiaries } from '../../lib/api';
|
||||||
|
|
||||||
export default function BeneficiariesPage() {
|
export default function BeneficiariesPage() {
|
||||||
|
const router = useRouter();
|
||||||
const [beneficiaries, setBeneficiaries] = useState([]);
|
const [beneficiaries, setBeneficiaries] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadBeneficiaries();
|
loadBeneficiaries();
|
||||||
@ -23,24 +28,73 @@ export default function BeneficiariesPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddSuccess = (newBeneficiary) => {
|
||||||
|
loadBeneficiaries();
|
||||||
|
router.push(`/admin/beneficiaries/${newBeneficiary.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCardClick = (id) => {
|
||||||
|
router.push(`/admin/beneficiaries/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
|
<div style={styles.header}>
|
||||||
|
<div>
|
||||||
<h1 style={styles.title}>Beneficiaries</h1>
|
<h1 style={styles.title}>Beneficiaries</h1>
|
||||||
<p style={styles.subtitle}>Users being monitored (elderly relatives)</p>
|
<p style={styles.subtitle}>Users being monitored (elderly relatives)</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => setIsModalOpen(true)}>
|
||||||
|
+ Add Beneficiary
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p style={styles.loading}>Loading...</p>
|
<p style={styles.loading}>Loading...</p>
|
||||||
) : beneficiaries.length === 0 ? (
|
) : beneficiaries.length === 0 ? (
|
||||||
<div style={styles.empty}>
|
<div style={styles.empty}>
|
||||||
<p>No beneficiaries yet</p>
|
<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}>
|
<p style={styles.emptyHint}>
|
||||||
Beneficiaries appear when caretakers add someone to monitor
|
Click the button above to add your first beneficiary
|
||||||
</p>
|
</p>
|
||||||
|
<div style={styles.emptyAction}>
|
||||||
|
<Button onClick={() => setIsModalOpen(true)}>
|
||||||
|
Add Your First Beneficiary
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={styles.grid}>
|
<div style={styles.grid}>
|
||||||
{beneficiaries.map((ben) => (
|
{beneficiaries.map((ben) => (
|
||||||
<div key={ben.id} style={styles.card}>
|
<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}>
|
<div style={styles.avatar}>
|
||||||
{getInitials(ben.first_name, ben.last_name)}
|
{getInitials(ben.first_name, ben.last_name)}
|
||||||
</div>
|
</div>
|
||||||
@ -70,6 +124,12 @@ export default function BeneficiariesPage() {
|
|||||||
<div style={styles.stats}>
|
<div style={styles.stats}>
|
||||||
<span>Total: {beneficiaries.length} beneficiaries</span>
|
<span>Total: {beneficiaries.length} beneficiaries</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<AddBeneficiaryModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
onSuccess={handleAddSuccess}
|
||||||
|
/>
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -81,6 +141,12 @@ function getInitials(first, last) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
|
header: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
marginBottom: '24px',
|
||||||
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: '24px',
|
fontSize: '24px',
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
@ -89,7 +155,6 @@ const styles = {
|
|||||||
subtitle: {
|
subtitle: {
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
color: 'var(--text-muted)',
|
color: 'var(--text-muted)',
|
||||||
marginBottom: '24px',
|
|
||||||
},
|
},
|
||||||
loading: {
|
loading: {
|
||||||
color: 'var(--text-muted)',
|
color: 'var(--text-muted)',
|
||||||
@ -100,10 +165,25 @@ const styles = {
|
|||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
|
emptyIcon: {
|
||||||
|
color: 'var(--text-muted)',
|
||||||
|
marginBottom: '16px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
emptyTitle: {
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: '600',
|
||||||
|
marginBottom: '8px',
|
||||||
|
},
|
||||||
emptyHint: {
|
emptyHint: {
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
color: 'var(--text-muted)',
|
color: 'var(--text-muted)',
|
||||||
marginTop: '8px',
|
marginBottom: '24px',
|
||||||
|
},
|
||||||
|
emptyAction: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
@ -116,6 +196,8 @@ const styles = {
|
|||||||
padding: '20px',
|
padding: '20px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: '16px',
|
gap: '16px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'box-shadow 0.2s, transform 0.2s',
|
||||||
},
|
},
|
||||||
avatar: {
|
avatar: {
|
||||||
width: '48px',
|
width: '48px',
|
||||||
|
|||||||
174
admin/components/AddBeneficiaryModal.tsx
Normal file
174
admin/components/AddBeneficiaryModal.tsx
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
'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>
|
||||||
|
);
|
||||||
|
}
|
||||||
208
admin/components/__tests__/AddBeneficiaryModal.test.tsx
Normal file
208
admin/components/__tests__/AddBeneficiaryModal.test.tsx
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { AddBeneficiaryModal } from '../AddBeneficiaryModal';
|
||||||
|
import * as api from '../../lib/api';
|
||||||
|
|
||||||
|
jest.mock('../../lib/api');
|
||||||
|
const mockCreateBeneficiary = api.createBeneficiary as jest.MockedFunction<
|
||||||
|
typeof api.createBeneficiary
|
||||||
|
>;
|
||||||
|
|
||||||
|
describe('AddBeneficiaryModal', () => {
|
||||||
|
const defaultProps = {
|
||||||
|
isOpen: true,
|
||||||
|
onClose: jest.fn(),
|
||||||
|
onSuccess: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const getNameInput = () => screen.getByPlaceholderText('e.g., Grandma Julia');
|
||||||
|
const getAddressInput = () => screen.getByPlaceholderText('123 Main St, City, State');
|
||||||
|
const getPhoneInput = () => screen.getByPlaceholderText('+1 (555) 123-4567');
|
||||||
|
const getSubmitButton = () => screen.getByRole('button', { name: /Add Beneficiary/i });
|
||||||
|
const getCancelButton = () => screen.getByRole('button', { name: /Cancel/i });
|
||||||
|
|
||||||
|
it('does not render when isOpen is false', () => {
|
||||||
|
render(<AddBeneficiaryModal {...defaultProps} isOpen={false} />);
|
||||||
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders modal when isOpen is true', () => {
|
||||||
|
render(<AddBeneficiaryModal {...defaultProps} />);
|
||||||
|
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Add New Beneficiary')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders all form fields', () => {
|
||||||
|
render(<AddBeneficiaryModal {...defaultProps} />);
|
||||||
|
expect(getNameInput()).toBeInTheDocument();
|
||||||
|
expect(getAddressInput()).toBeInTheDocument();
|
||||||
|
expect(getPhoneInput()).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows validation error when name is empty', async () => {
|
||||||
|
render(<AddBeneficiaryModal {...defaultProps} />);
|
||||||
|
|
||||||
|
fireEvent.click(getSubmitButton());
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Please enter a name for the beneficiary')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCreateBeneficiary).not.toHaveBeenCalled();
|
||||||
|
expect(defaultProps.onSuccess).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls createBeneficiary with correct data on submit', async () => {
|
||||||
|
mockCreateBeneficiary.mockResolvedValueOnce({ id: 123, name: 'Test Name' });
|
||||||
|
|
||||||
|
render(<AddBeneficiaryModal {...defaultProps} />);
|
||||||
|
|
||||||
|
fireEvent.change(getNameInput(), { target: { value: 'Grandma Julia' } });
|
||||||
|
fireEvent.change(getAddressInput(), { target: { value: '123 Main St' } });
|
||||||
|
fireEvent.change(getPhoneInput(), { target: { value: '+1 555-123-4567' } });
|
||||||
|
|
||||||
|
fireEvent.click(getSubmitButton());
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockCreateBeneficiary).toHaveBeenCalledWith({
|
||||||
|
name: 'Grandma Julia',
|
||||||
|
address: '123 Main St',
|
||||||
|
phone: '+1 555-123-4567',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onSuccess and onClose after successful creation', async () => {
|
||||||
|
mockCreateBeneficiary.mockResolvedValueOnce({ id: 456, name: 'Test Person' });
|
||||||
|
|
||||||
|
render(<AddBeneficiaryModal {...defaultProps} />);
|
||||||
|
|
||||||
|
await userEvent.type(getNameInput(), 'Test Person');
|
||||||
|
fireEvent.click(getSubmitButton());
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(defaultProps.onSuccess).toHaveBeenCalledWith({
|
||||||
|
id: 456,
|
||||||
|
name: 'Test Person',
|
||||||
|
});
|
||||||
|
expect(defaultProps.onClose).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error message on API failure', async () => {
|
||||||
|
mockCreateBeneficiary.mockRejectedValueOnce(new Error('Network error'));
|
||||||
|
|
||||||
|
render(<AddBeneficiaryModal {...defaultProps} />);
|
||||||
|
|
||||||
|
await userEvent.type(getNameInput(), 'Test Name');
|
||||||
|
fireEvent.click(getSubmitButton());
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Network error')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(defaultProps.onClose).not.toHaveBeenCalled();
|
||||||
|
expect(defaultProps.onSuccess).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes modal when Cancel button is clicked', () => {
|
||||||
|
render(<AddBeneficiaryModal {...defaultProps} />);
|
||||||
|
|
||||||
|
fireEvent.click(getCancelButton());
|
||||||
|
|
||||||
|
expect(defaultProps.onClose).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes modal when clicking backdrop', () => {
|
||||||
|
render(<AddBeneficiaryModal {...defaultProps} />);
|
||||||
|
|
||||||
|
const backdrop = screen.getByRole('dialog');
|
||||||
|
fireEvent.click(backdrop);
|
||||||
|
|
||||||
|
expect(defaultProps.onClose).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not close modal when clicking inside the modal content', () => {
|
||||||
|
render(<AddBeneficiaryModal {...defaultProps} />);
|
||||||
|
|
||||||
|
const modalTitle = screen.getByText('Add New Beneficiary');
|
||||||
|
fireEvent.click(modalTitle);
|
||||||
|
|
||||||
|
expect(defaultProps.onClose).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes modal when Escape key is pressed', () => {
|
||||||
|
render(<AddBeneficiaryModal {...defaultProps} />);
|
||||||
|
|
||||||
|
fireEvent.keyDown(document, { key: 'Escape' });
|
||||||
|
|
||||||
|
expect(defaultProps.onClose).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables form during submission', async () => {
|
||||||
|
let resolvePromise: (value: { id: number; name: string }) => void;
|
||||||
|
const promise = new Promise<{ id: number; name: string }>((resolve) => {
|
||||||
|
resolvePromise = resolve;
|
||||||
|
});
|
||||||
|
mockCreateBeneficiary.mockReturnValueOnce(promise);
|
||||||
|
|
||||||
|
render(<AddBeneficiaryModal {...defaultProps} />);
|
||||||
|
|
||||||
|
await userEvent.type(getNameInput(), 'Test Name');
|
||||||
|
fireEvent.click(getSubmitButton());
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getNameInput()).toBeDisabled();
|
||||||
|
expect(getCancelButton()).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
resolvePromise!({ id: 1, name: 'Test Name' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears form when modal is reopened', async () => {
|
||||||
|
const { rerender } = render(<AddBeneficiaryModal {...defaultProps} />);
|
||||||
|
|
||||||
|
await userEvent.type(getNameInput(), 'Some Name');
|
||||||
|
|
||||||
|
rerender(<AddBeneficiaryModal {...defaultProps} isOpen={false} />);
|
||||||
|
rerender(<AddBeneficiaryModal {...defaultProps} isOpen={true} />);
|
||||||
|
|
||||||
|
expect(getNameInput()).toHaveValue('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('trims whitespace from inputs', async () => {
|
||||||
|
mockCreateBeneficiary.mockResolvedValueOnce({ id: 789, name: 'Trimmed Name' });
|
||||||
|
|
||||||
|
render(<AddBeneficiaryModal {...defaultProps} />);
|
||||||
|
|
||||||
|
await userEvent.type(getNameInput(), ' Trimmed Name ');
|
||||||
|
fireEvent.click(getSubmitButton());
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockCreateBeneficiary).toHaveBeenCalledWith({
|
||||||
|
name: 'Trimmed Name',
|
||||||
|
address: undefined,
|
||||||
|
phone: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dismisses error when user starts typing', async () => {
|
||||||
|
render(<AddBeneficiaryModal {...defaultProps} />);
|
||||||
|
|
||||||
|
fireEvent.click(getSubmitButton());
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Please enter a name for the beneficiary')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
await userEvent.type(getNameInput(), 'N');
|
||||||
|
|
||||||
|
expect(screen.queryByText('Please enter a name for the beneficiary')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -64,6 +64,16 @@ export const getUser = (id) => apiRequest(`/api/admin/users/${id}`);
|
|||||||
|
|
||||||
export const getBeneficiaries = () => apiRequest('/api/admin/beneficiaries');
|
export const getBeneficiaries = () => apiRequest('/api/admin/beneficiaries');
|
||||||
export const getBeneficiary = (id) => apiRequest(`/api/admin/beneficiaries/${id}`);
|
export const getBeneficiary = (id) => apiRequest(`/api/admin/beneficiaries/${id}`);
|
||||||
|
export const createBeneficiary = (data) =>
|
||||||
|
apiRequest('/api/me/beneficiaries', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
export const updateBeneficiary = (id, data) =>
|
||||||
|
apiRequest(`/api/me/beneficiaries/${id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
export const getSubscriptions = () => apiRequest('/api/admin/subscriptions');
|
export const getSubscriptions = () => apiRequest('/api/admin/subscriptions');
|
||||||
|
|
||||||
// Deployments
|
// Deployments
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user