sync all form inputs and presence

This commit is contained in:
Julian Krauser 2025-03-01 17:07:11 +01:00
parent d9ca5e3102
commit be473c7e75
5 changed files with 139 additions and 28 deletions

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="w-full"> <div class="w-full">
<Combobox v-model="selected" by="id"> <Combobox v-model="selected">
<ComboboxLabel>{{ title }}</ComboboxLabel> <ComboboxLabel>{{ title }}</ComboboxLabel>
<div class="relative mt-1"> <div class="relative mt-1">
<div <div
@ -8,10 +8,7 @@
> >
<ComboboxInput <ComboboxInput
class="w-full border-none py-2 pl-3 pr-10 text-sm leading-5 text-gray-900 focus:ring-0" class="w-full border-none py-2 pl-3 pr-10 text-sm leading-5 text-gray-900 focus:ring-0"
:displayValue=" :displayValue="(force) => (selectedForce?.firstname ?? '') + ' ' + (selectedForce?.lastname ?? '')"
(force) =>
((force as ForceViewModel)?.firstname ?? '') + ' ' + ((force as ForceViewModel)?.lastname ?? '')
"
@input="query = $event.target.value" @input="query = $event.target.value"
/> />
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2"> <ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
@ -45,7 +42,7 @@
v-for="person in filtered" v-for="person in filtered"
as="template" as="template"
:key="person.id" :key="person.id"
:value="person" :value="person.id"
v-slot="{ selected, active }" v-slot="{ selected, active }"
> >
<li <li
@ -120,6 +117,9 @@ export default defineComponent({
this.$emit("update:model-value", val); this.$emit("update:model-value", val);
}, },
}, },
selectedForce() {
return this.availableForces.find((af) => af.id == this.selected);
},
filtered() { filtered() {
return this.query == "" return this.query == ""
? this.availableForces ? this.availableForces

View file

@ -56,9 +56,7 @@ export const useMissionDetailStore = defineStore("missionDetail", {
}); });
connectionStore.connection?.on("package-sync-awareness", (data) => { connectionStore.connection?.on("package-sync-awareness", (data) => {
// if (this.awareness != undefined) { // TODO self implement where users edit what and cursors
// AwarenessProtocol.applyAwarenessUpdate(this.awareness, new Uint8Array(data.update), this);
// }
}); });
this.joinDocument(); this.joinDocument();
@ -69,7 +67,7 @@ export const useMissionDetailStore = defineStore("missionDetail", {
const connectionStore = useConnectionStore(); const connectionStore = useConnectionStore();
if (connectionStore.connected) { if (connectionStore.connected) {
connectionStore.connection?.emit("mission:sync-client-updates", { connectionStore.connection?.emit("mission:sync-client-updates", {
update: Array.from(update), update: Uint8Array.from(update),
timestamp: Date.now(), timestamp: Date.now(),
}); });
} }
@ -88,7 +86,7 @@ export const useMissionDetailStore = defineStore("missionDetail", {
const connectionStore = useConnectionStore(); const connectionStore = useConnectionStore();
connectionStore.connection?.emit( connectionStore.connection?.emit(
"mission:sync-client-awareness", "mission:sync-client-awareness",
Array.from(AwarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients)) Uint8Array.from(AwarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients))
); );
} }
}); });

View file

@ -5,47 +5,47 @@
<input type="text" id="title" v-model="title" /> <input type="text" id="title" v-model="title" />
</div> </div>
<div class="flex flex-col sm:flex-row gap-2"> <div class="flex flex-col sm:flex-row gap-2">
<ForceSelect title="Einsatzleiter" :available-forces="availableForces" /> <ForceSelect title="Einsatzleiter" :available-forces="availableForces" v-model="command" />
<ForceSelect title="Bericht Ersteller" :available-forces="availableForces" /> <ForceSelect title="Bericht Ersteller" :available-forces="availableForces" v-model="secretary" />
</div> </div>
<div class="flex flex-col sm:flex-row gap-2"> <div class="flex flex-col sm:flex-row gap-2">
<div class="grow"> <div class="grow">
<label for="start">Einsatzbeginn</label> <label for="start">Einsatzbeginn</label>
<input type="datetime-local" id="start" /> <input type="datetime-local" id="start" v-model="start" />
</div> </div>
<div class="grow"> <div class="grow">
<label for="end">Einsatzende</label> <label for="end">Einsatzende</label>
<input type="datetime-local" id="end" /> <input type="datetime-local" id="end" v-model="end" :min="start" />
</div> </div>
<div class="w-full sm:w-fit min-w-fit"> <div class="w-full sm:w-fit min-w-fit">
<p>Dauer</p> <p>Dauer</p>
<p <p
class="rounded-md shadow-sm relative block w-full sm:w-fit px-3 py-2 border border-gray-300 text-gray-900 sm:text-sm" class="rounded-md shadow-sm relative block w-full sm:w-fit px-3 py-2 border border-gray-300 text-gray-900 sm:text-sm"
> >
00h 00m <span v-if="duration.days != '00'">{{ duration.days }}d</span> {{ duration.hours }}h {{ duration.minutes }}m
</p> </p>
</div> </div>
</div> </div>
<div> <div>
<label for="mission_short">Stichwort</label> <label for="mission_short">Stichwort</label>
<input type="text" id="mission_short" /> <input type="text" id="mission_short" v-model="mission_short" />
</div> </div>
<div> <div>
<label for="location">Einsatzort</label> <label for="location">Einsatzort</label>
<input type="text" id="title" /> <input type="text" id="location" v-model="location" />
</div> </div>
<div> <div>
<label for="title">Weitere Anwesende (andere Wehren, Polizei, Rettungsdienst)</label> <label for="others">Weitere Anwesende (andere Wehren, Polizei, Rettungsdienst)</label>
<input type="text" id="title" /> <input type="text" id="others" v-model="others" />
</div> </div>
<div class="flex flex-col sm:flex-row gap-2"> <div class="flex flex-col sm:flex-row gap-2">
<div class="w-full"> <div class="w-full">
<label for="rescued">Anzahl getretteter Personen</label> <label for="rescued">Anzahl getretteter Personen</label>
<input type="number" id="rescued" value="0" /> <input type="number" id="rescued" min="0" v-model="rescued" />
</div> </div>
<div class="w-full"> <div class="w-full">
<label for="recovered">Anzahl geborgener Personen</label> <label for="recovered">Anzahl geborgener Personen</label>
<input type="number" id="recovered" value="0" /> <input type="number" id="recovered" min="0" v-model="recovered" />
</div> </div>
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
@ -111,12 +111,98 @@ export default defineComponent({
...mapState(useForceStore, ["availableForces"]), ...mapState(useForceStore, ["availableForces"]),
title: { title: {
get() { get() {
return this.document.getMap("form").get("title"); return this.document.getMap("form").get("title") as string;
}, },
set(val: string) { set(val: string) {
this.document.getMap("form").set("title", val); this.document.getMap("form").set("title", val);
}, },
}, },
command: {
get() {
return this.document.getMap("form").get("command") as string;
},
set(val: string) {
this.document.getMap("form").set("command", val);
},
},
secretary: {
get() {
return this.document.getMap("form").get("secretary") as string;
},
set(val: string) {
this.document.getMap("form").set("secretary", val);
},
},
start: {
get() {
return (
(this.document.getMap("form").get("start") as string) ||
new Date(new Date().setHours(new Date().getHours() + 1)).toISOString().slice(0, -8)
);
},
set(val: string) {
this.document.getMap("form").set("start", val);
},
},
end: {
get() {
return this.document.getMap("form").get("end") as string;
},
set(val: string) {
this.document.getMap("form").set("end", val);
},
},
duration() {
const diffInMs = new Date(this.end).getTime() - new Date(this.start).getTime();
const durationDate = new Date(diffInMs || 0);
return {
days: (durationDate.getUTCDate() - 1).toString().padStart(2, "0"),
hours: durationDate.getUTCHours().toString().padStart(2, "0"),
minutes: durationDate.getUTCMinutes().toString().padStart(2, "0"),
};
},
mission_short: {
get() {
return this.document.getMap("form").get("mission_short");
},
set(val: string) {
this.document.getMap("form").set("mission_short", val);
},
},
location: {
get() {
return this.document.getMap("form").get("location");
},
set(val: string) {
this.document.getMap("form").set("location", val);
},
},
others: {
get() {
return this.document.getMap("form").get("others");
},
set(val: string) {
this.document.getMap("form").set("others", val);
},
},
rescued: {
get() {
return this.document.getMap("form").get("rescued") || 0;
},
set(val: number) {
this.document.getMap("form").set("rescued", val);
},
},
recovered: {
get() {
return this.document.getMap("form").get("recovered") || 0;
},
set(val: number) {
this.document.getMap("form").set("recovered", val);
},
},
editor() { editor() {
return this.document.getText("editor"); return this.document.getText("editor");
}, },
@ -130,7 +216,7 @@ export default defineComponent({
methods: { methods: {
initEditor(quill: Quill) { initEditor(quill: Quill) {
quill.history.clear(); quill.history.clear();
this.binding = new QuillBinding(this.editor, quill, this.awareness); this.binding = new QuillBinding(this.document.getText("editor"), quill); //this.awareness
this.cursors = quill.getModule("cursors") as QuillCursors; this.cursors = quill.getModule("cursors") as QuillCursors;
}, },
}, },

View file

@ -28,7 +28,7 @@
</div> </div>
<MissionDetail v-show="routeHash == '#edit'" :document="yDoc" :awareness="awareness" /> <MissionDetail v-show="routeHash == '#edit'" :document="yDoc" :awareness="awareness" />
<MissionPresence v-show="routeHash == '#presence'" /> <MissionPresence v-show="routeHash == '#presence'" :document="yDoc" />
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="flex flex-col gap-2 h-full w-full overflow-hidden"> <div class="flex flex-col gap-2 h-full w-full overflow-hidden">
<Combobox v-model="selected" by="id" multiple> <Combobox v-model="presence" multiple>
<div <div
class="rounded-md shadow-sm relative block w-full border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none" class="rounded-md shadow-sm relative block w-full border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
> >
@ -33,7 +33,7 @@
v-for="person in filtered" v-for="person in filtered"
as="template" as="template"
:key="person.id" :key="person.id"
:value="person" :value="person.id"
v-slot="{ selected, active }" v-slot="{ selected, active }"
> >
<li <li
@ -67,18 +67,45 @@ import { Combobox, ComboboxInput, ComboboxButton, ComboboxOptions, ComboboxOptio
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid"; import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { useForceStore } from "@/stores/admin/configuration/force"; import { useForceStore } from "@/stores/admin/configuration/force";
import { mapState } from "pinia"; import { mapState } from "pinia";
import * as Y from "yjs";
</script> </script>
<script lang="ts"> <script lang="ts">
export default defineComponent({ export default defineComponent({
props: {
document: {
type: Object as PropType<Y.Doc>,
required: true,
},
},
data() { data() {
return { return {
query: "" as string, query: "" as string,
selected: [],
}; };
}, },
computed: { computed: {
...mapState(useForceStore, ["availableForces"]), ...mapState(useForceStore, ["availableForces"]),
presence: {
get() {
return this.document.getArray<string>("presence").toArray();
},
set(val: Array<string>) {
let added = val.filter((e) => !this.document.getArray<string>("presence").toArray().includes(e));
let removed = this.document
.getArray<string>("presence")
.toArray()
.filter((e) => !val.includes(e));
console.log(added, removed);
this.document.getArray("presence").push(added);
removed.forEach((r) => {
let index = this.document
.getArray<string>("presence")
.toArray()
.findIndex((a) => a == r);
this.document.getArray("presence").delete(index, 1);
});
},
},
filtered() { filtered() {
return this.query == "" return this.query == ""
? this.availableForces ? this.availableForces