Nachalarmieren select

Alarmieren aus Einsatz erstellen Maske
Map-Tiles
SDS sound: Status J
SDS Nachricht: public-User
Audio: Es kann nur ein Nutzer gleichzeitig Funken
Select in Report und Chat: default value -> OnChange
This commit is contained in:
PxlLoewe
2025-06-09 01:10:39 -07:00
parent 1f8d9f1b72
commit ea78b41510
12 changed files with 114 additions and 195 deletions

View File

@@ -27,6 +27,7 @@ export const Audio = () => {
speakingParticipants,
isTalking,
toggleTalking,
transmitBlocked,
connect,
state,
connectionQuality,
@@ -41,6 +42,7 @@ export const Audio = () => {
isReceiving: speakingParticipants.length > 0,
isTransmitting: isTalking,
unpausedTracks: speakingParticipants,
transmitBlocked,
});
const { selectedStation, status: pilotState } = usePilotConnectionStore((state) => state);
@@ -154,6 +156,7 @@ export const Audio = () => {
"btn btn-sm btn-soft border-none hover:bg-inherit",
!isTalking && "bg-transparent hover:bg-sky-400/20",
isTalking && "bg-green-700 hover:bg-green-600",
transmitBlocked && "bg-yellow-500 hover:bg-yellow-500",
state === "disconnected" && "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",

View File

@@ -7,10 +7,12 @@ export const useSounds = ({
isReceiving,
isTransmitting,
unpausedTracks,
transmitBlocked,
}: {
isReceiving: boolean;
isTransmitting: boolean;
unpausedTracks: unknown[];
transmitBlocked?: boolean;
}) => {
const { room } = useAudioStore();
// Sounds as refs
@@ -56,6 +58,17 @@ export const useSounds = ({
}
}, [isReceiving, isTransmitting, soundConnectionStarted]);
useEffect(() => {
if (transmitBlocked && foreignCallBlocked.current) {
foreignCallBlocked.current.volume = 0.2;
foreignCallBlocked.current.currentTime = 0;
foreignCallBlocked.current.loop = true;
foreignCallBlocked.current.play().catch(() => {});
} else if (foreignCallBlocked.current) {
foreignCallBlocked.current.pause();
}
}, [transmitBlocked]);
useEffect(() => {
if (isTransmitting && connectionStart.current!.paused) {
ownCallStarted.current!.volume = 0.2;

View File

@@ -39,7 +39,7 @@ export const Chat = () => {
});
useEffect(() => {
if(!session.data?.user.id) return;
if (!session.data?.user.id) return;
setOwnId(session.data?.user.id);
}, [session.data?.user.id]);
@@ -47,7 +47,7 @@ export const Chat = () => {
const filteredAircrafts = aircrafts?.filter((a) => a.userId !== session.data?.user.id);
return (
<div className={cn("dropdown dropdown-right", chatOpen && "dropdown-open")}>
<div className={cn("dropdown dropdown-right dropdown-center", chatOpen && "dropdown-open")}>
<div className="indicator">
{Object.values(chats).some((c) => c.notification) && (
<span className="indicator-item status status-info"></span>
@@ -68,9 +68,9 @@ export const Chat = () => {
{chatOpen && (
<div
tabIndex={0}
className="dropdown-content card bg-base-200 w-150 shadow-md z-[1100] ml-2 border-1 border-primary"
className="dropdown-content card bg-base-200 w-150 shadow-md z-[1100] max-h-[400px] ml-2 border-1 border-primary"
>
<div className="card-body">
<div className="card-body overflow-y-auto">
<h2 className="inline-flex items-center gap-2 text-lg font-bold mb-2">
<ChatBubbleIcon /> Chat
</h2>
@@ -118,7 +118,7 @@ export const Chat = () => {
<span className="text-xl">+</span>
</button>
</div>
<div className="tabs tabs-lift">
<div className="tabs tabs-lift max-h-full">
{Object.keys(chats).map((userId) => {
const chat = chats[userId];
if (!chat) return null;
@@ -126,7 +126,6 @@ export const Chat = () => {
<Fragment key={userId}>
<input
type="radio"
name="my_tabs_3"
className="tab"
aria-label={`<${chat.name}>`}
checked={selectedChat === userId}
@@ -140,7 +139,7 @@ export const Chat = () => {
}
}}
/>
<div className="tab-content bg-base-100 border-base-300 p-6">
<div className="tab-content bg-base-100 border-base-300 p-6 overflow-y-auto">
{chat.messages.map((chatMessage) => {
const isSender = chatMessage.senderId === session.data?.user.id;
return (
@@ -173,6 +172,22 @@ export const Chat = () => {
onChange={(e) => {
setMessage(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
if (message.length < 1) return;
if (!selectedChat) return;
setSending(true);
sendMessage(selectedChat, message)
.then(() => {
setMessage("");
setSending(false);
})
.catch(() => {
setSending(false);
});
}
}}
value={message}
/>
</label>

View File

@@ -38,7 +38,9 @@ export const Report = () => {
const filteredAircrafts = aircrafts?.filter((a) => a.userId !== session.data?.user.id);
return (
<div className={cn("dropdown dropdown-right", reportTabOpen && "dropdown-open")}>
<div
className={cn("dropdown dropdown-right dropdown-center", reportTabOpen && "dropdown-open")}
>
<div className="indicator">
<button
className="btn btn-soft btn-sm btn-error"
@@ -70,12 +72,11 @@ export const Report = () => {
Keine Nutzer gefunden
</option>
)}
{filteredDispatcher?.length ||
(filteredAircrafts?.length && (
<option disabled value="default">
Nutzer auswählen
</option>
))}
{(filteredDispatcher?.length || filteredAircrafts?.length) && (
<option disabled value="default">
Nutzer auswählen
</option>
)}
{filteredDispatcher?.map((dispatcher) => (
<option key={dispatcher.userId} value={dispatcher.userId}>

View File

@@ -1,5 +1,6 @@
"use client";
import "leaflet/dist/leaflet.css";
import "./mapStyles.css";
import { useMapStore } from "_store/mapStore";
import { MapContainer } from "react-leaflet";
import { BaseMaps } from "_components/map/BaseMaps";
@@ -39,7 +40,7 @@ const Map = () => {
return (
<MapContainer
ref={ref}
className="flex-1"
className="flex-1 bg-base-200"
center={map.center}
zoom={map.zoom}
fadeAnimation={false}

View File

@@ -397,7 +397,9 @@ const SDSTab = ({
stationId: aircraft.Station.id,
station: aircraft.Station,
message: note,
user: getPublicUser(session.data!.user),
user: getPublicUser(session.data!.user, {
ignorePrivacy: true,
}),
},
},
})

View File

@@ -341,10 +341,10 @@ const Patientdetails = ({ mission }: { mission: Mission }) => {
const Rettungsmittel = ({ mission }: { mission: Mission }) => {
const queryClient = useQueryClient();
const [selectedStation, setSelectedStation] = useState<Station | "RTW" | "POL" | "FW" | null>(
const [selectedStation, setSelectedStation] = useState<number | "RTW" | "POL" | "FW" | null>(
null,
);
const { data: conenctedAircrafts } = useQuery({
const { data: connectedAircrafts } = useQuery({
queryKey: ["aircrafts"],
queryFn: getConnectedAircraftsAPI,
});
@@ -383,17 +383,6 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
queryFn: () => getStationsAPI(),
});
useEffect(() => {
if (allStations) {
const stationsNotItMission = allStations.filter(
(s) => !mission.missionStationIds.includes(s.id),
);
if (stationsNotItMission[0]) {
setSelectedStation(stationsNotItMission[0]);
}
}
}, [allStations, mission.missionStationIds]);
const sendAlertMutation = useMutation({
mutationKey: ["missions"],
mutationFn: ({
@@ -421,7 +410,7 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
label: station.bosCallsign,
value: station.id,
type: "station" as const,
isOnline: !!conenctedAircrafts?.find((a) => a.stationId === station.id),
isOnline: !!connectedAircrafts?.find((a) => a.stationId === station.id),
})) || []),
...(!mission.hpgFireEngineState || mission.hpgFireEngineState === "NOT_REQUESTED"
? [{ label: "Feuerwehr", value: "FW", type: "vehicle" as const }]
@@ -447,18 +436,6 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
return a.label.localeCompare(b.label);
});
useEffect(() => {
const firstOption = stationsOptions[0];
if (!firstOption) {
setSelectedStation(null);
} else if (firstOption.type === "station") {
const station = allStations?.find((s) => s.id === firstOption.value);
setSelectedStation(station ?? null);
} else {
setSelectedStation(firstOption.value as "RTW" | "POL" | "FW");
}
}, [stationsOptions, allStations]);
const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected";
const HPGVehicle = ({ state, name }: { state: HpgState; name: string }) => (
@@ -506,7 +483,7 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
</div>
<ul className="space-y-2 max-h-[300px] overflow-y-auto overflow-x-auto">
{missionStations?.map((station, index) => {
const connectedAircraft = conenctedAircrafts?.find(
const connectedAircraft = connectedAircrafts?.find(
(aircraft) => aircraft.stationId === station.id,
);
@@ -550,15 +527,15 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
<select
className="select select-sm select-primary select-bordered flex-1"
onChange={(e) => {
const selected = allStations?.find((s) => s.id.toString() === e.target.value);
if (selected) {
setSelectedStation(selected);
} else {
setSelectedStation(e.target.value as "RTW" | "POL" | "FW");
}
const value = e.target.value;
const parsedValue = !isNaN(Number(value)) ? parseInt(value, 10) : value;
setSelectedStation(parsedValue as number | "RTW" | "POL" | "FW" | null);
}}
value={typeof selectedStation === "string" ? selectedStation : selectedStation?.id}
value={selectedStation || "default"}
>
<option disabled value={"default"}>
Rettungsmittel auswählen
</option>
{stationsOptions.map((option) => (
<option
key={option.value}
@@ -582,18 +559,18 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
vehicleName: selectedStation,
});
} else {
if (!selectedStation?.id) return;
if (!selectedStation) return;
await updateMissionMutation.mutateAsync({
id: mission.id,
missionEdit: {
missionStationIds: {
push: selectedStation?.id,
push: selectedStation,
},
},
});
await sendAlertMutation.mutate({
id: mission.id,
stationId: selectedStation?.id ?? 0,
stationId: selectedStation,
});
}
}}

View File

@@ -1,106 +1,3 @@
.no-pointer {
cursor: unset;
}
.pointer {
cursor: pointer;
}
.leaflet-tooltip-aircraft {
padding: 0 !important;
border: 0 !important;
border-radius: 0 !important;
color: rgb(254, 254, 254) !important;
left: 35px !important;
top: 40px !important;
border: none !important;
box-shadow: none !important;
}
.leaflet-popup-content-wrapper {
background-color: var(--dark-background);
}
@keyframes fade-in {
from {
right: -20%;
opacity: 0;
}
to {
right: 0;
opacity: 1;
}
}
@keyframes fade-out {
from {
right: 0;
opacity: 1;
}
to {
right: -20%;
opacity: 0;
}
}
.leaflet-tooltip-aircraft:before {
padding: 0 !important;
border: 0 !important;
border-radius: 0 !important;
color: rgb(255, 255, 255) !important;
left: 0px !important;
top: 0px !important;
border: none !important;
box-shadow: none !important;
}
.custom-tooltip-bg {
background: transparent !important;
}
.custom-tooltip-bg:before {
background: transparent !important;
}
.no-pointer {
cursor: unset;
}
.pointer {
cursor: pointer;
}
.leaflet-tooltip-aircraft {
padding: 0 !important;
border: 0 !important;
background-color: var(--surface) !important;
color: var(--on-surface) !important;
}
.leaflet-tooltip-aircraft:before {
border-bottom-color: var(--dark-surface) !important;
}
.modal-box {
position: fixed;
bottom: 50%;
right: 50px;
background-color: rgba(255, 255, 255, 0.75);
border: 5px solid #243671;
border-radius: 8px;
padding: 10px;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
z-index: 1000;
max-width: 300px;
word-wrap: break-word;
overflow-wrap: break-word;
text-align: center;
}
.modal-box-close {
position: absolute;
top: 5px;
right: 5px;
font-size: 18px;
cursor: pointer;
color: #243671;
.leaflet-container {
background: var(--color-base-200) !important;
}

View File

@@ -20,13 +20,9 @@ export const handleTrackSubscribed = (
}
track.on("unmuted", () => {
useAudioStore.getState().addSpeakingParticipant(participant);
console.log("Track unmuted:", track);
});
track.on("muted", () => {
useAudioStore.getState().removeSpeakingParticipant(participant);
console.log("Track muted:", track);
});
if (track.kind === Track.Kind.Video || track.kind === Track.Kind.Audio) {
// attach it to a new HTMLVideoElement or HTMLAudioElement

View File

@@ -18,6 +18,7 @@ type TalkState = {
micDeviceId: string | null;
micVolume: number;
isTalking: boolean;
transmitBlocked: boolean;
removeMessage: () => void;
state: "connecting" | "connected" | "disconnected" | "error";
message: string | null;
@@ -40,12 +41,12 @@ const getToken = async (roomName: string) => {
export const useAudioStore = create<TalkState>((set, get) => ({
isTalking: false,
transmitBlocked: false,
message: null,
micDeviceId: null,
speakingParticipants: [],
micVolume: 1,
state: "disconnected",
source: "",
remoteParticipants: 0,
connectionQuality: ConnectionQuality.Unknown,
room: null,
@@ -61,39 +62,38 @@ export const useAudioStore = create<TalkState>((set, get) => ({
set({ message: null });
},
removeSpeakingParticipant: (participant) => {
const newSpeaktingParticipants = get().speakingParticipants.filter(
(p) => !(p.identity === participant.identity),
);
set((state) => ({
speakingParticipants: state.speakingParticipants.filter(
(p) => !(p.identity === participant.identity),
),
speakingParticipants: newSpeaktingParticipants,
}));
if (newSpeaktingParticipants.length === 0 && get().transmitBlocked) {
get().room?.localParticipant.setMicrophoneEnabled(true);
set({ transmitBlocked: false, message: null, isTalking: true });
}
},
setMic: (micDeviceId, micVolume) => {
set({ micDeviceId, micVolume });
},
toggleTalking: () => {
const { room, isTalking, micDeviceId, micVolume } = get();
const { room, isTalking, micDeviceId, micVolume, speakingParticipants } = get();
if (!room) return;
if (speakingParticipants.length > 0 && !isTalking) {
// Wenn andere sprechen, nicht reden
set({
message: "Rufgruppe besetzt",
transmitBlocked: true,
});
return;
}
// Todo: use micVolume
room.localParticipant.setMicrophoneEnabled(!isTalking, {
deviceId: micDeviceId ?? undefined,
});
if (!isTalking) {
// If old status was not talking, we need to emit the PTT event
if (pilotSocket.connected) {
pilotSocket.emit("ptt", {
shouldTransmit: true,
channel: room.name,
});
}
if (dispatchSocket.connected)
dispatchSocket.emit("ptt", {
shouldTransmit: true,
channel: room.name,
});
}
set((state) => ({ isTalking: !state.isTalking }));
set((state) => ({ isTalking: !state.isTalking, transmitBlocked: false }));
},
connect: async (roomName, role) => {
set({ state: "connecting" });
@@ -181,11 +181,21 @@ interface PTTData {
const handlePTT = (data: PTTData) => {
const { shouldTransmit, source } = data;
const { room } = useAudioStore.getState();
const { room, speakingParticipants } = useAudioStore.getState();
if (!room) return;
if (speakingParticipants.length > 0 && shouldTransmit) {
// Wenn andere sprechen, nicht reden
useAudioStore.setState({
message: "Rufgruppe besetzt",
transmitBlocked: true,
});
return;
}
useAudioStore.setState({
isTalking: shouldTransmit,
transmitBlocked: false,
});
if (shouldTransmit) {

View File

@@ -32,7 +32,7 @@ import { cn } from "_helpers/cn";
export const MissionForm = () => {
const { isEditingMission, editingMissionId, setEditingMission } = usePannelStore();
const queryClient = useQueryClient();
const { setSearchElements, searchElements } = useMapStore((s) => s);
const { setSearchElements, searchElements, setContextMenu } = useMapStore((s) => s);
const { data: keywords } = useQuery({
queryKey: ["keywords"],
@@ -169,6 +169,8 @@ export const MissionForm = () => {
}).catch((error) => {
toast.error(`Fehler beim Starten der HPG-Validierung: ${error.message}`);
});
} else if (alertWhenValid) {
await sendAlertMutation.mutateAsync(newMission.id);
}
return newMission;
} else {
@@ -447,12 +449,14 @@ export const MissionForm = () => {
onClick={form.handleSubmit(async (mission: MissionOptionalDefaults) => {
try {
const newMission = await saveMission(mission, {
createNewMission: true,
alertWhenValid: true,
});
if (!validationRequired) {
await sendAlertMutation.mutateAsync(newMission.id);
}
setSearchElements([]); // Reset search elements
setContextMenu(null);
toast.success(`Einsatz ${newMission.publicId} erstellt`);
form.reset();
setOpen(false);
} catch (error) {
if (error instanceof AxiosError) {
@@ -479,6 +483,7 @@ export const MissionForm = () => {
});
setSearchElements([]); // Reset search elements
setContextMenu(null);
toast.success(`Einsatz ${newMission.publicId} erstellt`);
form.reset();
setOpen(false);

View File

@@ -5,9 +5,7 @@ import { useEffect, useRef } from "react";
export const useSounds = () => {
const mrtState = useMrtStore((state) => state);
const { connectedAircraft, selectedStation } = usePilotConnectionStore(
(state) => state,
);
const { connectedAircraft, selectedStation } = usePilotConnectionStore((state) => state);
const setPage = useMrtStore((state) => state.setPage);
const MRTstatusSoundRef = useRef<HTMLAudioElement>(null);
@@ -16,9 +14,7 @@ export const useSounds = () => {
useEffect(() => {
if (typeof window !== "undefined") {
MRTstatusSoundRef.current = new Audio("/sounds/MRT-status.mp3");
MrtMessageReceivedSoundRef.current = new Audio(
"/sounds/MRT-message-received.mp3",
);
MrtMessageReceivedSoundRef.current = new Audio("/sounds/MRT-message-received.mp3");
MRTstatusSoundRef.current.onended = () => {
if (!selectedStation || !connectedAircraft?.fmsStatus) return;
setPage({
@@ -29,6 +25,7 @@ export const useSounds = () => {
};
MrtMessageReceivedSoundRef.current.onended = () => {
if (!selectedStation || !connectedAircraft?.fmsStatus) return;
if (mrtState.page === "sds") return;
setPage({
page: "home",
station: selectedStation,
@@ -36,7 +33,7 @@ export const useSounds = () => {
});
};
}
}, [connectedAircraft?.fmsStatus, selectedStation, setPage]);
}, [connectedAircraft?.fmsStatus, selectedStation, setPage, mrtState.page]);
const fmsStatus = connectedAircraft?.fmsStatus || "NaN";
@@ -48,6 +45,8 @@ export const useSounds = () => {
} else {
MRTstatusSoundRef.current?.play();
}
} else if (mrtState.page === "sds") {
MrtMessageReceivedSoundRef.current?.play();
}
}, [mrtState, fmsStatus, connectedAircraft, selectedStation]);
};