Workspace: Aufgabenbrett und Dashboard-Zahlen
Erster echter Arbeitsbereich aus dem Konzept. Kanban mit offen / in Arbeit / Review / erledigt, dazu Prioritaet, Frist, Verantwortlicher und Zuordnung zu einem Creator-Bereich. Kern ist die Datentrennung, und die sitzt AUSSCHLIESSLICH im Server -- in jeder einzelnen Abfrage, an einer Stelle definiert (sichtbar()): admin sieht alles creator sieht seinen Bereich und was ihm zugewiesen ist scout sieht nur, was ihm zugewiesen ist Geprueft mit vier Testkonten: - Chef sieht 4, Luna 2, Mika 1, Sam 1 Aufgaben - Luna auf Mikas Aufgabe: 404 (nicht 403 -- sonst liesse sich durch Ausprobieren herausfinden, welche Nummern es gibt) - Luna legt Aufgabe mit creator_id=Mika an: wird still auf ihren eigenen Bereich umgebogen, Mika sieht sie nicht - Anfrage mit fremdem Origin: 403 - ohne Anmeldung: 401, ungueltiger Status/leerer Titel: 400 - Scout bekommt in der Personenliste nur sich selbst - Dashboard-Zahlen je Rolle korrekt eingegrenzt Weitere Punkte: - Texte werden im Browser nur ueber textContent gesetzt, nie innerHTML -- ein Aufgabentitel darf keine Auszeichnung einschleusen - ueberfaellig = Frist vorbei UND nicht erledigt; die Kachel faerbt sich nur, wenn wirklich etwas ansteht - erledigt_am wird gesetzt bzw. wieder geleert, wenn eine Aufgabe zurueckgeholt wird - Erledigtes verschwindet nicht, wie im Konzept gefordert
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
/* ===================================================================
|
||||
Aufgabenbrett. Baut auf gate.css und start.css auf.
|
||||
=================================================================== */
|
||||
|
||||
.inhalt--breit { max-width: 1240px; }
|
||||
|
||||
.zurueck { color: inherit; text-decoration: none; border-bottom: 1px solid transparent; }
|
||||
.zurueck:hover { border-bottom-color: currentColor; }
|
||||
|
||||
.kopf-zeile {
|
||||
display: flex; align-items: flex-end; justify-content: space-between;
|
||||
gap: 20px; flex-wrap: wrap; margin-bottom: 24px;
|
||||
}
|
||||
.kopf-zeile .titel { margin: 0; font-size: clamp(1.4rem, 2.2vw, 1.85rem); }
|
||||
|
||||
.knopf--klein {
|
||||
width: auto; margin: 0; padding: 10px 18px; font-size: .92rem;
|
||||
}
|
||||
|
||||
.leise { color: var(--text-still); font-weight: 400; }
|
||||
|
||||
/* ---------- Formular -------------------------------------------------- */
|
||||
|
||||
.neu {
|
||||
margin: 0 0 28px;
|
||||
padding: 20px;
|
||||
background: rgba(255, 255, 255, .03);
|
||||
border: 1px solid var(--rand);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.neu__zeile { margin-bottom: 14px; }
|
||||
.neu__raster {
|
||||
display: grid; gap: 14px; margin-bottom: 8px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
||||
}
|
||||
.neu__knoepfe { display: flex; gap: 10px; align-items: center; margin-top: 12px; }
|
||||
|
||||
.feld-schild {
|
||||
display: block; margin-bottom: 6px;
|
||||
font-size: .76rem; font-weight: 600; letter-spacing: .06em;
|
||||
text-transform: uppercase; color: var(--text-still);
|
||||
}
|
||||
|
||||
.neu input, .neu textarea, .neu select {
|
||||
width: 100%; padding: 10px 12px;
|
||||
background: rgba(255, 255, 255, .045);
|
||||
border: 1px solid var(--rand);
|
||||
border-radius: var(--radius-klein);
|
||||
color: var(--text); font: inherit;
|
||||
transition: border-color var(--tempo), box-shadow var(--tempo);
|
||||
}
|
||||
.neu textarea { resize: vertical; min-height: 62px; }
|
||||
.neu input:focus, .neu textarea:focus, .neu select:focus {
|
||||
outline: none; border-color: rgba(63, 189, 245, .7);
|
||||
box-shadow: 0 0 0 3px rgba(63, 189, 245, .16);
|
||||
}
|
||||
/* Der Kalender-Knopf im Datumsfeld ist sonst fast schwarz auf dunkel. */
|
||||
.neu input[type="date"]::-webkit-calendar-picker-indicator { filter: invert(.75); cursor: pointer; }
|
||||
|
||||
/* ---------- Brett ----------------------------------------------------- */
|
||||
|
||||
.brett {
|
||||
display: grid; gap: 14px;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.spalte {
|
||||
background: rgba(255, 255, 255, .022);
|
||||
border: 1px solid var(--rand);
|
||||
border-radius: var(--radius);
|
||||
padding: 14px 12px 16px;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.spalte__kopf {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
margin: 0 2px 12px;
|
||||
font-size: .78rem; font-weight: 700;
|
||||
letter-spacing: .08em; text-transform: uppercase;
|
||||
color: var(--text-leise);
|
||||
}
|
||||
.spalte__punkt { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
|
||||
.spalte__zahl {
|
||||
margin-left: auto; padding: 1px 8px;
|
||||
background: rgba(255, 255, 255, .06); border-radius: 999px;
|
||||
font-size: .74rem; color: var(--text-still); letter-spacing: normal;
|
||||
}
|
||||
|
||||
.spalte[data-status="offen"] .spalte__punkt { background: #6f89a8; }
|
||||
.spalte[data-status="arbeit"] .spalte__punkt { background: var(--akzent); }
|
||||
.spalte[data-status="review"] .spalte__punkt { background: #e0a33c; }
|
||||
.spalte[data-status="erledigt"] .spalte__punkt { background: #4dab77; }
|
||||
|
||||
.karte {
|
||||
padding: 12px 13px;
|
||||
margin-bottom: 9px;
|
||||
background: rgba(9, 13, 22, .78);
|
||||
border: 1px solid var(--rand);
|
||||
border-left: 3px solid transparent;
|
||||
border-radius: var(--radius-klein);
|
||||
}
|
||||
.karte[data-prio="hoch"] { border-left-color: #d8656e; }
|
||||
.karte[data-prio="mittel"] { border-left-color: #4b6c8a; }
|
||||
.karte[data-prio="niedrig"] { border-left-color: #3c4a5a; }
|
||||
|
||||
.karte__titel { font-weight: 600; font-size: .93rem; line-height: 1.35; }
|
||||
.karte__text { margin: 6px 0 0; font-size: .84rem; color: var(--text-leise); }
|
||||
|
||||
.karte__zeile {
|
||||
display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
|
||||
margin-top: 10px; font-size: .76rem; color: var(--text-still);
|
||||
}
|
||||
.karte__frist--faellig { color: var(--warn); font-weight: 600; }
|
||||
|
||||
.karte__schritte { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
|
||||
.schritt {
|
||||
padding: 5px 10px;
|
||||
background: rgba(255, 255, 255, .04);
|
||||
border: 1px solid var(--rand);
|
||||
border-radius: 8px;
|
||||
color: var(--text-leise); font: inherit; font-size: .76rem; cursor: pointer;
|
||||
transition: background var(--tempo), border-color var(--tempo), color var(--tempo);
|
||||
}
|
||||
.schritt:hover { background: rgba(255, 255, 255, .08); color: var(--text); border-color: var(--rand-hell); }
|
||||
.schritt[disabled] { opacity: .4; cursor: default; }
|
||||
|
||||
.spalte__leer { margin: 4px 2px; font-size: .82rem; color: var(--text-still); }
|
||||
|
||||
@media (max-width: 1080px) { .brett { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 620px) { .brett { grid-template-columns: 1fr; } }
|
||||
@@ -66,6 +66,32 @@ body.start {
|
||||
.willkommen { margin-bottom: 38px; }
|
||||
.willkommen .titel { margin-bottom: 6px; }
|
||||
|
||||
/* ---------- Zahlen ---------------------------------------------------- */
|
||||
|
||||
.zahlen-block { margin-bottom: 34px; }
|
||||
|
||||
.zahlen {
|
||||
list-style: none; margin: 0; padding: 0;
|
||||
display: grid; gap: 10px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(122px, 1fr));
|
||||
}
|
||||
|
||||
.zahl {
|
||||
padding: 14px 16px;
|
||||
background: rgba(255, 255, 255, .03);
|
||||
border: 1px solid var(--rand);
|
||||
border-radius: var(--radius-klein);
|
||||
}
|
||||
.zahl__wert { display: block; font-size: 1.7rem; font-weight: 700; line-height: 1.1; }
|
||||
.zahl__name { display: block; margin-top: 2px; font-size: .78rem; color: var(--text-still); }
|
||||
|
||||
/* Nur wenn wirklich etwas überfällig ist, wird die Kachel hervorgehoben --
|
||||
sonst stumpft die Farbe ab und man übersieht sie im Ernstfall. */
|
||||
.zahl[data-warn="ja"] { border-color: rgba(255, 154, 162, .42); background: rgba(255, 154, 162, .07); }
|
||||
.zahl[data-warn="ja"] .zahl__wert { color: var(--warn); }
|
||||
|
||||
/* ---------- Bereiche --------------------------------------------------- */
|
||||
|
||||
.bereiche {
|
||||
list-style: none; margin: 0; padding: 0;
|
||||
display: grid; gap: 10px;
|
||||
@@ -93,6 +119,24 @@ body.start {
|
||||
.bereich[data-phase="1"] { border-color: rgba(63, 189, 245, .34); }
|
||||
.bereich[data-phase="1"] .bereich__stand { color: var(--akzent); }
|
||||
|
||||
/* Fertige Bereiche sind anklickbar und heben sich deutlich ab. */
|
||||
.bereich[data-fertig="ja"] {
|
||||
padding: 0;
|
||||
border-color: rgba(63, 189, 245, .5);
|
||||
background: linear-gradient(100deg, rgba(63, 189, 245, .10), rgba(138, 118, 255, .07));
|
||||
transition: border-color var(--tempo), background var(--tempo);
|
||||
}
|
||||
.bereich[data-fertig="ja"]:hover { border-color: rgba(63, 189, 245, .8); }
|
||||
|
||||
.bereich__link {
|
||||
display: flex; align-items: baseline; gap: 10px; width: 100%;
|
||||
padding: 13px 15px;
|
||||
color: inherit; text-decoration: none;
|
||||
border-radius: var(--radius-klein);
|
||||
}
|
||||
.bereich__link:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; }
|
||||
.bereich[data-fertig="ja"] .bereich__stand { color: var(--akzent); font-weight: 600; }
|
||||
|
||||
.inhalt .fuss { margin-top: 40px; text-align: left; }
|
||||
|
||||
@media (max-width: 520px) {
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
/* ===================================================================
|
||||
Aufgabenbrett.
|
||||
|
||||
Der Server entscheidet, wer was sehen und ändern darf -- diese Datei
|
||||
zeigt nur an. Was hier ausgeblendet wird, ist Bequemlichkeit, kein
|
||||
Schutz.
|
||||
=================================================================== */
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const SPALTEN = [
|
||||
{ status: 'offen', name: 'Offen' },
|
||||
{ status: 'arbeit', name: 'In Arbeit' },
|
||||
{ status: 'review', name: 'Review' },
|
||||
{ status: 'erledigt', name: 'Erledigt' },
|
||||
];
|
||||
const WEITER = { offen: 'arbeit', arbeit: 'review', review: 'erledigt' };
|
||||
const ZURUECK = { arbeit: 'offen', review: 'arbeit', erledigt: 'review' };
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const heute = new Date().toISOString().slice(0, 10);
|
||||
let ich = null;
|
||||
|
||||
/* Texte werden ausschließlich über textContent gesetzt, nie über
|
||||
innerHTML -- ein Aufgabentitel darf keine Auszeichnung einschleusen. */
|
||||
const el = (tag, klasse, text) => {
|
||||
const k = document.createElement(tag);
|
||||
if (klasse) k.className = klasse;
|
||||
if (text !== undefined) k.textContent = text;
|
||||
return k;
|
||||
};
|
||||
|
||||
const melde = (text) => { $('fehler').textContent = text || ''; };
|
||||
|
||||
async function hole(pfad, optionen = {}) {
|
||||
const antwort = await fetch(pfad, { credentials: 'same-origin', ...optionen });
|
||||
if (antwort.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
|
||||
return antwort;
|
||||
}
|
||||
|
||||
/* ---------- Brett zeichnen ------------------------------------------ */
|
||||
|
||||
function datumHuebsch(iso) {
|
||||
const [j, m, t] = iso.split('-');
|
||||
return `${t}.${m}.${j.slice(2)}`;
|
||||
}
|
||||
|
||||
function karte(a) {
|
||||
const k = el('article', 'karte');
|
||||
k.dataset.prio = a.prioritaet;
|
||||
k.append(el('h3', 'karte__titel', a.titel));
|
||||
if (a.beschreibung) k.append(el('p', 'karte__text', a.beschreibung));
|
||||
|
||||
const zeile = el('div', 'karte__zeile');
|
||||
if (a.frist) {
|
||||
const faellig = a.status !== 'erledigt' && a.frist < heute;
|
||||
const f = el('span', 'karte__frist' + (faellig ? ' karte__frist--faellig' : ''),
|
||||
(faellig ? 'überfällig ' : 'bis ') + datumHuebsch(a.frist));
|
||||
zeile.append(f);
|
||||
}
|
||||
if (a.verantwortlich_name) zeile.append(el('span', null, '· ' + a.verantwortlich_name));
|
||||
if (ich.rolle === 'admin' && a.creator_name && a.creator_name !== a.verantwortlich_name) {
|
||||
zeile.append(el('span', null, '· Bereich ' + a.creator_name));
|
||||
}
|
||||
if (zeile.childElementCount) k.append(zeile);
|
||||
|
||||
const schritte = el('div', 'karte__schritte');
|
||||
if (ZURUECK[a.status]) schritte.append(knopf('◀ zurück', a, ZURUECK[a.status]));
|
||||
if (WEITER[a.status]) schritte.append(knopf(beschriftung(a.status), a, WEITER[a.status]));
|
||||
if (schritte.childElementCount) k.append(schritte);
|
||||
return k;
|
||||
}
|
||||
|
||||
const beschriftung = (status) => ({
|
||||
offen: 'starten ▶', arbeit: 'zum Review ▶', review: 'erledigt ▶',
|
||||
})[status];
|
||||
|
||||
function knopf(text, aufgabe, neuerStatus) {
|
||||
const b = el('button', 'schritt', text);
|
||||
b.type = 'button';
|
||||
b.addEventListener('click', async () => {
|
||||
b.disabled = true;
|
||||
melde('');
|
||||
try {
|
||||
const antwort = await hole(`/workspace/api/aufgaben/${aufgabe.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: neuerStatus }),
|
||||
});
|
||||
if (!antwort.ok) {
|
||||
const d = await antwort.json().catch(() => ({}));
|
||||
melde(d.fehler === 'nicht_erlaubt'
|
||||
? 'Diese Aufgabe darfst du nicht ändern.'
|
||||
: 'Ändern hat nicht geklappt.');
|
||||
b.disabled = false;
|
||||
return;
|
||||
}
|
||||
await laden();
|
||||
} catch { b.disabled = false; }
|
||||
});
|
||||
return b;
|
||||
}
|
||||
|
||||
function zeichne(aufgaben) {
|
||||
const brett = $('brett');
|
||||
brett.textContent = '';
|
||||
brett.setAttribute('aria-busy', 'false');
|
||||
|
||||
for (const s of SPALTEN) {
|
||||
const eigene = aufgaben.filter((a) => a.status === s.status);
|
||||
const spalte = el('section', 'spalte');
|
||||
spalte.dataset.status = s.status;
|
||||
|
||||
const kopf = el('h2', 'spalte__kopf');
|
||||
kopf.append(el('span', 'spalte__punkt'), el('span', null, s.name),
|
||||
el('span', 'spalte__zahl', String(eigene.length)));
|
||||
spalte.append(kopf);
|
||||
|
||||
if (!eigene.length) spalte.append(el('p', 'spalte__leer', '—'));
|
||||
else for (const a of eigene) spalte.append(karte(a));
|
||||
|
||||
brett.append(spalte);
|
||||
}
|
||||
}
|
||||
|
||||
async function laden() {
|
||||
try {
|
||||
const antwort = await hole('/workspace/api/aufgaben');
|
||||
if (!antwort.ok) { melde('Aufgaben konnten nicht geladen werden.'); return; }
|
||||
zeichne((await antwort.json()).aufgaben);
|
||||
} catch { /* bereits umgeleitet */ }
|
||||
}
|
||||
|
||||
/* ---------- Neue Aufgabe -------------------------------------------- */
|
||||
|
||||
function formularUmschalten(auf) {
|
||||
$('neu').hidden = !auf;
|
||||
$('neu-oeffnen').hidden = auf;
|
||||
if (auf) $('f-titel').focus();
|
||||
}
|
||||
|
||||
$('neu-oeffnen').addEventListener('click', () => formularUmschalten(true));
|
||||
$('neu-abbrechen').addEventListener('click', () => {
|
||||
$('neu').reset(); $('neu-fehler').textContent = ''; formularUmschalten(false);
|
||||
});
|
||||
|
||||
$('neu').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const knopfEl = $('neu-speichern');
|
||||
$('neu-fehler').textContent = '';
|
||||
|
||||
const daten = {
|
||||
titel: $('f-titel').value,
|
||||
beschreibung: $('f-text').value,
|
||||
prioritaet: $('f-prio').value,
|
||||
frist: $('f-frist').value,
|
||||
};
|
||||
if (ich.rolle === 'admin') {
|
||||
daten.creator_id = $('f-creator').value || null;
|
||||
daten.verantwortlich_id = $('f-verant').value || null;
|
||||
}
|
||||
|
||||
knopfEl.disabled = true;
|
||||
try {
|
||||
const antwort = await hole('/workspace/api/aufgaben', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(daten),
|
||||
});
|
||||
if (!antwort.ok) {
|
||||
const d = await antwort.json().catch(() => ({}));
|
||||
$('neu-fehler').textContent = d.fehler || 'Anlegen hat nicht geklappt.';
|
||||
return;
|
||||
}
|
||||
$('neu').reset();
|
||||
formularUmschalten(false);
|
||||
await laden();
|
||||
} catch { /* bereits umgeleitet */ } finally { knopfEl.disabled = false; }
|
||||
});
|
||||
|
||||
/* ---------- Start ----------------------------------------------------- */
|
||||
|
||||
$('abmelden').addEventListener('click', async () => {
|
||||
try { await fetch('/workspace/api/abmelden', { method: 'POST', credentials: 'same-origin' }); }
|
||||
catch { /* egal */ }
|
||||
location.assign('/workspace/');
|
||||
});
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const antwort = await hole('/workspace/api/ich');
|
||||
if (!antwort.ok) { location.assign('/workspace/'); return; }
|
||||
ich = await antwort.json();
|
||||
} catch { return; }
|
||||
|
||||
$('wer').textContent = `${ich.name} · ${ich.rolle}`;
|
||||
|
||||
if (ich.rolle === 'admin') {
|
||||
$('feld-creator').hidden = false;
|
||||
$('feld-verant').hidden = false;
|
||||
try {
|
||||
const { personen } = await (await hole('/workspace/api/personen')).json();
|
||||
for (const p of personen) {
|
||||
for (const ziel of ['f-creator', 'f-verant']) {
|
||||
const o = el('option', null, `${p.name} (${p.rolle})`);
|
||||
o.value = String(p.id);
|
||||
$(ziel).append(o);
|
||||
}
|
||||
}
|
||||
} catch { /* Zuordnung bleibt dann leer -- kein Grund abzubrechen */ }
|
||||
}
|
||||
|
||||
await laden();
|
||||
})();
|
||||
})();
|
||||
@@ -19,7 +19,7 @@
|
||||
{ name: 'Dashboard', phase: 1, rollen: ['admin', 'creator', 'scout'] },
|
||||
{ name: 'Creator-Profile', phase: 1, rollen: ['admin'] },
|
||||
{ name: 'Mein Profil', phase: 1, rollen: ['creator'] },
|
||||
{ name: 'Aufgaben', phase: 1, rollen: ['admin', 'creator'] },
|
||||
{ name: 'Aufgaben', phase: 1, rollen: ['admin', 'creator', 'scout'], ziel: 'aufgaben.html' },
|
||||
{ name: 'Kalender', phase: 1, rollen: ['admin', 'creator', 'scout'] },
|
||||
{ name: 'Dateien', phase: 1, rollen: ['admin', 'creator'] },
|
||||
{ name: 'LIVE-Analyse', phase: 2, rollen: ['admin', 'creator'] },
|
||||
@@ -55,6 +55,7 @@
|
||||
const li = document.createElement('li');
|
||||
li.className = 'bereich';
|
||||
li.dataset.phase = String(b.phase);
|
||||
if (b.ziel) li.dataset.fertig = 'ja';
|
||||
|
||||
const name = document.createElement('span');
|
||||
name.className = 'bereich__name';
|
||||
@@ -62,9 +63,61 @@
|
||||
|
||||
const stand = document.createElement('span');
|
||||
stand.className = 'bereich__stand';
|
||||
stand.textContent = b.phase === 1 ? 'als Nächstes' : `Phase ${b.phase}`;
|
||||
stand.textContent = b.ziel ? 'öffnen' : (b.phase === 1 ? 'als Nächstes' : `Phase ${b.phase}`);
|
||||
|
||||
li.append(name, stand);
|
||||
if (b.ziel) {
|
||||
/* Fertige Bereiche sind echte Verweise -- Tastatur und
|
||||
Rechtsklick funktionieren damit wie gewohnt. */
|
||||
const a = document.createElement('a');
|
||||
a.className = 'bereich__link';
|
||||
a.href = b.ziel;
|
||||
a.append(name, stand);
|
||||
li.append(a);
|
||||
} else {
|
||||
li.append(name, stand);
|
||||
}
|
||||
liste.append(li);
|
||||
}
|
||||
|
||||
await zahlenLaden();
|
||||
}
|
||||
|
||||
/* Die Zahlen kommen aus derselben Quelle wie das Brett und sind bereits
|
||||
serverseitig auf die eigene Rolle eingegrenzt. */
|
||||
const ZAHLEN = [
|
||||
{ schluessel: 'ueberfaellig', name: 'Überfällig', warn: true },
|
||||
{ schluessel: 'heute', name: 'Heute fällig' },
|
||||
{ schluessel: 'offen', name: 'Offen' },
|
||||
{ schluessel: 'arbeit', name: 'In Arbeit' },
|
||||
{ schluessel: 'review', name: 'Review' },
|
||||
{ schluessel: 'erledigt', name: 'Erledigt' },
|
||||
];
|
||||
|
||||
async function zahlenLaden() {
|
||||
let daten;
|
||||
try {
|
||||
const antwort = await fetch('/workspace/api/uebersicht', { credentials: 'same-origin' });
|
||||
if (!antwort.ok) return;
|
||||
daten = await antwort.json();
|
||||
} catch { return; }
|
||||
|
||||
const liste = $('zahlen');
|
||||
liste.textContent = '';
|
||||
for (const z of ZAHLEN) {
|
||||
const wert = daten[z.schluessel] ?? 0;
|
||||
const li = document.createElement('li');
|
||||
li.className = 'zahl';
|
||||
if (z.warn && wert > 0) li.dataset.warn = 'ja';
|
||||
|
||||
const n = document.createElement('span');
|
||||
n.className = 'zahl__wert';
|
||||
n.textContent = String(wert);
|
||||
|
||||
const t = document.createElement('span');
|
||||
t.className = 'zahl__name';
|
||||
t.textContent = z.name;
|
||||
|
||||
li.append(n, t);
|
||||
liste.append(li);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<title>Aufgaben · Creator Workspace</title>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" href="assets/img/favicon.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css" />
|
||||
<link rel="stylesheet" href="assets/css/start.css" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
|
||||
<header class="kopfleiste">
|
||||
<p class="marke"><a class="zurueck" href="start.html">Creator Workspace</a> · Aufgaben</p>
|
||||
<div class="kopfleiste__rechts">
|
||||
<span class="wer" id="wer">…</span>
|
||||
<button type="button" class="abmelden" id="abmelden">Abmelden</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="inhalt inhalt--breit">
|
||||
|
||||
<section class="kopf-zeile">
|
||||
<div>
|
||||
<p class="marke">Aufgaben</p>
|
||||
<h1 class="titel">Offen. In Arbeit. Review. Erledigt.</h1>
|
||||
</div>
|
||||
<button type="button" class="knopf knopf--klein" id="neu-oeffnen">Neue Aufgabe</button>
|
||||
</section>
|
||||
|
||||
<!-- Formular zum Anlegen. Standardmäßig zu, damit das Brett im Vordergrund
|
||||
steht und die Seite nicht mit einem Formular begrüßt. -->
|
||||
<form class="neu" id="neu" hidden>
|
||||
<div class="neu__zeile">
|
||||
<label class="feld-schild" for="f-titel">Titel</label>
|
||||
<input id="f-titel" name="titel" maxlength="160" placeholder="Was ist zu tun?" required />
|
||||
</div>
|
||||
<div class="neu__zeile">
|
||||
<label class="feld-schild" for="f-text">Beschreibung <span class="leise">(optional)</span></label>
|
||||
<textarea id="f-text" name="beschreibung" rows="3" maxlength="4000"
|
||||
placeholder="Worum geht es genau?"></textarea>
|
||||
</div>
|
||||
<div class="neu__raster">
|
||||
<div>
|
||||
<label class="feld-schild" for="f-prio">Priorität</label>
|
||||
<select id="f-prio" name="prioritaet">
|
||||
<option value="hoch">hoch</option>
|
||||
<option value="mittel" selected>mittel</option>
|
||||
<option value="niedrig">niedrig</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="feld-schild" for="f-frist">Frist <span class="leise">(optional)</span></label>
|
||||
<input id="f-frist" name="frist" type="date" />
|
||||
</div>
|
||||
<div id="feld-creator" hidden>
|
||||
<label class="feld-schild" for="f-creator">Bereich von</label>
|
||||
<select id="f-creator" name="creator_id"><option value="">—</option></select>
|
||||
</div>
|
||||
<div id="feld-verant" hidden>
|
||||
<label class="feld-schild" for="f-verant">Verantwortlich</label>
|
||||
<select id="f-verant" name="verantwortlich_id"><option value="">—</option></select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="fehler" id="neu-fehler" role="alert" aria-live="polite"></p>
|
||||
<div class="neu__knoepfe">
|
||||
<button type="submit" class="knopf knopf--klein" id="neu-speichern">Anlegen</button>
|
||||
<button type="button" class="abmelden" id="neu-abbrechen">Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="fehler" id="fehler" role="alert" aria-live="polite"></p>
|
||||
|
||||
<div class="brett" id="brett" aria-busy="true">
|
||||
<p class="leise" id="laedt">Aufgaben werden geladen …</p>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/aufgaben.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -28,10 +28,16 @@
|
||||
<p class="unterzeile" id="rollentext">…</p>
|
||||
</section>
|
||||
|
||||
<!-- Die Bereiche aus dem Konzept. Noch keiner ist gebaut - das steht hier
|
||||
ehrlich dran, statt Kacheln anzubieten, die ins Leere führen. -->
|
||||
<!-- Zahlen zuerst: Das Konzept will "Prioritäten statt Datenfriedhof". -->
|
||||
<section class="zahlen-block">
|
||||
<p class="feldschild">Deine Aufgaben</p>
|
||||
<ul class="zahlen" id="zahlen"></ul>
|
||||
</section>
|
||||
|
||||
<!-- Bereiche aus dem Konzept. Was noch nicht gebaut ist, steht ehrlich
|
||||
dran, statt Kacheln anzubieten, die ins Leere führen. -->
|
||||
<section>
|
||||
<p class="feldschild">Bereiche aus dem Konzept</p>
|
||||
<p class="feldschild">Bereiche</p>
|
||||
<ul class="bereiche" id="bereiche"></ul>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user