8 Commits

Author SHA1 Message Date
PxlLoewe
7321759baf added other badges 2025-07-25 10:39:25 -07:00
PxlLoewe
383e15f38a Merge branch 'staging' of https://github.com/VAR-Virtual-Air-Rescue/var-monorepo into staging 2025-07-25 10:32:45 -07:00
PxlLoewe
afe32306d3 fixed #106 2025-07-25 10:32:35 -07:00
PxlLoewe
1b23aede89 fixed #105 2025-07-25 10:04:44 -07:00
PxlLoewe
54e0bc0b12 fixed #104 2025-07-25 09:53:58 -07:00
nocnico
2671571bfb maybe ghostmode fix now? 2025-07-25 17:43:05 +02:00
nocnico
b602a5836c Ghostmode fix now? 2025-07-25 17:19:58 +02:00
nocnico
4eb46cb783 fixed #100 // fix Marker Cluster naming, fix Marker Popup settings not used 2025-07-25 16:39:21 +02:00
19 changed files with 214 additions and 178 deletions

View File

@@ -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 };
}; };

View File

@@ -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);
}
}; };

View File

@@ -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}`,

View File

@@ -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;
} }
}); });

View File

@@ -70,7 +70,7 @@ export const handleConnectDispatch =
userId: user.id, userId: user.id,
}, },
}); });
if (discordAccount?.id) { if (discordAccount?.id && !ghostMode) {
await renameMember( await renameMember(
discordAccount.discordId.toString(), discordAccount.discordId.toString(),
`${getPublicUser(user).fullName}${selectedZone}`, `${getPublicUser(user).fullName}${selectedZone}`,

View File

@@ -253,7 +253,7 @@ const MissionMarker = ({
tab: "home", tab: "home",
}, },
], ],
close: openMissionMarker?.map((m) => m.id) || [], close: [],
}); });
} }
}; };

View File

@@ -158,7 +158,7 @@ const PopupContent = ({
</span> </span>
<span> <span>
{aircraft.Station.bosCallsign.length > 15 {aircraft.Station.bosCallsign.length > 15
? aircraft.Station.locationStateShort ? aircraft.Station.bosCallsignShort
: aircraft.Station.bosCallsign} : aircraft.Station.bosCallsign}
</span> </span>
</div> </div>

View File

@@ -177,6 +177,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,
}); });
} }

View File

@@ -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 (

View File

@@ -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) =>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 552 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -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,
}; };