Files
rosterchirp/frontend/src/components/AddChildAliasModal.jsx

265 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react';
import { useToast } from '../contexts/ToastContext.jsx';
import { useAuth } from '../contexts/AuthContext.jsx';
import { api } from '../utils/api.js';
export default function AddChildAliasModal({ onClose }) {
const toast = useToast();
const { user: currentUser } = useAuth();
const [aliases, setAliases] = useState([]);
const [editingAlias, setEditingAlias] = useState(null); // null = new entry
const [form, setForm] = useState({ firstName: '', lastName: '', dob: '', phone: '', email: '' });
const [avatarFile, setAvatarFile] = useState(null);
const [saving, setSaving] = useState(false);
// Partner state
const [partner, setPartner] = useState(null);
const [selectedPartnerId, setSelectedPartnerId] = useState('');
const [allUsers, setAllUsers] = useState([]);
const [savingPartner, setSavingPartner] = useState(false);
useEffect(() => {
Promise.all([
api.getAliases(),
api.getPartner(),
api.searchUsers(''),
]).then(([aliasRes, partnerRes, usersRes]) => {
setAliases(aliasRes.aliases || []);
setPartner(partnerRes.partner || null);
setSelectedPartnerId(partnerRes.partner?.id?.toString() || '');
setAllUsers((usersRes.users || []).filter(u => u.id !== currentUser?.id));
}).catch(() => {});
}, []);
const set = k => e => setForm(p => ({ ...p, [k]: e.target.value }));
const resetForm = () => {
setEditingAlias(null);
setForm({ firstName: '', lastName: '', dob: '', phone: '', email: '' });
setAvatarFile(null);
};
const handleSelectAlias = (a) => {
if (editingAlias?.id === a.id) { resetForm(); return; }
setEditingAlias(a);
setForm({
firstName: a.first_name || '',
lastName: a.last_name || '',
dob: a.date_of_birth ? a.date_of_birth.slice(0, 10) : '',
phone: a.phone || '',
email: a.email || '',
});
setAvatarFile(null);
};
const handleSavePartner = async () => {
setSavingPartner(true);
try {
if (!selectedPartnerId) {
await api.removePartner();
setPartner(null);
toast('Spouse/Partner removed', 'success');
} else {
const { partner: p } = await api.setPartner(parseInt(selectedPartnerId));
setPartner(p);
const { aliases: fresh } = await api.getAliases();
setAliases(fresh || []);
toast('Spouse/Partner saved', 'success');
}
} catch (e) {
toast(e.message, 'error');
} finally {
setSavingPartner(false);
}
};
const handleSave = async () => {
if (!form.firstName.trim() || !form.lastName.trim())
return toast('First and last name required', 'error');
setSaving(true);
try {
if (editingAlias) {
await api.updateAlias(editingAlias.id, {
firstName: form.firstName.trim(),
lastName: form.lastName.trim(),
dateOfBirth: form.dob || null,
phone: form.phone || null,
email: form.email || null,
});
if (avatarFile) await api.uploadAliasAvatar(editingAlias.id, avatarFile);
toast('Child alias updated', 'success');
} else {
const { alias } = await api.createAlias({
firstName: form.firstName.trim(),
lastName: form.lastName.trim(),
dateOfBirth: form.dob || null,
phone: form.phone || null,
email: form.email || null,
});
if (avatarFile) await api.uploadAliasAvatar(alias.id, avatarFile);
toast('Child alias added', 'success');
}
const { aliases: fresh } = await api.getAliases();
setAliases(fresh || []);
resetForm();
} catch (e) {
toast(e.message, 'error');
} finally {
setSaving(false);
}
};
const handleDelete = async (e, aliasId) => {
e.stopPropagation();
try {
await api.deleteAlias(aliasId);
setAliases(prev => prev.filter(a => a.id !== aliasId));
if (editingAlias?.id === aliasId) resetForm();
toast('Child alias removed', 'success');
} catch (err) { toast(err.message, 'error'); }
};
const lbl = (text, required) => (
<label className="text-sm" style={{ color: 'var(--text-secondary)', display: 'block', marginBottom: 4 }}>
{text}{required && <span style={{ color: 'var(--error)', marginLeft: 2 }}>*</span>}
</label>
);
return (
<div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
<div className="modal">
{/* Header */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20 }}>
<h2 className="modal-title" style={{ margin: 0 }}>Family Manager</h2>
<button className="btn-icon" onClick={onClose} aria-label="Close">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
{/* Spouse/Partner section */}
<div style={{ marginBottom: 16 }}>
{lbl('Spouse/Partner')}
<div style={{ display: 'flex', gap: 8 }}>
<select
className="input"
style={{ flex: 1 }}
value={selectedPartnerId}
onChange={e => setSelectedPartnerId(e.target.value)}
>
<option value=""> None </option>
{allUsers.map(u => (
<option key={u.id} value={u.id}>{u.display_name || u.name}</option>
))}
</select>
<button
className="btn btn-primary"
onClick={handleSavePartner}
disabled={savingPartner}
style={{ whiteSpace: 'nowrap' }}
>
{savingPartner ? 'Saving…' : 'Save'}
</button>
</div>
{partner && (
<div className="text-sm" style={{ color: 'var(--text-secondary)', marginTop: 4 }}>
Linked with {partner.display_name || partner.name}
</div>
)}
</div>
{/* Existing aliases list */}
{aliases.length > 0 && (
<div style={{ marginBottom: 16 }}>
<div className="text-sm font-medium" style={{ color: 'var(--text-secondary)', marginBottom: 8 }}>
Your Children click to edit
</div>
<div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius)', overflow: 'hidden' }}>
{aliases.map((a, i) => (
<div
key={a.id}
onClick={() => handleSelectAlias(a)}
style={{
display: 'flex', alignItems: 'center', gap: 10,
padding: '9px 12px', cursor: 'pointer',
borderBottom: i < aliases.length - 1 ? '1px solid var(--border)' : 'none',
background: editingAlias?.id === a.id ? 'var(--primary-light)' : 'transparent',
}}
>
<span style={{ flex: 1, fontSize: 14, fontWeight: editingAlias?.id === a.id ? 600 : 400 }}>
{a.first_name} {a.last_name}
</span>
{a.date_of_birth && (
<span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>
{a.date_of_birth.slice(0, 10)}
</span>
)}
<button
onClick={e => handleDelete(e, a.id)}
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--error)', fontSize: 18, lineHeight: 1, padding: '0 2px' }}
aria-label="Remove"
>×</button>
</div>
))}
</div>
</div>
)}
{/* Form section label */}
<div className="text-sm font-medium" style={{ color: 'var(--text-secondary)', marginBottom: 10 }}>
{editingAlias
? `Editing: ${editingAlias.first_name} ${editingAlias.last_name}`
: 'Add Child'}
</div>
{/* Form */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
<div>
{lbl('First Name', true)}
<input className="input" value={form.firstName} onChange={set('firstName')}
autoComplete="off" autoCapitalize="words" />
</div>
<div>
{lbl('Last Name', true)}
<input className="input" value={form.lastName} onChange={set('lastName')}
autoComplete="off" autoCapitalize="words" />
</div>
<div>
{lbl('Date of Birth')}
<input className="input" placeholder="YYYY-MM-DD" value={form.dob} onChange={set('dob')}
autoComplete="off" />
</div>
<div>
{lbl('Phone')}
<input className="input" type="tel" value={form.phone} onChange={set('phone')}
autoComplete="off" />
</div>
</div>
<div>
{lbl('Email (optional)')}
<input className="input" type="email" value={form.email} onChange={set('email')}
autoComplete="off" />
</div>
<div>
{lbl('Avatar (optional)')}
<input type="file" accept="image/*"
onChange={e => setAvatarFile(e.target.files?.[0] || null)} />
</div>
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 4 }}>
{editingAlias && (
<button className="btn btn-secondary" onClick={resetForm}>Cancel Edit</button>
)}
<button className="btn btn-primary" onClick={handleSave} disabled={saving}>
{saving ? 'Saving…' : editingAlias ? 'Update Alias' : 'Add Alias'}
</button>
</div>
</div>
</div>
</div>
);
}