Implement inline editing and image upload/delete/edit

This commit is contained in:
april 2024-01-15 16:33:26 -06:00
parent 325730a9da
commit 924252b38f
17 changed files with 1213 additions and 50 deletions

View File

@ -26,6 +26,13 @@ import { useMutation, useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import classes from "./route.module.css";
import { AircraftLogItem } from "@/ui/display/editable/aircraft-log-item";
import { DateLogItem } from "@/ui/display/editable/date-log-item";
import { HourLogItem } from "@/ui/display/editable/hour-log-item";
import { IntLogItem } from "@/ui/display/editable/int-log-item";
import { ListLogItem } from "@/ui/display/editable/list-log-item";
import { TimeLogItem } from "@/ui/display/editable/time-log-item";
import { TextLogItem } from "@/ui/display/editable/text-log-item";
export default function Flight() {
const params = useParams();
@ -155,43 +162,55 @@ export default function Flight() {
) : null}
<CollapsibleFieldset legend="About" mt="sm" w="100%">
<Group grow>
<VerticalLogItem label="Date" content={log.date} date />
<VerticalLogItem
<DateLogItem
label="Date"
content={log.date}
id={params.id}
field="date"
/>
<AircraftLogItem
label="Aircraft"
content={log.aircraft}
id={params.id}
field="aircraft"
/>
</Group>
{(log.pax || log.crew) &&
(log.pax.length > 0 || log.crew.length > 0) ? (
<Group grow mt="sm">
<VerticalLogItem
<ListLogItem
label="Pax"
content={log.pax}
list
listColor="gray"
id={params.id}
field="pax"
/>
<VerticalLogItem
<ListLogItem
label="Crew"
content={log.crew}
list
listColor="gray"
id={params.id}
field="crew"
/>
</Group>
) : null}
{log.tags && log.tags.length > 0 ? (
<Group grow mt="sm">
<VerticalLogItem
<ListLogItem
label="Tags"
content={log.tags}
list
id={params.id}
field="tags"
/>
</Group>
) : null}
{log.comments?.length > 0 ? (
<Group grow mt="sm">
<VerticalLogItem
<TextLogItem
label="Comments"
content={log.comments}
id={params.id}
field="comments"
/>
</Group>
) : null}
@ -223,15 +242,17 @@ export default function Flight() {
{log.hobbs_start || log.hobbs_end ? (
<CollapsibleFieldset legend="Times" w="100%" mt="sm">
<Group grow>
<VerticalLogItem
<HourLogItem
label="Hobbs Start"
content={log.hobbs_start}
hours
id={params.id}
field="hobbs_start"
/>
<VerticalLogItem
<HourLogItem
label="Hobbs End"
content={log.hobbs_end}
hours
id={params.id}
field="hobbs_end"
/>
</Group>
</CollapsibleFieldset>
@ -243,29 +264,37 @@ export default function Flight() {
<CollapsibleFieldset legend="Start/Stop" w="100%" mt="sm">
{log.time_start || log.time_off ? (
<Group grow>
<VerticalLogItem
<TimeLogItem
label="Time Start"
content={log.time_start}
time
date={log.date}
id={params.id}
field="time_start"
/>
<VerticalLogItem
<TimeLogItem
label="Time Off"
content={log.time_off}
time
date={log.date}
id={params.id}
field="time_off"
/>
</Group>
) : null}
{log.time_down || log.time_stop ? (
<Group grow mt="sm">
<VerticalLogItem
<TimeLogItem
label="Time Down"
content={log.time_down}
time
date={log.date}
id={params.id}
field="time_down"
/>
<VerticalLogItem
<TimeLogItem
label="Time Stop"
content={log.time_stop}
time
date={log.date}
id={params.id}
field="time_stop"
/>
</Group>
) : null}
@ -273,32 +302,37 @@ export default function Flight() {
) : null}
<CollapsibleFieldset legend="Hours" w="100%" mt="sm">
<Group grow>
<VerticalLogItem
<HourLogItem
label="Total"
content={log.time_total}
hours
id={params.id}
field="time_total"
/>
<VerticalLogItem
<HourLogItem
label="Solo"
content={log.time_solo}
hours
id={params.id}
field="time_solo"
/>
<VerticalLogItem
<HourLogItem
label="Night"
content={log.time_night}
hours
id={params.id}
field="time_night"
/>
</Group>
<Group grow mt="sm">
<VerticalLogItem
<HourLogItem
label="PIC"
content={log.time_pic}
hours
id={params.id}
field="time_pic"
/>
<VerticalLogItem
<HourLogItem
label="SIC"
content={log.time_sic}
hours
id={params.id}
field="time_sic"
/>
</Group>
</CollapsibleFieldset>
@ -309,10 +343,11 @@ export default function Flight() {
mt="sm"
>
<Group grow>
<VerticalLogItem
<HourLogItem
label="Hours"
content={log.time_xc}
hours
id={params.id}
field="time_xc"
/>
<VerticalLogItem
label="Distance"
@ -324,13 +359,17 @@ export default function Flight() {
) : null}
<CollapsibleFieldset legend="Landings" w="100%">
<Group grow>
<VerticalLogItem
<IntLogItem
label="Day"
content={log.landings_day}
id={params.id}
field="landings_day"
/>
<VerticalLogItem
<IntLogItem
label="Night"
content={log.landings_night}
id={params.id}
field="landings_night"
/>
</Group>
</CollapsibleFieldset>
@ -339,19 +378,22 @@ export default function Flight() {
log.holds_instrument ? (
<CollapsibleFieldset legend="Instrument" mt="sm" w="100%">
<Group grow>
<VerticalLogItem
<HourLogItem
label="Instrument Time"
content={log.time_instrument}
hours
id={params.id}
field="time_instrument"
/>
<VerticalLogItem
<HourLogItem
label="Simulated Instrument Time"
content={log.time_sim_instrument}
hours
id={params.id}
/>
<VerticalLogItem
<IntLogItem
label="Instrument Holds"
content={log.holds_instrument}
id={params.id}
field="holds_instrument"
/>
</Group>
</CollapsibleFieldset>

View File

@ -29,7 +29,42 @@ export default function EditFlight() {
mutationFn: async (values: FlightFormSchema) => {
const newFlight = flightCreateHelper(values);
if (newFlight) {
const res = await client.put(`/flights/${params.id}`, newFlight);
const existing_img = values.existing_images ?? [];
const missing = flight.data.images.filter(
(item: string) => existing_img?.indexOf(item) < 0
);
for (const img of missing) {
await client.delete(`/img/${img}`);
}
const res = await client.put(`/flights/${params.id}`, {
...newFlight,
images: values.existing_images,
});
// Upload images
if (values.images.length > 0) {
const imageForm = new FormData();
for (const img of values.images ?? []) {
imageForm.append("images", img);
}
console.log(imageForm);
const img_id = await client.post(
`/flights/${params.id}/add_images`,
imageForm,
{ headers: { "Content-Type": "multipart/form-data" } }
);
if (!img_id) {
await queryClient.invalidateQueries({ queryKey: ["flights-list"] });
throw new Error("Image upload failed");
}
}
return res.data;
}
throw new Error("Flight updating failed");

View File

@ -0,0 +1,170 @@
import AircraftForm from "@/ui/form/aircraft-form";
import { useApi } from "@/util/api";
import { useAircraft, usePatchFlight } from "@/util/hooks";
import { AircraftFormSchema, AircraftSchema } from "@/util/types";
import {
ActionIcon,
Group,
Tooltip,
Text,
Select,
Modal,
Card,
Stack,
Button,
Loader,
UnstyledButton,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { IconPlus, IconPencil, IconX } from "@tabler/icons-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
export function AircraftLogItem({
label,
content,
id = "",
field = "",
}: {
label: string;
content: string | null;
id?: string;
field?: string;
}) {
const [editValue, setEditValue] = useState<string>(content ?? "");
const [editError, setEditError] = useState("");
const [editOpened, { open: openEdit, close: closeEdit }] =
useDisclosure(false);
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 getAircraft = useAircraft();
const updateValue = usePatchFlight(id, field, closeEdit);
if (content === null) content = "";
const editForm = (
<Select
label={
<Group gap="0">
<Text size="sm" fw={700} span>
Aircraft
</Text>
<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,
}))
: content
? [
{
value: content,
label: content,
},
]
: null
}
allowDeselect={false}
value={editValue}
onChange={(_value, option) => {
setEditError("");
setEditValue(option.label);
}}
error={editError}
/>
);
return (
<>
<Modal
opened={aircraftOpened}
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>
<Modal
opened={editOpened}
onClose={closeEdit}
title={`Edit ${label}`}
centered
>
<Stack>
{editForm}
<Group justify="flex-end">
{updateValue.isPending ? <Loader /> : null}
{updateValue.isError ? (
<Text c="red">{updateValue.error?.message}</Text>
) : null}
<Button
onClick={() => {
if (editValue.length === 0) {
setEditError("Please select an aircraft");
} else {
updateValue.mutate(editValue);
}
}}
leftSection={<IconPencil />}
>
Update
</Button>
</Group>
</Stack>
</Modal>
<Card shadow="sm" withBorder h="100%">
<Stack gap="xs" align="center" h="100%">
<Text c="dimmed">{label}</Text>
<Tooltip label={`Edit ${label}`}>
<UnstyledButton onClick={openEdit}>
<Text
size="lg"
style={{ textAlign: "center" }}
c={content === "" ? "dimmed" : ""}
>
{content === "" ? <IconX /> : content}
</Text>
</UnstyledButton>
</Tooltip>
</Stack>
</Card>
</>
);
}

View File

@ -0,0 +1,105 @@
import { usePatchFlight } from "@/util/hooks";
import {
Button,
Card,
Group,
Loader,
Modal,
Stack,
Text,
Tooltip,
UnstyledButton,
} from "@mantine/core";
import { DatePickerInput } from "@mantine/dates";
import { useDisclosure } from "@mantine/hooks";
import { IconPencil, IconX } from "@tabler/icons-react";
import { useState } from "react";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc.js";
dayjs.extend(utc);
export function DateLogItem({
label,
content,
id = "",
field = "",
}: {
label: string;
content: Date | string | null;
id?: string;
field?: string;
}) {
const [editValue, setEditValue] = useState<Date | null>(
content ? new Date(content as string) : null
);
const [editError, setEditError] = useState("");
const [editOpened, { open: openEdit, close: closeEdit }] =
useDisclosure(false);
const updateValue = usePatchFlight(id, field, closeEdit);
content = (content as string).split("T")[0];
const editForm = (
<DatePickerInput
label={label}
value={editValue}
onChange={setEditValue}
error={editError}
/>
);
return (
<>
<Modal
opened={editOpened}
onClose={closeEdit}
title={`Edit ${label}`}
centered
>
<Stack>
{editForm}
<Group justify="flex-end">
{updateValue.isPending ? <Loader /> : null}
{updateValue.isError ? (
<Text c="red">{updateValue.error?.message}</Text>
) : null}
<Button
onClick={() => {
if (editValue === null) {
setEditError("Please select a date");
} else {
updateValue.mutate(
dayjs(editValue).utc().startOf("day").toISOString()
);
}
}}
leftSection={<IconPencil />}
>
Update
</Button>
</Group>
</Stack>
</Modal>
<Card shadow="sm" withBorder h="100%">
<Stack gap="xs" align="center" h="100%">
<Text c="dimmed" style={{ textalign: "center" }}>
{label}
</Text>
<Tooltip label={`Edit ${label}`}>
<UnstyledButton onClick={openEdit}>
<Text
size="lg"
style={{ textAlign: "center" }}
c={content === "" ? "dimmed" : ""}
>
{content === "" ? <IconX /> : content}
</Text>
</UnstyledButton>
</Tooltip>
</Stack>
</Card>
</>
);
}

View File

@ -0,0 +1,99 @@
import {
Button,
Card,
Group,
Loader,
Modal,
Stack,
Text,
Tooltip,
UnstyledButton,
} from "@mantine/core";
import { IconPencil, IconX } from "@tabler/icons-react";
import { useState } from "react";
import { ZeroHourInput } from "@/ui/input/hour-input";
import { useDisclosure } from "@mantine/hooks";
import { usePatchFlight } from "@/util/hooks";
export function HourLogItem({
label,
content,
id = "",
field = "",
}: {
label: string;
content: number | string | null;
id?: string;
field?: string;
}) {
content = Number(content);
const [editValue, setEditValue] = useState<number | null>(content);
const [editError, setEditError] = useState("");
const [editOpened, { open: openEdit, close: closeEdit }] =
useDisclosure(false);
const updateValue = usePatchFlight(id, field, closeEdit);
const editForm = (
<ZeroHourInput
label=""
value={editValue}
setValue={setEditValue}
error={editError}
/>
);
return (
<>
<Modal
opened={editOpened}
onClose={closeEdit}
title={`Edit ${label}`}
centered
>
<Stack>
{editForm}
<Group justify="flex-end">
{updateValue.isPending ? <Loader /> : null}
{updateValue.isError ? (
<Text c="red">{updateValue.error?.message}</Text>
) : null}
<Button
onClick={() => {
if (editValue === null || editValue < 0) {
setEditError("Please enter a valid hour number");
} else {
updateValue.mutate(editValue);
}
}}
leftSection={<IconPencil />}
>
Update
</Button>
</Group>
</Stack>
</Modal>
<Card shadow="sm" withBorder h="100%">
<Stack gap="xs" align="center" h="100%">
<Text c="dimmed" style={{ textalign: "center" }}>
{label}
</Text>
<Tooltip label={`Edit ${label}`}>
<UnstyledButton onClick={openEdit}>
<Text
size="lg"
style={{ textAlign: "center" }}
c={content === null ? "dimmed" : ""}
>
{content === null ? <IconX /> : content}
</Text>
</UnstyledButton>
</Tooltip>
</Stack>
</Card>
</>
);
}

View File

@ -0,0 +1,99 @@
import {
Button,
Card,
Group,
Loader,
Modal,
Stack,
Text,
Tooltip,
UnstyledButton,
} from "@mantine/core";
import { IconPencil, IconX } from "@tabler/icons-react";
import { useState } from "react";
import { useDisclosure } from "@mantine/hooks";
import { usePatchFlight } from "@/util/hooks";
import { ZeroIntInput } from "@/ui/input/int-input";
export function IntLogItem({
label,
content,
id = "",
field = "",
}: {
label: string;
content: number | string | null;
id?: string;
field?: string;
}) {
content = Number(content);
const [editValue, setEditValue] = useState<number>(content);
const [editError, setEditError] = useState("");
const [editOpened, { open: openEdit, close: closeEdit }] =
useDisclosure(false);
const updateValue = usePatchFlight(id, field, closeEdit);
const editForm = (
<ZeroIntInput
label=""
value={editValue}
setValue={setEditValue}
error={editError}
/>
);
return (
<>
<Modal
opened={editOpened}
onClose={closeEdit}
title={`Edit ${label}`}
centered
>
<Stack>
{editForm}
<Group justify="flex-end">
{updateValue.isPending ? <Loader /> : null}
{updateValue.isError ? (
<Text c="red">{updateValue.error?.message}</Text>
) : null}
<Button
onClick={() => {
if (editValue === null || editValue < 0) {
setEditError("Please enter a valid number");
} else {
updateValue.mutate(editValue);
}
}}
leftSection={<IconPencil />}
>
Update
</Button>
</Group>
</Stack>
</Modal>
<Card shadow="sm" withBorder h="100%">
<Stack gap="xs" align="center" h="100%">
<Text c="dimmed" style={{ textalign: "center" }}>
{label}
</Text>
<Tooltip label={`Edit ${label}`}>
<UnstyledButton onClick={openEdit}>
<Text
size="lg"
style={{ textAlign: "center" }}
c={content === null ? "dimmed" : ""}
>
{content === null ? <IconX /> : content}
</Text>
</UnstyledButton>
</Tooltip>
</Stack>
</Card>
</>
);
}

View File

@ -0,0 +1,115 @@
import {
Badge,
Button,
Card,
Group,
Loader,
Modal,
Stack,
Text,
Tooltip,
UnstyledButton,
} from "@mantine/core";
import { randomId, useDisclosure } from "@mantine/hooks";
import { IconPencil, IconX } from "@tabler/icons-react";
import { useState } from "react";
import ListInput from "@/ui/input/list-input";
import { usePatchFlight } from "@/util/hooks";
export function LogItem({
label,
content,
}: {
label: string;
content: string | null;
}) {
if (content === null) content = "";
return (
<Group justify="space-between" px="sm">
<Text>{label}</Text>
<Text>{content}</Text>
</Group>
);
}
export function ListLogItem({
label,
content,
listColor = "",
id = "",
field = "",
}: {
label: string;
content: string | string[] | null;
listColor?: string;
id?: string;
field?: string;
}) {
if (content === null) content = [];
if (content instanceof String) content = [content as string];
const [editValue, setEditValue] = useState<string[]>(content as string[]);
const [editOpened, { open: openEdit, close: closeEdit }] =
useDisclosure(false);
const updateValue = usePatchFlight(id, field, closeEdit);
const editForm = (
<ListInput label={label} value={editValue} setValue={setEditValue} />
);
return (
<>
<Modal
opened={editOpened}
onClose={closeEdit}
title={`Edit ${label}`}
centered
>
<Stack>
{editForm}
<Group justify="flex-end">
{updateValue.isPending ? <Loader /> : null}
{updateValue.isError ? (
<Text c="red">{updateValue.error?.message}</Text>
) : null}
<Button
onClick={() => {
updateValue.mutate(editValue);
}}
leftSection={<IconPencil />}
>
Update
</Button>
</Group>
</Stack>
</Modal>
<Card shadow="sm" withBorder h="100%">
<Stack gap="xs" align="center" h="100%">
<Text c="dimmed" style={{ textalign: "center" }}>
{label}
</Text>
<Tooltip label={`Edit ${label}`}>
<UnstyledButton onClick={openEdit}>
{(content as string[]).length > 0 ? (
<Text size="lg">
{(content as string[]).map((item) => (
<Badge key={randomId()} size="lg" mx="xs" color={listColor}>
{item}
</Badge>
))}
</Text>
) : (
<Text size="lg" style={{ textAlign: "center" }} c="dimmed">
<IconX />
</Text>
)}
</UnstyledButton>
</Tooltip>
</Stack>
</Card>
</>
);
}

View File

@ -0,0 +1,88 @@
import {
Button,
Card,
Group,
Loader,
Modal,
Stack,
Text,
Textarea,
Tooltip,
UnstyledButton,
} from "@mantine/core";
import { IconPencil, IconX } from "@tabler/icons-react";
import { useState } from "react";
import { useDisclosure } from "@mantine/hooks";
import { usePatchFlight } from "@/util/hooks";
export function TextLogItem({
label,
content,
id = "",
field = "",
}: {
label: string;
content: string | null;
id?: string;
field?: string;
}) {
const [editValue, setEditValue] = useState<string>(content ?? "");
const [editOpened, { open: openEdit, close: closeEdit }] =
useDisclosure(false);
const updateValue = usePatchFlight(id, field, closeEdit);
const editForm = (
<Textarea
label=""
value={editValue}
onChange={(event) => setEditValue(event.currentTarget.value)}
/>
);
return (
<>
<Modal
opened={editOpened}
onClose={closeEdit}
title={`Edit ${label}`}
centered
>
<Stack>
{editForm}
<Group justify="flex-end">
{updateValue.isPending ? <Loader /> : null}
{updateValue.isError ? (
<Text c="red">{updateValue.error?.message}</Text>
) : null}
<Button
onClick={() => updateValue.mutate(editValue)}
leftSection={<IconPencil />}
>
Update
</Button>
</Group>
</Stack>
</Modal>
<Card shadow="sm" withBorder h="100%">
<Stack gap="xs" align="center" h="100%">
<Text c="dimmed" style={{ textalign: "center" }}>
{label}
</Text>
<Tooltip label={`Edit ${label}`}>
<UnstyledButton onClick={openEdit}>
<Text
size="lg"
style={{ textAlign: "center" }}
c={content === null ? "dimmed" : ""}
>
{content === null ? <IconX /> : content}
</Text>
</UnstyledButton>
</Tooltip>
</Stack>
</Card>
</>
);
}

View File

@ -0,0 +1,120 @@
import {
Button,
Card,
Group,
Loader,
Modal,
Stack,
Text,
Tooltip,
UnstyledButton,
} from "@mantine/core";
import { IconPencil, IconX } from "@tabler/icons-react";
import { useState } from "react";
import TimeInput from "@/ui/input/time-input";
import { useDisclosure } from "@mantine/hooks";
import dayjs from "dayjs";
import { usePatchFlight } from "@/util/hooks";
import utc from "dayjs/plugin/utc.js";
dayjs.extend(utc);
export function TimeLogItem({
label,
content,
date,
id = "",
field = "",
}: {
label: string;
content: string | null;
date: dayjs.Dayjs | string;
id?: string;
field?: string;
}) {
if (date instanceof String) date = dayjs(date);
const time = (content as string).split("T")[1].split(":");
const [editValue, setEditValue] = useState<number | string | undefined>(
Number(`${time[0]}${time[1]}`)
);
const [editError, setEditError] = useState("");
const [editOpened, { open: openEdit, close: closeEdit }] =
useDisclosure(false);
const updateValue = usePatchFlight(id, field, closeEdit);
content = `${time[0]}:${time[1]}`;
const editForm = (
<TimeInput
label={label}
value={editValue}
setValue={setEditValue}
error={editError}
allowLeadingZeros
/>
);
return (
<>
<Modal
opened={editOpened}
onClose={closeEdit}
title={`Edit ${label}`}
centered
>
<Stack>
{editForm}
<Group justify="flex-end">
{updateValue.isPending ? <Loader /> : null}
{updateValue.isError ? (
<Text c="red">{updateValue.error?.message}</Text>
) : null}
<Button
onClick={() => {
if (Number(editValue) > 2359)
setEditError("Time must be between 0000 and 2359");
else if (Number(editValue) % 100 > 59)
setEditError("Minutes must not exceed 59");
else {
updateValue.mutate(
dayjs(date)
.utc()
.hour(Math.floor((Number(editValue) ?? 0) / 100))
.minute(Math.floor((Number(editValue) ?? 0) % 100))
.second(0)
.millisecond(0)
.toISOString()
);
}
}}
leftSection={<IconPencil />}
>
Update
</Button>
</Group>
</Stack>
</Modal>
<Card shadow="sm" withBorder h="100%">
<Stack gap="xs" align="center" h="100%">
<Text c="dimmed" style={{ textalign: "center" }}>
{label}
</Text>
<Tooltip label={`Edit ${label}`}>
<UnstyledButton onClick={openEdit}>
<Text
size="lg"
style={{ textAlign: "center" }}
c={content === null ? "dimmed" : ""}
>
{content === null ? <IconX /> : content}
</Text>
</UnstyledButton>
</Tooltip>
</Stack>
</Card>
</>
);
}

View File

@ -1,6 +1,7 @@
import {
AircraftFormSchema,
AircraftSchema,
FlightDisplaySchema,
FlightFormSchema,
} from "@/util/types";
import {
@ -109,6 +110,7 @@ export default function FlightForm({
comments: "",
existing_images: [],
images: [],
},
validate: {
@ -120,6 +122,8 @@ export default function FlightForm({
time_stop: (value) => validate_time(value),
},
});
console.log(initialValues);
console.log(form.getTransformedValues());
const [aircraftOpened, { open: openAircraft, close: closeAircraft }] =
useDisclosure(false);
@ -509,6 +513,15 @@ export default function FlightForm({
minRows={4}
{...form.getInputProps("comments")}
/>
{initialValues?.existing_images?.length ?? 0 > 0 ? (
<ListInput
form={form}
field="existing_images"
mt="md"
label="Existing Images"
canAdd={false}
/>
) : null}
<ImageUpload
form={form}
mt="md"

View File

@ -7,6 +7,8 @@ export default function ListInput({
form,
field,
label,
mt = "",
canAdd = true,
}: {
form: UseFormReturnType<
FlightFormSchema,
@ -14,6 +16,8 @@ export default function ListInput({
>;
field: string;
label: string;
mt?: string;
canAdd?: boolean;
}) {
const field_key = field as keyof typeof form.getTransformedValues;
const [inputValue, setInputValue] = useState<string>("");
@ -37,7 +41,11 @@ export default function ListInput({
};
return (
<PillsInput label={label} description="Press enter or comma to add item">
<PillsInput
mt={mt}
label={label}
description="Press enter or comma to add item"
>
<Pill.Group>
{(form.getTransformedValues()[field_key] as string[]).map(
(item: string) => (
@ -58,11 +66,13 @@ export default function ListInput({
</Pill>
)
)}
<PillsInput.Field
value={inputValue}
onChange={(event) => setInputValue(event.currentTarget.value)}
onKeyDown={handleKeyDown}
/>
{canAdd ? (
<PillsInput.Field
value={inputValue}
onChange={(event) => setInputValue(event.currentTarget.value)}
onKeyDown={handleKeyDown}
/>
) : null}
</Pill.Group>
</PillsInput>
);

View File

@ -0,0 +1,71 @@
import { CloseButton, NumberInput } from "@mantine/core";
import { Dispatch, SetStateAction } from "react";
function HourInput({
label,
value,
setValue,
}: {
label: string;
value: number | string | null;
setValue: Dispatch<SetStateAction<string | number | null>>;
}) {
return (
<NumberInput
label={label}
decimalScale={1}
step={0.1}
min={0}
fixedDecimalScale
leftSection={
<CloseButton
aria-label="Clear input"
onClick={() => setValue("")}
style={{
display: Number.isFinite(value) ? undefined : undefined,
}}
/>
}
onChange={(value) =>
setValue(Number.isFinite(value) ? (value as number) : 0)
}
/>
);
}
function ZeroHourInput({
label,
value,
setValue,
error = null,
}: {
label: string;
value: number | null;
setValue: Dispatch<SetStateAction<number | null>>;
error?: string | null;
}) {
return (
<NumberInput
label={label}
decimalScale={1}
step={0.1}
min={0}
fixedDecimalScale
error={error}
leftSection={
<CloseButton
aria-label="Clear input"
onClick={() => setValue(0)}
style={{
display: !value || value === 0 ? "none" : undefined,
}}
/>
}
onChange={(value) =>
setValue(Number.isFinite(value) ? (value as number) : 0)
}
/>
);
}
export { HourInput, ZeroHourInput };

View File

@ -0,0 +1,65 @@
import { CloseButton, NumberInput } from "@mantine/core";
import { Dispatch, SetStateAction } from "react";
function IntInput({
label,
value,
setValue,
}: {
label: string;
value: number | string | undefined;
setValue: Dispatch<SetStateAction<number | string | undefined>>;
}) {
return (
<NumberInput
label={label}
min={0}
allowDecimal={false}
value={value}
leftSection={
<CloseButton
aria-label="Clear input"
onClick={() => setValue("")}
style={{
display: Number.isFinite(value) ? "none" : undefined,
}}
/>
}
onChange={setValue}
/>
);
}
function ZeroIntInput({
label,
value,
setValue,
error = null,
}: {
label: string;
value: number;
setValue: Dispatch<SetStateAction<number>>;
error: string | null;
}) {
return (
<NumberInput
label={label}
min={0}
allowDecimal={false}
error={error}
value={value}
leftSection={
<CloseButton
aria-label="Clear input"
onClick={() => setValue(0)}
style={{
display: !value || value === 0 ? "none" : undefined,
}}
/>
}
onChange={(value) => setValue(Number(value))}
/>
);
}
export { IntInput, ZeroIntInput };

View File

@ -0,0 +1,54 @@
import { Pill, PillsInput } from "@mantine/core";
import { Dispatch, SetStateAction, useState } from "react";
export default function ListInput({
label,
value,
setValue,
}: {
label: string;
value: string[];
setValue: Dispatch<SetStateAction<string[]>>;
}) {
const [inputValue, setInputValue] = useState<string>("");
const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === "Enter" || event.key === ",") {
event.preventDefault();
const newItem = inputValue.trim();
if (newItem && value.indexOf(newItem) == -1) {
setValue([...value, newItem]);
setInputValue("");
}
} else if (event.key === "Backspace") {
const newItem = inputValue.trim();
if (newItem === "") {
setValue(value.slice(0, -1));
}
}
};
return (
<PillsInput label={label} description="Press enter or comma to add item">
<Pill.Group>
{value.map((item: string) => (
<Pill
radius="sm"
key={item}
withRemoveButton
onRemove={() =>
setValue(value.filter((value: string) => value !== item))
}
>
{item}
</Pill>
))}
<PillsInput.Field
value={inputValue}
onChange={(event) => setInputValue(event.currentTarget.value)}
onKeyDown={handleKeyDown}
/>
</Pill.Group>
</PillsInput>
);
}

View File

@ -0,0 +1,53 @@
import { ActionIcon, CloseButton, NumberInput, Tooltip } from "@mantine/core";
import { IconClock } from "@tabler/icons-react";
import dayjs from "dayjs";
import { Dispatch, SetStateAction } from "react";
export default function TimeInput({
label,
value,
setValue,
allowLeadingZeros = false,
error = "",
}: {
label: string;
value: number | string | undefined;
setValue: Dispatch<SetStateAction<number | string | undefined>>;
allowLeadingZeros?: boolean;
error?: string | null;
}) {
return (
<NumberInput
label={label}
allowDecimal={false}
min={0}
max={2359}
allowLeadingZeros={allowLeadingZeros}
error={error}
value={value}
leftSection={
<CloseButton
aria-label="Clear input"
onClick={() => setValue("")}
style={{
display: Number.isFinite(value) ? "none" : undefined,
}}
/>
}
rightSection={
<Tooltip label="Now">
<ActionIcon
variant="transparent"
mr="sm"
onClick={() => {
setValue(dayjs().format("HHmm"));
}}
>
<IconClock style={{ width: "70%", height: "70%" }} />
</ActionIcon>
</Tooltip>
}
onChange={setValue}
/>
);
}

View File

@ -1,4 +1,4 @@
import { useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useApi } from "./api";
export function useAircraft() {
@ -31,3 +31,24 @@ export function useFlights(filter: string = "", value: string = "") {
return flights;
}
export function usePatchFlight(
id: string,
field: string,
onSuccess: () => void
) {
const client = useApi();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (value: string | string[] | number | Date | null) =>
await client
.patch(`/flights/${id}`, { [field]: value })
.then((res) => res.data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [id] });
queryClient.invalidateQueries({ queryKey: ["flights-list"] });
onSuccess();
},
});
}

View File

@ -51,7 +51,8 @@ type FlightFormSchema = FlightBaseSchema & {
time_down: number | null;
time_stop: number | null;
images: File[];
existing_images?: string[] | null;
images: File[] | string[];
};
type FlightCreateSchema = FlightBaseSchema & {
@ -136,7 +137,7 @@ const flightCreateHelper = (
};
const flightEditHelper = (
values: FlightCreateSchema
values: FlightDisplaySchema
): FlightFormSchema | void => {
try {
const flight = {
@ -154,6 +155,8 @@ const flightEditHelper = (
time_stop: Number(
`${dayjs(values.time_stop).hour()}${dayjs(values.time_stop).minute()}`
),
existing_images: values.images as string[],
images: [],
};
return flight;
} catch (err) {