Neues Design „Bauplan“, Dashboard, Notizen-Bereich, Seitenleiste
- Gestaltung „Bauplan“: ruhigere Farben, Schriftskala, Abstände im 4-px-Raster, Kalenderwochen und Nummern in Mono-Ziffern, Glas beruhigt - Dashboard „Meine Aufgaben“: Kopf mit Kalenderwoche, Band „Diese Woche“ (Mo–Fr), je Projekt Meine Rolle / Team / Notizen; offene Termine blau hervorgehoben; Monatskalender unverändert - Neuer Bereich „Notizen“ in der Seitenleiste: Notizen aller eigenen Projekte und eigene Notizen ohne Projekt, später einem Projekt zuordenbar - Seitenleiste neu gegliedert (Projekte, Team, Notizen, Studenten, Einstellungen/Admin unten), Admin-Punkte in Personen/Vorgaben/System - Reiter im Projektkopf wie die Seitenleiste hervorgehoben - Behoben: „&amp;…“ in Kommentaren wuchs bei jedem Speichern; bestehende Kommentare werden beim Start repariert - Behoben: Lösch-Rückfrage bei Notizen unlesbar bzw. blieb stehen, Cursor im Notiztitel verdeckt, Dropdown im Dunkelmodus gekachelt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e697057b86
commit
e291ca8438
18 files changed
+2185
-1250
No files matched your search
@@ -1020,6 +1020,30 @@ function htmlEscape(s){
|
||||
.replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// textMaskieren: wie htmlEscape(), aber ein '&', das schon eine fertige
|
||||
// Entity beginnt (& ' …), bleibt stehen. Der Text zwischen den
|
||||
// Tags kommt aus gespeichertem HTML, in dem solche Entities ganz normal
|
||||
// stehen. htmlEscape() machte daraus bei JEDEM Speichern eine Stufe mehr
|
||||
// ( → &nbsp; → &amp;nbsp; …) — Eriks Meldung nach dem
|
||||
// Sitzungswechsel KW 45. So bleibt die Bereinigung beliebig oft wiederholbar.
|
||||
function textMaskieren(s){
|
||||
return String(s)
|
||||
.replace(/&(?!(?:[a-zA-Z][a-zA-Z0-9]{1,31}|#\d{1,7}|#x[0-9a-fA-F]{1,6});)/g, '&')
|
||||
.replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
// entitiesEntstapeln: macht gestapelte Maskierung rückgängig
|
||||
// (&amp;nbsp; → , &amp; → &). Ein „&“ vor einer
|
||||
// weiteren Entity ist immer das Ergebnis des Fehlers oben, nie Absicht.
|
||||
function entitiesEntstapeln(html){
|
||||
let alt, neu = String(html || '');
|
||||
do {
|
||||
alt = neu;
|
||||
neu = neu.replace(/&((?:[a-zA-Z][a-zA-Z0-9]{1,31}|#\d{1,7}|#x[0-9a-fA-F]{1,6});)/g, '&$1');
|
||||
} while(neu !== alt);
|
||||
return neu;
|
||||
}
|
||||
|
||||
// sicheresStyleAttribut: lässt aus einem style-Attribut nur color und
|
||||
// font-weight durch — identisch zur Client-Regel. Rückgabe: bereinigter
|
||||
// Wert oder '' (dann wird das Attribut ganz weggelassen).
|
||||
@@ -1108,7 +1132,7 @@ function sanitizeFormatHtml(html){
|
||||
if(c !== '<'){
|
||||
const naechstes = quelle.indexOf('<', i);
|
||||
const text = quelle.slice(i, naechstes === -1 ? quelle.length : naechstes);
|
||||
if(!verwerfenBis) aus += htmlEscape(text);
|
||||
if(!verwerfenBis) aus += textMaskieren(text);
|
||||
i += text.length;
|
||||
continue;
|
||||
}
|
||||
@@ -2611,6 +2635,30 @@ function kommentareVorsatzUmstellen(){
|
||||
if(n) console.log('Gewerke-Vorsatz aus ' + n + ' Kommentaren entfernt.');
|
||||
}
|
||||
|
||||
// entitiesReparieren: führt gestapelte Maskierung (&amp;nbsp; …) in
|
||||
// allen Sitzungskommentaren und Protokoll-Punkten auf das ursprüngliche
|
||||
// Zeichen zurück — Folge des Fehlers in sanitizeFormatHtml() vor
|
||||
// textMaskieren(). Läuft bei jedem Start und nach /import, idempotent.
|
||||
function entitiesReparieren(){
|
||||
const teile = [
|
||||
['project_kommentare', 'status', 'week_label, project_id, gewerk', 'week_label = ? AND project_id = ? AND gewerk = ?', r => [r.week_label, r.project_id, r.gewerk]],
|
||||
['protokoll_punkte', 'text', 'id', 'id = ?', r => [r.id]]
|
||||
];
|
||||
let n = 0;
|
||||
db.exec('BEGIN');
|
||||
try {
|
||||
teile.forEach(([tab, feld, schluessel, wo, werte]) => {
|
||||
const upd = db.prepare('UPDATE ' + tab + ' SET ' + feld + ' = ? WHERE ' + wo);
|
||||
db.prepare('SELECT ' + schluessel + ', ' + feld + " AS wert FROM " + tab + " WHERE " + feld + " LIKE '%&%'").all().forEach(r => {
|
||||
const neu = entitiesEntstapeln(r.wert);
|
||||
if(neu !== r.wert){ upd.run(neu, ...werte(r)); n++; }
|
||||
});
|
||||
});
|
||||
db.exec('COMMIT');
|
||||
} catch(e){ db.exec('ROLLBACK'); throw e; }
|
||||
if(n) console.log('Doppelt maskierte Zeichen in ' + n + ' Kommentaren/Protokoll-Punkten repariert.');
|
||||
}
|
||||
|
||||
function kommentareSpiegelstricheUmstellen(){
|
||||
const zeilen = db.prepare("SELECT week_label, project_id, gewerk, status FROM project_kommentare WHERE status != ''").all();
|
||||
const upd = db.prepare('UPDATE project_kommentare SET status = ? WHERE week_label = ? AND project_id = ? AND gewerk = ?');
|
||||
@@ -3558,6 +3606,7 @@ einmaligeUmstellung('siemon-anteile-2021', siemon2021Umstellen);
|
||||
// Idempotent (ein bereits entfernter Vorsatz ist weg) — läuft deshalb bei
|
||||
// jedem Start und nach /import, damit ältere Backups ihn nicht zurückbringen.
|
||||
kommentareVorsatzUmstellen();
|
||||
entitiesReparieren();
|
||||
ensureSandboxWeek();
|
||||
ensureUserPersonIdUniqueIndex();
|
||||
migrateRetirePeopleTablesIntoUsers();
|
||||
@@ -7362,6 +7411,68 @@ function notizAnlegen(req, uid, titel, html, quelle){
|
||||
return notizLesen(id);
|
||||
}
|
||||
|
||||
// GET /projekt-notizen/anzahl — je Projekt (project_uid) Anzahl und zuletzt
|
||||
// geändert, für die Projektblöcke auf „Meine Aufgaben“. Nur Zählung, kein Inhalt.
|
||||
app.get('/projekt-notizen/anzahl', requireAuth, (req, res) => {
|
||||
try {
|
||||
const anzahl = {};
|
||||
db.prepare("SELECT project_uid AS uid, COUNT(*) AS n, MAX(geaendert_am) AS zuletzt FROM projekt_notizen WHERE project_uid != '' GROUP BY project_uid")
|
||||
.all().forEach(r => { anzahl[r.uid] = { anzahl: r.n, zuletzt: r.zuletzt || '' }; });
|
||||
res.json({ ok: true, anzahl });
|
||||
} catch(err){
|
||||
console.error('Notizen zählen fehlgeschlagen:', err);
|
||||
res.status(500).json({ ok: false, error: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- Seitenleiste „Notizen“: alle Notizen meiner Projekte ----------
|
||||
//
|
||||
// Eriks Wunsch: ein eigener Bereich, in dem jedes Konto die Notizen ALLER
|
||||
// seiner Projekte sieht und auch projektunabhängige Notizen anlegt, die sich
|
||||
// später einem Projekt zuordnen lassen. Eine projektunabhängige Notiz ist eine
|
||||
// Zeile mit project_uid = '' und gehört allein ihrem Verfasser (sonst sieht
|
||||
// und ändert sie niemand). „Meine Projekte“ = in der jüngsten echten Sitzung
|
||||
// zugeordnet (assign > 0) oder mit eigener Aufgabe — dieselbe Regel wie das
|
||||
// Dashboard (dashDaten()).
|
||||
function meineProjektUids(user){
|
||||
const ich = user && user.personId;
|
||||
const uids = new Set();
|
||||
if(!ich) return uids;
|
||||
const label = echteSitzungenSortiert().map(w => w.label).pop();
|
||||
const w = label ? loadWeeks(label)[0] : null;
|
||||
((w && w.projects) || []).forEach(p => {
|
||||
const zugeordnet = Object.keys(p.assign || {}).some(k => k.split('|')[0] === ich && Number(p.assign[k]) > 0);
|
||||
const eigene = ((p.plan && p.plan.todos) || []).some(t => Array.isArray(t.assignees) && t.assignees.includes(ich));
|
||||
if((zugeordnet || eigene) && p.projectUid) uids.add(String(p.projectUid));
|
||||
});
|
||||
return uids;
|
||||
}
|
||||
const NOTIZ_SELECT = `SELECT n.*, COALESCE(u1.display_name, 'Gelöschter Benutzer') AS erstellt_name,
|
||||
COALESCE(u2.display_name, 'Gelöschter Benutzer') AS geaendert_name
|
||||
FROM projekt_notizen n LEFT JOIN users u1 ON u1.id = n.erstellt_von LEFT JOIN users u2 ON u2.id = n.geaendert_von`;
|
||||
|
||||
// GET /meine-notizen — {notizen, meineProjekte:[uid]}: die Notizen meiner
|
||||
// Projekte plus meine projektunabhängigen.
|
||||
app.get('/meine-notizen', requireAuth, (req, res) => {
|
||||
try {
|
||||
const uids = Array.from(meineProjektUids(req.user));
|
||||
const rows = db.prepare(NOTIZ_SELECT + ` WHERE (n.project_uid = '' AND n.erstellt_von = ?)`
|
||||
+ (uids.length ? ' OR n.project_uid IN (' + uids.map(() => '?').join(',') + ')' : '')
|
||||
+ ' ORDER BY n.angeheftet DESC, n.geaendert_am DESC').all(req.user.id, ...uids);
|
||||
res.json({ ok: true, notizen: rows.map(notizZeile), meineProjekte: uids });
|
||||
} catch(err){
|
||||
console.error('Meine Notizen laden fehlgeschlagen:', err);
|
||||
res.status(500).json({ ok: false, error: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
// notizDarf: Schreibrecht auf eine Notiz bzw. ein Ziel. Projektunabhängig
|
||||
// (uid '') nur der Verfasser; an einem Projekt das Recht proj.plan.
|
||||
function notizDarf(user, uid, erstelltVonId){
|
||||
if(!uid) return erstelltVonId === undefined || erstelltVonId === user.id;
|
||||
return hatRecht(user, 'proj.plan');
|
||||
}
|
||||
|
||||
app.get('/projekt-notizen', requireAuth, (req, res) => {
|
||||
try {
|
||||
const uid = String(req.query.uid || '');
|
||||
@@ -7377,11 +7488,11 @@ app.get('/projekt-notizen', requireAuth, (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/projekt-notizen', requireAuth, requireRecht('proj.plan'), (req, res) => {
|
||||
app.post('/projekt-notizen', requireAuth, (req, res) => {
|
||||
try {
|
||||
const b = req.body || {};
|
||||
const uid = String(b.uid || '');
|
||||
if(!uid) return res.status(400).json({ ok: false, error: 'uid fehlt.' });
|
||||
if(!notizDarf(req.user, uid)) return res.status(403).json({ ok: false, recht: 'proj.plan', error: 'Keine Berechtigung, Notizen in Projekten anzulegen.' });
|
||||
res.json({ ok: true, notiz: notizAnlegen(req, uid, b.titel, b.html, '') });
|
||||
} catch(err){
|
||||
console.error('Notiz anlegen fehlgeschlagen:', err);
|
||||
@@ -7392,11 +7503,20 @@ app.post('/projekt-notizen', requireAuth, requireRecht('proj.plan'), (req, res)
|
||||
// PUT {titel?, html?, angeheftet?, basis?} — basis = geaendertAm des Stands,
|
||||
// auf dem die Änderung beruht; weicht der gespeicherte ab, 409 mit dem
|
||||
// aktuellen Stand (jemand anderes hat inzwischen gespeichert).
|
||||
app.put('/projekt-notizen/:id', requireAuth, requireRecht('proj.plan'), (req, res) => {
|
||||
// uid (optional): einem Projekt zuordnen bzw. mit '' wieder projektunabhängig
|
||||
// machen (das nur der Verfasser) — aus der Seitenleiste „Notizen“.
|
||||
app.put('/projekt-notizen/:id', requireAuth, (req, res) => {
|
||||
try {
|
||||
const alt = notizLesen(req.params.id);
|
||||
if(!alt) return res.status(404).json({ ok: false, error: 'Die Notiz gibt es nicht mehr.' });
|
||||
if(!alt.uid && alt.erstelltVonId !== req.user.id) return res.status(404).json({ ok: false, error: 'Die Notiz gibt es nicht mehr.' });
|
||||
if(!notizDarf(req.user, alt.uid, alt.erstelltVonId)) return res.status(403).json({ ok: false, recht: 'proj.plan', error: 'Keine Berechtigung, Notizen zu ändern.' });
|
||||
const b = req.body || {};
|
||||
const neueUid = typeof b.uid === 'string' ? b.uid : alt.uid;
|
||||
if(neueUid !== alt.uid){
|
||||
if(!neueUid && alt.erstelltVonId !== req.user.id) return res.status(403).json({ ok: false, error: 'Nur wer die Notiz angelegt hat, kann sie vom Projekt lösen.' });
|
||||
if(!notizDarf(req.user, neueUid, alt.erstelltVonId)) return res.status(403).json({ ok: false, recht: 'proj.plan', error: 'Keine Berechtigung, Notizen in Projekten anzulegen.' });
|
||||
}
|
||||
if(typeof b.basis === 'string' && b.basis && b.basis !== alt.geaendertAm){
|
||||
return res.status(409).json({ ok: false, konflikt: true, notiz: alt,
|
||||
error: alt.geaendertVon + ' hat diese Notiz inzwischen geändert.' });
|
||||
@@ -7408,10 +7528,11 @@ app.put('/projekt-notizen/:id', requireAuth, requireRecht('proj.plan'), (req, re
|
||||
// Bearbeiter bleiben, damit die Sortierung „zuletzt geändert“ stimmt.
|
||||
const nurPin = typeof b.titel !== 'string' && typeof b.html !== 'string';
|
||||
const jetzt = nurPin ? alt.geaendertAm : new Date().toISOString();
|
||||
db.prepare(`UPDATE projekt_notizen SET titel = ?, html = ?, angeheftet = ?, geaendert_am = ?
|
||||
db.prepare(`UPDATE projekt_notizen SET titel = ?, html = ?, angeheftet = ?, geaendert_am = ?, project_uid = ?
|
||||
${nurPin ? '' : ', geaendert_von = ' + Number(req.user.id)} WHERE id = ?`)
|
||||
.run(titel, html, angeheftet, jetzt, alt.id);
|
||||
.run(titel, html, angeheftet, jetzt, neueUid, alt.id);
|
||||
ereignisSenden({ typ: 'notizen', uid: alt.uid }, req);
|
||||
if(neueUid !== alt.uid) ereignisSenden({ typ: 'notizen', uid: neueUid }, req);
|
||||
res.json({ ok: true, notiz: notizLesen(alt.id) });
|
||||
} catch(err){
|
||||
console.error('Notiz speichern fehlgeschlagen:', err);
|
||||
@@ -7419,10 +7540,11 @@ app.put('/projekt-notizen/:id', requireAuth, requireRecht('proj.plan'), (req, re
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/projekt-notizen/:id', requireAuth, requireRecht('proj.plan'), (req, res) => {
|
||||
app.delete('/projekt-notizen/:id', requireAuth, (req, res) => {
|
||||
try {
|
||||
const alt = notizLesen(req.params.id);
|
||||
if(!alt) return res.json({ ok: true });
|
||||
if(!notizDarf(req.user, alt.uid, alt.erstelltVonId)) return res.status(403).json({ ok: false, recht: 'proj.plan', error: 'Keine Berechtigung, Notizen zu löschen.' });
|
||||
if(alt.erstelltVonId !== req.user.id && !istAdministrator(req.user)){
|
||||
return res.status(403).json({ ok: false, error: 'Löschen darf nur, wer die Notiz angelegt hat, oder ein Administrator.' });
|
||||
}
|
||||
@@ -7438,11 +7560,11 @@ app.delete('/projekt-notizen/:id', requireAuth, requireRecht('proj.plan'), (req,
|
||||
// POST /projekt-notizen/aus-mail {uid, text|eml} — Mail als neue Notiz, OHNE
|
||||
// KI: Titel = Betreff (sonst erste Zeile), oben Absender und Datum, darunter
|
||||
// der Text (zitierte Vorgänger-Mails abgeschnitten, wie bei „Termin aus Mail“).
|
||||
app.post('/projekt-notizen/aus-mail', requireAuth, requireRecht('proj.plan'), (req, res) => {
|
||||
app.post('/projekt-notizen/aus-mail', requireAuth, (req, res) => {
|
||||
try {
|
||||
const b = req.body || {};
|
||||
const uid = String(b.uid || '');
|
||||
if(!uid) return res.status(400).json({ ok: false, error: 'uid fehlt.' });
|
||||
if(!notizDarf(req.user, uid)) return res.status(403).json({ ok: false, recht: 'proj.plan', error: 'Keine Berechtigung, Notizen in Projekten anzulegen.' });
|
||||
const mail = (typeof b.eml === 'string' && b.eml.trim()) ? leseEml(b.eml)
|
||||
: { betreff: '', absender: '', datum: '', text: String(b.text || '') };
|
||||
const text = mailZitateAbschneiden(mail.text).slice(0, 50000);
|
||||
@@ -9262,6 +9384,7 @@ app.post('/import', requireRecht('a.import'), express.raw({ type: '*/*', limit:
|
||||
// Ältere Backups tragen Kommentare noch gewerkübergreifend in projects.
|
||||
migrateKommentareJeGewerk();
|
||||
kommentareVorsatzUmstellen();
|
||||
entitiesReparieren();
|
||||
// Ältere Backups kennen das Feld leistungsphase noch nicht.
|
||||
phasenbalkenAusTitel();
|
||||
console.log('Datenbank aus Backup wiederhergestellt (' + buf.length + ' Bytes)');
|
||||
@@ -9292,7 +9415,7 @@ app.get('/', (req,res)=>{
|
||||
// Projektliste aufgegangen ist: Weitergegebene Links sollen nicht mit 404
|
||||
// enden. Die Oberfläche leitet den Abschnitt auf die Projektliste um
|
||||
// (TAB_BY_SLUG in public/js/globals.js) und erzeugt ihn selbst nicht mehr.
|
||||
const VALID_TAB_SLUGS = new Set(['Projektübersicht','Wochenbericht','Auslastung','Abwesenheiten','Projekte','Projektbeteiligte','Planung','PlanChecker','MeineAufgaben','Wandtafel','Einstellungen','Admin']);
|
||||
const VALID_TAB_SLUGS = new Set(['Projektübersicht','Wochenbericht','Auslastung','Abwesenheiten','Notizen','Projekte','Projektbeteiligte','Planung','PlanChecker','MeineAufgaben','Wandtafel','Einstellungen','Admin']);
|
||||
// Slug der Projektplan-Seite (muss zu PLAN_SLUG in js/globals.js passen)
|
||||
const PLAN_SLUG = 'Projektplan';
|
||||
|
||||
|
||||
Reference in new issue
Block a user