v0.12.49 Login Type and Event bug fixes
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`),
|
||||
|
||||
Reference in New Issue
Block a user