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,
|
AdminMessage,
|
||||||
getPublicUser,
|
getPublicUser,
|
||||||
MissionLog,
|
MissionLog,
|
||||||
|
MissionSdsStatusLog,
|
||||||
NotificationPayload,
|
NotificationPayload,
|
||||||
Prisma,
|
Prisma,
|
||||||
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
|
// Kick a connectedAircraft by ID
|
||||||
router.delete("/:id", async (req, res) => {
|
router.delete("/:id", async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
FROM node:22-alpine AS base
|
FROM node:22-alpine AS base
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_DISPATCH_URL
|
ARG NEXT_PUBLIC_DISPATCH_URL="http://localhost:3001"
|
||||||
ARG NEXT_PUBLIC_DISPATCH_SERVER_URL
|
ARG NEXT_PUBLIC_DISPATCH_SERVER_URL="http://localhost:4001"
|
||||||
ARG NEXT_PUBLIC_HUB_URL
|
ARG NEXT_PUBLIC_HUB_URL="http://localhost:3002"
|
||||||
ARG NEXT_PUBLIC_DISPATCH_SERVICE_ID
|
ARG NEXT_PUBLIC_DISPATCH_SERVICE_ID="1"
|
||||||
ARG NEXT_PUBLIC_LIVEKIT_URL
|
ARG NEXT_PUBLIC_LIVEKIT_URL="http://localhost:7880"
|
||||||
ARG NEXT_PUBLIC_DISCORD_URL
|
ARG NEXT_PUBLIC_DISCORD_URL="https://discord.com"
|
||||||
ARG NEXT_PUBLIC_OPENAIP_ACCESS
|
ARG NEXT_PUBLIC_OPENAIP_ACCESS=""
|
||||||
|
|
||||||
ENV NEXT_PUBLIC_DISPATCH_SERVER_URL=$NEXT_PUBLIC_DISPATCH_SERVER_URL
|
ENV NEXT_PUBLIC_DISPATCH_SERVER_URL=$NEXT_PUBLIC_DISPATCH_SERVER_URL
|
||||||
ENV NEXT_PUBLIC_DISPATCH_URL=$NEXT_PUBLIC_DISPATCH_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_OPENAIP_ACCESS=$NEXT_PUBLIC_OPENAIP_ACCESS
|
||||||
ENV NEXT_PUBLIC_DISCORD_URL=$NEXT_PUBLIC_DISCORD_URL
|
ENV NEXT_PUBLIC_DISCORD_URL=$NEXT_PUBLIC_DISCORD_URL
|
||||||
|
|
||||||
|
FROM base AS builder
|
||||||
|
|
||||||
ENV PNPM_HOME="/usr/local/pnpm"
|
ENV PNPM_HOME="/usr/local/pnpm"
|
||||||
ENV PATH="${PNPM_HOME}:${PATH}"
|
ENV PATH="${PNPM_HOME}:${PATH}"
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
|
||||||
RUN pnpm add -g turbo@^2.5
|
RUN pnpm add -g turbo@^2.5
|
||||||
|
|
||||||
FROM base AS builder
|
|
||||||
RUN apk update
|
RUN apk update
|
||||||
RUN apk add --no-cache libc6-compat
|
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_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_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_DISPATCH_SERVER_URL is: $NEXT_PUBLIC_DISPATCH_SERVER_URL"
|
||||||
|
RUN echo "NEXT_PUBLIC_LIVEKIT_URL is: $NEXT_PUBLIC_LIVEKIT_URL"
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN turbo prune dispatch --docker
|
RUN turbo prune dispatch --docker
|
||||||
|
|
||||||
FROM base AS installer
|
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 update
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
|
|
||||||
@@ -50,19 +58,22 @@ COPY --from=builder /usr/app/out/full/ .
|
|||||||
|
|
||||||
RUN turbo run build
|
RUN turbo run build
|
||||||
|
|
||||||
FROM base AS runner
|
FROM node:22-alpine AS runner
|
||||||
WORKDIR /usr/app
|
WORKDIR /usr/app
|
||||||
|
|
||||||
# Don't run production as root
|
# Don't run production as root
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
RUN adduser --system --uid 1001 nextjs
|
RUN adduser --system --uid 1001 nextjs
|
||||||
USER nextjs
|
|
||||||
|
|
||||||
# Automatically leverage output traces to reduce image size
|
# Automatically leverage output traces to reduce image size
|
||||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
# 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 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 connection = useDispatchConnectionStore((state) => state);
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
logoffTime: "",
|
logoffTime: "",
|
||||||
selectedZone: "LST_01",
|
selectedZone: "VAR_LST_RD_01",
|
||||||
ghostMode: false,
|
ghostMode: false,
|
||||||
});
|
});
|
||||||
const changeDispatcherMutation = useMutation({
|
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 { 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 "./mrt.css";
|
||||||
import Image from "next/image";
|
import { MrtBase } from "./Base";
|
||||||
import { useMrtStore } from "_store/pilot/MrtStore";
|
import { MrtDisplay } from "./MrtDisplay";
|
||||||
|
import { MrtButtons } from "./MrtButtons";
|
||||||
const MRT_BUTTON_STYLES: CSSProperties = {
|
import { MrtPopups } from "./MrtPopups";
|
||||||
cursor: "pointer",
|
|
||||||
zIndex: "9999",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
border: "none",
|
|
||||||
};
|
|
||||||
const MRT_DISPLAYLINE_STYLES: CSSProperties = {
|
|
||||||
color: "white",
|
|
||||||
zIndex: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface DisplayLineProps {
|
export interface DisplayLineProps {
|
||||||
lineStyle?: CSSProperties;
|
lineStyle?: CSSProperties;
|
||||||
@@ -27,45 +14,7 @@ export interface DisplayLineProps {
|
|||||||
textSize: "1" | "2" | "3" | "4";
|
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 = () => {
|
export const Mrt = () => {
|
||||||
useSounds();
|
|
||||||
const { handleButton } = useButtons();
|
|
||||||
const { lines, page } = useMrtStore((state) => state);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="mrt-container"
|
id="mrt-container"
|
||||||
@@ -78,150 +27,16 @@ export const Mrt = () => {
|
|||||||
maxHeight: "100%",
|
maxHeight: "100%",
|
||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
color: "white",
|
color: "white",
|
||||||
gridTemplateColumns: "21.83% 4.43% 24.42% 18.08% 5.93% 1.98% 6.00% 1.69% 6.00% 9.35%",
|
gridTemplateColumns:
|
||||||
gridTemplateRows: "21.58% 11.87% 3.55% 5.00% 6.84% 0.53% 3.03% 11.84% 3.55% 11.84% 20.39%",
|
"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" && (
|
<MrtPopups />
|
||||||
<Image
|
<MrtDisplay />
|
||||||
src={MrtImage}
|
<MrtButtons />
|
||||||
alt="MrtImage"
|
<MrtBase />
|
||||||
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,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</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 { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
||||||
import { useMrtStore } from "_store/pilot/MrtStore";
|
import { useMrtStore } from "_store/pilot/MrtStore";
|
||||||
import { pilotSocket } from "(app)/pilot/socket";
|
import { pilotSocket } from "(app)/pilot/socket";
|
||||||
import { editConnectedAircraftAPI } from "_querys/aircrafts";
|
import { editConnectedAircraftAPI } from "_querys/aircrafts";
|
||||||
import { useEffect } from "react";
|
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 = () => {
|
export const useButtons = () => {
|
||||||
const station = usePilotConnectionStore((state) => state.selectedStation);
|
const session = useSession();
|
||||||
const connectedAircraft = usePilotConnectionStore((state) => state.connectedAircraft);
|
const { connect, setSelectedRoom, selectedRoom } = useAudioStore((state) => state);
|
||||||
const connectionStatus = usePilotConnectionStore((state) => state.status);
|
|
||||||
|
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({
|
const updateAircraftMutation = useMutation({
|
||||||
mutationKey: ["edit-pilot-connected-aircraft"],
|
mutationKey: ["edit-pilot-connected-aircraft"],
|
||||||
mutationFn: ({
|
mutationFn: ({
|
||||||
@@ -21,56 +64,161 @@ export const useButtons = () => {
|
|||||||
}) => editConnectedAircraftAPI(aircraftId, data),
|
}) => editConnectedAircraftAPI(aircraftId, data),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { setPage } = useMrtStore((state) => state);
|
const { setPage, setPopup, page, popup, setStringifiedData, stringifiedData } = useMrtStore(
|
||||||
|
(state) => state,
|
||||||
|
);
|
||||||
|
|
||||||
const handleButton =
|
const role =
|
||||||
(button: "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0" | "home") => () => {
|
(pilotState == "connected" && selectedStation?.bosCallsignShort) ||
|
||||||
if (connectionStatus !== "connected") return;
|
session.data?.user?.publicId;
|
||||||
if (!station) 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"
|
|
||||||
) {
|
|
||||||
setPage({ page: "sending-status", station });
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
const handleHold = (button: ButtonTypes) => async () => {
|
||||||
await updateAircraftMutation.mutateAsync({
|
/* if (connectionStatus !== "connected") return; */
|
||||||
aircraftId: connectedAircraft.id,
|
if (button === "end-call") {
|
||||||
data: {
|
setPage({ page: "off" });
|
||||||
fmsStatus: button,
|
setPopup(null);
|
||||||
},
|
}
|
||||||
});
|
if (button === "1" && page === "off") {
|
||||||
setPage({
|
setPage({ page: "startup" });
|
||||||
page: "home",
|
return;
|
||||||
station,
|
}
|
||||||
|
if (!selectedStation) return;
|
||||||
|
if (!session.data?.user) return;
|
||||||
|
if (!connectedAircraft?.id) return;
|
||||||
|
if (
|
||||||
|
button === "1" ||
|
||||||
|
button === "2" ||
|
||||||
|
button === "3" ||
|
||||||
|
button === "4" ||
|
||||||
|
button === "6" ||
|
||||||
|
button === "7" ||
|
||||||
|
button === "8"
|
||||||
|
) {
|
||||||
|
longBtnPressSoundRef.current?.play();
|
||||||
|
const delay = Math.random() * 1500 + 500;
|
||||||
|
setTimeout(async () => {
|
||||||
|
await updateAircraftMutation.mutateAsync({
|
||||||
|
aircraftId: connectedAircraft.id,
|
||||||
|
data: {
|
||||||
fmsStatus: button,
|
fmsStatus: button,
|
||||||
});
|
},
|
||||||
}, 1000);
|
});
|
||||||
} else {
|
setPopup({ popup: "status-sent" });
|
||||||
setPage({ page: "home", fmsStatus: connectedAircraft.fmsStatus || "6", station });
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
pilotSocket.on("connect", () => {
|
pilotSocket.on("connect", () => {
|
||||||
if (!station) return;
|
const { page } = useMrtStore.getState();
|
||||||
setPage({ page: "home", fmsStatus: "6", station });
|
if (!selectedStation || page !== "off") return;
|
||||||
|
setPage({ page: "startup" });
|
||||||
});
|
});
|
||||||
|
}, [setPage, selectedStation, setPopup]);
|
||||||
|
|
||||||
pilotSocket.on("aircraft-update", () => {
|
return { handleKlick, handleHold };
|
||||||
if (!station) return;
|
|
||||||
setPage({ page: "new-status", station });
|
|
||||||
});
|
|
||||||
}, [setPage, station]);
|
|
||||||
|
|
||||||
return { handleButton };
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,52 +1,39 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
import { useAudioStore } from "_store/audioStore";
|
||||||
import { useMrtStore } from "_store/pilot/MrtStore";
|
import { RoomEvent } from "livekit-client";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
export const useSounds = () => {
|
export const useSounds = () => {
|
||||||
const mrtState = useMrtStore((state) => state);
|
const { room } = useAudioStore((state) => state);
|
||||||
const { connectedAircraft, selectedStation } = usePilotConnectionStore((state) => state);
|
const longBtnPressSoundRef = useRef<HTMLAudioElement>(null);
|
||||||
|
const statusSentSoundRef = useRef<HTMLAudioElement>(null);
|
||||||
const setPage = useMrtStore((state) => state.setPage);
|
const sdsReceivedSoundRef = useRef<HTMLAudioElement>(null);
|
||||||
const MRTstatusSoundRef = useRef<HTMLAudioElement>(null);
|
|
||||||
const MrtMessageReceivedSoundRef = useRef<HTMLAudioElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
MRTstatusSoundRef.current = new Audio("/sounds/MRT-status.mp3");
|
longBtnPressSoundRef.current = new Audio("/sounds/1504.wav");
|
||||||
MrtMessageReceivedSoundRef.current = new Audio("/sounds/MRT-message-received.mp3");
|
statusSentSoundRef.current = new Audio("/sounds/403.wav");
|
||||||
MRTstatusSoundRef.current.onended = () => {
|
sdsReceivedSoundRef.current = new Audio("/sounds/775.wav");
|
||||||
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,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}, [connectedAircraft?.fmsStatus, selectedStation, setPage, mrtState.page]);
|
}, []);
|
||||||
|
|
||||||
const fmsStatus = connectedAircraft?.fmsStatus || "NaN";
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!connectedAircraft) return;
|
const handleRoomConnected = () => {
|
||||||
if (mrtState.page === "new-status") {
|
// Play a sound when connected to the room
|
||||||
if (fmsStatus === "J" || fmsStatus === "c") {
|
// connectedSound.play();
|
||||||
MrtMessageReceivedSoundRef.current?.play();
|
statusSentSoundRef.current?.play();
|
||||||
} else {
|
console.log("Room connected - played sound");
|
||||||
MRTstatusSoundRef.current?.play();
|
};
|
||||||
}
|
room?.on(RoomEvent.Connected, handleRoomConnected);
|
||||||
} else if (mrtState.page === "sds") {
|
|
||||||
MrtMessageReceivedSoundRef.current?.play();
|
return () => {
|
||||||
}
|
room?.off(RoomEvent.Connected, handleRoomConnected);
|
||||||
}, [mrtState, fmsStatus, connectedAircraft, selectedStation]);
|
};
|
||||||
|
}, [room]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
longBtnPressSoundRef,
|
||||||
|
statusSentSoundRef,
|
||||||
|
sdsReceivedSoundRef,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ export const ConnectionBtn = () => {
|
|||||||
const session = useSession();
|
const session = useSession();
|
||||||
const uid = session.data?.user?.id;
|
const uid = session.data?.user?.id;
|
||||||
if (!uid) return null;
|
if (!uid) return null;
|
||||||
console.log(bookings);
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-box bg-base-200 flex items-center justify-center gap-2 p-1">
|
<div className="rounded-box bg-base-200 flex items-center justify-center gap-2 p-1">
|
||||||
{connection.message.length > 0 && (
|
{connection.message.length > 0 && (
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const Map = dynamic(() => import("_components/map/Map"), {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const PilotPage = () => {
|
const PilotPage = () => {
|
||||||
const { connectedAircraft, status, } = usePilotConnectionStore((state) => state);
|
const { connectedAircraft, status } = usePilotConnectionStore((state) => state);
|
||||||
const { latestMission } = useDmeStore((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
|
// Query will be cached anyway, due to this, displayed Markers are in sync with own Aircraft connection-warning
|
||||||
const { data: aircrafts } = useQuery({
|
const { data: aircrafts } = useQuery({
|
||||||
@@ -94,10 +94,20 @@ const PilotPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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="bg-base-300 flex h-full w-full flex-col p-4">
|
||||||
<div className="flex justify-between">
|
<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
|
<div
|
||||||
className="tooltip tooltip-left mb-4"
|
className="tooltip tooltip-left mb-4"
|
||||||
data-tip="Dadurch wird der Einsatz erneut an den Desktop-Client gesendet."
|
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 = () => {
|
export const Audio = () => {
|
||||||
const {
|
const {
|
||||||
|
selectedRoom,
|
||||||
speakingParticipants,
|
speakingParticipants,
|
||||||
resetSpeakingParticipants,
|
resetSpeakingParticipants,
|
||||||
isTalking,
|
isTalking,
|
||||||
@@ -37,8 +38,8 @@ export const Audio = () => {
|
|||||||
room,
|
room,
|
||||||
message,
|
message,
|
||||||
removeMessage,
|
removeMessage,
|
||||||
|
setSelectedRoom,
|
||||||
} = useAudioStore();
|
} = useAudioStore();
|
||||||
const [selectedRoom, setSelectedRoom] = useState<string>("LST_01");
|
|
||||||
|
|
||||||
useSounds({
|
useSounds({
|
||||||
isReceiving: speakingParticipants.length > 0,
|
isReceiving: speakingParticipants.length > 0,
|
||||||
@@ -48,7 +49,7 @@ export const Audio = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { selectedStation, status: pilotState } = usePilotConnectionStore((state) => state);
|
const { selectedStation, status: pilotState } = usePilotConnectionStore((state) => state);
|
||||||
const { selectedZone, status: dispatcherState } = useDispatchConnectionStore((state) => state);
|
const { status: dispatcherState } = useDispatchConnectionStore((state) => state);
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const [isReceivingBlick, setIsReceivingBlick] = useState(false);
|
const [isReceivingBlick, setIsReceivingBlick] = useState(false);
|
||||||
const [recentSpeakers, setRecentSpeakers] = useState<typeof speakingParticipants>([]);
|
const [recentSpeakers, setRecentSpeakers] = useState<typeof speakingParticipants>([]);
|
||||||
@@ -93,7 +94,7 @@ export const Audio = () => {
|
|||||||
const canStopOtherSpeakers = dispatcherState === "connected";
|
const canStopOtherSpeakers = dispatcherState === "connected";
|
||||||
|
|
||||||
const role =
|
const role =
|
||||||
(dispatcherState === "connected" && selectedZone) ||
|
(dispatcherState === "connected" && "VAR LST") ||
|
||||||
(pilotState == "connected" && selectedStation?.bosCallsignShort) ||
|
(pilotState == "connected" && selectedStation?.bosCallsignShort) ||
|
||||||
session.data?.user?.publicId;
|
session.data?.user?.publicId;
|
||||||
|
|
||||||
@@ -185,20 +186,20 @@ export const Audio = () => {
|
|||||||
</summary>
|
</summary>
|
||||||
<ul className="menu dropdown-content bg-base-200 rounded-box z-[1050] w-52 p-2 shadow-sm">
|
<ul className="menu dropdown-content bg-base-200 rounded-box z-[1050] w-52 p-2 shadow-sm">
|
||||||
{ROOMS.map((r) => (
|
{ROOMS.map((r) => (
|
||||||
<li key={r}>
|
<li key={r.id}>
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-ghost relative flex items-center justify-start gap-2 text-left"
|
className="btn btn-sm btn-ghost relative flex items-center justify-start gap-2 text-left"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!role) return;
|
if (!role) return;
|
||||||
if (selectedRoom === r) return;
|
if (selectedRoom?.name === r.name) return;
|
||||||
setSelectedRoom(r);
|
setSelectedRoom(r);
|
||||||
connect(r, role);
|
connect(r, role);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{room?.name === r && (
|
{room?.name === r.name && (
|
||||||
<Disc className="text-success absolute left-2 text-sm" width={15} />
|
<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>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -63,14 +63,15 @@ export function QueryProvider({ children }: { children: ReactNode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleNotification = (notification: NotificationPayload) => {
|
const handleNotification = (notification: NotificationPayload) => {
|
||||||
|
console.log("Received notification:", notification);
|
||||||
const playNotificationSound = () => {
|
const playNotificationSound = () => {
|
||||||
if (notificationSound.current) {
|
if (notificationSound.current) {
|
||||||
notificationSound.current.currentTime = 0;
|
notificationSound.current.currentTime = 0;
|
||||||
notificationSound.current
|
notificationSound.current
|
||||||
.play()
|
.play()
|
||||||
.catch((e) => console.error("Notification sound error:", e));
|
.catch((e) => console.error("Notification sound error:", e));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
switch (notification.type) {
|
switch (notification.type) {
|
||||||
case "hpg-validation":
|
case "hpg-validation":
|
||||||
@@ -90,6 +91,7 @@ export function QueryProvider({ children }: { children: ReactNode }) {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "station-status":
|
case "station-status":
|
||||||
|
console.log("station Status", QUICK_RESPONSE[notification.status]);
|
||||||
if (!QUICK_RESPONSE[notification.status]) return;
|
if (!QUICK_RESPONSE[notification.status]) return;
|
||||||
toast.custom((e) => <StatusToast event={notification} t={e} />, {
|
toast.custom((e) => <StatusToast event={notification} t={e} />, {
|
||||||
duration: 60000,
|
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 { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { BaseNotification } from "_components/customToasts/BaseNotification";
|
import { BaseNotification } from "_components/customToasts/BaseNotification";
|
||||||
import { FMS_STATUS_COLORS } from "_helpers/fmsStatusColors";
|
import { FMS_STATUS_COLORS } from "_helpers/fmsStatusColors";
|
||||||
import { editConnectedAircraftAPI, getConnectedAircraftsAPI } from "_querys/aircrafts";
|
import { getConnectedAircraftsAPI } from "_querys/aircrafts";
|
||||||
import { getLivekitRooms } from "_querys/livekit";
|
import { getLivekitRooms } from "_querys/livekit";
|
||||||
|
import { sendSdsStatusMessageAPI } from "_querys/missions";
|
||||||
import { getStationsAPI } from "_querys/stations";
|
import { getStationsAPI } from "_querys/stations";
|
||||||
import { useAudioStore } from "_store/audioStore";
|
import { useAudioStore } from "_store/audioStore";
|
||||||
import { useMapStore } from "_store/mapStore";
|
import { useMapStore } from "_store/mapStore";
|
||||||
import { X } from "lucide-react";
|
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";
|
import { Toast, toast } from "react-hot-toast";
|
||||||
|
|
||||||
export const QUICK_RESPONSE: Record<string, string[]> = {
|
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 status5Sounds = useRef<HTMLAudioElement | null>(null);
|
||||||
const status9Sounds = useRef<HTMLAudioElement | null>(null);
|
const status9Sounds = useRef<HTMLAudioElement | null>(null);
|
||||||
|
|
||||||
|
const session = useSession();
|
||||||
|
|
||||||
const { data: livekitRooms } = useQuery({
|
const { data: livekitRooms } = useQuery({
|
||||||
queryKey: ["livekit-rooms"],
|
queryKey: ["livekit-rooms"],
|
||||||
queryFn: () => getLivekitRooms(),
|
queryFn: () => getLivekitRooms(),
|
||||||
@@ -46,7 +50,7 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) =>
|
|||||||
status9Sounds.current = new Audio("/sounds/status-9.mp3");
|
status9Sounds.current = new Audio("/sounds/status-9.mp3");
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
const [aircraftDataAcurate, setAircraftDataAccurate] = useState(false);
|
|
||||||
//const mapStore = useMapStore((s) => s);
|
//const mapStore = useMapStore((s) => s);
|
||||||
const { setOpenAircraftMarker, setMap } = useMapStore((store) => store);
|
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 station = stations?.find((s) => s.id === event.data?.stationId);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const changeAircraftMutation = useMutation({
|
const sendSdsStatusMutation = useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async ({ sdsMessage }: { sdsMessage: MissionSdsStatusLog }) => {
|
||||||
id,
|
if (!connectedAircraft?.id) throw new Error("No connected aircraft");
|
||||||
update,
|
await sendSdsStatusMessageAPI({ sdsMessage, aircraftId: connectedAircraft?.id });
|
||||||
}: {
|
|
||||||
id: number;
|
|
||||||
update: Prisma.ConnectedAircraftUpdateInput;
|
|
||||||
}) => {
|
|
||||||
await editConnectedAircraftAPI(id, update);
|
|
||||||
queryClient.invalidateQueries({
|
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(() => {
|
useEffect(() => {
|
||||||
let soundRef: React.RefObject<HTMLAudioElement | null> | null = null;
|
let soundRef: React.RefObject<HTMLAudioElement | null> | null = null;
|
||||||
switch (event.status) {
|
switch (event.status) {
|
||||||
@@ -103,7 +94,8 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) =>
|
|||||||
default:
|
default:
|
||||||
soundRef = null;
|
soundRef = null;
|
||||||
}
|
}
|
||||||
if (audioRoom !== livekitUser?.roomName) {
|
|
||||||
|
if (audioRoom && livekitUser?.roomName && audioRoom !== livekitUser?.roomName) {
|
||||||
toast.remove(t.id);
|
toast.remove(t.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -121,7 +113,8 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) =>
|
|||||||
};
|
};
|
||||||
}, [event.status, livekitUser?.roomName, audioRoom, t.id]);
|
}, [event.status, livekitUser?.roomName, audioRoom, t.id]);
|
||||||
|
|
||||||
if (!connectedAircraft || !station) return null;
|
console.log(connectedAircraft, station);
|
||||||
|
if (!connectedAircraft || !station || !session.data) return null;
|
||||||
return (
|
return (
|
||||||
<BaseNotification>
|
<BaseNotification>
|
||||||
<div className="flex flex-row items-center gap-14">
|
<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");
|
toast.error("Keine Flugzeug-ID gefunden");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await changeAircraftMutation.mutateAsync({
|
await sendSdsStatusMutation.mutateAsync({
|
||||||
id: event.data?.aircraftId,
|
sdsMessage: {
|
||||||
update: {
|
type: "sds-status-log",
|
||||||
fmsStatus: status,
|
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);
|
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 { DivIcon, Marker as LMarker, Popup as LPopup } from "leaflet";
|
||||||
import { useMapStore } from "_store/mapStore";
|
import { useMapStore } from "_store/mapStore";
|
||||||
import { Fragment, useCallback, useEffect, useRef, useState, useMemo } from "react";
|
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 { ChevronsRightLeft, House, MessageSquareText, Minimize2 } from "lucide-react";
|
||||||
import { SmartPopup, calculateAnchor, useSmartPopup } from "_components/SmartPopup";
|
import { SmartPopup, calculateAnchor, useSmartPopup } from "_components/SmartPopup";
|
||||||
import FMSStatusHistory, {
|
import FMSStatusHistory, {
|
||||||
@@ -396,27 +396,11 @@ const AircraftMarker = ({ aircraft }: { aircraft: ConnectedAircraft & { Station:
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AircraftLayer = () => {
|
export const AircraftLayer = () => {
|
||||||
const [aircrafts, setAircrafts] = useState<(ConnectedAircraft & { Station: Station })[]>([]);
|
const { data: aircrafts } = useQuery({
|
||||||
|
queryKey: ["connected-aircrafts", "map"],
|
||||||
useEffect(() => {
|
queryFn: () => getConnectedAircraftsAPI(),
|
||||||
const fetchAircrafts = async () => {
|
refetchInterval: 15000,
|
||||||
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 { setMap } = useMapStore((state) => state);
|
const { setMap } = useMapStore((state) => state);
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Mission,
|
Mission,
|
||||||
MissionLog,
|
MissionLog,
|
||||||
MissionSdsLog,
|
MissionSdsLog,
|
||||||
|
MissionSdsStatusLog,
|
||||||
MissionStationLog,
|
MissionStationLog,
|
||||||
Prisma,
|
Prisma,
|
||||||
PublicUser,
|
PublicUser,
|
||||||
@@ -40,7 +41,7 @@ import {
|
|||||||
TextSearch,
|
TextSearch,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { sendSdsMessageAPI } from "_querys/missions";
|
import { sendSdsMessageAPI, sendSdsStatusMessageAPI } from "_querys/missions";
|
||||||
import { getLivekitRooms } from "_querys/livekit";
|
import { getLivekitRooms } from "_querys/livekit";
|
||||||
import { findLeitstelleForPosition } from "_helpers/findLeitstelleinPoint";
|
import { findLeitstelleForPosition } from "_helpers/findLeitstelleinPoint";
|
||||||
import { formatDistance } from "date-fns";
|
import { formatDistance } from "date-fns";
|
||||||
@@ -54,9 +55,13 @@ const FMSStatusHistory = ({
|
|||||||
mission?: Mission;
|
mission?: Mission;
|
||||||
}) => {
|
}) => {
|
||||||
const log = ((mission?.missionLog as unknown as MissionLog[]) || [])
|
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()
|
.reverse()
|
||||||
.splice(0, 6) as MissionStationLog[];
|
.splice(0, 6) as (MissionStationLog | MissionSdsStatusLog)[];
|
||||||
|
|
||||||
const aircraftUser: PublicUser =
|
const aircraftUser: PublicUser =
|
||||||
typeof aircraft.publicUser === "string" ? JSON.parse(aircraft.publicUser) : aircraft.publicUser;
|
typeof aircraft.publicUser === "string" ? JSON.parse(aircraft.publicUser) : aircraft.publicUser;
|
||||||
@@ -103,10 +108,13 @@ const FMSStatusHistory = ({
|
|||||||
<span
|
<span
|
||||||
className="text-base font-bold"
|
className="text-base font-bold"
|
||||||
style={{
|
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>
|
||||||
<span className="text-base-content">
|
<span className="text-base-content">
|
||||||
{new Date(entry.timeStamp).toLocaleTimeString([], {
|
{new Date(entry.timeStamp).toLocaleTimeString([], {
|
||||||
@@ -126,6 +134,7 @@ const FMSStatusSelector = ({
|
|||||||
}: {
|
}: {
|
||||||
aircraft: ConnectedAircraft & { Station: Station };
|
aircraft: ConnectedAircraft & { Station: Station };
|
||||||
}) => {
|
}) => {
|
||||||
|
const session = useSession();
|
||||||
const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected";
|
const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected";
|
||||||
const [hoveredStatus, setHoveredStatus] = useState<string | null>(null);
|
const [hoveredStatus, setHoveredStatus] = useState<string | null>(null);
|
||||||
const queryClient = useQueryClient();
|
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 (
|
return (
|
||||||
<div className="text-base-content mt-2 flex flex-col gap-2 p-4">
|
<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">
|
<div className="flex h-full items-center justify-center gap-2">
|
||||||
@@ -213,12 +236,21 @@ const FMSStatusSelector = ({
|
|||||||
onMouseEnter={() => setHoveredStatus(status)}
|
onMouseEnter={() => setHoveredStatus(status)}
|
||||||
onMouseLeave={() => setHoveredStatus(null)}
|
onMouseLeave={() => setHoveredStatus(null)}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await changeAircraftMutation.mutateAsync({
|
await sendSdsStatusMutation.mutateAsync({
|
||||||
id: aircraft.id,
|
sdsMessage: {
|
||||||
update: {
|
type: "sds-status-log",
|
||||||
fmsStatus: status,
|
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}
|
{status}
|
||||||
@@ -378,7 +410,9 @@ const SDSTab = ({
|
|||||||
?.slice()
|
?.slice()
|
||||||
.reverse()
|
.reverse()
|
||||||
.filter(
|
.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],
|
[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">
|
<ul className="max-h-[300px] space-y-2 overflow-x-auto overflow-y-auto">
|
||||||
{log.map((entry, index) => {
|
{log.map((entry, index) => {
|
||||||
const sdsEntry = entry as MissionSdsLog;
|
const sdsEntry = entry as MissionSdsLog | MissionSdsStatusLog;
|
||||||
return (
|
return (
|
||||||
<li key={index} className="flex items-center gap-2">
|
<li key={index} className="flex items-center gap-2">
|
||||||
<span className="text-base-content">
|
<span className="text-base-content">
|
||||||
@@ -489,7 +523,9 @@ const SDSTab = ({
|
|||||||
{sdsEntry.data.user.firstname?.[0]?.toUpperCase() ?? "?"}
|
{sdsEntry.data.user.firstname?.[0]?.toUpperCase() ?? "?"}
|
||||||
{sdsEntry.data.user.lastname?.[0]?.toUpperCase() ?? "?"}
|
{sdsEntry.data.user.lastname?.[0]?.toUpperCase() ?? "?"}
|
||||||
</span>
|
</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>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -726,7 +726,11 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
|
|||||||
<span className="text-base-content">{entry.data.station.bosCallsign}</span>
|
<span className="text-base-content">{entry.data.station.bosCallsign}</span>
|
||||||
</li>
|
</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 (
|
return (
|
||||||
<li key={index} className="flex items-center gap-2">
|
<li key={index} className="flex items-center gap-2">
|
||||||
<span className="text-base-content">
|
<span className="text-base-content">
|
||||||
@@ -741,9 +745,10 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
|
|||||||
color: FMS_STATUS_TEXT_COLORS[6],
|
color: FMS_STATUS_TEXT_COLORS[6],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{entry.data.user.firstname?.[0]?.toUpperCase() ?? "?"}
|
{entry.type == "sds-status-log" && entry.data.direction == "to-lst"
|
||||||
{entry.data.user.lastname?.[0]?.toUpperCase() ?? "?"}
|
? entry.data.station.bosCallsignShort
|
||||||
{entry.type === "sds-log" && (
|
: `${entry.data.user.firstname?.[0]?.toUpperCase() ?? "?"}${entry.data.user.lastname?.[0]?.toUpperCase() ?? "?"}`}
|
||||||
|
{(entry.type === "sds-log" || entry.type === "sds-status-log") && (
|
||||||
<>
|
<>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -760,11 +765,17 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
{entry.data.station.bosCallsignShort}
|
{entry.type == "sds-status-log" && entry.data.direction == "to-aircraft"
|
||||||
|
? entry.data.station.bosCallsignShort
|
||||||
|
: "LST"}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</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>
|
</li>
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -24,3 +24,30 @@ export const fmsStatusDescription: { [key: string]: string } = {
|
|||||||
o: "Warten, alle Abfrageplätze belegt",
|
o: "Warten, alle Abfrageplätze belegt",
|
||||||
u: "Verstanden",
|
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 axios from "axios";
|
||||||
import { serverApi } from "_helpers/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);
|
const respone = await serverApi.patch<Mission>(`/mission/${id}`, mission);
|
||||||
return respone.data;
|
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 ({
|
export const sendSdsMessageAPI = async ({
|
||||||
missionId,
|
missionId,
|
||||||
sdsMessage,
|
sdsMessage,
|
||||||
|
|||||||
@@ -21,12 +21,13 @@ import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
|
|||||||
import { changeDispatcherAPI } from "_querys/dispatcher";
|
import { changeDispatcherAPI } from "_querys/dispatcher";
|
||||||
import { getRadioStream } from "_helpers/radioEffect";
|
import { getRadioStream } from "_helpers/radioEffect";
|
||||||
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
||||||
|
import { ROOMS } from "_data/livekitRooms";
|
||||||
|
|
||||||
let interval: NodeJS.Timeout;
|
let interval: NodeJS.Timeout;
|
||||||
|
|
||||||
type TalkState = {
|
type TalkState = {
|
||||||
addSpeakingParticipant: (participant: Participant) => void;
|
addSpeakingParticipant: (participant: Participant) => void;
|
||||||
connect: (roomName: string, role: string) => void;
|
connect: (room: (typeof ROOMS)[number] | undefined, role: string) => void;
|
||||||
connectionQuality: ConnectionQuality;
|
connectionQuality: ConnectionQuality;
|
||||||
disconnect: () => void;
|
disconnect: () => void;
|
||||||
isTalking: boolean;
|
isTalking: boolean;
|
||||||
@@ -44,6 +45,8 @@ type TalkState = {
|
|||||||
radioVolume: number;
|
radioVolume: number;
|
||||||
dmeVolume: number;
|
dmeVolume: number;
|
||||||
};
|
};
|
||||||
|
selectedRoom?: (typeof ROOMS)[number];
|
||||||
|
setSelectedRoom: (room: (typeof ROOMS)[number]) => void;
|
||||||
speakingParticipants: Participant[];
|
speakingParticipants: Participant[];
|
||||||
state: "connecting" | "connected" | "disconnected" | "error";
|
state: "connecting" | "connected" | "disconnected" | "error";
|
||||||
toggleTalking: () => void;
|
toggleTalking: () => void;
|
||||||
@@ -72,6 +75,10 @@ export const useAudioStore = create<TalkState>((set, get) => ({
|
|||||||
remoteParticipants: 0,
|
remoteParticipants: 0,
|
||||||
connectionQuality: ConnectionQuality.Unknown,
|
connectionQuality: ConnectionQuality.Unknown,
|
||||||
room: null,
|
room: null,
|
||||||
|
selectedRoom: ROOMS[0],
|
||||||
|
setSelectedRoom: (room) => {
|
||||||
|
set({ selectedRoom: room });
|
||||||
|
},
|
||||||
resetSpeakingParticipants: (source: string) => {
|
resetSpeakingParticipants: (source: string) => {
|
||||||
set({
|
set({
|
||||||
speakingParticipants: [],
|
speakingParticipants: [],
|
||||||
@@ -117,11 +124,11 @@ export const useAudioStore = create<TalkState>((set, get) => ({
|
|||||||
(oldSettings.micDeviceId !== newSettings.micDeviceId ||
|
(oldSettings.micDeviceId !== newSettings.micDeviceId ||
|
||||||
oldSettings.micVolume !== newSettings.micVolume)
|
oldSettings.micVolume !== newSettings.micVolume)
|
||||||
) {
|
) {
|
||||||
const { room, disconnect, connect } = get();
|
const { room, disconnect, connect, selectedRoom } = get();
|
||||||
const role = room?.localParticipant.attributes.role;
|
const role = room?.localParticipant.attributes.role;
|
||||||
if (room?.name || role) {
|
if (selectedRoom || role) {
|
||||||
disconnect();
|
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 }));
|
set((state) => ({ isTalking: !state.isTalking, transmitBlocked: false }));
|
||||||
},
|
},
|
||||||
connect: async (roomName, role) => {
|
connect: async (_room, role) => {
|
||||||
set({ state: "connecting" });
|
set({ state: "connecting" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -172,13 +179,16 @@ export const useAudioStore = create<TalkState>((set, get) => ({
|
|||||||
connectedRoom.removeAllListeners();
|
connectedRoom.removeAllListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { selectedRoom } = get();
|
||||||
|
|
||||||
const url = process.env.NEXT_PUBLIC_LIVEKIT_URL;
|
const url = process.env.NEXT_PUBLIC_LIVEKIT_URL;
|
||||||
if (!url) return console.error("NEXT_PUBLIC_LIVEKIT_URL not set");
|
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");
|
if (!token) throw new Error("Fehlende Berechtigung");
|
||||||
const room = new Room({});
|
const room = new Room({});
|
||||||
await room.prepareConnection(url, token);
|
await room.prepareConnection(url, token);
|
||||||
|
const roomConnectedSound = new Audio("/sounds/403.wav");
|
||||||
room
|
room
|
||||||
// Connection events
|
// Connection events
|
||||||
.on(RoomEvent.Connected, async () => {
|
.on(RoomEvent.Connected, async () => {
|
||||||
@@ -186,7 +196,7 @@ export const useAudioStore = create<TalkState>((set, get) => ({
|
|||||||
|
|
||||||
if (dispatchState.status === "connected" && dispatchState.connectedDispatcher?.id) {
|
if (dispatchState.status === "connected" && dispatchState.connectedDispatcher?.id) {
|
||||||
changeDispatcherAPI(dispatchState.connectedDispatcher?.id, {
|
changeDispatcherAPI(dispatchState.connectedDispatcher?.id, {
|
||||||
zone: roomName,
|
zone: _room?.name || selectedRoom?.name || "VAR_LST_RD_01",
|
||||||
ghostMode: dispatchState.ghostMode,
|
ghostMode: dispatchState.ghostMode,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -208,7 +218,7 @@ export const useAudioStore = create<TalkState>((set, get) => ({
|
|||||||
source: Track.Source.Microphone,
|
source: Track.Source.Microphone,
|
||||||
});
|
});
|
||||||
await publishedTrack.mute();
|
await publishedTrack.mute();
|
||||||
|
roomConnectedSound.play();
|
||||||
set({ localRadioTrack: publishedTrack });
|
set({ localRadioTrack: publishedTrack });
|
||||||
|
|
||||||
set({ state: "connected", room, isTalking: false, message: null });
|
set({ state: "connected", room, isTalking: false, message: null });
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const useDispatchConnectionStore = create<ConnectionStore>((set) => ({
|
|||||||
setHideDraftMissions: (hide) => set({ hideDraftMissions: hide }),
|
setHideDraftMissions: (hide) => set({ hideDraftMissions: hide }),
|
||||||
connectedDispatcher: null,
|
connectedDispatcher: null,
|
||||||
message: "",
|
message: "",
|
||||||
selectedZone: "LST_01",
|
selectedZone: "VAR_LST_RD_01",
|
||||||
logoffTime: "",
|
logoffTime: "",
|
||||||
ghostMode: false,
|
ghostMode: false,
|
||||||
connect: async (uid, selectedZone, logoffTime, ghostMode) =>
|
connect: async (uid, selectedZone, logoffTime, ghostMode) =>
|
||||||
@@ -48,7 +48,7 @@ export const useDispatchConnectionStore = create<ConnectionStore>((set) => ({
|
|||||||
|
|
||||||
dispatchSocket.on("connect", () => {
|
dispatchSocket.on("connect", () => {
|
||||||
const { logoffTime, selectedZone, ghostMode } = useDispatchConnectionStore.getState();
|
const { logoffTime, selectedZone, ghostMode } = useDispatchConnectionStore.getState();
|
||||||
useAudioStore.getState().connect("LST_01", selectedZone || "Leitstelle");
|
useAudioStore.getState().connect(undefined, selectedZone || "Leitstelle");
|
||||||
dispatchSocket.emit("connect-dispatch", {
|
dispatchSocket.emit("connect-dispatch", {
|
||||||
logoffTime,
|
logoffTime,
|
||||||
selectedZone,
|
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 { create } from "zustand";
|
||||||
import { syncTabs } from "zustand-sync-tabs";
|
|
||||||
|
|
||||||
interface SetSdsPageParams {
|
interface SetOffPageParams {
|
||||||
page: "sds";
|
page: "off";
|
||||||
station: Station;
|
}
|
||||||
sdsMessage: MissionSdsLog;
|
|
||||||
|
interface SetStartupPageParams {
|
||||||
|
page: "startup";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SetHomePageParams {
|
interface SetHomePageParams {
|
||||||
page: "home";
|
page: "home";
|
||||||
station: Station;
|
|
||||||
fmsStatus: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SetSendingStatusPageParams {
|
interface SetVoicecallPageParams {
|
||||||
page: "sending-status";
|
page: "voice-call";
|
||||||
station: Station;
|
}
|
||||||
|
interface SetSdsReceivedPopupParams {
|
||||||
|
popup: "sds-received";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SetNewStatusPageParams {
|
interface SetGroupSelectionPopupParams {
|
||||||
page: "new-status";
|
popup: "group-selection";
|
||||||
station: Station;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SetPageParams =
|
interface SetStatusSentPopupParams {
|
||||||
|
popup: "status-sent";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SetLoginPopupParams {
|
||||||
|
popup: "login";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SetSdsSentPopupParams {
|
||||||
|
popup: "sds-sent";
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SetPageParams =
|
||||||
| SetHomePageParams
|
| SetHomePageParams
|
||||||
| SetSendingStatusPageParams
|
| SetOffPageParams
|
||||||
| SetSdsPageParams
|
| SetStartupPageParams
|
||||||
| SetNewStatusPageParams;
|
| SetVoicecallPageParams;
|
||||||
|
|
||||||
|
export type SetPopupParams =
|
||||||
|
| SetStatusSentPopupParams
|
||||||
|
| SetSdsSentPopupParams
|
||||||
|
| SetGroupSelectionPopupParams
|
||||||
|
| SetSdsReceivedPopupParams
|
||||||
|
| SetLoginPopupParams;
|
||||||
|
|
||||||
|
interface StringifiedData {
|
||||||
|
sdsText?: string;
|
||||||
|
sentSdsText?: string;
|
||||||
|
|
||||||
|
groupSelectionGroupId?: string;
|
||||||
|
callTextHeader?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface MrtStore {
|
interface MrtStore {
|
||||||
page: SetPageParams["page"];
|
page: SetPageParams["page"];
|
||||||
|
popup?: SetPopupParams["popup"];
|
||||||
|
|
||||||
lines: DisplayLineProps[];
|
stringifiedData: StringifiedData;
|
||||||
|
setStringifiedData: (data: Partial<StringifiedData>) => void;
|
||||||
|
|
||||||
setPage: (pageData: SetPageParams) => 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>(
|
export const useMrtStore = create<MrtStore>((set) => ({
|
||||||
syncTabs(
|
page: "off",
|
||||||
(set) => ({
|
nightMode: false,
|
||||||
page: "home",
|
stringifiedData: {
|
||||||
pageData: {
|
groupSelectionGroupId: "2201",
|
||||||
message: "",
|
},
|
||||||
},
|
setNightMode: (nightMode) => set({ nightMode }),
|
||||||
lines: [
|
setStringifiedData: (data) =>
|
||||||
{
|
set((state) => ({
|
||||||
textLeft: "VAR.#",
|
stringifiedData: { ...state.stringifiedData, ...data },
|
||||||
textSize: "2",
|
})),
|
||||||
},
|
setPopup: (popupData) => {
|
||||||
{
|
set({ popup: popupData ? popupData.popup : undefined });
|
||||||
textLeft: "No Data",
|
},
|
||||||
textSize: "3",
|
setPage: (pageData) => {
|
||||||
},
|
set({ page: pageData.page });
|
||||||
],
|
},
|
||||||
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",
|
|
||||||
},
|
|
||||||
{ 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: "" });
|
usePilotConnectionStore.setState({ status: "connected", message: "" });
|
||||||
const { logoffTime, selectedStation, debug } = usePilotConnectionStore.getState();
|
const { logoffTime, selectedStation, debug } = usePilotConnectionStore.getState();
|
||||||
dispatchSocket.disconnect();
|
dispatchSocket.disconnect();
|
||||||
useAudioStore.getState().connect("LST_01", selectedStation?.bosCallsignShort || "pilot");
|
useAudioStore.getState().connect(undefined, selectedStation?.bosCallsignShort || "pilot");
|
||||||
|
|
||||||
pilotSocket.emit("connect-pilot", {
|
pilotSocket.emit("connect-pilot", {
|
||||||
logoffTime,
|
logoffTime,
|
||||||
@@ -109,7 +109,7 @@ pilotSocket.on("connect-message", (data) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
pilotSocket.on("disconnect", () => {
|
pilotSocket.on("disconnect", () => {
|
||||||
usePilotConnectionStore.setState({ status: "disconnected" });
|
usePilotConnectionStore.setState({ status: "disconnected", connectedAircraft: null });
|
||||||
useAudioStore.getState().disconnect();
|
useAudioStore.getState().disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -142,11 +142,13 @@ pilotSocket.on("mission-alert", (data: Mission & { Stations: Station[] }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
pilotSocket.on("sds-message", (sdsMessage: MissionSdsLog) => {
|
pilotSocket.on("sds-message", (sdsMessage: MissionSdsLog) => {
|
||||||
|
console.log("Received sds-message via socket:", sdsMessage);
|
||||||
const station = usePilotConnectionStore.getState().selectedStation;
|
const station = usePilotConnectionStore.getState().selectedStation;
|
||||||
if (!station) return;
|
if (!station) return;
|
||||||
useMrtStore.getState().setPage({
|
useMrtStore.getState().setPopup({
|
||||||
page: "sds",
|
popup: "sds-received",
|
||||||
station,
|
});
|
||||||
sdsMessage,
|
useMrtStore.getState().setStringifiedData({
|
||||||
|
sdsText: sdsMessage.data.message,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Mission, Station, User } from "@repo/db";
|
import { Mission, Station, User } from "@repo/db";
|
||||||
import { DisplayLineProps } from "(app)/pilot/_components/dme/Dme";
|
import { DisplayLineProps } from "(app)/pilot/_components/dme/Dme";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { syncTabs } from "zustand-sync-tabs";
|
|
||||||
|
|
||||||
interface SetHomePageParams {
|
interface SetHomePageParams {
|
||||||
page: "home";
|
page: "home";
|
||||||
@@ -45,197 +44,190 @@ interface MrtStore {
|
|||||||
|
|
||||||
let interval: NodeJS.Timeout | null = null;
|
let interval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
export const useDmeStore = create<MrtStore>(
|
export const useDmeStore = create<MrtStore>((set) => ({
|
||||||
syncTabs(
|
page: "home",
|
||||||
(set) => ({
|
pageData: {
|
||||||
page: "home",
|
message: "",
|
||||||
pageData: {
|
},
|
||||||
message: "",
|
lines: [
|
||||||
},
|
|
||||||
lines: [
|
|
||||||
{
|
|
||||||
textLeft: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
textMid: "VAR . DME# No Data",
|
|
||||||
textSize: "2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
textLeft: "",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
setLines: (lines) => set({ lines }),
|
|
||||||
latestMission: null,
|
|
||||||
setPage: (pageData) => {
|
|
||||||
if (interval) clearInterval(interval);
|
|
||||||
switch (pageData.page) {
|
|
||||||
case "home": {
|
|
||||||
const setHomePage = () =>
|
|
||||||
set({
|
|
||||||
page: "home",
|
|
||||||
lines: [
|
|
||||||
{
|
|
||||||
textMid: pageData.station.bosCallsign
|
|
||||||
? `${pageData.station.bosCallsign}`
|
|
||||||
: "no Data",
|
|
||||||
style: { fontWeight: "bold" },
|
|
||||||
},
|
|
||||||
{ textMid: "⠀" },
|
|
||||||
{
|
|
||||||
textMid: new Date().toLocaleDateString("de-DE", {
|
|
||||||
year: "numeric",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
textMid: new Date().toLocaleTimeString(),
|
|
||||||
style: { fontWeight: "bold" },
|
|
||||||
},
|
|
||||||
{ textMid: "⠀" },
|
|
||||||
{
|
|
||||||
textMid: `${pageData.user.lastname} ${pageData.user.firstname}`,
|
|
||||||
},
|
|
||||||
{ textMid: "⠀" },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
setHomePage();
|
|
||||||
|
|
||||||
interval = setInterval(() => {
|
|
||||||
setHomePage();
|
|
||||||
}, 1000);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "new-mission": {
|
|
||||||
set({
|
|
||||||
page: "new-mission",
|
|
||||||
lines: [
|
|
||||||
{ textMid: "⠀" },
|
|
||||||
{
|
|
||||||
textMid: "new mission received",
|
|
||||||
style: { fontWeight: "bold" },
|
|
||||||
},
|
|
||||||
{ textMid: "⠀" },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "mission": {
|
|
||||||
set({
|
|
||||||
latestMission: pageData.mission,
|
|
||||||
page: "mission",
|
|
||||||
lines: [
|
|
||||||
{
|
|
||||||
textLeft: `${pageData.mission.missionKeywordAbbreviation}`,
|
|
||||||
textRight: pageData.mission.Stations.map((s) => s.bosCallsignShort).join(","),
|
|
||||||
style: { fontWeight: "bold" },
|
|
||||||
},
|
|
||||||
...(pageData.mission.type == "primär"
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
textMid: `${pageData.mission.missionKeywordName}`,
|
|
||||||
style: { fontWeight: "bold" },
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
|
|
||||||
{ textLeft: `${pageData.mission.addressStreet}` },
|
|
||||||
{
|
|
||||||
textLeft: `${pageData.mission.addressZip} ${pageData.mission.addressCity}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
textMid: "Weitere Standortinformationen:",
|
|
||||||
style: { fontWeight: "bold" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
textLeft: pageData.mission.addressAdditionalInfo || "keine Daten",
|
|
||||||
},
|
|
||||||
...(pageData.mission.type === "sekundär"
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
textMid: "Zielort:",
|
|
||||||
style: { fontWeight: "bold" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
textLeft: pageData.mission.addressMissionDestination || "keine Daten",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...(pageData.mission.missionPatientInfo &&
|
|
||||||
pageData.mission.missionPatientInfo.length > 0
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
textMid: "Patienteninfos:",
|
|
||||||
style: { fontWeight: "bold" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
textLeft: pageData.mission.missionPatientInfo,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...(pageData.mission.missionAdditionalInfo &&
|
|
||||||
pageData.mission.missionAdditionalInfo.length > 0
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
textMid: "Weitere Infos:",
|
|
||||||
style: { fontWeight: "bold" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
textLeft: pageData.mission.missionAdditionalInfo,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "error": {
|
|
||||||
set({
|
|
||||||
page: "error",
|
|
||||||
lines: [
|
|
||||||
{ textMid: "Fehler:" },
|
|
||||||
{
|
|
||||||
textMid: pageData.error,
|
|
||||||
style: { fontWeight: "bold" },
|
|
||||||
},
|
|
||||||
{ textMid: "⠀" },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "acknowledge": {
|
|
||||||
set({
|
|
||||||
page: "acknowledge",
|
|
||||||
lines: [
|
|
||||||
{ textMid: "⠀" },
|
|
||||||
{
|
|
||||||
textMid: "Einsatz angenommen",
|
|
||||||
style: { fontWeight: "bold" },
|
|
||||||
},
|
|
||||||
{ textMid: "⠀" },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
set({
|
|
||||||
page: "error",
|
|
||||||
lines: [
|
|
||||||
{ textMid: "Fehler:" },
|
|
||||||
{
|
|
||||||
textMid: `Unbekannte Seite`,
|
|
||||||
style: { fontWeight: "bold" },
|
|
||||||
},
|
|
||||||
{ textMid: "⠀" },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
{
|
||||||
name: "dme-store", // unique name
|
textLeft: "",
|
||||||
},
|
},
|
||||||
),
|
{
|
||||||
);
|
textMid: "VAR . DME# No Data",
|
||||||
|
textSize: "2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
textLeft: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
setLines: (lines) => set({ lines }),
|
||||||
|
latestMission: null,
|
||||||
|
setPage: (pageData) => {
|
||||||
|
if (interval) clearInterval(interval);
|
||||||
|
switch (pageData.page) {
|
||||||
|
case "home": {
|
||||||
|
const setHomePage = () =>
|
||||||
|
set({
|
||||||
|
page: "home",
|
||||||
|
lines: [
|
||||||
|
{
|
||||||
|
textMid: pageData.station.bosCallsign
|
||||||
|
? `${pageData.station.bosCallsign}`
|
||||||
|
: "no Data",
|
||||||
|
style: { fontWeight: "bold" },
|
||||||
|
},
|
||||||
|
{ textMid: "⠀" },
|
||||||
|
{
|
||||||
|
textMid: new Date().toLocaleDateString("de-DE", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
textMid: new Date().toLocaleTimeString(),
|
||||||
|
style: { fontWeight: "bold" },
|
||||||
|
},
|
||||||
|
{ textMid: "⠀" },
|
||||||
|
{
|
||||||
|
textMid: `${pageData.user.lastname} ${pageData.user.firstname}`,
|
||||||
|
},
|
||||||
|
{ textMid: "⠀" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
setHomePage();
|
||||||
|
|
||||||
|
interval = setInterval(() => {
|
||||||
|
setHomePage();
|
||||||
|
}, 1000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "new-mission": {
|
||||||
|
set({
|
||||||
|
page: "new-mission",
|
||||||
|
lines: [
|
||||||
|
{ textMid: "⠀" },
|
||||||
|
{
|
||||||
|
textMid: "new mission received",
|
||||||
|
style: { fontWeight: "bold" },
|
||||||
|
},
|
||||||
|
{ textMid: "⠀" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "mission": {
|
||||||
|
set({
|
||||||
|
latestMission: pageData.mission,
|
||||||
|
page: "mission",
|
||||||
|
lines: [
|
||||||
|
{
|
||||||
|
textLeft: `${pageData.mission.missionKeywordAbbreviation}`,
|
||||||
|
textRight: pageData.mission.Stations.map((s) => s.bosCallsignShort).join(","),
|
||||||
|
style: { fontWeight: "bold" },
|
||||||
|
},
|
||||||
|
...(pageData.mission.type == "primär"
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
textMid: `${pageData.mission.missionKeywordName}`,
|
||||||
|
style: { fontWeight: "bold" },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
|
||||||
|
{ textLeft: `${pageData.mission.addressStreet}` },
|
||||||
|
{
|
||||||
|
textLeft: `${pageData.mission.addressZip} ${pageData.mission.addressCity}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
textMid: "Weitere Standortinformationen:",
|
||||||
|
style: { fontWeight: "bold" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
textLeft: pageData.mission.addressAdditionalInfo || "keine Daten",
|
||||||
|
},
|
||||||
|
...(pageData.mission.type === "sekundär"
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
textMid: "Zielort:",
|
||||||
|
style: { fontWeight: "bold" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
textLeft: pageData.mission.addressMissionDestination || "keine Daten",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(pageData.mission.missionPatientInfo &&
|
||||||
|
pageData.mission.missionPatientInfo.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
textMid: "Patienteninfos:",
|
||||||
|
style: { fontWeight: "bold" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
textLeft: pageData.mission.missionPatientInfo,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(pageData.mission.missionAdditionalInfo &&
|
||||||
|
pageData.mission.missionAdditionalInfo.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
textMid: "Weitere Infos:",
|
||||||
|
style: { fontWeight: "bold" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
textLeft: pageData.mission.missionAdditionalInfo,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "error": {
|
||||||
|
set({
|
||||||
|
page: "error",
|
||||||
|
lines: [
|
||||||
|
{ textMid: "Fehler:" },
|
||||||
|
{
|
||||||
|
textMid: pageData.error,
|
||||||
|
style: { fontWeight: "bold" },
|
||||||
|
},
|
||||||
|
{ textMid: "⠀" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "acknowledge": {
|
||||||
|
set({
|
||||||
|
page: "acknowledge",
|
||||||
|
lines: [
|
||||||
|
{ textMid: "⠀" },
|
||||||
|
{
|
||||||
|
textMid: "Einsatz angenommen",
|
||||||
|
style: { fontWeight: "bold" },
|
||||||
|
},
|
||||||
|
{ textMid: "⠀" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
set({
|
||||||
|
page: "error",
|
||||||
|
lines: [
|
||||||
|
{ textMid: "Fehler:" },
|
||||||
|
{
|
||||||
|
textMid: `Unbekannte Seite`,
|
||||||
|
style: { fontWeight: "bold" },
|
||||||
|
},
|
||||||
|
{ textMid: "⠀" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|||||||
@@ -9,6 +9,11 @@
|
|||||||
src: url("/fonts/MelderV2.ttf") format("truetype"); /* Chrome 4+, Firefox 3.5, Opera 10+, Safari 3—5 */
|
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 {
|
@theme {
|
||||||
--color-rescuetrack: #46b7a3;
|
--color-rescuetrack: #46b7a3;
|
||||||
--color-rescuetrack-highlight: #ff4500;
|
--color-rescuetrack-highlight: #ff4500;
|
||||||
|
|||||||
1
apps/dispatch/next-env.d.ts
vendored
@@ -1,5 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {};
|
const nextConfig = {
|
||||||
|
output: "standalone",
|
||||||
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -60,7 +60,6 @@
|
|||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"zod": "^3.25.67",
|
"zod": "^3.25.67",
|
||||||
"zustand": "^5.0.6",
|
"zustand": "^5.0.6"
|
||||||
"zustand-sync-tabs": "^0.2.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
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_URL
|
||||||
ARG NEXT_PUBLIC_HUB_SERVER_URL
|
ARG NEXT_PUBLIC_HUB_SERVER_URL
|
||||||
ARG NEXT_PUBLIC_DISCORD_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_MOODLE_URL=${NEXT_PUBLIC_MOODLE_URL}
|
||||||
ENV NEXT_PUBLIC_DISPATCH_URL=${NEXT_PUBLIC_DISPATCH_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
|
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 update
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
|
|
||||||
@@ -33,6 +29,13 @@ COPY . .
|
|||||||
RUN turbo prune hub --docker
|
RUN turbo prune hub --docker
|
||||||
|
|
||||||
FROM base AS installer
|
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 update
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
|
|
||||||
@@ -44,21 +47,24 @@ RUN pnpm install
|
|||||||
# Build the project
|
# Build the project
|
||||||
COPY --from=builder /usr/app/out/full/ .
|
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
|
WORKDIR /usr/app
|
||||||
|
|
||||||
# Don't run production as root
|
# Don't run production as root
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
RUN adduser --system --uid 1001 nextjs
|
RUN adduser --system --uid 1001 nextjs
|
||||||
USER nextjs
|
|
||||||
|
|
||||||
# Automatically leverage output traces to reduce image size
|
# Automatically leverage output traces to reduce image size
|
||||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
# 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 the application port
|
||||||
EXPOSE 3000
|
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
|
showSearch
|
||||||
getFilter={(searchTerm) =>
|
getFilter={(searchTerm) =>
|
||||||
({
|
({
|
||||||
|
AND: [{ eventId: event?.id }],
|
||||||
OR: [
|
OR: [
|
||||||
{
|
{
|
||||||
User: {
|
User: {
|
||||||
|
|||||||
@@ -776,13 +776,19 @@ export const AdminForm = ({
|
|||||||
{discordAccount && (
|
{discordAccount && (
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<Image
|
{discordAccount.avatar ? (
|
||||||
src={`https://cdn.discordapp.com/avatars/${discordAccount.discordId}/${discordAccount.avatar}.png`}
|
<Image
|
||||||
alt="Discord Avatar"
|
src={`https://cdn.discordapp.com/avatars/${discordAccount.discordId}/${discordAccount.avatar}.png`}
|
||||||
width={40}
|
alt="Discord Avatar"
|
||||||
height={40}
|
width={40}
|
||||||
className="h-10 w-10 rounded-full"
|
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>
|
||||||
<td>{discordAccount.username}</td>
|
<td>{discordAccount.username}</td>
|
||||||
<td>{discordAccount.discordId}</td>
|
<td>{discordAccount.discordId}</td>
|
||||||
@@ -792,15 +798,20 @@ export const AdminForm = ({
|
|||||||
{formerDiscordAccounts.map((account) => (
|
{formerDiscordAccounts.map((account) => (
|
||||||
<tr key={account.discordId}>
|
<tr key={account.discordId}>
|
||||||
<td>
|
<td>
|
||||||
{account.DiscordAccount && (
|
{account.DiscordAccount &&
|
||||||
<Image
|
(account.DiscordAccount.avatar ? (
|
||||||
src={`https://cdn.discordapp.com/avatars/${account.DiscordAccount.discordId}/${account.DiscordAccount.avatar}.png`}
|
<Image
|
||||||
alt="Discord Avatar"
|
src={`https://cdn.discordapp.com/avatars/${account.DiscordAccount.discordId}/${account.DiscordAccount.avatar}.png`}
|
||||||
width={40}
|
alt="Discord Avatar"
|
||||||
height={40}
|
width={40}
|
||||||
className="h-10 w-10 rounded-full"
|
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>
|
||||||
<td>{account.DiscordAccount?.username || "Unbekannt"}</td>
|
<td>{account.DiscordAccount?.username || "Unbekannt"}</td>
|
||||||
<td>{account.DiscordAccount?.discordId || "Unbekannt"}</td>
|
<td>{account.DiscordAccount?.discordId || "Unbekannt"}</td>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
/* const removeImports = require("next-remove-imports")(); */
|
/* const removeImports = require("next-remove-imports")(); */
|
||||||
/* const nextConfig = removeImports({}); */
|
/* const nextConfig = removeImports({}); */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
|
output: "standalone",
|
||||||
images: {
|
images: {
|
||||||
domains: ["cdn.discordapp.com", "nextcloud.virtualairrescue.com"],
|
domains: ["cdn.discordapp.com", "nextcloud.virtualairrescue.com"],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"node": ">=18",
|
"node": ">=18",
|
||||||
"pnpm": ">=10"
|
"pnpm": ">=10"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.13.1",
|
"packageManager": "pnpm@10.28.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Station } from "../../generated/client";
|
import { Station } from "../../generated/client";
|
||||||
|
import { StationStatus } from "./SocketEvents";
|
||||||
import { PublicUser } from "./User";
|
import { PublicUser } from "./User";
|
||||||
|
|
||||||
export interface MissionVehicleLog {
|
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 {
|
export interface MissionMessageLog {
|
||||||
type: "message-log";
|
type: "message-log";
|
||||||
auto: false;
|
auto: false;
|
||||||
@@ -90,4 +104,5 @@ export type MissionLog =
|
|||||||
| MissionAlertLogAuto
|
| MissionAlertLogAuto
|
||||||
| MissionCompletedLog
|
| MissionCompletedLog
|
||||||
| MissionVehicleLog
|
| 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 <=15.4.4: '>=15.4.5'
|
||||||
next@>=15.0.0-canary.0 <15.4.7: '>=15.4.7'
|
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.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.7: '>=7.0.7'
|
||||||
nodemailer@<=7.0.10: '>=7.0.11'
|
nodemailer@<=7.0.10: '>=7.0.11'
|
||||||
playwright@<1.55.1: '>=1.55.1'
|
playwright@<1.55.1: '>=1.55.1'
|
||||||
|
preact@>=10.26.5 <10.26.10: '>=10.26.10'
|
||||||
|
qs@<6.14.1: '>=6.14.1'
|
||||||
|
|||||||