update
This commit is contained in:
1 parent
33b3975b61
commit
db77c1f985
10 files changed
+488
-298
No files matched your search
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { computed, reactive, ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
items: { type: Array, default: () => [] },
|
||||
@@ -7,272 +7,310 @@ const props = defineProps({
|
||||
host: Boolean,
|
||||
// Ohne gewählten Namen bleibt der Reiter zum Mitlesen da
|
||||
angemeldet: Boolean,
|
||||
})
|
||||
});
|
||||
|
||||
const emit = defineEmits(['changed'])
|
||||
const emit = defineEmits(["changed"]);
|
||||
|
||||
const entwurf = ref('')
|
||||
const entwurf = ref("");
|
||||
|
||||
// Der Host darf jeden Wunsch anfassen, alle anderen nur die eigenen
|
||||
const bearbeitet = ref(null)
|
||||
const bearbeitung = ref('')
|
||||
const bearbeitet = ref(null);
|
||||
const bearbeitung = ref("");
|
||||
|
||||
function darfAendern(item) {
|
||||
return props.host || item.mine
|
||||
return props.host || item.mine;
|
||||
}
|
||||
|
||||
function bearbeiten(item) {
|
||||
bearbeitet.value = item.id
|
||||
bearbeitung.value = item.text
|
||||
bearbeitet.value = item.id;
|
||||
bearbeitung.value = item.text;
|
||||
}
|
||||
|
||||
async function speichern() {
|
||||
const text = bearbeitung.value.trim()
|
||||
const text = bearbeitung.value.trim();
|
||||
if (text) {
|
||||
await fetch(`/api/features/${bearbeitet.value}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ text }),
|
||||
})
|
||||
emit('changed')
|
||||
});
|
||||
emit("changed");
|
||||
}
|
||||
bearbeitet.value = null
|
||||
bearbeitet.value = null;
|
||||
}
|
||||
|
||||
// Die Liste kommt vom Server schon sortiert: offene nach Stimmen, dann
|
||||
// die erledigten. Hier nur noch in zwei Abschnitte teilen.
|
||||
const offene = computed(() => props.items.filter((w) => !w.done_at))
|
||||
const erledigte = computed(() => props.items.filter((w) => w.done_at))
|
||||
const offene = computed(() => props.items.filter((w) => !w.done_at));
|
||||
const erledigte = computed(() => props.items.filter((w) => w.done_at));
|
||||
|
||||
async function anlegen() {
|
||||
if (!entwurf.value.trim()) return
|
||||
await fetch('/api/features', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
if (!entwurf.value.trim()) return;
|
||||
await fetch("/api/features", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ text: entwurf.value }),
|
||||
})
|
||||
entwurf.value = ''
|
||||
emit('changed')
|
||||
});
|
||||
entwurf.value = "";
|
||||
emit("changed");
|
||||
}
|
||||
|
||||
async function loeschen(item) {
|
||||
await fetch(`/api/features/${item.id}`, { method: 'DELETE' })
|
||||
emit('changed')
|
||||
await fetch(`/api/features/${item.id}`, { method: "DELETE" });
|
||||
emit("changed");
|
||||
}
|
||||
|
||||
async function abstimmen(item) {
|
||||
await fetch(`/api/features/${item.id}/vote`, { method: 'POST' })
|
||||
emit('changed')
|
||||
await fetch(`/api/features/${item.id}/vote`, { method: "POST" });
|
||||
emit("changed");
|
||||
}
|
||||
|
||||
async function erledigen(item, done) {
|
||||
await fetch(`/api/features/${item.id}/done`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ done }),
|
||||
})
|
||||
emit('changed')
|
||||
});
|
||||
emit("changed");
|
||||
}
|
||||
|
||||
// Ergänzungen: aufgeklappt je Wunsch, mit eigenem Entwurf
|
||||
const aufgeklappt = reactive(new Set())
|
||||
const notizEntwurf = reactive({})
|
||||
const aufgeklappt = reactive(new Set());
|
||||
const notizEntwurf = reactive({});
|
||||
|
||||
function umschalten(id) {
|
||||
if (aufgeklappt.has(id)) aufgeklappt.delete(id)
|
||||
else aufgeklappt.add(id)
|
||||
if (aufgeklappt.has(id)) aufgeklappt.delete(id);
|
||||
else aufgeklappt.add(id);
|
||||
}
|
||||
|
||||
function ergaenzungsLabel(item) {
|
||||
const n = item.notes.length
|
||||
if (!n) return 'Ergänzen'
|
||||
return n === 1 ? '1 Ergänzung' : `${n} Ergänzungen`
|
||||
const n = item.notes.length;
|
||||
if (!n) return "Ergänzen";
|
||||
return n === 1 ? "1 Ergänzung" : `${n} Ergänzungen`;
|
||||
}
|
||||
|
||||
async function anmerken(item) {
|
||||
const text = (notizEntwurf[item.id] || '').trim()
|
||||
if (!text) return
|
||||
const text = (notizEntwurf[item.id] || "").trim();
|
||||
if (!text) return;
|
||||
await fetch(`/api/features/${item.id}/notes`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ text }),
|
||||
})
|
||||
notizEntwurf[item.id] = ''
|
||||
emit('changed')
|
||||
});
|
||||
notizEntwurf[item.id] = "";
|
||||
emit("changed");
|
||||
}
|
||||
|
||||
async function notizLoeschen(notiz) {
|
||||
await fetch(`/api/feature-notes/${notiz.id}`, { method: 'DELETE' })
|
||||
emit('changed')
|
||||
await fetch(`/api/feature-notes/${notiz.id}`, { method: "DELETE" });
|
||||
emit("changed");
|
||||
}
|
||||
|
||||
function datum(iso) {
|
||||
return new Date(iso).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
})
|
||||
return new Date(iso).toLocaleDateString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wuensche-tab">
|
||||
<div v-if="angemeldet" class="neu">
|
||||
<textarea
|
||||
v-model="entwurf"
|
||||
rows="2"
|
||||
maxlength="500"
|
||||
placeholder="Was fehlt dir auf der Seite?"
|
||||
@keydown.ctrl.enter="anlegen"
|
||||
></textarea>
|
||||
<button type="button" :disabled="!entwurf.trim()" @click="anlegen">
|
||||
Eintragen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="!items.length" class="note">Noch keine Wünsche.</p>
|
||||
|
||||
<section v-for="[titel, liste] in [['Offen', offene], ['Erledigt', erledigte]]" :key="titel">
|
||||
<template v-if="liste.length">
|
||||
<h3>
|
||||
{{ titel }}
|
||||
<span class="anzahl">{{ liste.length }}</span>
|
||||
</h3>
|
||||
<!-- Links das Offene samt Eingabe, rechts das Erledigte -->
|
||||
<div class="spalten">
|
||||
<div v-if="angemeldet" class="neu">
|
||||
<textarea
|
||||
v-model="entwurf"
|
||||
rows="2"
|
||||
maxlength="500"
|
||||
placeholder="Was fehlt dir auf der Seite?"
|
||||
@keydown.ctrl.enter="anlegen"
|
||||
></textarea>
|
||||
<button type="button" :disabled="!entwurf.trim()" @click="anlegen">
|
||||
Eintragen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul class="liste">
|
||||
<li v-for="w in liste" :key="w.id" :class="{ erledigt: w.done_at }">
|
||||
<!-- Eine Stimme je Person; nochmal klicken nimmt sie zurück -->
|
||||
<button
|
||||
type="button"
|
||||
class="stimme"
|
||||
:class="{ an: w.voted, hat: w.votes > 0 }"
|
||||
:disabled="!angemeldet"
|
||||
:title="w.voted ? 'Stimme zurücknehmen' : 'Dafür stimmen'"
|
||||
@click="abstimmen(w)"
|
||||
>
|
||||
▲ {{ w.votes }}
|
||||
</button>
|
||||
<section
|
||||
v-for="[titel, liste] in [
|
||||
['Offen', offene],
|
||||
['Erledigt', erledigte],
|
||||
]"
|
||||
:key="titel"
|
||||
:class="titel === 'Offen' ? 'spalte-offen' : 'spalte-erledigt'"
|
||||
>
|
||||
<template v-if="liste.length">
|
||||
<h3>
|
||||
{{ titel }}
|
||||
<span class="anzahl">{{ liste.length }}</span>
|
||||
</h3>
|
||||
|
||||
<div class="inhalt">
|
||||
<template v-if="bearbeitet === w.id">
|
||||
<textarea
|
||||
v-model="bearbeitung"
|
||||
rows="2"
|
||||
maxlength="500"
|
||||
@keydown.ctrl.enter="speichern"
|
||||
@keydown.esc="bearbeitet = null"
|
||||
></textarea>
|
||||
<div class="zeile">
|
||||
<button type="button" class="klein" @click="speichern">Speichern</button>
|
||||
<button type="button" class="klein" @click="bearbeitet = null">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<ul class="liste">
|
||||
<li v-for="w in liste" :key="w.id" :class="{ erledigt: w.done_at }">
|
||||
<!-- Eine Stimme je Person; nochmal klicken nimmt sie zurück -->
|
||||
<button
|
||||
type="button"
|
||||
class="stimme"
|
||||
:class="{ an: w.voted, hat: w.votes > 0 }"
|
||||
:disabled="!angemeldet"
|
||||
:title="w.voted ? 'Stimme zurücknehmen' : 'Dafür stimmen'"
|
||||
@click="abstimmen(w)"
|
||||
>
|
||||
▲ {{ w.votes }}
|
||||
</button>
|
||||
|
||||
<template v-else>
|
||||
<p class="text">{{ w.text }}</p>
|
||||
<p class="meta">
|
||||
<span :class="{ eigen: w.mine }">{{ w.name || 'Anonym' }}</span>
|
||||
<span class="punkt">·</span>{{ datum(w.created_at) }}
|
||||
<template v-if="w.done_at">
|
||||
<span class="punkt">·</span>erledigt {{ datum(w.done_at) }}
|
||||
</template>
|
||||
<span class="punkt">·</span>
|
||||
<button
|
||||
type="button"
|
||||
class="link"
|
||||
:class="{ offen: aufgeklappt.has(w.id) }"
|
||||
@click="umschalten(w.id)"
|
||||
>
|
||||
{{ ergaenzungsLabel(w) }}
|
||||
</button>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<div v-if="aufgeklappt.has(w.id)" class="notizen">
|
||||
<p v-for="n in w.notes" :key="n.id" class="notiz">
|
||||
<span class="wer" :class="{ eigen: n.mine }">{{ n.name || 'Anonym' }}</span>
|
||||
<span class="notiz-text">{{ n.text }}</span>
|
||||
<button
|
||||
v-if="host"
|
||||
type="button"
|
||||
class="weg sichtbar"
|
||||
title="Ergänzung entfernen"
|
||||
@click="notizLoeschen(n)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</p>
|
||||
<div v-if="angemeldet" class="notiz-neu">
|
||||
<input
|
||||
v-model="notizEntwurf[w.id]"
|
||||
type="text"
|
||||
<div class="inhalt">
|
||||
<template v-if="bearbeitet === w.id">
|
||||
<textarea
|
||||
v-model="bearbeitung"
|
||||
rows="2"
|
||||
maxlength="500"
|
||||
placeholder="Ergänzung …"
|
||||
@keydown.enter="anmerken(w)"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="klein"
|
||||
:disabled="!(notizEntwurf[w.id] || '').trim()"
|
||||
@click="anmerken(w)"
|
||||
>
|
||||
Anhängen
|
||||
</button>
|
||||
@keydown.ctrl.enter="speichern"
|
||||
@keydown.esc="bearbeitet = null"
|
||||
></textarea>
|
||||
<div class="zeile">
|
||||
<button type="button" class="klein" @click="speichern">
|
||||
Speichern
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="klein"
|
||||
@click="bearbeitet = null"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<p class="text">{{ w.text }}</p>
|
||||
<p class="meta">
|
||||
<span :class="{ eigen: w.mine }">{{
|
||||
w.name || "Anonym"
|
||||
}}</span>
|
||||
<span class="punkt">·</span>{{ datum(w.created_at) }}
|
||||
<template v-if="w.done_at">
|
||||
<span class="punkt">·</span>erledigt
|
||||
{{ datum(w.done_at) }}
|
||||
</template>
|
||||
<span class="punkt">·</span>
|
||||
<button
|
||||
type="button"
|
||||
class="link"
|
||||
:class="{ offen: aufgeklappt.has(w.id) }"
|
||||
@click="umschalten(w.id)"
|
||||
>
|
||||
{{ ergaenzungsLabel(w) }}
|
||||
</button>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<div v-if="aufgeklappt.has(w.id)" class="notizen">
|
||||
<p v-for="n in w.notes" :key="n.id" class="notiz">
|
||||
<span class="wer" :class="{ eigen: n.mine }">{{
|
||||
n.name || "Anonym"
|
||||
}}</span>
|
||||
<span class="notiz-text">{{ n.text }}</span>
|
||||
<button
|
||||
v-if="host"
|
||||
type="button"
|
||||
class="weg sichtbar"
|
||||
title="Ergänzung entfernen"
|
||||
@click="notizLoeschen(n)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</p>
|
||||
<div v-if="angemeldet" class="notiz-neu">
|
||||
<input
|
||||
v-model="notizEntwurf[w.id]"
|
||||
type="text"
|
||||
maxlength="500"
|
||||
placeholder="Ergänzung …"
|
||||
@keydown.enter="anmerken(w)"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="klein"
|
||||
:disabled="!(notizEntwurf[w.id] || '').trim()"
|
||||
@click="anmerken(w)"
|
||||
>
|
||||
Anhängen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Erst beim Überfahren sichtbar, sonst ist jede Zeile voller Knöpfe -->
|
||||
<div class="aktionen">
|
||||
<button
|
||||
v-if="host"
|
||||
type="button"
|
||||
class="weg"
|
||||
:title="w.done_at ? 'Wieder öffnen' : 'Als erledigt markieren'"
|
||||
@click="erledigen(w, !w.done_at)"
|
||||
>
|
||||
{{ w.done_at ? '↺' : '✓' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="darfAendern(w) && bearbeitet !== w.id"
|
||||
type="button"
|
||||
class="weg"
|
||||
title="Wunsch bearbeiten"
|
||||
@click="bearbeiten(w)"
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
<button
|
||||
v-if="darfAendern(w)"
|
||||
type="button"
|
||||
class="weg"
|
||||
title="Wunsch entfernen"
|
||||
@click="loeschen(w)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</section>
|
||||
<!-- Erst beim Überfahren sichtbar, sonst ist jede Zeile voller Knöpfe -->
|
||||
<div class="aktionen">
|
||||
<button
|
||||
v-if="host"
|
||||
type="button"
|
||||
class="weg"
|
||||
:title="
|
||||
w.done_at ? 'Wieder öffnen' : 'Als erledigt markieren'
|
||||
"
|
||||
@click="erledigen(w, !w.done_at)"
|
||||
>
|
||||
{{ w.done_at ? "↺" : "✓" }}
|
||||
</button>
|
||||
<button
|
||||
v-if="darfAendern(w) && bearbeitet !== w.id"
|
||||
type="button"
|
||||
class="weg"
|
||||
title="Wunsch bearbeiten"
|
||||
@click="bearbeiten(w)"
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
<button
|
||||
v-if="darfAendern(w)"
|
||||
type="button"
|
||||
class="weg"
|
||||
title="Wunsch entfernen"
|
||||
@click="loeschen(w)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wuensche-tab {
|
||||
max-width: 46rem;
|
||||
.spalten {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"neu erledigt"
|
||||
"offen erledigt";
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
gap: 0 2.5rem;
|
||||
}
|
||||
|
||||
.neu {
|
||||
display: flex;
|
||||
grid-area: neu;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
|
||||
.spalte-offen {
|
||||
grid-area: offen;
|
||||
}
|
||||
|
||||
.spalte-erledigt {
|
||||
grid-area: erledigt;
|
||||
}
|
||||
|
||||
.neu textarea {
|
||||
flex: 1;
|
||||
padding: 0.6rem 0.8rem;
|
||||
@@ -517,6 +555,16 @@ h3 {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.spalten {
|
||||
grid-template-areas:
|
||||
"neu"
|
||||
"offen"
|
||||
"erledigt";
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px), (pointer: coarse) {
|
||||
.stimme {
|
||||
padding: 0.4rem 0.7rem;
|
||||
|
||||
Reference in new issue
Block a user