diff --git a/web/app/routes/logbook.aircraft/route.tsx b/web/app/routes/logbook.aircraft/route.tsx new file mode 100644 index 0000000..1aa57b2 --- /dev/null +++ b/web/app/routes/logbook.aircraft/route.tsx @@ -0,0 +1,321 @@ +import ErrorDisplay from "@/ui/error-display"; +import { useApi } from "@/util/api"; +import { AircraftFormSchema, AircraftSchema } from "@/util/types"; +import { + ActionIcon, + Button, + Card, + Center, + Container, + Group, + Loader, + Modal, + NumberInput, + ScrollArea, + Select, + Stack, + Text, + TextInput, + Title, + Tooltip, +} from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { randomId, useDisclosure } from "@mantine/hooks"; +import { IconPencil, IconPlus, IconTrash, IconX } from "@tabler/icons-react"; +import { + UseQueryResult, + useMutation, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; +import { AxiosError } from "axios"; +import { useState } from "react"; + +function useAircraft() { + const client = useApi(); + + const aircraft = useQuery({ + queryKey: ["aircraft-list"], + queryFn: async () => await client.get(`/aircraft`).then((res) => res.data), + }); + + return aircraft; +} + +function AircraftCard({ aircraft }: { aircraft: AircraftSchema }) { + const [deleteOpened, { open: openDelete, close: closeDelete }] = + useDisclosure(false); + + const client = useApi(); + const queryClient = useQueryClient(); + + const deleteAircraft = useMutation({ + mutationFn: async () => + await client.delete(`/aircraft/${aircraft.id}`).then((res) => res.data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["aircraft-list"] }); + }, + }); + + return ( + <> + + + + Are you sure you want to delete this aircraft? This action cannot be + undone. + + {deleteAircraft.isError ? ( + + {deleteAircraft.error.message} + + ) : null} + + + + + + + + + + {aircraft.tail_no} + + + + + + + + + + + {aircraft.make} + {aircraft.model} + + + {aircraft.aircraft_category} + / + {aircraft.aircraft_class} + + {aircraft.hobbs ? Hobbs: {aircraft.hobbs} : null} + + + + ); +} + +function NewAircraftModal({ + opened, + close, +}: { + opened: boolean; + close: () => void; +}) { + const newForm = useForm({ + initialValues: { + tail_no: "", + make: "", + model: "", + aircraft_category: "", + aircraft_class: "", + hobbs: 0.0, + }, + validate: { + tail_no: (value) => + value === null || value.trim() === "" + ? "Please enter a tail number" + : null, + make: (value) => + value === null || value.trim() === "" ? "Please enter a make" : null, + model: (value) => + value === null || value.trim() === "" ? "Please enter a model" : null, + aircraft_category: (value) => + value === null || value.trim() === "" + ? "Please select a category" + : null, + aircraft_class: (value) => + value === null || value.trim() === "" ? "Please select a class" : null, + }, + }); + + const client = useApi(); + const queryClient = useQueryClient(); + + const categories = useQuery({ + queryKey: ["categories"], + queryFn: async () => + await client.get(`/aircraft/categories`).then((res) => res.data), + }); + + const [category, setCategory] = useState(""); + const [classSelection, setClassSelection] = useState(""); + + const classes = useQuery({ + queryKey: ["classes", category], + queryFn: async () => + await client + .get(`/aircraft/class?category=${category}`) + .then((res) => res.data), + enabled: !!category, + }); + + const addAircraft = useMutation({ + mutationFn: async (values: AircraftFormSchema) => { + const newAircraft = values; + if (newAircraft) { + const res = await client.post("/aircraft", newAircraft); + return res.data; + } + throw new Error("Aircraft creation failed"); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["aircraft-list"] }); + close(); + }, + }); + + return ( + +
addAircraft.mutate(values))}> + + + + + + + + + {addAircraft.isError ? ( + + {(addAircraft.error as AxiosError)?.response?.data?.detail ?? + "Error adding aircraft"} + + ) : addAircraft.isPending ? ( + Adding aircraft... + ) : null} + + + + +
+
+ ); +} + +export default function Aircraft() { + const aircraft: UseQueryResult = useAircraft(); + + const [newOpened, { open: openNew, close: closeNew }] = useDisclosure(false); + + return ( + <> + + + + Aircraft + + + + + + + + + + {aircraft.isLoading ? ( +
+ +
+ ) : aircraft.isError ? ( +
+ +
+ ) : aircraft.data && aircraft.data.length === 0 ? ( +
+ + + No Aircraft + +
+ ) : ( + + {aircraft.data?.map((item) => ( + + ))} + + )} +
+
+ + ); +} diff --git a/web/app/routes/logbook.flights.$id/route.tsx b/web/app/routes/logbook.flights.$id/route.tsx index d18961c..c589e79 100644 --- a/web/app/routes/logbook.flights.$id/route.tsx +++ b/web/app/routes/logbook.flights.$id/route.tsx @@ -88,11 +88,9 @@ export default function Flight() { ) : flight.data ? ( <> - - - Flight Log - - + + Flight Log + ) : null} - {log.hobbs_start || - log.hobbs_end || - log.tach_start || - log.tach_end ? ( + {log.hobbs_start || log.hobbs_end ? ( - {log.hobbs_start || log.hobbs_end ? ( - - - - - ) : null} - {log.tach_start || log.tach_end ? ( - - - - - ) : null} + + + + ) : null} {log.time_start || diff --git a/web/app/routes/logbook.flights.new/route.tsx b/web/app/routes/logbook.flights.new/route.tsx index 0f38997..c9a0161 100644 --- a/web/app/routes/logbook.flights.new/route.tsx +++ b/web/app/routes/logbook.flights.new/route.tsx @@ -3,7 +3,6 @@ import { FlightFormSchema, flightCreateHelper } from "@/util/types"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useApi } from "@/util/api"; import { useNavigate } from "@remix-run/react"; -import { AxiosError } from "axios"; import FlightForm from "@/ui/form/flight-form"; export default function NewFlight() { @@ -21,9 +20,6 @@ export default function NewFlight() { } throw new Error("Flight creation failed"); }, - retry: (failureCount, error: AxiosError) => { - return !error || error.response?.status !== 401; - }, onSuccess: async (data: { id: string }) => { await queryClient.invalidateQueries({ queryKey: ["flights-list"] }); navigate(`/logbook/flights/${data.id}`); diff --git a/web/app/routes/logbook.me/route.tsx b/web/app/routes/logbook.me/route.tsx index 1174b7d..ffc7eb4 100644 --- a/web/app/routes/logbook.me/route.tsx +++ b/web/app/routes/logbook.me/route.tsx @@ -31,11 +31,16 @@ export default function Me() { current_psk: string; new_psk: string; confirm_new_psk: string; - }) => + }) => { + console.log({ + current_password: values.current_psk, + new_password: values.new_psk, + }); await client.put(`/users/me/password`, { current_password: values.current_psk, new_password: values.new_psk, - }), + }); + }, }); const updatePskForm = useForm({ @@ -95,14 +100,16 @@ export default function Me() { {...updatePskForm.getInputProps("current_psk")} /> - + {updatePassword.isPending ? ( Updating... ) : updatePassword.isError ? ( diff --git a/web/app/routes/logbook/route.tsx b/web/app/routes/logbook/route.tsx index d76396d..ea5dd8b 100644 --- a/web/app/routes/logbook/route.tsx +++ b/web/app/routes/logbook/route.tsx @@ -1,7 +1,7 @@ import { TailfinAppShell } from "@/ui/nav/app-shell"; import { useAuth } from "@/util/auth"; import type { MetaFunction } from "@remix-run/node"; -import { Outlet, useNavigate } from "@remix-run/react"; +import { Outlet, useLocation, useNavigate } from "@remix-run/react"; import { QueryCache, QueryClient, @@ -22,13 +22,15 @@ export default function Index() { const { user, loading } = useAuth(); const navigate = useNavigate(); + const location = useLocation(); + useEffect(() => { if (!loading && !user) { navigate("/login"); - } else { + } else if (location.pathname === "/logbook") { navigate("/logbook/dashboard"); } - }, [user, loading, navigate]); + }, [user, loading, navigate, location]); const [queryClient] = useState( () => diff --git a/web/app/ui/form/flight-form.tsx b/web/app/ui/form/flight-form.tsx index fee87a7..1b04880 100644 --- a/web/app/ui/form/flight-form.tsx +++ b/web/app/ui/form/flight-form.tsx @@ -50,8 +50,6 @@ export default function FlightForm({ hobbs_start: null, hobbs_end: null, - tach_start: null, - tach_end: null, time_start: null, time_off: null, @@ -124,10 +122,6 @@ export default function FlightForm({ - - - - {/* Start/Stop */} diff --git a/web/app/ui/nav/navbar.tsx b/web/app/ui/nav/navbar.tsx index 4766a9f..b862a59 100644 --- a/web/app/ui/nav/navbar.tsx +++ b/web/app/ui/nav/navbar.tsx @@ -3,8 +3,9 @@ import { Stack, NavLink } from "@mantine/core"; import { Link, useLocation } from "@remix-run/react"; import { IconBook2, + IconDashboard, IconLogout, - IconPlaneDeparture, + IconPlaneTilt, IconUser, } from "@tabler/icons-react"; @@ -28,7 +29,7 @@ export default function Navbar({ component={Link} to="/logbook/dashboard" label="Dashboard" - leftSection={} + leftSection={} active={page == "dashboard"} onClick={() => (opened ? toggle() : null)} /> @@ -41,6 +42,15 @@ export default function Navbar({ active={page === "flights"} onClick={() => (opened ? toggle() : null)} /> + } + active={page === "aircraft"} + onClick={() => (opened ? toggle() : null)} + />