Dashboard-Reihenfolge, Drei-Punkte-Menü, Mehrrollen-Kürzel, Login-Rechte

- Meine Aufgaben: Projekte in der Reihenfolge der Projektliste, je Konto
  per Ziehen umsortierbar (users.dashboard_reihenfolge)
- Projektliste: Öffnen/Löschen hinter einem "⋯"-Menü
- Avatare je Person statt je Rolle; mehrere Rollen als senkrechte
  Farbstreifen; Titelleiste zieht Rollenänderungen sofort nach
- Nach dem Anmelden vollständiges Konto über /whoami laden (Seitenleiste
  zeigte sonst erst nach Neuladen alle Einträge)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
erikandClaude Opus 5.5 committed 2026-10-29 15:21:29 +01:00
1 parent b7e0bd0d92
commit a2266662ad
11 files changed
+358 -36

No files matched your search

+112 -9
View File
@@ -130,7 +130,9 @@ async function renderDashboard(){
html += dashAbschnitt('Meine Projekte', daten.projekte.length, null,
renderDashProjekte(daten),
daten.projekte.length > 1
? '<button type="button" class="dash-proj-alle" id="dashAlleKnopf" onclick="dashAlleUmschalten()">'
? '<button type="button" class="dash-proj-alle" id="dashReihenfolgeReset" onclick="dashReihenfolgeZuruecksetzen()"'
+ (dashEigeneReihenfolge().length ? '' : ' hidden') + ' title="Reihenfolge wieder wie in der Projektliste">Wie Projektliste</button>'
+ '<button type="button" class="dash-proj-alle" id="dashAlleKnopf" onclick="dashAlleUmschalten()">'
+ (alleOffen ? 'Alle einklappen' : 'Alle aufklappen') + '</button>'
: '');
html += '</div>';
@@ -214,12 +216,9 @@ function dashDaten(){
relevant.sort((a, b) => (a.datum || '9999').localeCompare(b.datum || '9999'));
projekte.push({ p, rollen, termine: relevant, alleTermine: termine });
});
// Projekte mit dem dringendsten Termin zuerst, Projekte ohne Termin am Ende.
projekte.sort((a, b) => {
const da = (a.termine.find(t => t.datum) || {}).datum || '9999';
const db_ = (b.termine.find(t => t.datum) || {}).datum || '9999';
return da.localeCompare(db_) || String(a.p.nr || '').localeCompare(String(b.p.nr || ''));
});
// Reihenfolge wie in der Projektliste (Gruppe, dann Position darin) —
// oder die eigene, auf dem Dashboard gezogene (dashSortieren()).
dashSortieren(projekte);
// Kalendereinträge: alle datierten Termine dieser Projekte (auch vergangene
// Meilensteine — beim Zurückblättern sollen sie da sein).
@@ -273,6 +272,109 @@ function dashZuSchreiben(set){
}
function dashProjKey(p){ return p.projectUid || p.nr || p.id; }
// ---------- Reihenfolge der Projekte ----------
// Grundlage ist die Reihenfolge der Projektliste (GROUP_ORDER, dann p.order).
// Jedes Konto kann sie auf dem Dashboard durch Ziehen am Griff ändern; die
// eigene Reihenfolge liegt am Server (users.dashboard_reihenfolge, über
// currentUser.dashboardReihenfolge), gilt also auf jedem Gerät. Projekte,
// die darin noch fehlen (neu zugeordnet), stehen nach ihrer Stelle in der
// Projektliste dahinter.
function dashListenRang(p){
const g = GROUP_ORDER.indexOf(p.group);
return (g === -1 ? 999 : g) * 1e6 + (Number(p.order) || 0);
}
function dashEigeneReihenfolge(){
return (currentUser && Array.isArray(currentUser.dashboardReihenfolge)) ? currentUser.dashboardReihenfolge : [];
}
function dashSortieren(projekte){
const eigen = dashEigeneReihenfolge();
const pos = new Map(eigen.map((k, i) => [k, i]));
projekte.sort((a, b) => {
const ia = pos.has(dashProjKey(a.p)) ? pos.get(dashProjKey(a.p)) : Infinity;
const ib = pos.has(dashProjKey(b.p)) ? pos.get(dashProjKey(b.p)) : Infinity;
if(ia !== ib) return ia - ib;
return dashListenRang(a.p) - dashListenRang(b.p);
});
}
function dashReihenfolgeSpeichern(liste){
if(currentUser) currentUser.dashboardReihenfolge = liste;
fetch(baseUrl() + '/dashboard-reihenfolge', {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: JSON.stringify({ reihenfolge: liste })
}).then(r => { if(!r.ok) throw new Error(r.status); })
.catch(() => alert('Die Reihenfolge konnte nicht gespeichert werden.'));
}
// dashReihenfolgeZuruecksetzen: wieder wie in der Projektliste.
function dashReihenfolgeZuruecksetzen(){
dashReihenfolgeSpeichern([]);
renderDashboard();
}
// Ziehen: Der Block wird nur ziehbar, solange der Griff gedrückt ist —
// sonst ließe sich Text in den Terminen nicht mehr markieren.
let dashGezogen = null;
function dashGriffDruck(griff){
const block = griff.closest('.dash-proj');
if(block) block.draggable = true;
}
document.addEventListener('mouseup', () => {
if(dashGezogen) return;
document.querySelectorAll('.dash-proj[draggable="true"]').forEach(b => { b.draggable = false; });
});
document.addEventListener('dragstart', e => {
const block = e.target && e.target.closest && e.target.closest('.dash-proj[draggable="true"]');
if(!block) return;
dashGezogen = block;
block.classList.add('ist-gezogen');
e.dataTransfer.effectAllowed = 'move';
try { e.dataTransfer.setData('text/plain', block.dataset.key || ''); } catch(err){ /* egal */ }
});
document.addEventListener('dragover', e => {
if(!dashGezogen) return;
const liste = dashGezogen.parentElement;
if(!liste || !liste.contains(e.target)) return;
e.preventDefault();
const andere = [...liste.querySelectorAll('.dash-proj:not(.ist-gezogen)')];
const danach = andere.find(b => { const r = b.getBoundingClientRect(); return e.clientY < r.top + r.height / 2; });
if(danach) liste.insertBefore(dashGezogen, danach); else liste.appendChild(dashGezogen);
});
document.addEventListener('drop', e => { if(dashGezogen) e.preventDefault(); });
document.addEventListener('dragend', () => {
if(!dashGezogen) return;
const block = dashGezogen;
dashGezogen = null;
block.classList.remove('ist-gezogen');
block.draggable = false;
const neu = [...block.parentElement.querySelectorAll('.dash-proj')].map(b => b.dataset.key);
const alt = dashEigeneReihenfolge();
// Schon bekannte, gerade nicht angezeigte Projekte (z. B. vorübergehend
// ohne Zuordnung) behalten ihren Platz am Ende.
const liste = neu.concat(alt.filter(k => !neu.includes(k)));
if(JSON.stringify(liste) !== JSON.stringify(alt)) dashReihenfolgeSpeichern(liste);
const reset = document.getElementById('dashReihenfolgeReset');
if(reset) reset.hidden = false;
});
// dashVerschieben: Tastatur-Alternative zum Ziehen (Griff fokussiert,
// Pfeil hoch/runter).
function dashGriffTaste(e, griff){
if(e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
e.preventDefault();
const block = griff.closest('.dash-proj');
const liste = block && block.parentElement;
if(!liste) return;
if(e.key === 'ArrowUp' && block.previousElementSibling) liste.insertBefore(block, block.previousElementSibling);
else if(e.key === 'ArrowDown' && block.nextElementSibling) liste.insertBefore(block.nextElementSibling, block);
else return;
griff.focus();
const neu = [...liste.querySelectorAll('.dash-proj')].map(b => b.dataset.key);
const alt = dashEigeneReihenfolge();
dashReihenfolgeSpeichern(neu.concat(alt.filter(k => !neu.includes(k))));
const reset = document.getElementById('dashReihenfolgeReset');
if(reset) reset.hidden = false;
}
// dashProjUmschalten: klappt einen Projektblock auf oder zu — direkt am DOM,
// ohne das Dashboard neu zu laden. Parameter: knopf (der Kopf-Button).
function dashProjUmschalten(knopf){
@@ -316,7 +418,7 @@ function renderDashProjekte(daten){
if(!daten.projekte.length) return dashLeer('Sie sind in der aktuellen Sitzung keinem Projekt zugeordnet und haben keine offenen Aufgaben.');
const heute = heuteIso();
const zu = dashZuLesen();
return daten.projekte.map(x => {
return '<div class="dash-proj-liste">' + daten.projekte.map(x => {
const p = x.p;
const key = dashProjKey(p);
const istZu = zu.has(key);
@@ -347,6 +449,7 @@ function renderDashProjekte(daten){
+ '<span class="dash-proj-zahl">' + kommend.length + ' offen</span>';
return '<div class="dash-karte dash-proj' + (istZu ? ' ist-zu' : '') + '" data-key="' + esc(key) + '">'
+ '<div class="dash-proj-kopf">'
+ (daten.projekte.length > 1 ? '<span class="dash-proj-griff" tabindex="0" role="button" title="Ziehen, um die Reihenfolge zu ändern (oder Pfeiltasten)" aria-label="Reihenfolge ändern" onmousedown="dashGriffDruck(this)" ontouchstart="dashGriffDruck(this)" onkeydown="dashGriffTaste(event, this)">' + icon('grip') + '</span>' : '')
+ '<button type="button" class="dash-proj-toggle" aria-expanded="' + (!istZu) + '" onclick="dashProjUmschalten(this)" title="Ein-/ausklappen">'
+ '<span class="dash-proj-chev" aria-hidden="true">›</span>'
+ '<span class="dash-proj-nr">' + esc(p.nr || '') + '</span><span class="dash-proj-titel">' + esc(p.name || '') + '</span>'
@@ -360,7 +463,7 @@ function renderDashProjekte(daten){
+ '<div class="dash-proj-termine">' + zeilen + mehr + '</div>'
+ '</div>'
+ '</div>';
}).join('');
}).join('') + '</div>';
}
// renderDashTermin: eine Terminzeile im Projektblock und in der Tagesliste