280 lines
9.4 KiB
Vue
280 lines
9.4 KiB
Vue
<script setup>
|
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
import { fetchProjekte } from './api.js'
|
|
import SeitenLeiste from './components/SeitenLeiste.vue'
|
|
import FeatureListe from './components/FeatureListe.vue'
|
|
import KontextMenue from './components/KontextMenue.vue'
|
|
import PruefPanel from './components/PruefPanel.vue'
|
|
import ChatPanel from './components/ChatPanel.vue'
|
|
import {
|
|
laeuft as planungLaeuft,
|
|
sicht as planungSicht,
|
|
anzahl as planungAnzahl,
|
|
meldung as planungMeldung,
|
|
lade as ladePlanung,
|
|
starte as startePlanung,
|
|
umsetzen as setzePlanungUm,
|
|
brichAb as brichPlanungAb,
|
|
} from './composables/planung.js'
|
|
import {
|
|
offen as pruefungOffen,
|
|
laeuft as pruefungLaeuft,
|
|
prozent as pruefungProzent,
|
|
starte as startePruefung,
|
|
schliesse as schliessePruefung,
|
|
} from './composables/pruefung.js'
|
|
|
|
const projekte = ref([])
|
|
const aktivesProjekt = ref(null)
|
|
const meldung = ref('')
|
|
|
|
// Gespräch und Prüfung teilen sich die rechte Spalte — beide gleichzeitig
|
|
// wären zwei Panels für dieselbe Fläche.
|
|
const chatOffen = ref(false)
|
|
|
|
// In der Übersicht schaut man nur nach. Wer etwas ändern will, wechselt in die Planung.
|
|
const inPlanung = computed(() => planungLaeuft.value && planungSicht.value === 'planung')
|
|
const nurAnsehen = computed(() => planungLaeuft.value && !inPlanung.value)
|
|
|
|
// Umlegen heißt: die andere Seite zeigen. Gibt es noch keine Planung, entsteht sie hier.
|
|
function schalte() {
|
|
if (!planungLaeuft.value) return startePlanung()
|
|
planungSicht.value = inPlanung.value ? 'uebersicht' : 'planung'
|
|
}
|
|
|
|
const stand = ref(0)
|
|
|
|
function oeffneChat() {
|
|
if (pruefungOffen.value) schliessePruefung()
|
|
chatOffen.value = !chatOffen.value
|
|
}
|
|
|
|
// Drei Modi im Kreis; „auto" folgt der Systemeinstellung
|
|
const MODI = ['auto', 'hell', 'dunkel']
|
|
const ICONS = { auto: 'A', hell: '☀', dunkel: '☾' }
|
|
const TITEL = { auto: 'Systemeinstellung', hell: 'Hellmodus', dunkel: 'Dunkelmodus' }
|
|
|
|
const systemDunkel = window.matchMedia('(prefers-color-scheme: dark)')
|
|
const gemerkterModus = localStorage.getItem('darkModus')
|
|
const modus = ref(MODI.includes(gemerkterModus) ? gemerkterModus : 'auto')
|
|
|
|
async function ladeProjekte() {
|
|
meldung.value = ''
|
|
try {
|
|
projekte.value = await fetchProjekte()
|
|
} catch (e) {
|
|
meldung.value = e.message
|
|
return
|
|
}
|
|
// Auswahl verwerfen, wenn das Projekt (z. B. nach Löschen) weg ist
|
|
if (aktivesProjekt.value && !projekte.value.some(p => p.name === aktivesProjekt.value)) {
|
|
aktivesProjekt.value = null
|
|
}
|
|
}
|
|
|
|
function umbenannt(alt, neu) {
|
|
if (aktivesProjekt.value === alt) aktivesProjekt.value = neu
|
|
ladeProjekte()
|
|
}
|
|
|
|
// Prüfung gehört zum Projekt — beim Wechsel wird sie verworfen
|
|
watch(aktivesProjekt, () => { if (pruefungOffen.value) schliessePruefung() })
|
|
watch(aktivesProjekt, n => ladePlanung(n))
|
|
|
|
// Planen heißt, die Karten mit der KI zu entwickeln. Das Gespräch ist dabei kein
|
|
// Zusatz, sondern das Werkzeug — wer die Planung sieht, sieht es auch. Am Reiter
|
|
// und nicht am Startknopf, damit es auch nach einem Neustart wieder dasteht.
|
|
watch(planungSicht, s => {
|
|
if (s !== 'planung') {
|
|
chatOffen.value = false // in der Übersicht gibt es nichts zu besprechen
|
|
return
|
|
}
|
|
if (pruefungOffen.value) schliessePruefung()
|
|
chatOffen.value = true
|
|
})
|
|
|
|
// Umsetzen und Abbrechen ändern die Karten — die Liste muss neu lesen.
|
|
async function planungBeenden(wie) {
|
|
const ok = wie === 'umsetzen' ? await setzePlanungUm() : await brichPlanungAb()
|
|
if (ok) stand.value++
|
|
}
|
|
watch(pruefungOffen, o => { if (o) chatOffen.value = false })
|
|
|
|
// Zuletzt aktives Projekt überlebt das Neuladen
|
|
watch(aktivesProjekt, name => {
|
|
if (name) localStorage.setItem('aktivesProjekt', name)
|
|
else localStorage.removeItem('aktivesProjekt')
|
|
})
|
|
|
|
function wendeAn() {
|
|
const dunkel = modus.value === 'auto' ? systemDunkel.matches : modus.value === 'dunkel'
|
|
document.documentElement.classList.toggle('dark', dunkel)
|
|
}
|
|
|
|
function wechsleDarkMode() {
|
|
modus.value = MODI[(MODI.indexOf(modus.value) + 1) % MODI.length]
|
|
localStorage.setItem('darkModus', modus.value)
|
|
wendeAn()
|
|
}
|
|
|
|
// Systemwechsel wirkt sofort, solange „auto" aktiv ist
|
|
systemDunkel.addEventListener('change', () => { if (modus.value === 'auto') wendeAn() })
|
|
|
|
onMounted(async () => {
|
|
await ladeProjekte()
|
|
const gemerkt = localStorage.getItem('aktivesProjekt')
|
|
if (gemerkt && projekte.value.some(p => p.name === gemerkt)) {
|
|
aktivesProjekt.value = gemerkt
|
|
}
|
|
// Eine Planung liegt auf der Platte. Nach einem Neustart steht sie wieder da.
|
|
await ladePlanung(aktivesProjekt.value)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="raster" :class="{ pruefung: pruefungOffen || chatOffen }">
|
|
<SeitenLeiste
|
|
:projekte="projekte"
|
|
:aktiv="aktivesProjekt"
|
|
@auswaehlen="aktivesProjekt = $event"
|
|
@geaendert="ladeProjekte"
|
|
@umbenannt="umbenannt"
|
|
/>
|
|
|
|
<main>
|
|
<p v-if="meldung" class="meldung">{{ meldung }}</p>
|
|
<p v-if="planungMeldung" class="meldung">{{ planungMeldung }}</p>
|
|
<FeatureListe
|
|
v-if="aktivesProjekt"
|
|
:projekt="aktivesProjekt"
|
|
:projekte="projekte"
|
|
:stand="stand"
|
|
>
|
|
<template #werkzeuge>
|
|
<!-- Ein Schalter, zwei Stellungen. Der Text nennt, was gerade zu sehen ist.
|
|
Er steht immer da: „wo bin ich" ist auch dann eine Frage, wenn noch
|
|
nichts geplant wird. Wer dann umlegt, beginnt eine Planung. -->
|
|
<button
|
|
class="schalter" :class="{ planung: inPlanung }"
|
|
:title="inPlanung ? 'Wechseln zu Übersicht'
|
|
: (planungLaeuft ? 'Wechseln zu Planung' : 'Planung beginnen')"
|
|
@click="schalte"
|
|
>
|
|
⇄ {{ inPlanung ? 'Planung' : 'Übersicht' }}
|
|
<span v-if="inPlanung && planungAnzahl" class="zahl">{{ planungAnzahl }}</span>
|
|
</button>
|
|
|
|
<!-- Jedes Werkzeug hier ändert Karten. Die Übersicht sagt nur, was das Projekt
|
|
heute kann — dort wird nichts entschieden und nichts angefasst. -->
|
|
<template v-if="!nurAnsehen">
|
|
<button
|
|
class="icon-knopf"
|
|
:class="{ an: chatOffen }"
|
|
title="Karten im Gespräch entwickeln"
|
|
@click="oeffneChat"
|
|
>✎</button>
|
|
<button class="icon-knopf" title="Stil prüfen" @click="startePruefung(aktivesProjekt, 'stil')">⌾</button>
|
|
<template v-if="planungLaeuft">
|
|
<button class="icon-knopf" title="Planung umsetzen" @click="planungBeenden('umsetzen')">▶</button>
|
|
<button class="icon-knopf rot" title="Planung verwerfen" @click="planungBeenden('abbrechen')">✕</button>
|
|
</template>
|
|
<span v-if="pruefungLaeuft" class="fortschritt">{{ pruefungProzent }} %</span>
|
|
</template>
|
|
|
|
<button class="icon-knopf" :title="TITEL[modus]" @click="wechsleDarkMode">
|
|
{{ ICONS[modus] }}
|
|
</button>
|
|
</template>
|
|
</FeatureListe>
|
|
<p v-else-if="!meldung" class="leer">Projekt wählen oder anlegen.</p>
|
|
</main>
|
|
|
|
<PruefPanel v-if="pruefungOffen" />
|
|
<ChatPanel
|
|
v-else-if="chatOffen && aktivesProjekt"
|
|
:projekt="aktivesProjekt"
|
|
@schliessen="chatOffen = false"
|
|
/>
|
|
<KontextMenue />
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* App-Shell: feste Höhe, drei eigene Scrollbereiche. Vorher wanderten die
|
|
Leisten beim Scrollen mit nach oben weg. */
|
|
.raster {
|
|
display: grid;
|
|
grid-template-columns: 15rem 1fr;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
.raster.pruefung { grid-template-columns: 15rem 1fr 20rem; }
|
|
main {
|
|
padding: 1.2rem 1.5rem;
|
|
min-width: 0;
|
|
overflow-y: auto;
|
|
overscroll-behavior: contain;
|
|
}
|
|
|
|
/* Mittlere Fenster: drei Spalten passen nicht mehr. Bei 760px blieben für die
|
|
Karten 200px übrig, und die Kartenköpfe liefen über. Das Panel legt sich dann
|
|
unter den Arbeitsbereich, die Projektliste bleibt daneben. */
|
|
@media (min-width: 46.01rem) and (max-width: 64rem) {
|
|
.raster.pruefung {
|
|
grid-template-columns: 15rem 1fr;
|
|
grid-template-rows: 1fr 45vh;
|
|
/* Benannte Bereiche statt Auto-Platzierung: mit `grid-row` allein landete
|
|
der Arbeitsbereich unter der Leiste statt daneben. */
|
|
grid-template-areas:
|
|
"leiste inhalt"
|
|
"leiste panel";
|
|
}
|
|
.raster.pruefung > aside { grid-area: leiste; }
|
|
.raster.pruefung > main { grid-area: inhalt; }
|
|
.raster.pruefung > .panel,
|
|
.raster.pruefung > .chat { grid-area: panel; }
|
|
}
|
|
|
|
/* Schmale Fenster: die Projektliste legt sich als Leiste nach oben */
|
|
@media (max-width: 46rem) {
|
|
.raster, .raster.pruefung {
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: auto 1fr;
|
|
}
|
|
.raster.pruefung { grid-template-rows: auto 1fr auto; }
|
|
main { padding: 0.9rem 1rem; }
|
|
}
|
|
.leer {
|
|
color: var(--dezent);
|
|
margin-top: 3rem;
|
|
text-align: center;
|
|
}
|
|
.fortschritt {
|
|
font-size: 0.7rem;
|
|
color: var(--dezent);
|
|
margin-left: 0.2rem;
|
|
}
|
|
.icon-knopf.an { color: var(--akzent); background: var(--code-bg); }
|
|
|
|
/* Ein Schalter statt zweier Reiter. Beschriftet mit dem, was zu sehen ist —
|
|
das Pfeilpaar sagt, dass ein Klick die andere Seite zeigt. */
|
|
.schalter {
|
|
border: 1px solid var(--rand);
|
|
background: none;
|
|
color: var(--dezent);
|
|
border-radius: 0.4rem;
|
|
padding: 0.2rem 0.55rem;
|
|
margin-right: 0.3rem;
|
|
cursor: pointer;
|
|
font: inherit;
|
|
font-size: 0.78rem;
|
|
white-space: nowrap;
|
|
}
|
|
.schalter.planung {
|
|
color: var(--akzent-text);
|
|
background: var(--akzent);
|
|
border-color: var(--akzent);
|
|
}
|
|
.schalter .zahl { margin-left: 0.3rem; font-weight: 700; }
|
|
</style>
|