import { useEffect, useState } from 'react'; import { api } from '../lib/api.ts'; import { useServerStore } from '../stores/server.ts'; import { usePermissions } from '../lib/usePermissions.ts'; import { RoleManager } from './RoleManager.tsx'; interface ServerGroup { id: string; server_id: string; name: string; position: number; } interface ServerSettingsModalProps { serverId: string; onClose: () => void; } interface AuditEntry { id: string; server_id: string; user_id: string | null; username: string | null; action_type: string; target_type: string | null; target_id: string | null; reason: string | null; changes: unknown; created_at: string; } interface AvailabilitySlot { day_of_week: string; start_time: string; end_time: string; } const ACTION_LABELS: Record = { KICK: 'kicked member', BAN: 'banned member', UNBAN: 'unbanned member', MUTE: 'muted member', UNMUTE: 'unmuted member', ROLE_CREATE: 'created role', ROLE_UPDATE: 'updated role', ROLE_DELETE: 'deleted role', MEMBER_ROLES_UPDATE: 'updated member roles', CHANNEL_CREATE: 'created channel', CHANNEL_UPDATE: 'updated channel', CHANNEL_DELETE: 'deleted channel', SERVER_UPDATE: 'updated server', MESSAGE_DELETE: 'deleted message', BULK_DELETE: 'bulk deleted messages', }; const DAYS = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; export function ServerSettingsModal({ serverId, onClose }: ServerSettingsModalProps) { const [tab, setTab] = useState<'audit' | 'availability' | 'roles' | 'groups'>('audit'); const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const servers = useServerStore((s) => s.servers); const server = servers.find((s) => s.id === serverId); const { isOwner, canManageRoles, canManageChannels, canManageServer } = usePermissions(serverId); // Availability state const [slots, setSlots] = useState([]); const [availLoading, setAvailLoading] = useState(false); const [availMessage, setAvailMessage] = useState(null); const [editDay, setEditDay] = useState('monday'); const [editStart, setEditStart] = useState('09:00'); const [editEnd, setEditEnd] = useState('17:00'); // Groups state const [groups, setGroups] = useState([]); const [groupName, setGroupName] = useState(''); const [editingGroupId, setEditingGroupId] = useState(null); const [editingGroupName, setEditingGroupName] = useState(''); const [groupsLoading, setGroupsLoading] = useState(false); const handleClose = () => { window.dispatchEvent(new CustomEvent('refresh-server-groups')); onClose(); }; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault(); handleClose(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [onClose]); useEffect(() => { if (tab === 'audit' && (isOwner || canManageServer)) { setLoading(true); setError(null); api.get(`/servers/${serverId}/audit-log`) .then((data) => setEntries(Array.isArray(data) ? data : [])) .catch((err) => setError(err instanceof Error ? err.message : 'Failed to load audit log')) .finally(() => setLoading(false)); } else if (tab === 'availability') { setAvailLoading(true); api.get(`/users/me/availability?server_id=${serverId}`) .then((data) => setSlots(Array.isArray(data) ? data : [])) .catch(() => {}) .finally(() => setAvailLoading(false)); } else if (tab === 'groups' && (isOwner || canManageChannels)) { setGroupsLoading(true); setError(null); api.get(`/servers/${serverId}/groups`) .then((data) => setGroups(Array.isArray(data) ? data : [])) .catch((err) => setError(err instanceof Error ? err.message : 'Failed to load groups')) .finally(() => setGroupsLoading(false)); } }, [tab, serverId, isOwner, canManageServer, canManageChannels]); const formatTime = (iso: string) => { const d = new Date(iso); return d.toLocaleString(); }; const addSlot = () => { if (slots.some((s) => s.day_of_week === editDay && s.start_time === editStart)) return; setSlots([...slots, { day_of_week: editDay, start_time: editStart, end_time: editEnd }]); }; const removeSlot = (idx: number) => { setSlots(slots.filter((_, i) => i !== idx)); }; const saveSlots = async () => { setAvailMessage(null); try { await api.put(`/users/me/availability?server_id=${serverId}`, slots); setAvailMessage('[saved]'); } catch (err) { setAvailMessage(err instanceof Error ? `ERR: ${err.message}` : 'ERR: failed to save'); } }; const slotsForDay = (day: string) => slots.filter((s) => s.day_of_week === day); const handleCreateGroup = async (e: React.FormEvent) => { e.preventDefault(); if (!groupName.trim()) return; setError(null); try { await api.post(`/servers/${serverId}/groups`, { name: groupName.trim() }); setGroupName(''); const data = await api.get(`/servers/${serverId}/groups`); setGroups(Array.isArray(data) ? data : []); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create group'); } }; const handleRenameGroup = async (groupId: string, name: string) => { if (!name.trim()) return; setError(null); try { await api.patch(`/servers/${serverId}/groups/${groupId}`, { name: name.trim() }); setEditingGroupId(null); const data = await api.get(`/servers/${serverId}/groups`); setGroups(Array.isArray(data) ? data : []); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to rename group'); } }; const handleDeleteGroup = async (groupId: string) => { if (!confirm('Are you sure you want to delete this group? Channels inside will be moved to ungrouped.')) return; setError(null); try { await api.delete(`/servers/${serverId}/groups/${groupId}`); const data = await api.get(`/servers/${serverId}/groups`); setGroups(Array.isArray(data) ? data : []); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to delete group'); } }; const handleMoveGroup = async (groupId: string, direction: 'up' | 'down') => { const idx = groups.findIndex((g) => g.id === groupId); if (idx === -1) return; const targetIdx = direction === 'up' ? idx - 1 : idx + 1; if (targetIdx < 0 || targetIdx >= groups.length) return; setError(null); const g1 = groups[idx]; const g2 = groups[targetIdx]; try { await api.patch(`/servers/${serverId}/groups/${g1.id}`, { position: g2.position }); await api.patch(`/servers/${serverId}/groups/${g2.id}`, { position: g1.position }); const data = await api.get(`/servers/${serverId}/groups`); setGroups(Array.isArray(data) ? data : []); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to reorder group'); } }; return (
e.stopPropagation()} >
SERVER SETTINGS: {server?.name ?? serverId}
{(isOwner || canManageServer) && ( )} {(isOwner || canManageRoles) && ( )} {(isOwner || canManageChannels) && ( )}
{error &&

ERR: {error}

} {tab === 'audit' && ( <> {loading &&

[loading...]

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

[no audit log entries]

)}
{entries.map((entry) => (
{formatTime(entry.created_at)} {entry.action_type}
{entry.username ?? entry.user_id ?? 'system'} {' '}{ACTION_LABELS[entry.action_type] ?? entry.action_type} {entry.target_id && ( target:{entry.target_id} )}
{entry.reason && (
reason: {entry.reason}
)}
))}
)} {tab === 'availability' && (
{availLoading &&

[loading...]

}
set your weekly availability for this server
{/* Add slot form */}
setEditStart(e.target.value)} className="terminal-input text-xs py-1 px-2 w-20" /> to setEditEnd(e.target.value)} className="terminal-input text-xs py-1 px-2 w-20" />
{/* Grid by day */}
{DAYS.map((day) => (
{day.slice(0,3)}
{slotsForDay(day).map((s, i) => { const globalIdx = slots.findIndex((x) => x === slots.filter((sl) => sl.day_of_week === day)[i]); return (
{s.start_time}-{s.end_time}
); })}
))}
{availMessage && {availMessage}}
)} {tab === 'roles' && (
)} {tab === 'groups' && (

manage channel groups for layout organization

{/* Create group form */}
setGroupName(e.target.value)} className="terminal-input text-xs px-2 py-1 flex-1" />
{groupsLoading &&

[loading...]

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

[no groups created yet]

)}
{groups.map((g, index) => { const isEditing = editingGroupId === g.id; return (
{isEditing ? (
setEditingGroupName(e.target.value)} className="bg-gb-bg-t text-gb-fg px-1 py-0.5 text-xs outline-none border border-gb-orange flex-1" />
) : ( <> {g.name}
)}
); })}
)}
); }