2026-03-04 21:14:18 +00:00
|
|
|
import React from "react";
|
2026-03-07 18:38:44 +00:00
|
|
|
import {
|
|
|
|
|
Title,
|
|
|
|
|
Card,
|
|
|
|
|
Text,
|
|
|
|
|
Flex,
|
|
|
|
|
Badge,
|
|
|
|
|
Box,
|
|
|
|
|
RingProgress,
|
2026-03-07 20:49:09 +00:00
|
|
|
Alert,
|
2026-03-08 23:47:28 +00:00
|
|
|
Grid,
|
2026-03-07 18:38:44 +00:00
|
|
|
} from "@mantine/core";
|
2026-03-07 20:49:09 +00:00
|
|
|
import { IconAlertTriangle } from "@tabler/icons-react";
|
2026-03-04 21:14:18 +00:00
|
|
|
import { TimeValue } from "@mantine/dates";
|
|
|
|
|
import { IconPencil, IconPlus, IconTrash } from "@tabler/icons-react";
|
|
|
|
|
|
|
|
|
|
import { ConfirmDialogButton } from "./ConfirmDialogButton";
|
|
|
|
|
import { NavigateButton } from "./NavigateButton";
|
2026-03-07 20:49:09 +00:00
|
|
|
import { Link, useFetcher } from "react-router";
|
|
|
|
|
import { useUserContext } from "./UserContext";
|
2026-03-07 23:54:41 +00:00
|
|
|
import { DonutChart } from "@mantine/charts";
|
|
|
|
|
|
|
|
|
|
const COLORS_ROTATION = [
|
|
|
|
|
"teal",
|
|
|
|
|
"pink",
|
|
|
|
|
"lime",
|
|
|
|
|
"violet",
|
|
|
|
|
"orange",
|
|
|
|
|
"blue",
|
|
|
|
|
"yellow",
|
|
|
|
|
"grape",
|
|
|
|
|
"green",
|
|
|
|
|
"red",
|
|
|
|
|
"cyan",
|
|
|
|
|
"gray",
|
|
|
|
|
];
|
2026-03-04 21:14:18 +00:00
|
|
|
|
|
|
|
|
export interface OrderSetProps {
|
|
|
|
|
orderSets: (Pick<
|
|
|
|
|
OrderSet,
|
2026-03-07 18:38:44 +00:00
|
|
|
| "id"
|
|
|
|
|
| "name"
|
|
|
|
|
| "scheduled"
|
|
|
|
|
| "time"
|
|
|
|
|
| "weekends"
|
|
|
|
|
| "weekdays"
|
|
|
|
|
| "orders"
|
|
|
|
|
| "probability"
|
|
|
|
|
> & {
|
2026-03-07 23:54:41 +00:00
|
|
|
orders: Pick<OrderSetOrder, "id" | "name" | "weight">;
|
2026-03-07 18:38:44 +00:00
|
|
|
punishment_pool_name: string;
|
|
|
|
|
})[];
|
2026-03-04 21:14:18 +00:00
|
|
|
username: string;
|
|
|
|
|
linkBack?: React.ReactNode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const OrderSets: React.FC<OrderSetProps> = ({
|
|
|
|
|
orderSets,
|
|
|
|
|
username,
|
|
|
|
|
linkBack,
|
|
|
|
|
}) => {
|
2026-03-07 20:49:09 +00:00
|
|
|
const { username: current_user } = useUserContext();
|
2026-03-04 21:14:18 +00:00
|
|
|
const fetcher = useFetcher();
|
|
|
|
|
const handleDelete = React.useCallback(
|
|
|
|
|
(id: number) => {
|
|
|
|
|
fetcher.submit(null, {
|
|
|
|
|
action: `/orders/${username}/${id}`,
|
|
|
|
|
method: "DELETE",
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
[fetcher],
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-07 20:49:09 +00:00
|
|
|
const [isMastodonSet, setIsMastodonSet] = React.useState(true);
|
|
|
|
|
React.useEffect(() => {
|
2026-03-07 23:54:41 +00:00
|
|
|
if (username) {
|
|
|
|
|
fetch(`/api/subs/${username}`)
|
|
|
|
|
.then((response) => response.json())
|
|
|
|
|
.then((data) => {
|
|
|
|
|
if (!data.mastodon_server || !data.mastodon_username) {
|
|
|
|
|
setIsMastodonSet(false);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-03-07 20:49:09 +00:00
|
|
|
}, [username]);
|
|
|
|
|
|
2026-03-07 23:54:41 +00:00
|
|
|
const [portalRef, setPortalRef] = React.useState<HTMLElement | null>();
|
|
|
|
|
|
2026-03-04 21:14:18 +00:00
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<Box mb="lg">
|
|
|
|
|
<Title order={1}>Order Sets for {username}</Title>
|
|
|
|
|
{linkBack ? linkBack : null}
|
|
|
|
|
</Box>
|
2026-03-07 20:49:09 +00:00
|
|
|
{orderSets.length > 0 && isMastodonSet ? null : (
|
|
|
|
|
<Flex justify="center">
|
|
|
|
|
<Alert
|
|
|
|
|
variant="light"
|
|
|
|
|
color="orange"
|
|
|
|
|
title="Warning"
|
|
|
|
|
icon={<IconAlertTriangle />}
|
|
|
|
|
my="md"
|
|
|
|
|
w="40rem"
|
|
|
|
|
>
|
|
|
|
|
<b>{username}</b> must authorize with a Mastodon account before
|
|
|
|
|
orders can be issued.
|
|
|
|
|
{username === current_user ? (
|
|
|
|
|
<>
|
|
|
|
|
<br />
|
|
|
|
|
<Link to="/profile/">Edit Profile</Link>
|
|
|
|
|
</>
|
|
|
|
|
) : null}
|
|
|
|
|
</Alert>
|
|
|
|
|
</Flex>
|
|
|
|
|
)}
|
2026-03-07 23:54:41 +00:00
|
|
|
<div
|
|
|
|
|
ref={(node) => {
|
|
|
|
|
if (portalRef == null && node != null) {
|
|
|
|
|
setPortalRef(node);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-03-08 23:47:28 +00:00
|
|
|
<Grid gutter="md">
|
2026-03-07 23:54:41 +00:00
|
|
|
{orderSets
|
|
|
|
|
? orderSets.map(
|
|
|
|
|
({
|
|
|
|
|
id,
|
|
|
|
|
name,
|
|
|
|
|
scheduled,
|
|
|
|
|
orders,
|
|
|
|
|
time,
|
|
|
|
|
weekdays,
|
|
|
|
|
weekends,
|
|
|
|
|
probability,
|
|
|
|
|
punishment_pool_name,
|
|
|
|
|
}) => (
|
2026-03-08 23:47:28 +00:00
|
|
|
<Grid.Col span={{ base: 12, sm: 6 }}>
|
|
|
|
|
<Card
|
|
|
|
|
key={id}
|
|
|
|
|
shadow="sm"
|
|
|
|
|
padding="lg"
|
|
|
|
|
radius="md"
|
|
|
|
|
withBorder
|
|
|
|
|
bg="gray.2"
|
|
|
|
|
mb="0"
|
|
|
|
|
>
|
|
|
|
|
<Flex direction="column" gap="md" h="100%">
|
|
|
|
|
<Title order={4}>{name}</Title>
|
|
|
|
|
{scheduled ? (
|
|
|
|
|
<>
|
|
|
|
|
<Flex gap="xs">
|
|
|
|
|
<Text>
|
|
|
|
|
<b>Scheduled:</b>
|
|
|
|
|
</Text>
|
|
|
|
|
{time.split(",").map((time) => (
|
|
|
|
|
<div key={time}>
|
|
|
|
|
<TimeValue
|
|
|
|
|
key={time}
|
|
|
|
|
value={time}
|
|
|
|
|
format="12h"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</Flex>
|
|
|
|
|
<Flex gap="md" align="center">
|
|
|
|
|
{weekdays ? (
|
|
|
|
|
<Badge color="blue">Weekdays</Badge>
|
|
|
|
|
) : null}
|
|
|
|
|
{weekends ? (
|
|
|
|
|
<Badge color="blue">Weekends</Badge>
|
|
|
|
|
) : null}
|
|
|
|
|
<RingProgress
|
|
|
|
|
size={30}
|
|
|
|
|
thickness={5}
|
|
|
|
|
label={
|
|
|
|
|
<Text size="xs" ml="lg">
|
|
|
|
|
{probability * 100}%
|
|
|
|
|
</Text>
|
|
|
|
|
}
|
|
|
|
|
sections={[
|
|
|
|
|
{ color: "cyan", value: probability * 100 },
|
|
|
|
|
]}
|
|
|
|
|
/>
|
|
|
|
|
</Flex>
|
|
|
|
|
</>
|
|
|
|
|
) : null}
|
|
|
|
|
{punishment_pool_name ? (
|
|
|
|
|
<Text flex={1}>
|
|
|
|
|
<b>Punishments:</b> {punishment_pool_name}
|
|
|
|
|
</Text>
|
2026-03-07 23:54:41 +00:00
|
|
|
) : null}
|
2026-03-08 23:47:28 +00:00
|
|
|
<Flex justify="end" align="flex-end" gap="md">
|
|
|
|
|
{orders.length > 0 ? (
|
|
|
|
|
<DonutChart
|
|
|
|
|
flex={1}
|
|
|
|
|
size={130}
|
|
|
|
|
thickness={30}
|
|
|
|
|
data={orders.map(({ name, weight }, idx) => ({
|
|
|
|
|
name,
|
|
|
|
|
value: weight,
|
|
|
|
|
color:
|
|
|
|
|
COLORS_ROTATION[idx % COLORS_ROTATION.length],
|
|
|
|
|
}))}
|
|
|
|
|
tooltipDataSource="segment"
|
|
|
|
|
/>
|
|
|
|
|
) : null}
|
|
|
|
|
<ConfirmDialogButton
|
|
|
|
|
buttonColor="red.8"
|
|
|
|
|
buttonText="Delete"
|
|
|
|
|
text={`Are you sure you want to delete ${name}?`}
|
|
|
|
|
onConfirm={() => handleDelete(id)}
|
|
|
|
|
>
|
|
|
|
|
<IconTrash />
|
|
|
|
|
</ConfirmDialogButton>
|
|
|
|
|
<NavigateButton to={`/orders/${username}/${id}`}>
|
|
|
|
|
<IconPencil style={{ marginRight: "0.5rem" }} />
|
|
|
|
|
Edit
|
|
|
|
|
</NavigateButton>
|
|
|
|
|
</Flex>
|
2026-03-07 23:54:41 +00:00
|
|
|
</Flex>
|
2026-03-08 23:47:28 +00:00
|
|
|
</Card>
|
|
|
|
|
</Grid.Col>
|
2026-03-07 23:54:41 +00:00
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
: null}
|
2026-03-08 23:47:28 +00:00
|
|
|
</Grid>
|
2026-03-07 23:54:41 +00:00
|
|
|
</div>
|
|
|
|
|
<Box my="lg">
|
|
|
|
|
<NavigateButton to={`/orders/${username}/new`}>
|
|
|
|
|
<IconPlus style={{ marginRight: "0.5rem" }} />
|
|
|
|
|
New
|
|
|
|
|
</NavigateButton>
|
|
|
|
|
</Box>
|
2026-03-04 21:14:18 +00:00
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|