343 lines
12 KiB
TypeScript
343 lines
12 KiB
TypeScript
"use client";
|
|
import { PublicUser } from "@repo/db";
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { PenaltyDropdown } from "@repo/shared-components";
|
|
import { getConnectedAircraftsAPI, kickAircraftAPI } from "_querys/aircrafts";
|
|
import { getConnectedDispatcherAPI, kickDispatcherAPI } from "_querys/dispatcher";
|
|
import { getLivekitRooms, kickLivekitParticipant } from "_querys/livekit";
|
|
import { ParticipantInfo } from "livekit-server-sdk";
|
|
import {
|
|
LockKeyhole,
|
|
Plane,
|
|
RedoDot,
|
|
Shield,
|
|
Speaker,
|
|
User,
|
|
UserCheck,
|
|
Workflow,
|
|
} from "lucide-react";
|
|
import { useRef } from "react";
|
|
import toast from "react-hot-toast";
|
|
|
|
export default function AdminPanel() {
|
|
const queryClient = useQueryClient();
|
|
const { data: pilots } = useQuery({
|
|
queryKey: ["pilots"],
|
|
queryFn: () => getConnectedAircraftsAPI(),
|
|
refetchInterval: 10000,
|
|
});
|
|
const { data: dispatcher } = useQuery({
|
|
queryKey: ["dispatcher"],
|
|
|
|
queryFn: () => getConnectedDispatcherAPI(),
|
|
refetchInterval: 10000,
|
|
});
|
|
const { data: livekitRooms } = useQuery({
|
|
queryKey: ["livekit-rooms"],
|
|
queryFn: () => getLivekitRooms(),
|
|
refetchInterval: 10000,
|
|
});
|
|
const kickLivekitParticipantMutation = useMutation({
|
|
mutationFn: kickLivekitParticipant,
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["livekit-rooms"] });
|
|
},
|
|
});
|
|
const kickPilotMutation = useMutation({
|
|
mutationFn: kickAircraftAPI,
|
|
onSuccess: () => {
|
|
toast.success("Pilot wurde erfolgreich gekickt");
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["aircrafts"],
|
|
});
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["connected-audio-users"],
|
|
});
|
|
},
|
|
});
|
|
const kickDispatchMutation = useMutation({
|
|
mutationFn: kickDispatcherAPI,
|
|
onSuccess: () => {
|
|
toast.success("Disponent wurde erfolgreich gekickt");
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["dispatcher"],
|
|
});
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["connected-audio-users"],
|
|
});
|
|
},
|
|
});
|
|
|
|
const participants: { participant: ParticipantInfo; room: string }[] = [];
|
|
|
|
if (livekitRooms) {
|
|
livekitRooms?.forEach((room) => {
|
|
room.participants.forEach((participant) => {
|
|
participants.push({
|
|
participant,
|
|
room: room.room.name,
|
|
});
|
|
});
|
|
});
|
|
}
|
|
const livekitUserNotConnected = participants.filter((p) => {
|
|
const pilot = pilots?.find(
|
|
(d) => (d.publicUser as unknown as PublicUser).publicId === p.participant.identity,
|
|
);
|
|
const fDispatcher = dispatcher?.find(
|
|
(d) => (d.publicUser as unknown as PublicUser).publicId === p.participant.identity,
|
|
);
|
|
return !pilot && !fDispatcher;
|
|
});
|
|
|
|
const modalRef = useRef<HTMLDialogElement>(null);
|
|
|
|
return (
|
|
<div>
|
|
<button
|
|
className="btn btn-soft btn-primary btn-sm flex items-center gap-2"
|
|
onSubmit={() => false}
|
|
onClick={() => {
|
|
modalRef.current?.showModal();
|
|
}}
|
|
>
|
|
<Shield size={18} /> Admin Panel
|
|
</button>
|
|
<dialog ref={modalRef} className="modal min-w-[500px]">
|
|
<div className="modal-box w-11/12 max-w-7xl">
|
|
<form method="dialog">
|
|
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
|
</form>
|
|
<h3 className="font-bold text-lg flex items-center gap-2">
|
|
<Shield size={22} /> Admin Panel
|
|
</h3>
|
|
<div className="flex gap-2 mt-4 w-full">
|
|
<div className="card bg-base-300 shadow-md w-full h-96 overflow-y-auto">
|
|
<div className="card-body">
|
|
<div className="card-title flex items-center gap-2">
|
|
<UserCheck size={20} /> Verbundene Clients
|
|
</div>
|
|
<table className="table w-full">
|
|
<thead>
|
|
<tr>
|
|
<th>VAR #</th>
|
|
<th>Name</th>
|
|
<th>Station</th>
|
|
<th>Voice</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{pilots?.map((p) => {
|
|
const publicUser = p.publicUser as unknown as PublicUser;
|
|
const livekitParticipant = participants.find(
|
|
(p) => p.participant.identity === publicUser.publicId,
|
|
);
|
|
return (
|
|
<tr key={p.id}>
|
|
<td className="flex items-center gap-2">
|
|
<Plane /> {publicUser.publicId}
|
|
</td>
|
|
<td>{publicUser.fullName}</td>
|
|
<td>{p.Station.bosCallsign}</td>
|
|
<td>
|
|
{!livekitParticipant ? (
|
|
<span className="text-error">Nicht verbunden</span>
|
|
) : (
|
|
<span className="text-success">{livekitParticipant.room}</span>
|
|
)}
|
|
</td>
|
|
<td className="flex gap-2">
|
|
<PenaltyDropdown
|
|
btnName="Verbindung trennen"
|
|
btnClassName="btn-warning"
|
|
btnTip="Die Verbindung zur Leitstelle wird für diesesn Nutzer unterbrochen"
|
|
Icon={<RedoDot size={15} />}
|
|
onClick={({ reason }) => {
|
|
if (!reason.length)
|
|
return toast.error("Bitte gib einen Grund für die Strafe an.");
|
|
kickPilotMutation.mutate({ id: p.id, reason });
|
|
}}
|
|
/>
|
|
<PenaltyDropdown
|
|
btnName="Kick + Berechtigungen entfernen"
|
|
btnClassName="btn-error tooltip-error"
|
|
btnTip="Dadurch wird sich der Pilot nicht mehr mit dem VAR verbinden können."
|
|
showDatePicker
|
|
Icon={<LockKeyhole size={15} />}
|
|
onClick={({ reason, until }) => {
|
|
if (!reason.length)
|
|
return toast.error("Bitte gib einen Grund für die Strafe an.");
|
|
if (!until) {
|
|
toast.error(
|
|
"Bitte wähle ein Datum aus. Ein permanenter Bann ist nur vom HUB aus möglich.",
|
|
);
|
|
return;
|
|
}
|
|
kickPilotMutation.mutate({ id: p.id, reason, bann: true, until });
|
|
}}
|
|
/>
|
|
<a
|
|
href={`${process.env.NEXT_PUBLIC_HUB_URL}/admin/user/${p.userId}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<button
|
|
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
|
data-tip="Profil"
|
|
>
|
|
<User size={15} />
|
|
</button>
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
{dispatcher?.map((d) => {
|
|
const publicUser = d.publicUser as unknown as PublicUser;
|
|
const livekitParticipant = participants.find(
|
|
(p) => p.participant.identity === publicUser.publicId,
|
|
);
|
|
return (
|
|
<tr key={d.id}>
|
|
<td className="flex items-center gap-2">
|
|
<Workflow /> {publicUser.publicId}
|
|
</td>
|
|
<td>{publicUser.fullName}</td>
|
|
<td>{d.zone}</td>
|
|
<td>
|
|
{!livekitParticipant ? (
|
|
<span className="text-error">Nicht verbunden</span>
|
|
) : (
|
|
<span className="text-success">{livekitParticipant.room}</span>
|
|
)}
|
|
</td>
|
|
<td className="flex gap-2">
|
|
<PenaltyDropdown
|
|
btnName="Verbindung trennen"
|
|
btnClassName="btn-warning"
|
|
btnTip="Die Verbindung zur Leitstelle wird für diesesn Nutzer unterbrochen"
|
|
Icon={<RedoDot size={15} />}
|
|
onClick={({ reason }) =>
|
|
kickDispatchMutation.mutate({ id: d.id, reason })
|
|
}
|
|
/>
|
|
<PenaltyDropdown
|
|
btnName="Kick + Berechtigungen entfernen"
|
|
btnClassName="btn-error tooltip-error"
|
|
btnTip="Dadurch wird sich der Pilot nicht mehr mit dem VAR verbinden können."
|
|
showDatePicker
|
|
Icon={<LockKeyhole size={15} />}
|
|
onClick={({ reason, until }) => {
|
|
if (!reason.length)
|
|
return toast.error("Bitte gib einen Grund für die Strafe an.");
|
|
if (!until) {
|
|
toast.error(
|
|
"Bitte wähle ein Datum aus. Ein permanenter Bann ist nur vom HUB aus möglich.",
|
|
);
|
|
return;
|
|
}
|
|
kickDispatchMutation.mutate({
|
|
id: d.id,
|
|
reason,
|
|
bann: true,
|
|
until,
|
|
});
|
|
}}
|
|
/>
|
|
<a
|
|
href={`${process.env.NEXT_PUBLIC_HUB_URL}/admin/user/${d.userId}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<button
|
|
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
|
data-tip="Profil"
|
|
>
|
|
<User size={15} />
|
|
</button>
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
{livekitUserNotConnected.map((p) => {
|
|
const publicUser = JSON.parse(
|
|
p.participant.attributes.publicUser || "{}",
|
|
) as PublicUser;
|
|
return (
|
|
<tr key={p.participant.identity}>
|
|
<td className="flex items-center gap-2">
|
|
<Speaker /> {p.participant.identity}
|
|
</td>
|
|
<td>{publicUser?.fullName}</td>
|
|
<td>
|
|
<span className="text-error">Nicht verbunden</span>
|
|
</td>
|
|
<td>
|
|
<span className="text-success">{p.room}</span>
|
|
</td>
|
|
<td className="flex gap-2">
|
|
<button
|
|
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
|
|
data-tip="Kick"
|
|
onClick={() =>
|
|
kickLivekitParticipantMutation.mutate({
|
|
roomName: p.room,
|
|
identity: p.participant.identity,
|
|
})
|
|
}
|
|
>
|
|
<RedoDot size={15} />
|
|
</button>
|
|
<a
|
|
href={`${process.env.NEXT_PUBLIC_HUB_URL}/admin/user/${p.participant.attributes.userId}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<button
|
|
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
|
data-tip="Profil"
|
|
>
|
|
<User size={15} />
|
|
</button>
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* <div className="card bg-base-300 shadow-md w-full mt-4 max-h-48 overflow-y-auto">
|
|
<div className="card-body">
|
|
<div className="card-title flex items-center gap-2">
|
|
<ShieldAlert size={20} /> Allgemeine Befehle
|
|
</div>
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className="flex items-center justify-left gap-2">
|
|
<button className="btn btn-soft btn-error mt-2">Kick Everybody</button>
|
|
<button className="btn btn-soft btn-warning mt-2">
|
|
Wartungsmodus einschalten
|
|
</button>
|
|
<button className="btn btn-soft btn-info mt-2">Delete All Missions</button>
|
|
</div>
|
|
|
|
<button className="btn btn-outline btn-info mt-2 flex items-center gap-2">
|
|
<Eye size={22} />
|
|
Als Observer verbinden
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div> */}
|
|
</div>
|
|
<form method="dialog" className="modal-backdrop">
|
|
<button>close</button>
|
|
</form>
|
|
</dialog>
|
|
</div>
|
|
);
|
|
}
|