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:
1 parent
b7e0bd0d92
commit
a2266662ad
11 files changed
+358
-36
No files matched your search
@@ -113,6 +113,96 @@ function roleClass(role){
|
||||
// hier bearbeitbar (nurAnsicht).
|
||||
const BOARD_OPTS = { nurAnsicht: true, nurBeauftragteLph: true };
|
||||
|
||||
const PROJEKT_PUNKTE_SVG = '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><circle cx="3.2" cy="8" r="1.4"/><circle cx="8" cy="8" r="1.4"/><circle cx="12.8" cy="8" r="1.4"/></svg>';
|
||||
|
||||
// ---------- Zeilenmenü der Projektliste ("⋯") ----------
|
||||
// Öffnen und Löschen. Löschen fragt im Menü selbst nach (wie die
|
||||
// Benutzerverwaltung), nicht über confirm(). Das Menü hängt an <body> mit
|
||||
// position: fixed und klappt nach oben, wenn unten kein Platz ist.
|
||||
let projektMenue = null; // { el, knopf, zeile }
|
||||
|
||||
function schliesseProjektMenue(fokusZurueck){
|
||||
if(!projektMenue) return;
|
||||
const { el, knopf, zeile } = projektMenue;
|
||||
projektMenue = null;
|
||||
el.remove();
|
||||
knopf.setAttribute('aria-expanded', 'false');
|
||||
if(zeile) zeile.classList.remove('is-menu-open');
|
||||
if(fokusZurueck && document.contains(knopf)) knopf.focus();
|
||||
}
|
||||
|
||||
function projektMenuePositionieren(){
|
||||
if(!projektMenue) return;
|
||||
const { el, knopf } = projektMenue;
|
||||
const r = knopf.getBoundingClientRect();
|
||||
const m = el.getBoundingClientRect();
|
||||
const nachOben = (window.innerHeight - r.bottom) < m.height + 12 && r.top > m.height + 12;
|
||||
el.classList.toggle('nach-oben', nachOben);
|
||||
el.style.top = (nachOben ? r.top - m.height - 6 : r.bottom + 6) + 'px';
|
||||
el.style.left = Math.max(8, Math.min(r.right - m.width, window.innerWidth - m.width - 8)) + 'px';
|
||||
}
|
||||
|
||||
function oeffneProjektMenue(event, pid, locked){
|
||||
event.stopPropagation();
|
||||
const knopf = event.currentTarget;
|
||||
if(projektMenue && projektMenue.knopf === knopf){ schliesseProjektMenue(true); return; }
|
||||
schliesseProjektMenue(false);
|
||||
const w = currentWeek();
|
||||
const p = w && w.projects.find(x => x.id === pid);
|
||||
const bez = p ? ((p.nr ? p.nr + ' ' : '') + (p.name || '')).trim() : '';
|
||||
const darfLoeschen = !locked && darfProjektlisteAendern();
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.className = 'user-menu';
|
||||
el.setAttribute('role', 'menu');
|
||||
document.body.appendChild(el);
|
||||
const zeile = knopf.closest('.board-row');
|
||||
projektMenue = { el, knopf, zeile };
|
||||
knopf.setAttribute('aria-expanded', 'true');
|
||||
if(zeile) zeile.classList.add('is-menu-open');
|
||||
|
||||
const eintraege = () => {
|
||||
el.innerHTML = `<button type="button" role="menuitem" class="user-menu-item" data-aktion="oeffnen">
|
||||
${icon('folder', 'user-menu-ic')}<span class="user-menu-text"><span>Öffnen</span><span class="user-menu-sub">Projektbereich</span></span></button>`
|
||||
+ (darfLoeschen ? `<hr class="user-menu-trenner"><button type="button" role="menuitem" class="user-menu-item is-danger" data-aktion="loeschen">
|
||||
${icon('trash', 'user-menu-ic')}<span class="user-menu-text"><span>Löschen</span><span class="user-menu-sub">30 Tage im Papierkorb</span></span></button>` : '');
|
||||
projektMenuePositionieren();
|
||||
};
|
||||
eintraege();
|
||||
|
||||
el.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const b = e.target.closest('button');
|
||||
if(!b) return;
|
||||
if(b.dataset.zurueck){ eintraege(); return; }
|
||||
if(b.dataset.aktion === 'oeffnen'){ schliesseProjektMenue(false); openProjectPlan(pid); return; }
|
||||
if(b.dataset.aktion === 'loeschen'){
|
||||
el.innerHTML = `<div class="user-menu-confirm"><p>Projekt ${bez ? '<b>' + esc(bez) + '</b> ' : ''}löschen? Ein Administrator kann es 30 Tage lang wiederherstellen.</p>
|
||||
<div class="user-menu-confirm-row"><button type="button" class="secondary small" data-zurueck="1">Abbrechen</button>
|
||||
<button type="button" class="danger small" data-aktion="endgueltig">Löschen</button></div></div>`;
|
||||
projektMenuePositionieren();
|
||||
el.querySelector('[data-aktion="endgueltig"]').focus();
|
||||
return;
|
||||
}
|
||||
if(b.dataset.aktion === 'endgueltig'){ schliesseProjektMenue(false); delProjectFromList(pid, true); }
|
||||
});
|
||||
|
||||
if(event.detail === 0){ const erstes = el.querySelector('button'); if(erstes) erstes.focus(); }
|
||||
}
|
||||
|
||||
document.addEventListener('click', () => schliesseProjektMenue(false));
|
||||
document.addEventListener('keydown', e => {
|
||||
if(!projektMenue) return;
|
||||
const ziele = Array.from(projektMenue.el.querySelectorAll('button'));
|
||||
const i = ziele.indexOf(document.activeElement);
|
||||
if(e.key === 'Escape'){ e.preventDefault(); schliesseProjektMenue(true); }
|
||||
else if(e.key === 'ArrowDown'){ e.preventDefault(); if(ziele.length) ziele[(i + 1) % ziele.length].focus(); }
|
||||
else if(e.key === 'ArrowUp'){ e.preventDefault(); if(ziele.length) ziele[(i - 1 + ziele.length) % ziele.length].focus(); }
|
||||
else if(e.key === 'Tab') schliesseProjektMenue(false);
|
||||
});
|
||||
window.addEventListener('resize', () => schliesseProjektMenue(false));
|
||||
window.addEventListener('scroll', () => schliesseProjektMenue(false), true);
|
||||
|
||||
function renderBoardRow(p, allPeople, locked, weekLabel, opts){
|
||||
const isOpen = !!boardRowOpen[p.id];
|
||||
const tags = Array.isArray(p.tags) ? p.tags : [];
|
||||
@@ -123,11 +213,10 @@ function renderBoardRow(p, allPeople, locked, weekLabel, opts){
|
||||
// Avatar-Stapel der eingeklappten Zeile: nur tatsächlich zugeordnete
|
||||
// Personen (Zustand 1 = Haupt oder 2 = optional), volle Rollen-Matrix nur
|
||||
// im aufgeklappten Detailpanel (siehe renderBoardDrawer).
|
||||
const stackHtml = allPeople.map(entry=>{
|
||||
const state = p.assign[assignKey(entry.person.id, entry.role)] || 0;
|
||||
if(state === 0) return '';
|
||||
return `<span class="board-avatar ${roleClass(entry.role)} state-${state}" title="${esc(entry.person.fullName || entry.person.name)} · ${esc(entry.role)}">${esc(entry.person.name)}</span>`;
|
||||
}).join('');
|
||||
// Eine Person mit mehreren Rollen erscheint nur einmal (beteiligteJePerson()).
|
||||
const stackHtml = beteiligteJePerson(allPeople.map(entry => ({
|
||||
person: entry.person, role: entry.role, state: p.assign[assignKey(entry.person.id, entry.role)] || 0
|
||||
}))).map(b => beteiligtenAvatarHtml(b, roleClass(b.role))).join('');
|
||||
|
||||
const aktLp = lpActiveList(p)[0];
|
||||
const aktPct = aktLp && p.lph[aktLp] !== undefined && p.lph[aktLp] !== '' ? p.lph[aktLp] : null;
|
||||
@@ -155,9 +244,12 @@ function renderBoardRow(p, allPeople, locked, weekLabel, opts){
|
||||
// Listenoperationen am Zeilenende (aus dem früheren Projekte-Tab). Das
|
||||
// Bearbeiten der Projektangaben führt in den Projektbereich, wo sie ihren
|
||||
// einen Ort haben — hier steht nur, was die LISTE betrifft.
|
||||
// Öffnen/Löschen hinter einem Drei-Punkte-Menü (Eriks Wunsch), gebaut wie
|
||||
// das Zeilenmenü der Benutzerverwaltung (.user-kebab/.user-menu).
|
||||
html += `<div class="board-row-actions" onclick="event.stopPropagation()">
|
||||
<button type="button" class="secondary small" onclick="openProjectPlan('${p.id}')" title="Projektbereich öffnen">Öffnen</button>
|
||||
${locked || !darfProjektlisteAendern() ? '' : `<button type="button" class="danger small" onclick="delProjectFromList('${p.id}')" title="Projekt löschen (30 Tage durch Administratoren wiederherstellbar)">Löschen</button>`}
|
||||
<button type="button" class="user-kebab" aria-haspopup="menu" aria-expanded="false"
|
||||
aria-label="Aktionen für ${esc(p.name || 'Projekt')}" title="Aktionen"
|
||||
onclick="oeffneProjektMenue(event, '${p.id}', ${locked ? 'true' : 'false'})">${PROJEKT_PUNKTE_SVG}</button>
|
||||
</div>`;
|
||||
html += `</div>`; // board-row-main
|
||||
|
||||
|
||||
Reference in new issue
Block a user