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
@@ -1210,6 +1210,14 @@ The split follows what kind of data it is:
|
|||||||
|
|
||||||
### Eine Projektliste (tab `board`, labelled "Projekte")
|
### Eine Projektliste (tab `board`, labelled "Projekte")
|
||||||
|
|
||||||
|
- **Avatar-Stapel je Person, nicht je Rolle** (Projektliste und Titelzeile
|
||||||
|
des Projektbereichs, Eriks Wunsch): `beteiligteJePerson()` (`js/utils.js`)
|
||||||
|
fasst die Rollen einer Person zusammen; `beteiligtenAvatarHtml()` zeichnet
|
||||||
|
bei mehreren Rollen einen Kreis aus senkrechten Streifen in den
|
||||||
|
Rollenfarben (`linear-gradient(to right …)`, `ROLLEN_FARBE`, immer volle
|
||||||
|
Farbe — Haupt/Unterstützung nur im Tooltip,
|
||||||
|
`.board-avatar.ist-mehrfach`), der Tooltip nennt alle Rollen.
|
||||||
|
|
||||||
The former `Projekte` tab is gone; `renderBoard()` is the single project list.
|
The former `Projekte` tab is gone; `renderBoard()` is the single project list.
|
||||||
It kept everything that concerns the LIST and gave up everything that concerns
|
It kept everything that concerns the LIST and gave up everything that concerns
|
||||||
a single project's data:
|
a single project's data:
|
||||||
@@ -1226,9 +1234,12 @@ a single project's data:
|
|||||||
- All four groups from `GROUP_ORDER` are pre-seeded even when empty, so a group
|
- All four groups from `GROUP_ORDER` are pre-seeded even when empty, so a group
|
||||||
emptied by dragging stays a drop target (a bug already fixed once in the old
|
emptied by dragging stays a drop target (a bug already fixed once in the old
|
||||||
Projekte tab).
|
Projekte tab).
|
||||||
- `.board-row-actions` at the end of each row: "Öffnen" (Projektbereich) and
|
- `.board-row-actions` at the end of each row: a "⋯" button
|
||||||
"Löschen" (`delProjectFromList()`), hidden until hover/focus so the list
|
(`oeffneProjektMenue()` in `board.js`, Eriks Wunsch; same `.user-kebab`/
|
||||||
stays calm. "+ Projekt" sits in the list's title row now (it was only in the
|
`.user-menu` styling as the Benutzerverwaltung) with "Öffnen"
|
||||||
|
(Projektbereich) and "Löschen" — the latter confirms inside the menu and
|
||||||
|
then calls `delProjectFromList(id, true)` without `confirm()`. Hidden until
|
||||||
|
hover/focus so the list stays calm. "+ Projekt" sits in the list's title row now (it was only in the
|
||||||
sidebar before); the sidebar entry stays and opens the same mask.
|
sidebar before); the sidebar entry stays and opens the same mask.
|
||||||
- `js/projects/projects.js` no longer renders anything by default:
|
- `js/projects/projects.js` no longer renders anything by default:
|
||||||
`renderProjects()` returns immediately unless `#projectsList` exists. Every
|
`renderProjects()` returns immediately unless `#projectsList` exists. Every
|
||||||
@@ -3540,7 +3551,15 @@ Zuständigen, Meilensteine eine Raute (`--meilenstein`, eigenes Token). **Jeder
|
|||||||
Block ist einklappbar** (`dashProjUmschalten()`, „Alle einklappen/aufklappen“
|
Block ist einklappbar** (`dashProjUmschalten()`, „Alle einklappen/aufklappen“
|
||||||
in der Abschnittsüberschrift); eingeklappt zeigt der Kopf „N überfällig / N
|
in der Abschnittsüberschrift); eingeklappt zeigt der Kopf „N überfällig / N
|
||||||
offen“. Der Zustand liegt je `projectUid` in `localStorage`
|
offen“. Der Zustand liegt je `projectUid` in `localStorage`
|
||||||
(`projuebersicht-dash-zu`) — reiner UI-Zustand. **Rechts ein Monatskalender**
|
(`projuebersicht-dash-zu`) — reiner UI-Zustand. **Reihenfolge der Projekte** (Eriks Wunsch): wie in der Projektliste
|
||||||
|
(`GROUP_ORDER`, dann `p.order`; `dashSortieren()`), nicht mehr nach dem
|
||||||
|
dringendsten Termin. Jedes Konto kann sie am Griff vorn im Projektkopf
|
||||||
|
umziehen (oder Griff fokussieren + Pfeiltasten); gespeichert je Konto am
|
||||||
|
Server (`users.dashboard_reihenfolge`, JSON-Liste von `projectUid`s,
|
||||||
|
Migration in `migrateUsersDepartment()`, `POST /dashboard-reihenfolge`,
|
||||||
|
geliefert als `req.user.dashboardReihenfolge` über `/whoami`). Projekte, die
|
||||||
|
darin fehlen, folgen dahinter in Listenreihenfolge; „Wie Projektliste“ setzt
|
||||||
|
zurück. **Rechts ein Monatskalender**
|
||||||
(`renderDashKalender()`, KW-Spalte, Feiertage aus `HOLIDAYS`, Punkte für
|
(`renderDashKalender()`, KW-Spalte, Feiertage aus `HOLIDAYS`, Punkte für
|
||||||
eigene Aufgabe / Projektaufgabe / Meilenstein / überfällig / Abwesenheit, Klick
|
eigene Aufgabe / Projektaufgabe / Meilenstein / überfällig / Abwesenheit, Klick
|
||||||
auf einen Tag listet ihn), darunter Freigaben und Abwesenheiten.
|
auf einen Tag listet ihn), darunter Freigaben und Abwesenheiten.
|
||||||
|
|||||||
+8
-8
@@ -38,7 +38,7 @@
|
|||||||
<link rel="icon" type="image/png" href="/logo.png">
|
<link rel="icon" type="image/png" href="/logo.png">
|
||||||
<link rel="alternate icon" href="/logo.ico">
|
<link rel="alternate icon" href="/logo.ico">
|
||||||
<link rel="apple-touch-icon" href="/logo.png">
|
<link rel="apple-touch-icon" href="/logo.png">
|
||||||
<link rel="stylesheet" href="/styles.css?v=102">
|
<link rel="stylesheet" href="/styles.css?v=106">
|
||||||
<link rel="stylesheet" href="/plan-styles.css?v=43">
|
<link rel="stylesheet" href="/plan-styles.css?v=43">
|
||||||
<!-- Glas-Darstellung (public/glas.css, als letztes Stylesheet): greift nur mit
|
<!-- Glas-Darstellung (public/glas.css, als letztes Stylesheet): greift nur mit
|
||||||
der Klasse html.glas. Gesetzt wird sie hier, vor dem ersten Zeichnen,
|
der Klasse html.glas. Gesetzt wird sie hier, vor dem ersten Zeichnen,
|
||||||
@@ -1913,21 +1913,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/globals.js?v=8"></script>
|
<script src="/js/globals.js?v=8"></script>
|
||||||
<script src="/js/utils.js?v=25"></script>
|
<script src="/js/utils.js?v=30"></script>
|
||||||
<script src="/js/icons.js?v=2"></script>
|
<script src="/js/icons.js?v=2"></script>
|
||||||
<script src="/js/ui.js?v=9"></script>
|
<script src="/js/ui.js?v=9"></script>
|
||||||
<script src="/js/date-picker.js?v=5"></script>
|
<script src="/js/date-picker.js?v=5"></script>
|
||||||
<script src="/js/server.js?v=7"></script>
|
<script src="/js/server.js?v=7"></script>
|
||||||
<script src="/js/auth.js?v=9"></script>
|
<script src="/js/auth.js?v=10"></script>
|
||||||
<script src="/js/store.js?v=14"></script>
|
<script src="/js/store.js?v=14"></script>
|
||||||
<script src="/js/weeks.js?v=11"></script>
|
<script src="/js/weeks.js?v=11"></script>
|
||||||
<script src="/js/board/board.js?v=38"></script>
|
<script src="/js/board/board.js?v=41"></script>
|
||||||
<script src="/js/wochenbericht/wochenbericht.js?v=28"></script>
|
<script src="/js/wochenbericht/wochenbericht.js?v=28"></script>
|
||||||
<script src="/js/people/people.js?v=8"></script>
|
<script src="/js/people/people.js?v=8"></script>
|
||||||
<script src="/js/planung/planung.js?v=3"></script>
|
<script src="/js/planung/planung.js?v=3"></script>
|
||||||
<script src="/js/planchecker/planchecker.js?v=1"></script>
|
<script src="/js/planchecker/planchecker.js?v=1"></script>
|
||||||
<script src="/js/abwesenheit/abwesenheit.js?v=9"></script>
|
<script src="/js/abwesenheit/abwesenheit.js?v=9"></script>
|
||||||
<script src="/js/projects/projects.js?v=9"></script>
|
<script src="/js/projects/projects.js?v=10"></script>
|
||||||
<script src="/js/load/load.js?v=23"></script>
|
<script src="/js/load/load.js?v=23"></script>
|
||||||
<!-- auftrag.js: Auftragsstatus und Dialog "Beauftragung". Nach load.js, weil
|
<!-- auftrag.js: Auftragsstatus und Dialog "Beauftragung". Nach load.js, weil
|
||||||
der Neues-Projekt-Dialog dort die Statusauswahl mit AUFTRAGSSTATUS füllt. -->
|
der Neues-Projekt-Dialog dort die Statusauswahl mit AUFTRAGSSTATUS füllt. -->
|
||||||
@@ -1935,7 +1935,7 @@
|
|||||||
<!-- Assistent "Neues Projekt": braucht auftrag.js (Auftragsstatus) und
|
<!-- Assistent "Neues Projekt": braucht auftrag.js (Auftragsstatus) und
|
||||||
load.js (faktorFormat), steht deshalb dahinter. -->
|
load.js (faktorFormat), steht deshalb dahinter. -->
|
||||||
<script src="/js/projekt-wizard.js?v=6"></script>
|
<script src="/js/projekt-wizard.js?v=6"></script>
|
||||||
<script src="/js/dashboard/dashboard.js?v=9"></script>
|
<script src="/js/dashboard/dashboard.js?v=10"></script>
|
||||||
<script src="/js/benachrichtigungen.js?v=5"></script>
|
<script src="/js/benachrichtigungen.js?v=5"></script>
|
||||||
<!-- wand.js braucht berechneAuslastung()/loadAmpelColor()/kapazitaetsFaktor()/
|
<!-- wand.js braucht berechneAuslastung()/loadAmpelColor()/kapazitaetsFaktor()/
|
||||||
stundenFormat() (load.js), classifyProjectChange()/findPreviousWeek()
|
stundenFormat() (load.js), classifyProjectChange()/findPreviousWeek()
|
||||||
@@ -1957,7 +1957,7 @@
|
|||||||
(das seine Vorwochen-Helfer nutzt), und alle zusammen muessen nach
|
(das seine Vorwochen-Helfer nutzt), und alle zusammen muessen nach
|
||||||
js/gantt.js kommen, dessen buildGanttHtml() plan-build-gantt.js
|
js/gantt.js kommen, dessen buildGanttHtml() plan-build-gantt.js
|
||||||
umhuellt. -->
|
umhuellt. -->
|
||||||
<script src="/js/plan/plan-state.js?v=19"></script>
|
<script src="/js/plan/plan-state.js?v=21"></script>
|
||||||
<script src="/js/plan/plan-sections.js?v=10"></script>
|
<script src="/js/plan/plan-sections.js?v=10"></script>
|
||||||
<script src="/js/plan/plan-info.js?v=14"></script>
|
<script src="/js/plan/plan-info.js?v=14"></script>
|
||||||
<script src="/js/plan/plan-history.js?v=4"></script>
|
<script src="/js/plan/plan-history.js?v=4"></script>
|
||||||
@@ -1977,7 +1977,7 @@
|
|||||||
invalidateHonorarConfig(), damit eine Aenderung der Anteile auch seinen
|
invalidateHonorarConfig(), damit eine Aenderung der Anteile auch seinen
|
||||||
Zwischenspeicher verwirft. -->
|
Zwischenspeicher verwirft. -->
|
||||||
<script src="/js/plan/plan-leistung.js?v=1"></script>
|
<script src="/js/plan/plan-leistung.js?v=1"></script>
|
||||||
<script src="/js/plan/plan-settings.js?v=13"></script>
|
<script src="/js/plan/plan-settings.js?v=14"></script>
|
||||||
<script src="/js/plan/plan-schedule.js?v=6"></script>
|
<script src="/js/plan/plan-schedule.js?v=6"></script>
|
||||||
<script src="/js/tabs-auth.js?v=17"></script>
|
<script src="/js/tabs-auth.js?v=17"></script>
|
||||||
<script src="/js/pdf.js?v=31"></script>
|
<script src="/js/pdf.js?v=31"></script>
|
||||||
|
|||||||
@@ -301,7 +301,16 @@ async function checkAuthAndInit(onAuthenticated){
|
|||||||
document.getElementById('loginPassword').select();
|
document.getElementById('loginPassword').select();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Das vollständige Konto (Rechte, Rollenname, Gewerk, Rundgang …)
|
||||||
|
// liefert nur /whoami — die Antwort von /login enthält nur das Nötigste.
|
||||||
|
// Ohne diesen Abruf fehlten nach dem Anmelden alle Einträge der
|
||||||
|
// Seitenleiste, die ein Recht brauchen, bis man die Seite neu lud.
|
||||||
currentUser = d.user;
|
currentUser = d.user;
|
||||||
|
try {
|
||||||
|
const w = await fetch(baseUrl() + '/whoami');
|
||||||
|
const wd = await w.json();
|
||||||
|
if(wd && wd.user) currentUser = wd.user;
|
||||||
|
} catch(e){ /* Rückfall auf d.user */ }
|
||||||
document.getElementById('loginPassword').value = '';
|
document.getElementById('loginPassword').value = '';
|
||||||
afterAuthenticated(onAuthenticated);
|
afterAuthenticated(onAuthenticated);
|
||||||
} catch(err){
|
} catch(err){
|
||||||
|
|||||||
@@ -113,6 +113,96 @@ function roleClass(role){
|
|||||||
// hier bearbeitbar (nurAnsicht).
|
// hier bearbeitbar (nurAnsicht).
|
||||||
const BOARD_OPTS = { nurAnsicht: true, nurBeauftragteLph: true };
|
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){
|
function renderBoardRow(p, allPeople, locked, weekLabel, opts){
|
||||||
const isOpen = !!boardRowOpen[p.id];
|
const isOpen = !!boardRowOpen[p.id];
|
||||||
const tags = Array.isArray(p.tags) ? p.tags : [];
|
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
|
// Avatar-Stapel der eingeklappten Zeile: nur tatsächlich zugeordnete
|
||||||
// Personen (Zustand 1 = Haupt oder 2 = optional), volle Rollen-Matrix nur
|
// Personen (Zustand 1 = Haupt oder 2 = optional), volle Rollen-Matrix nur
|
||||||
// im aufgeklappten Detailpanel (siehe renderBoardDrawer).
|
// im aufgeklappten Detailpanel (siehe renderBoardDrawer).
|
||||||
const stackHtml = allPeople.map(entry=>{
|
// Eine Person mit mehreren Rollen erscheint nur einmal (beteiligteJePerson()).
|
||||||
const state = p.assign[assignKey(entry.person.id, entry.role)] || 0;
|
const stackHtml = beteiligteJePerson(allPeople.map(entry => ({
|
||||||
if(state === 0) return '';
|
person: entry.person, role: entry.role, state: p.assign[assignKey(entry.person.id, entry.role)] || 0
|
||||||
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>`;
|
}))).map(b => beteiligtenAvatarHtml(b, roleClass(b.role))).join('');
|
||||||
}).join('');
|
|
||||||
|
|
||||||
const aktLp = lpActiveList(p)[0];
|
const aktLp = lpActiveList(p)[0];
|
||||||
const aktPct = aktLp && p.lph[aktLp] !== undefined && p.lph[aktLp] !== '' ? p.lph[aktLp] : null;
|
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
|
// Listenoperationen am Zeilenende (aus dem früheren Projekte-Tab). Das
|
||||||
// Bearbeiten der Projektangaben führt in den Projektbereich, wo sie ihren
|
// Bearbeiten der Projektangaben führt in den Projektbereich, wo sie ihren
|
||||||
// einen Ort haben — hier steht nur, was die LISTE betrifft.
|
// 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()">
|
html += `<div class="board-row-actions" onclick="event.stopPropagation()">
|
||||||
<button type="button" class="secondary small" onclick="openProjectPlan('${p.id}')" title="Projektbereich öffnen">Öffnen</button>
|
<button type="button" class="user-kebab" aria-haspopup="menu" aria-expanded="false"
|
||||||
${locked || !darfProjektlisteAendern() ? '' : `<button type="button" class="danger small" onclick="delProjectFromList('${p.id}')" title="Projekt löschen (30 Tage durch Administratoren wiederherstellbar)">Löschen</button>`}
|
aria-label="Aktionen für ${esc(p.name || 'Projekt')}" title="Aktionen"
|
||||||
|
onclick="oeffneProjektMenue(event, '${p.id}', ${locked ? 'true' : 'false'})">${PROJEKT_PUNKTE_SVG}</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
html += `</div>`; // board-row-main
|
html += `</div>`; // board-row-main
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,9 @@ async function renderDashboard(){
|
|||||||
html += dashAbschnitt('Meine Projekte', daten.projekte.length, null,
|
html += dashAbschnitt('Meine Projekte', daten.projekte.length, null,
|
||||||
renderDashProjekte(daten),
|
renderDashProjekte(daten),
|
||||||
daten.projekte.length > 1
|
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>'
|
+ (alleOffen ? 'Alle einklappen' : 'Alle aufklappen') + '</button>'
|
||||||
: '');
|
: '');
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
@@ -214,12 +216,9 @@ function dashDaten(){
|
|||||||
relevant.sort((a, b) => (a.datum || '9999').localeCompare(b.datum || '9999'));
|
relevant.sort((a, b) => (a.datum || '9999').localeCompare(b.datum || '9999'));
|
||||||
projekte.push({ p, rollen, termine: relevant, alleTermine: termine });
|
projekte.push({ p, rollen, termine: relevant, alleTermine: termine });
|
||||||
});
|
});
|
||||||
// Projekte mit dem dringendsten Termin zuerst, Projekte ohne Termin am Ende.
|
// Reihenfolge wie in der Projektliste (Gruppe, dann Position darin) —
|
||||||
projekte.sort((a, b) => {
|
// oder die eigene, auf dem Dashboard gezogene (dashSortieren()).
|
||||||
const da = (a.termine.find(t => t.datum) || {}).datum || '9999';
|
dashSortieren(projekte);
|
||||||
const db_ = (b.termine.find(t => t.datum) || {}).datum || '9999';
|
|
||||||
return da.localeCompare(db_) || String(a.p.nr || '').localeCompare(String(b.p.nr || ''));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Kalendereinträge: alle datierten Termine dieser Projekte (auch vergangene
|
// Kalendereinträge: alle datierten Termine dieser Projekte (auch vergangene
|
||||||
// Meilensteine — beim Zurückblättern sollen sie da sein).
|
// 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; }
|
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,
|
// dashProjUmschalten: klappt einen Projektblock auf oder zu — direkt am DOM,
|
||||||
// ohne das Dashboard neu zu laden. Parameter: knopf (der Kopf-Button).
|
// ohne das Dashboard neu zu laden. Parameter: knopf (der Kopf-Button).
|
||||||
function dashProjUmschalten(knopf){
|
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.');
|
if(!daten.projekte.length) return dashLeer('Sie sind in der aktuellen Sitzung keinem Projekt zugeordnet und haben keine offenen Aufgaben.');
|
||||||
const heute = heuteIso();
|
const heute = heuteIso();
|
||||||
const zu = dashZuLesen();
|
const zu = dashZuLesen();
|
||||||
return daten.projekte.map(x => {
|
return '<div class="dash-proj-liste">' + daten.projekte.map(x => {
|
||||||
const p = x.p;
|
const p = x.p;
|
||||||
const key = dashProjKey(p);
|
const key = dashProjKey(p);
|
||||||
const istZu = zu.has(key);
|
const istZu = zu.has(key);
|
||||||
@@ -347,6 +449,7 @@ function renderDashProjekte(daten){
|
|||||||
+ '<span class="dash-proj-zahl">' + kommend.length + ' offen</span>';
|
+ '<span class="dash-proj-zahl">' + kommend.length + ' offen</span>';
|
||||||
return '<div class="dash-karte dash-proj' + (istZu ? ' ist-zu' : '') + '" data-key="' + esc(key) + '">'
|
return '<div class="dash-karte dash-proj' + (istZu ? ' ist-zu' : '') + '" data-key="' + esc(key) + '">'
|
||||||
+ '<div class="dash-proj-kopf">'
|
+ '<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">'
|
+ '<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-chev" aria-hidden="true">›</span>'
|
||||||
+ '<span class="dash-proj-nr">' + esc(p.nr || '') + '</span><span class="dash-proj-titel">' + esc(p.name || '') + '</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 class="dash-proj-termine">' + zeilen + mehr + '</div>'
|
||||||
+ '</div>'
|
+ '</div>'
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
}).join('');
|
}).join('') + '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderDashTermin: eine Terminzeile im Projektblock und in der Tagesliste
|
// renderDashTermin: eine Terminzeile im Projektblock und in der Tagesliste
|
||||||
|
|||||||
@@ -329,6 +329,10 @@
|
|||||||
window.renderPlanSettings = function(){
|
window.renderPlanSettings = function(){
|
||||||
const p = app.project;
|
const p = app.project;
|
||||||
if(!p) return;
|
if(!p) return;
|
||||||
|
// Kürzel in der Titelleiste mitziehen — sonst zeigte sie eine geänderte
|
||||||
|
// Rolle erst nach dem Neuladen (renderPlanPage() baut sie nur beim Öffnen).
|
||||||
|
const kopfTeam = document.getElementById('planPeopleList');
|
||||||
|
if(kopfTeam) kopfTeam.innerHTML = app.renderProjectAssignees();
|
||||||
const locked = app.stammGesperrt();
|
const locked = app.stammGesperrt();
|
||||||
const sandbox = app.isSandboxPlan();
|
const sandbox = app.isSandboxPlan();
|
||||||
|
|
||||||
|
|||||||
@@ -313,10 +313,8 @@ window.PlanApp = {
|
|||||||
function renderProjectAssignees(){
|
function renderProjectAssignees(){
|
||||||
const assignees = projectAssignees(app.project, app.week);
|
const assignees = projectAssignees(app.project, app.week);
|
||||||
if(assignees.length === 0) return '';
|
if(assignees.length === 0) return '';
|
||||||
return assignees.map(a => {
|
// Eine Person mit mehreren Rollen nur einmal (beteiligteJePerson(), utils.js).
|
||||||
const title = (a.person.fullName || a.person.name) + (a.role ? ' · ' + a.role : '');
|
return beteiligteJePerson(assignees).map(b => beteiligtenAvatarHtml(b, planRoleClass(b.role))).join('');
|
||||||
return `<span class="board-avatar ${planRoleClass(a.role)} state-${a.state}" title="${esc(title)}">${esc(a.person.name)}</span>`;
|
|
||||||
}).join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// savePlan: merkt die Sitzung des Projektbereichs als geändert vor und
|
// savePlan: merkt die Sitzung des Projektbereichs als geändert vor und
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
// durchgesetzt). Der Server legt das Projekt beim Speichern in den
|
// durchgesetzt). Der Server legt das Projekt beim Speichern in den
|
||||||
// Papierkorb, aus dem es ein Administrator 30 Tage lang zurückholen kann
|
// Papierkorb, aus dem es ein Administrator 30 Tage lang zurückholen kann
|
||||||
// (Admin → Papierkorb, js/admin/admin.js).
|
// (Admin → Papierkorb, js/admin/admin.js).
|
||||||
function delProjectFromList(id){
|
function delProjectFromList(id, ohneRueckfrage){
|
||||||
if(guardWeekLocked()) return;
|
if(guardWeekLocked()) return;
|
||||||
if(!darfProjektlisteAendern()){
|
if(!darfProjektlisteAendern()){
|
||||||
alert('Projekte löschen dürfen nur Projektleiter und Administratoren.');
|
alert('Projekte löschen dürfen nur Projektleiter und Administratoren.');
|
||||||
@@ -38,7 +38,7 @@ function delProjectFromList(id){
|
|||||||
}
|
}
|
||||||
const p = currentWeek().projects.find(x => x.id === id);
|
const p = currentWeek().projects.find(x => x.id === id);
|
||||||
const bez = p ? ((p.nr ? p.nr + ' ' : '') + (p.name || '')).trim() : '';
|
const bez = p ? ((p.nr ? p.nr + ' ' : '') + (p.name || '')).trim() : '';
|
||||||
if(!confirm('Projekt ' + (bez ? '„' + bez + '" ' : '') + 'löschen?\n\nEin Administrator kann es 30 Tage lang wiederherstellen.')) return;
|
if(!ohneRueckfrage && !confirm('Projekt ' + (bez ? '„' + bez + '" ' : '') + 'löschen?\n\nEin Administrator kann es 30 Tage lang wiederherstellen.')) return;
|
||||||
const w = currentWeek();
|
const w = currentWeek();
|
||||||
w.projects = w.projects.filter(p=>p.id!==id);
|
w.projects = w.projects.filter(p=>p.id!==id);
|
||||||
save(); renderProjects(); renderBoard();
|
save(); renderProjects(); renderBoard();
|
||||||
|
|||||||
@@ -87,6 +87,54 @@ function weekSortKey(label){
|
|||||||
// Persistente ID fuer eine Person aus dem Kuerzel, damit Anwesenheit und
|
// Persistente ID fuer eine Person aus dem Kuerzel, damit Anwesenheit und
|
||||||
// Zuordnungen auch nach einem Neuladen stabil bleiben.
|
// Zuordnungen auch nach einem Neuladen stabil bleiben.
|
||||||
// Parameter: name (String). Rückgabe: String wie 'p_ab'.
|
// 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){
|
function personIdForName(name){
|
||||||
return 'p_' + String(name).trim().toLowerCase().replace(/[^a-z0-9]/g,'_');
|
return 'p_' + String(name).trim().toLowerCase().replace(/[^a-z0-9]/g,'_');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4802,6 +4802,7 @@ tr.is-menu-open { background: var(--surface-alt); }
|
|||||||
}
|
}
|
||||||
.board-row:hover .board-row-actions,
|
.board-row:hover .board-row-actions,
|
||||||
.board-row.open .board-row-actions,
|
.board-row.open .board-row-actions,
|
||||||
|
.board-row.is-menu-open .board-row-actions,
|
||||||
.board-row-actions:focus-within{ opacity: 1; }
|
.board-row-actions:focus-within{ opacity: 1; }
|
||||||
|
|
||||||
/* Der Ziehgriff steht vor dem Aufklapp-Pfeil. */
|
/* Der Ziehgriff steht vor dem Aufklapp-Pfeil. */
|
||||||
@@ -6172,3 +6173,29 @@ html.glas .abw-tl-wer { background: var(--glas-deckend, var(--surface)); }
|
|||||||
/* Verdeckte Abwesenheit einer anderen Person: nur „abwesend“, neutral, nicht änderbar */
|
/* Verdeckte Abwesenheit einer anderen Person: nur „abwesend“, neutral, nicht änderbar */
|
||||||
.abw-verdeckt-zeile { --abw-c: var(--text-subtle); }
|
.abw-verdeckt-zeile { --abw-c: var(--text-subtle); }
|
||||||
.abw-verdeckt-zeile .board-row-main { cursor: default; }
|
.abw-verdeckt-zeile .board-row-main { cursor: default; }
|
||||||
|
|
||||||
|
/* ---------- Meine Aufgaben: eigene Reihenfolge der Projekte ----------
|
||||||
|
Griff vorn im Projektkopf (dashGriffDruck()/dashGriffTaste(),
|
||||||
|
js/dashboard/dashboard.js). */
|
||||||
|
.dash-proj-griff {
|
||||||
|
display: inline-flex; align-items: center; margin-right: -4px;
|
||||||
|
color: var(--text-subtle); cursor: grab; border-radius: var(--r-sm);
|
||||||
|
}
|
||||||
|
.dash-proj-griff:hover { color: var(--text); }
|
||||||
|
.dash-proj-griff:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.dash-proj-griff svg { width: 14px; height: 14px; }
|
||||||
|
.dash-proj.ist-gezogen { opacity: .5; }
|
||||||
|
#dashReihenfolgeReset:not([hidden]) + #dashAlleKnopf { margin-left: 10px; }
|
||||||
|
|
||||||
|
/* Kürzel-Kreis einer Person mit mehreren Rollen (beteiligtenAvatarHtml(),
|
||||||
|
js/utils.js): Segmente in den Rollenfarben, weiße Schrift mit Schatten. */
|
||||||
|
.board-avatar.ist-mehrfach {
|
||||||
|
/* Das Verlaufsbild beginnt an der Außenkante des Rands und wiederholt sich
|
||||||
|
nicht — sonst kachelt es in den (durchsichtigen) Rand hinein, und links
|
||||||
|
und rechts erscheint je ein schmaler Streifen der jeweils anderen Farbe. */
|
||||||
|
background-color: transparent;
|
||||||
|
background-origin: border-box;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
color: #fff; border-color: transparent;
|
||||||
|
text-shadow: 0 0 2px rgba(0,0,0,.55), 0 0 1px rgba(0,0,0,.6);
|
||||||
|
}
|
||||||
@@ -3035,6 +3035,13 @@ function migrateUsersDepartment(){
|
|||||||
db.exec("ALTER TABLE users ADD COLUMN einfuehrung_am TEXT NOT NULL DEFAULT ''");
|
db.exec("ALTER TABLE users ADD COLUMN einfuehrung_am TEXT NOT NULL DEFAULT ''");
|
||||||
console.log('Benutzer-Tabelle um einfuehrung_am ergänzt.');
|
console.log('Benutzer-Tabelle um einfuehrung_am ergänzt.');
|
||||||
}
|
}
|
||||||
|
// dashboard_reihenfolge: eigene Reihenfolge der Projekte auf "Meine
|
||||||
|
// Aufgaben" (JSON-Liste von Projekt-Schlüsseln, meist project_uid); leer =
|
||||||
|
// wie in der Projektliste. Je Konto, damit sie auf jedem Gerät gilt.
|
||||||
|
if(!cols.includes('dashboard_reihenfolge')){
|
||||||
|
db.exec("ALTER TABLE users ADD COLUMN dashboard_reihenfolge TEXT NOT NULL DEFAULT ''");
|
||||||
|
console.log('Benutzer-Tabelle um dashboard_reihenfolge ergänzt.');
|
||||||
|
}
|
||||||
if(!cols.includes('department')){
|
if(!cols.includes('department')){
|
||||||
db.exec("ALTER TABLE users ADD COLUMN department TEXT NOT NULL DEFAULT ''");
|
db.exec("ALTER TABLE users ADD COLUMN department TEXT NOT NULL DEFAULT ''");
|
||||||
console.log('Benutzer-Tabelle um department ergänzt.');
|
console.log('Benutzer-Tabelle um department ergänzt.');
|
||||||
@@ -3652,7 +3659,7 @@ function attachUser(req, res, next){
|
|||||||
u.display_name AS display_name, u.email AS email, u.role AS role,
|
u.display_name AS display_name, u.email AS email, u.role AS role,
|
||||||
u.person_id AS person_id, u.active AS active,
|
u.person_id AS person_id, u.active AS active,
|
||||||
u.must_change_password AS must_change_password, u.department AS department,
|
u.must_change_password AS must_change_password, u.department AS department,
|
||||||
u.einfuehrung_am AS einfuehrung_am
|
u.einfuehrung_am AS einfuehrung_am, u.dashboard_reihenfolge AS dashboard_reihenfolge
|
||||||
FROM user_sessions s JOIN users u ON u.id = s.user_id
|
FROM user_sessions s JOIN users u ON u.id = s.user_id
|
||||||
WHERE s.token = ?
|
WHERE s.token = ?
|
||||||
`).get(token);
|
`).get(token);
|
||||||
@@ -3670,7 +3677,9 @@ function attachUser(req, res, next){
|
|||||||
// jedes Mal manuell das eigene Gewerk wählen muss.
|
// jedes Mal manuell das eigene Gewerk wählen muss.
|
||||||
department: row.department || '',
|
department: row.department || '',
|
||||||
// einfuehrungFaellig: Rundgang noch nicht abgeschlossen (js/einfuehrung.js)
|
// einfuehrungFaellig: Rundgang noch nicht abgeschlossen (js/einfuehrung.js)
|
||||||
einfuehrungFaellig: !row.einfuehrung_am
|
einfuehrungFaellig: !row.einfuehrung_am,
|
||||||
|
// dashboardReihenfolge: eigene Projektreihenfolge auf "Meine Aufgaben"
|
||||||
|
dashboardReihenfolge: (() => { try { const a = JSON.parse(row.dashboard_reihenfolge || '[]'); return Array.isArray(a) ? a : []; } catch(e){ return []; } })()
|
||||||
};
|
};
|
||||||
// last_seen_at höchstens einmal je Minute und Sitzung schreiben. Vorher
|
// last_seen_at höchstens einmal je Minute und Sitzung schreiben. Vorher
|
||||||
// schrieb JEDE Anfrage — auch jede der rund 60 JS/CSS-Dateien eines
|
// schrieb JEDE Anfrage — auch jede der rund 60 JS/CSS-Dateien eines
|
||||||
@@ -3849,6 +3858,19 @@ app.post('/einfuehrung', requireAuth, (req, res) => {
|
|||||||
} catch(err){ console.error('Einführung speichern fehlgeschlagen:', err); res.status(500).json({ ok: false, error: String(err) }); }
|
} catch(err){ console.error('Einführung speichern fehlgeschlagen:', err); res.status(500).json({ ok: false, error: String(err) }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// POST /dashboard-reihenfolge {reihenfolge:[schlüssel]} — eigene Reihenfolge
|
||||||
|
// der Projekte auf "Meine Aufgaben"; leere Liste = wie in der Projektliste.
|
||||||
|
// Nur das eigene Konto.
|
||||||
|
app.post('/dashboard-reihenfolge', requireAuth, (req, res) => {
|
||||||
|
try {
|
||||||
|
const liste = (req.body || {}).reihenfolge;
|
||||||
|
if(!Array.isArray(liste)) return res.status(400).json({ ok: false, error: 'reihenfolge muss eine Liste sein.' });
|
||||||
|
const sauber = [...new Set(liste.map(x => String(x || '').slice(0, 100)).filter(Boolean))].slice(0, 500);
|
||||||
|
db.prepare('UPDATE users SET dashboard_reihenfolge = ? WHERE id = ?').run(sauber.length ? JSON.stringify(sauber) : '', req.user.id);
|
||||||
|
res.json({ ok: true, reihenfolge: sauber });
|
||||||
|
} catch(err){ console.error('Dashboard-Reihenfolge speichern fehlgeschlagen:', err); res.status(500).json({ ok: false, error: String(err) }); }
|
||||||
|
});
|
||||||
|
|
||||||
// ---------- Rollen & Rechte: Routen ----------
|
// ---------- Rollen & Rechte: Routen ----------
|
||||||
// GET liefert Katalog und Rollen (mit Anzahl Konten) — jedes angemeldete
|
// GET liefert Katalog und Rollen (mit Anzahl Konten) — jedes angemeldete
|
||||||
// Konto mit Grundzugang, weil die Benutzerverwaltung die Liste für ihre
|
// Konto mit Grundzugang, weil die Benutzerverwaltung die Liste für ihre
|
||||||
|
|||||||
Reference in new issue
Block a user