some Fixes, see #45

This commit is contained in:
PxlLoewe
2025-07-05 00:59:22 -07:00
parent 80d2704852
commit 2cdbab9f28
9 changed files with 240 additions and 176 deletions

View File

@@ -296,6 +296,7 @@ export const MissionForm = () => {
<option value="primär">PRIMÄR</option> <option value="primär">PRIMÄR</option>
<option value="sekundär">SEKUNDÄR</option> <option value="sekundär">SEKUNDÄR</option>
</select> </select>
{form.watch("type") === "primär" && ( {form.watch("type") === "primär" && (
<> <>
<select <select
@@ -321,74 +322,75 @@ export const MissionForm = () => {
{form.formState.errors.missionKeywordCategory && ( {form.formState.errors.missionKeywordCategory && (
<p className="text-error text-sm mb-4">Bitte wähle eine Kategorie aus.</p> <p className="text-error text-sm mb-4">Bitte wähle eine Kategorie aus.</p>
)} )}
<select
{...form.register("missionKeywordAbbreviation")}
className="select select-primary select-bordered w-full mb-4"
onChange={(e) => {
const keyword = keywords?.find((k) => k.abreviation === e.target.value);
form.setValue("missionKeywordName", keyword?.name || (null as any));
form.setValue("missionKeywordAbbreviation", keyword?.abreviation || (null as any));
form.setValue("hpgMissionString", null);
}}
value={form.watch("missionKeywordAbbreviation") || "please_select"}
>
<option disabled value={"please_select"}>
Einsatzstichwort auswählen...
</option>
{keywords &&
keywords
.filter((k) => k.category === form.watch("missionKeywordCategory"))
.map((keyword) => (
<option key={keyword.id} value={keyword.abreviation}>
{keyword.name}
</option>
))}
</select>
{form.formState.errors.missionKeywordAbbreviation && (
<p className="text-error text-sm mb-4">Bitte wähle ein Stichwort aus.</p>
)}
<div className="mb-4">
<select
{...form.register("hpgMissionString")}
onChange={(e) => {
form.setValue("hpgMissionString", e.target.value);
const [name] = e.target.value.split(":");
const allHpgMissionTypes = keywords?.map((k) => k.hpgMissionTypes).flat();
const missionAdditionalInfo = form.watch("missionAdditionalInfo");
if (
!missionAdditionalInfo ||
allHpgMissionTypes?.find((t) => {
const [hpgName] = t.split(":");
return hpgName === missionAdditionalInfo;
})
) {
form.setValue("missionAdditionalInfo", name || "");
}
}}
className="select select-primary select-bordered w-full mb-2"
value={form.watch("hpgMissionString") || "please_select"}
>
<option disabled value="please_select">
Einsatz Szenario auswählen...
</option>
{keywords &&
keywords
.find((k) => k.name === form.watch("missionKeywordName"))
?.hpgMissionTypes?.map((missionString) => {
const [name] = missionString.split(":");
return (
<option key={missionString} value={missionString}>
{name}
</option>
);
})}
</select>
{validationRequired && (
<p className="text-sm text-warning">Szenario wird vor Alarmierung HPG-Validiert.</p>
)}
</div>
</> </>
)} )}
<select
{...form.register("missionKeywordAbbreviation")}
className="select select-primary select-bordered w-full mb-4"
onChange={(e) => {
const keyword = keywords?.find((k) => k.abreviation === e.target.value);
form.setValue("missionKeywordName", keyword?.name || (null as any));
form.setValue("missionKeywordAbbreviation", keyword?.abreviation || (null as any));
form.setValue("hpgMissionString", null);
}}
value={form.watch("missionKeywordAbbreviation") || "please_select"}
>
<option disabled value={"please_select"}>
Einsatzstichwort auswählen...
</option>
{keywords &&
keywords
.filter((k) => k.category === form.watch("missionKeywordCategory"))
.map((keyword) => (
<option key={keyword.id} value={keyword.abreviation}>
{keyword.name}
</option>
))}
</select>
{form.formState.errors.missionKeywordAbbreviation && (
<p className="text-error text-sm mb-4">Bitte wähle ein Stichwort aus.</p>
)}
<div className="mb-4">
<select
{...form.register("hpgMissionString")}
onChange={(e) => {
form.setValue("hpgMissionString", e.target.value);
const [name] = e.target.value.split(":");
const allHpgMissionTypes = keywords?.map((k) => k.hpgMissionTypes).flat();
const missionAdditionalInfo = form.watch("missionAdditionalInfo");
if (
!missionAdditionalInfo ||
allHpgMissionTypes?.find((t) => {
const [hpgName] = t.split(":");
return hpgName === missionAdditionalInfo;
})
) {
form.setValue("missionAdditionalInfo", name || "");
}
}}
className="select select-primary select-bordered w-full mb-2"
value={form.watch("hpgMissionString") || "please_select"}
>
<option disabled value="please_select">
Einsatz Szenario auswählen...
</option>
{keywords &&
keywords
.find((k) => k.name === form.watch("missionKeywordName"))
?.hpgMissionTypes?.map((missionString) => {
const [name] = missionString.split(":");
return (
<option key={missionString} value={missionString}>
{name}
</option>
);
})}
</select>
{validationRequired && (
<p className="text-sm text-warning">Szenario wird vor Alarmierung HPG-Validiert.</p>
)}
</div>
<textarea <textarea
{...form.register("missionAdditionalInfo")} {...form.register("missionAdditionalInfo")}
placeholder="Einsatzinformationen" placeholder="Einsatzinformationen"

View File

@@ -5,10 +5,12 @@ import { useEffect, useRef, useState } from "react";
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import { getStationsAPI } from "_querys/stations"; import { getStationsAPI } from "_querys/stations";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { editConnectedAircraftAPI } from "_querys/aircrafts"; import { editConnectedAircraftAPI, getConnectedAircraftsAPI } from "_querys/aircrafts";
import { Prisma } from "@repo/db"; import { Prisma } from "@repo/db";
import { debug } from "console";
import { getNextDateWithTime } from "@repo/shared-components"; import { getNextDateWithTime } from "@repo/shared-components";
import { Select } from "_components/Select";
import { components } from "react-select";
import { Radio } from "lucide-react";
export const ConnectionBtn = () => { export const ConnectionBtn = () => {
const modalRef = useRef<HTMLDialogElement>(null); const modalRef = useRef<HTMLDialogElement>(null);
@@ -56,6 +58,11 @@ export const ConnectionBtn = () => {
const [logoffHours, logoffMinutes] = form.logoffTime?.split(":").map(Number) || []; const [logoffHours, logoffMinutes] = form.logoffTime?.split(":").map(Number) || [];
const { data: connectedAircrafts } = useQuery({
queryKey: ["aircrafts"],
queryFn: () => getConnectedAircraftsAPI(),
});
useEffect(() => { useEffect(() => {
if (!logoffHours || !logoffMinutes || !connection.connectedAircraft) return; if (!logoffHours || !logoffMinutes || !connection.connectedAircraft) return;
@@ -126,27 +133,35 @@ export const ConnectionBtn = () => {
<h3 className="text-lg font-bold mb-5">Als Pilot anmelden</h3> <h3 className="text-lg font-bold mb-5">Als Pilot anmelden</h3>
)} )}
{connection.status !== "connected" && ( {connection.status !== "connected" && (
<fieldset className="fieldset w-full"> <div className="w-full">
<label className="floating-label w-full text-base"> <Select
<span>Station</span> placeholder="Wähle eine Station"
<select onChange={(v) =>
onChange={(e) => setForm({
setForm({ ...form,
...form, selectedStationId: parseInt(v),
selectedStationId: parseInt(e.target.value), })
}) }
} value={form.selectedStationId ?? ""}
value={form.selectedStationId ?? ""} formatOptionLabel={(option: any) => option.component}
className="input w-full" options={
> stations?.map((station) => ({
{stations?.map((station) => ( value: station.id.toString(),
<option key={station.id} value={station.id}> label: station.bosCallsign,
{station.bosCallsign} component: (
</option> <div>
))} <span className="flex items-center gap-2">
</select> {connectedAircrafts?.find((a) => a.stationId == station.id) && (
</label> <Radio className="text-warning" size={15} />
</fieldset> )}
{station.bosCallsign}
</span>
</div>
),
})) ?? []
}
/>
</div>
)} )}
<fieldset className="fieldset w-full mt-2"> <fieldset className="fieldset w-full mt-2">
<label className="floating-label w-full text-base"> <label className="floating-label w-full text-base">

View File

@@ -223,6 +223,7 @@ const AircraftMarker = ({ aircraft }: { aircraft: ConnectedAircraft & { Station:
id: aircraft.id, id: aircraft.id,
}), }),
refetchInterval: 10000, refetchInterval: 10000,
enabled: openAircraftMarker.some((m) => m.id === aircraft.id),
}); });
useEffect(() => { useEffect(() => {

View File

@@ -19,6 +19,7 @@ import {
CheckCheck, CheckCheck,
Cross, Cross,
Radio, Radio,
Route,
} from "lucide-react"; } from "lucide-react";
import { import {
getPublicUser, getPublicUser,
@@ -190,6 +191,16 @@ const Einsatzdetails = ({
)} )}
</p> </p>
</div> </div>
{mission.type == "sekundär" && (
<>
<div className="divider mt-0 mb-0" />
<div className="text-sm font-semibold">
<p className="flex items-center gap-2">
<Route size={16} /> {mission.addressMissionDestination}
</p>
</div>
</>
)}
{mission.state === "draft" && ( {mission.state === "draft" && (
<div> <div>
<div className="divider mt-0 mb-0" /> <div className="divider mt-0 mb-0" />

View File

@@ -16,6 +16,7 @@ export const getOsmAddress = async (lat: number, lng: number) => {
road?: string; road?: string;
state?: string; state?: string;
city?: string; city?: string;
village?: string;
town?: string; town?: string;
}; };
display_name?: string; display_name?: string;
@@ -43,7 +44,7 @@ export const getOsmAddress = async (lat: number, lng: number) => {
return { return {
raw: data, raw: data,
parsed: { parsed: {
addressCity: data.address?.city || data.address?.town || "", addressCity: data.address?.city || data.address?.town || data.address?.village || "",
addressStreet, addressStreet,
addressZip: data.address?.postcode || "", addressZip: data.address?.postcode || "",
}, },

View File

@@ -15,6 +15,9 @@ export async function GET(request: NextRequest): Promise<NextResponse> {
gte: new Date(Date.now() - 2 * 60 * 60 * 1000), // Last 2 hours gte: new Date(Date.now() - 2 * 60 * 60 * 1000), // Last 2 hours
}, },
}, },
orderBy: {
timestamp: "desc",
},
}); });
return NextResponse.json(positionLog, { return NextResponse.json(positionLog, {

View File

@@ -177,9 +177,13 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }: ProfileFormPro
type="button" type="button"
className="btn btn-sm btn-outline" className="btn btn-sm btn-outline"
onClick={() => onClick={() =>
form.setValue("permissions", ["LOGIN_NEXTCLOUD", "PILOT", "DISPO", "AUDIO"], { form.setValue(
shouldDirty: true, "permissions",
}) ["LOGIN_NEXTCLOUD", "PILOT", "DISPO", "AUDIO", "ADMIN_EVENT"],
{
shouldDirty: true,
},
)
} }
onSubmit={() => false} onSubmit={() => false}
> >

View File

@@ -112,6 +112,7 @@ export function PaginatedTable<TData>({
onChange={(e) => { onChange={(e) => {
setSearchTerm(e.target.value); setSearchTerm(e.target.value);
handleSearchChange(e.target.value); handleSearchChange(e.target.value);
setPage(0); // Reset to first page on search
}} }}
className="input input-bordered w-full max-w-xs justify-end" className="input input-bordered w-full max-w-xs justify-end"
/> />

View File

@@ -17,96 +17,122 @@ export const PenaltyDropdown = ({
btnTip?: string; btnTip?: string;
Icon: ReactNode; Icon: ReactNode;
}) => { }) => {
const [open, setOpen] = useState(true);
const [reason, setReason] = useState(""); const [reason, setReason] = useState("");
const [until, setUntil] = useState<string>("default"); const [until, setUntil] = useState<string>("default");
return ( return (
<details className="dropdown dropdown-left dropdown-center"> <div className="dropdown dropdown-open">
<summary className={cn("btn btn-xs btn-square btn-soft", btnClassName)}>{Icon}</summary> <div tabIndex={0} role="button"></div>
<div <div className="indicator">
className="dropdown-content bg-base-100 rounded-box z-1 p-4 shadow-sm space-y-4 shadow-md"
style={{ minWidth: "500px", right: "40px" }}
>
<h2 className="text-xl font-blod text-center">{btnName}</h2>
<textarea
value={reason}
onChange={(e) => setReason(e.target.value)}
className="input w-full"
placeholder="Begründung"
style={{ minHeight: "100px" }}
/>
{showDatePicker && (
<select
className="select w-full select-bordered"
value={until}
onChange={(e) => setUntil(e.target.value)}
>
<option value="default" disabled>
Keine
</option>
<option value="1h">1 Stunde</option>
<option value="6h">6 Stunden</option>
<option value="12h">12 Stunden</option>
<option value="24h">24 Stunden</option>
<option value="72h">72 Stunden</option>
<option value="1w">1 Woche</option>
<option value="2w">2 Wochen</option>
<option value="1m">1 Monat</option>
<option value="3m">3 Monate</option>
<option value="6m">6 Monate</option>
<option value="1y">1 Jahr</option>
</select>
)}
<button <button
className={cn("btn w-full btn-square btn-soft tooltip tooltip-bottom", btnClassName)} className={cn("btn btn-xs btn-square btn-soft cursor-pointer", btnClassName)}
data-tip={btnTip} onClick={() => setOpen(!open)}
onClick={() => {
let untilDate: Date | null = null;
if (until !== "default") {
const now = new Date();
switch (until) {
case "1h":
untilDate = new Date(now.getTime() + 1 * 60 * 60 * 1000);
break;
case "6h":
untilDate = new Date(now.getTime() + 6 * 60 * 60 * 1000);
break;
case "12h":
untilDate = new Date(now.getTime() + 12 * 60 * 60 * 1000);
break;
case "24h":
untilDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
break;
case "72h":
untilDate = new Date(now.getTime() + 72 * 60 * 60 * 1000);
break;
case "1w":
untilDate = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
break;
case "2w":
untilDate = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000);
break;
case "1m":
untilDate = new Date(now.setMonth(now.getMonth() + 1));
break;
case "3m":
untilDate = new Date(now.setMonth(now.getMonth() + 3));
break;
case "6m":
untilDate = new Date(now.setMonth(now.getMonth() + 6));
break;
case "1y":
untilDate = new Date(now.setFullYear(now.getFullYear() + 1));
break;
default:
untilDate = null;
}
}
onClick({ reason, until: untilDate });
}}
> >
{Icon} {btnName} {Icon}
</button> </button>
</div> </div>
{open && (
<div
className="dropdown-content bg-base-100 rounded-box z-1 p-4 shadow-sm space-y-4 shadow-md"
style={{ minWidth: "500px", right: "40px" }}
>
<button
className="absolute top-2 right-2 btn btn-xs btn-circle btn-ghost"
onClick={() => setOpen(false)}
type="button"
>
<span className="text-xl leading-none">&times;</span>
</button>
<h2 className="text-xl font-bold text-center">{btnName}</h2>
<textarea
value={reason}
onChange={(e) => setReason(e.target.value)}
className="input w-full"
placeholder="Begründung"
style={{ minHeight: "100px" }}
/>
{showDatePicker && (
<select
className="select w-full select-bordered"
value={until}
onChange={(e) => setUntil(e.target.value)}
>
<option value="default" disabled>
Keine
</option>
<option value="1h">1 Stunde</option>
<option value="6h">6 Stunden</option>
<option value="12h">12 Stunden</option>
<option value="24h">24 Stunden</option>
<option value="72h">72 Stunden</option>
<option value="1w">1 Woche</option>
<option value="2w">2 Wochen</option>
<option value="1m">1 Monat</option>
<option value="3m">3 Monate</option>
<option value="6m">6 Monate</option>
<option value="1y">1 Jahr</option>
</select>
)}
<button
className={cn("btn w-full btn-square btn-soft tooltip tooltip-bottom", btnClassName)}
data-tip={btnTip}
onClick={() => {
let untilDate: Date | null = null;
if (until !== "default") {
const now = new Date();
switch (until) {
case "1h":
untilDate = new Date(now.getTime() + 1 * 60 * 60 * 1000);
break;
case "6h":
untilDate = new Date(now.getTime() + 6 * 60 * 60 * 1000);
break;
case "12h":
untilDate = new Date(now.getTime() + 12 * 60 * 60 * 1000);
break;
case "24h":
untilDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
break;
case "72h":
untilDate = new Date(now.getTime() + 72 * 60 * 60 * 1000);
break;
case "1w":
untilDate = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
break;
case "2w":
untilDate = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000);
break;
case "1m":
untilDate = new Date(now.setMonth(now.getMonth() + 1));
break;
case "3m":
untilDate = new Date(now.setMonth(now.getMonth() + 3));
break;
case "6m":
untilDate = new Date(now.setMonth(now.getMonth() + 6));
break;
case "1y":
untilDate = new Date(now.setFullYear(now.getFullYear() + 1));
break;
default:
untilDate = null;
}
}
onClick({ reason, until: untilDate });
}}
>
{Icon} {btnName}
</button>
</div>
)}
</div>
);
return (
<details className={"dropdown dropdown-left dropdown-center dropdown-open"}>
<summary
className={cn("btn btn-xs btn-square btn-soft", btnClassName)}
onClick={() => setOpen(true)}
></summary>
</details> </details>
); );
}; };