feat(phase5): scheduling/availability - DB, API, UI in server settings
This commit is contained in:
@@ -20,6 +20,12 @@ interface AuditEntry {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface AvailabilitySlot {
|
||||
day_of_week: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
}
|
||||
|
||||
const ACTION_LABELS: Record<string, string> = {
|
||||
KICK: 'kicked member',
|
||||
BAN: 'banned member',
|
||||
@@ -38,14 +44,24 @@ const ACTION_LABELS: Record<string, string> = {
|
||||
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'>('audit');
|
||||
const [tab, setTab] = useState<'audit' | 'availability'>('audit');
|
||||
const [entries, setEntries] = useState<AuditEntry[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const servers = useServerStore((s) => s.servers);
|
||||
const server = servers.find((s) => s.id === serverId);
|
||||
|
||||
// Availability state
|
||||
const [slots, setSlots] = useState<AvailabilitySlot[]>([]);
|
||||
const [availLoading, setAvailLoading] = useState(false);
|
||||
const [availMessage, setAvailMessage] = useState<string | null>(null);
|
||||
const [editDay, setEditDay] = useState('monday');
|
||||
const [editStart, setEditStart] = useState('09:00');
|
||||
const [editEnd, setEditEnd] = useState('17:00');
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
@@ -58,12 +74,20 @@ export function ServerSettingsModal({ serverId, onClose }: ServerSettingsModalPr
|
||||
}, [onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tab !== 'audit') return;
|
||||
setLoading(true);
|
||||
api.get<AuditEntry[]>(`/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));
|
||||
if (tab === 'audit') {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
api.get<AuditEntry[]>(`/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<AvailabilitySlot[]>(`/users/me/availability?server_id=${serverId}`)
|
||||
.then((data) => setSlots(Array.isArray(data) ? data : []))
|
||||
.catch(() => {})
|
||||
.finally(() => setAvailLoading(false));
|
||||
}
|
||||
}, [tab, serverId]);
|
||||
|
||||
const formatTime = (iso: string) => {
|
||||
@@ -71,6 +95,27 @@ export function ServerSettingsModal({ serverId, onClose }: ServerSettingsModalPr
|
||||
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);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
||||
@@ -91,6 +136,12 @@ export function ServerSettingsModal({ serverId, onClose }: ServerSettingsModalPr
|
||||
>
|
||||
[AUDIT LOG]
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTab('availability')}
|
||||
className={`px-4 py-2 text-xs ${tab === 'availability' ? 'text-gb-orange bg-gb-bg-s' : 'text-gb-fg-f hover:text-gb-orange'}`}
|
||||
>
|
||||
[AVAILABILITY]
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{tab === 'audit' && (
|
||||
@@ -122,6 +173,65 @@ export function ServerSettingsModal({ serverId, onClose }: ServerSettingsModalPr
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{tab === 'availability' && (
|
||||
<div className="space-y-4">
|
||||
{availLoading && <p className="text-gb-fg-f text-xs">[loading...]</p>}
|
||||
<div className="text-xs text-gb-fg-s mb-2">set your weekly availability for this server</div>
|
||||
|
||||
{/* Add slot form */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<select
|
||||
value={editDay}
|
||||
onChange={(e) => setEditDay(e.target.value)}
|
||||
className="terminal-input text-xs py-1 px-2"
|
||||
>
|
||||
{DAYS.map((d) => <option key={d} value={d}>{d.slice(0,3)}</option>)}
|
||||
</select>
|
||||
<input
|
||||
type="time"
|
||||
value={editStart}
|
||||
onChange={(e) => setEditStart(e.target.value)}
|
||||
className="terminal-input text-xs py-1 px-2 w-20"
|
||||
/>
|
||||
<span className="text-gb-fg-f text-xs">to</span>
|
||||
<input
|
||||
type="time"
|
||||
value={editEnd}
|
||||
onChange={(e) => setEditEnd(e.target.value)}
|
||||
className="terminal-input text-xs py-1 px-2 w-20"
|
||||
/>
|
||||
<button onClick={addSlot} className="px-2 py-1 bg-gb-orange text-gb-bg text-xs">[ADD]</button>
|
||||
</div>
|
||||
|
||||
{/* Grid by day */}
|
||||
<div className="grid grid-cols-7 gap-1">
|
||||
{DAYS.map((day) => (
|
||||
<div key={day} className="text-xs">
|
||||
<div className="text-gb-orange font-bold mb-1 text-center">{day.slice(0,3)}</div>
|
||||
<div className="space-y-1">
|
||||
{slotsForDay(day).map((s, i) => {
|
||||
const globalIdx = slots.findIndex((x) => x === slots.filter((sl) => sl.day_of_week === day)[i]);
|
||||
return (
|
||||
<div key={globalIdx} className="bg-gb-bg-t rounded px-1 py-0.5 flex items-center justify-between gap-1 group">
|
||||
<span className="text-gb-fg-s">{s.start_time}-{s.end_time}</span>
|
||||
<button
|
||||
onClick={() => removeSlot(globalIdx)}
|
||||
className="text-gb-fg-f hover:text-gb-red opacity-0 group-hover:opacity-100"
|
||||
>✕</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={saveSlots} className="px-3 py-1 bg-gb-orange text-gb-bg text-xs">[SAVE]</button>
|
||||
{availMessage && <span className="text-xs text-gb-fg-s">{availMessage}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user