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

View file

@ -56,9 +56,7 @@ export const useMissionDetailStore = defineStore("missionDetail", {
});
connectionStore.connection?.on("package-sync-awareness", (data) => {
// if (this.awareness != undefined) {
// AwarenessProtocol.applyAwarenessUpdate(this.awareness, new Uint8Array(data.update), this);
// }
// TODO self implement where users edit what and cursors
});
this.joinDocument();
@ -69,7 +67,7 @@ export const useMissionDetailStore = defineStore("missionDetail", {
const connectionStore = useConnectionStore();
if (connectionStore.connected) {
connectionStore.connection?.emit("mission:sync-client-updates", {
update: Array.from(update),
update: Uint8Array.from(update),
timestamp: Date.now(),
});
}
@ -88,7 +86,7 @@ export const useMissionDetailStore = defineStore("missionDetail", {
const connectionStore = useConnectionStore();
connectionStore.connection?.emit(
"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" />
</div>
<div class="flex flex-col sm:flex-row gap-2">
<ForceSelect title="Einsatzleiter" :available-forces="availableForces" />
<ForceSelect title="Bericht Ersteller" :available-forces="availableForces" />
<ForceSelect title="Einsatzleiter" :available-forces="availableForces" v-model="command" />
<ForceSelect title="Bericht Ersteller" :available-forces="availableForces" v-model="secretary" />
</div>
<div class="flex flex-col sm:flex-row gap-2">
<div class="grow">
<label for="start">Einsatzbeginn</label>
<input type="datetime-local" id="start" />
<input type="datetime-local" id="start" v-model="start" />
</div>
<div class="grow">
<label for="end">Einsatzende</label>
<input type="datetime-local" id="end" />
<input type="datetime-local" id="end" v-model="end" :min="start" />
</div>
<div class="w-full sm:w-fit min-w-fit">
<p>Dauer</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"
>
00h 00m
<span v-if="duration.days != '00'">{{ duration.days }}d</span> {{ duration.hours }}h {{ duration.minutes }}m
</p>
</div>
</div>
<div>
<label for="mission_short">Stichwort</label>
<input type="text" id="mission_short" />
<input type="text" id="mission_short" v-model="mission_short" />
</div>
<div>
<label for="location">Einsatzort</label>
<input type="text" id="title" />
<input type="text" id="location" v-model="location" />
</div>
<div>
<label for="title">Weitere Anwesende (andere Wehren, Polizei, Rettungsdienst)</label>
<input type="text" id="title" />
<label for="others">Weitere Anwesende (andere Wehren, Polizei, Rettungsdienst)</label>
<input type="text" id="others" v-model="others" />
</div>
<div class="flex flex-col sm:flex-row gap-2">
<div class="w-full">
<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 class="w-full">
<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 class="flex flex-col">
@ -111,12 +111,98 @@ export default defineComponent({
...mapState(useForceStore, ["availableForces"]),
title: {
get() {
return this.document.getMap("form").get("title");
return this.document.getMap("form").get("title") as string;
},
set(val: string) {
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() {
return this.document.getText("editor");
},
@ -130,7 +216,7 @@ export default defineComponent({
methods: {
initEditor(quill: Quill) {
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;
},
},

View file

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

View file

@ -1,6 +1,6 @@
<template>
<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
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"
as="template"
:key="person.id"
:value="person"
:value="person.id"
v-slot="{ selected, active }"
>
<li
@ -67,18 +67,45 @@ import { Combobox, ComboboxInput, ComboboxButton, ComboboxOptions, ComboboxOptio
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { useForceStore } from "@/stores/admin/configuration/force";
import { mapState } from "pinia";
import * as Y from "yjs";
</script>
<script lang="ts">
export default defineComponent({
props: {
document: {
type: Object as PropType<Y.Doc>,
required: true,
},
},
data() {
return {
query: "" as string,
selected: [],
};
},
computed: {
...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() {
return this.query == ""
? this.availableForces