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

+48
View File
@@ -87,6 +87,54 @@ function weekSortKey(label){
// Persistente ID fuer eine Person aus dem Kuerzel, damit Anwesenheit und
// Zuordnungen auch nach einem Neuladen stabil bleiben.
// Parameter: name (String). Rückgabe: String wie 'p_ab'.
// beteiligteJePerson: fasst die Zuordnungen eines Projekts ({person, role,
// state}; state 1 = Haupt, 2 = Unterstützung) zu EINEM Eintrag je Person
// zusammen — wer Planung und Projektleitung macht, erscheint im
// Avatar-Stapel nur einmal (Eriks Wunsch). Die Farbe folgt der wichtigsten
// Rolle (Haupt vor Unterstützung, dann Reihenfolge ROLES), der Tooltip nennt
// alle. Rückgabe: [{person, role, state, title}] in der Reihenfolge des
// ersten Auftretens.
function beteiligteJePerson(eintraege){
const jePerson = new Map();
(eintraege || []).forEach(e => {
if(!e || !e.person || !(e.state > 0)) return;
const key = e.person.id;
if(!jePerson.has(key)) jePerson.set(key, { person: e.person, rollen: [] });
jePerson.get(key).rollen.push({ role: e.role, state: Number(e.state) });
});
const rang = r => (r.state === 1 ? 0 : 10) + Math.max(0, ROLES.indexOf(r.role));
return Array.from(jePerson.values()).map(({ person, rollen }) => {
const haupt = rollen.slice().sort((a, b) => rang(a) - rang(b))[0];
const liste = rollen.slice().sort((a, b) => ROLES.indexOf(a.role) - ROLES.indexOf(b.role))
.map(r => r.role + (r.state === 2 ? ' (Unterstützung)' : '')).join(', ');
const sortiert = rollen.slice().sort((a, b) => ROLES.indexOf(a.role) - ROLES.indexOf(b.role));
return { person, role: haupt.role, state: haupt.state, rollen: sortiert, title: (person.fullName || person.name) + ' · ' + liste };
});
}
// ROLLEN_FARBE: dieselben Farben wie .board-avatar.role-* in styles.css.
const ROLLEN_FARBE = { 'Planung': 'var(--green)', 'Projektleitung': 'var(--red)', 'Bauüberwachung': 'var(--accent)' };
// beteiligtenAvatarHtml: Kürzel-Kreis eines Eintrags aus beteiligteJePerson().
// Bei mehreren Rollen (Eriks Wunsch) ist der Kreis in Segmente geteilt — je
// Rolle eines in ihrer Farbe. Parameter: b, klasse
// (Rollen-Klasse, z. B. roleClass(b.role)). Rückgabe: HTML-String.
function beteiligtenAvatarHtml(b, klasse){
const rollen = (b.rollen || []).filter(r => ROLLEN_FARBE[r.role]);
if(rollen.length < 2){
return `<span class="board-avatar ${klasse} state-${b.state}" title="${esc(b.title)}">${esc(b.person.name)}</span>`;
}
const teil = 100 / rollen.length;
const stopps = rollen.map((r, i) => {
// Immer die volle Rollenfarbe — eine blassere Abstufung für
// Unterstützung wurde im Dunkelmodus zu einem eigenen, dunklen Ton.
// Ob Haupt oder Unterstützung, steht im Tooltip.
const farbe = ROLLEN_FARBE[r.role];
return `${farbe} ${(i * teil).toFixed(2)}% ${((i + 1) * teil).toFixed(2)}%`;
}).join(', ');
return `<span class="board-avatar ist-mehrfach" style="background-image: linear-gradient(to right, ${stopps})" title="${esc(b.title)}">${esc(b.person.name)}</span>`;
}
function personIdForName(name){
return 'p_' + String(name).trim().toLowerCase().replace(/[^a-z0-9]/g,'_');
}