import { useState, useEffect, useCallback } from 'react' import { adminApi, type AdminUser, type AdminGroup, type GroupMember, type GroupFolder } from '../../api/client' import ConfirmDialog from '../../components/ConfirmDialog' import { FolderGlyph } from '../../components/icons' import { Card, Btn, Input, Modal, formatBytes } from './ui' // Gruppen und Gruppenordner. // ── Groups Tab ──────────────────────────────────────────────────────────────── /** Gruppenordner: ein Ordner, der der Gruppe gehört statt einer Person. Jedes * Mitglied sieht ihn in seiner Wurzel; entfernen kann ihn nur der Admin – und * zwar hier, nicht in der Dateiansicht. Deshalb steht der Löschen-Knopf * ausschließlich an dieser Stelle. */ function GroupFolders({ groups }: { groups: AdminGroup[] }) { const [folders, setFolders] = useState([]) const [loading, setLoading] = useState(true) const [groupId, setGroupId] = useState('') const [name, setName] = useState('') const [busy, setBusy] = useState(false) const [error, setError] = useState('') const [confirming, setConfirming] = useState(null) useEffect(() => { load() }, []) async function load() { try { setFolders(await adminApi.listGroupFolders() ?? []) } catch { setError('Could not load group folders') } finally { setLoading(false) } } async function create() { if (!groupId || !name.trim()) return setBusy(true); setError('') try { await adminApi.createGroupFolder(groupId, name.trim()) setName('') await load() } catch (e) { setError(e instanceof Error ? e.message : 'Could not create group folder') } finally { setBusy(false) } } async function remove(folder: GroupFolder) { setConfirming(null) try { await adminApi.deleteGroupFolder(folder.id) await load() } catch { setError('Could not delete group folder') } } return (

Group folders

A folder that belongs to the group instead of a person. Every member sees it next to their own folders and can work in it. Members cannot delete or rename the folder itself — only you can, here.

setName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') create() }} placeholder="Folder name" style={{ flex: 1, minWidth: 0, padding: '8px 10px', borderRadius: 8, border: '1px solid var(--color-border)', background: 'var(--color-surface-2)', color: 'var(--color-text)', fontSize: 13, outline: 'none', }} />
{error &&

{error}

} {loading ? (

Loading…

) : folders.length === 0 ? (

No group folders yet.

) : (
{folders.map(f => (
{f.name}
{f.group_name} · {f.members} {f.members === 1 ? 'member' : 'members'} · {formatBytes(f.used_bytes)} used
))}
)} {confirming && ( remove(confirming)} onCancel={() => setConfirming(null)} /> )}
) } export default function GroupsTab({ allUsers }: { allUsers: AdminUser[] }) { const [groups, setGroups] = useState([]) const [members, setMembers] = useState>({}) const [expanded, setExpanded] = useState(null) const [loading, setLoading] = useState(true) const [newName, setNewName] = useState('') const [newDesc, setNewDesc] = useState('') const [showCreate, setShowCreate] = useState(false) const [editing, setEditing] = useState(null) const [confirmGroup, setConfirmGroup] = useState(null) const [groupError, setGroupError] = useState('') const [editName, setEditName] = useState('') const [editDesc, setEditDesc] = useState('') const load = useCallback(() => { adminApi.listGroups() .then(data => setGroups(data ?? [])) .catch(() => {}) .finally(() => setLoading(false)) }, []) useEffect(() => { load() }, [load]) async function toggleExpand(groupId: string) { if (expanded === groupId) { setExpanded(null); return } setExpanded(groupId) if (!members[groupId]) { const data = await adminApi.listMembers(groupId).catch(() => []) setMembers(m => ({ ...m, [groupId]: data ?? [] })) } } async function handleCreate() { if (!newName) return await adminApi.createGroup(newName, newDesc) setNewName(''); setNewDesc(''); setShowCreate(false); load() } async function handleDelete(g: AdminGroup) { setConfirmGroup(null) try { await adminApi.deleteGroup(g.id) load() } catch (e) { setGroupError(`Could not delete group: ${e instanceof Error ? e.message : e}`) } } function openEdit(g: AdminGroup) { setEditing(g); setEditName(g.name); setEditDesc(g.description ?? '') } async function handleUpdate() { if (!editing || !editName.trim()) return try { await adminApi.updateGroup(editing.id, editName.trim(), editDesc) setEditing(null); load() } catch (e) { setGroupError(`Could not save group: ${e instanceof Error ? e.message : e}`) } } async function handleAddMember(groupId: string, userId: string) { await adminApi.addMember(groupId, userId) const data = await adminApi.listMembers(groupId).catch(() => []) setMembers(m => ({ ...m, [groupId]: data ?? [] })) load() } async function handleRemoveMember(groupId: string, userId: string) { await adminApi.removeMember(groupId, userId) setMembers(m => ({ ...m, [groupId]: (m[groupId] ?? []).filter(x => x.user_id !== userId) })) load() } return (
{/* Gruppenordner zuerst: Mitgliedschaft ohne gemeinsamen Ordner tut nichts, der Ordner ist der eigentliche Zweck einer Gruppe. */}

Groups

setShowCreate(v => !v)}>+ Create group
{showCreate && (
Create setShowCreate(false)}>Cancel
)} {groupError && ( {groupError} )} {confirmGroup && ( handleDelete(confirmGroup)} onCancel={() => setConfirmGroup(null)} /> )} {editing && ( setEditing(null)}>
setEditing(null)}>Cancel Save
)} {loading &&

Loading…

} {!loading && groups.length === 0 && (

No groups yet

)} {groups.map((g, i) => { const isOpen = expanded === g.id const groupMembers = members[g.id] ?? [] const nonMembers = allUsers.filter(u => !groupMembers.some(m => m.user_id === u.id)) return (
{g.name}
{g.description &&
{g.description}
}
{g.member_count} {g.member_count === 1 ? 'member' : 'members'} openEdit(g)}>Edit handleDelete(g)}>Delete
{isOpen && (
{groupMembers.length === 0 &&

No members

} {groupMembers.map(m => (
{m.username}{m.display_name ? ` (${m.display_name})` : ''} handleRemoveMember(g.id, m.user_id)}>Remove
))} {nonMembers.length > 0 && (
)}
)}
) })}
) }