From 29a092b7965b30c5a2bb313a3272614ba0d92bf3 Mon Sep 17 00:00:00 2001 From: Yuze Fu Date: Mon, 15 Jul 2024 14:47:16 +0900 Subject: [PATCH] feat(event): import slots --- Net.Vatprc.Uniapi.UI.Event/package.json | 2 + Net.Vatprc.Uniapi.UI.Event/pnpm-lock.yaml | 39 +++++ .../src/components/slot-import.tsx | 141 ++++++++++++++++++ Net.Vatprc.Uniapi.UI.Event/src/main.tsx | 1 + .../src/routes/events/$event_id.tsx | 20 +-- 5 files changed, 187 insertions(+), 16 deletions(-) create mode 100644 Net.Vatprc.Uniapi.UI.Event/src/components/slot-import.tsx diff --git a/Net.Vatprc.Uniapi.UI.Event/package.json b/Net.Vatprc.Uniapi.UI.Event/package.json index 079b701..b0943fc 100644 --- a/Net.Vatprc.Uniapi.UI.Event/package.json +++ b/Net.Vatprc.Uniapi.UI.Event/package.json @@ -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", @@ -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" diff --git a/Net.Vatprc.Uniapi.UI.Event/pnpm-lock.yaml b/Net.Vatprc.Uniapi.UI.Event/pnpm-lock.yaml index dae2f25..7a14e43 100644 --- a/Net.Vatprc.Uniapi.UI.Event/pnpm-lock.yaml +++ b/Net.Vatprc.Uniapi.UI.Event/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@mantine/dates': specifier: ^7.11.0 version: 7.11.0(@mantine/core@7.11.0(@mantine/hooks@7.11.0(react@18.2.0))(@types/react@18.2.63)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mantine/hooks@7.11.0(react@18.2.0))(dayjs@1.11.11)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@mantine/dropzone': + specifier: ^7.11.2 + version: 7.11.2(@mantine/core@7.11.0(@mantine/hooks@7.11.0(react@18.2.0))(@types/react@18.2.63)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mantine/hooks@7.11.0(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mantine/hooks': specifier: ^7.11.0 version: 7.11.0(react@18.2.0) @@ -47,6 +50,9 @@ importers: openapi-fetch: specifier: ^0.10.2 version: 0.10.2 + radash: + specifier: ^12.1.0 + version: 12.1.0 react: specifier: ^18.2.0 version: 18.2.0 @@ -626,6 +632,14 @@ packages: react: ^18.2.0 react-dom: ^18.2.0 + '@mantine/dropzone@7.11.2': + resolution: {integrity: sha512-/X29ym1uM1ca7ZMLVSSxm9KBbDNIpXBwZbzNIpopH0hxjbo2bSpPTbNUVB3EjRAVWDx9RGlyNxQV2jkhCos7QQ==} + peerDependencies: + '@mantine/core': 7.11.2 + '@mantine/hooks': 7.11.2 + react: ^18.2.0 + react-dom: ^18.2.0 + '@mantine/hooks@7.11.0': resolution: {integrity: sha512-T3472GhUXFhuhXUHlxjHv0wfb73lFyNuaw631c7Ddtgvewq0WKtNqYd7j/Zz/k02DuS3r0QXA7e12/XgqHBZjg==} peerDependencies: @@ -1834,11 +1848,21 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + radash@12.1.0: + resolution: {integrity: sha512-b0Zcf09AhqKS83btmUeYBS8tFK7XL2e3RvLmZcm0sTdF1/UUlHSsjXdCcWNxe7yfmAlPve5ym0DmKGtTzP6kVQ==} + engines: {node: '>=14.18.0'} + react-dom@18.2.0: resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: react: ^18.2.0 + react-dropzone-esm@15.0.1: + resolution: {integrity: sha512-RdeGpqwHnoV/IlDFpQji7t7pTtlC2O1i/Br0LWkRZ9hYtLyce814S71h5NolnCZXsIN5wrZId6+8eQj2EBnEzg==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8 || 18.0.0' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -2839,6 +2863,14 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + '@mantine/dropzone@7.11.2(@mantine/core@7.11.0(@mantine/hooks@7.11.0(react@18.2.0))(@types/react@18.2.63)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mantine/hooks@7.11.0(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@mantine/core': 7.11.0(@mantine/hooks@7.11.0(react@18.2.0))(@types/react@18.2.63)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@mantine/hooks': 7.11.0(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-dropzone-esm: 15.0.1(react@18.2.0) + '@mantine/hooks@7.11.0(react@18.2.0)': dependencies: react: 18.2.0 @@ -4222,12 +4254,19 @@ snapshots: queue-microtask@1.2.3: {} + radash@12.1.0: {} + react-dom@18.2.0(react@18.2.0): dependencies: loose-envify: 1.4.0 react: 18.2.0 scheduler: 0.23.0 + react-dropzone-esm@15.0.1(react@18.2.0): + dependencies: + prop-types: 15.8.1 + react: 18.2.0 + react-is@16.13.1: {} react-number-format@5.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): diff --git a/Net.Vatprc.Uniapi.UI.Event/src/components/slot-import.tsx b/Net.Vatprc.Uniapi.UI.Event/src/components/slot-import.tsx new file mode 100644 index 0000000..96f1304 --- /dev/null +++ b/Net.Vatprc.Uniapi.UI.Event/src/components/slot-import.tsx @@ -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([]); + + 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 ( + <> + + + + + + wrapPromiseWithToast(onDrop(f))}> + + + + + + + + + + + + + Drag CSV here or click to select files + + + + + + + + Area + Enter at + + + + {slots?.map((slot, i) => ( + + {slot.airspace} + + + + CTOT + {slot.enter_at} + + + TTA + {slot.enter_at} + + + + + ))} + +
+
+
+ + ); +}; diff --git a/Net.Vatprc.Uniapi.UI.Event/src/main.tsx b/Net.Vatprc.Uniapi.UI.Event/src/main.tsx index 5725136..9e03787 100644 --- a/Net.Vatprc.Uniapi.UI.Event/src/main.tsx +++ b/Net.Vatprc.Uniapi.UI.Event/src/main.tsx @@ -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"; diff --git a/Net.Vatprc.Uniapi.UI.Event/src/routes/events/$event_id.tsx b/Net.Vatprc.Uniapi.UI.Event/src/routes/events/$event_id.tsx index b162fc0..00f2855 100644 --- a/Net.Vatprc.Uniapi.UI.Event/src/routes/events/$event_id.tsx +++ b/Net.Vatprc.Uniapi.UI.Event/src/routes/events/$event_id.tsx @@ -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 = () => { @@ -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) => ( - + {slot.airspace.name} @@ -84,6 +71,7 @@ const EventComponent = () => { Slots <CreateSlot ml={4} eventId={event_id} /> + <ImportSlot eventId={event_id} /> {slots?.length === 0 && } {(slots?.length ?? 0) > 0 && (