265 lines
10 KiB
JavaScript
265 lines
10 KiB
JavaScript
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>
|
||
);
|
||
}
|