'use client'; import { useEffect, useRef, useState } from 'react'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { ClientRequestError, requestJson, requestMayHaveCompleted } from '@/lib/client-request'; import { retainAdminRoomRequest } from '@/lib/admin-room-control-client'; import { adminClosureResponse, adminClosureConfirmation, clearAdminClosureRequest, newAdminClosureRequest, readAdminClosureRequest, saveAdminClosureRequest } from '@/lib/admin-closure-client'; import type { AdminClosureInput, AdminClosureView } from '@/lib/admin-closure'; type Props = { accountId: string; code: string; canManage: boolean; onClose: () => void; onUpdated: () => void; onDenied: () => void; onBusyChange: (busy: boolean) => void; }; const message = (error: unknown) => error instanceof Error ? error.message : 'Room availability could not be loaded.'; export function AdminRoomClosure({ accountId, code, canManage, onClose, onUpdated, onDenied, onBusyChange }: Props) { const [room, setRoom] = useState(null); const [reason, setReason] = useState(''); const [confirmed, setConfirmed] = useState(false); const [confirmationCode, setConfirmationCode] = useState(''); const [pending, setPending] = useState(null); const [storageProblem, setStorageProblem] = useState(false); const [notice, setNotice] = useState(''); const [busy, setBusy] = useState(false); const [initialRequestPending, setInitialRequestPending] = useState(false); const [loading, setLoading] = useState(true); const alive = useRef(false), inFlight = useRef(false); const title = useRef(null); const url = `/api/admin/rooms/${code}/closure`; useEffect(() => { alive.current = true; title.current?.focus(); let canceled = false; try { const saved = readAdminClosureRequest(window.sessionStorage, accountId, code); // oxlint-disable-next-line react/react-compiler -- Restore the account/room's tab-scoped request after SSR. setPending(saved); if (saved) setNotice('A saved closure or reopening request needs confirmation. Review it below, then retry the same request.'); } catch (error) { setStorageProblem(true); setNotice(message(error)); } requestJson(url, { cache: 'no-store', headers: { 'X-Dune-Admin-Id': accountId } }) .then(result => { const current = adminClosureResponse(result, code); if (!canceled) setRoom(current); }) .catch(error => { if (!canceled) { setNotice(message(error)); if (error instanceof ClientRequestError && [401, 403].includes(error.status ?? 0)) onDenied(); } }) .finally(() => { if (!canceled) setLoading(false); }); return () => { canceled = true; alive.current = false; }; }, [accountId, code, url, onDenied]); async function refresh() { if (inFlight.current) return; inFlight.current = true; setBusy(true); onBusyChange(true); try { const current = adminClosureResponse(await requestJson(url, { cache: 'no-store', headers: { 'X-Dune-Admin-Id': accountId } }), code); if (alive.current) { setRoom(current); setConfirmed(false); setConfirmationCode(''); } } catch (error) { if (alive.current) { setNotice(message(error)); if (error instanceof ClientRequestError && [401, 403].includes(error.status ?? 0)) onDenied(); } } finally { inFlight.current = false; onBusyChange(false); if (alive.current) setBusy(false); } } async function submit(saved?: AdminClosureInput) { if (inFlight.current || loading || !canManage || storageProblem || (!saved && (!room || room.removed || room.archived || pending || !confirmed || (!room.closed && confirmationCode !== code)))) return; let input: AdminClosureInput; try { input = saved ?? newAdminClosureRequest(room!, !room!.closed, reason); saveAdminClosureRequest(window.sessionStorage, accountId, code, input); } catch (error) { setNotice(message(error)); return; } inFlight.current = true; setInitialRequestPending(!saved); setBusy(true); onBusyChange(true); setPending(input); setNotice(input.closed ? 'Closing room…' : 'Reopening room…'); try { const result = adminClosureConfirmation(await requestJson(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Dune-Admin-Id': accountId }, body: JSON.stringify(input), }), code, input); clearAdminClosureRequest(window.sessionStorage, accountId, code); if (alive.current) { setPending(null); setRoom(result.room); setConfirmed(false); setConfirmationCode(''); setReason(''); setNotice(result.replayed ? 'The original request was already recorded. Current room availability is shown below.' : input.closed ? 'Room closed to further play. Existing players can still read their saved table and discussion.' : 'Room reopened with its saved game, seats and previous pause and joining settings.'); onUpdated(); } } catch (error) { let cleared = false; if (!retainAdminRoomRequest(error)) { try { clearAdminClosureRequest(window.sessionStorage, accountId, code); cleared = true; } catch { /* Keep the exact retry after storage failure. */ } } if (alive.current) { if (cleared) setPending(null); setConfirmed(false); setConfirmationCode(''); setNotice(requestMayHaveCompleted(error) ? `${message(error)} The change may have been saved. Use Retry saved request to confirm.` : message(error)); if (error instanceof ClientRequestError && [401, 403].includes(error.status ?? 0)) onDenied(); else try { const current = adminClosureResponse(await requestJson(url, { cache: 'no-store', headers: { 'X-Dune-Admin-Id': accountId } }), code); if (alive.current) setRoom(current); } catch { /* Preserve the original outcome and saved retry. */ } } } finally { inFlight.current = false; onBusyChange(false); if (alive.current) { setInitialRequestPending(false); setBusy(false); } } } function discardUnreadableRecord() { if (!canManage || !room || !confirmed || busy) return; try { clearAdminClosureRequest(window.sessionStorage, accountId, code); setStorageProblem(false); setConfirmed(false); setNotice('Local retry record removed. This did not undo any server change. Review current room availability before continuing.'); } catch (error) { setNotice(message(error)); } } return

Close or reopen · {code}

{notice && {notice}} {loading ? Loading room availability… : <> {room ? <>

Current availability: {room.closed ? 'Closed' : 'Open'}. Saved pause setting: {room.paused ? 'Paused' : 'Running'} · New joins {room.joinLocked ? 'locked' : 'open'}.

{room.closed &&

The table and discussion history remain readable to existing players. New play, messages and seat changes are stopped.

} {room.closedAt !== null &&

Closed at: .

} : } {!canManage ?

Your viewer role can inspect room availability. An owner or operator can close or reopen rooms.

: storageProblem ?

No new changes can be sent until this tab’s unreadable retry record is resolved. Discarding the record does not cancel a change already saved on the server.

: initialRequestPending ? null : pending ?

Saved request: {pending.closed ? 'Close' : 'Reopen'} room {code}.

Reason: {pending.reason}

The exact request stays in this tab across refresh and reopening these controls. Retrying confirms a completed change without applying it again. Current availability can differ if another administrator changed the room afterward.

After refreshing the page or signing in again, choose All rooms in both the Directory and Archive filters to find this room and reopen these controls.

: room?.removed ?

Restore this removed room before changing its closure. Any saved close/reopen request can still be confirmed above.

: room?.archived ?

Unarchive this room before reopening it. The saved game remains closed and readable to existing players.

: room &&
{ event.preventDefault(); void submit(); }}>

Use a short operational reason. Do not include credentials or private game information.

{room.closed ? <>

Reopen play in room {code}. Saved progress and seats are retained, and current credentials and access revocations remain in effect.

Reopening preserves the previous pause and joining settings. {room.paused ? 'This room will remain paused.' : 'This room will resume with the existing AI pace.'} New joins will remain {room.joinLocked ? 'locked' : 'open'}.

: <>

Close room {code} to further play. This stops joining, game decisions, AI, new messages and seat security changes. Existing players can still read their own table and discussion history.

Warn active players before interrupting their game. Closing preserves the exact pending game and seats. It does not award a winner. An administrator can reopen it later.

}
} }
; }