This commit is contained in:
team3 committed 2026-09-18 22:12:01 +02:00
1 parent 33b3975b61
commit db77c1f985
10 files changed
+488 -298

No files matched your search

+235 -187
View File
@@ -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;