v0.12.49 Login Type and Event bug fixes
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rosterchirp-backend",
|
"name": "rosterchirp-backend",
|
||||||
"version": "0.12.48",
|
"version": "0.12.49",
|
||||||
"description": "RosterChirp backend server",
|
"description": "RosterChirp backend server",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -65,7 +65,15 @@ async function canViewEvent(schema, event, userId, isToolManager) {
|
|||||||
JOIN user_group_members ugm ON ugm.user_group_id=eug.user_group_id
|
JOIN user_group_members ugm ON ugm.user_group_id=eug.user_group_id
|
||||||
WHERE eug.event_id=$1 AND ugm.user_id=$2
|
WHERE eug.event_id=$1 AND ugm.user_id=$2
|
||||||
`, [event.id, userId]);
|
`, [event.id, userId]);
|
||||||
return !!assigned;
|
if (assigned) return true;
|
||||||
|
// Also allow if user has an alias in one of the event's user groups (Guardian Only mode)
|
||||||
|
const aliasAssigned = await queryOne(schema, `
|
||||||
|
SELECT 1 FROM event_user_groups eug
|
||||||
|
JOIN alias_group_members agm ON agm.user_group_id=eug.user_group_id
|
||||||
|
JOIN guardian_aliases ga ON ga.id=agm.alias_id
|
||||||
|
WHERE eug.event_id=$1 AND ga.guardian_id=$2
|
||||||
|
`, [event.id, userId]);
|
||||||
|
return !!aliasAssigned;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function enrichEvent(schema, event) {
|
async function enrichEvent(schema, event) {
|
||||||
@@ -235,11 +243,21 @@ router.get('/:id', authMiddleware, async (req, res) => {
|
|||||||
const itm = await isToolManagerFn(req.schema, req.user);
|
const itm = await isToolManagerFn(req.schema, req.user);
|
||||||
if (!(await canViewEvent(req.schema, event, req.user.id, itm))) return res.status(403).json({ error: 'Access denied' });
|
if (!(await canViewEvent(req.schema, event, req.user.id, itm))) return res.status(403).json({ error: 'Access denied' });
|
||||||
await enrichEvent(req.schema, event);
|
await enrichEvent(req.schema, event);
|
||||||
const isMember = !itm && !!(await queryOne(req.schema, `
|
const isMember = !itm && !!(
|
||||||
|
(await queryOne(req.schema, `
|
||||||
SELECT 1 FROM event_user_groups eug
|
SELECT 1 FROM event_user_groups eug
|
||||||
JOIN user_group_members ugm ON ugm.user_group_id=eug.user_group_id
|
JOIN user_group_members ugm ON ugm.user_group_id=eug.user_group_id
|
||||||
WHERE eug.event_id=$1 AND ugm.user_id=$2
|
WHERE eug.event_id=$1 AND ugm.user_id=$2
|
||||||
`, [event.id, req.user.id]));
|
`, [event.id, req.user.id]))
|
||||||
|
||
|
||||||
|
// Guardian Only: user has an alias in one of the event's user groups
|
||||||
|
(await queryOne(req.schema, `
|
||||||
|
SELECT 1 FROM event_user_groups eug
|
||||||
|
JOIN alias_group_members agm ON agm.user_group_id=eug.user_group_id
|
||||||
|
JOIN guardian_aliases ga ON ga.id=agm.alias_id
|
||||||
|
WHERE eug.event_id=$1 AND ga.guardian_id=$2
|
||||||
|
`, [event.id, req.user.id]))
|
||||||
|
);
|
||||||
if (event.track_availability && (itm || isMember)) {
|
if (event.track_availability && (itm || isMember)) {
|
||||||
// User responses
|
// User responses
|
||||||
const userAvail = await query(req.schema, `
|
const userAvail = await query(req.schema, `
|
||||||
@@ -253,6 +271,18 @@ router.get('/:id', authMiddleware, async (req, res) => {
|
|||||||
`, [req.params.id]);
|
`, [req.params.id]);
|
||||||
event.availability = [...userAvail, ...aliasAvail];
|
event.availability = [...userAvail, ...aliasAvail];
|
||||||
|
|
||||||
|
// For non-tool-managers: mask notes on entries that don't belong to them or their aliases
|
||||||
|
if (!itm) {
|
||||||
|
const myAliasIds = new Set(
|
||||||
|
(await query(req.schema, 'SELECT id FROM guardian_aliases WHERE guardian_id=$1', [req.user.id])).map(r => r.id)
|
||||||
|
);
|
||||||
|
event.availability = event.availability.map(r => {
|
||||||
|
const isOwn = !r.is_alias && r.user_id === req.user.id;
|
||||||
|
const isOwnAlias = r.is_alias && myAliasIds.has(r.alias_id);
|
||||||
|
return (isOwn || isOwnAlias) ? r : { ...r, note: null };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (itm) {
|
if (itm) {
|
||||||
const assignedRows = await query(req.schema, `
|
const assignedRows = await query(req.schema, `
|
||||||
SELECT DISTINCT u.id AS user_id, u.name, u.first_name, u.last_name, u.display_name
|
SELECT DISTINCT u.id AS user_id, u.name, u.first_name, u.last_name, u.display_name
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ router.post('/', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
|||||||
|
|
||||||
// PATCH /:id
|
// PATCH /:id
|
||||||
router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
||||||
const { name, memberIds, createDm = false } = req.body;
|
const { name, memberIds, createDm = false, aliasMemberIds } = req.body;
|
||||||
try {
|
try {
|
||||||
let ug = await queryOne(req.schema, 'SELECT * FROM user_groups WHERE id=$1', [req.params.id]);
|
let ug = await queryOne(req.schema, 'SELECT * FROM user_groups WHERE id=$1', [req.params.id]);
|
||||||
if (!ug) return res.status(404).json({ error: 'Not found' });
|
if (!ug) return res.status(404).json({ error: 'Not found' });
|
||||||
@@ -365,6 +365,24 @@ router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Alias member management (Guardian Only mode — players group)
|
||||||
|
if (Array.isArray(aliasMemberIds)) {
|
||||||
|
const newAliasIds = new Set(aliasMemberIds.map(Number).filter(Boolean));
|
||||||
|
const currentAliasSet = new Set(
|
||||||
|
(await query(req.schema, 'SELECT alias_id FROM alias_group_members WHERE user_group_id=$1', [ug.id])).map(r => r.alias_id)
|
||||||
|
);
|
||||||
|
for (const aid of newAliasIds) {
|
||||||
|
if (!currentAliasSet.has(aid)) {
|
||||||
|
await exec(req.schema, 'INSERT INTO alias_group_members (user_group_id,alias_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [ug.id, aid]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const aid of currentAliasSet) {
|
||||||
|
if (!newAliasIds.has(aid)) {
|
||||||
|
await exec(req.schema, 'DELETE FROM alias_group_members WHERE user_group_id=$1 AND alias_id=$2', [ug.id, aid]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updated = await queryOne(req.schema, 'SELECT * FROM user_groups WHERE id=$1', [req.params.id]);
|
const updated = await queryOne(req.schema, 'SELECT * FROM user_groups WHERE id=$1', [req.params.id]);
|
||||||
res.json({ group: updated });
|
res.json({ group: updated });
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
|
|||||||
@@ -437,6 +437,20 @@ router.post('/me/avatar', authMiddleware, uploadAvatar.single('avatar'), async (
|
|||||||
|
|
||||||
// ── Guardian alias routes (Guardian Only mode) ──────────────────────────────
|
// ── Guardian alias routes (Guardian Only mode) ──────────────────────────────
|
||||||
|
|
||||||
|
// List ALL aliases — admin/manager only (for Group Manager alias management)
|
||||||
|
router.get('/aliases-all', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const aliases = await query(req.schema,
|
||||||
|
`SELECT ga.id, ga.first_name, ga.last_name, ga.guardian_id, ga.avatar, ga.date_of_birth,
|
||||||
|
u.name AS guardian_name, u.display_name AS guardian_display_name
|
||||||
|
FROM guardian_aliases ga
|
||||||
|
JOIN users u ON u.id = ga.guardian_id
|
||||||
|
ORDER BY ga.first_name, ga.last_name`,
|
||||||
|
);
|
||||||
|
res.json({ aliases });
|
||||||
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
// List current user's aliases
|
// List current user's aliases
|
||||||
router.get('/me/aliases', authMiddleware, async (req, res) => {
|
router.get('/me/aliases', authMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
2
build.sh
2
build.sh
@@ -13,7 +13,7 @@
|
|||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="${1:-0.12.48}"
|
VERSION="${1:-0.12.49}"
|
||||||
ACTION="${2:-}"
|
ACTION="${2:-}"
|
||||||
REGISTRY="${REGISTRY:-}"
|
REGISTRY="${REGISTRY:-}"
|
||||||
IMAGE_NAME="rosterchirp"
|
IMAGE_NAME="rosterchirp"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rosterchirp-frontend",
|
"name": "rosterchirp-frontend",
|
||||||
"version": "0.12.48",
|
"version": "0.12.49",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -820,18 +820,28 @@ function EventDetailModal({ event, onClose, onEdit, onAvailabilityChange, isTool
|
|||||||
? [{ type:'self' }]
|
? [{ type:'self' }]
|
||||||
: [{ type:'alias', aliasId:parseInt(responder.replace('alias:','')) }];
|
: [{ 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 {
|
try {
|
||||||
for (const t of targets) {
|
for (const t of targets) {
|
||||||
const prevResp = t.type === 'self'
|
const prevResp = t.type === 'self'
|
||||||
? myResp
|
? myResp
|
||||||
: (avail.find(r => r.is_alias && r.alias_id === t.aliasId)?.response || null);
|
: (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);
|
await api.deleteAvailability(event.id, t.type === 'alias' ? t.aliasId : undefined);
|
||||||
} else {
|
} else {
|
||||||
await api.setAvailability(event.id, resp, note, t.type === 'alias' ? t.aliasId : undefined);
|
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);
|
onAvailabilityChange?.(resp);
|
||||||
} catch(e) { toast(e.message,'error'); }
|
} catch(e) { toast(e.message,'error'); }
|
||||||
return;
|
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 }) {
|
function GroupCheckList({ allGroups, selectedIds, onChange }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ border:'1px solid var(--border)', borderRadius:'var(--radius)', maxHeight:220, overflowY:'auto' }}>
|
<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 ────────────────────────────────────────────────────────────
|
// ── All Groups tab ────────────────────────────────────────────────────────────
|
||||||
function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
|
function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB, playersGroupId }) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [groups, setGroups] = useState([]);
|
const [groups, setGroups] = useState([]);
|
||||||
const [selected, setSelected] = useState(null);
|
const [selected, setSelected] = useState(null);
|
||||||
@@ -68,6 +91,8 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
|
|||||||
const [members, setMembers] = useState(new Set());
|
const [members, setMembers] = useState(new Set());
|
||||||
const [fullMembers, setFullMembers] = useState([]); // full member objects including deleted
|
const [fullMembers, setFullMembers] = useState([]); // full member objects including deleted
|
||||||
const [aliasMembers, setAliasMembers] = useState([]); // child aliases in this group
|
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 [editName, setEditName] = useState('');
|
||||||
const [noDm, setNoDm] = useState(false);
|
const [noDm, setNoDm] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -89,12 +114,25 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
|
|||||||
setAliasMembers(aliases || []);
|
setAliasMembers(aliases || []);
|
||||||
// No DM → checkbox enabled+checked; has DM → checkbox disabled+unchecked
|
// No DM → checkbox enabled+checked; has DM → checkbox disabled+unchecked
|
||||||
setNoDm(!g.dm_group_id);
|
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 = () => {
|
const clearSelection = () => {
|
||||||
setSelected(null); setEditName(''); setMembers(new Set()); setSavedMembers(new Set());
|
setSelected(null); setEditName(''); setMembers(new Set()); setSavedMembers(new Set());
|
||||||
setShowDelete(false); setFullMembers([]); setAliasMembers([]); setNoDm(false);
|
setShowDelete(false); setFullMembers([]); setAliasMembers([]); setNoDm(false);
|
||||||
|
setAllAliases([]); setAliasSelection(new Set());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isPlayersGroup = !!(playersGroupId && selected?.id === playersGroupId);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!editName.trim()) return toast('Name required', 'error');
|
if (!editName.trim()) return toast('Name required', 'error');
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
@@ -102,11 +140,18 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
|
|||||||
if (selected) {
|
if (selected) {
|
||||||
// createDm=true when the group has no DM and the user unchecked "Do not create Group DM"
|
// 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 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');
|
toast('Group updated', 'success');
|
||||||
const { members: fresh, aliasMembers: freshAliases } = await api.getUserGroup(selected.id);
|
const { members: fresh, aliasMembers: freshAliases } = await api.getUserGroup(selected.id);
|
||||||
const freshIds = new Set(fresh.map(m => m.id));
|
const freshIds = new Set(fresh.map(m => m.id));
|
||||||
setSavedMembers(freshIds); setMembers(freshIds); setFullMembers(fresh); setAliasMembers(freshAliases || []);
|
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
|
// 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 }));
|
setSelected(prev => ({ ...prev, name: editName.trim(), dm_group_id: updated?.dm_group_id ?? prev.dm_group_id }));
|
||||||
if (createDm) setNoDm(false);
|
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>}
|
{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>
|
||||||
<div>
|
<div>
|
||||||
<label className="settings-section-label">Members</label>
|
<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>
|
<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>
|
<p style={{ fontSize:12, color:'var(--text-tertiary)', marginTop:5 }}>{members.size} selected</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{aliasMembers.length > 0 && (
|
{!isPlayersGroup && aliasMembers.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<label className="settings-section-label">Child Aliases</label>
|
<label className="settings-section-label">Child Aliases</label>
|
||||||
<div style={{ marginTop:6, border:'1px solid var(--border)', borderRadius:'var(--radius)', overflow:'hidden' }}>
|
<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 [allUserGroups, setAllUserGroups] = useState([]);
|
||||||
const [refreshKey, setRefreshKey] = useState(0);
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
const [inputFocused, setInputFocused] = useState(false);
|
const [inputFocused, setInputFocused] = useState(false);
|
||||||
|
const [playersGroupId, setPlayersGroupId] = useState(null);
|
||||||
const onIF = () => setInputFocused(true);
|
const onIF = () => setInputFocused(true);
|
||||||
const onIB = () => setInputFocused(false);
|
const onIB = () => setInputFocused(false);
|
||||||
const onRefresh = () => setRefreshKey(k => k+1);
|
const onRefresh = () => setRefreshKey(k => k+1);
|
||||||
@@ -706,6 +761,10 @@ export default function GroupManagerPage({ isMobile = false, onProfile, onHelp,
|
|||||||
useEffect(() => {
|
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.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.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]);
|
}, [refreshKey]);
|
||||||
|
|
||||||
// Nav item helper — matches Schedule page style
|
// Nav item helper — matches Schedule page style
|
||||||
@@ -758,7 +817,7 @@ export default function GroupManagerPage({ isMobile = false, onProfile, onHelp,
|
|||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div style={{ flex:1, display:'flex', overflow: isMobile ? 'auto' : 'hidden', paddingBottom: isMobile ? 'calc(82px + env(safe-area-inset-bottom, 0px))' : 0 }}>
|
<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==='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} />}
|
{tab==='u2u' && <U2URestrictionsTab allUserGroups={allUserGroups} isMobile={isMobile} onIF={onIF} onIB={onIB} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export const api = {
|
|||||||
linkMinor: (minorId) => req('PATCH', `/users/me/link-minor/${minorId}`),
|
linkMinor: (minorId) => req('PATCH', `/users/me/link-minor/${minorId}`),
|
||||||
// Guardian aliases
|
// Guardian aliases
|
||||||
getAliases: () => req('GET', '/users/me/aliases'),
|
getAliases: () => req('GET', '/users/me/aliases'),
|
||||||
|
getAllAliases: () => req('GET', '/users/aliases-all'),
|
||||||
createAlias: (body) => req('POST', '/users/me/aliases', body),
|
createAlias: (body) => req('POST', '/users/me/aliases', body),
|
||||||
updateAlias: (id, body) => req('PATCH', `/users/me/aliases/${id}`, body),
|
updateAlias: (id, body) => req('PATCH', `/users/me/aliases/${id}`, body),
|
||||||
deleteAlias: (id) => req('DELETE', `/users/me/aliases/${id}`),
|
deleteAlias: (id) => req('DELETE', `/users/me/aliases/${id}`),
|
||||||
|
|||||||
Reference in New Issue
Block a user