v0.12.49 Login Type and Event bug fixes

This commit is contained in:
2026-04-01 09:25:17 -04:00
parent a3a878854e
commit 7031979571
9 changed files with 151 additions and 19 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "rosterchirp-frontend",
"version": "0.12.48",
"version": "0.12.49",
"private": true,
"scripts": {
"dev": "vite",

View File

@@ -820,18 +820,28 @@ function EventDetailModal({ event, onClose, onEdit, onAvailabilityChange, isTool
? [{ type:'self' }]
: [{ type:'alias', aliasId:parseInt(responder.replace('alias:','')) }];
// For "All": toggle all off only when every target already has this response;
// otherwise set all to this response (avoids partial-toggle confusion)
const allHaveResp = responder === 'all' && targets.every(t =>
t.type === 'self'
? myResp === resp
: (avail.find(r => r.is_alias && r.alias_id === t.aliasId)?.response || null) === resp
);
try {
for (const t of targets) {
const prevResp = t.type === 'self'
? myResp
: (avail.find(r => r.is_alias && r.alias_id === t.aliasId)?.response || null);
if (prevResp === resp) {
const shouldDelete = responder === 'all' ? allHaveResp : prevResp === resp;
if (shouldDelete) {
await api.deleteAvailability(event.id, t.type === 'alias' ? t.aliasId : undefined);
} else {
await api.setAvailability(event.id, resp, note, t.type === 'alias' ? t.aliasId : undefined);
}
}
if (targets.some(t => t.type === 'self')) setMyResp(prev => prev === resp ? null : resp);
if (targets.some(t => t.type === 'self')) {
setMyResp(responder === 'all' ? (allHaveResp ? null : resp) : (myResp === resp ? null : resp));
}
onAvailabilityChange?.(resp);
} catch(e) { toast(e.message,'error'); }
return;

View File

@@ -43,6 +43,29 @@ function UserCheckList({ allUsers, selectedIds, onChange, onIF, onIB }) {
);
}
function AliasCheckList({ allAliases, selectedIds, onChange, onIF, onIB }) {
const [search, setSearch] = useState('');
const filtered = allAliases
.filter(a => `${a.first_name} ${a.last_name}`.toLowerCase().includes(search.toLowerCase()))
.sort((a, b) => `${a.first_name} ${a.last_name}`.localeCompare(`${b.first_name} ${b.last_name}`));
return (
<div>
<input className="input" placeholder="Search aliases…" value={search} onChange={e => setSearch(e.target.value)} autoComplete="off" style={{ marginBottom:8 }} onFocus={onIF} onBlur={onIB} />
<div style={{ maxHeight:220, overflowY:'auto', border:'1px solid var(--border)', borderRadius:'var(--radius)' }}>
{filtered.map(a => (
<label key={a.id} style={{ display:'flex', alignItems:'center', gap:10, padding:'8px 12px', borderBottom:'1px solid var(--border)', cursor:'pointer' }}>
<input type="checkbox" checked={selectedIds.has(a.id)} onChange={() => { const n=new Set(selectedIds); n.has(a.id)?n.delete(a.id):n.add(a.id); onChange(n); }}
style={{ accentColor:'var(--primary)', width:15, height:15 }} />
<span className="flex-1 text-sm">{a.first_name} {a.last_name}</span>
<span className="text-xs" style={{ color:'var(--text-tertiary)' }}>{a.guardian_display_name || a.guardian_name}</span>
</label>
))}
{filtered.length === 0 && <div style={{ padding:16, textAlign:'center', color:'var(--text-tertiary)', fontSize:13 }}>No aliases found</div>}
</div>
</div>
);
}
function GroupCheckList({ allGroups, selectedIds, onChange }) {
return (
<div style={{ border:'1px solid var(--border)', borderRadius:'var(--radius)', maxHeight:220, overflowY:'auto' }}>
@@ -60,7 +83,7 @@ function GroupCheckList({ allGroups, selectedIds, onChange }) {
}
// ── All Groups tab ────────────────────────────────────────────────────────────
function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB, playersGroupId }) {
const toast = useToast();
const [groups, setGroups] = useState([]);
const [selected, setSelected] = useState(null);
@@ -68,6 +91,8 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
const [members, setMembers] = useState(new Set());
const [fullMembers, setFullMembers] = useState([]); // full member objects including deleted
const [aliasMembers, setAliasMembers] = useState([]); // child aliases in this group
const [allAliases, setAllAliases] = useState([]); // all aliases for players group management
const [aliasSelection, setAliasSelection] = useState(new Set()); // selected alias ids for players group
const [editName, setEditName] = useState('');
const [noDm, setNoDm] = useState(false);
const [saving, setSaving] = useState(false);
@@ -89,12 +114,25 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
setAliasMembers(aliases || []);
// No DM → checkbox enabled+checked; has DM → checkbox disabled+unchecked
setNoDm(!g.dm_group_id);
// Players group: load all aliases for alias-based membership management
if (playersGroupId && g.id === playersGroupId) {
api.getAllAliases().then(({ aliases: all }) => {
setAllAliases(all || []);
setAliasSelection(new Set((aliases || []).map(a => a.id)));
}).catch(() => {});
} else {
setAllAliases([]);
setAliasSelection(new Set());
}
};
const clearSelection = () => {
setSelected(null); setEditName(''); setMembers(new Set()); setSavedMembers(new Set());
setShowDelete(false); setFullMembers([]); setAliasMembers([]); setNoDm(false);
setAllAliases([]); setAliasSelection(new Set());
};
const isPlayersGroup = !!(playersGroupId && selected?.id === playersGroupId);
const handleSave = async () => {
if (!editName.trim()) return toast('Name required', 'error');
setSaving(true);
@@ -102,11 +140,18 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
if (selected) {
// createDm=true when the group has no DM and the user unchecked "Do not create Group DM"
const createDm = !selected.dm_group_id && !noDm;
const { group: updated } = await api.updateUserGroup(selected.id, { name: editName.trim(), memberIds: [...members], createDm });
const body = isPlayersGroup
? { name: editName.trim(), memberIds: [], aliasMemberIds: [...aliasSelection], createDm }
: { name: editName.trim(), memberIds: [...members], createDm };
const { group: updated } = await api.updateUserGroup(selected.id, body);
toast('Group updated', 'success');
const { members: fresh, aliasMembers: freshAliases } = await api.getUserGroup(selected.id);
const freshIds = new Set(fresh.map(m => m.id));
setSavedMembers(freshIds); setMembers(freshIds); setFullMembers(fresh); setAliasMembers(freshAliases || []);
if (isPlayersGroup) {
setAliasSelection(new Set((freshAliases || []).map(a => a.id)));
setAllAliases(prev => prev); // keep existing list
}
// Reflect new dm_group_id if a DM was just created
setSelected(prev => ({ ...prev, name: editName.trim(), dm_group_id: updated?.dm_group_id ?? prev.dm_group_id }));
if (createDm) setNoDm(false);
@@ -218,11 +263,20 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
{selected && selected.dm_group_id && <p style={{ fontSize:12, color:'var(--text-tertiary)', marginTop:4 }}>Group DM already exists cannot be removed.</p>}
</div>
<div>
<label className="settings-section-label">Members</label>
<div style={{ marginTop:6 }}><UserCheckList allUsers={allUsers} selectedIds={members} onChange={setMembers} onIF={onIF} onIB={onIB} /></div>
<p style={{ fontSize:12, color:'var(--text-tertiary)', marginTop:5 }}>{members.size} selected</p>
<label className="settings-section-label">{isPlayersGroup ? 'Child Aliases' : 'Members'}</label>
{isPlayersGroup ? (
<div style={{ marginTop:6 }}>
<AliasCheckList allAliases={allAliases} selectedIds={aliasSelection} onChange={setAliasSelection} onIF={onIF} onIB={onIB} />
<p style={{ fontSize:12, color:'var(--text-tertiary)', marginTop:5 }}>{aliasSelection.size} selected</p>
</div>
) : (
<>
<div style={{ marginTop:6 }}><UserCheckList allUsers={allUsers} selectedIds={members} onChange={setMembers} onIF={onIF} onIB={onIB} /></div>
<p style={{ fontSize:12, color:'var(--text-tertiary)', marginTop:5 }}>{members.size} selected</p>
</>
)}
</div>
{aliasMembers.length > 0 && (
{!isPlayersGroup && aliasMembers.length > 0 && (
<div>
<label className="settings-section-label">Child Aliases</label>
<div style={{ marginTop:6, border:'1px solid var(--border)', borderRadius:'var(--radius)', overflow:'hidden' }}>
@@ -699,6 +753,7 @@ export default function GroupManagerPage({ isMobile = false, onProfile, onHelp,
const [allUserGroups, setAllUserGroups] = useState([]);
const [refreshKey, setRefreshKey] = useState(0);
const [inputFocused, setInputFocused] = useState(false);
const [playersGroupId, setPlayersGroupId] = useState(null);
const onIF = () => setInputFocused(true);
const onIB = () => setInputFocused(false);
const onRefresh = () => setRefreshKey(k => k+1);
@@ -706,6 +761,10 @@ export default function GroupManagerPage({ isMobile = false, onProfile, onHelp,
useEffect(() => {
api.searchUsers('').then(({ users }) => setAllUsers(users.filter(u => u.status==='active').sort((a, b) => (a.display_name||a.name).localeCompare(b.display_name||b.name)))).catch(() => {});
api.getUserGroups().then(({ groups }) => setAllUserGroups([...(groups||[])].sort((a, b) => a.name.localeCompare(b.name)))).catch(() => {});
api.getSettings().then(({ settings }) => {
const pgid = (settings || []).find(s => s.key === 'feature_players_group_id')?.value;
setPlayersGroupId(pgid ? parseInt(pgid) : null);
}).catch(() => {});
}, [refreshKey]);
// Nav item helper — matches Schedule page style
@@ -758,7 +817,7 @@ export default function GroupManagerPage({ isMobile = false, onProfile, onHelp,
{/* Content */}
<div style={{ flex:1, display:'flex', overflow: isMobile ? 'auto' : 'hidden', paddingBottom: isMobile ? 'calc(82px + env(safe-area-inset-bottom, 0px))' : 0 }}>
{tab==='all' && <AllGroupsTab allUsers={allUsers} onRefresh={onRefresh} isMobile={isMobile} onIF={onIF} onIB={onIB} />}
{tab==='all' && <AllGroupsTab allUsers={allUsers} onRefresh={onRefresh} isMobile={isMobile} onIF={onIF} onIB={onIB} playersGroupId={playersGroupId} />}
{tab==='dm' && <DirectMessagesTab allUserGroups={allUserGroups} onRefresh={onRefresh} refreshKey={refreshKey} isMobile={isMobile} onIF={onIF} onIB={onIB} />}
{tab==='u2u' && <U2URestrictionsTab allUserGroups={allUserGroups} isMobile={isMobile} onIF={onIF} onIB={onIB} />}
</div>

View File

@@ -75,6 +75,7 @@ export const api = {
linkMinor: (minorId) => req('PATCH', `/users/me/link-minor/${minorId}`),
// Guardian aliases
getAliases: () => req('GET', '/users/me/aliases'),
getAllAliases: () => req('GET', '/users/aliases-all'),
createAlias: (body) => req('POST', '/users/me/aliases', body),
updateAlias: (id, body) => req('PATCH', `/users/me/aliases/${id}`, body),
deleteAlias: (id) => req('DELETE', `/users/me/aliases/${id}`),