Merge pull request #147 from VAR-Virtual-Air-Rescue/staging
@everyone | Wir haben eine kurze Downtime überwunden und stellen euch heute v2.0.7 vor. In der Vergangenheit haben wir viel an der Dispositionsseite gearbeitet. Das ändert sich heute. Wir aktualisieren die Bedieneinheit für Piloten auf eine neue Softwareversion, die dem Sepura SCG22 nachempfunden ist und so tatsächlich in vielen Hubschraubern verbaut ist. ### Neue Features: - Ladescreen beim Einschalten - Wechseln der Rufgruppe direkt im Gerät - Status senden/empfangen - SDS-Text bzw. senden/empfangen - Nachtmodus ab 22:00 Uhr - Popup bei Funkverkehr auf der Rufgruppe Eine entsprechende Dokumentation findet ihr[ in den Docs](https://docs.virtualairrescue.com/allgemein/var-systeme/leitstelle/pilot.html). ### Weiteres: - kleinere Bugfixes - Performanceupgrades durch verbessertes Backup-Handling
@@ -2,6 +2,7 @@ import {
|
||||
AdminMessage,
|
||||
getPublicUser,
|
||||
MissionLog,
|
||||
MissionSdsStatusLog,
|
||||
NotificationPayload,
|
||||
Prisma,
|
||||
prisma,
|
||||
@@ -130,6 +131,44 @@ router.patch("/:id", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/:id/send-sds-message", async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { sdsMessage } = req.body as { sdsMessage: MissionSdsStatusLog };
|
||||
|
||||
if (!sdsMessage.data.stationId || !id) {
|
||||
res.status(400).json({ error: "Missing aircraftId or stationId" });
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.mission.updateMany({
|
||||
where: {
|
||||
state: "running",
|
||||
missionStationIds: {
|
||||
has: sdsMessage.data.stationId,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
missionLog: {
|
||||
push: sdsMessage as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
io.to(
|
||||
sdsMessage.data.direction === "to-lst" ? "dispatchers" : `station:${sdsMessage.data.stationId}`,
|
||||
).emit(sdsMessage.data.direction === "to-lst" ? "notification" : "sds-status", {
|
||||
type: "station-status",
|
||||
status: sdsMessage.data.status,
|
||||
message: "SDS Status Message",
|
||||
data: {
|
||||
aircraftId: parseInt(id),
|
||||
stationId: sdsMessage.data.stationId,
|
||||
},
|
||||
} as NotificationPayload);
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
// Kick a connectedAircraft by ID
|
||||
router.delete("/:id", async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
ARG NEXT_PUBLIC_DISPATCH_URL
|
||||
ARG NEXT_PUBLIC_DISPATCH_SERVER_URL
|
||||
ARG NEXT_PUBLIC_HUB_URL
|
||||
ARG NEXT_PUBLIC_DISPATCH_SERVICE_ID
|
||||
ARG NEXT_PUBLIC_LIVEKIT_URL
|
||||
ARG NEXT_PUBLIC_DISCORD_URL
|
||||
ARG NEXT_PUBLIC_OPENAIP_ACCESS
|
||||
ARG NEXT_PUBLIC_DISPATCH_URL="http://localhost:3001"
|
||||
ARG NEXT_PUBLIC_DISPATCH_SERVER_URL="http://localhost:4001"
|
||||
ARG NEXT_PUBLIC_HUB_URL="http://localhost:3002"
|
||||
ARG NEXT_PUBLIC_DISPATCH_SERVICE_ID="1"
|
||||
ARG NEXT_PUBLIC_LIVEKIT_URL="http://localhost:7880"
|
||||
ARG NEXT_PUBLIC_DISCORD_URL="https://discord.com"
|
||||
ARG NEXT_PUBLIC_OPENAIP_ACCESS=""
|
||||
|
||||
ENV NEXT_PUBLIC_DISPATCH_SERVER_URL=$NEXT_PUBLIC_DISPATCH_SERVER_URL
|
||||
ENV NEXT_PUBLIC_DISPATCH_URL=$NEXT_PUBLIC_DISPATCH_URL
|
||||
@@ -16,13 +16,13 @@ ENV NEXT_PUBLIC_LIVEKIT_URL=$NEXT_PUBLIC_LIVEKIT_URL
|
||||
ENV NEXT_PUBLIC_OPENAIP_ACCESS=$NEXT_PUBLIC_OPENAIP_ACCESS
|
||||
ENV NEXT_PUBLIC_DISCORD_URL=$NEXT_PUBLIC_DISCORD_URL
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
ENV PNPM_HOME="/usr/local/pnpm"
|
||||
ENV PATH="${PNPM_HOME}:${PATH}"
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
RUN pnpm add -g turbo@^2.5
|
||||
|
||||
FROM base AS builder
|
||||
RUN apk update
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
@@ -31,12 +31,20 @@ WORKDIR /usr/app
|
||||
RUN echo "NEXT_PUBLIC_HUB_URL is: $NEXT_PUBLIC_HUB_URL"
|
||||
RUN echo "NEXT_PUBLIC_DISPATCH_SERVICE_ID is: $NEXT_PUBLIC_DISPATCH_SERVICE_ID"
|
||||
RUN echo "NEXT_PUBLIC_DISPATCH_SERVER_URL is: $NEXT_PUBLIC_DISPATCH_SERVER_URL"
|
||||
RUN echo "NEXT_PUBLIC_LIVEKIT_URL is: $NEXT_PUBLIC_LIVEKIT_URL"
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN turbo prune dispatch --docker
|
||||
|
||||
FROM base AS installer
|
||||
|
||||
ENV PNPM_HOME="/usr/local/pnpm"
|
||||
ENV PATH="${PNPM_HOME}:${PATH}"
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
RUN pnpm add -g turbo@^2.5
|
||||
|
||||
RUN apk update
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
@@ -50,19 +58,22 @@ COPY --from=builder /usr/app/out/full/ .
|
||||
|
||||
RUN turbo run build
|
||||
|
||||
FROM base AS runner
|
||||
FROM node:22-alpine AS runner
|
||||
WORKDIR /usr/app
|
||||
|
||||
# Don't run production as root
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
USER nextjs
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=installer --chown=nextjs:nodejs /usr/app/ ./
|
||||
COPY --from=installer --chown=nextjs:nodejs /usr/app/apps/dispatch/.next/standalone ./
|
||||
COPY --from=installer --chown=nextjs:nodejs /usr/app/apps/dispatch/.next/static ./apps/dispatch/.next/static
|
||||
COPY --from=installer --chown=nextjs:nodejs /usr/app/apps/dispatch/public ./apps/dispatch/public
|
||||
|
||||
USER nextjs
|
||||
|
||||
# Expose the application port
|
||||
EXPOSE 3001
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["pnpm", "--dir", "apps/dispatch", "run", "start"]
|
||||
CMD ["node", "apps/dispatch/server.js"]
|
||||
@@ -14,7 +14,7 @@ export const ConnectionBtn = () => {
|
||||
const connection = useDispatchConnectionStore((state) => state);
|
||||
const [form, setForm] = useState({
|
||||
logoffTime: "",
|
||||
selectedZone: "LST_01",
|
||||
selectedZone: "VAR_LST_RD_01",
|
||||
ghostMode: false,
|
||||
});
|
||||
const changeDispatcherMutation = useMutation({
|
||||
|
||||
29
apps/dispatch/app/(app)/pilot/_components/mrt/Base.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useEffect } from "react"; // ...existing code...
|
||||
import { useMrtStore } from "_store/pilot/MrtStore";
|
||||
import Image from "next/image";
|
||||
import DAY_BASE_IMG from "./images/Base_NoScreen_Day.png";
|
||||
import NIGHT_BASE_IMG from "./images/Base_NoScreen_Night.png";
|
||||
|
||||
export const MrtBase = () => {
|
||||
const { nightMode, setNightMode, page } = useMrtStore((state) => state);
|
||||
|
||||
useEffect(() => {
|
||||
const checkNightMode = () => {
|
||||
const currentHour = new Date().getHours();
|
||||
setNightMode(currentHour >= 22 || currentHour < 8);
|
||||
};
|
||||
|
||||
checkNightMode(); // Initial check
|
||||
const intervalId = setInterval(checkNightMode, 60000); // Check every minute
|
||||
|
||||
return () => clearInterval(intervalId); // Cleanup on unmount
|
||||
}, [setNightMode]); // ...existing code...
|
||||
|
||||
return (
|
||||
<Image
|
||||
src={nightMode && page !== "off" ? NIGHT_BASE_IMG : DAY_BASE_IMG}
|
||||
alt=""
|
||||
className="z-30 col-span-full row-span-full"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
Before Width: | Height: | Size: 252 KiB |
|
Before Width: | Height: | Size: 366 KiB |
@@ -1,22 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
import MrtImage from "./MRT.png";
|
||||
import MrtMessageImage from "./MRT_MESSAGE.png";
|
||||
import { useButtons } from "./useButtons";
|
||||
import { useSounds } from "./useSounds";
|
||||
import "./mrt.css";
|
||||
import Image from "next/image";
|
||||
import { useMrtStore } from "_store/pilot/MrtStore";
|
||||
|
||||
const MRT_BUTTON_STYLES: CSSProperties = {
|
||||
cursor: "pointer",
|
||||
zIndex: "9999",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
};
|
||||
const MRT_DISPLAYLINE_STYLES: CSSProperties = {
|
||||
color: "white",
|
||||
zIndex: 1,
|
||||
};
|
||||
import { MrtBase } from "./Base";
|
||||
import { MrtDisplay } from "./MrtDisplay";
|
||||
import { MrtButtons } from "./MrtButtons";
|
||||
import { MrtPopups } from "./MrtPopups";
|
||||
|
||||
export interface DisplayLineProps {
|
||||
lineStyle?: CSSProperties;
|
||||
@@ -27,45 +14,7 @@ export interface DisplayLineProps {
|
||||
textSize: "1" | "2" | "3" | "4";
|
||||
}
|
||||
|
||||
const DisplayLine = ({
|
||||
style = {},
|
||||
textLeft,
|
||||
textMid,
|
||||
textRight,
|
||||
textSize,
|
||||
lineStyle,
|
||||
}: DisplayLineProps) => {
|
||||
const INNER_TEXT_PARTS: CSSProperties = {
|
||||
fontFamily: "Melder",
|
||||
flex: "1",
|
||||
flexBasis: "auto",
|
||||
overflowWrap: "break-word",
|
||||
...lineStyle,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`text-${textSize}`}
|
||||
style={{
|
||||
fontFamily: "Famirids",
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<span style={INNER_TEXT_PARTS}>{textLeft}</span>
|
||||
<span style={{ textAlign: "center", ...INNER_TEXT_PARTS }}>{textMid}</span>
|
||||
<span style={{ textAlign: "end", ...INNER_TEXT_PARTS }}>{textRight}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Mrt = () => {
|
||||
useSounds();
|
||||
const { handleButton } = useButtons();
|
||||
const { lines, page } = useMrtStore((state) => state);
|
||||
|
||||
return (
|
||||
<div
|
||||
id="mrt-container"
|
||||
@@ -78,150 +27,16 @@ export const Mrt = () => {
|
||||
maxHeight: "100%",
|
||||
maxWidth: "100%",
|
||||
color: "white",
|
||||
gridTemplateColumns: "21.83% 4.43% 24.42% 18.08% 5.93% 1.98% 6.00% 1.69% 6.00% 9.35%",
|
||||
gridTemplateRows: "21.58% 11.87% 3.55% 5.00% 6.84% 0.53% 3.03% 11.84% 3.55% 11.84% 20.39%",
|
||||
gridTemplateColumns:
|
||||
"9.75% 4.23% 8.59% 7.30% 1.16% 7.30% 1.23% 7.16% 1.09% 7.30% 3.68% 4.23% 5.59% 6.07% 1.91% 6.07% 1.84% 6.21% 9.28%",
|
||||
gridTemplateRows:
|
||||
"21.55% 11.83% 3.55% 2.50% 9.46% 2.76% 0.66% 4.99% 6.83% 3.55% 1.97% 9.99% 4.20% 11.04% 5.12%",
|
||||
}}
|
||||
>
|
||||
{page !== "sds" && (
|
||||
<Image
|
||||
src={MrtImage}
|
||||
alt="MrtImage"
|
||||
style={{
|
||||
zIndex: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
gridArea: "1 / 1 / 13 / 13",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{page === "sds" && (
|
||||
<Image
|
||||
src={MrtMessageImage}
|
||||
alt="MrtImage-Message"
|
||||
style={{
|
||||
zIndex: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
gridArea: "1 / 1 / 13 / 13",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleButton("home")}
|
||||
style={{ gridArea: "2 / 4 / 3 / 5", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleButton("1")}
|
||||
style={{ gridArea: "2 / 5 / 3 / 6", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleButton("2")}
|
||||
style={{ gridArea: "2 / 7 / 3 / 7", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleButton("3")}
|
||||
style={{ gridArea: "2 / 9 / 3 / 10", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleButton("4")}
|
||||
style={{ gridArea: "4 / 5 / 6 / 6", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleButton("5")}
|
||||
style={{ gridArea: "4 / 7 / 6 / 7", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleButton("6")}
|
||||
style={{ gridArea: "4 / 9 / 6 / 10", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleButton("7")}
|
||||
style={{ gridArea: "8 / 5 / 9 / 6", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleButton("8")}
|
||||
style={{ gridArea: "8 / 7 / 9 / 7", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleButton("9")}
|
||||
style={{ gridArea: "8 / 9 / 9 / 10", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleButton("0")}
|
||||
style={{ gridArea: "10 / 7 / 11 / 8", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
|
||||
{lines[0] && (
|
||||
<DisplayLine
|
||||
{...lines[0]}
|
||||
style={
|
||||
page === "sds"
|
||||
? {
|
||||
gridArea: "2 / 3 / 3 / 4",
|
||||
marginLeft: "9px",
|
||||
marginTop: "auto",
|
||||
|
||||
...MRT_DISPLAYLINE_STYLES,
|
||||
...lines[0]?.style,
|
||||
}
|
||||
: {
|
||||
gridArea: "4 / 3 / 5 / 4",
|
||||
marginLeft: "9px",
|
||||
marginTop: "auto",
|
||||
...MRT_DISPLAYLINE_STYLES,
|
||||
...lines[0]?.style,
|
||||
}
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{lines[1] && (
|
||||
<DisplayLine
|
||||
lineStyle={{
|
||||
overflowX: "hidden",
|
||||
maxHeight: "100%",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
{...lines[1]}
|
||||
style={
|
||||
page === "sds"
|
||||
? {
|
||||
gridArea: "4 / 2 / 10 / 4",
|
||||
marginLeft: "3px",
|
||||
...MRT_DISPLAYLINE_STYLES,
|
||||
...lines[1].style,
|
||||
}
|
||||
: {
|
||||
gridArea: "5 / 3 / 7 / 4",
|
||||
marginLeft: "3px",
|
||||
marginTop: "auto",
|
||||
...MRT_DISPLAYLINE_STYLES,
|
||||
...lines[1].style,
|
||||
}
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{lines[2] && (
|
||||
<DisplayLine
|
||||
{...lines[2]}
|
||||
style={{
|
||||
gridArea: "8 / 2 / 9 / 4",
|
||||
...MRT_DISPLAYLINE_STYLES,
|
||||
...lines[2]?.style,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{lines[3] && (
|
||||
<DisplayLine
|
||||
{...lines[3]}
|
||||
style={{
|
||||
gridArea: "9 / 2 / 10 / 4",
|
||||
marginRight: "10px",
|
||||
...MRT_DISPLAYLINE_STYLES,
|
||||
...lines[3]?.style,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<MrtPopups />
|
||||
<MrtDisplay />
|
||||
<MrtButtons />
|
||||
<MrtBase />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
150
apps/dispatch/app/(app)/pilot/_components/mrt/MrtButtons.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import { CSSProperties, useRef } from "react";
|
||||
import { useButtons } from "./useButtons";
|
||||
|
||||
const MRT_BUTTON_STYLES: CSSProperties = {
|
||||
cursor: "pointer",
|
||||
zIndex: "9999",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
};
|
||||
|
||||
interface MrtButtonProps {
|
||||
onClick: () => void;
|
||||
onHold?: () => void;
|
||||
style: CSSProperties;
|
||||
}
|
||||
|
||||
const MrtButton = ({ onClick, onHold, style }: MrtButtonProps) => {
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleMouseDown = () => {
|
||||
if (!onHold) return;
|
||||
timeoutRef.current = setTimeout(handleTimeoutExpired, 500);
|
||||
};
|
||||
|
||||
const handleTimeoutExpired = () => {
|
||||
timeoutRef.current = null;
|
||||
if (onHold) {
|
||||
onHold();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={() => timeoutRef.current && clearTimeout(timeoutRef.current)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const MrtButtons = () => {
|
||||
const { handleHold, handleKlick } = useButtons();
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* BELOW DISPLAY */}
|
||||
<MrtButton
|
||||
onClick={handleKlick("arrow-left")}
|
||||
onHold={handleHold("arrow-left")}
|
||||
style={{ gridArea: "14 / 4 / 15 / 5", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<MrtButton
|
||||
onClick={handleKlick("arrow-down")}
|
||||
onHold={handleHold("arrow-down")}
|
||||
style={{ gridArea: "14 / 6 / 15 / 7", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<MrtButton
|
||||
onClick={handleKlick("arrow-up")}
|
||||
onHold={handleHold("arrow-up")}
|
||||
style={{ gridArea: "14 / 8 / 15 / 9", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<MrtButton
|
||||
onClick={handleKlick("arrow-right")}
|
||||
onHold={handleHold("arrow-right")}
|
||||
style={{ gridArea: "14 / 10 / 15 / 11", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
|
||||
<MrtButton
|
||||
onClick={handleKlick("wheel-knob")}
|
||||
onHold={handleHold("wheel-knob")}
|
||||
style={{ gridArea: "14 / 2 / 15 / 4", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
{/* LINE SELECT KEY */}
|
||||
<MrtButton
|
||||
onHold={handleHold("3r")}
|
||||
onClick={handleKlick("3r")}
|
||||
style={{ gridArea: "9 / 12 / 11 / 13", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<MrtButton
|
||||
onHold={handleHold("3l")}
|
||||
onClick={handleKlick("3l")}
|
||||
style={{ gridArea: "9 / 2 / 11 / 3", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
{/* NUM PAD */}
|
||||
<MrtButton
|
||||
onHold={handleHold("1")}
|
||||
onClick={handleKlick("1")}
|
||||
style={{ gridArea: "2 / 14 / 3 / 15", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<MrtButton
|
||||
onHold={handleHold("2")}
|
||||
onClick={handleKlick("2")}
|
||||
style={{ gridArea: "2 / 16 / 3 / 17", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<MrtButton
|
||||
onHold={handleHold("3")}
|
||||
onClick={handleKlick("3")}
|
||||
style={{ gridArea: "2 / 18 / 3 / 19", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<MrtButton
|
||||
onHold={handleHold("4")}
|
||||
onClick={handleKlick("4")}
|
||||
style={{ gridArea: "4 / 14 / 6 / 15", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<MrtButton
|
||||
onHold={handleHold("5")}
|
||||
onClick={handleKlick("5")}
|
||||
style={{ gridArea: "4 / 16 / 6 / 17", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<MrtButton
|
||||
onHold={handleHold("6")}
|
||||
onClick={handleKlick("6")}
|
||||
style={{ gridArea: "4 / 18 / 6 / 19", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<MrtButton
|
||||
onHold={handleHold("7")}
|
||||
onClick={handleKlick("7")}
|
||||
style={{ gridArea: "8 / 14 / 10 / 15", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<MrtButton
|
||||
onHold={handleHold("8")}
|
||||
onClick={handleKlick("8")}
|
||||
style={{ gridArea: "8 / 16 / 10 / 17", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<MrtButton
|
||||
onHold={handleHold("9")}
|
||||
onClick={handleKlick("9")}
|
||||
style={{ gridArea: "8 / 18 / 10 / 19", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<MrtButton
|
||||
onHold={handleHold("0")}
|
||||
onClick={handleKlick("0")}
|
||||
style={{ gridArea: "11 / 16 / 13 / 17", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<MrtButton
|
||||
onHold={handleHold("end-call")}
|
||||
onClick={handleKlick("end-call")}
|
||||
style={{ gridArea: "13 / 16 / 15 / 17", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
16
apps/dispatch/app/(app)/pilot/_components/mrt/MrtDisplay.css
Normal file
@@ -0,0 +1,16 @@
|
||||
.transition-image {
|
||||
clip-path: inset(0 0 100% 0);
|
||||
}
|
||||
|
||||
.transition-image.animate-reveal {
|
||||
animation: revealFromTop 0.6s linear forwards;
|
||||
}
|
||||
|
||||
@keyframes revealFromTop {
|
||||
from {
|
||||
clip-path: inset(0 0 100% 0);
|
||||
}
|
||||
to {
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
}
|
||||
270
apps/dispatch/app/(app)/pilot/_components/mrt/MrtDisplay.tsx
Normal file
@@ -0,0 +1,270 @@
|
||||
"use client";
|
||||
|
||||
import { SetPageParams, useMrtStore } from "_store/pilot/MrtStore";
|
||||
import Image, { StaticImageData } from "next/image";
|
||||
import PAGE_HOME from "./images/PAGE_Home.png";
|
||||
import PAGE_HOME_NO_GROUP from "./images/PAGE_Home_no_group.png";
|
||||
import PAGE_Call from "./images/PAGE_Call.png";
|
||||
import PAGE_Off from "./images/PAGE_Off.png";
|
||||
import PAGE_STARTUP from "./images/PAGE_Startup.png";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { cn, useDebounce } from "@repo/shared-components";
|
||||
import "./MrtDisplay.css";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
||||
import { fmsStatusDescriptionShort } from "_data/fmsStatusDescription";
|
||||
import { useAudioStore } from "_store/audioStore";
|
||||
import { ROOMS } from "_data/livekitRooms";
|
||||
|
||||
export const MrtDisplay = () => {
|
||||
const { page, setPage, popup, setPopup, setStringifiedData, stringifiedData } = useMrtStore(
|
||||
(state) => state,
|
||||
);
|
||||
const callEstablishedRef = useRef(false);
|
||||
const session = useSession();
|
||||
const { connectedAircraft, selectedStation } = usePilotConnectionStore((state) => state);
|
||||
const { room, speakingParticipants, isTalking, state } = useAudioStore((state) => state);
|
||||
const [pageImage, setPageImage] = useState<{
|
||||
src: StaticImageData;
|
||||
name: SetPageParams["page"];
|
||||
}>({
|
||||
src: PAGE_Off,
|
||||
name: "off",
|
||||
});
|
||||
const [nextImage, setNextImage] = useState<
|
||||
| {
|
||||
src: StaticImageData;
|
||||
name: SetPageParams["page"];
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
if (!nextImage) return;
|
||||
setPageImage(nextImage);
|
||||
setNextImage(undefined);
|
||||
},
|
||||
1000,
|
||||
[nextImage],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if ((speakingParticipants.length > 0 || isTalking) && page === "home") {
|
||||
setPage({
|
||||
page: "voice-call",
|
||||
});
|
||||
}
|
||||
}, [speakingParticipants, isTalking, page, setPage]);
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
if (page === "startup") {
|
||||
setPage({
|
||||
page: "home",
|
||||
});
|
||||
}
|
||||
},
|
||||
6000,
|
||||
[page, setPage],
|
||||
);
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
if (page === "startup") {
|
||||
setPopup({
|
||||
popup: "login",
|
||||
});
|
||||
}
|
||||
},
|
||||
7500,
|
||||
[page, setPage],
|
||||
);
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
if (page === "voice-call" && speakingParticipants.length === 0 && !isTalking) {
|
||||
setPage({
|
||||
page: "home",
|
||||
});
|
||||
}
|
||||
},
|
||||
4000,
|
||||
[page, setPage, speakingParticipants, isTalking],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const timeouts: NodeJS.Timeout[] = [];
|
||||
if (page === "voice-call") {
|
||||
setStringifiedData({
|
||||
callTextHeader: "Wählen",
|
||||
});
|
||||
timeouts.push(
|
||||
setTimeout(() => {
|
||||
setStringifiedData({
|
||||
callTextHeader: "Anruf...",
|
||||
});
|
||||
}, 500),
|
||||
|
||||
setTimeout(() => {
|
||||
setStringifiedData({
|
||||
callTextHeader: "Gruppenruf",
|
||||
});
|
||||
}, 800),
|
||||
setTimeout(() => {
|
||||
callEstablishedRef.current = true;
|
||||
}, 1500),
|
||||
);
|
||||
}
|
||||
return () => {
|
||||
timeouts.forEach((t) => clearTimeout(t));
|
||||
};
|
||||
}, [page, setStringifiedData]);
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
if (isTalking && page === "voice-call") {
|
||||
setStringifiedData({
|
||||
callTextHeader: "Sprechen",
|
||||
});
|
||||
}
|
||||
},
|
||||
1500,
|
||||
[page, isTalking],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTalking && page === "voice-call" && callEstablishedRef.current) {
|
||||
console.log("SET TO SPRECHEN", stringifiedData.callTextHeader);
|
||||
setStringifiedData({
|
||||
callTextHeader: "Sprechen",
|
||||
});
|
||||
} else if (
|
||||
!isTalking &&
|
||||
page === "voice-call" &&
|
||||
stringifiedData.callTextHeader === "Sprechen"
|
||||
) {
|
||||
setStringifiedData({
|
||||
callTextHeader: "Gruppenruf",
|
||||
});
|
||||
}
|
||||
}, [page, stringifiedData.callTextHeader, isTalking, setStringifiedData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (page !== "voice-call") {
|
||||
callEstablishedRef.current = false;
|
||||
}
|
||||
|
||||
switch (page) {
|
||||
case "home":
|
||||
if (state == "connected") {
|
||||
setNextImage({ src: PAGE_HOME, name: "home" });
|
||||
} else {
|
||||
setNextImage({ src: PAGE_HOME_NO_GROUP, name: "home" });
|
||||
}
|
||||
break;
|
||||
case "voice-call":
|
||||
setNextImage({ src: PAGE_Call, name: "voice-call" });
|
||||
break;
|
||||
case "off":
|
||||
setNextImage({ src: PAGE_Off, name: "off" });
|
||||
break;
|
||||
case "startup":
|
||||
setNextImage({ src: PAGE_STARTUP, name: "startup" });
|
||||
break;
|
||||
}
|
||||
}, [page, state]);
|
||||
|
||||
const DisplayText = ({ pageName }: { pageName: SetPageParams["page"] }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn("font-semibold text-[#000d60]", !!popup && "filter")}
|
||||
style={{
|
||||
fontFamily: "Bahnschrift",
|
||||
}}
|
||||
>
|
||||
{pageName == "startup" && (
|
||||
<p className="absolute left-[17%] top-[65%] h-[10%] w-[39%] text-center">
|
||||
Bediengerät #{session.data?.user?.publicId}
|
||||
</p>
|
||||
)}
|
||||
{pageName == "home" && (
|
||||
<>
|
||||
<p className="absolute left-[24%] top-[21%] h-[4%] w-[1%] text-xs">
|
||||
{(room?.numParticipants || 1) - 1}
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
"absolute left-[17%] top-[25%] h-[8%] w-[39%] text-center",
|
||||
!connectedAircraft && "text-red-600",
|
||||
)}
|
||||
>
|
||||
{connectedAircraft && (
|
||||
<>
|
||||
Status {connectedAircraft.fmsStatus} -{" "}
|
||||
{fmsStatusDescriptionShort[connectedAircraft?.fmsStatus || "0"]}
|
||||
</>
|
||||
)}
|
||||
{!connectedAircraft && <>Keine Verbindung</>}
|
||||
</p>
|
||||
<p className="absolute left-[22.7%] top-[37.8%] flex h-[5%] w-[34%] items-center text-xs">
|
||||
{state == "connected" ? room?.name : "Keine RG gewählt"}
|
||||
</p>
|
||||
<p className="absolute left-[28%] top-[44.5%] h-[8%] w-[34%] text-xs">
|
||||
{state == "connected" && ROOMS.find((r) => r.name === room?.name)?.id}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{pageName == "voice-call" && (
|
||||
<div>
|
||||
<p className="absolute left-[18%] top-[18.8%] flex h-[10%] w-[37%] items-center">
|
||||
{stringifiedData.callTextHeader}
|
||||
</p>
|
||||
<p className="absolute left-[18%] top-[35%] h-[8%] w-[38%]">
|
||||
{isTalking && selectedStation?.bosCallsignShort}
|
||||
{speakingParticipants.length > 0 &&
|
||||
speakingParticipants.map((p) => p.attributes.role).join(", ")}
|
||||
</p>
|
||||
<p className="absolute left-[18%] top-[53.5%] h-[8%] w-[38%]">
|
||||
{room?.name || "Keine RG gefunden"}
|
||||
</p>
|
||||
<p className="absolute left-[18%] top-[60%] h-[8%] w-[36.7%] text-right">
|
||||
{ROOMS.find((r) => r.name === room?.name)?.id}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
src={pageImage.src}
|
||||
alt=""
|
||||
width={1000}
|
||||
height={1000}
|
||||
className={cn(popup && "brightness-75 filter", "z-10 col-span-full row-span-full")}
|
||||
/>
|
||||
{nextImage && (
|
||||
<div
|
||||
className={cn(
|
||||
popup && "brightness-75 filter",
|
||||
"transition-image animate-reveal relative z-20 col-span-full row-span-full",
|
||||
)}
|
||||
>
|
||||
<Image src={nextImage.src} alt="" width={1000} height={1000} className="h-full w-full" />
|
||||
<DisplayText pageName={nextImage.name} />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
popup && "brightness-75 filter",
|
||||
"relative z-10 col-span-full row-span-full overflow-hidden",
|
||||
)}
|
||||
>
|
||||
<DisplayText pageName={pageImage.name} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
152
apps/dispatch/app/(app)/pilot/_components/mrt/MrtPopups.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { SetPopupParams, useMrtStore } from "_store/pilot/MrtStore";
|
||||
import Image, { StaticImageData } from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import IAMGE_POPUP_LOGIN from "./images/POPUP_login.png";
|
||||
import GROUP_SELECTION_POPUP_LOGIN from "./images/POPUP_group_selection.png";
|
||||
import IAMGE_POPUP_SDS_RECEIVED from "./images/POPUP_SDS_incomming.png";
|
||||
import IAMGE_POPUP_SDS_SENT from "./images/POPUP_SDS_sent.png";
|
||||
import IAMGE_POPUP_STATUS_SENT from "./images/POPUP_Status_sent.png";
|
||||
import { ROOMS } from "_data/livekitRooms";
|
||||
import { cn, useDebounce } from "@repo/shared-components";
|
||||
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
||||
import { fmsStatusDescription, fmsStatusDescriptionShort } from "_data/fmsStatusDescription";
|
||||
import { pilotSocket } from "(app)/pilot/socket";
|
||||
import { StationStatus } from "@repo/db";
|
||||
import { useSounds } from "./useSounds";
|
||||
import { useButtons } from "./useButtons";
|
||||
import { useAudioStore } from "_store/audioStore";
|
||||
|
||||
export const MrtPopups = () => {
|
||||
const { sdsReceivedSoundRef } = useSounds();
|
||||
const { handleKlick } = useButtons();
|
||||
const { selectedRoom } = useAudioStore();
|
||||
const { popup, page, setPopup, setStringifiedData, stringifiedData } = useMrtStore(
|
||||
(state) => state,
|
||||
);
|
||||
const { connectedAircraft, status } = usePilotConnectionStore((state) => state);
|
||||
const [popupImage, setPopupImage] = useState<StaticImageData | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
switch (popup) {
|
||||
case "status-sent":
|
||||
setPopupImage(IAMGE_POPUP_STATUS_SENT);
|
||||
break;
|
||||
case "sds-sent":
|
||||
setPopupImage(IAMGE_POPUP_SDS_SENT);
|
||||
break;
|
||||
case "sds-received":
|
||||
setPopupImage(IAMGE_POPUP_SDS_RECEIVED);
|
||||
break;
|
||||
case "login":
|
||||
setPopupImage(IAMGE_POPUP_LOGIN);
|
||||
break;
|
||||
case "group-selection":
|
||||
setPopupImage(GROUP_SELECTION_POPUP_LOGIN);
|
||||
break;
|
||||
case undefined:
|
||||
case null:
|
||||
setPopupImage(null);
|
||||
break;
|
||||
}
|
||||
}, [popup]);
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
if (popup == "login") return;
|
||||
if (popup == "sds-received") return;
|
||||
if (popup == "group-selection") return;
|
||||
setPopup(null);
|
||||
},
|
||||
3000,
|
||||
[popup],
|
||||
);
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
if (popup == "group-selection") {
|
||||
if (selectedRoom?.id === stringifiedData.groupSelectionGroupId) {
|
||||
setPopup(null);
|
||||
} else {
|
||||
handleKlick("3l")();
|
||||
}
|
||||
}
|
||||
},
|
||||
5000,
|
||||
[page, stringifiedData.groupSelectionGroupId, selectedRoom],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "connecting" && page !== "off" && page !== "startup") {
|
||||
setPopup({ popup: "login" });
|
||||
}
|
||||
}, [status, setPopup, page]);
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
if (status === "connected") {
|
||||
setPopup(null);
|
||||
}
|
||||
},
|
||||
5000,
|
||||
[status],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
pilotSocket.on("sds-status", (data: StationStatus) => {
|
||||
setStringifiedData({ sdsText: data.status + " - " + fmsStatusDescriptionShort[data.status] });
|
||||
setPopup({ popup: "sds-received" });
|
||||
if (sdsReceivedSoundRef.current) {
|
||||
sdsReceivedSoundRef.current.currentTime = 0;
|
||||
sdsReceivedSoundRef.current.play();
|
||||
}
|
||||
});
|
||||
}, [setPopup, setStringifiedData, sdsReceivedSoundRef]);
|
||||
|
||||
if (!popupImage || !popup) return null;
|
||||
|
||||
const DisplayText = ({ pageName }: { pageName: SetPopupParams["popup"] }) => {
|
||||
const group = ROOMS.find((r) => r.id === stringifiedData.groupSelectionGroupId);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("font-semibold text-[#000d60]", !!popup && "filter")}
|
||||
style={{
|
||||
fontFamily: "Bahnschrift",
|
||||
}}
|
||||
>
|
||||
{pageName == "status-sent" && (
|
||||
<p className="absolute left-[17.5%] top-[44%] h-[10%] w-[39%] text-lg">
|
||||
{fmsStatusDescription[connectedAircraft?.fmsStatus || "0"]}
|
||||
</p>
|
||||
)}
|
||||
{pageName == "sds-sent" && (
|
||||
<p className="absolute left-[17.5%] top-[44%] h-[10%] w-[39%] text-lg">
|
||||
{fmsStatusDescription[stringifiedData.sentSdsText || "0"]}
|
||||
</p>
|
||||
)}
|
||||
{pageName == "sds-received" && (
|
||||
<p className="absolute left-[17.5%] top-[39%] h-[24%] w-[60%] whitespace-normal break-words">
|
||||
{stringifiedData.sdsText}
|
||||
</p>
|
||||
)}
|
||||
{pageName == "group-selection" && (
|
||||
<>
|
||||
<p className="absolute left-[24%] top-[39%] h-[5%] w-[30%]">{group?.name}</p>
|
||||
<p className="absolute left-[24%] top-[50%] flex h-[9%] w-[31%] items-end justify-end">
|
||||
{stringifiedData.groupSelectionGroupId}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Image src={popupImage} alt="" className="z-30 col-span-full row-span-full" />
|
||||
<div className="relative z-30 col-span-full row-span-full overflow-hidden">
|
||||
<DisplayText pageName={popup} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
@@ -1,15 +1,58 @@
|
||||
import { Prisma } from "@repo/db";
|
||||
import { getPublicUser, MissionSdsStatusLog, Prisma } from "@repo/db";
|
||||
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
||||
import { useMrtStore } from "_store/pilot/MrtStore";
|
||||
import { pilotSocket } from "(app)/pilot/socket";
|
||||
import { editConnectedAircraftAPI } from "_querys/aircrafts";
|
||||
import { useEffect } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useSounds } from "./useSounds";
|
||||
import { sendSdsStatusMessageAPI } from "_querys/missions";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { ROOMS } from "_data/livekitRooms";
|
||||
import { useAudioStore } from "_store/audioStore";
|
||||
|
||||
type ButtonTypes =
|
||||
| "1"
|
||||
| "2"
|
||||
| "3"
|
||||
| "4"
|
||||
| "5"
|
||||
| "6"
|
||||
| "7"
|
||||
| "8"
|
||||
| "9"
|
||||
| "0"
|
||||
| "home"
|
||||
| "3l"
|
||||
| "3r"
|
||||
| "wheel-knob"
|
||||
| "arrow-up"
|
||||
| "arrow-down"
|
||||
| "arrow-left"
|
||||
| "arrow-right"
|
||||
| "end-call";
|
||||
|
||||
export const useButtons = () => {
|
||||
const station = usePilotConnectionStore((state) => state.selectedStation);
|
||||
const connectedAircraft = usePilotConnectionStore((state) => state.connectedAircraft);
|
||||
const connectionStatus = usePilotConnectionStore((state) => state.status);
|
||||
const session = useSession();
|
||||
const { connect, setSelectedRoom, selectedRoom } = useAudioStore((state) => state);
|
||||
|
||||
const { longBtnPressSoundRef, statusSentSoundRef } = useSounds();
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
status: pilotState,
|
||||
selectedStation,
|
||||
connectedAircraft,
|
||||
} = usePilotConnectionStore((state) => state);
|
||||
|
||||
const sendSdsStatusMutation = useMutation({
|
||||
mutationFn: async ({ sdsMessage }: { sdsMessage: MissionSdsStatusLog }) => {
|
||||
if (!connectedAircraft?.id) throw new Error("No connected aircraft");
|
||||
await sendSdsStatusMessageAPI({ sdsMessage, aircraftId: connectedAircraft?.id });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["missions"],
|
||||
});
|
||||
},
|
||||
});
|
||||
const updateAircraftMutation = useMutation({
|
||||
mutationKey: ["edit-pilot-connected-aircraft"],
|
||||
mutationFn: ({
|
||||
@@ -21,27 +64,38 @@ export const useButtons = () => {
|
||||
}) => editConnectedAircraftAPI(aircraftId, data),
|
||||
});
|
||||
|
||||
const { setPage } = useMrtStore((state) => state);
|
||||
const { setPage, setPopup, page, popup, setStringifiedData, stringifiedData } = useMrtStore(
|
||||
(state) => state,
|
||||
);
|
||||
|
||||
const handleButton =
|
||||
(button: "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0" | "home") => () => {
|
||||
if (connectionStatus !== "connected") return;
|
||||
if (!station) return;
|
||||
const role =
|
||||
(pilotState == "connected" && selectedStation?.bosCallsignShort) ||
|
||||
session.data?.user?.publicId;
|
||||
|
||||
const handleHold = (button: ButtonTypes) => async () => {
|
||||
/* if (connectionStatus !== "connected") return; */
|
||||
if (button === "end-call") {
|
||||
setPage({ page: "off" });
|
||||
setPopup(null);
|
||||
}
|
||||
if (button === "1" && page === "off") {
|
||||
setPage({ page: "startup" });
|
||||
return;
|
||||
}
|
||||
if (!selectedStation) return;
|
||||
if (!session.data?.user) return;
|
||||
if (!connectedAircraft?.id) return;
|
||||
if (
|
||||
button === "1" ||
|
||||
button === "2" ||
|
||||
button === "3" ||
|
||||
button === "4" ||
|
||||
button === "5" ||
|
||||
button === "6" ||
|
||||
button === "7" ||
|
||||
button === "8" ||
|
||||
button === "9" ||
|
||||
button === "0"
|
||||
button === "8"
|
||||
) {
|
||||
setPage({ page: "sending-status", station });
|
||||
|
||||
longBtnPressSoundRef.current?.play();
|
||||
const delay = Math.random() * 1500 + 500;
|
||||
setTimeout(async () => {
|
||||
await updateAircraftMutation.mutateAsync({
|
||||
aircraftId: connectedAircraft.id,
|
||||
@@ -49,28 +103,122 @@ export const useButtons = () => {
|
||||
fmsStatus: button,
|
||||
},
|
||||
});
|
||||
setPage({
|
||||
page: "home",
|
||||
station,
|
||||
fmsStatus: button,
|
||||
setPopup({ popup: "status-sent" });
|
||||
statusSentSoundRef.current?.play();
|
||||
}, delay);
|
||||
} else if (button === "5" || button === "9" || button === "0") {
|
||||
longBtnPressSoundRef.current?.play();
|
||||
const delay = Math.random() * 1500 + 500;
|
||||
setTimeout(async () => {
|
||||
await sendSdsStatusMutation.mutateAsync({
|
||||
sdsMessage: {
|
||||
type: "sds-status-log",
|
||||
auto: false,
|
||||
timeStamp: new Date().toISOString(),
|
||||
data: {
|
||||
direction: "to-lst",
|
||||
stationId: selectedStation.id,
|
||||
station: selectedStation,
|
||||
user: getPublicUser(session.data?.user),
|
||||
status: button,
|
||||
},
|
||||
},
|
||||
});
|
||||
}, 1000);
|
||||
} else {
|
||||
setPage({ page: "home", fmsStatus: connectedAircraft.fmsStatus || "6", station });
|
||||
setStringifiedData({ sentSdsText: button });
|
||||
statusSentSoundRef.current?.play();
|
||||
setPopup({ popup: "sds-sent" });
|
||||
}, delay);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKlick = (button: ButtonTypes) => async () => {
|
||||
console.log("Button clicked:", button);
|
||||
//implement Kurzwahl when button is clicked short to dial
|
||||
|
||||
switch (button) {
|
||||
case "0":
|
||||
case "1":
|
||||
case "2":
|
||||
case "3":
|
||||
case "4":
|
||||
case "5":
|
||||
case "6":
|
||||
case "7":
|
||||
case "8":
|
||||
case "9":
|
||||
//handle short press number buttons for kurzwahl
|
||||
if (popup === "group-selection") {
|
||||
if (stringifiedData.groupSelectionGroupId?.length === 4) {
|
||||
setStringifiedData({ groupSelectionGroupId: button });
|
||||
} else {
|
||||
setStringifiedData({
|
||||
groupSelectionGroupId: (stringifiedData.groupSelectionGroupId || "") + button,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (page === "home" && !popup) {
|
||||
setPopup({ popup: "group-selection" });
|
||||
setStringifiedData({ groupSelectionGroupId: button });
|
||||
}
|
||||
break;
|
||||
case "3r":
|
||||
if (popup === "sds-received" || popup === "group-selection") {
|
||||
setPopup(null);
|
||||
} else if (page === "home") {
|
||||
setPopup({ popup: "group-selection" });
|
||||
setStringifiedData({ groupSelectionGroupId: selectedRoom?.id || ROOMS[0]!.id });
|
||||
} else if (page === "voice-call") {
|
||||
setPage({ page: "home" });
|
||||
}
|
||||
break;
|
||||
case "wheel-knob":
|
||||
setPopup(popup === "group-selection" ? null : { popup: "group-selection" });
|
||||
setStringifiedData({ groupSelectionGroupId: selectedRoom?.id || ROOMS[0]!.id });
|
||||
break;
|
||||
case "arrow-right":
|
||||
if (popup === "group-selection") {
|
||||
let currentGroupIndex = ROOMS.findIndex(
|
||||
(r) => r.id === stringifiedData.groupSelectionGroupId,
|
||||
);
|
||||
if (currentGroupIndex === ROOMS.length - 1) currentGroupIndex = -1;
|
||||
const nextGroup = ROOMS[currentGroupIndex + 1];
|
||||
if (nextGroup) {
|
||||
setStringifiedData({ groupSelectionGroupId: nextGroup.id });
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "arrow-left":
|
||||
if (popup === "group-selection") {
|
||||
let currentGroupIndex = ROOMS.findIndex(
|
||||
(r) => r.id === stringifiedData.groupSelectionGroupId,
|
||||
);
|
||||
if (currentGroupIndex === 0) currentGroupIndex = ROOMS.length;
|
||||
const previousGroup = ROOMS[currentGroupIndex - 1];
|
||||
if (previousGroup) {
|
||||
setStringifiedData({ groupSelectionGroupId: previousGroup.id });
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "3l":
|
||||
if (popup === "group-selection") {
|
||||
const group = ROOMS.find((r) => r.id === stringifiedData.groupSelectionGroupId);
|
||||
if (group && role) {
|
||||
setSelectedRoom(group);
|
||||
connect(group, role);
|
||||
setPopup(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
pilotSocket.on("connect", () => {
|
||||
if (!station) return;
|
||||
setPage({ page: "home", fmsStatus: "6", station });
|
||||
const { page } = useMrtStore.getState();
|
||||
if (!selectedStation || page !== "off") return;
|
||||
setPage({ page: "startup" });
|
||||
});
|
||||
}, [setPage, selectedStation, setPopup]);
|
||||
|
||||
pilotSocket.on("aircraft-update", () => {
|
||||
if (!station) return;
|
||||
setPage({ page: "new-status", station });
|
||||
});
|
||||
}, [setPage, station]);
|
||||
|
||||
return { handleButton };
|
||||
return { handleKlick, handleHold };
|
||||
};
|
||||
|
||||
@@ -1,52 +1,39 @@
|
||||
"use client";
|
||||
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
||||
import { useMrtStore } from "_store/pilot/MrtStore";
|
||||
import { useAudioStore } from "_store/audioStore";
|
||||
import { RoomEvent } from "livekit-client";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export const useSounds = () => {
|
||||
const mrtState = useMrtStore((state) => state);
|
||||
const { connectedAircraft, selectedStation } = usePilotConnectionStore((state) => state);
|
||||
|
||||
const setPage = useMrtStore((state) => state.setPage);
|
||||
const MRTstatusSoundRef = useRef<HTMLAudioElement>(null);
|
||||
const MrtMessageReceivedSoundRef = useRef<HTMLAudioElement>(null);
|
||||
const { room } = useAudioStore((state) => state);
|
||||
const longBtnPressSoundRef = useRef<HTMLAudioElement>(null);
|
||||
const statusSentSoundRef = useRef<HTMLAudioElement>(null);
|
||||
const sdsReceivedSoundRef = useRef<HTMLAudioElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
MRTstatusSoundRef.current = new Audio("/sounds/MRT-status.mp3");
|
||||
MrtMessageReceivedSoundRef.current = new Audio("/sounds/MRT-message-received.mp3");
|
||||
MRTstatusSoundRef.current.onended = () => {
|
||||
if (!selectedStation || !connectedAircraft?.fmsStatus) return;
|
||||
setPage({
|
||||
page: "home",
|
||||
station: selectedStation,
|
||||
fmsStatus: connectedAircraft?.fmsStatus,
|
||||
});
|
||||
};
|
||||
MrtMessageReceivedSoundRef.current.onended = () => {
|
||||
if (!selectedStation || !connectedAircraft?.fmsStatus) return;
|
||||
if (mrtState.page === "sds") return;
|
||||
setPage({
|
||||
page: "home",
|
||||
station: selectedStation,
|
||||
fmsStatus: connectedAircraft?.fmsStatus,
|
||||
});
|
||||
};
|
||||
longBtnPressSoundRef.current = new Audio("/sounds/1504.wav");
|
||||
statusSentSoundRef.current = new Audio("/sounds/403.wav");
|
||||
sdsReceivedSoundRef.current = new Audio("/sounds/775.wav");
|
||||
}
|
||||
}, [connectedAircraft?.fmsStatus, selectedStation, setPage, mrtState.page]);
|
||||
|
||||
const fmsStatus = connectedAircraft?.fmsStatus || "NaN";
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!connectedAircraft) return;
|
||||
if (mrtState.page === "new-status") {
|
||||
if (fmsStatus === "J" || fmsStatus === "c") {
|
||||
MrtMessageReceivedSoundRef.current?.play();
|
||||
} else {
|
||||
MRTstatusSoundRef.current?.play();
|
||||
}
|
||||
} else if (mrtState.page === "sds") {
|
||||
MrtMessageReceivedSoundRef.current?.play();
|
||||
}
|
||||
}, [mrtState, fmsStatus, connectedAircraft, selectedStation]);
|
||||
const handleRoomConnected = () => {
|
||||
// Play a sound when connected to the room
|
||||
// connectedSound.play();
|
||||
statusSentSoundRef.current?.play();
|
||||
console.log("Room connected - played sound");
|
||||
};
|
||||
room?.on(RoomEvent.Connected, handleRoomConnected);
|
||||
|
||||
return () => {
|
||||
room?.off(RoomEvent.Connected, handleRoomConnected);
|
||||
};
|
||||
}, [room]);
|
||||
|
||||
return {
|
||||
longBtnPressSoundRef,
|
||||
statusSentSoundRef,
|
||||
sdsReceivedSoundRef,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -76,7 +76,6 @@ export const ConnectionBtn = () => {
|
||||
const session = useSession();
|
||||
const uid = session.data?.user?.id;
|
||||
if (!uid) return null;
|
||||
console.log(bookings);
|
||||
return (
|
||||
<div className="rounded-box bg-base-200 flex items-center justify-center gap-2 p-1">
|
||||
{connection.message.length > 0 && (
|
||||
|
||||
@@ -23,7 +23,7 @@ const Map = dynamic(() => import("_components/map/Map"), {
|
||||
});
|
||||
|
||||
const PilotPage = () => {
|
||||
const { connectedAircraft, status, } = usePilotConnectionStore((state) => state);
|
||||
const { connectedAircraft, status } = usePilotConnectionStore((state) => state);
|
||||
const { latestMission } = useDmeStore((state) => state);
|
||||
// Query will be cached anyway, due to this, displayed Markers are in sync with own Aircraft connection-warning
|
||||
const { data: aircrafts } = useQuery({
|
||||
@@ -94,10 +94,20 @@ const PilotPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full w-1/3">
|
||||
<div className="flex h-full w-1/3 min-w-[500px]">
|
||||
<div className="bg-base-300 flex h-full w-full flex-col p-4">
|
||||
<div className="flex justify-between">
|
||||
<h2 className="card-title mb-2">MRT & DME</h2>
|
||||
<div className="mb-2 flex items-center justify-end gap-2">
|
||||
<h2 className="card-title">MRT & DME</h2>
|
||||
<a
|
||||
href="https://docs.virtualairrescue.com/allgemein/var-systeme/leitstelle/pilot.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="link text-xs text-gray-500 hover:underline"
|
||||
>
|
||||
Hilfe
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="tooltip tooltip-left mb-4"
|
||||
data-tip="Dadurch wird der Einsatz erneut an den Desktop-Client gesendet."
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useSounds } from "_components/Audio/useSounds";
|
||||
|
||||
export const Audio = () => {
|
||||
const {
|
||||
selectedRoom,
|
||||
speakingParticipants,
|
||||
resetSpeakingParticipants,
|
||||
isTalking,
|
||||
@@ -37,8 +38,8 @@ export const Audio = () => {
|
||||
room,
|
||||
message,
|
||||
removeMessage,
|
||||
setSelectedRoom,
|
||||
} = useAudioStore();
|
||||
const [selectedRoom, setSelectedRoom] = useState<string>("LST_01");
|
||||
|
||||
useSounds({
|
||||
isReceiving: speakingParticipants.length > 0,
|
||||
@@ -48,7 +49,7 @@ export const Audio = () => {
|
||||
});
|
||||
|
||||
const { selectedStation, status: pilotState } = usePilotConnectionStore((state) => state);
|
||||
const { selectedZone, status: dispatcherState } = useDispatchConnectionStore((state) => state);
|
||||
const { status: dispatcherState } = useDispatchConnectionStore((state) => state);
|
||||
const session = useSession();
|
||||
const [isReceivingBlick, setIsReceivingBlick] = useState(false);
|
||||
const [recentSpeakers, setRecentSpeakers] = useState<typeof speakingParticipants>([]);
|
||||
@@ -93,7 +94,7 @@ export const Audio = () => {
|
||||
const canStopOtherSpeakers = dispatcherState === "connected";
|
||||
|
||||
const role =
|
||||
(dispatcherState === "connected" && selectedZone) ||
|
||||
(dispatcherState === "connected" && "VAR LST") ||
|
||||
(pilotState == "connected" && selectedStation?.bosCallsignShort) ||
|
||||
session.data?.user?.publicId;
|
||||
|
||||
@@ -185,20 +186,20 @@ export const Audio = () => {
|
||||
</summary>
|
||||
<ul className="menu dropdown-content bg-base-200 rounded-box z-[1050] w-52 p-2 shadow-sm">
|
||||
{ROOMS.map((r) => (
|
||||
<li key={r}>
|
||||
<li key={r.id}>
|
||||
<button
|
||||
className="btn btn-sm btn-ghost relative flex items-center justify-start gap-2 text-left"
|
||||
onClick={() => {
|
||||
if (!role) return;
|
||||
if (selectedRoom === r) return;
|
||||
if (selectedRoom?.name === r.name) return;
|
||||
setSelectedRoom(r);
|
||||
connect(r, role);
|
||||
}}
|
||||
>
|
||||
{room?.name === r && (
|
||||
{room?.name === r.name && (
|
||||
<Disc className="text-success absolute left-2 text-sm" width={15} />
|
||||
)}
|
||||
<span className="flex-1 text-center">{r}</span>
|
||||
<span className="flex-1 text-center">{r.name}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -63,6 +63,7 @@ export function QueryProvider({ children }: { children: ReactNode }) {
|
||||
};
|
||||
|
||||
const handleNotification = (notification: NotificationPayload) => {
|
||||
console.log("Received notification:", notification);
|
||||
const playNotificationSound = () => {
|
||||
if (notificationSound.current) {
|
||||
notificationSound.current.currentTime = 0;
|
||||
@@ -70,7 +71,7 @@ export function QueryProvider({ children }: { children: ReactNode }) {
|
||||
.play()
|
||||
.catch((e) => console.error("Notification sound error:", e));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
switch (notification.type) {
|
||||
case "hpg-validation":
|
||||
@@ -90,6 +91,7 @@ export function QueryProvider({ children }: { children: ReactNode }) {
|
||||
});
|
||||
break;
|
||||
case "station-status":
|
||||
console.log("station Status", QUICK_RESPONSE[notification.status]);
|
||||
if (!QUICK_RESPONSE[notification.status]) return;
|
||||
toast.custom((e) => <StatusToast event={notification} t={e} />, {
|
||||
duration: 60000,
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { Prisma, StationStatus } from "@repo/db";
|
||||
import { getPublicUser, MissionSdsStatusLog, StationStatus } from "@repo/db";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { BaseNotification } from "_components/customToasts/BaseNotification";
|
||||
import { FMS_STATUS_COLORS } from "_helpers/fmsStatusColors";
|
||||
import { editConnectedAircraftAPI, getConnectedAircraftsAPI } from "_querys/aircrafts";
|
||||
import { getConnectedAircraftsAPI } from "_querys/aircrafts";
|
||||
import { getLivekitRooms } from "_querys/livekit";
|
||||
import { sendSdsStatusMessageAPI } from "_querys/missions";
|
||||
import { getStationsAPI } from "_querys/stations";
|
||||
import { useAudioStore } from "_store/audioStore";
|
||||
import { useMapStore } from "_store/mapStore";
|
||||
import { X } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Toast, toast } from "react-hot-toast";
|
||||
|
||||
export const QUICK_RESPONSE: Record<string, string[]> = {
|
||||
@@ -22,6 +24,8 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) =>
|
||||
const status5Sounds = useRef<HTMLAudioElement | null>(null);
|
||||
const status9Sounds = useRef<HTMLAudioElement | null>(null);
|
||||
|
||||
const session = useSession();
|
||||
|
||||
const { data: livekitRooms } = useQuery({
|
||||
queryKey: ["livekit-rooms"],
|
||||
queryFn: () => getLivekitRooms(),
|
||||
@@ -46,7 +50,7 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) =>
|
||||
status9Sounds.current = new Audio("/sounds/status-9.mp3");
|
||||
}
|
||||
}, []);
|
||||
const [aircraftDataAcurate, setAircraftDataAccurate] = useState(false);
|
||||
|
||||
//const mapStore = useMapStore((s) => s);
|
||||
const { setOpenAircraftMarker, setMap } = useMapStore((store) => store);
|
||||
|
||||
@@ -65,29 +69,16 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) =>
|
||||
const station = stations?.find((s) => s.id === event.data?.stationId);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const changeAircraftMutation = useMutation({
|
||||
mutationFn: async ({
|
||||
id,
|
||||
update,
|
||||
}: {
|
||||
id: number;
|
||||
update: Prisma.ConnectedAircraftUpdateInput;
|
||||
}) => {
|
||||
await editConnectedAircraftAPI(id, update);
|
||||
const sendSdsStatusMutation = useMutation({
|
||||
mutationFn: async ({ sdsMessage }: { sdsMessage: MissionSdsStatusLog }) => {
|
||||
if (!connectedAircraft?.id) throw new Error("No connected aircraft");
|
||||
await sendSdsStatusMessageAPI({ sdsMessage, aircraftId: connectedAircraft?.id });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["aircrafts"],
|
||||
queryKey: ["missions"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (event.status !== connectedAircraft?.fmsStatus && aircraftDataAcurate) {
|
||||
toast.remove(t.id);
|
||||
} else if (event.status == connectedAircraft?.fmsStatus && !aircraftDataAcurate) {
|
||||
setAircraftDataAccurate(true);
|
||||
}
|
||||
}, [aircraftDataAcurate, connectedAircraft, event.status, t.id]);
|
||||
|
||||
useEffect(() => {
|
||||
let soundRef: React.RefObject<HTMLAudioElement | null> | null = null;
|
||||
switch (event.status) {
|
||||
@@ -103,7 +94,8 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) =>
|
||||
default:
|
||||
soundRef = null;
|
||||
}
|
||||
if (audioRoom !== livekitUser?.roomName) {
|
||||
|
||||
if (audioRoom && livekitUser?.roomName && audioRoom !== livekitUser?.roomName) {
|
||||
toast.remove(t.id);
|
||||
return;
|
||||
}
|
||||
@@ -121,7 +113,8 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) =>
|
||||
};
|
||||
}, [event.status, livekitUser?.roomName, audioRoom, t.id]);
|
||||
|
||||
if (!connectedAircraft || !station) return null;
|
||||
console.log(connectedAircraft, station);
|
||||
if (!connectedAircraft || !station || !session.data) return null;
|
||||
return (
|
||||
<BaseNotification>
|
||||
<div className="flex flex-row items-center gap-14">
|
||||
@@ -162,10 +155,18 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) =>
|
||||
toast.error("Keine Flugzeug-ID gefunden");
|
||||
return;
|
||||
}
|
||||
await changeAircraftMutation.mutateAsync({
|
||||
id: event.data?.aircraftId,
|
||||
update: {
|
||||
fmsStatus: status,
|
||||
await sendSdsStatusMutation.mutateAsync({
|
||||
sdsMessage: {
|
||||
type: "sds-status-log",
|
||||
auto: false,
|
||||
data: {
|
||||
direction: "to-aircraft",
|
||||
stationId: event.data.stationId!,
|
||||
station: station,
|
||||
user: getPublicUser(session.data?.user),
|
||||
status,
|
||||
},
|
||||
timeStamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
toast.remove(t.id);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Marker, Polyline, useMap } from "react-leaflet";
|
||||
import { DivIcon, Marker as LMarker, Popup as LPopup } from "leaflet";
|
||||
import { useMapStore } from "_store/mapStore";
|
||||
import { Fragment, useCallback, useEffect, useRef, useState, useMemo } from "react";
|
||||
import { checkSimulatorConnected, cn } from "@repo/shared-components";
|
||||
import { cn } from "@repo/shared-components";
|
||||
import { ChevronsRightLeft, House, MessageSquareText, Minimize2 } from "lucide-react";
|
||||
import { SmartPopup, calculateAnchor, useSmartPopup } from "_components/SmartPopup";
|
||||
import FMSStatusHistory, {
|
||||
@@ -396,27 +396,11 @@ const AircraftMarker = ({ aircraft }: { aircraft: ConnectedAircraft & { Station:
|
||||
};
|
||||
|
||||
export const AircraftLayer = () => {
|
||||
const [aircrafts, setAircrafts] = useState<(ConnectedAircraft & { Station: Station })[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAircrafts = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/aircrafts");
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to fetch aircrafts");
|
||||
}
|
||||
const data: (ConnectedAircraft & { Station: Station })[] = await res.json();
|
||||
setAircrafts(data.filter((a) => checkSimulatorConnected(a)));
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch aircrafts:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAircrafts();
|
||||
const interval = setInterval(fetchAircrafts, 10_000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
const { data: aircrafts } = useQuery({
|
||||
queryKey: ["connected-aircrafts", "map"],
|
||||
queryFn: () => getConnectedAircraftsAPI(),
|
||||
refetchInterval: 15000,
|
||||
});
|
||||
const { setMap } = useMapStore((state) => state);
|
||||
const map = useMap();
|
||||
const {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Mission,
|
||||
MissionLog,
|
||||
MissionSdsLog,
|
||||
MissionSdsStatusLog,
|
||||
MissionStationLog,
|
||||
Prisma,
|
||||
PublicUser,
|
||||
@@ -40,7 +41,7 @@ import {
|
||||
TextSearch,
|
||||
} from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { sendSdsMessageAPI } from "_querys/missions";
|
||||
import { sendSdsMessageAPI, sendSdsStatusMessageAPI } from "_querys/missions";
|
||||
import { getLivekitRooms } from "_querys/livekit";
|
||||
import { findLeitstelleForPosition } from "_helpers/findLeitstelleinPoint";
|
||||
import { formatDistance } from "date-fns";
|
||||
@@ -54,9 +55,13 @@ const FMSStatusHistory = ({
|
||||
mission?: Mission;
|
||||
}) => {
|
||||
const log = ((mission?.missionLog as unknown as MissionLog[]) || [])
|
||||
.filter((entry) => entry.type === "station-log" && entry.data.stationId === aircraft.Station.id)
|
||||
.filter(
|
||||
(entry) =>
|
||||
(entry.type === "station-log" || entry.type == "sds-status-log") &&
|
||||
entry.data.stationId === aircraft.Station.id,
|
||||
)
|
||||
.reverse()
|
||||
.splice(0, 6) as MissionStationLog[];
|
||||
.splice(0, 6) as (MissionStationLog | MissionSdsStatusLog)[];
|
||||
|
||||
const aircraftUser: PublicUser =
|
||||
typeof aircraft.publicUser === "string" ? JSON.parse(aircraft.publicUser) : aircraft.publicUser;
|
||||
@@ -103,10 +108,13 @@ const FMSStatusHistory = ({
|
||||
<span
|
||||
className="text-base font-bold"
|
||||
style={{
|
||||
color: FMS_STATUS_TEXT_COLORS[entry.data.newFMSstatus],
|
||||
color:
|
||||
FMS_STATUS_TEXT_COLORS[
|
||||
entry.type === "sds-status-log" ? entry.data.status : entry.data.newFMSstatus
|
||||
],
|
||||
}}
|
||||
>
|
||||
{entry.data.newFMSstatus}
|
||||
{entry.type === "sds-status-log" ? entry.data.status : entry.data.newFMSstatus}
|
||||
</span>
|
||||
<span className="text-base-content">
|
||||
{new Date(entry.timeStamp).toLocaleTimeString([], {
|
||||
@@ -126,6 +134,7 @@ const FMSStatusSelector = ({
|
||||
}: {
|
||||
aircraft: ConnectedAircraft & { Station: Station };
|
||||
}) => {
|
||||
const session = useSession();
|
||||
const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected";
|
||||
const [hoveredStatus, setHoveredStatus] = useState<string | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
@@ -144,6 +153,20 @@ const FMSStatusSelector = ({
|
||||
},
|
||||
});
|
||||
|
||||
const sendSdsStatusMutation = useMutation({
|
||||
mutationFn: async ({ sdsMessage }: { sdsMessage: MissionSdsStatusLog }) => {
|
||||
if (!aircraft?.id) throw new Error("No connected aircraft");
|
||||
await sendSdsStatusMessageAPI({ sdsMessage, aircraftId: aircraft?.id });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["missions"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (!session.data?.user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-base-content mt-2 flex flex-col gap-2 p-4">
|
||||
<div className="flex h-full items-center justify-center gap-2">
|
||||
@@ -213,12 +236,21 @@ const FMSStatusSelector = ({
|
||||
onMouseEnter={() => setHoveredStatus(status)}
|
||||
onMouseLeave={() => setHoveredStatus(null)}
|
||||
onClick={async () => {
|
||||
await changeAircraftMutation.mutateAsync({
|
||||
id: aircraft.id,
|
||||
update: {
|
||||
fmsStatus: status,
|
||||
await sendSdsStatusMutation.mutateAsync({
|
||||
sdsMessage: {
|
||||
type: "sds-status-log",
|
||||
auto: false,
|
||||
timeStamp: new Date().toISOString(),
|
||||
data: {
|
||||
status: status,
|
||||
direction: "to-aircraft",
|
||||
stationId: aircraft.Station.id,
|
||||
station: aircraft.Station,
|
||||
user: getPublicUser(session.data?.user),
|
||||
},
|
||||
},
|
||||
});
|
||||
toast.success(`SDS Status ${status} gesendet`);
|
||||
}}
|
||||
>
|
||||
{status}
|
||||
@@ -378,7 +410,9 @@ const SDSTab = ({
|
||||
?.slice()
|
||||
.reverse()
|
||||
.filter(
|
||||
(entry) => entry.type === "sds-log" && entry.data.stationId === aircraft.Station.id,
|
||||
(entry) =>
|
||||
(entry.type === "sds-log" || entry.type == "sds-status-log") &&
|
||||
entry.data.stationId === aircraft.Station.id,
|
||||
) || [],
|
||||
[mission?.missionLog, aircraft.Station.id],
|
||||
);
|
||||
@@ -471,7 +505,7 @@ const SDSTab = ({
|
||||
)}
|
||||
<ul className="max-h-[300px] space-y-2 overflow-x-auto overflow-y-auto">
|
||||
{log.map((entry, index) => {
|
||||
const sdsEntry = entry as MissionSdsLog;
|
||||
const sdsEntry = entry as MissionSdsLog | MissionSdsStatusLog;
|
||||
return (
|
||||
<li key={index} className="flex items-center gap-2">
|
||||
<span className="text-base-content">
|
||||
@@ -489,7 +523,9 @@ const SDSTab = ({
|
||||
{sdsEntry.data.user.firstname?.[0]?.toUpperCase() ?? "?"}
|
||||
{sdsEntry.data.user.lastname?.[0]?.toUpperCase() ?? "?"}
|
||||
</span>
|
||||
<span className="text-base-content">{sdsEntry.data.message}</span>
|
||||
<span className="text-base-content">
|
||||
{sdsEntry.type == "sds-log" ? sdsEntry.data.message : sdsEntry.data.status}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -726,7 +726,11 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
|
||||
<span className="text-base-content">{entry.data.station.bosCallsign}</span>
|
||||
</li>
|
||||
);
|
||||
if (entry.type === "message-log" || entry.type === "sds-log")
|
||||
if (
|
||||
entry.type === "message-log" ||
|
||||
entry.type === "sds-log" ||
|
||||
entry.type === "sds-status-log"
|
||||
)
|
||||
return (
|
||||
<li key={index} className="flex items-center gap-2">
|
||||
<span className="text-base-content">
|
||||
@@ -741,9 +745,10 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
|
||||
color: FMS_STATUS_TEXT_COLORS[6],
|
||||
}}
|
||||
>
|
||||
{entry.data.user.firstname?.[0]?.toUpperCase() ?? "?"}
|
||||
{entry.data.user.lastname?.[0]?.toUpperCase() ?? "?"}
|
||||
{entry.type === "sds-log" && (
|
||||
{entry.type == "sds-status-log" && entry.data.direction == "to-lst"
|
||||
? entry.data.station.bosCallsignShort
|
||||
: `${entry.data.user.firstname?.[0]?.toUpperCase() ?? "?"}${entry.data.user.lastname?.[0]?.toUpperCase() ?? "?"}`}
|
||||
{(entry.type === "sds-log" || entry.type === "sds-status-log") && (
|
||||
<>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -760,11 +765,17 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{entry.data.station.bosCallsignShort}
|
||||
{entry.type == "sds-status-log" && entry.data.direction == "to-aircraft"
|
||||
? entry.data.station.bosCallsignShort
|
||||
: "LST"}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-base-content">{entry.data.message}</span>
|
||||
<span className="text-base-content">
|
||||
{entry.type === "sds-log" || entry.type === "message-log"
|
||||
? entry.data.message
|
||||
: entry.data.status}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
if (
|
||||
|
||||
@@ -24,3 +24,30 @@ export const fmsStatusDescription: { [key: string]: string } = {
|
||||
o: "Warten, alle Abfrageplätze belegt",
|
||||
u: "Verstanden",
|
||||
};
|
||||
|
||||
export const fmsStatusDescriptionShort: { [key: string]: string } = {
|
||||
NaN: "Keine D.",
|
||||
"0": "Prio. Sprechen",
|
||||
"1": "E.-bereit Funk",
|
||||
"2": "E.-bereit Wache",
|
||||
"3": "E.-übernahme",
|
||||
"4": "Einsatzort",
|
||||
"5": "Sprechwunsch",
|
||||
"6": "Nicht e.-bereit",
|
||||
"7": "Einsatzgeb.",
|
||||
"8": "Bed. Verfügbar",
|
||||
"9": "F-anmeldung",
|
||||
E: "Einsatzabbruch",
|
||||
C: "Melden Sie Einsatzübernahme",
|
||||
F: "Kommen Sie über Draht",
|
||||
H: "Fahren Sie Wache an",
|
||||
J: "Sprechen Sie",
|
||||
L: "Geben Sie Lagemeldung",
|
||||
P: "Einsatz mit Polizei",
|
||||
U: "Ungültige Statusfolge",
|
||||
c: "Status korrigieren",
|
||||
d: "Nennen Sie Transportziel",
|
||||
h: "Zielklinik verständigt",
|
||||
o: "Warten, alle Abfrageplätze belegt",
|
||||
u: "Verstanden",
|
||||
};
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
export const ROOMS = ["LST_01", "LST_02", "LST_03", "LST_04", "LST_05"];
|
||||
export const ROOMS = [
|
||||
{ name: "VAR_LST_RD_01", id: "2201" },
|
||||
{ name: "VAR_LST_RD_02", id: "2202" },
|
||||
{ name: "VAR_LST_RD_03", id: "2203" },
|
||||
{ name: "VAR_LST_RD_04", id: "2204" },
|
||||
{ name: "VAR_LST_RD_05", id: "2205" },
|
||||
];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Mission, MissionSdsLog, Prisma } from "@repo/db";
|
||||
import { Mission, MissionSdsLog, MissionSdsStatusLog, Prisma } from "@repo/db";
|
||||
import axios from "axios";
|
||||
import { serverApi } from "_helpers/axios";
|
||||
|
||||
@@ -29,6 +29,20 @@ export const editMissionAPI = async (id: number, mission: Prisma.MissionUpdateIn
|
||||
const respone = await serverApi.patch<Mission>(`/mission/${id}`, mission);
|
||||
return respone.data;
|
||||
};
|
||||
|
||||
export const sendSdsStatusMessageAPI = async ({
|
||||
sdsMessage,
|
||||
aircraftId,
|
||||
}: {
|
||||
aircraftId: number;
|
||||
sdsMessage: MissionSdsStatusLog;
|
||||
}) => {
|
||||
const respone = await serverApi.post<Mission>(`/aircrafts/${aircraftId}/send-sds-message`, {
|
||||
sdsMessage,
|
||||
});
|
||||
return respone.data;
|
||||
};
|
||||
|
||||
export const sendSdsMessageAPI = async ({
|
||||
missionId,
|
||||
sdsMessage,
|
||||
|
||||
@@ -21,12 +21,13 @@ import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
|
||||
import { changeDispatcherAPI } from "_querys/dispatcher";
|
||||
import { getRadioStream } from "_helpers/radioEffect";
|
||||
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
||||
import { ROOMS } from "_data/livekitRooms";
|
||||
|
||||
let interval: NodeJS.Timeout;
|
||||
|
||||
type TalkState = {
|
||||
addSpeakingParticipant: (participant: Participant) => void;
|
||||
connect: (roomName: string, role: string) => void;
|
||||
connect: (room: (typeof ROOMS)[number] | undefined, role: string) => void;
|
||||
connectionQuality: ConnectionQuality;
|
||||
disconnect: () => void;
|
||||
isTalking: boolean;
|
||||
@@ -44,6 +45,8 @@ type TalkState = {
|
||||
radioVolume: number;
|
||||
dmeVolume: number;
|
||||
};
|
||||
selectedRoom?: (typeof ROOMS)[number];
|
||||
setSelectedRoom: (room: (typeof ROOMS)[number]) => void;
|
||||
speakingParticipants: Participant[];
|
||||
state: "connecting" | "connected" | "disconnected" | "error";
|
||||
toggleTalking: () => void;
|
||||
@@ -72,6 +75,10 @@ export const useAudioStore = create<TalkState>((set, get) => ({
|
||||
remoteParticipants: 0,
|
||||
connectionQuality: ConnectionQuality.Unknown,
|
||||
room: null,
|
||||
selectedRoom: ROOMS[0],
|
||||
setSelectedRoom: (room) => {
|
||||
set({ selectedRoom: room });
|
||||
},
|
||||
resetSpeakingParticipants: (source: string) => {
|
||||
set({
|
||||
speakingParticipants: [],
|
||||
@@ -117,11 +124,11 @@ export const useAudioStore = create<TalkState>((set, get) => ({
|
||||
(oldSettings.micDeviceId !== newSettings.micDeviceId ||
|
||||
oldSettings.micVolume !== newSettings.micVolume)
|
||||
) {
|
||||
const { room, disconnect, connect } = get();
|
||||
const { room, disconnect, connect, selectedRoom } = get();
|
||||
const role = room?.localParticipant.attributes.role;
|
||||
if (room?.name || role) {
|
||||
if (selectedRoom || role) {
|
||||
disconnect();
|
||||
connect(room?.name || "", role || "user");
|
||||
connect(selectedRoom, role || "user");
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -160,7 +167,7 @@ export const useAudioStore = create<TalkState>((set, get) => ({
|
||||
|
||||
set((state) => ({ isTalking: !state.isTalking, transmitBlocked: false }));
|
||||
},
|
||||
connect: async (roomName, role) => {
|
||||
connect: async (_room, role) => {
|
||||
set({ state: "connecting" });
|
||||
|
||||
try {
|
||||
@@ -172,13 +179,16 @@ export const useAudioStore = create<TalkState>((set, get) => ({
|
||||
connectedRoom.removeAllListeners();
|
||||
}
|
||||
|
||||
const { selectedRoom } = get();
|
||||
|
||||
const url = process.env.NEXT_PUBLIC_LIVEKIT_URL;
|
||||
if (!url) return console.error("NEXT_PUBLIC_LIVEKIT_URL not set");
|
||||
|
||||
const token = await getToken(roomName);
|
||||
const token = await getToken(_room?.name || selectedRoom?.name || "VAR_LST_RD_01");
|
||||
if (!token) throw new Error("Fehlende Berechtigung");
|
||||
const room = new Room({});
|
||||
await room.prepareConnection(url, token);
|
||||
const roomConnectedSound = new Audio("/sounds/403.wav");
|
||||
room
|
||||
// Connection events
|
||||
.on(RoomEvent.Connected, async () => {
|
||||
@@ -186,7 +196,7 @@ export const useAudioStore = create<TalkState>((set, get) => ({
|
||||
|
||||
if (dispatchState.status === "connected" && dispatchState.connectedDispatcher?.id) {
|
||||
changeDispatcherAPI(dispatchState.connectedDispatcher?.id, {
|
||||
zone: roomName,
|
||||
zone: _room?.name || selectedRoom?.name || "VAR_LST_RD_01",
|
||||
ghostMode: dispatchState.ghostMode,
|
||||
});
|
||||
}
|
||||
@@ -208,7 +218,7 @@ export const useAudioStore = create<TalkState>((set, get) => ({
|
||||
source: Track.Source.Microphone,
|
||||
});
|
||||
await publishedTrack.mute();
|
||||
|
||||
roomConnectedSound.play();
|
||||
set({ localRadioTrack: publishedTrack });
|
||||
|
||||
set({ state: "connected", room, isTalking: false, message: null });
|
||||
|
||||
@@ -27,7 +27,7 @@ export const useDispatchConnectionStore = create<ConnectionStore>((set) => ({
|
||||
setHideDraftMissions: (hide) => set({ hideDraftMissions: hide }),
|
||||
connectedDispatcher: null,
|
||||
message: "",
|
||||
selectedZone: "LST_01",
|
||||
selectedZone: "VAR_LST_RD_01",
|
||||
logoffTime: "",
|
||||
ghostMode: false,
|
||||
connect: async (uid, selectedZone, logoffTime, ghostMode) =>
|
||||
@@ -48,7 +48,7 @@ export const useDispatchConnectionStore = create<ConnectionStore>((set) => ({
|
||||
|
||||
dispatchSocket.on("connect", () => {
|
||||
const { logoffTime, selectedZone, ghostMode } = useDispatchConnectionStore.getState();
|
||||
useAudioStore.getState().connect("LST_01", selectedZone || "Leitstelle");
|
||||
useAudioStore.getState().connect(undefined, selectedZone || "Leitstelle");
|
||||
dispatchSocket.emit("connect-dispatch", {
|
||||
logoffTime,
|
||||
selectedZone,
|
||||
|
||||
@@ -1,173 +1,92 @@
|
||||
import { MissionSdsLog, Station } from "@repo/db";
|
||||
import { fmsStatusDescription } from "_data/fmsStatusDescription";
|
||||
import { DisplayLineProps } from "(app)/pilot/_components/mrt/Mrt";
|
||||
import { create } from "zustand";
|
||||
import { syncTabs } from "zustand-sync-tabs";
|
||||
|
||||
interface SetSdsPageParams {
|
||||
page: "sds";
|
||||
station: Station;
|
||||
sdsMessage: MissionSdsLog;
|
||||
interface SetOffPageParams {
|
||||
page: "off";
|
||||
}
|
||||
|
||||
interface SetStartupPageParams {
|
||||
page: "startup";
|
||||
}
|
||||
|
||||
interface SetHomePageParams {
|
||||
page: "home";
|
||||
station: Station;
|
||||
fmsStatus: string;
|
||||
}
|
||||
|
||||
interface SetSendingStatusPageParams {
|
||||
page: "sending-status";
|
||||
station: Station;
|
||||
interface SetVoicecallPageParams {
|
||||
page: "voice-call";
|
||||
}
|
||||
interface SetSdsReceivedPopupParams {
|
||||
popup: "sds-received";
|
||||
}
|
||||
|
||||
interface SetNewStatusPageParams {
|
||||
page: "new-status";
|
||||
station: Station;
|
||||
interface SetGroupSelectionPopupParams {
|
||||
popup: "group-selection";
|
||||
}
|
||||
|
||||
type SetPageParams =
|
||||
interface SetStatusSentPopupParams {
|
||||
popup: "status-sent";
|
||||
}
|
||||
|
||||
interface SetLoginPopupParams {
|
||||
popup: "login";
|
||||
}
|
||||
|
||||
interface SetSdsSentPopupParams {
|
||||
popup: "sds-sent";
|
||||
}
|
||||
|
||||
export type SetPageParams =
|
||||
| SetHomePageParams
|
||||
| SetSendingStatusPageParams
|
||||
| SetSdsPageParams
|
||||
| SetNewStatusPageParams;
|
||||
| SetOffPageParams
|
||||
| SetStartupPageParams
|
||||
| SetVoicecallPageParams;
|
||||
|
||||
export type SetPopupParams =
|
||||
| SetStatusSentPopupParams
|
||||
| SetSdsSentPopupParams
|
||||
| SetGroupSelectionPopupParams
|
||||
| SetSdsReceivedPopupParams
|
||||
| SetLoginPopupParams;
|
||||
|
||||
interface StringifiedData {
|
||||
sdsText?: string;
|
||||
sentSdsText?: string;
|
||||
|
||||
groupSelectionGroupId?: string;
|
||||
callTextHeader?: string;
|
||||
}
|
||||
|
||||
interface MrtStore {
|
||||
page: SetPageParams["page"];
|
||||
popup?: SetPopupParams["popup"];
|
||||
|
||||
lines: DisplayLineProps[];
|
||||
stringifiedData: StringifiedData;
|
||||
setStringifiedData: (data: Partial<StringifiedData>) => void;
|
||||
|
||||
setPage: (pageData: SetPageParams) => void;
|
||||
setLines: (lines: MrtStore["lines"]) => void;
|
||||
setPopup: (popupData: SetPopupParams | null) => void;
|
||||
|
||||
// internal
|
||||
updateIntervall?: number;
|
||||
nightMode: boolean;
|
||||
setNightMode: (nightMode: boolean) => void;
|
||||
}
|
||||
|
||||
export const useMrtStore = create<MrtStore>(
|
||||
syncTabs(
|
||||
(set) => ({
|
||||
page: "home",
|
||||
pageData: {
|
||||
message: "",
|
||||
export const useMrtStore = create<MrtStore>((set) => ({
|
||||
page: "off",
|
||||
nightMode: false,
|
||||
stringifiedData: {
|
||||
groupSelectionGroupId: "2201",
|
||||
},
|
||||
lines: [
|
||||
{
|
||||
textLeft: "VAR.#",
|
||||
textSize: "2",
|
||||
setNightMode: (nightMode) => set({ nightMode }),
|
||||
setStringifiedData: (data) =>
|
||||
set((state) => ({
|
||||
stringifiedData: { ...state.stringifiedData, ...data },
|
||||
})),
|
||||
setPopup: (popupData) => {
|
||||
set({ popup: popupData ? popupData.popup : undefined });
|
||||
},
|
||||
{
|
||||
textLeft: "No Data",
|
||||
textSize: "3",
|
||||
},
|
||||
],
|
||||
setLines: (lines) => set({ lines }),
|
||||
setPage: (pageData) => {
|
||||
switch (pageData.page) {
|
||||
case "home": {
|
||||
const { station, fmsStatus } = pageData as SetHomePageParams;
|
||||
set({
|
||||
page: "home",
|
||||
lines: [
|
||||
{
|
||||
textLeft: `${station?.bosCallsign}`,
|
||||
style: { fontWeight: "bold" },
|
||||
textSize: "2",
|
||||
set({ page: pageData.page });
|
||||
},
|
||||
{ textLeft: "ILS VAR#", textSize: "3" },
|
||||
{
|
||||
textLeft: fmsStatus,
|
||||
style: { fontWeight: "extrabold" },
|
||||
textSize: "4",
|
||||
},
|
||||
{
|
||||
textLeft: fmsStatusDescription[fmsStatus],
|
||||
textSize: "1",
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "sending-status": {
|
||||
const { station } = pageData as SetSendingStatusPageParams;
|
||||
set({
|
||||
page: "sending-status",
|
||||
lines: [
|
||||
{
|
||||
textLeft: `${station?.bosCallsign}`,
|
||||
style: { fontWeight: "bold" },
|
||||
textSize: "2",
|
||||
},
|
||||
{ textLeft: "ILS VAR#", textSize: "3" },
|
||||
{
|
||||
textMid: "sending...",
|
||||
style: { fontWeight: "bold" },
|
||||
textSize: "4",
|
||||
},
|
||||
{
|
||||
textLeft: "Status wird gesendet...",
|
||||
textSize: "1",
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "new-status": {
|
||||
const { station } = pageData as SetNewStatusPageParams;
|
||||
set({
|
||||
page: "new-status",
|
||||
lines: [
|
||||
{
|
||||
textLeft: `${station?.bosCallsign}`,
|
||||
style: { fontWeight: "bold" },
|
||||
textSize: "2",
|
||||
},
|
||||
{ textLeft: "ILS VAR#", textSize: "3" },
|
||||
{
|
||||
textLeft: "empfangen",
|
||||
style: { fontWeight: "bold" },
|
||||
textSize: "4",
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "sds": {
|
||||
const { sdsMessage } = pageData as SetSdsPageParams;
|
||||
const msg = sdsMessage.data.message;
|
||||
set({
|
||||
page: "sds",
|
||||
lines: [
|
||||
{
|
||||
textLeft: `SDS-Nachricht`,
|
||||
style: { fontWeight: "bold" },
|
||||
textSize: "2",
|
||||
},
|
||||
{
|
||||
textLeft: msg,
|
||||
style: {
|
||||
whiteSpace: "normal",
|
||||
overflowWrap: "break-word",
|
||||
wordBreak: "break-word",
|
||||
display: "block",
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
overflow: "auto",
|
||||
textOverflow: "ellipsis",
|
||||
lineHeight: "1.2em",
|
||||
},
|
||||
textSize: "2",
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
set({ page: "home" });
|
||||
break;
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "mrt-store", // unique name
|
||||
},
|
||||
),
|
||||
);
|
||||
}));
|
||||
|
||||
@@ -86,7 +86,7 @@ pilotSocket.on("connect", () => {
|
||||
usePilotConnectionStore.setState({ status: "connected", message: "" });
|
||||
const { logoffTime, selectedStation, debug } = usePilotConnectionStore.getState();
|
||||
dispatchSocket.disconnect();
|
||||
useAudioStore.getState().connect("LST_01", selectedStation?.bosCallsignShort || "pilot");
|
||||
useAudioStore.getState().connect(undefined, selectedStation?.bosCallsignShort || "pilot");
|
||||
|
||||
pilotSocket.emit("connect-pilot", {
|
||||
logoffTime,
|
||||
@@ -109,7 +109,7 @@ pilotSocket.on("connect-message", (data) => {
|
||||
});
|
||||
|
||||
pilotSocket.on("disconnect", () => {
|
||||
usePilotConnectionStore.setState({ status: "disconnected" });
|
||||
usePilotConnectionStore.setState({ status: "disconnected", connectedAircraft: null });
|
||||
useAudioStore.getState().disconnect();
|
||||
});
|
||||
|
||||
@@ -142,11 +142,13 @@ pilotSocket.on("mission-alert", (data: Mission & { Stations: Station[] }) => {
|
||||
});
|
||||
|
||||
pilotSocket.on("sds-message", (sdsMessage: MissionSdsLog) => {
|
||||
console.log("Received sds-message via socket:", sdsMessage);
|
||||
const station = usePilotConnectionStore.getState().selectedStation;
|
||||
if (!station) return;
|
||||
useMrtStore.getState().setPage({
|
||||
page: "sds",
|
||||
station,
|
||||
sdsMessage,
|
||||
useMrtStore.getState().setPopup({
|
||||
popup: "sds-received",
|
||||
});
|
||||
useMrtStore.getState().setStringifiedData({
|
||||
sdsText: sdsMessage.data.message,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Mission, Station, User } from "@repo/db";
|
||||
import { DisplayLineProps } from "(app)/pilot/_components/dme/Dme";
|
||||
import { create } from "zustand";
|
||||
import { syncTabs } from "zustand-sync-tabs";
|
||||
|
||||
interface SetHomePageParams {
|
||||
page: "home";
|
||||
@@ -45,9 +44,7 @@ interface MrtStore {
|
||||
|
||||
let interval: NodeJS.Timeout | null = null;
|
||||
|
||||
export const useDmeStore = create<MrtStore>(
|
||||
syncTabs(
|
||||
(set) => ({
|
||||
export const useDmeStore = create<MrtStore>((set) => ({
|
||||
page: "home",
|
||||
pageData: {
|
||||
message: "",
|
||||
@@ -233,9 +230,4 @@ export const useDmeStore = create<MrtStore>(
|
||||
break;
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "dme-store", // unique name
|
||||
},
|
||||
),
|
||||
);
|
||||
}));
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
src: url("/fonts/MelderV2.ttf") format("truetype"); /* Chrome 4+, Firefox 3.5, Opera 10+, Safari 3—5 */
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Bahnschrift";
|
||||
src: url("/fonts/bahnschrift.ttf") format("truetype"); /* Chrome 4+, Firefox 3.5, Opera 10+, Safari 3—5 */
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-rescuetrack: #46b7a3;
|
||||
--color-rescuetrack-highlight: #ff4500;
|
||||
|
||||
1
apps/dispatch/next-env.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
const nextConfig = {
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -60,7 +60,6 @@
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "^5.8.3",
|
||||
"zod": "^3.25.67",
|
||||
"zustand": "^5.0.6",
|
||||
"zustand-sync-tabs": "^0.2.2"
|
||||
"zustand": "^5.0.6"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
apps/dispatch/public/fonts/bahnschrift.ttf
Normal file
BIN
apps/dispatch/public/sounds/1504.wav
Normal file
BIN
apps/dispatch/public/sounds/403.wav
Normal file
BIN
apps/dispatch/public/sounds/775.wav
Normal file
@@ -1,9 +1,5 @@
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
|
||||
ENV PNPM_HOME="/usr/local/pnpm"
|
||||
ENV PATH="${PNPM_HOME}:${PATH}"
|
||||
|
||||
ARG NEXT_PUBLIC_HUB_URL
|
||||
ARG NEXT_PUBLIC_HUB_SERVER_URL
|
||||
ARG NEXT_PUBLIC_DISCORD_URL
|
||||
@@ -16,13 +12,13 @@ ENV NEXT_PUBLIC_DISCORD_URL=${NEXT_PUBLIC_DISCORD_URL}
|
||||
ENV NEXT_PUBLIC_MOODLE_URL=${NEXT_PUBLIC_MOODLE_URL}
|
||||
ENV NEXT_PUBLIC_DISPATCH_URL=${NEXT_PUBLIC_DISPATCH_URL}
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
|
||||
RUN echo "NEXT_PUBLIC_DISCORD_URL=${NEXT_PUBLIC_DISCORD_URL}"
|
||||
RUN pnpm add -g turbo@^2.5
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
ENV PNPM_HOME="/usr/local/pnpm"
|
||||
ENV PATH="${PNPM_HOME}:${PATH}"
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
RUN pnpm add -g turbo@^2.5
|
||||
RUN apk update
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
@@ -33,6 +29,13 @@ COPY . .
|
||||
RUN turbo prune hub --docker
|
||||
|
||||
FROM base AS installer
|
||||
|
||||
ENV PNPM_HOME="/usr/local/pnpm"
|
||||
ENV PATH="${PNPM_HOME}:${PATH}"
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
RUN pnpm add -g turbo@^2.5
|
||||
|
||||
RUN apk update
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
@@ -44,21 +47,24 @@ RUN pnpm install
|
||||
# Build the project
|
||||
COPY --from=builder /usr/app/out/full/ .
|
||||
|
||||
RUN turbo run build
|
||||
RUN turbo run build --filter=hub...
|
||||
|
||||
FROM base AS runner
|
||||
FROM node:22-alpine AS runner
|
||||
WORKDIR /usr/app
|
||||
|
||||
# Don't run production as root
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
USER nextjs
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=installer --chown=nextjs:nodejs /usr/app/ ./
|
||||
COPY --from=installer --chown=nextjs:nodejs /usr/app/apps/hub/.next/standalone ./
|
||||
COPY --from=installer --chown=nextjs:nodejs /usr/app/apps/hub/.next/static ./apps/hub/.next/static
|
||||
COPY --from=installer --chown=nextjs:nodejs /usr/app/apps/hub/public ./apps/hub/public
|
||||
|
||||
USER nextjs
|
||||
|
||||
# Expose the application port
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["pnpm", "--dir", "apps/hub", "run", "start"]
|
||||
CMD ["node", "apps/hub/server.js"]
|
||||
@@ -264,6 +264,7 @@ export const Form = ({ event }: { event?: Event }) => {
|
||||
showSearch
|
||||
getFilter={(searchTerm) =>
|
||||
({
|
||||
AND: [{ eventId: event?.id }],
|
||||
OR: [
|
||||
{
|
||||
User: {
|
||||
|
||||
@@ -776,6 +776,7 @@ export const AdminForm = ({
|
||||
{discordAccount && (
|
||||
<tr>
|
||||
<td>
|
||||
{discordAccount.avatar ? (
|
||||
<Image
|
||||
src={`https://cdn.discordapp.com/avatars/${discordAccount.discordId}/${discordAccount.avatar}.png`}
|
||||
alt="Discord Avatar"
|
||||
@@ -783,6 +784,11 @@ export const AdminForm = ({
|
||||
height={40}
|
||||
className="h-10 w-10 rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-gray-500 text-white">
|
||||
N/A
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td>{discordAccount.username}</td>
|
||||
<td>{discordAccount.discordId}</td>
|
||||
@@ -792,7 +798,8 @@ export const AdminForm = ({
|
||||
{formerDiscordAccounts.map((account) => (
|
||||
<tr key={account.discordId}>
|
||||
<td>
|
||||
{account.DiscordAccount && (
|
||||
{account.DiscordAccount &&
|
||||
(account.DiscordAccount.avatar ? (
|
||||
<Image
|
||||
src={`https://cdn.discordapp.com/avatars/${account.DiscordAccount.discordId}/${account.DiscordAccount.avatar}.png`}
|
||||
alt="Discord Avatar"
|
||||
@@ -800,7 +807,11 @@ export const AdminForm = ({
|
||||
height={40}
|
||||
className="h-10 w-10 rounded-full"
|
||||
/>
|
||||
)}
|
||||
) : (
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-gray-500 text-white">
|
||||
N/A
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
<td>{account.DiscordAccount?.username || "Unbekannt"}</td>
|
||||
<td>{account.DiscordAccount?.discordId || "Unbekannt"}</td>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/* const removeImports = require("next-remove-imports")(); */
|
||||
/* const nextConfig = removeImports({}); */
|
||||
const nextConfig = {
|
||||
output: "standalone",
|
||||
images: {
|
||||
domains: ["cdn.discordapp.com", "nextcloud.virtualairrescue.com"],
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"node": ">=18",
|
||||
"pnpm": ">=10"
|
||||
},
|
||||
"packageManager": "pnpm@10.13.1",
|
||||
"packageManager": "pnpm@10.28.0",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Station } from "../../generated/client";
|
||||
import { StationStatus } from "./SocketEvents";
|
||||
import { PublicUser } from "./User";
|
||||
|
||||
export interface MissionVehicleLog {
|
||||
@@ -37,6 +38,19 @@ export interface MissionSdsLog {
|
||||
};
|
||||
}
|
||||
|
||||
export interface MissionSdsStatusLog {
|
||||
type: "sds-status-log";
|
||||
auto: false;
|
||||
timeStamp: string;
|
||||
data: {
|
||||
direction: "to-lst" | "to-aircraft";
|
||||
stationId: number;
|
||||
station: Station;
|
||||
user: PublicUser;
|
||||
status: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MissionMessageLog {
|
||||
type: "message-log";
|
||||
auto: false;
|
||||
@@ -90,4 +104,5 @@ export type MissionLog =
|
||||
| MissionAlertLogAuto
|
||||
| MissionCompletedLog
|
||||
| MissionVehicleLog
|
||||
| MissionReopenedLog;
|
||||
| MissionReopenedLog
|
||||
| MissionSdsStatusLog;
|
||||
|
||||
521
pnpm-lock.yaml
generated
@@ -23,6 +23,9 @@ overrides:
|
||||
next@>=15.0.0 <=15.4.4: '>=15.4.5'
|
||||
next@>=15.0.0-canary.0 <15.4.7: '>=15.4.7'
|
||||
next@>=15.4.0-canary.0 <15.4.8: '>=15.4.8'
|
||||
next@>=15.4.0-canary.0 <15.4.9: '>=15.4.9'
|
||||
nodemailer@<7.0.7: '>=7.0.7'
|
||||
nodemailer@<=7.0.10: '>=7.0.11'
|
||||
playwright@<1.55.1: '>=1.55.1'
|
||||
preact@>=10.26.5 <10.26.10: '>=10.26.10'
|
||||
qs@<6.14.1: '>=6.14.1'
|
||||
|
||||