Externe Personen bei Aufgaben, Aufwand-Feld, Teams persönlich
- Aufgaben auch Kollegen außerhalb des Projekts zuweisen (Abschnitt „Externe Personen“ im Aufgaben-Panel) - Aufwand-Feld neu gestaltet: −/+, Schnellwahl, Umrechnung in Arbeitstage - Teams: persönliche Nachricht an die Zuständigen (Flow-Bot) statt Kanal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4c2e04e78b
commit
e7243f6f26
6 files changed
+304
-101
No files matched your search
@@ -1115,12 +1115,12 @@ async function loadAdminTeams(){
|
||||
if(!d || !d.ok){ el.innerHTML = '<span class="settings-warn">Keine Berechtigung.</span>'; return; }
|
||||
el.innerHTML = `
|
||||
<div class="settings-kv-row"><span class="settings-kv-key">Status</span>
|
||||
<span>${d.konfiguriert ? '<span class="sitzung-chip ist-geoeffnet">Verbunden</span> <span class="hint" style="margin:0">' + esc(d.host) + '</span>'
|
||||
<span>${d.konfiguriert ? '<span class="sitzung-chip ist-geoeffnet">Eingerichtet</span> <span class="hint" style="margin:0">' + esc(d.host) + '</span>'
|
||||
: '<span class="sitzung-chip ist-gesperrt">Nicht eingerichtet</span>'}</span></div>
|
||||
<label class="settings-field"><span>${d.konfiguriert ? 'Neue Adresse (ersetzt die bisherige)' : 'Adresse aus dem Teams-Workflow'}</span>
|
||||
<input type="password" id="teamsWebhook" placeholder="https://…" autocomplete="off"></label>
|
||||
<label class="settings-glas-schalter"><input type="checkbox" id="teamsNeueAufgaben" ${d.neueAufgaben ? 'checked' : ''}
|
||||
onchange="teamsSpeichern({ neueAufgaben: this.checked })"> Neue Aufgaben melden</label>
|
||||
onchange="teamsSpeichern({ neueAufgaben: this.checked })"> Zuständige über neue Aufgaben benachrichtigen</label>
|
||||
<div class="kalender-aktionen" style="margin-top:10px">
|
||||
<button type="button" onclick="teamsSpeichern({ webhook: document.getElementById('teamsWebhook').value })">Adresse speichern</button>
|
||||
<button type="button" class="secondary" id="teamsTestKnopf" onclick="teamsTesten()" ${d.konfiguriert ? '' : 'disabled'}>Testnachricht senden</button>
|
||||
@@ -1156,7 +1156,7 @@ async function teamsTesten(){
|
||||
try {
|
||||
const d = await fetch(baseUrl() + '/admin/teams/test', { method: 'POST' }).then(r => r.json());
|
||||
if(fehler){ fehler.hidden = d.ok; fehler.textContent = d.ok ? '' : 'Nicht gesendet: ' + (d.error || 'unbekannter Fehler'); }
|
||||
if(d.ok) alert('Testnachricht gesendet — bitte im Teams-Kanal nachsehen.');
|
||||
if(d.ok) alert('Testnachricht gesendet an ' + (d.an || 'Sie') + ' — bitte im Teams-Chat mit dem Flow-Bot nachsehen.');
|
||||
} catch(e){
|
||||
if(fehler){ fehler.hidden = false; fehler.textContent = 'Der Server ist nicht erreichbar.'; }
|
||||
} finally {
|
||||
|
||||
+111
-30
@@ -488,6 +488,104 @@
|
||||
// Anzeige und Dauer-Badge, Status-Segment, Zuständigen-Chips und die
|
||||
// Vorgänger-Liste. "Löschen" erscheint nur beim Bearbeiten, nicht beim
|
||||
// Anlegen.
|
||||
// ---------- Aufwand: −/+, Schnellwahl, Umrechnung ----------
|
||||
// Ein Arbeitstag = APP_SETTINGS.arbeitsstundenProTag (Admin → Termine).
|
||||
function stundenProTag(){ return Number(APP_SETTINGS && APP_SETTINGS.arbeitsstundenProTag) || 8; }
|
||||
function aufwandLesen(){
|
||||
const el = document.getElementById('todoEditAufwand');
|
||||
return el ? Math.max(0, parseFloat(String(el.value).replace(',', '.')) || 0) : 0;
|
||||
}
|
||||
function aufwandSchreiben(h){
|
||||
const el = document.getElementById('todoEditAufwand');
|
||||
if(!el) return;
|
||||
const w = Math.max(0, Math.round(h * 2) / 2);
|
||||
el.value = w > 0 ? String(w) : '';
|
||||
todoAufwandInfo();
|
||||
}
|
||||
window.todoAufwandAendern = function(delta){ aufwandSchreiben(aufwandLesen() + delta); };
|
||||
// todoAufwandSetzen(h) oder todoAufwandSetzen('tag', n): fester Wert; ein
|
||||
// zweiter Klick auf den gewählten Wert leert das Feld.
|
||||
window.todoAufwandSetzen = function(a, n){
|
||||
const h = a === 'tag' ? n * stundenProTag() : Number(a);
|
||||
aufwandSchreiben(aufwandLesen() === h ? 0 : h);
|
||||
};
|
||||
// todoAufwandInfo: Umrechnung in Arbeitstage und Markierung der Schnellwahl.
|
||||
window.todoAufwandInfo = function(){
|
||||
const h = aufwandLesen();
|
||||
const tag = stundenProTag();
|
||||
const info = document.getElementById('todoAufwandInfoText');
|
||||
if(info){
|
||||
const tage = h / tag;
|
||||
const zahl = x => (Math.round(x * 10) / 10).toLocaleString('de-DE');
|
||||
info.textContent = h > 0 ? '≈ ' + zahl(tage) + (Math.abs(tage - 1) < 0.05 ? ' Arbeitstag' : ' Arbeitstage') + ' (' + zahl(tag) + ' h je Tag)' : 'Ohne Aufwand zählt die Aufgabe nicht zur Auslastung.';
|
||||
}
|
||||
document.querySelectorAll('#todoAufwandSchnell button').forEach(b => {
|
||||
const wert = b.dataset.tage ? Number(b.dataset.tage) * tag : Number(b.dataset.h);
|
||||
b.setAttribute('aria-pressed', String(h > 0 && Math.abs(wert - h) < 0.01));
|
||||
});
|
||||
};
|
||||
|
||||
// ---------- Zuständige: Projekt und „Externe Personen“ ----------
|
||||
// todoProjektPersonen: die dem Projekt zugeordneten Personen (je einmal).
|
||||
function todoProjektPersonen(){
|
||||
const gesehen = new Set();
|
||||
const liste = [];
|
||||
projectAssignees(app.project, app.week).forEach(a => {
|
||||
if(gesehen.has(a.person.id)) return;
|
||||
gesehen.add(a.person.id);
|
||||
liste.push(a.person);
|
||||
});
|
||||
return liste;
|
||||
}
|
||||
function todoPersonById(id){
|
||||
return ((app.week && app.week.people) || []).find(x => x.id === id) || { id, name: id, fullName: id };
|
||||
}
|
||||
function todoPersonChip(person, gedrueckt, extern){
|
||||
const short = person.name || initialsForName(person.fullName || '') || '?';
|
||||
const full = person.fullName || person.name || '';
|
||||
const klick = extern ? `todoExternEntfernen('${esc(person.id)}')` : 'toggleTodoAssignee(this)';
|
||||
const titel = extern ? ' title="Nicht im Projekt — Klick entfernt die Person"' : '';
|
||||
return `<button type="button" class="person-chip${extern ? ' ist-fremd' : ''}"${titel} data-person-id="${esc(person.id)}" aria-pressed="${gedrueckt ? 'true' : 'false'}" onclick="${klick}">`
|
||||
+ `<span class="person-chip-ini">${esc(short)}</span>${esc(full)}${extern ? ' <span class="person-chip-x" aria-hidden="true">×</span>' : ''}</button>`;
|
||||
}
|
||||
// todoExterneSetzen: zeichnet die gewählten externen Personen und die
|
||||
// Auswahl der übrigen Kollegen (Teilnehmerkreis der Sitzung ohne Projekt
|
||||
// und ohne bereits gewählte), nach Gewerk gruppiert.
|
||||
function todoExterneSetzen(ids){
|
||||
const box = document.getElementById('todoEditExterne');
|
||||
const wahl = document.getElementById('todoEditExterneWahl');
|
||||
if(!box || !wahl) return;
|
||||
box.innerHTML = ids.map(id => todoPersonChip(todoPersonById(id), true, true)).join('');
|
||||
const projektIds = new Set(todoProjektPersonen().map(p => p.id));
|
||||
const frei = ((app.week && app.week.people) || [])
|
||||
.filter(p => p && p.id && !projektIds.has(p.id) && !ids.includes(p.id))
|
||||
.sort((a, b) => String(a.fullName || a.name).localeCompare(String(b.fullName || b.name), 'de'));
|
||||
const gruppen = new Map();
|
||||
frei.forEach(p => {
|
||||
const g = (typeof personGewerk === 'function' && personGewerk(p)) || 'Ohne Gewerk';
|
||||
if(!gruppen.has(g)) gruppen.set(g, []);
|
||||
gruppen.get(g).push(p);
|
||||
});
|
||||
const reihenfolge = [...(typeof DEPARTMENTS !== 'undefined' ? Object.keys(DEPARTMENTS) : []), 'Ohne Gewerk'];
|
||||
const schluessel = [...gruppen.keys()].sort((a, b) => reihenfolge.indexOf(a) - reihenfolge.indexOf(b));
|
||||
wahl.innerHTML = `<option value="">${frei.length ? '+ Person hinzufügen…' : 'Keine weiteren Kollegen'}</option>`
|
||||
+ schluessel.map(g => `<optgroup label="${esc(g)}">${gruppen.get(g).map(p =>
|
||||
`<option value="${esc(p.id)}">${esc(p.fullName || p.name)}${p.name && p.fullName ? ' (' + esc(p.name) + ')' : ''}</option>`).join('')}</optgroup>`).join('');
|
||||
wahl.disabled = !frei.length;
|
||||
}
|
||||
function todoExterneIds(){
|
||||
return Array.from(document.querySelectorAll('#todoEditExterne .person-chip')).map(b => b.dataset.personId);
|
||||
}
|
||||
window.todoExternHinzufuegen = function(id){
|
||||
if(!id) return;
|
||||
const ids = todoExterneIds();
|
||||
if(!ids.includes(id)) ids.push(id);
|
||||
todoExterneSetzen(ids);
|
||||
};
|
||||
window.todoExternEntfernen = function(id){
|
||||
todoExterneSetzen(todoExterneIds().filter(x => x !== id));
|
||||
};
|
||||
|
||||
window.openTodoModal = function(index, prefill){
|
||||
if(app.isLocked()) return;
|
||||
const plan = app.ensurePlanLocal();
|
||||
@@ -544,6 +642,7 @@
|
||||
const aufwandWert = isNew ? Number(pre.aufwand) : Number(currentTodo.aufwand);
|
||||
aufwandEl.value = aufwandWert > 0 ? String(aufwandWert) : '';
|
||||
}
|
||||
todoAufwandInfo();
|
||||
todoPhasenModusSetzen(isNew ? !!pre.phase : !!currentTodo.lph, isNew ? null : currentTodo, isNew, isNew ? pre.lph : null);
|
||||
|
||||
const storedProgress = isNew ? 0 : (parseInt(currentTodo.progress, 10) || 0);
|
||||
@@ -594,39 +693,20 @@
|
||||
|
||||
const assigneesDiv = document.getElementById('todoEditAssignees');
|
||||
if(assigneesDiv){
|
||||
const assignees = projectAssignees(app.project, app.week);
|
||||
const currentAssignees = currentTodo
|
||||
? (Array.isArray(currentTodo.assignees) ? currentTodo.assignees : [])
|
||||
: (Array.isArray(pre.assignees) ? pre.assignees : []);
|
||||
const seenPeople = new Set();
|
||||
const uniquePeople = [];
|
||||
assignees.forEach(a => {
|
||||
if(!seenPeople.has(a.person.id)){
|
||||
seenPeople.add(a.person.id);
|
||||
uniquePeople.push(a.person);
|
||||
}
|
||||
});
|
||||
// Bereits Zuständige, die (nicht mehr) dem Projekt zugeordnet sind —
|
||||
// etwa über "Aufgabe aus Kommentar" aus dem ganzen Teilnehmerkreis
|
||||
// gewählt — ebenfalls als Chip zeigen. Ohne sie gingen sie beim
|
||||
// Speichern stillschweigend verloren, weil saveTodoEdit() nur die
|
||||
// gedrückten Chips liest.
|
||||
currentAssignees.forEach(id => {
|
||||
if(seenPeople.has(id)) return;
|
||||
seenPeople.add(id);
|
||||
const p = ((app.week && app.week.people) || []).find(x => x.id === id) || { id, name: id, fullName: id };
|
||||
uniquePeople.push(Object.assign({}, p, { nichtImProjekt: true }));
|
||||
});
|
||||
const projektLeute = todoProjektPersonen();
|
||||
const projektIds = new Set(projektLeute.map(p => p.id));
|
||||
// Chips statt Checkbox-Reihe: Kürzel + Name, dieselbe Lesart wie die
|
||||
// Personenspalten im Projektboard.
|
||||
assigneesDiv.innerHTML = uniquePeople.map(person => {
|
||||
const pressed = currentAssignees.includes(person.id) ? 'true' : 'false';
|
||||
const short = person.name || initialsForName(person.fullName || '') || '?';
|
||||
const full = person.fullName || person.name || '';
|
||||
const fremd = person.nichtImProjekt ? ' title="Nicht (mehr) dem Projekt zugeordnet"' : '';
|
||||
return `<button type="button" class="person-chip${person.nichtImProjekt ? ' ist-fremd' : ''}"${fremd} data-person-id="${esc(person.id)}" aria-pressed="${pressed}" onclick="toggleTodoAssignee(this)">`
|
||||
+ `<span class="person-chip-ini">${esc(short)}</span>${esc(full)}</button>`;
|
||||
}).join('') || '<span class="hint">Keine Projektzuordnungen im Board.</span>';
|
||||
assigneesDiv.innerHTML = projektLeute.map(person =>
|
||||
todoPersonChip(person, currentAssignees.includes(person.id), false)
|
||||
).join('') || '<span class="hint">Keine Projektzuordnungen im Board.</span>';
|
||||
// Zuständige außerhalb des Projekts stehen unter „Externe Personen“ —
|
||||
// auch solche, die das Projekt inzwischen verlassen haben (sonst gingen
|
||||
// sie beim Speichern stillschweigend verloren).
|
||||
todoExterneSetzen(currentAssignees.filter(id => !projektIds.has(id)));
|
||||
}
|
||||
|
||||
const deleteBtn = document.getElementById('todoEditDelete');
|
||||
@@ -664,7 +744,7 @@
|
||||
function todoFormSnapshot(){
|
||||
const read = id => { const el = document.getElementById(id); return el ? el.value : ''; };
|
||||
const deps = Array.from(document.querySelectorAll('#todoEditDependsOn input[type="checkbox"]:checked')).map(cb => cb.value);
|
||||
const people = Array.from(document.querySelectorAll('#todoEditAssignees .person-chip[aria-pressed="true"]')).map(b => b.dataset.personId);
|
||||
const people = Array.from(document.querySelectorAll('#todoEditAssignees .person-chip[aria-pressed="true"], #todoEditExterne .person-chip')).map(b => b.dataset.personId);
|
||||
const gantt = document.getElementById('todoEditShowInGantt');
|
||||
return JSON.stringify([read('todoEditTitle'), read('todoEditStart'), read('todoEditEnd'),
|
||||
currentTodoEditStatus(), read('todoEditProgress'), deps, people,
|
||||
@@ -727,7 +807,8 @@
|
||||
const dependsOn = dependsOnList ? Array.from(dependsOnList.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value).filter(Boolean) : [];
|
||||
|
||||
const assigneesDiv = document.getElementById('todoEditAssignees');
|
||||
const assignees = assigneesDiv ? Array.from(assigneesDiv.querySelectorAll('.person-chip[aria-pressed="true"]')).map(btn => btn.dataset.personId) : [];
|
||||
const assignees = (assigneesDiv ? Array.from(assigneesDiv.querySelectorAll('.person-chip[aria-pressed="true"]')).map(btn => btn.dataset.personId) : [])
|
||||
.concat(todoExterneIds());
|
||||
|
||||
const showInGanttEl = document.getElementById('todoEditShowInGantt');
|
||||
const showInGantt = showInGanttEl ? showInGanttEl.checked : true;
|
||||
|
||||
Reference in new issue
Block a user