v2.0.2 #119
@@ -14,113 +14,118 @@ export const sendAlert = async (
|
|||||||
connectedAircrafts: ConnectedAircraft[];
|
connectedAircrafts: ConnectedAircraft[];
|
||||||
mission: Mission;
|
mission: Mission;
|
||||||
}> => {
|
}> => {
|
||||||
const mission = await prisma.mission.findUnique({
|
try {
|
||||||
where: { id: id },
|
const mission = await prisma.mission.findUnique({
|
||||||
});
|
where: { id: id },
|
||||||
const Stations = await prisma.station.findMany({
|
|
||||||
where: {
|
|
||||||
id: {
|
|
||||||
in: mission?.missionStationIds,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!mission) {
|
|
||||||
throw new Error("Mission not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
// connectedAircrafts the alert is sent to
|
|
||||||
const connectedAircrafts = await prisma.connectedAircraft.findMany({
|
|
||||||
where: {
|
|
||||||
stationId: stationId
|
|
||||||
? stationId
|
|
||||||
: {
|
|
||||||
in: mission.missionStationIds,
|
|
||||||
},
|
|
||||||
logoutTime: null,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Station: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const aircraft of connectedAircrafts) {
|
|
||||||
io.to(`station:${aircraft.stationId}`).emit("mission-alert", {
|
|
||||||
...mission,
|
|
||||||
Stations,
|
|
||||||
});
|
});
|
||||||
io.to(`desktop:${aircraft.userId}`).emit("mission-alert", {
|
const Stations = await prisma.station.findMany({
|
||||||
missionId: mission.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: { id: aircraft.userId },
|
|
||||||
});
|
|
||||||
if (!user) continue;
|
|
||||||
if (user.settingsNtfyRoom) {
|
|
||||||
await sendNtfyMission(mission, Stations, aircraft.Station, user.settingsNtfyRoom);
|
|
||||||
}
|
|
||||||
const existingMissionOnStationUser = await prisma.missionOnStationUsers.findFirst({
|
|
||||||
where: {
|
where: {
|
||||||
missionId: mission.id,
|
id: {
|
||||||
userId: aircraft.userId,
|
in: mission?.missionStationIds,
|
||||||
stationId: aircraft.stationId,
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existingMissionOnStationUser)
|
if (!mission) {
|
||||||
await prisma.missionOnStationUsers.create({
|
throw new Error("Mission not found");
|
||||||
data: {
|
}
|
||||||
|
|
||||||
|
// connectedAircrafts the alert is sent to
|
||||||
|
const connectedAircrafts = await prisma.connectedAircraft.findMany({
|
||||||
|
where: {
|
||||||
|
stationId: stationId
|
||||||
|
? stationId
|
||||||
|
: {
|
||||||
|
in: mission.missionStationIds,
|
||||||
|
},
|
||||||
|
logoutTime: null,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
Station: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const aircraft of connectedAircrafts) {
|
||||||
|
io.to(`station:${aircraft.stationId}`).emit("mission-alert", {
|
||||||
|
...mission,
|
||||||
|
Stations,
|
||||||
|
});
|
||||||
|
io.to(`desktop:${aircraft.userId}`).emit("mission-alert", {
|
||||||
|
missionId: mission.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: aircraft.userId },
|
||||||
|
});
|
||||||
|
if (!user) continue;
|
||||||
|
if (user.settingsNtfyRoom) {
|
||||||
|
await sendNtfyMission(mission, Stations, aircraft.Station, user.settingsNtfyRoom);
|
||||||
|
}
|
||||||
|
const existingMissionOnStationUser = await prisma.missionOnStationUsers.findFirst({
|
||||||
|
where: {
|
||||||
missionId: mission.id,
|
missionId: mission.id,
|
||||||
userId: aircraft.userId,
|
userId: aircraft.userId,
|
||||||
stationId: aircraft.stationId,
|
stationId: aircraft.stationId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// for statistics only
|
if (!existingMissionOnStationUser)
|
||||||
await prisma.missionsOnStations
|
await prisma.missionOnStationUsers.create({
|
||||||
.createMany({
|
data: {
|
||||||
data: mission.missionStationIds.map((stationId) => ({
|
missionId: mission.id,
|
||||||
missionId: mission.id,
|
userId: aircraft.userId,
|
||||||
stationId,
|
stationId: aircraft.stationId,
|
||||||
})),
|
},
|
||||||
})
|
});
|
||||||
.catch((err) => {
|
}
|
||||||
// Ignore if the entry already exists
|
|
||||||
});
|
// for statistics only
|
||||||
if (user === "HPG") {
|
await prisma.missionsOnStations
|
||||||
await prisma.mission.update({
|
.createMany({
|
||||||
where: { id: Number(id) },
|
data: mission.missionStationIds.map((stationId) => ({
|
||||||
data: {
|
missionId: mission.id,
|
||||||
state: "running",
|
stationId,
|
||||||
missionLog: {
|
})),
|
||||||
push: {
|
})
|
||||||
type: "alert-log",
|
.catch((err) => {
|
||||||
auto: true,
|
// Ignore if the entry already exists
|
||||||
timeStamp: new Date().toISOString(),
|
});
|
||||||
} as any,
|
if (user === "HPG") {
|
||||||
|
await prisma.mission.update({
|
||||||
|
where: { id: Number(id) },
|
||||||
|
data: {
|
||||||
|
state: "running",
|
||||||
|
missionLog: {
|
||||||
|
push: {
|
||||||
|
type: "alert-log",
|
||||||
|
auto: true,
|
||||||
|
timeStamp: new Date().toISOString(),
|
||||||
|
} as any,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
} else {
|
||||||
} else {
|
await prisma.mission.update({
|
||||||
await prisma.mission.update({
|
where: { id: Number(id) },
|
||||||
where: { id: Number(id) },
|
data: {
|
||||||
data: {
|
state: "running",
|
||||||
state: "running",
|
missionLog: {
|
||||||
missionLog: {
|
push: {
|
||||||
push: {
|
type: "alert-log",
|
||||||
type: "alert-log",
|
auto: false,
|
||||||
auto: false,
|
timeStamp: new Date().toISOString(),
|
||||||
timeStamp: new Date().toISOString(),
|
data: {
|
||||||
data: {
|
stationId: stationId,
|
||||||
stationId: stationId,
|
user: getPublicUser(user, { ignorePrivacy: true }),
|
||||||
user: getPublicUser(user, { ignorePrivacy: true }),
|
},
|
||||||
},
|
} as any,
|
||||||
} as any,
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
}
|
||||||
|
return { connectedAircrafts, mission };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending mission alert:", error);
|
||||||
|
throw new Error("Ein Fehler ist aufgetreten. Bitte melde den Fehler als Bug");
|
||||||
}
|
}
|
||||||
return { connectedAircrafts, mission };
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -50,10 +50,7 @@ const getRthCallsigns = (mission: Mission, stations: Station[]) => {
|
|||||||
return `🚁 RTH${callsigns.length > 1 ? "s" : ""}: ${callsigns.join(" / ")} `;
|
return `🚁 RTH${callsigns.length > 1 ? "s" : ""}: ${callsigns.join(" / ")} `;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNtfyHeader = (
|
const getNtfyHeader = (mission: Mission, clientStation: Station): NtfyHeader => ({
|
||||||
mission: Mission,
|
|
||||||
clientStation: Station,
|
|
||||||
): NtfyHeader => ({
|
|
||||||
headers: {
|
headers: {
|
||||||
Title: `${clientStation.bosCallsignShort} / ${mission.missionKeywordAbbreviation} / ${mission.missionKeywordCategory}`,
|
Title: `${clientStation.bosCallsignShort} / ${mission.missionKeywordAbbreviation} / ${mission.missionKeywordCategory}`,
|
||||||
Tags: "pager",
|
Tags: "pager",
|
||||||
@@ -76,9 +73,13 @@ export const sendNtfyMission = async (
|
|||||||
clientStation: Station,
|
clientStation: Station,
|
||||||
ntfyRoom: string,
|
ntfyRoom: string,
|
||||||
) => {
|
) => {
|
||||||
axios.post(
|
try {
|
||||||
`https://ntfy.sh/${ntfyRoom}`,
|
await axios.post(
|
||||||
getNtfyData(mission, stations),
|
`https://ntfy.sh/${ntfyRoom}`,
|
||||||
getNtfyHeader(mission, clientStation),
|
getNtfyData(mission, stations),
|
||||||
);
|
getNtfyHeader(mission, clientStation),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending Ntfy mission:", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ router.patch("/:id", async (req, res) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (discordAccount?.id) {
|
if (discordAccount?.id && !disaptcherUpdate.ghostMode) {
|
||||||
await renameMember(
|
await renameMember(
|
||||||
discordAccount.discordId.toString(),
|
discordAccount.discordId.toString(),
|
||||||
`${getPublicUser(newDispatcher.user).fullName} • ${newDispatcher.zone}`,
|
`${getPublicUser(newDispatcher.user).fullName} • ${newDispatcher.zone}`,
|
||||||
|
|||||||
@@ -189,7 +189,11 @@ router.post("/:id/send-alert", async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
res.status(500).json({ error: "Failed to send mission" });
|
res
|
||||||
|
.status(500)
|
||||||
|
.json({
|
||||||
|
error: `Ein Fehler ist aufgetreten. Bitte melde den Fehler als Bug (${(error as Error).message})`,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export const handleConnectDispatch =
|
|||||||
|
|
||||||
io.to("dispatchers").emit("dispatchers-update");
|
io.to("dispatchers").emit("dispatchers-update");
|
||||||
io.to("pilots").emit("dispatchers-update");
|
io.to("pilots").emit("dispatchers-update");
|
||||||
if (discordAccount?.id && !ghostMode) {
|
if (discordAccount?.id) {
|
||||||
await renameMember(
|
await renameMember(
|
||||||
discordAccount.discordId.toString(),
|
discordAccount.discordId.toString(),
|
||||||
`${getPublicUser(user).fullName} - ${user.publicId}`,
|
`${getPublicUser(user).fullName} - ${user.publicId}`,
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ ARG NEXT_PUBLIC_HUB_URL
|
|||||||
ARG NEXT_PUBLIC_DISPATCH_SERVICE_ID
|
ARG NEXT_PUBLIC_DISPATCH_SERVICE_ID
|
||||||
ARG NEXT_PUBLIC_LIVEKIT_URL
|
ARG NEXT_PUBLIC_LIVEKIT_URL
|
||||||
ARG NEXT_PUBLIC_DISCORD_URL
|
ARG NEXT_PUBLIC_DISCORD_URL
|
||||||
|
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
|
||||||
ENV NEXT_PUBLIC_HUB_URL=$NEXT_PUBLIC_HUB_URL
|
ENV NEXT_PUBLIC_HUB_URL=$NEXT_PUBLIC_HUB_URL
|
||||||
ENV NEXT_PUBLIC_DISPATCH_SERVICE_ID=$NEXT_PUBLIC_DISPATCH_SERVICE_ID
|
ENV NEXT_PUBLIC_DISPATCH_SERVICE_ID=$NEXT_PUBLIC_DISPATCH_SERVICE_ID
|
||||||
ENV NEXT_PUBLIC_LIVEKIT_URL=$NEXT_PUBLIC_LIVEKIT_URL
|
ENV NEXT_PUBLIC_LIVEKIT_URL=$NEXT_PUBLIC_LIVEKIT_URL
|
||||||
|
ENV NEXT_PUBLIC_OPENAIP_ACCESS=$NEXT_PUBLIC_OPENAIP_ACCESS
|
||||||
ENV NEXT_PUBLIC_DISCORD_URL=$NEXT_PUBLIC_DISCORD_URL
|
ENV NEXT_PUBLIC_DISCORD_URL=$NEXT_PUBLIC_DISCORD_URL
|
||||||
|
|
||||||
ENV PNPM_HOME="/usr/local/pnpm"
|
ENV PNPM_HOME="/usr/local/pnpm"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { useSounds } from "_components/Audio/useSounds";
|
|||||||
export const Audio = () => {
|
export const Audio = () => {
|
||||||
const {
|
const {
|
||||||
speakingParticipants,
|
speakingParticipants,
|
||||||
|
resetSpeakingParticipants,
|
||||||
isTalking,
|
isTalking,
|
||||||
toggleTalking,
|
toggleTalking,
|
||||||
transmitBlocked,
|
transmitBlocked,
|
||||||
@@ -104,7 +105,7 @@ export const Audio = () => {
|
|||||||
data-tip="Nachricht entfernen"
|
data-tip="Nachricht entfernen"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className={cn("btn btn-sm btn-ghost border-warning bg-transparent ")}
|
className={cn("btn btn-sm btn-ghost border-warning bg-transparent")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
removeMessage();
|
removeMessage();
|
||||||
}}
|
}}
|
||||||
@@ -123,9 +124,9 @@ export const Audio = () => {
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
"btn btn-sm btn-soft bg-transparent border",
|
"btn btn-sm btn-soft border bg-transparent",
|
||||||
canStopOtherSpeakers && speakingParticipants.length > 0 && "hover:bg-error",
|
canStopOtherSpeakers && speakingParticipants.length > 0 && "hover:bg-error",
|
||||||
speakingParticipants.length > 0 && " hover:bg-errorborder",
|
speakingParticipants.length > 0 && "hover:bg-errorborder",
|
||||||
isReceivingBlick && "border-warning",
|
isReceivingBlick && "border-warning",
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -133,6 +134,7 @@ export const Audio = () => {
|
|||||||
const payload = JSON.stringify({
|
const payload = JSON.stringify({
|
||||||
by: role,
|
by: role,
|
||||||
});
|
});
|
||||||
|
resetSpeakingParticipants("dich");
|
||||||
speakingParticipants.forEach(async (p) => {
|
speakingParticipants.forEach(async (p) => {
|
||||||
await room?.localParticipant.performRpc({
|
await room?.localParticipant.performRpc({
|
||||||
destinationIdentity: p.identity,
|
destinationIdentity: p.identity,
|
||||||
@@ -159,24 +161,24 @@ export const Audio = () => {
|
|||||||
transmitBlocked && "bg-yellow-500 hover:bg-yellow-500",
|
transmitBlocked && "bg-yellow-500 hover:bg-yellow-500",
|
||||||
state === "disconnected" && "bg-red-500 hover:bg-red-500",
|
state === "disconnected" && "bg-red-500 hover:bg-red-500",
|
||||||
state === "error" && "bg-red-500 hover:bg-red-500",
|
state === "error" && "bg-red-500 hover:bg-red-500",
|
||||||
state === "connecting" && "bg-yellow-500 hover:bg-yellow-500 cursor-default",
|
state === "connecting" && "cursor-default bg-yellow-500 hover:bg-yellow-500",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{state === "connected" && <Mic className="w-5 h-5" />}
|
{state === "connected" && <Mic className="h-5 w-5" />}
|
||||||
{state === "disconnected" && <WifiOff className="w-5 h-5" />}
|
{state === "disconnected" && <WifiOff className="h-5 w-5" />}
|
||||||
{state === "connecting" && <PlugZap className="w-5 h-5" />}
|
{state === "connecting" && <PlugZap className="h-5 w-5" />}
|
||||||
{state === "error" && <ServerCrash className="w-5 h-5" />}
|
{state === "error" && <ServerCrash className="h-5 w-5" />}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{state === "connected" && (
|
{state === "connected" && (
|
||||||
<details className="dropdown relative z-[1050]">
|
<details className="dropdown relative z-[1050]">
|
||||||
<summary className="dropdown btn btn-ghost flex items-center gap-1">
|
<summary className="dropdown btn btn-ghost flex items-center gap-1">
|
||||||
{connectionQuality === ConnectionQuality.Excellent && <Signal className="w-5 h-5" />}
|
{connectionQuality === ConnectionQuality.Excellent && <Signal className="h-5 w-5" />}
|
||||||
{connectionQuality === ConnectionQuality.Good && <SignalMedium className="w-5 h-5" />}
|
{connectionQuality === ConnectionQuality.Good && <SignalMedium className="h-5 w-5" />}
|
||||||
{connectionQuality === ConnectionQuality.Poor && <SignalLow className="w-5 h-5" />}
|
{connectionQuality === ConnectionQuality.Poor && <SignalLow className="h-5 w-5" />}
|
||||||
{connectionQuality === ConnectionQuality.Lost && <ZapOff className="w-5 h-5" />}
|
{connectionQuality === ConnectionQuality.Lost && <ZapOff className="h-5 w-5" />}
|
||||||
{connectionQuality === ConnectionQuality.Unknown && (
|
{connectionQuality === ConnectionQuality.Unknown && (
|
||||||
<ShieldQuestion className="w-5 h-5" />
|
<ShieldQuestion className="h-5 w-5" />
|
||||||
)}
|
)}
|
||||||
<div className="badge badge-sm badge-soft badge-success">{remoteParticipants}</div>
|
<div className="badge badge-sm badge-soft badge-success">{remoteParticipants}</div>
|
||||||
</summary>
|
</summary>
|
||||||
@@ -184,7 +186,7 @@ export const Audio = () => {
|
|||||||
{ROOMS.map((r) => (
|
{ROOMS.map((r) => (
|
||||||
<li key={r}>
|
<li key={r}>
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-ghost text-left flex items-center justify-start gap-2 relative"
|
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 === r) return;
|
||||||
@@ -193,7 +195,7 @@ export const Audio = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{room?.name === r && (
|
{room?.name === r && (
|
||||||
<Disc className="text-success text-sm absolute left-2" 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}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -201,12 +203,12 @@ export const Audio = () => {
|
|||||||
))}
|
))}
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-ghost text-left flex items-center justify-start gap-2 relative"
|
className="btn btn-sm btn-ghost relative flex items-center justify-start gap-2 text-left"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
disconnect();
|
disconnect();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<WifiOff className="text-error text-sm absolute left-2" width={15} />
|
<WifiOff className="text-error absolute left-2 text-sm" width={15} />
|
||||||
<span className="flex-1 text-center">Disconnect</span>
|
<span className="flex-1 text-center">Disconnect</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ import { getConnectedDispatcherAPI } from "_querys/dispatcher";
|
|||||||
import { getConnectedAircraftsAPI } from "_querys/aircrafts";
|
import { getConnectedAircraftsAPI } from "_querys/aircrafts";
|
||||||
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
|
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
|
||||||
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
||||||
|
import { Trash } from "lucide-react";
|
||||||
|
|
||||||
export const Chat = () => {
|
export const Chat = () => {
|
||||||
const {
|
const {
|
||||||
|
removeChat,
|
||||||
setReportTabOpen,
|
setReportTabOpen,
|
||||||
chatOpen,
|
chatOpen,
|
||||||
setChatOpen,
|
setChatOpen,
|
||||||
@@ -50,13 +52,21 @@ export const Chat = () => {
|
|||||||
setOwnId(session.data?.user.id);
|
setOwnId(session.data?.user.id);
|
||||||
}, [session.data?.user.id, setOwnId]);
|
}, [session.data?.user.id, setOwnId]);
|
||||||
|
|
||||||
const filteredDispatcher = dispatcher?.filter((d) => d.userId !== session.data?.user.id);
|
const filteredDispatcher = dispatcher?.filter(
|
||||||
|
(d) => d.userId !== session.data?.user.id && !chats[d.userId],
|
||||||
|
);
|
||||||
const filteredAircrafts = aircrafts?.filter(
|
const filteredAircrafts = aircrafts?.filter(
|
||||||
(a) => a.userId !== session.data?.user.id && dispatcherConnected,
|
(a) => a.userId !== session.data?.user.id && dispatcherConnected && chats[a.userId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const btnActive = pilotConnected || dispatcherConnected;
|
const btnActive = pilotConnected || dispatcherConnected;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!filteredDispatcher?.length && !filteredAircrafts?.length) {
|
||||||
|
setAddTabValue("default");
|
||||||
|
}
|
||||||
|
}, [filteredDispatcher, filteredAircrafts]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!btnActive) {
|
if (!btnActive) {
|
||||||
setChatOpen(false);
|
setChatOpen(false);
|
||||||
@@ -146,13 +156,17 @@ export const Chat = () => {
|
|||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-soft btn-primary join-item"
|
className="btn btn-sm btn-soft btn-primary join-item"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (addTabValue === "default") return;
|
||||||
const aircraftUser = aircrafts?.find((a) => a.userId === addTabValue);
|
const aircraftUser = aircrafts?.find((a) => a.userId === addTabValue);
|
||||||
const dispatcherUser = dispatcher?.find((d) => d.userId === addTabValue);
|
const dispatcherUser = dispatcher?.find((d) => d.userId === addTabValue);
|
||||||
const user = aircraftUser || dispatcherUser;
|
const user = aircraftUser || dispatcherUser;
|
||||||
|
console.log("Adding chat for user:", addTabValue, user);
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
const role = "Station" in user ? user.Station.bosCallsignShort : user.zone;
|
const role = "Station" in user ? user.Station.bosCallsignShort : user.zone;
|
||||||
|
console.log("Adding chat for user:", addTabValue);
|
||||||
addChat(addTabValue, `${asPublicUser(user.publicUser).fullName} (${role})`);
|
addChat(addTabValue, `${asPublicUser(user.publicUser).fullName} (${role})`);
|
||||||
setSelectedChat(addTabValue);
|
setSelectedChat(addTabValue);
|
||||||
|
setAddTabValue("default");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="text-xl">+</span>
|
<span className="text-xl">+</span>
|
||||||
@@ -164,7 +178,7 @@ export const Chat = () => {
|
|||||||
if (!chat) return null;
|
if (!chat) return null;
|
||||||
return (
|
return (
|
||||||
<Fragment key={userId}>
|
<Fragment key={userId}>
|
||||||
<a
|
<div
|
||||||
className={cn("indicator tab", selectedChat === userId && "tab-active")}
|
className={cn("indicator tab", selectedChat === userId && "tab-active")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (selectedChat === userId) {
|
if (selectedChat === userId) {
|
||||||
@@ -176,7 +190,7 @@ export const Chat = () => {
|
|||||||
>
|
>
|
||||||
{chat.name}
|
{chat.name}
|
||||||
{chat.notification && <span className="indicator-item status status-info" />}
|
{chat.notification && <span className="indicator-item status status-info" />}
|
||||||
</a>
|
</div>
|
||||||
<div className="tab-content bg-base-100 border-base-300 max-h-[250px] overflow-y-auto p-6">
|
<div className="tab-content bg-base-100 border-base-300 max-h-[250px] overflow-y-auto p-6">
|
||||||
{/* So macht man kein overflow handeling, weiß ich. Aber es funktioniert... */}
|
{/* So macht man kein overflow handeling, weiß ich. Aber es funktioniert... */}
|
||||||
{chat.messages.map((chatMessage) => {
|
{chat.messages.map((chatMessage) => {
|
||||||
@@ -208,6 +222,16 @@ export const Chat = () => {
|
|||||||
)}
|
)}
|
||||||
{selectedChat && (
|
{selectedChat && (
|
||||||
<div className="join">
|
<div className="join">
|
||||||
|
<button
|
||||||
|
className="join-item btn btn-error btn-outline"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
removeChat(selectedChat);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Trash size={16} />
|
||||||
|
</button>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<label className="input join-item w-full">
|
<label className="input join-item w-full">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ const HeliportsLayer = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filterVisibleHeliports = () => {
|
const filterVisibleHeliports = () => {
|
||||||
const bounds = map.getBounds();
|
const bounds = map?.getBounds();
|
||||||
if (!heliports?.length) return;
|
if (!heliports?.length) return;
|
||||||
// Filtere die Heliports, die innerhalb der Kartenansicht liegen
|
// Filtere die Heliports, die innerhalb der Kartenansicht liegen
|
||||||
const visibleHeliports = heliports.filter((heliport) => {
|
const visibleHeliports = heliports.filter((heliport) => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
|
|||||||
export const MapAdditionals = () => {
|
export const MapAdditionals = () => {
|
||||||
const { isOpen, missionFormValues } = usePannelStore((state) => state);
|
const { isOpen, missionFormValues } = usePannelStore((state) => state);
|
||||||
const dispatcherConnectionState = useDispatchConnectionStore((state) => state.status);
|
const dispatcherConnectionState = useDispatchConnectionStore((state) => state.status);
|
||||||
|
|
||||||
const { data: missions = [] } = useQuery({
|
const { data: missions = [] } = useQuery({
|
||||||
queryKey: ["missions"],
|
queryKey: ["missions"],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
@@ -35,7 +36,8 @@ export const MapAdditionals = () => {
|
|||||||
m.state === "draft" &&
|
m.state === "draft" &&
|
||||||
m.hpgLocationLat &&
|
m.hpgLocationLat &&
|
||||||
dispatcherConnectionState === "connected" &&
|
dispatcherConnectionState === "connected" &&
|
||||||
m.hpgLocationLng,
|
m.hpgLocationLng &&
|
||||||
|
mapStore.openMissionMarker.find((openMission) => openMission.id === m.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -56,7 +58,7 @@ export const MapAdditionals = () => {
|
|||||||
key={mission.id}
|
key={mission.id}
|
||||||
position={[mission.hpgLocationLat!, mission.hpgLocationLng!]}
|
position={[mission.hpgLocationLat!, mission.hpgLocationLng!]}
|
||||||
icon={L.icon({
|
icon={L.icon({
|
||||||
iconUrl: "/icons/mapMarker.png",
|
iconUrl: "/icons/mapMarkerAttention.png",
|
||||||
iconSize: [40, 40],
|
iconSize: [40, 40],
|
||||||
iconAnchor: [20, 35],
|
iconAnchor: [20, 35],
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const MISSION_STATUS_COLORS: Record<MissionState | "attention", string> =
|
|||||||
draft: "#0092b8",
|
draft: "#0092b8",
|
||||||
running: "#155dfc",
|
running: "#155dfc",
|
||||||
finished: "#155dfc",
|
finished: "#155dfc",
|
||||||
attention: "rgb(186,105,0)",
|
attention: "#ba6900",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MISSION_STATUS_TEXT_COLORS: Record<MissionState, string> = {
|
export const MISSION_STATUS_TEXT_COLORS: Record<MissionState, string> = {
|
||||||
|
|||||||
@@ -25,28 +25,29 @@ import { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
|||||||
let interval: NodeJS.Timeout;
|
let interval: NodeJS.Timeout;
|
||||||
|
|
||||||
type TalkState = {
|
type TalkState = {
|
||||||
|
addSpeakingParticipant: (participant: Participant) => void;
|
||||||
|
connect: (roomName: string, role: string) => void;
|
||||||
|
connectionQuality: ConnectionQuality;
|
||||||
|
disconnect: () => void;
|
||||||
|
isTalking: boolean;
|
||||||
|
localRadioTrack: LocalTrackPublication | undefined;
|
||||||
|
message: string | null;
|
||||||
|
removeMessage: () => void;
|
||||||
|
removeSpeakingParticipant: (speakingParticipants: Participant) => void;
|
||||||
|
remoteParticipants: number;
|
||||||
|
resetSpeakingParticipants: (source: string) => void;
|
||||||
|
room: Room | null;
|
||||||
|
setSettings: (settings: Partial<TalkState["settings"]>) => void;
|
||||||
settings: {
|
settings: {
|
||||||
micDeviceId: string | null;
|
micDeviceId: string | null;
|
||||||
micVolume: number;
|
micVolume: number;
|
||||||
radioVolume: number;
|
radioVolume: number;
|
||||||
dmeVolume: number;
|
dmeVolume: number;
|
||||||
};
|
};
|
||||||
isTalking: boolean;
|
|
||||||
transmitBlocked: boolean;
|
|
||||||
removeMessage: () => void;
|
|
||||||
state: "connecting" | "connected" | "disconnected" | "error";
|
|
||||||
message: string | null;
|
|
||||||
connectionQuality: ConnectionQuality;
|
|
||||||
remoteParticipants: number;
|
|
||||||
toggleTalking: () => void;
|
|
||||||
setSettings: (settings: Partial<TalkState["settings"]>) => void;
|
|
||||||
connect: (roomName: string, role: string) => void;
|
|
||||||
disconnect: () => void;
|
|
||||||
speakingParticipants: Participant[];
|
speakingParticipants: Participant[];
|
||||||
addSpeakingParticipant: (participant: Participant) => void;
|
state: "connecting" | "connected" | "disconnected" | "error";
|
||||||
removeSpeakingParticipant: (speakingParticipants: Participant) => void;
|
toggleTalking: () => void;
|
||||||
room: Room | null;
|
transmitBlocked: boolean;
|
||||||
localRadioTrack: LocalTrackPublication | undefined;
|
|
||||||
};
|
};
|
||||||
const getToken = async (roomName: string) => {
|
const getToken = async (roomName: string) => {
|
||||||
const response = await axios.get(`/api/livekit-token?roomName=${roomName}`);
|
const response = await axios.get(`/api/livekit-token?roomName=${roomName}`);
|
||||||
@@ -71,6 +72,15 @@ export const useAudioStore = create<TalkState>((set, get) => ({
|
|||||||
remoteParticipants: 0,
|
remoteParticipants: 0,
|
||||||
connectionQuality: ConnectionQuality.Unknown,
|
connectionQuality: ConnectionQuality.Unknown,
|
||||||
room: null,
|
room: null,
|
||||||
|
resetSpeakingParticipants: (source: string) => {
|
||||||
|
set({
|
||||||
|
speakingParticipants: [],
|
||||||
|
isTalking: false,
|
||||||
|
transmitBlocked: false,
|
||||||
|
message: `Ruf beendet durch ${source || "eine unsichtbare Macht"}`,
|
||||||
|
});
|
||||||
|
get().room?.localParticipant.setMicrophoneEnabled(false);
|
||||||
|
},
|
||||||
addSpeakingParticipant: (participant) => {
|
addSpeakingParticipant: (participant) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
if (!state.speakingParticipants.some((p) => p.identity === participant.identity)) {
|
if (!state.speakingParticipants.some((p) => p.identity === participant.identity)) {
|
||||||
@@ -177,6 +187,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: roomName,
|
||||||
|
ghostMode: dispatchState.ghostMode,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,10 +211,15 @@ export const useAudioStore = create<TalkState>((set, get) => ({
|
|||||||
|
|
||||||
set({ localRadioTrack: publishedTrack });
|
set({ localRadioTrack: publishedTrack });
|
||||||
|
|
||||||
set({ state: "connected", room, message: null });
|
set({ state: "connected", room, isTalking: false, message: null });
|
||||||
})
|
})
|
||||||
.on(RoomEvent.Disconnected, () => {
|
.on(RoomEvent.Disconnected, () => {
|
||||||
set({ state: "disconnected", speakingParticipants: [], transmitBlocked: false });
|
set({
|
||||||
|
state: "disconnected",
|
||||||
|
speakingParticipants: [],
|
||||||
|
transmitBlocked: false,
|
||||||
|
isTalking: false,
|
||||||
|
});
|
||||||
|
|
||||||
handleDisconnect();
|
handleDisconnect();
|
||||||
})
|
})
|
||||||
@@ -222,17 +238,22 @@ export const useAudioStore = create<TalkState>((set, get) => ({
|
|||||||
|
|
||||||
room.registerRpcMethod("force-mute", async (data: RpcInvocationData) => {
|
room.registerRpcMethod("force-mute", async (data: RpcInvocationData) => {
|
||||||
const { by } = JSON.parse(data.payload);
|
const { by } = JSON.parse(data.payload);
|
||||||
room.localParticipant.setMicrophoneEnabled(false);
|
get().resetSpeakingParticipants(by);
|
||||||
useAudioStore.setState({
|
return "OK";
|
||||||
isTalking: false,
|
|
||||||
message: `Ruf beendet durch ${by || "eine unsichtbare Macht"}`,
|
|
||||||
});
|
|
||||||
return `Hello, ${data.callerIdentity}!`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
interval = setInterval(() => {
|
interval = setInterval(() => {
|
||||||
|
// Filter forgotten participants
|
||||||
|
const oldSpeakingParticipants = get().speakingParticipants;
|
||||||
|
const speakingParticipants = oldSpeakingParticipants.filter((oP) => {
|
||||||
|
return Array.from(room.remoteParticipants.values()).find(
|
||||||
|
(p) => p.identity === oP.identity,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
set({
|
set({
|
||||||
remoteParticipants: room.numParticipants === 0 ? 0 : room.numParticipants - 1, // Unreliable and delayed
|
remoteParticipants: room.numParticipants === 0 ? 0 : room.numParticipants - 1, // Unreliable and delayed
|
||||||
|
speakingParticipants,
|
||||||
});
|
});
|
||||||
}, 500);
|
}, 500);
|
||||||
} catch (error: Error | unknown) {
|
} catch (error: Error | unknown) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ interface ChatStore {
|
|||||||
sendMessage: (userId: string, message: string) => Promise<void>;
|
sendMessage: (userId: string, message: string) => Promise<void>;
|
||||||
addChat: (userId: string, name: string) => void;
|
addChat: (userId: string, name: string) => void;
|
||||||
addMessage: (userId: string, message: ChatMessage) => void;
|
addMessage: (userId: string, message: ChatMessage) => void;
|
||||||
|
removeChat: (userId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useLeftMenuStore = create<ChatStore>((set, get) => ({
|
export const useLeftMenuStore = create<ChatStore>((set, get) => ({
|
||||||
@@ -37,6 +38,15 @@ export const useLeftMenuStore = create<ChatStore>((set, get) => ({
|
|||||||
setChatNotification(chatId, false); // Set notification to false when chat is selected
|
setChatNotification(chatId, false); // Set notification to false when chat is selected
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
removeChat: (userId: string) => {
|
||||||
|
const { chats, setSelectedChat, selectedChat } = get();
|
||||||
|
const newChats = { ...chats };
|
||||||
|
delete newChats[userId];
|
||||||
|
set({ chats: newChats });
|
||||||
|
if (selectedChat === userId) {
|
||||||
|
setSelectedChat(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
setOwnId: (id: string) => set({ ownId: id }),
|
setOwnId: (id: string) => set({ ownId: id }),
|
||||||
chats: {},
|
chats: {},
|
||||||
sendMessage: (userId: string, message: string) => {
|
sendMessage: (userId: string, message: string) => {
|
||||||
|
|||||||
BIN
apps/dispatch/public/icons/mapMarkerAttention.png
Normal file
BIN
apps/dispatch/public/icons/mapMarkerAttention.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
@@ -7,55 +7,59 @@ export const handleParticipantFinished = async (
|
|||||||
participant: Participant,
|
participant: Participant,
|
||||||
user: User,
|
user: User,
|
||||||
) => {
|
) => {
|
||||||
const discordAccount = await prisma.discordAccount.findFirst({
|
try {
|
||||||
where: {
|
const discordAccount = await prisma.discordAccount.findFirst({
|
||||||
userId: user.id,
|
where: {
|
||||||
},
|
userId: user.id,
|
||||||
});
|
|
||||||
|
|
||||||
const badgedToAdd = event.finishedBadges.filter((badge) => {
|
|
||||||
return !user.badges.includes(badge);
|
|
||||||
});
|
|
||||||
const permissionsToAdd = event.finishedPermissions.filter((permission) => {
|
|
||||||
return !user.permissions.includes(permission);
|
|
||||||
});
|
|
||||||
|
|
||||||
await prisma.user.update({
|
|
||||||
where: {
|
|
||||||
id: user.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
badges: {
|
|
||||||
push: badgedToAdd,
|
|
||||||
},
|
},
|
||||||
permissions: {
|
|
||||||
push: permissionsToAdd,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (discordAccount) {
|
|
||||||
await setStandardName({
|
|
||||||
memberId: discordAccount.discordId,
|
|
||||||
userId: user.id,
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
await sendCourseCompletedEmail(user.email, user, event);
|
|
||||||
|
|
||||||
await prisma.participant.update({
|
const badgedToAdd = event.finishedBadges.filter((badge) => {
|
||||||
where: {
|
return !user.badges.includes(badge);
|
||||||
id: participant.id,
|
});
|
||||||
},
|
const permissionsToAdd = event.finishedPermissions.filter((permission) => {
|
||||||
data: {
|
return !user.permissions.includes(permission);
|
||||||
statusLog: {
|
});
|
||||||
push: {
|
|
||||||
event: "Berechtigungen und Badges vergeben",
|
await prisma.user.update({
|
||||||
timestamp: new Date(),
|
where: {
|
||||||
user: "system",
|
id: user.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
badges: {
|
||||||
|
push: badgedToAdd,
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
push: permissionsToAdd,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
if (discordAccount) {
|
||||||
|
await setStandardName({
|
||||||
|
memberId: discordAccount.discordId,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await sendCourseCompletedEmail(user.email, user, event);
|
||||||
|
|
||||||
|
await prisma.participant.update({
|
||||||
|
where: {
|
||||||
|
id: participant.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
statusLog: {
|
||||||
|
push: {
|
||||||
|
event: "Berechtigungen und Badges vergeben",
|
||||||
|
timestamp: new Date(),
|
||||||
|
user: "system",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error handling participant finished:", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleParticipantEnrolled = async (
|
export const handleParticipantEnrolled = async (
|
||||||
|
|||||||
@@ -34,43 +34,61 @@ const initTransporter = () => {
|
|||||||
initTransporter();
|
initTransporter();
|
||||||
|
|
||||||
export const sendCourseCompletedEmail = async (to: string, user: User, event: Event) => {
|
export const sendCourseCompletedEmail = async (to: string, user: User, event: Event) => {
|
||||||
const emailHtml = await renderCourseCompleted({ user, event });
|
try {
|
||||||
|
const emailHtml = await renderCourseCompleted({ user, event });
|
||||||
|
|
||||||
if (!transporter) {
|
if (!transporter) {
|
||||||
console.error("Transporter is not initialized");
|
console.error("Transporter is not initialized");
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
await sendMail(to, `Kurs ${event.name} erfolgreich abgeschlossen`, emailHtml);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending course completed email:", error);
|
||||||
}
|
}
|
||||||
sendMail(to, `Kurs ${event.name} erfolgreich abgeschlossen`, emailHtml);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendPasswordChanged = async (to: string, user: User, password: string) => {
|
export const sendPasswordChanged = async (to: string, user: User, password: string) => {
|
||||||
const emailHtml = await renderPasswordChanged({ user, password });
|
try {
|
||||||
|
const emailHtml = await renderPasswordChanged({ user, password });
|
||||||
|
|
||||||
await sendMail(to, `Dein Passwort wurde geändert`, emailHtml);
|
await sendMail(to, `Dein Passwort wurde geändert`, emailHtml);
|
||||||
|
} catch (error) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendEmailVerification = async (to: string, user: User, code: string) => {
|
export const sendEmailVerification = async (to: string, user: User, code: string) => {
|
||||||
const emailHtml = await renderVerificationCode({
|
try {
|
||||||
user,
|
const emailHtml = await renderVerificationCode({
|
||||||
code,
|
user,
|
||||||
});
|
code,
|
||||||
await sendMail(to, "Bestätige deine E-Mail-Adresse", emailHtml);
|
});
|
||||||
|
await sendMail(to, "Bestätige deine E-Mail-Adresse", emailHtml);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending email verification:", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendBannEmail = async (to: string, user: User, staffName: string) => {
|
export const sendBannEmail = async (to: string, user: User, staffName: string) => {
|
||||||
const emailHtml = await renderBannNotice({
|
try {
|
||||||
user,
|
const emailHtml = await renderBannNotice({
|
||||||
staffName,
|
user,
|
||||||
});
|
staffName,
|
||||||
await sendMail(to, "Deine Sperrung bei Virtual Air Rescue", emailHtml);
|
});
|
||||||
|
await sendMail(to, "Deine Sperrung bei Virtual Air Rescue", emailHtml);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending ban email:", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendTimebannEmail = async (to: string, user: User, staffName: string) => {
|
export const sendTimebannEmail = async (to: string, user: User, staffName: string) => {
|
||||||
const emailHtml = await renderTimeBanNotice({
|
try {
|
||||||
user,
|
const emailHtml = await renderTimeBanNotice({
|
||||||
staffName,
|
user,
|
||||||
});
|
staffName,
|
||||||
await sendMail(to, "Deine vorrübergehende Sperrung bei Virtual Air Rescue", emailHtml);
|
});
|
||||||
|
await sendMail(to, "Deine vorrübergehende Sperrung bei Virtual Air Rescue", emailHtml);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending time ban email:", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendMail = async (to: string, subject: string, html: string) =>
|
export const sendMail = async (to: string, subject: string, html: string) =>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Button } from "../../../../_components/ui/Button";
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import { cn } from "@repo/shared-components";
|
||||||
|
|
||||||
const MarkdownEditor = dynamic(() => import("@uiw/react-md-editor"), { ssr: false });
|
const MarkdownEditor = dynamic(() => import("@uiw/react-md-editor"), { ssr: false });
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ export const ChangelogForm = ({ changelog }: { changelog?: Changelog }) => {
|
|||||||
previewImage: changelog?.previewImage || "", // Changed to accept a URL as a string
|
previewImage: changelog?.previewImage || "", // Changed to accept a URL as a string
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const [skipUserUpdate, setSkipUserUpdate] = useState(false);
|
||||||
const [markdownText, setMarkdownText] = useState(changelog?.text || "");
|
const [markdownText, setMarkdownText] = useState(changelog?.text || "");
|
||||||
const [imageError, setImageError] = useState(false);
|
const [imageError, setImageError] = useState(false);
|
||||||
const [showImage, setShowImage] = useState(false);
|
const [showImage, setShowImage] = useState(false);
|
||||||
@@ -61,6 +63,9 @@ export const ChangelogForm = ({ changelog }: { changelog?: Changelog }) => {
|
|||||||
text: markdownText,
|
text: markdownText,
|
||||||
},
|
},
|
||||||
changelog?.id,
|
changelog?.id,
|
||||||
|
{
|
||||||
|
skipUserUpdate: skipUserUpdate,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
toast.success("Daten gespeichert");
|
toast.success("Daten gespeichert");
|
||||||
if (!changelog) redirect(`/admin/changelog`);
|
if (!changelog) redirect(`/admin/changelog`);
|
||||||
@@ -96,6 +101,7 @@ export const ChangelogForm = ({ changelog }: { changelog?: Changelog }) => {
|
|||||||
{(() => {
|
{(() => {
|
||||||
if (showImage && isValidImageUrl(previewImage) && !imageError) {
|
if (showImage && isValidImageUrl(previewImage) && !imageError) {
|
||||||
return (
|
return (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img
|
<img
|
||||||
src={previewImage}
|
src={previewImage}
|
||||||
alt="Preview"
|
alt="Preview"
|
||||||
@@ -127,6 +133,19 @@ export const ChangelogForm = ({ changelog }: { changelog?: Changelog }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card bg-base-200 col-span-6 shadow-xl">
|
<div className="card bg-base-200 col-span-6 shadow-xl">
|
||||||
|
{!changelog?.id && (
|
||||||
|
<label className="label mx-6 mt-6 w-full cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className={cn("toggle")}
|
||||||
|
checked={skipUserUpdate}
|
||||||
|
onChange={(e) => setSkipUserUpdate(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span className={cn("label-text w-full text-left")}>
|
||||||
|
Kein Popup für Nutzer für dieses Update anzeigen
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<div className="flex w-full gap-4">
|
<div className="flex w-full gap-4">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import { prisma, Prisma, Changelog } from "@repo/db";
|
|||||||
export const upsertChangelog = async (
|
export const upsertChangelog = async (
|
||||||
changelog: Prisma.ChangelogCreateInput,
|
changelog: Prisma.ChangelogCreateInput,
|
||||||
id?: Changelog["id"],
|
id?: Changelog["id"],
|
||||||
|
options?: {
|
||||||
|
skipUserUpdate?: boolean;
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
const newChangelog = id
|
const newChangelog = id
|
||||||
? await prisma.changelog.update({
|
? await prisma.changelog.update({
|
||||||
@@ -12,10 +15,13 @@ export const upsertChangelog = async (
|
|||||||
})
|
})
|
||||||
: await prisma.$transaction(async (prisma) => {
|
: await prisma.$transaction(async (prisma) => {
|
||||||
const createdChangelog = await prisma.changelog.create({ data: changelog });
|
const createdChangelog = await prisma.changelog.create({ data: changelog });
|
||||||
|
if (!options?.skipUserUpdate) {
|
||||||
|
// Update all users to acknowledge the new changelog
|
||||||
|
|
||||||
await prisma.user.updateMany({
|
await prisma.user.updateMany({
|
||||||
data: { changelogAck: false },
|
data: { changelogAck: false },
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return createdChangelog;
|
return createdChangelog;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const GET = async (req: NextRequest) => {
|
|||||||
email: discordUser.email,
|
email: discordUser.email,
|
||||||
avatar: discordUser.avatar,
|
avatar: discordUser.avatar,
|
||||||
username: discordUser.username,
|
username: discordUser.username,
|
||||||
globalName: discordUser.global_name,
|
globalName: discordUser.global_name || discordUser.username,
|
||||||
verified: discordUser.verified,
|
verified: discordUser.verified,
|
||||||
tokenType: authData.token_type,
|
tokenType: authData.token_type,
|
||||||
} as DiscordAccount;
|
} as DiscordAccount;
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ services:
|
|||||||
- NEXT_PUBLIC_DISPATCH_SERVICE_ID=1
|
- NEXT_PUBLIC_DISPATCH_SERVICE_ID=1
|
||||||
- NEXT_PUBLIC_LIVEKIT_URL=$NEXT_PUBLIC_LIVEKIT_URL
|
- NEXT_PUBLIC_LIVEKIT_URL=$NEXT_PUBLIC_LIVEKIT_URL
|
||||||
- NEXT_PUBLIC_DISPATCH_SERVER_URL=$NEXT_PUBLIC_DISPATCH_SERVER_URL
|
- NEXT_PUBLIC_DISPATCH_SERVER_URL=$NEXT_PUBLIC_DISPATCH_SERVER_URL
|
||||||
|
- NEXT_PUBLIC_OPENAIP_ACCESS=$NEXT_PUBLIC_OPENAIP_ACCESS
|
||||||
env_file:
|
env_file:
|
||||||
- .env.prod
|
- .env.prod
|
||||||
deploy:
|
deploy:
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ services:
|
|||||||
- NEXT_PUBLIC_DISPATCH_SERVICE_ID=1
|
- NEXT_PUBLIC_DISPATCH_SERVICE_ID=1
|
||||||
- NEXT_PUBLIC_LIVEKIT_URL=$NEXT_PUBLIC_LIVEKIT_URL
|
- NEXT_PUBLIC_LIVEKIT_URL=$NEXT_PUBLIC_LIVEKIT_URL
|
||||||
- NEXT_PUBLIC_DISPATCH_SERVER_URL=$NEXT_PUBLIC_DISPATCH_SERVER_URL
|
- NEXT_PUBLIC_DISPATCH_SERVER_URL=$NEXT_PUBLIC_DISPATCH_SERVER_URL
|
||||||
|
- NEXT_PUBLIC_OPENAIP_ACCESS=$NEXT_PUBLIC_OPENAIP_ACCESS
|
||||||
env_file:
|
env_file:
|
||||||
- .env.prod
|
- .env.prod
|
||||||
deploy:
|
deploy:
|
||||||
|
|||||||
@@ -22,13 +22,16 @@ export const getPublicUser = (
|
|||||||
},
|
},
|
||||||
): PublicUser => {
|
): PublicUser => {
|
||||||
const lastName = user.lastname
|
const lastName = user.lastname
|
||||||
|
.trim()
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.map((part) => `${part[0]}.`)
|
.map((part) => `${part[0] || ""}.`)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
firstname: user.firstname,
|
firstname: user.firstname,
|
||||||
lastname: user.settingsHideLastname && !options.ignorePrivacy ? "" : lastName, // Only take the first letter of each section of the last name
|
lastname: user.settingsHideLastname && !options.ignorePrivacy ? "" : lastName.trim(), // Only take the first letter of each section of the last name
|
||||||
fullName: `${user.firstname} ${user.settingsHideLastname && !options.ignorePrivacy ? "" : lastName}`,
|
fullName:
|
||||||
|
`${user.firstname} ${user.settingsHideLastname && !options.ignorePrivacy ? "" : lastName}`.trim(),
|
||||||
publicId: user.publicId,
|
publicId: user.publicId,
|
||||||
badges: user.badges,
|
badges: user.badges,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button, cn } from "@repo/shared-components";
|
import { Button, cn } from "@repo/shared-components";
|
||||||
import MDEditor from "@uiw/react-md-editor";
|
import MDEditor from "@uiw/react-md-editor";
|
||||||
import { RefreshCw } from "lucide-react";
|
import { Check, RefreshCw } from "lucide-react";
|
||||||
import { Changelog } from "@repo/db";
|
import { Changelog } from "@repo/db";
|
||||||
|
|
||||||
export const ChangelogModal = ({
|
export const ChangelogModal = ({
|
||||||
@@ -50,7 +50,8 @@ export const ChangelogModal = ({
|
|||||||
|
|
||||||
<div className="modal-action">
|
<div className="modal-action">
|
||||||
<Button className="btn btn-info btn-outline" onClick={onClose}>
|
<Button className="btn btn-info btn-outline" onClick={onClose}>
|
||||||
Weiter zum HUB
|
<Check size={20} />
|
||||||
|
gelesen
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user