Skip to content

Commit

Permalink
feat(event): import slots
Browse files Browse the repository at this point in the history
  • Loading branch information
xfoxfu committed Jul 15, 2024
1 parent 1555877 commit 29a092b
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 16 deletions.
2 changes: 2 additions & 0 deletions Net.Vatprc.Uniapi.UI.Event/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"dependencies": {
"@mantine/core": "^7.11.0",
"@mantine/dates": "^7.11.0",
"@mantine/dropzone": "^7.11.2",
"@mantine/hooks": "^7.11.0",
"@mantine/notifications": "^7.11.0",
"@tabler/icons-react": "^3.10.0",
Expand All @@ -29,6 +30,7 @@
"date-fns-tz": "^3.1.3",
"jotai": "^2.7.0",
"openapi-fetch": "^0.10.2",
"radash": "^12.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwind-merge": "^2.3.0"
Expand Down
39 changes: 39 additions & 0 deletions Net.Vatprc.Uniapi.UI.Event/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

141 changes: 141 additions & 0 deletions Net.Vatprc.Uniapi.UI.Event/src/components/slot-import.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { DateTime } from "./datetime";
import client, { formatPath, queryClient } from "@/client";
import { useUser } from "@/services/auth";
import { wrapPromiseWithToast } from "@/utils";
import { ActionIcon, Button, Group, Modal, Pill, Stack, Table, Text, useMantineTheme } from "@mantine/core";
import { Dropzone, FileWithPath } from "@mantine/dropzone";
import { useDisclosure } from "@mantine/hooks";
import { IconFileImport, IconFileTypeCsv, IconUpload, IconX } from "@tabler/icons-react";
import { useMutation } from "@tanstack/react-query";
import { parse } from "date-fns";
import { fromZonedTime } from "date-fns-tz";
import { unique } from "radash";
import { useState } from "react";

interface Slot {
airspace: string;
icao_codes: string[];
enter_at: Date;
leave_at?: Date;
}

export const ImportSlot = ({ eventId }: { eventId: string }) => {
const [opened, { toggle, close }] = useDisclosure(false);
const theme = useMantineTheme();
const user = useUser();

const [slots, setSlots] = useState<Slot[]>([]);

const onDrop = async (files: FileWithPath[]) => {
const file = files[0];
const data = (await file?.text()) ?? "";
setSlots(
data
.split("\n")
.map((line) => {
const [dep, dep_time, arr, arr_time] = line.split(",");
if (!dep || !dep_time || !arr) return;
return {
airspace: `${dep} - ${arr}`,
icao_codes: [dep, arr],
enter_at: fromZonedTime(parse(dep_time ?? "", "yyyy/MM/dd HH:mm:ss", Date.now()), "UTC"),
leave_at: arr_time ? fromZonedTime(parse(arr_time, "yyyy/MM/dd HH:mm:ss", Date.now()), "UTC") : undefined,
};
})
.filter((x) => !!x),
);
};

const { isPending, mutate } = useMutation({
mutationKey: formatPath("/api/events/{eid}/slots", { eid: eventId }),
mutationFn: async () => {
const airspaces = await Promise.all(
unique(slots, (s) => s.airspace).map((slot) =>
client.POST("/api/events/{eid}/airspaces", {
params: { path: { eid: eventId } },
body: { name: slot.airspace, icao_codes: slot.icao_codes },
}),
),
);
await Promise.all(
slots.map((slot) =>
client.POST("/api/events/{eid}/slots", {
params: { path: { eid: eventId } },
body: {
airspace_id: airspaces.find((a) => a.data?.name === slot.airspace)?.data?.id ?? "",
enter_at: slot.enter_at.toISOString(),
leave_at: slot.leave_at?.toISOString(),
},
}),
),
);
},
onSuccess: () => {
close();
return queryClient.invalidateQueries({ queryKey: formatPath("/api/events/{eid}", { eid: eventId }) });
},
});
const onSubmit = () => {
mutate();
};

if (!user?.roles.includes("ec")) return null;
return (
<>
<ActionIcon variant="subtle" aria-label="Settings" onClick={toggle}>
<IconFileImport size={18} />
</ActionIcon>
<Modal opened={opened} onClose={close} title="Import slots" size="xl">
<Stack>
<Dropzone onDrop={(f) => wrapPromiseWithToast(onDrop(f))}>
<Group justify="center" gap="xl" style={{ pointerEvents: "none" }}>
<Dropzone.Accept>
<IconUpload color={theme.colors.green[7]} size={52} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX color={theme.colors.red[7]} size={52} stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconFileTypeCsv color={theme.colors.gray[7]} size={52} stroke={1.5} />
</Dropzone.Idle>

<Text size="xl" inline>
Drag CSV here or click to select files
</Text>
</Group>
</Dropzone>
<Button onClick={onSubmit} disabled={slots.length === 0} loading={isPending}>
Create Slots
</Button>
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Area</Table.Th>
<Table.Th>Enter at</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{slots?.map((slot, i) => (
<Table.Tr key={i}>
<Table.Td>{slot.airspace}</Table.Td>
<Table.Td>
<Stack>
<Text>
<Pill mr="xs">CTOT</Pill>
<DateTime>{slot.enter_at}</DateTime>
</Text>
<Text>
<Pill mr="xs">TTA</Pill>
<DateTime>{slot.enter_at}</DateTime>
</Text>
</Stack>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Stack>
</Modal>
</>
);
};
1 change: 1 addition & 0 deletions Net.Vatprc.Uniapi.UI.Event/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { routeTree } from "@/routeTree.gen";
import { MantineProvider } from "@mantine/core";
import "@mantine/core/styles.css";
import "@mantine/dates/styles.css";
import "@mantine/dropzone/styles.css";
import "@mantine/notifications/styles.css";
import { QueryClientProvider } from "@tanstack/react-query";
import { RouterProvider, createRouter } from "@tanstack/react-router";
Expand Down
20 changes: 4 additions & 16 deletions Net.Vatprc.Uniapi.UI.Event/src/routes/events/$event_id.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,9 @@ import { SlotReleaseButton } from "@/components/slot-button-release";
import { CreateSlot } from "@/components/slot-create";
import { DeleteSlot } from "@/components/slot-delete";
import { SlotDetail } from "@/components/slot-detail";
import { ImportSlot } from "@/components/slot-import";
import { useUser } from "@/services/auth";
import {
ActionIcon,
Alert,
Card,
Group,
Image,
LoadingOverlay,
Pill,
Stack,
Table,
Text,
Title,
useMantineTheme,
} from "@mantine/core";
import { ActionIcon, Alert, Card, Group, Image, LoadingOverlay, Pill, Stack, Table, Text, Title } from "@mantine/core";
import { createFileRoute } from "@tanstack/react-router";

const EventComponent = () => {
Expand All @@ -35,11 +23,10 @@ const EventComponent = () => {
const { data: airspaces, isLoading: isLoadingAirspaces } = useApi("/api/events/{eid}/airspaces", {
path: { eid: event_id },
});
const theme = useMantineTheme();
const user = useUser();

const rows = slots?.map((slot) => (
<Table.Tr key={slot.id} bg={slot?.booking?.user_id === user?.id ? theme.colors.green[0] : undefined}>
<Table.Tr key={slot.id} bg={slot?.booking?.user_id === user?.id ? "green.0" : undefined}>
<Table.Td>{slot.airspace.name}</Table.Td>
<Table.Td>
<Stack gap="xs">
Expand Down Expand Up @@ -84,6 +71,7 @@ const EventComponent = () => {
<Title order={2}>
Slots
<CreateSlot ml={4} eventId={event_id} />
<ImportSlot eventId={event_id} />
</Title>
{slots?.length === 0 && <Alert title="No available slot now." />}
{(slots?.length ?? 0) > 0 && (
Expand Down

0 comments on commit 29a092b

Please sign in to comment.