Implement aircraft values in flight creation/updating

This commit is contained in:
april 2024-01-11 11:30:10 -06:00
parent d6d03c9027
commit b50d333677
7 changed files with 449 additions and 215 deletions

View File

@ -1,6 +1,7 @@
import ErrorDisplay from "@/ui/error-display"; import ErrorDisplay from "@/ui/error-display";
import AircraftForm from "@/ui/form/aircraft-form"; import AircraftForm from "@/ui/form/aircraft-form";
import { useApi } from "@/util/api"; import { useApi } from "@/util/api";
import { useAircraft } from "@/util/hooks";
import { AircraftFormSchema, AircraftSchema } from "@/util/types"; import { AircraftFormSchema, AircraftSchema } from "@/util/types";
import { import {
ActionIcon, ActionIcon,
@ -11,37 +12,19 @@ import {
Group, Group,
Loader, Loader,
Modal, Modal,
NumberInput,
ScrollArea, ScrollArea,
Select,
Stack, Stack,
Text, Text,
TextInput,
Title, Title,
Tooltip, Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { useForm } from "@mantine/form";
import { randomId, useDisclosure } from "@mantine/hooks"; import { randomId, useDisclosure } from "@mantine/hooks";
import { IconPencil, IconPlus, IconTrash, IconX } from "@tabler/icons-react"; import { IconPencil, IconPlus, IconTrash, IconX } from "@tabler/icons-react";
import { import {
UseQueryResult, UseQueryResult,
useMutation, useMutation,
useQuery,
useQueryClient, useQueryClient,
} from "@tanstack/react-query"; } 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 }) { function AircraftCard({ aircraft }: { aircraft: AircraftSchema }) {
const [deleteOpened, { open: openDelete, close: closeDelete }] = const [deleteOpened, { open: openDelete, close: closeDelete }] =

View File

@ -1,4 +1,4 @@
import { Container, Stack, Title } from "@mantine/core"; import { Center, Container, Loader, Stack, Title } from "@mantine/core";
import { import {
FlightFormSchema, FlightFormSchema,
flightCreateHelper, flightCreateHelper,
@ -9,6 +9,7 @@ import { useApi } from "@/util/api";
import { useNavigate, useParams } from "@remix-run/react"; import { useNavigate, useParams } from "@remix-run/react";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import FlightForm from "@/ui/form/flight-form"; import FlightForm from "@/ui/form/flight-form";
import ErrorDisplay from "@/ui/error-display";
export default function EditFlight() { export default function EditFlight() {
const params = useParams(); const params = useParams();
@ -47,16 +48,30 @@ export default function EditFlight() {
<Stack> <Stack>
<Title order={2}>Edit Flight</Title> <Title order={2}>Edit Flight</Title>
<FlightForm {flight.isLoading ? (
initialValues={flightEditHelper(flight.data) ?? null} <Center h="calc(100vh - 95px - 110px)">
onSubmit={editFlight.mutate} <Loader />
isError={editFlight.isError} </Center>
error={editFlight.error} ) : flight.isError ? (
submitButtonLabel="Update" <Center h="calc(100vh - 95px - 110px)">
withCancelButton <ErrorDisplay error={flight.error.message} />
cancelFunc={() => navigate(`/logbook/flights/${params.id}`)} </Center>
mah="calc(100vh - 95px - 110px)" ) : (
/> <FlightForm
initialValues={
flight.data ? flightEditHelper(flight.data) ?? null : null
}
onSubmit={editFlight.mutate}
isPending={editFlight.isPending}
isError={editFlight.isError}
error={editFlight.error}
submitButtonLabel="Update"
withCancelButton
cancelFunc={() => navigate(`/logbook/flights/${params.id}`)}
mah="calc(100vh - 95px - 110px)"
autofillHobbs={false}
/>
)}
</Stack> </Stack>
</Container> </Container>
); );

View File

@ -33,9 +33,11 @@ export default function NewFlight() {
<FlightForm <FlightForm
onSubmit={createFlight.mutate} onSubmit={createFlight.mutate}
isPending={createFlight.isPending}
isError={createFlight.isError} isError={createFlight.isError}
error={createFlight.error} error={createFlight.error}
mah="calc(100vh - 95px - 110px)" mah="calc(100vh - 95px - 110px)"
autofillHobbs
/> />
</Stack> </Stack>
</Container> </Container>

View File

@ -47,7 +47,7 @@ export default function Index() {
staleTime: 1000, staleTime: 1000,
retry: (failureCount, error: Error) => { retry: (failureCount, error: Error) => {
return ( return (
failureCount < 5 && failureCount < 3 &&
(!error || (!error ||
(error instanceof AxiosError && (error instanceof AxiosError &&
error.response?.status !== 401 && error.response?.status !== 401 &&

View File

@ -12,7 +12,7 @@ import {
} from "@mantine/core"; } from "@mantine/core";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import { IconPencil, IconX } from "@tabler/icons-react"; import { IconPencil, IconX } from "@tabler/icons-react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { useState } from "react"; import { useState } from "react";

View File

@ -1,15 +1,23 @@
import { FlightFormSchema } from "@/util/types";
import { import {
AircraftFormSchema,
AircraftSchema,
FlightFormSchema,
} from "@/util/types";
import {
ActionIcon,
Button, Button,
CloseButton, CloseButton,
Container, Container,
Fieldset, Fieldset,
Group, Group,
Modal,
NumberInput, NumberInput,
ScrollArea, ScrollArea,
Select,
Text, Text,
TextInput, TextInput,
Textarea, Textarea,
Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { DatePickerInput } from "@mantine/dates"; import { DatePickerInput } from "@mantine/dates";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
@ -18,11 +26,18 @@ import { HourInput, ZeroHourInput } from "./hour-input";
import TimeInput from "./time-input"; import TimeInput from "./time-input";
import { ZeroIntInput } from "./int-input"; import { ZeroIntInput } from "./int-input";
import ListInput from "./list-input"; import ListInput from "./list-input";
import { IconPencil } from "@tabler/icons-react"; import { IconPencil, IconPlaneTilt, IconPlus } from "@tabler/icons-react";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { useDisclosure } from "@mantine/hooks";
import AircraftForm from "./aircraft-form";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useApi } from "@/util/api";
import { useAircraft } from "@/util/hooks";
import { useEffect, useState } from "react";
export default function FlightForm({ export default function FlightForm({
onSubmit, onSubmit,
isPending,
isError, isError,
error, error,
initialValues, initialValues,
@ -30,8 +45,10 @@ export default function FlightForm({
submitButtonLabel, submitButtonLabel,
withCancelButton, withCancelButton,
cancelFunc, cancelFunc,
autofillHobbs = false,
}: { }: {
onSubmit: (values: FlightFormSchema) => void; onSubmit: (values: FlightFormSchema) => void;
isPending: boolean;
isError: boolean; isError: boolean;
error: Error | null; error: Error | null;
initialValues?: FlightFormSchema | null; initialValues?: FlightFormSchema | null;
@ -39,6 +56,7 @@ export default function FlightForm({
submitButtonLabel?: string; submitButtonLabel?: string;
withCancelButton?: boolean; withCancelButton?: boolean;
cancelFunc?: () => void; cancelFunc?: () => void;
autofillHobbs?: boolean;
}) { }) {
const form = useForm<FlightFormSchema>({ const form = useForm<FlightFormSchema>({
initialValues: initialValues ?? { initialValues: initialValues ?? {
@ -86,196 +104,399 @@ export default function FlightForm({
}, },
}); });
const [aircraftOpened, { open: openAircraft, close: closeAircraft }] =
useDisclosure(false);
const client = useApi();
const queryClient = useQueryClient();
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();
},
});
const [aircraft, setAircraft] = useState<string | null>(
initialValues?.aircraft ?? ""
);
const [hobbsTouched, setHobbsTouched] = useState(false);
const getHobbs = useQuery({
queryKey: ["hobbs", aircraft],
queryFn: async () =>
await client.get(`/aircraft/tail/${aircraft}`).then((res) => res.data),
enabled: !!aircraft && aircraft !== "",
});
const getAircraft = useAircraft();
useEffect(() => {
if (autofillHobbs && getHobbs.isFetched && getHobbs.data && !hobbsTouched) {
form.setFieldValue(
"hobbs_start",
getHobbs.data.hobbs ?? form.getTransformedValues()["hobbs_start"]
);
}
}, [getHobbs.data]);
return ( return (
<form onSubmit={form.onSubmit((values) => onSubmit(values))}> <>
<ScrollArea.Autosize mah={mah}> <Modal
<Container> opened={aircraftOpened}
{/* Date and Aircraft */} onClose={closeAircraft}
title="New Aircraft"
centered
>
<AircraftForm
onSubmit={addAircraft.mutate}
isError={addAircraft.isError}
error={addAircraft.error}
isPending={addAircraft.isPending}
submitButtonLabel="Add"
withCancelButton
cancelFunc={closeAircraft}
/>
</Modal>
<form onSubmit={form.onSubmit((values) => onSubmit(values))}>
<ScrollArea.Autosize mah={mah}>
<Container>
{/* Date and Aircraft */}
<Fieldset> <Fieldset>
<Group justify="center" grow> <Group justify="center" grow>
<DatePickerInput label="Date" {...form.getInputProps("date")} /> <DatePickerInput
<TextInput label="Aircraft" {...form.getInputProps("aircraft")} /> label="Date"
</Group> {...form.getInputProps("date")}
</Fieldset> withAsterisk
/>
<Select
label={
<Group gap="0">
<Text size="sm" fw={700} span>
Aircraft
</Text>
<Text
pl="0.3rem"
style={{ color: "var(--mantine-color-error)" }}
span
>
*
</Text>
{/* Route */} <Tooltip label="Add Aircraft">
<ActionIcon
variant="transparent"
onClick={openAircraft}
>
<IconPlus size="1rem" />
</ActionIcon>
</Tooltip>
</Group>
}
data={
getAircraft.isFetched
? getAircraft.data?.map((item: AircraftSchema) => ({
value: item.tail_no,
label: item.tail_no,
}))
: initialValues
? [
{
value: initialValues?.aircraft,
label: initialValues?.aircraft,
},
]
: null
}
value={aircraft}
{...form.getInputProps("aircraft")}
onChange={(_value, option) => {
form.setFieldValue("aircraft", option.label);
setAircraft(option.label);
queryClient.invalidateQueries({
queryKey: ["hobbs", aircraft],
});
}}
/>
</Group>
</Fieldset>
<Fieldset legend="Route" mt="lg"> {/* Route */}
<Group justify="center" grow>
<Fieldset legend="Route" mt="lg">
<Group justify="center" grow>
<TextInput
label="Waypoint From"
{...form.getInputProps("waypoint_from")}
/>
<TextInput
label="Waypoint To"
{...form.getInputProps("waypoint_to")}
/>
</Group>
<TextInput <TextInput
label="Waypoint From" label="Route"
{...form.getInputProps("waypoint_from")} {...form.getInputProps("route")}
mt="md"
/> />
<TextInput </Fieldset>
label="Waypoint To"
{...form.getInputProps("waypoint_to")} {/* Times */}
<Fieldset legend="Times" mt="md">
<Group justify="center" grow>
<NumberInput
label={
<Group gap="0">
<Text size="sm" fw={700} span>
Hobbs Start
</Text>
<Tooltip
label={
getHobbs.isFetched &&
getHobbs.data &&
getHobbs.data.hobbs ===
form.getTransformedValues()["hobbs_start"]
? "Using aircraft time"
: "Use Aircraft Time"
}
>
<ActionIcon
variant="transparent"
disabled={
!(
getHobbs.isFetched &&
getHobbs.data &&
getHobbs.data.hobbs !==
form.getTransformedValues()["hobbs_start"]
)
}
style={
!(
getHobbs.isFetched &&
getHobbs.data &&
getHobbs.data.hobbs !==
form.getTransformedValues()["hobbs_start"]
)
? { backgroundColor: "transparent" }
: {}
}
onClick={() =>
form.setFieldValue(
"hobbs_start",
getHobbs.data?.hobbs ?? 0.0
)
}
>
<IconPlaneTilt size="1rem" />
</ActionIcon>
</Tooltip>
</Group>
}
decimalScale={1}
step={0.1}
min={0}
fixedDecimalScale
leftSection={
<CloseButton
aria-label="Clear input"
onClick={() => form.setFieldValue("hobbs_start", "")}
style={{
display:
["", null].indexOf(
form.getTransformedValues()["hobbs_start"]
) > -1
? "none"
: undefined,
}}
/>
}
{...form.getInputProps("hobbs_start")}
onChange={(e) => {
form.setFieldValue("hobbs_start", e);
setHobbsTouched(true);
}}
/>
<HourInput form={form} field="hobbs_end" label="Hobbs End" />
</Group>
</Fieldset>
{/* Start/Stop */}
<Fieldset legend="Start/Stop" mt="md">
<Group justify="center" grow>
<TimeInput form={form} field="time_start" label="Start Time" />
<TimeInput form={form} field="time_off" label="Time Off" />
</Group>
<Group justify="center" grow mt="md">
<TimeInput form={form} field="time_down" label="Time Down" />
<TimeInput form={form} field="time_stop" label="Stop Time" />
</Group>
</Fieldset>
{/* Hours */}
<Fieldset legend="Hours" mt="md">
<Group justify="center" grow>
<ZeroHourInput
form={form}
field="time_total"
label="Time Total"
/>
<ZeroHourInput form={form} field="time_pic" label="Time PIC" />
<ZeroHourInput form={form} field="time_sic" label="Time SIC" />
</Group>
<Group justify="center" grow mt="md">
<ZeroHourInput
form={form}
field="time_night"
label="Time Night"
/>
<ZeroHourInput
form={form}
field="time_solo"
label="Time Solo"
/>
</Group>
</Fieldset>
{/* Cross-Country */}
<Fieldset legend="Cross-Country" mt="md">
<Group justify="center" grow>
<ZeroHourInput form={form} field="time_xc" label="Hours" />
<NumberInput
label="Distance"
decimalScale={1}
min={0}
fixedDecimalScale
leftSection={
<CloseButton
aria-label="Clear input"
onClick={() => form.setFieldValue("dist_xc", 0)}
style={{
display:
form.getTransformedValues().dist_xc == 0
? "none"
: undefined,
}}
/>
}
{...form.getInputProps("dist_xc")}
/>
</Group>
</Fieldset>
{/* Landings */}
<Fieldset legend="Landings" mt="md">
<Group justify="center" grow>
<ZeroIntInput form={form} field="landings_day" label="Day" />
<ZeroIntInput
form={form}
field="landings_night"
label="Night"
/>
</Group>
</Fieldset>
{/* Instrument */}
<Fieldset legend="Instrument" mt="md">
<Group justify="center" grow>
<ZeroHourInput
form={form}
field="time_instrument"
label="Time Instrument"
/>
<ZeroHourInput
form={form}
field="time_sim_instrument"
label="Time Sim Instrument"
/>
<ZeroIntInput
form={form}
field="holds_instrument"
label="Instrument Holds"
/>
</Group>
</Fieldset>
{/* Instruction */}
<Fieldset legend="Instruction" mt="md">
<Group justify="center" grow>
<ZeroHourInput
form={form}
field="dual_given"
label="Dual Given"
/>
<ZeroHourInput
form={form}
field="dual_recvd"
label="Dual Received"
/>
<ZeroHourInput form={form} field="time_sim" label="Sim Time" />
<ZeroHourInput
form={form}
field="time_ground"
label="Ground Time"
/>
</Group>
</Fieldset>
{/* About the Flight */}
<Fieldset legend="About" mt="md">
<ListInput form={form} field="tags" label="Tags" />
<Group justify="center" grow mt="md">
<ListInput form={form} field="pax" label="Pax" />
<ListInput form={form} field="crew" label="Crew" />
</Group>
<Textarea
label="Comments"
mt="md"
autosize
minRows={4}
{...form.getInputProps("comments")}
/> />
</Group> </Fieldset>
<TextInput label="Route" {...form.getInputProps("route")} mt="md" /> </Container>
</Fieldset> </ScrollArea.Autosize>
{/* Times */} <Group justify="flex-end" mt="md">
{isPending ? (
<Fieldset legend="Times" mt="md"> <Text c="yellow" fw={700}>
<Group justify="center" grow> Loading...
<HourInput form={form} field="hobbs_start" label="Hobbs Start" /> </Text>
<HourInput form={form} field="hobbs_end" label="Hobbs End" /> ) : isError ? (
</Group> <Text c="red" fw={700}>
</Fieldset> {error instanceof AxiosError
? error.response?.data.detail
{/* Start/Stop */} : error?.message}
</Text>
<Fieldset legend="Start/Stop" mt="md"> ) : null}
<Group justify="center" grow> {withCancelButton ? (
<TimeInput form={form} field="time_start" label="Start Time" /> <Button onClick={cancelFunc} color="gray">
<TimeInput form={form} field="time_off" label="Time Off" /> Cancel
</Group> </Button>
<Group justify="center" grow mt="md"> ) : null}
<TimeInput form={form} field="time_down" label="Time Down" /> <Button type="submit" leftSection={<IconPencil />}>
<TimeInput form={form} field="time_stop" label="Stop Time" /> {submitButtonLabel ?? "Create"}
</Group>
</Fieldset>
{/* Hours */}
<Fieldset legend="Hours" mt="md">
<Group justify="center" grow>
<ZeroHourInput
form={form}
field="time_total"
label="Time Total"
/>
<ZeroHourInput form={form} field="time_pic" label="Time PIC" />
<ZeroHourInput form={form} field="time_sic" label="Time SIC" />
</Group>
<Group justify="center" grow mt="md">
<ZeroHourInput
form={form}
field="time_night"
label="Time Night"
/>
<ZeroHourInput form={form} field="time_solo" label="Time Solo" />
</Group>
</Fieldset>
{/* Cross-Country */}
<Fieldset legend="Cross-Country" mt="md">
<Group justify="center" grow>
<ZeroHourInput form={form} field="time_xc" label="Hours" />
<NumberInput
label="Distance"
decimalScale={1}
min={0}
fixedDecimalScale
leftSection={
<CloseButton
aria-label="Clear input"
onClick={() => form.setFieldValue("dist_xc", 0)}
style={{
display:
form.getTransformedValues().dist_xc == 0
? "none"
: undefined,
}}
/>
}
{...form.getInputProps("dist_xc")}
/>
</Group>
</Fieldset>
{/* Landings */}
<Fieldset legend="Landings" mt="md">
<Group justify="center" grow>
<ZeroIntInput form={form} field="landings_day" label="Day" />
<ZeroIntInput form={form} field="landings_night" label="Night" />
</Group>
</Fieldset>
{/* Instrument */}
<Fieldset legend="Instrument" mt="md">
<Group justify="center" grow>
<ZeroHourInput
form={form}
field="time_instrument"
label="Time Instrument"
/>
<ZeroHourInput
form={form}
field="time_sim_instrument"
label="Time Sim Instrument"
/>
<ZeroIntInput
form={form}
field="holds_instrument"
label="Instrument Holds"
/>
</Group>
</Fieldset>
{/* Instruction */}
<Fieldset legend="Instruction" mt="md">
<Group justify="center" grow>
<ZeroHourInput
form={form}
field="dual_given"
label="Dual Given"
/>
<ZeroHourInput
form={form}
field="dual_recvd"
label="Dual Received"
/>
<ZeroHourInput form={form} field="time_sim" label="Sim Time" />
<ZeroHourInput
form={form}
field="time_ground"
label="Ground Time"
/>
</Group>
</Fieldset>
{/* About the Flight */}
<Fieldset legend="About" mt="md">
<ListInput form={form} field="tags" label="Tags" />
<Group justify="center" grow mt="md">
<ListInput form={form} field="pax" label="Pax" />
<ListInput form={form} field="crew" label="Crew" />
</Group>
<Textarea
label="Comments"
mt="md"
autosize
minRows={4}
{...form.getInputProps("comments")}
/>
</Fieldset>
</Container>
</ScrollArea.Autosize>
<Group justify="flex-end" mt="md">
{isError ? (
<Text c="red" fw={700}>
{error instanceof AxiosError
? error.response?.data.detail
: error?.message}
</Text>
) : null}
{withCancelButton ? (
<Button onClick={cancelFunc} color="gray">
Cancel
</Button> </Button>
) : null} </Group>
<Button type="submit" leftSection={<IconPencil />}> </form>
{submitButtonLabel ?? "Create"} </>
</Button>
</Group>
</form>
); );
} }

13
web/app/util/hooks.ts Normal file
View File

@ -0,0 +1,13 @@
import { useQuery } from "@tanstack/react-query";
import { useApi } from "./api";
export function useAircraft() {
const client = useApi();
const aircraft = useQuery({
queryKey: ["aircraft-list"],
queryFn: async () => await client.get(`/aircraft`).then((res) => res.data),
});
return aircraft;
}