Implement inline editing and image upload/delete/edit
This commit is contained in:
parent
325730a9da
commit
924252b38f
@ -26,6 +26,13 @@ import { useMutation, useQuery } from "@tanstack/react-query";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import classes from "./route.module.css";
|
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() {
|
export default function Flight() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@ -155,43 +162,55 @@ export default function Flight() {
|
|||||||
) : null}
|
) : null}
|
||||||
<CollapsibleFieldset legend="About" mt="sm" w="100%">
|
<CollapsibleFieldset legend="About" mt="sm" w="100%">
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<VerticalLogItem label="Date" content={log.date} date />
|
<DateLogItem
|
||||||
<VerticalLogItem
|
label="Date"
|
||||||
|
content={log.date}
|
||||||
|
id={params.id}
|
||||||
|
field="date"
|
||||||
|
/>
|
||||||
|
<AircraftLogItem
|
||||||
label="Aircraft"
|
label="Aircraft"
|
||||||
content={log.aircraft}
|
content={log.aircraft}
|
||||||
|
id={params.id}
|
||||||
|
field="aircraft"
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
{(log.pax || log.crew) &&
|
{(log.pax || log.crew) &&
|
||||||
(log.pax.length > 0 || log.crew.length > 0) ? (
|
(log.pax.length > 0 || log.crew.length > 0) ? (
|
||||||
<Group grow mt="sm">
|
<Group grow mt="sm">
|
||||||
<VerticalLogItem
|
<ListLogItem
|
||||||
label="Pax"
|
label="Pax"
|
||||||
content={log.pax}
|
content={log.pax}
|
||||||
list
|
|
||||||
listColor="gray"
|
listColor="gray"
|
||||||
|
id={params.id}
|
||||||
|
field="pax"
|
||||||
/>
|
/>
|
||||||
<VerticalLogItem
|
<ListLogItem
|
||||||
label="Crew"
|
label="Crew"
|
||||||
content={log.crew}
|
content={log.crew}
|
||||||
list
|
|
||||||
listColor="gray"
|
listColor="gray"
|
||||||
|
id={params.id}
|
||||||
|
field="crew"
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
) : null}
|
) : null}
|
||||||
{log.tags && log.tags.length > 0 ? (
|
{log.tags && log.tags.length > 0 ? (
|
||||||
<Group grow mt="sm">
|
<Group grow mt="sm">
|
||||||
<VerticalLogItem
|
<ListLogItem
|
||||||
label="Tags"
|
label="Tags"
|
||||||
content={log.tags}
|
content={log.tags}
|
||||||
list
|
id={params.id}
|
||||||
|
field="tags"
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
) : null}
|
) : null}
|
||||||
{log.comments?.length > 0 ? (
|
{log.comments?.length > 0 ? (
|
||||||
<Group grow mt="sm">
|
<Group grow mt="sm">
|
||||||
<VerticalLogItem
|
<TextLogItem
|
||||||
label="Comments"
|
label="Comments"
|
||||||
content={log.comments}
|
content={log.comments}
|
||||||
|
id={params.id}
|
||||||
|
field="comments"
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
) : null}
|
) : null}
|
||||||
@ -223,15 +242,17 @@ export default function Flight() {
|
|||||||
{log.hobbs_start || log.hobbs_end ? (
|
{log.hobbs_start || log.hobbs_end ? (
|
||||||
<CollapsibleFieldset legend="Times" w="100%" mt="sm">
|
<CollapsibleFieldset legend="Times" w="100%" mt="sm">
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<VerticalLogItem
|
<HourLogItem
|
||||||
label="Hobbs Start"
|
label="Hobbs Start"
|
||||||
content={log.hobbs_start}
|
content={log.hobbs_start}
|
||||||
hours
|
id={params.id}
|
||||||
|
field="hobbs_start"
|
||||||
/>
|
/>
|
||||||
<VerticalLogItem
|
<HourLogItem
|
||||||
label="Hobbs End"
|
label="Hobbs End"
|
||||||
content={log.hobbs_end}
|
content={log.hobbs_end}
|
||||||
hours
|
id={params.id}
|
||||||
|
field="hobbs_end"
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
</CollapsibleFieldset>
|
</CollapsibleFieldset>
|
||||||
@ -243,29 +264,37 @@ export default function Flight() {
|
|||||||
<CollapsibleFieldset legend="Start/Stop" w="100%" mt="sm">
|
<CollapsibleFieldset legend="Start/Stop" w="100%" mt="sm">
|
||||||
{log.time_start || log.time_off ? (
|
{log.time_start || log.time_off ? (
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<VerticalLogItem
|
<TimeLogItem
|
||||||
label="Time Start"
|
label="Time Start"
|
||||||
content={log.time_start}
|
content={log.time_start}
|
||||||
time
|
date={log.date}
|
||||||
|
id={params.id}
|
||||||
|
field="time_start"
|
||||||
/>
|
/>
|
||||||
<VerticalLogItem
|
<TimeLogItem
|
||||||
label="Time Off"
|
label="Time Off"
|
||||||
content={log.time_off}
|
content={log.time_off}
|
||||||
time
|
date={log.date}
|
||||||
|
id={params.id}
|
||||||
|
field="time_off"
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
) : null}
|
) : null}
|
||||||
{log.time_down || log.time_stop ? (
|
{log.time_down || log.time_stop ? (
|
||||||
<Group grow mt="sm">
|
<Group grow mt="sm">
|
||||||
<VerticalLogItem
|
<TimeLogItem
|
||||||
label="Time Down"
|
label="Time Down"
|
||||||
content={log.time_down}
|
content={log.time_down}
|
||||||
time
|
date={log.date}
|
||||||
|
id={params.id}
|
||||||
|
field="time_down"
|
||||||
/>
|
/>
|
||||||
<VerticalLogItem
|
<TimeLogItem
|
||||||
label="Time Stop"
|
label="Time Stop"
|
||||||
content={log.time_stop}
|
content={log.time_stop}
|
||||||
time
|
date={log.date}
|
||||||
|
id={params.id}
|
||||||
|
field="time_stop"
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
) : null}
|
) : null}
|
||||||
@ -273,32 +302,37 @@ export default function Flight() {
|
|||||||
) : null}
|
) : null}
|
||||||
<CollapsibleFieldset legend="Hours" w="100%" mt="sm">
|
<CollapsibleFieldset legend="Hours" w="100%" mt="sm">
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<VerticalLogItem
|
<HourLogItem
|
||||||
label="Total"
|
label="Total"
|
||||||
content={log.time_total}
|
content={log.time_total}
|
||||||
hours
|
id={params.id}
|
||||||
|
field="time_total"
|
||||||
/>
|
/>
|
||||||
<VerticalLogItem
|
<HourLogItem
|
||||||
label="Solo"
|
label="Solo"
|
||||||
content={log.time_solo}
|
content={log.time_solo}
|
||||||
hours
|
id={params.id}
|
||||||
|
field="time_solo"
|
||||||
/>
|
/>
|
||||||
<VerticalLogItem
|
<HourLogItem
|
||||||
label="Night"
|
label="Night"
|
||||||
content={log.time_night}
|
content={log.time_night}
|
||||||
hours
|
id={params.id}
|
||||||
|
field="time_night"
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<Group grow mt="sm">
|
<Group grow mt="sm">
|
||||||
<VerticalLogItem
|
<HourLogItem
|
||||||
label="PIC"
|
label="PIC"
|
||||||
content={log.time_pic}
|
content={log.time_pic}
|
||||||
hours
|
id={params.id}
|
||||||
|
field="time_pic"
|
||||||
/>
|
/>
|
||||||
<VerticalLogItem
|
<HourLogItem
|
||||||
label="SIC"
|
label="SIC"
|
||||||
content={log.time_sic}
|
content={log.time_sic}
|
||||||
hours
|
id={params.id}
|
||||||
|
field="time_sic"
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
</CollapsibleFieldset>
|
</CollapsibleFieldset>
|
||||||
@ -309,10 +343,11 @@ export default function Flight() {
|
|||||||
mt="sm"
|
mt="sm"
|
||||||
>
|
>
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<VerticalLogItem
|
<HourLogItem
|
||||||
label="Hours"
|
label="Hours"
|
||||||
content={log.time_xc}
|
content={log.time_xc}
|
||||||
hours
|
id={params.id}
|
||||||
|
field="time_xc"
|
||||||
/>
|
/>
|
||||||
<VerticalLogItem
|
<VerticalLogItem
|
||||||
label="Distance"
|
label="Distance"
|
||||||
@ -324,13 +359,17 @@ export default function Flight() {
|
|||||||
) : null}
|
) : null}
|
||||||
<CollapsibleFieldset legend="Landings" w="100%">
|
<CollapsibleFieldset legend="Landings" w="100%">
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<VerticalLogItem
|
<IntLogItem
|
||||||
label="Day"
|
label="Day"
|
||||||
content={log.landings_day}
|
content={log.landings_day}
|
||||||
|
id={params.id}
|
||||||
|
field="landings_day"
|
||||||
/>
|
/>
|
||||||
<VerticalLogItem
|
<IntLogItem
|
||||||
label="Night"
|
label="Night"
|
||||||
content={log.landings_night}
|
content={log.landings_night}
|
||||||
|
id={params.id}
|
||||||
|
field="landings_night"
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
</CollapsibleFieldset>
|
</CollapsibleFieldset>
|
||||||
@ -339,19 +378,22 @@ export default function Flight() {
|
|||||||
log.holds_instrument ? (
|
log.holds_instrument ? (
|
||||||
<CollapsibleFieldset legend="Instrument" mt="sm" w="100%">
|
<CollapsibleFieldset legend="Instrument" mt="sm" w="100%">
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<VerticalLogItem
|
<HourLogItem
|
||||||
label="Instrument Time"
|
label="Instrument Time"
|
||||||
content={log.time_instrument}
|
content={log.time_instrument}
|
||||||
hours
|
id={params.id}
|
||||||
|
field="time_instrument"
|
||||||
/>
|
/>
|
||||||
<VerticalLogItem
|
<HourLogItem
|
||||||
label="Simulated Instrument Time"
|
label="Simulated Instrument Time"
|
||||||
content={log.time_sim_instrument}
|
content={log.time_sim_instrument}
|
||||||
hours
|
id={params.id}
|
||||||
/>
|
/>
|
||||||
<VerticalLogItem
|
<IntLogItem
|
||||||
label="Instrument Holds"
|
label="Instrument Holds"
|
||||||
content={log.holds_instrument}
|
content={log.holds_instrument}
|
||||||
|
id={params.id}
|
||||||
|
field="holds_instrument"
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
</CollapsibleFieldset>
|
</CollapsibleFieldset>
|
||||||
|
@ -29,7 +29,42 @@ export default function EditFlight() {
|
|||||||
mutationFn: async (values: FlightFormSchema) => {
|
mutationFn: async (values: FlightFormSchema) => {
|
||||||
const newFlight = flightCreateHelper(values);
|
const newFlight = flightCreateHelper(values);
|
||||||
if (newFlight) {
|
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;
|
return res.data;
|
||||||
}
|
}
|
||||||
throw new Error("Flight updating failed");
|
throw new Error("Flight updating failed");
|
||||||
|
170
web/app/ui/display/editable/aircraft-log-item.tsx
Normal file
170
web/app/ui/display/editable/aircraft-log-item.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
105
web/app/ui/display/editable/date-log-item.tsx
Normal file
105
web/app/ui/display/editable/date-log-item.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
99
web/app/ui/display/editable/hour-log-item.tsx
Normal file
99
web/app/ui/display/editable/hour-log-item.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
99
web/app/ui/display/editable/int-log-item.tsx
Normal file
99
web/app/ui/display/editable/int-log-item.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
115
web/app/ui/display/editable/list-log-item.tsx
Normal file
115
web/app/ui/display/editable/list-log-item.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
88
web/app/ui/display/editable/text-log-item.tsx
Normal file
88
web/app/ui/display/editable/text-log-item.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
120
web/app/ui/display/editable/time-log-item.tsx
Normal file
120
web/app/ui/display/editable/time-log-item.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
AircraftFormSchema,
|
AircraftFormSchema,
|
||||||
AircraftSchema,
|
AircraftSchema,
|
||||||
|
FlightDisplaySchema,
|
||||||
FlightFormSchema,
|
FlightFormSchema,
|
||||||
} from "@/util/types";
|
} from "@/util/types";
|
||||||
import {
|
import {
|
||||||
@ -109,6 +110,7 @@ export default function FlightForm({
|
|||||||
|
|
||||||
comments: "",
|
comments: "",
|
||||||
|
|
||||||
|
existing_images: [],
|
||||||
images: [],
|
images: [],
|
||||||
},
|
},
|
||||||
validate: {
|
validate: {
|
||||||
@ -120,6 +122,8 @@ export default function FlightForm({
|
|||||||
time_stop: (value) => validate_time(value),
|
time_stop: (value) => validate_time(value),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
console.log(initialValues);
|
||||||
|
console.log(form.getTransformedValues());
|
||||||
|
|
||||||
const [aircraftOpened, { open: openAircraft, close: closeAircraft }] =
|
const [aircraftOpened, { open: openAircraft, close: closeAircraft }] =
|
||||||
useDisclosure(false);
|
useDisclosure(false);
|
||||||
@ -509,6 +513,15 @@ export default function FlightForm({
|
|||||||
minRows={4}
|
minRows={4}
|
||||||
{...form.getInputProps("comments")}
|
{...form.getInputProps("comments")}
|
||||||
/>
|
/>
|
||||||
|
{initialValues?.existing_images?.length ?? 0 > 0 ? (
|
||||||
|
<ListInput
|
||||||
|
form={form}
|
||||||
|
field="existing_images"
|
||||||
|
mt="md"
|
||||||
|
label="Existing Images"
|
||||||
|
canAdd={false}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<ImageUpload
|
<ImageUpload
|
||||||
form={form}
|
form={form}
|
||||||
mt="md"
|
mt="md"
|
||||||
|
@ -7,6 +7,8 @@ export default function ListInput({
|
|||||||
form,
|
form,
|
||||||
field,
|
field,
|
||||||
label,
|
label,
|
||||||
|
mt = "",
|
||||||
|
canAdd = true,
|
||||||
}: {
|
}: {
|
||||||
form: UseFormReturnType<
|
form: UseFormReturnType<
|
||||||
FlightFormSchema,
|
FlightFormSchema,
|
||||||
@ -14,6 +16,8 @@ export default function ListInput({
|
|||||||
>;
|
>;
|
||||||
field: string;
|
field: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
mt?: string;
|
||||||
|
canAdd?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const field_key = field as keyof typeof form.getTransformedValues;
|
const field_key = field as keyof typeof form.getTransformedValues;
|
||||||
const [inputValue, setInputValue] = useState<string>("");
|
const [inputValue, setInputValue] = useState<string>("");
|
||||||
@ -37,7 +41,11 @@ export default function ListInput({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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>
|
<Pill.Group>
|
||||||
{(form.getTransformedValues()[field_key] as string[]).map(
|
{(form.getTransformedValues()[field_key] as string[]).map(
|
||||||
(item: string) => (
|
(item: string) => (
|
||||||
@ -58,11 +66,13 @@ export default function ListInput({
|
|||||||
</Pill>
|
</Pill>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
{canAdd ? (
|
||||||
<PillsInput.Field
|
<PillsInput.Field
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(event) => setInputValue(event.currentTarget.value)}
|
onChange={(event) => setInputValue(event.currentTarget.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
|
) : null}
|
||||||
</Pill.Group>
|
</Pill.Group>
|
||||||
</PillsInput>
|
</PillsInput>
|
||||||
);
|
);
|
||||||
|
71
web/app/ui/input/hour-input.tsx
Normal file
71
web/app/ui/input/hour-input.tsx
Normal 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 };
|
65
web/app/ui/input/int-input.tsx
Normal file
65
web/app/ui/input/int-input.tsx
Normal 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 };
|
54
web/app/ui/input/list-input.tsx
Normal file
54
web/app/ui/input/list-input.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
53
web/app/ui/input/time-input.tsx
Normal file
53
web/app/ui/input/time-input.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useApi } from "./api";
|
import { useApi } from "./api";
|
||||||
|
|
||||||
export function useAircraft() {
|
export function useAircraft() {
|
||||||
@ -31,3 +31,24 @@ export function useFlights(filter: string = "", value: string = "") {
|
|||||||
|
|
||||||
return flights;
|
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();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -51,7 +51,8 @@ type FlightFormSchema = FlightBaseSchema & {
|
|||||||
time_down: number | null;
|
time_down: number | null;
|
||||||
time_stop: number | null;
|
time_stop: number | null;
|
||||||
|
|
||||||
images: File[];
|
existing_images?: string[] | null;
|
||||||
|
images: File[] | string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type FlightCreateSchema = FlightBaseSchema & {
|
type FlightCreateSchema = FlightBaseSchema & {
|
||||||
@ -136,7 +137,7 @@ const flightCreateHelper = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const flightEditHelper = (
|
const flightEditHelper = (
|
||||||
values: FlightCreateSchema
|
values: FlightDisplaySchema
|
||||||
): FlightFormSchema | void => {
|
): FlightFormSchema | void => {
|
||||||
try {
|
try {
|
||||||
const flight = {
|
const flight = {
|
||||||
@ -154,6 +155,8 @@ const flightEditHelper = (
|
|||||||
time_stop: Number(
|
time_stop: Number(
|
||||||
`${dayjs(values.time_stop).hour()}${dayjs(values.time_stop).minute()}`
|
`${dayjs(values.time_stop).hour()}${dayjs(values.time_stop).minute()}`
|
||||||
),
|
),
|
||||||
|
existing_images: values.images as string[],
|
||||||
|
images: [],
|
||||||
};
|
};
|
||||||
return flight;
|
return flight;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user