Implement flight deletion

This commit is contained in:
april 2024-01-05 14:54:44 -06:00
parent 40108d1070
commit 654393bab8
6 changed files with 356 additions and 275 deletions

View File

@ -37,6 +37,7 @@ import {
Stack, Stack,
Title, Title,
} from "@mantine/core"; } from "@mantine/core";
import { ModalsProvider } from "@mantine/modals";
import { IconRocket } from "@tabler/icons-react"; import { IconRocket } from "@tabler/icons-react";
import { AuthProvider } from "./util/auth"; import { AuthProvider } from "./util/auth";
@ -159,12 +160,14 @@ export default function App() {
<HydrationBoundary state={dehydratedState}> <HydrationBoundary state={dehydratedState}>
<ApiProvider apiUrl={data.ENV.TAILFIN_API_URL}> <ApiProvider apiUrl={data.ENV.TAILFIN_API_URL}>
<MantineProvider theme={{ primaryColor: "violet" }}> <MantineProvider theme={{ primaryColor: "violet" }}>
<AuthProvider> <ModalsProvider>
<Outlet /> <AuthProvider>
<ScrollRestoration /> <Outlet />
<Scripts /> <ScrollRestoration />
<LiveReload /> <Scripts />
</AuthProvider> <LiveReload />
</AuthProvider>
</ModalsProvider>
</MantineProvider> </MantineProvider>
</ApiProvider> </ApiProvider>
<ReactQueryDevtools initialIsOpen={false} /> <ReactQueryDevtools initialIsOpen={false} />

View File

@ -2,21 +2,31 @@ import { VerticalLogItem } from "@/ui/display/log-item";
import ErrorDisplay from "@/ui/error-display"; import ErrorDisplay from "@/ui/error-display";
import { useApi } from "@/util/api"; import { useApi } from "@/util/api";
import { import {
ActionIcon,
Center, Center,
Container, Container,
Grid, Grid,
Group,
Loader, Loader,
ScrollArea, ScrollArea,
Stack, Stack,
Title, Title,
Tooltip,
Text,
Modal,
Button,
} from "@mantine/core"; } from "@mantine/core";
import { useParams } from "@remix-run/react"; import { useDisclosure } from "@mantine/hooks";
import { useQuery } from "@tanstack/react-query"; import { modals } from "@mantine/modals";
import { useNavigate, useParams } from "@remix-run/react";
import { IconPencil, IconTrash } from "@tabler/icons-react";
import { useMutation, useQuery } from "@tanstack/react-query";
export default function Flight() { export default function Flight() {
const params = useParams(); const params = useParams();
const client = useApi(); const client = useApi();
const navigate = useNavigate();
const flight = useQuery({ const flight = useQuery({
queryKey: [params.id], queryKey: [params.id],
@ -24,241 +34,302 @@ export default function Flight() {
await client.get(`/flights/${params.id}`).then((res) => res.data), await client.get(`/flights/${params.id}`).then((res) => res.data),
}); });
const [deleteOpened, { open: openDelete, close: closeDelete }] =
useDisclosure(false);
const deleteFlight = useMutation({
mutationFn: async () =>
await client.delete(`/flights/${params.id}`).then((res) => res.data),
onSuccess: () => {
navigate("/logbook/flights");
},
});
const log = flight.data; const log = flight.data;
return ( return (
// <Container> <>
<Stack h="calc(100vh-95px)"> <Modal
{flight.isError ? ( opened={deleteOpened}
<Center h="calc(100vh - 95px)"> onClose={closeDelete}
<ErrorDisplay error="Error Fetching Flight" /> title="Delete Flight?"
</Center> centered
) : flight.isPending ? ( >
<Center h="calc(100vh - 95px)"> <Stack>
<Loader /> <Text>
</Center> Are you sure you want to delete this flight? This action cannot be
) : flight.data ? ( undone.
<> </Text>
<Title order={3} py="lg" style={{ textAlign: "center" }}> {deleteFlight.isError ? (
Flight Log <Text c="red" fw={700}>
</Title> {deleteFlight.error.message}
<ScrollArea h="calc(100vh - 95px - 110px)" m="0" p="0"> </Text>
<Container h="100%"> ) : null}
<Grid justify="center"> <Group justify="flex-end">
<Grid.Col span={6}> <Button color="red" onClick={() => deleteFlight.mutate()}>
<VerticalLogItem label="Date" content={log.date} date /> Delete
</Grid.Col> </Button>
<Grid.Col span={6}> <Button color="gray" onClick={closeDelete}>
<VerticalLogItem label="Aircraft" content={log.aircraft} /> Cancel
</Grid.Col> </Button>
{log.waypoint_from || log.waypoint_to ? ( </Group>
<> </Stack>
</Modal>
<Container>
<Stack h="calc(100vh-95px)">
{flight.isError ? (
<Center h="calc(100vh - 95px)">
<ErrorDisplay error="Error Fetching Flight" />
</Center>
) : flight.isPending ? (
<Center h="calc(100vh - 95px)">
<Loader />
</Center>
) : flight.data ? (
<>
<Group justify="space-between" px="xl">
<Title order={2} py="lg" style={{ textAlign: "center" }}>
Flight Log
</Title>
<Group>
<Tooltip label="Edit Flight">
<ActionIcon variant="subtle" size="lg">
<IconPencil />
</ActionIcon>
</Tooltip>
<Tooltip label="Delete Flight">
<ActionIcon
variant="subtle"
size="lg"
color="red"
onClick={openDelete}
>
<IconTrash />
</ActionIcon>
</Tooltip>
</Group>
</Group>
<ScrollArea h="calc(100vh - 95px - 110px)" m="0" p="0">
<Container h="100%">
<Grid justify="center">
<Grid.Col span={6}> <Grid.Col span={6}>
<VerticalLogItem <VerticalLogItem label="Date" content={log.date} date />
label="Waypoint From"
content={log.waypoint_from}
/>
</Grid.Col> </Grid.Col>
<Grid.Col span={6}> <Grid.Col span={6}>
<VerticalLogItem <VerticalLogItem
label="Waypoint To" label="Aircraft"
content={log.waypoint_to} content={log.aircraft}
/> />
</Grid.Col> </Grid.Col>
</> {log.waypoint_from || log.waypoint_to ? (
) : null} <>
{log.route ? ( <Grid.Col span={6}>
<> <VerticalLogItem
<Grid.Col span={12}> label="Waypoint From"
<VerticalLogItem label="Route" content={log.route} /> content={log.waypoint_from}
</Grid.Col> />
</> </Grid.Col>
) : null} <Grid.Col span={6}>
{log.hobbs_start || log.hobbs_end ? ( <VerticalLogItem
<> label="Waypoint To"
<Grid.Col span={6}> content={log.waypoint_to}
<VerticalLogItem />
label="Hobbs Start" </Grid.Col>
content={log.hobbs_start} </>
hours ) : null}
/> {log.route ? (
</Grid.Col> <>
<Grid.Col span={6}> <Grid.Col span={12}>
<VerticalLogItem <VerticalLogItem label="Route" content={log.route} />
label="Hobbs End" </Grid.Col>
content={log.hobbs_end} </>
hours ) : null}
/> {log.hobbs_start || log.hobbs_end ? (
</Grid.Col> <>
</> <Grid.Col span={6}>
) : null} <VerticalLogItem
{log.tach_start || log.tach_end ? ( label="Hobbs Start"
<> content={log.hobbs_start}
<Grid.Col span={6}> hours
<VerticalLogItem />
label="Tach Start" </Grid.Col>
content={log.tach_start} <Grid.Col span={6}>
hours <VerticalLogItem
/> label="Hobbs End"
</Grid.Col> content={log.hobbs_end}
<Grid.Col span={6}> hours
<VerticalLogItem />
label="Tach End" </Grid.Col>
content={log.tach_end} </>
hours ) : null}
/> {log.tach_start || log.tach_end ? (
</Grid.Col> <>
</> <Grid.Col span={6}>
) : null} <VerticalLogItem
{log.time_start || log.time_off ? ( label="Tach Start"
<> content={log.tach_start}
<Grid.Col span={6}> hours
<VerticalLogItem />
label="Time Start" </Grid.Col>
content={log.time_start} <Grid.Col span={6}>
time <VerticalLogItem
/> label="Tach End"
</Grid.Col> content={log.tach_end}
<Grid.Col span={6}> hours
<VerticalLogItem />
label="Time Off" </Grid.Col>
content={log.time_off} </>
time ) : null}
/> {log.time_start || log.time_off ? (
</Grid.Col> <>
</> <Grid.Col span={6}>
) : null} <VerticalLogItem
{log.time_down || log.time_stop ? ( label="Time Start"
<> content={log.time_start}
<Grid.Col span={6}> time
<VerticalLogItem />
label="Time Down" </Grid.Col>
content={log.time_down} <Grid.Col span={6}>
time <VerticalLogItem
/> label="Time Off"
</Grid.Col> content={log.time_off}
<Grid.Col span={6}> time
<VerticalLogItem />
label="Time Stop" </Grid.Col>
content={log.time_stop} </>
time ) : null}
/> {log.time_down || log.time_stop ? (
</Grid.Col> <>
</> <Grid.Col span={6}>
) : null} <VerticalLogItem
<Grid.Col span={4}> label="Time Down"
<VerticalLogItem content={log.time_down}
label="Total Time" time
content={log.time_total} />
hours </Grid.Col>
/> <Grid.Col span={6}>
</Grid.Col> <VerticalLogItem
<Grid.Col span={4}> label="Time Stop"
<VerticalLogItem content={log.time_stop}
label="Time Solo" time
content={log.time_solo} />
hours </Grid.Col>
/> </>
</Grid.Col> ) : null}
<Grid.Col span={4}>
<VerticalLogItem
label="Time Night"
content={log.time_night}
hours
/>
</Grid.Col>
<Grid.Col span={6}>
<VerticalLogItem
label="Time PIC"
content={log.time_pic}
hours
/>
</Grid.Col>
<Grid.Col span={6}>
<VerticalLogItem
label="Time SIC"
content={log.time_sic}
hours
/>
</Grid.Col>
{log.time_xc && log.dist_xc ? (
<>
<Grid.Col span={6}>
<VerticalLogItem
label="Time Cross-Country"
content={log.time_xc}
hours
/>
</Grid.Col>
<Grid.Col span={6}>
<VerticalLogItem
label="Cross-Country Distance"
content={log.dist_xc}
decimal={2}
/>
</Grid.Col>
</>
) : null}
<Grid.Col span={6}>
<VerticalLogItem
label="Takeoffs (Day)"
content={log.takeoffs_day}
/>
</Grid.Col>
<Grid.Col span={6}>
<VerticalLogItem
label="Landings (Day)"
content={log.landings_day}
/>
</Grid.Col>
<Grid.Col span={6}>
<VerticalLogItem
label="Takeoffs (Night)"
content={log.takeoffs_night}
/>
</Grid.Col>
<Grid.Col span={6}>
<VerticalLogItem
label="Landings (Night)"
content={log.landings_night}
/>
</Grid.Col>
{log.time_instrument ||
log.time_sim_instrument ||
log.holds_instrument ? (
<>
<Grid.Col span={4}> <Grid.Col span={4}>
<VerticalLogItem <VerticalLogItem
label="Instrument Time" label="Total Time"
content={log.time_instrument} content={log.time_total}
hours hours
/> />
</Grid.Col> </Grid.Col>
<Grid.Col span={4}> <Grid.Col span={4}>
<VerticalLogItem <VerticalLogItem
label="Simulated Instrument Time" label="Time Solo"
content={log.time_sim_instrument} content={log.time_solo}
hours hours
/> />
</Grid.Col> </Grid.Col>
<Grid.Col span={4}> <Grid.Col span={4}>
<VerticalLogItem <VerticalLogItem
label="Instrument Holds" label="Time Night"
content={log.holds_instrument} content={log.time_night}
hours
/> />
</Grid.Col> </Grid.Col>
</> <Grid.Col span={6}>
) : null} <VerticalLogItem
</Grid> label="Time PIC"
</Container> content={log.time_pic}
</ScrollArea> hours
</> />
) : ( </Grid.Col>
<Center h="calc(100vh - 95px)"> <Grid.Col span={6}>
<ErrorDisplay error="Unknown Error" /> <VerticalLogItem
</Center> label="Time SIC"
)} content={log.time_sic}
</Stack> hours
// </Container> />
</Grid.Col>
{log.time_xc && log.dist_xc ? (
<>
<Grid.Col span={6}>
<VerticalLogItem
label="Time Cross-Country"
content={log.time_xc}
hours
/>
</Grid.Col>
<Grid.Col span={6}>
<VerticalLogItem
label="Cross-Country Distance"
content={log.dist_xc}
decimal={2}
/>
</Grid.Col>
</>
) : null}
<Grid.Col span={6}>
<VerticalLogItem
label="Takeoffs (Day)"
content={log.takeoffs_day}
/>
</Grid.Col>
<Grid.Col span={6}>
<VerticalLogItem
label="Landings (Day)"
content={log.landings_day}
/>
</Grid.Col>
<Grid.Col span={6}>
<VerticalLogItem
label="Takeoffs (Night)"
content={log.takeoffs_night}
/>
</Grid.Col>
<Grid.Col span={6}>
<VerticalLogItem
label="Landings (Night)"
content={log.landings_night}
/>
</Grid.Col>
{log.time_instrument ||
log.time_sim_instrument ||
log.holds_instrument ? (
<>
<Grid.Col span={4}>
<VerticalLogItem
label="Instrument Time"
content={log.time_instrument}
hours
/>
</Grid.Col>
<Grid.Col span={4}>
<VerticalLogItem
label="Simulated Instrument Time"
content={log.time_sim_instrument}
hours
/>
</Grid.Col>
<Grid.Col span={4}>
<VerticalLogItem
label="Instrument Holds"
content={log.holds_instrument}
/>
</Grid.Col>
</>
) : null}
</Grid>
</Container>
</ScrollArea>
</>
) : (
<Center h="calc(100vh - 95px)">
<ErrorDisplay error="Unknown Error" />
</Center>
)}
</Stack>
</Container>
</>
); );
} }

View File

@ -5,8 +5,9 @@ import {
Fieldset, Fieldset,
Group, Group,
NumberInput, NumberInput,
ScrollAreaAutosize, ScrollArea,
Stack, Stack,
Text,
TextInput, TextInput,
Textarea, Textarea,
Title, Title,
@ -23,7 +24,6 @@ import ListInput from "@/ui/form/list-input";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useApi } from "@/util/api"; import { useApi } from "@/util/api";
import { useNavigate } from "@remix-run/react"; import { useNavigate } from "@remix-run/react";
import { useAuth } from "@/util/auth";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
export default function NewFlight() { export default function NewFlight() {
@ -82,24 +82,18 @@ export default function NewFlight() {
const client = useApi(); const client = useApi();
const { clearUser } = useAuth();
const createFlight = useMutation({ const createFlight = useMutation({
mutationFn: async (values: FlightFormSchema) => { mutationFn: async (values: FlightFormSchema) => {
const newFlight = flightCreateHelper(values); const newFlight = flightCreateHelper(values);
const res = await client.post("/flights", newFlight); if (newFlight) {
return res.data; const res = await client.post("/flights", newFlight);
return res.data;
}
throw new Error("Flight creation failed");
}, },
retry: (failureCount, error: AxiosError) => { retry: (failureCount, error: AxiosError) => {
return !error || error.response?.status !== 401; return !error || error.response?.status !== 401;
}, },
onError: (error: AxiosError) => {
console.log(error);
if (error.response?.status === 401) {
clearUser();
navigate("/login");
}
},
onSuccess: async (data: { id: string }) => { onSuccess: async (data: { id: string }) => {
await queryClient.invalidateQueries({ queryKey: ["flights-list"] }); await queryClient.invalidateQueries({ queryKey: ["flights-list"] });
navigate(`/logbook/flights/${data.id}`); navigate(`/logbook/flights/${data.id}`);
@ -112,7 +106,7 @@ export default function NewFlight() {
<Title order={2}>New Flight</Title> <Title order={2}>New Flight</Title>
<form onSubmit={form.onSubmit((values) => createFlight.mutate(values))}> <form onSubmit={form.onSubmit((values) => createFlight.mutate(values))}>
<ScrollAreaAutosize mah="calc(100vh - 95px - 110px)"> <ScrollArea.Autosize mah="calc(100vh - 95px - 110px)">
<Container> <Container>
{/* Date and Aircraft */} {/* Date and Aircraft */}
@ -347,9 +341,14 @@ export default function NewFlight() {
/> />
</Fieldset> </Fieldset>
</Container> </Container>
</ScrollAreaAutosize> </ScrollArea.Autosize>
<Group justify="flex-end" mt="md"> <Group justify="flex-end" mt="md">
{createFlight.isError ? (
<Text c="red" fw={700}>
{createFlight.error.message}
</Text>
) : null}
<Button type="submit" leftSection={<IconPencil />}> <Button type="submit" leftSection={<IconPencil />}>
Create Create
</Button> </Button>

View File

@ -37,7 +37,7 @@ export default function ListInput({
}; };
return ( return (
<PillsInput label={label}> <PillsInput label={label} description="Press enter or comma to add item">
<Pill.Group> <Pill.Group>
{(form.getTransformedValues()[field_key] as string[]).map( {(form.getTransformedValues()[field_key] as string[]).map(
(item: string) => ( (item: string) => (

View File

@ -88,44 +88,51 @@ type FlightConciseSchema = {
comments: string; comments: string;
}; };
const flightCreateHelper = (values: FlightFormSchema): FlightCreateSchema => { const flightCreateHelper = (
console.log(values.date.utc().startOf("date").toISOString()); values: FlightFormSchema
return { ): FlightCreateSchema | void => {
...values, const date = dayjs(values.date);
date: values.date.utc().startOf("day").toISOString(), try {
hobbs_start: Number(values.hobbs_start), const newFlight = {
hobbs_end: Number(values.hobbs_end), ...values,
tach_start: Number(values.tach_start), date: date.utc().startOf("day").toISOString(),
tach_end: Number(values.tach_end), hobbs_start: values.hobbs_start ? Number(values.hobbs_start) : null,
time_start: values.date hobbs_end: values.hobbs_end ? Number(values.hobbs_end) : null,
.utc() tach_start: values.tach_start ? Number(values.tach_start) : null,
.hour(Math.floor((values.time_start ?? 0) / 100)) tach_end: values.tach_end ? Number(values.tach_end) : null,
.minute(Math.floor((values.time_start ?? 0) % 100)) time_start: date
.second(0) .utc()
.millisecond(0) .hour(Math.floor((values.time_start ?? 0) / 100))
.toISOString(), .minute(Math.floor((values.time_start ?? 0) % 100))
time_off: values.date .second(0)
.utc() .millisecond(0)
.hour(Math.floor((values.time_off ?? 0) / 100)) .toISOString(),
.minute(Math.floor((values.time_off ?? 0) % 100)) time_off: date
.second(0) .utc()
.millisecond(0) .hour(Math.floor((values.time_off ?? 0) / 100))
.toISOString(), .minute(Math.floor((values.time_off ?? 0) % 100))
time_down: values.date .second(0)
.utc() .millisecond(0)
.hour(Math.floor((values.time_down ?? 0) / 100)) .toISOString(),
.minute(Math.floor((values.time_down ?? 0) % 100)) time_down: date
.second(0) .utc()
.millisecond(0) .hour(Math.floor((values.time_down ?? 0) / 100))
.toISOString(), .minute(Math.floor((values.time_down ?? 0) % 100))
time_stop: values.date .second(0)
.utc() .millisecond(0)
.hour(Math.floor((values.time_stop ?? 0) / 100)) .toISOString(),
.minute(Math.floor((values.time_stop ?? 0) % 100)) time_stop: date
.second(0) .utc()
.millisecond(0) .hour(Math.floor((values.time_stop ?? 0) / 100))
.toISOString(), .minute(Math.floor((values.time_stop ?? 0) % 100))
}; .second(0)
.millisecond(0)
.toISOString(),
};
return newFlight;
} catch (err) {
console.log(err);
}
}; };
export { export {

1
web/package-lock.json generated
View File

@ -4,6 +4,7 @@
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "tailfin-web",
"dependencies": { "dependencies": {
"@mantine/core": "^7.4.0", "@mantine/core": "^7.4.0",
"@mantine/dates": "^7.4.0", "@mantine/dates": "^7.4.0",