This commit is contained in:
Team3
2026-07-27 07:54:09 +02:00
parent 37b9078b34
commit 37100f1b81
22 changed files with 837 additions and 21 deletions

View File

@@ -3,12 +3,15 @@ import { nextTick, ref, watch } from 'vue'
import { marked } from 'marked'
import { featureSpeichern } from '../api.js'
import { oeffneMenue } from '../composables/kontextmenue.js'
import { erledige } from '../composables/pruefung.js'
import ModalBox from './ModalBox.vue'
import ProblemHinweis from './ProblemHinweis.vue'
const props = defineProps({
projekt: { type: String, required: true },
feature: { type: Object, required: true },
vollmodus: { type: Boolean, default: false }, // frisch angelegt: direkt im Vollmodus öffnen
probleme: { type: Array, default: () => [] },
})
const emit = defineEmits(['aktualisiert', 'entfernen'])
@@ -173,12 +176,59 @@ function punkte(text) {
function wechsleAnsicht() {
gross.value = !gross.value
}
// Prüfung: fehlerhafte Karten öffnen sich, damit die Stellen sichtbar sind
watch(() => props.probleme.length, n => { if (n) gross.value = true }, { immediate: true })
function problemeAn(feld, funktion = '', original = null) {
return props.probleme.filter(p =>
p.feld === feld &&
(p.funktion || '') === funktion &&
(original === null || p.original === original))
}
// Vorschlag übernehmen: Titel und Ziel ganz ersetzen, bei Regeln und Testen die Zeile
function anwenden(p) {
const ziel = p.funktion ? s.value.funktionen.find(f => f.text === p.funktion) : s.value
if (!ziel) return
if (p.feld === 'titel') {
if (p.funktion) ziel.text = p.vorschlag
else ziel.titel = p.vorschlag
} else if (p.feld === 'ziel') {
ziel.ziel = p.vorschlag
} else {
const feld = p.feld === 'testen' ? 'testen' : 'regeln'
ziel[feld] = ziel[feld]
.split('\n')
.map(z => (z.replace(/^[-*]\s*/, '').trim() === p.original ? `- ${p.vorschlag}` : z))
.join('\n')
}
}
async function annehmen(p) {
anwenden(p)
erledige(p)
await speichern()
}
function ablehnen(p) {
erledige(p)
}
async function alleAnnehmen() {
props.probleme.forEach(p => { anwenden(p); erledige(p) })
await speichern()
}
function alleAblehnen() {
props.probleme.forEach(erledige)
}
</script>
<template>
<article
class="karte"
:class="[s.art, { gross }]"
:class="[s.art, { gross, fehlerhaft: probleme.length }]"
@mousedown.middle.prevent
@auxclick.middle.prevent="wechsleAnsicht"
@contextmenu.prevent.stop="kartenMenue"
@@ -207,7 +257,12 @@ function wechsleAnsicht() {
:title="gross ? 'Kompaktmodus' : 'Vollmodus'"
@click="wechsleAnsicht"
>👁</button>
<span v-if="probleme.length" class="alle">
<button class="icon-knopf" title="Alle annehmen" @click="alleAnnehmen"></button>
<button class="icon-knopf rot" title="Alle ablehnen" @click="alleAblehnen"></button>
</span>
</header>
<ProblemHinweis :probleme="problemeAn('titel')" @annehmen="annehmen" @ablehnen="ablehnen" />
<section :class="{ leer: !s.ziel && !istAktiv('ziel') }">
<p class="gruppe">Ziel</p>
@@ -221,6 +276,7 @@ function wechsleAnsicht() {
/>
<div v-else-if="s.ziel" class="inhalt klickbar" v-html="md(s.ziel)" @click="starte('ziel')" />
<p v-else-if="gross" class="hinweis" @click="starte('ziel')">Ziel ergänzen</p>
<ProblemHinweis :probleme="problemeAn('ziel')" @annehmen="annehmen" @ablehnen="ablehnen" />
</section>
<!-- Karten-Regeln nur in der ausführlichen Ansicht -->
@@ -235,7 +291,14 @@ function wechsleAnsicht() {
@keydown.esc="abbrechen"
/>
<div v-else-if="s.regeln" class="klickbar" @click="starte('regeln')">
<div v-for="(r, k) in punkte(s.regeln)" :key="k" class="regel">{{ r }}</div>
<template v-for="(r, k) in punkte(s.regeln)" :key="k">
<div class="regel" :class="{ fehler: problemeAn('regeln', '', r).length }">{{ r }}</div>
<ProblemHinweis
:probleme="problemeAn('regeln', '', r)"
@annehmen="annehmen"
@ablehnen="ablehnen"
/>
</template>
</div>
<p v-else-if="gross" class="hinweis" @click="starte('regeln')">Regeln ergänzen</p>
</section>
@@ -260,8 +323,14 @@ function wechsleAnsicht() {
@keyup.enter="$event.target.blur()"
@keydown.esc="abbrechen"
>
<div v-else class="funktionstitel klickbar" @click="starte('funktion', i)">{{ f.text }}</div>
<div
v-else
class="funktionstitel klickbar"
:class="{ fehler: problemeAn('titel', f.text).length }"
@click="starte('funktion', i)"
>{{ f.text }}</div>
</div>
<ProblemHinweis :probleme="problemeAn('titel', f.text)" @annehmen="annehmen" @ablehnen="ablehnen" />
<!-- Ziel, Regeln und Testen nur im Vollmodus -->
<div class="details">
@@ -277,6 +346,7 @@ function wechsleAnsicht() {
@keydown.esc="abbrechen"
/>
<div v-else class="inhalt klickbar" v-html="md(f.ziel)" @click="starte('ziel', i)" />
<ProblemHinweis :probleme="problemeAn('ziel', f.text)" @annehmen="annehmen" @ablehnen="ablehnen" />
</div>
<button v-if="!f.ziel && !istAktiv('ziel', i)" class="hinzu" @click="starte('ziel', i)">+ Ziel</button>
@@ -292,7 +362,14 @@ function wechsleAnsicht() {
@keydown.esc="abbrechen"
/>
<div v-else class="klickbar" @click="starte('regeln', i)">
<div v-for="(r, k) in punkte(f.regeln)" :key="k" class="regel">{{ r }}</div>
<template v-for="(r, k) in punkte(f.regeln)" :key="k">
<div class="regel" :class="{ fehler: problemeAn('regeln', f.text, r).length }">{{ r }}</div>
<ProblemHinweis
:probleme="problemeAn('regeln', f.text, r)"
@annehmen="annehmen"
@ablehnen="ablehnen"
/>
</template>
</div>
</div>
<button v-if="!f.regeln && !istAktiv('regeln', i)" class="hinzu" @click="starte('regeln', i)">+ Regeln</button>
@@ -309,9 +386,16 @@ function wechsleAnsicht() {
@keydown.esc="abbrechen"
/>
<div v-else class="klickbar" @click="starte('testen', i)">
<div v-for="(schritt, k) in punkte(f.testen)" :key="k" class="schritt">
<span class="nr">{{ k + 1 }}.</span>{{ schritt }}
</div>
<template v-for="(schritt, k) in punkte(f.testen)" :key="k">
<div class="schritt" :class="{ fehler: problemeAn('testen', f.text, schritt).length }">
<span class="nr">{{ k + 1 }}.</span>{{ schritt }}
</div>
<ProblemHinweis
:probleme="problemeAn('testen', f.text, schritt)"
@annehmen="annehmen"
@ablehnen="ablehnen"
/>
</template>
</div>
</div>
@@ -344,6 +428,10 @@ function wechsleAnsicht() {
<style scoped>
.karte.kern { border-color: var(--akzent); }
.karte.optional { border-color: var(--gruen); }
/* Prüfung: fehlerhafte Karte und Stelle rot */
.karte.fehlerhaft { border-color: var(--rot); }
.fehler { color: var(--rot); }
.alle { white-space: nowrap; }
/* Vollmodus: volle Breite, Funktionen als Boxen in responsiven Spalten,
alle Details offen */

View File

@@ -2,6 +2,7 @@
import { ref, watch } from 'vue'
import { fetchFeatures, featureAnlegen, featureLoeschen } from '../api.js'
import { oeffneMenue } from '../composables/kontextmenue.js'
import { problemeFuer } from '../composables/pruefung.js'
import FeatureKarte from './FeatureKarte.vue'
import ModalBox from './ModalBox.vue'
@@ -88,6 +89,7 @@ watch(() => props.projekt, laden, { immediate: true })
:projekt="projekt"
:feature="f"
:vollmodus="f.key === neuKey"
:probleme="problemeFuer(f.id)"
@aktualisiert="aktualisiert"
@entfernen="oeffneModal({ art: 'entfernen', feature: f })"
/>

View File

@@ -0,0 +1,52 @@
<script setup>
defineProps({
probleme: { type: Array, required: true },
})
const emit = defineEmits(['annehmen', 'ablehnen'])
</script>
<template>
<div v-for="(p, i) in probleme" :key="i" class="hinweis-block" @click.stop>
<div class="kopf">
<p class="problem">{{ p.problem }}</p>
<span class="knoepfe">
<button class="icon-knopf" title="Annehmen" @click="emit('annehmen', p)"></button>
<button class="icon-knopf rot" title="Ablehnen" @click="emit('ablehnen', p)"></button>
</span>
</div>
<p class="vorschlag">{{ p.vorschlag }}</p>
</div>
</template>
<style scoped>
.hinweis-block {
border-left: 2px solid var(--rot);
background: var(--rot-bg);
border-radius: 0 4px 4px 0;
padding: 0.25rem 0.5rem 0.35rem;
margin: 0.15rem 0 0.3rem;
min-width: 0;
cursor: default;
}
.kopf {
display: flex;
align-items: baseline;
gap: 0.4rem;
}
.problem {
margin: 0;
flex: 1;
min-width: 0;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.04em;
line-height: 1.35;
color: var(--rot);
}
.knoepfe { white-space: nowrap; }
.vorschlag {
margin: 0.1rem 0 0;
font-size: 0.85rem;
overflow-wrap: anywhere;
}
</style>

View File

@@ -0,0 +1,72 @@
<script setup>
import { laeuft, prozent, fertig, gesamt, meldung, probleme, schliesse } from '../composables/pruefung.js'
</script>
<template>
<aside class="panel">
<header>
<p class="gruppe">Prüfung</p>
<button class="icon-knopf" title="Schließen" @click="schliesse"></button>
</header>
<p class="fortschritt">
<template v-if="laeuft">{{ prozent }} % geprüft</template>
<template v-else-if="gesamt">{{ gesamt }} Karten geprüft</template>
<template v-else>Keine Karten</template>
<span v-if="laeuft" class="dezent"> ({{ fertig }}/{{ gesamt }})</span>
</p>
<p v-if="meldung" class="meldung">{{ meldung }}</p>
<p v-if="!probleme.length && !laeuft && !meldung" class="dezent">Keine Probleme gefunden.</p>
<ul>
<li v-for="(p, i) in probleme" :key="i">
<span class="stelle">{{ p.funktion || p.feature }} · {{ p.feld }}</span>
<span class="problem">{{ p.problem }}</span>
</li>
</ul>
</aside>
</template>
<style scoped>
.panel {
background: var(--panel);
border-left: 1px solid var(--rand);
padding: 1rem 0.8rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
overflow-y: auto;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
}
.gruppe {
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--dezent);
margin: 0;
}
.fortschritt { margin: 0; font-size: 0.9rem; }
.dezent { color: var(--dezent); font-size: 0.85rem; }
ul { list-style: none; margin: 0.3rem 0 0; padding: 0; }
li {
border-top: 1px solid var(--rand);
padding: 0.35rem 0;
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.stelle {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--akzent);
}
.problem { font-size: 0.85rem; }
</style>