car-crushers-portal/app/routes/events-team_.outstanding.tsx

304 lines
7.8 KiB
TypeScript

import {
Alert,
AlertIcon,
Box,
Button,
Heading,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
StackDivider,
Table,
TableCaption,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
useDisclosure,
useToast,
VStack,
} from "@chakra-ui/react";
import { useLoaderData } from "@remix-run/react";
import { useEffect, useState } from "react";
export async function loader({ context }: { context: RequestContext }) {
const now = new Date();
let month = now.getUTCMonth();
let year = now.getUTCFullYear();
if (month === 0) {
month = 12;
year--;
}
const data = await context.env.D1.prepare(
"SELECT day, details, id FROM events WHERE approved = 1 AND month = ? AND year = ? AND (performed_at IS NULL OR (reached_minimum_player_count = 0 AND type = 'gamenight'));",
)
.bind(month, year)
.all();
return {
events: data.results as Record<string, string | number>[],
past_cutoff: now.getUTCDate() > 7,
};
}
export default function () {
const { events, past_cutoff } = useLoaderData<typeof loader>();
const { isOpen, onClose, onOpen } = useDisclosure();
const [eventData, setEventData] = useState({} as { [k: string]: any });
const [isBrowserSupported, setIsBrowserSupported] = useState(true);
const toast = useToast();
useEffect(() => {
if (typeof structuredClone === "undefined") setIsBrowserSupported(false);
}, []);
async function displayErrorToast(response: Response, title: string) {
let msg = "Unknown error";
try {
msg = ((await response.json()) as { error: string }).error;
} catch {}
toast({
description: msg,
status: "error",
title,
});
}
function getStatus(event: { [k: string]: string | number }) {
if (!event.performed_at) return "Approved";
if (event.type === "rotw" && event.answered_at) return "Solved";
if (event.type === "gamenight" && event.areached_minimum_player_count)
return "Certified";
return "Completed";
}
async function certify() {
const response = await fetch(
`/api/events-team/events/${eventData.id}/certify`,
{
body: "{}",
headers: {
"content-type": "application/json",
},
method: "POST",
},
);
if (!response.ok) {
await displayErrorToast(response, "Failed to certify game night");
return;
}
toast({
status: "success",
title: "Game night certified",
});
const newData = structuredClone(eventData);
newData.reached_minimum_player_count = 1;
setEventData(newData);
}
async function completed() {
const response = await fetch(
`/api/events-team/events/${eventData.id}/complete`,
{
body: "{}",
headers: {
"content-type": "application/json",
},
method: "POST",
},
);
if (!response.ok) {
await displayErrorToast(response, "Failed to mark as completed");
return;
}
toast({
status: "success",
title: "Event marked as complete",
});
const newData = structuredClone(eventData);
newData.performed_at = Date.now();
setEventData(newData);
}
async function forgotten() {
const response = await fetch(
`/api/events-team/events/${eventData.id}/forgotten`,
{
body: "{}",
headers: {
"content-type": "application/json",
},
method: "POST",
},
);
if (!response.ok) {
await displayErrorToast(response, "Failed to mark as forgotten");
return;
}
toast({
title: "Event marked as forgotten",
status: "success",
});
const newData = structuredClone(eventData);
newData.performed_at = 0;
setEventData(newData);
}
async function solve() {
const response = await fetch(
`/api/events-team/events/${eventData.id}/solve`,
{
body: "{}",
headers: {
"content-type": "application/json",
},
method: "POST",
},
);
if (!response.ok) {
await displayErrorToast(response, "Failed to mark as solved");
return;
}
toast({
status: "success",
title: "Riddle marked as solved",
});
const newData = structuredClone(eventData);
newData.answered_at = Date.now();
setEventData(newData);
}
return (
<>
<Alert display={past_cutoff ? undefined : "none"} status="warning">
<AlertIcon />
The cutoff period for retroactively actioning events has passed.
</Alert>
<Alert display={isBrowserSupported ? "none" : undefined} status="error">
<AlertIcon />
This browser is unsupported. Please upgrade to a browser not several
years out of date.
</Alert>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Action Menu</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack divider={<StackDivider />}>
<Box gap="8px">
<Heading size="xs">Completion</Heading>
<Button
disabled={typeof eventData.completed_at === "number"}
onClick={async () => await completed()}
>
Mark as Complete
</Button>
<Button
disabled={typeof eventData.completed_at === "number"}
onClick={async () => await forgotten()}
>
Mark as Forgotten
</Button>
</Box>
{eventData.type === "rotw" ? (
<Box gap="8px">
<Heading size="xs">Solved Status</Heading>
<Button
disabled={Boolean(eventData.answered_at)}
onClick={async () => await solve()}
>
{eventData.answered_at ? "Solved" : "Mark as Solved"}
</Button>
</Box>
) : null}
{eventData.type === "gamenight" ? (
<Box gap="8px">
<Heading size="xs">Certified Status</Heading>
<Button
disabled={Boolean(eventData.reached_minimum_player_count)}
onClick={async () => await certify()}
>
{eventData.reached_minimum_player_count
? "Certified"
: "Certify"}
</Button>
</Box>
) : null}
</VStack>
</ModalBody>
<ModalFooter>
<Button onClick={onClose}>Close</Button>
</ModalFooter>
</ModalContent>
</Modal>
<TableContainer>
<Table variant="simple">
<TableCaption>
Events that are not denied or left pending which need to be actioned
</TableCaption>
<Thead>
<Tr>
<Th>Day</Th>
<Th>Details</Th>
<Th>Current Status</Th>
<Th>Action</Th>
</Tr>
</Thead>
<Tbody>
{events.map((event) => (
<Tr>
<Td>{event.day}</Td>
<Td>
{(event.details as string).length > 100
? `${(event.details as string).substring(0, 97)}...`
: event.details}
</Td>
<Td>{getStatus(event)}</Td>
<Td>
<Button
disabled={past_cutoff}
onClick={() => {
setEventData(event);
onOpen();
}}
>
Action Menu
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</>
);
}