added ntfy
This commit is contained in:
84
apps/dispatch-server/modules/ntfy.ts
Normal file
84
apps/dispatch-server/modules/ntfy.ts
Normal 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),
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { prisma } from "@repo/db";
|
import { prisma } from "@repo/db";
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { io } from "../index";
|
import { io } from "../index";
|
||||||
|
import { sendNtfyMission } from "modules/ntfy";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -107,6 +108,9 @@ router.post("/:id/send-alert", async (req, res) => {
|
|||||||
},
|
},
|
||||||
logoutTime: null,
|
logoutTime: null,
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
Station: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const aircraft of connectedAircrafts) {
|
for (const aircraft of connectedAircrafts) {
|
||||||
@@ -115,6 +119,18 @@ router.post("/:id/send-alert", async (req, res) => {
|
|||||||
...mission,
|
...mission,
|
||||||
Stations,
|
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 =
|
const existingMissionOnStationUser =
|
||||||
await prisma.missionOnStationUsers.findFirst({
|
await prisma.missionOnStationUsers.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export function QueryProvider({ children }: { children: ReactNode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const invalidateConenctedAircrafts = () => {
|
const invalidateConenctedAircrafts = () => {
|
||||||
|
console.log("invalidateConenctedAircrafts");
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["aircrafts"],
|
queryKey: ["aircrafts"],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { dispatchSocket } from "../../dispatch/socket";
|
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 { pilotSocket } from "pilot/socket";
|
||||||
import { useDmeStore } from "_store/pilot/dmeStore";
|
import { useDmeStore } from "_store/pilot/dmeStore";
|
||||||
|
|
||||||
@@ -19,6 +19,7 @@ interface ConnectionStore {
|
|||||||
stationId: string,
|
stationId: string,
|
||||||
logoffTime: string,
|
logoffTime: string,
|
||||||
station: Station,
|
station: Station,
|
||||||
|
user: User,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
disconnect: () => void;
|
disconnect: () => void;
|
||||||
}
|
}
|
||||||
@@ -29,12 +30,18 @@ export const usePilotConnectionStore = create<ConnectionStore>((set) => ({
|
|||||||
selectedStation: null,
|
selectedStation: null,
|
||||||
connectedAircraft: null,
|
connectedAircraft: null,
|
||||||
activeMission: null,
|
activeMission: null,
|
||||||
connect: async (uid, stationId, logoffTime, station) =>
|
connect: async (uid, stationId, logoffTime, station, user) =>
|
||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
set({ status: "connecting", message: "", selectedStation: station });
|
set({ status: "connecting", message: "", selectedStation: station });
|
||||||
|
|
||||||
pilotSocket.auth = { uid };
|
pilotSocket.auth = { uid };
|
||||||
pilotSocket.connect();
|
pilotSocket.connect();
|
||||||
pilotSocket.once("connect", () => {
|
pilotSocket.once("connect", () => {
|
||||||
|
useDmeStore.getState().setPage({
|
||||||
|
page: "home",
|
||||||
|
station,
|
||||||
|
user,
|
||||||
|
});
|
||||||
pilotSocket.emit("connect-pilot", {
|
pilotSocket.emit("connect-pilot", {
|
||||||
logoffTime,
|
logoffTime,
|
||||||
stationId,
|
stationId,
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ interface MrtStore {
|
|||||||
setLines: (lines: MrtStore["lines"]) => void;
|
setLines: (lines: MrtStore["lines"]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let interval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
export const useDmeStore = create<MrtStore>(
|
export const useDmeStore = create<MrtStore>(
|
||||||
syncTabs(
|
syncTabs(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
@@ -52,42 +54,52 @@ export const useDmeStore = create<MrtStore>(
|
|||||||
},
|
},
|
||||||
lines: [
|
lines: [
|
||||||
{
|
{
|
||||||
textLeft: "VAR.#",
|
textLeft: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
textMid: "VAR . DME# No Data",
|
||||||
textSize: "2",
|
textSize: "2",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
textLeft: "No Data",
|
textLeft: "",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
setLines: (lines) => set({ lines }),
|
setLines: (lines) => set({ lines }),
|
||||||
setPage: (pageData) => {
|
setPage: (pageData) => {
|
||||||
|
if (interval) clearInterval(interval);
|
||||||
switch (pageData.page) {
|
switch (pageData.page) {
|
||||||
case "home": {
|
case "home": {
|
||||||
set({
|
const setHomePage = () =>
|
||||||
page: "home",
|
set({
|
||||||
lines: [
|
page: "home",
|
||||||
{ textMid: "⠀" },
|
lines: [
|
||||||
{
|
{ textMid: "⠀" },
|
||||||
textMid: pageData.station.bosCallsign
|
{
|
||||||
? `VAR#.${pageData.station.bosCallsign}`
|
textMid: pageData.station.bosCallsign
|
||||||
: "no Data",
|
? `VAR#.${pageData.station.bosCallsign}`
|
||||||
style: { fontWeight: "bold" },
|
: "no Data",
|
||||||
},
|
style: { fontWeight: "bold" },
|
||||||
{ textMid: "⠀" },
|
},
|
||||||
{
|
{ textMid: "⠀" },
|
||||||
textMid: new Date().toLocaleDateString(),
|
{
|
||||||
},
|
textMid: new Date().toLocaleDateString(),
|
||||||
{
|
},
|
||||||
textMid: new Date().toLocaleTimeString(),
|
{
|
||||||
style: { fontWeight: "bold" },
|
textMid: new Date().toLocaleTimeString(),
|
||||||
},
|
style: { fontWeight: "bold" },
|
||||||
{ textMid: "⠀" },
|
},
|
||||||
{
|
{ textMid: "⠀" },
|
||||||
textMid: `${pageData.user.lastname} ${pageData.user.firstname}`,
|
{
|
||||||
},
|
textMid: `${pageData.user.lastname} ${pageData.user.firstname}`,
|
||||||
{ textMid: "⠀" },
|
},
|
||||||
],
|
{ textMid: "⠀" },
|
||||||
});
|
],
|
||||||
|
});
|
||||||
|
setHomePage();
|
||||||
|
|
||||||
|
interval = setInterval(() => {
|
||||||
|
setHomePage();
|
||||||
|
}, 1000);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,16 @@ export const useButtons = () => {
|
|||||||
const handleButton = (button: "main" | "menu" | "left" | "right") => () => {
|
const handleButton = (button: "main" | "menu" | "left" | "right") => () => {
|
||||||
switch (button) {
|
switch (button) {
|
||||||
case "main":
|
case "main":
|
||||||
if (page === "mission") {
|
if (page === "mission" || page === "new-mission") {
|
||||||
setPage({ page: "acknowledge" });
|
setPage({ page: "acknowledge" });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "menu":
|
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) {
|
if (station && user) {
|
||||||
setPage({ page: "home", station, user });
|
setPage({ page: "home", station, user });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -130,16 +130,20 @@ export const ConnectionBtn = () => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
onSubmit={() => false}
|
onSubmit={() => false}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
connection.connect(
|
const selectedStation = stations?.find(
|
||||||
uid,
|
(station) =>
|
||||||
form.selectedStationId?.toString() || "",
|
station.id ===
|
||||||
form.logoffTime || "",
|
parseInt(form.selectedStationId?.toString() || ""),
|
||||||
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"
|
className="btn btn-soft btn-info"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import { useState } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { unlinkDiscord, updateUser, changePassword } from "../actions";
|
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 { useRouter } from "next/navigation";
|
||||||
import { Button } from "../../../_components/ui/Button";
|
import { Button } from "../../../_components/ui/Button";
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +19,9 @@ import {
|
|||||||
LockOpen2Icon,
|
LockOpen2Icon,
|
||||||
LockOpen1Icon,
|
LockOpen1Icon,
|
||||||
} from "@radix-ui/react-icons";
|
} 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 }) => {
|
export const ProfileForm = ({ user }: { user: User }) => {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
@@ -242,7 +243,7 @@ export const SocialForm = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PasswordForm = ({ user }: { user: User }) => {
|
export const PasswordForm = () => {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
password: z.string().min(2).max(30),
|
password: z.string().min(2).max(30),
|
||||||
newPassword: z.string().min(2).max(30),
|
newPassword: z.string().min(2).max(30),
|
||||||
@@ -345,3 +346,64 @@ export const PasswordForm = ({ user }: { user: User }) => {
|
|||||||
</form>
|
</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,38 +1,48 @@
|
|||||||
import { getServerSession } from "../../api/auth/[...nextauth]/auth";
|
import { getServerSession } from "../../api/auth/[...nextauth]/auth";
|
||||||
import { PrismaClient } from "@repo/db";
|
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";
|
import { GearIcon } from "@radix-ui/react-icons";
|
||||||
|
|
||||||
export default async () => {
|
export const page = async () => {
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
const session = await getServerSession();
|
const session = await getServerSession();
|
||||||
if (!session) return null;
|
if (!session) return null;
|
||||||
const user = await prisma.user.findFirst({
|
const user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: session.user.id,
|
id: session.user.id,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
discordAccounts: true,
|
discordAccounts: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
const discordAccount = user?.discordAccounts[0];
|
const discordAccount = user?.discordAccounts[0];
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-6 gap-4">
|
<div className="grid grid-cols-6 gap-4">
|
||||||
<div className="col-span-full">
|
<div className="col-span-full">
|
||||||
<p className="text-2xl font-semibold text-left flex items-center gap-2">
|
<p className="text-2xl font-semibold text-left flex items-center gap-2">
|
||||||
<GearIcon className="w-5 h-5" /> Einstellungen
|
<GearIcon className="w-5 h-5" /> Einstellungen
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
|
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
|
||||||
<ProfileForm user={user} />
|
<ProfileForm user={user} />
|
||||||
</div>
|
</div>
|
||||||
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
|
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
|
||||||
<SocialForm discordAccount={discordAccount} user={user} />
|
<SocialForm discordAccount={discordAccount} user={user} />
|
||||||
</div>
|
</div>
|
||||||
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
|
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
|
||||||
<PasswordForm user={user} />
|
<PasswordForm />
|
||||||
</div>
|
</div>
|
||||||
</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.
@@ -31,6 +31,7 @@ model User {
|
|||||||
emailVerified DateTime? @map(name: "email_verified")
|
emailVerified DateTime? @map(name: "email_verified")
|
||||||
|
|
||||||
// Settings:
|
// Settings:
|
||||||
|
settingsNtfyRoom String? @map(name: "settings_ntfy_room")
|
||||||
|
|
||||||
image String?
|
image String?
|
||||||
badges BADGES[] @default([])
|
badges BADGES[] @default([])
|
||||||
|
|||||||
Reference in New Issue
Block a user