added ntfy

This commit is contained in:
PxlLoewe
2025-05-19 23:11:33 -07:00
parent 61e7caf6c8
commit 060810f1b0
11 changed files with 277 additions and 76 deletions

View File

@@ -0,0 +1,84 @@
import { Mission, Station } from "@repo/db";
import axios from "axios";
interface NtfyHeader {
headers: {
Title: string;
Tags: string;
Priority: string;
};
}
const getLocation = (mission: Mission) => {
return `🏡 Ort:
${mission.addressStreet}, ${mission.addressZip} ${mission.addressCity}\n\n`;
};
const getCordinates = (mission: Mission) => {
let cords = `📍 Koordinaten:
- Breite: ${mission.addressLat}
- Länge: ${mission.addressLng}`;
return `${cords}\n\n`;
};
const getInfo = (mission: Mission) => {
return `⚕️ Info:
${mission.missionKeywordAbbreviation}\n\n`;
};
const getPatientInfo = (mission: Mission) => {
if (!mission.missionPatientInfo) return "";
return `🧍‍♂️ Patient:
${mission.missionPatientInfo}\n\n`;
};
const getAdditionalInfo = (mission: Mission) => {
if (!mission.missionAdditionalInfo) return "";
return `📋 Anmerkung:
${mission.missionAdditionalInfo}\n\n`;
};
const getRthCallsigns = (mission: Mission, stations: Station[]) => {
const callsigns: Array<string> = [];
stations.forEach((station) => {
callsigns.push(station.bosCallsignShort);
});
return `🚁 RTH${callsigns.length > 1 ? "s" : ""}: ${callsigns.join(" / ")} `;
};
const getNtfyHeader = (
mission: Mission,
clientStation: Station,
): NtfyHeader => ({
headers: {
Title: `${clientStation.bosCallsignShort} / ${mission.missionKeywordAbbreviation} / ${mission.missionKeywordCategory}`,
Tags: "pager",
Priority: "4",
},
});
const getNtfyData = (mission: Mission, stations: Station[]): string =>
`${new Date().toLocaleString()}\n\n` +
`${getInfo(mission)}` +
`${getLocation(mission)}` +
`${getCordinates(mission)}` +
`${getPatientInfo(mission)}` +
`${getAdditionalInfo(mission)}` +
`${getRthCallsigns(mission, stations)}`;
export const sendNtfyMission = async (
mission: Mission,
stations: Station[],
clientStation: Station,
ntfyRoom: string,
) => {
axios.post(
`https://ntfy.sh/${ntfyRoom}`,
getNtfyData(mission, stations),
getNtfyHeader(mission, clientStation),
);
};

View File

@@ -1,6 +1,7 @@
import { prisma } from "@repo/db";
import { Router } from "express";
import { io } from "../index";
import { sendNtfyMission } from "modules/ntfy";
const router = Router();
@@ -107,6 +108,9 @@ router.post("/:id/send-alert", async (req, res) => {
},
logoutTime: null,
},
include: {
Station: true,
},
});
for (const aircraft of connectedAircrafts) {
@@ -115,6 +119,18 @@ router.post("/:id/send-alert", async (req, res) => {
...mission,
Stations,
});
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: {

View File

@@ -38,6 +38,7 @@ export function QueryProvider({ children }: { children: ReactNode }) {
};
const invalidateConenctedAircrafts = () => {
console.log("invalidateConenctedAircrafts");
queryClient.invalidateQueries({
queryKey: ["aircrafts"],
});

View File

@@ -1,6 +1,6 @@
import { create } from "zustand";
import { dispatchSocket } from "../../dispatch/socket";
import { ConnectedAircraft, Mission, Station } from "@repo/db";
import { ConnectedAircraft, Mission, Station, User } from "@repo/db";
import { pilotSocket } from "pilot/socket";
import { useDmeStore } from "_store/pilot/dmeStore";
@@ -19,6 +19,7 @@ interface ConnectionStore {
stationId: string,
logoffTime: string,
station: Station,
user: User,
) => Promise<void>;
disconnect: () => void;
}
@@ -29,12 +30,18 @@ export const usePilotConnectionStore = create<ConnectionStore>((set) => ({
selectedStation: null,
connectedAircraft: null,
activeMission: null,
connect: async (uid, stationId, logoffTime, station) =>
connect: async (uid, stationId, logoffTime, station, user) =>
new Promise((resolve) => {
set({ status: "connecting", message: "", selectedStation: station });
pilotSocket.auth = { uid };
pilotSocket.connect();
pilotSocket.once("connect", () => {
useDmeStore.getState().setPage({
page: "home",
station,
user,
});
pilotSocket.emit("connect-pilot", {
logoffTime,
stationId,

View File

@@ -43,6 +43,8 @@ interface MrtStore {
setLines: (lines: MrtStore["lines"]) => void;
}
let interval: NodeJS.Timeout | null = null;
export const useDmeStore = create<MrtStore>(
syncTabs(
(set) => ({
@@ -52,42 +54,52 @@ export const useDmeStore = create<MrtStore>(
},
lines: [
{
textLeft: "VAR.#",
textLeft: "",
},
{
textMid: "VAR . DME# No Data",
textSize: "2",
},
{
textLeft: "No Data",
textLeft: "",
},
],
setLines: (lines) => set({ lines }),
setPage: (pageData) => {
if (interval) clearInterval(interval);
switch (pageData.page) {
case "home": {
set({
page: "home",
lines: [
{ textMid: "" },
{
textMid: pageData.station.bosCallsign
? `VAR#.${pageData.station.bosCallsign}`
: "no Data",
style: { fontWeight: "bold" },
},
{ textMid: "" },
{
textMid: new Date().toLocaleDateString(),
},
{
textMid: new Date().toLocaleTimeString(),
style: { fontWeight: "bold" },
},
{ textMid: "" },
{
textMid: `${pageData.user.lastname} ${pageData.user.firstname}`,
},
{ textMid: "" },
],
});
const setHomePage = () =>
set({
page: "home",
lines: [
{ textMid: "" },
{
textMid: pageData.station.bosCallsign
? `VAR#.${pageData.station.bosCallsign}`
: "no Data",
style: { fontWeight: "bold" },
},
{ textMid: "" },
{
textMid: new Date().toLocaleDateString(),
},
{
textMid: new Date().toLocaleTimeString(),
style: { fontWeight: "bold" },
},
{ textMid: "" },
{
textMid: `${pageData.user.lastname} ${pageData.user.firstname}`,
},
{ textMid: "" },
],
});
setHomePage();
interval = setInterval(() => {
setHomePage();
}, 1000);
break;
}

View File

@@ -10,12 +10,16 @@ export const useButtons = () => {
const handleButton = (button: "main" | "menu" | "left" | "right") => () => {
switch (button) {
case "main":
if (page === "mission") {
if (page === "mission" || page === "new-mission") {
setPage({ page: "acknowledge" });
}
break;
case "menu":
console.log("home", page, { station, user });
if (page === "mission" || page === "new-mission") {
setPage({ page: "acknowledge" });
if (station && user) setPage({ page: "home", station, user });
break;
}
if (station && user) {
setPage({ page: "home", station, user });
} else {

View File

@@ -130,16 +130,20 @@ export const ConnectionBtn = () => {
type="submit"
onSubmit={() => false}
onClick={() => {
connection.connect(
uid,
form.selectedStationId?.toString() || "",
form.logoffTime || "",
stations?.find(
(station) =>
station.id ===
parseInt(form.selectedStationId?.toString() || ""),
)!,
const selectedStation = stations?.find(
(station) =>
station.id ===
parseInt(form.selectedStationId?.toString() || ""),
);
if (selectedStation) {
connection.connect(
uid,
form.selectedStationId?.toString() || "",
form.logoffTime || "",
selectedStation,
session.data!.user,
);
}
}}
className="btn btn-soft btn-info"
>

View File

@@ -5,8 +5,6 @@ import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { unlinkDiscord, updateUser, changePassword } from "../actions";
import { Toaster, toast } from "react-hot-toast";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { Button } from "../../../_components/ui/Button";
import {
@@ -21,6 +19,9 @@ import {
LockOpen2Icon,
LockOpen1Icon,
} from "@radix-ui/react-icons";
import toast from "react-hot-toast";
import { UserSchema } from "@repo/db/zod";
import { Bell, Plane } from "lucide-react";
export const ProfileForm = ({ user }: { user: User }) => {
const schema = z.object({
@@ -242,7 +243,7 @@ export const SocialForm = ({
);
};
export const PasswordForm = ({ user }: { user: User }) => {
export const PasswordForm = () => {
const schema = z.object({
password: z.string().min(2).max(30),
newPassword: z.string().min(2).max(30),
@@ -345,3 +346,64 @@ export const PasswordForm = ({ user }: { user: User }) => {
</form>
);
};
export const PilotForm = ({ user }: { user: User }) => {
const [isLoading, setIsLoading] = useState(false);
const form = useForm<User>({
defaultValues: user,
resolver: zodResolver(UserSchema),
});
if (!user) return null;
return (
<form
className="card-body"
onSubmit={form.handleSubmit(async (values) => {
form.handleSubmit(async () => {
setIsLoading(true);
});
await updateUser(values);
setIsLoading(false);
form.reset(values);
toast.success("Deine Änderungen wurden gespeichert!", {
style: {
background: "var(--color-base-100)",
color: "var(--color-base-content)",
},
});
})}
>
<h2 className="card-title mb-5">
<Plane className="w-5 h-5" /> Pilot
</h2>
<div className="content-center">
<label className="floating-label w-full mt-5">
<span className="text-lg flex items-center gap-2">
<Bell /> NTFY room
</span>
<input
placeholder="erhalte eine Benachrichtigung aufs Handy über NTFY"
className="input input-bordered w-full"
{...form.register("settingsNtfyRoom")}
/>
</label>
{form.formState.errors.settingsNtfyRoom && (
<p className="text-error">
{form.formState.errors.settingsNtfyRoom.message}
</p>
)}
</div>
<div className="card-actions justify-center pt-6 mt-auto">
<Button
className="btn-sm btn-wide btn-outline btn-primary"
isLoading={isLoading}
disabled={!form.formState.isDirty}
role="submit"
>
<BookmarkIcon /> Speichern
</Button>
</div>
</form>
);
};

View File

@@ -1,38 +1,48 @@
import { getServerSession } from "../../api/auth/[...nextauth]/auth";
import { PrismaClient } from "@repo/db";
import { ProfileForm, SocialForm, PasswordForm } from "./_components/forms";
import {
ProfileForm,
SocialForm,
PasswordForm,
PilotForm,
} from "./_components/forms";
import { GearIcon } from "@radix-ui/react-icons";
export default async () => {
const prisma = new PrismaClient();
const session = await getServerSession();
if (!session) return null;
const user = await prisma.user.findFirst({
where: {
id: session.user.id,
},
include: {
discordAccounts: true,
},
});
if (!user) return null;
const discordAccount = user?.discordAccounts[0];
return (
<div className="grid grid-cols-6 gap-4">
<div className="col-span-full">
<p className="text-2xl font-semibold text-left flex items-center gap-2">
<GearIcon className="w-5 h-5" /> Einstellungen
</p>
</div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<ProfileForm user={user} />
</div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<SocialForm discordAccount={discordAccount} user={user} />
</div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<PasswordForm user={user} />
</div>
</div>
);
export const page = async () => {
const prisma = new PrismaClient();
const session = await getServerSession();
if (!session) return null;
const user = await prisma.user.findFirst({
where: {
id: session.user.id,
},
include: {
discordAccounts: true,
},
});
if (!user) return null;
const discordAccount = user?.discordAccounts[0];
return (
<div className="grid grid-cols-6 gap-4">
<div className="col-span-full">
<p className="text-2xl font-semibold text-left flex items-center gap-2">
<GearIcon className="w-5 h-5" /> Einstellungen
</p>
</div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<ProfileForm user={user} />
</div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<SocialForm discordAccount={discordAccount} user={user} />
</div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<PasswordForm />
</div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<PilotForm user={user} />
</div>
</div>
);
};
export default page;

Binary file not shown.

View File

@@ -31,6 +31,7 @@ model User {
emailVerified DateTime? @map(name: "email_verified")
// Settings:
settingsNtfyRoom String? @map(name: "settings_ntfy_room")
image String?
badges BADGES[] @default([])