|
|
|
|
@@ -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>
|
|
|
|
|
|