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:
erikandClaude Opus 5.5 committed 2026-10-30 09:36:06 +01:00
1 parent 4c2e04e78b
commit e7243f6f26
6 files changed
+304 -101

No files matched your search

+55 -21
View File
@@ -39,7 +39,7 @@
<link rel="alternate icon" href="/logo.ico">
<link rel="apple-touch-icon" href="/logo.png">
<link rel="stylesheet" href="/styles.css?v=110">
<link rel="stylesheet" href="/plan-styles.css?v=45">
<link rel="stylesheet" href="/plan-styles.css?v=48">
<!-- Glas-Darstellung (public/glas.css, als letztes Stylesheet): greift nur mit
der Klasse html.glas. Gesetzt wird sie hier, vor dem ersten Zeichnen,
damit die Seite nicht erst ohne Glas aufblitzt; abschaltbar je Browser
@@ -679,23 +679,31 @@
<!-- Teams-Anbindung (loadAdminTeams(), js/admin/admin.js; Server: /admin/teams*) -->
<section class="settings-card" id="adminTeamsCard" data-admin-section="teams" style="display:none">
<h3 class="settings-card-title">Teams <span class="settings-scope">firmenweit · nur Administratoren</span></h3>
<p class="hint">Bekommt jemand eine Aufgabe zugewiesen, erscheint in einem Teams-Kanal eine
Nachricht mit Link direkt auf die Aufgabe im Projekt.</p>
<p class="hint">Bekommt jemand eine Aufgabe zugewiesen, erhält genau diese Person in Teams
eine persönliche Nachricht (Chat mit dem Flow-Bot) mit Link direkt auf die Aufgabe.
Gefunden wird sie über die E-Mail-Adresse ihres Kontos — die muss ihrem Teams-Anmeldenamen
entsprechen. Wer kein Konto oder keine E-Mail hat, bekommt nichts.</p>
<div id="adminTeamsInhalt"></div>
<details class="kalender-anleitung">
<summary>So richten Sie die Adresse in Teams ein</summary>
<summary>So richten Sie den Workflow in Teams ein</summary>
<ol>
<li>In Teams den gewünschten Kanal öffnen, oben bei den drei Punkten
<i>Workflows</i> wählen.</li>
<li>Die Vorlage <i>„Beim Empfang einer Webhook-Anfrage in einem Kanal posten“</i>
auswählen, einen Namen vergeben (z. B. „Projektübersicht“) und bestätigen.</li>
<li>Teams zeigt am Ende eine lange Adresse (beginnt mit https://). Diese kopieren,
<li>In Teams die App <i>Workflows</i> öffnen (oder im Browser make.powerautomate.com) und
einen <b>leeren</b> Workflow anlegen.</li>
<li><b>Auslöser:</b> „Bei Empfang einer Teams-Webhookanforderung“, bei
<i>Wer kann den Flow auslösen</i> „Jeder“ wählen.</li>
<li><b>Aktion:</b> „Karte in einem Chat oder Kanal veröffentlichen“ —
<i>Posten als</i>: Flow-Bot, <i>Posten in</i>: Chat mit Flow-Bot,
<i>Empfänger</i>: als Ausdruck <code>triggerBody()?['empfaenger']</code>,
<i>Adaptive Karte</i>: als Ausdruck <code>string(triggerBody()?['karte'])</code>.</li>
<li>Speichern. Beim Auslöser steht jetzt eine lange Adresse (https://…) — kopieren,
hier einfügen und speichern.</li>
<li>„Testnachricht senden“ — im Kanal sollte eine Karte erscheinen.</li>
<li>„Testnachricht senden“ — die Nachricht kommt an Sie selbst.</li>
</ol>
<p class="hint">Der Server braucht dafür Internetzugang nach außen. Der Link in der
Nachricht führt auf die Adresse, unter der die App im Büro aufgerufen wird — er
funktioniert also nur im Büronetz (bzw. per VPN).</p>
<p class="hint">Der Workflow läuft unter dem Konto, das ihn anlegt. Am besten legt ihn ein
Sammelkonto oder die IT an — sonst hört er auf, wenn dieses Konto gesperrt wird.
Der Server braucht Internetzugang nach außen; der Link in der Nachricht funktioniert
nur im Büronetz (bzw. per VPN). Die Bezeichnungen in Teams können je nach Version
leicht abweichen.</p>
</details>
</section>
<section class="settings-card" id="adminBetriebCard" data-admin-section="betrieb" style="display:none">
@@ -1602,12 +1610,38 @@
<section class="pd-abs" id="todoEditWerAbs">
<h3>Wer</h3>
<div id="todoEditAssignees" class="person-chips"></div>
<label class="pd-aufwand" for="todoEditAufwand">
<span>Aufwand</span>
<input type="number" id="todoEditAufwand" min="0" step="0.5" placeholder="0" inputmode="decimal">
<span>Stunden, optional</span>
</label>
<p class="hint pd-hint">Zählt zur Auslastung der Zuständigen, verteilt auf den Zeitraum der Aufgabe.</p>
<!-- Externe Personen: Kollegen mit Konto, die dem Projekt NICHT
zugeordnet sind (Eriks Wunsch). todoExternHinzufuegen()/
todoExternEntfernen(), js/plan/plan-modals.js -->
<div class="pd-externe">
<div class="pd-externe-kopf">Externe Personen <span class="hint">nicht im Projekt</span></div>
<div id="todoEditExterne" class="person-chips"></div>
<select id="todoEditExterneWahl" aria-label="Externe Person hinzufügen" onchange="todoExternHinzufuegen(this.value)"></select>
</div>
<!-- Aufwand: −/+, Schnellwahl, Umrechnung in Arbeitstage
(todoAufwandAendern()/todoAufwandSetzen()/todoAufwandInfo(), plan-modals.js) -->
<div class="pd-aufwand">
<div class="pd-aufwand-kopf">
<label for="todoEditAufwand">Aufwand</label>
<span>optional · zählt zur Auslastung der Zuständigen</span>
</div>
<div class="pd-aufwand-zeile">
<div class="pd-stepper">
<button type="button" class="pd-stepper-knopf" onclick="todoAufwandAendern(-1)" aria-label="Eine Stunde weniger">−</button>
<input type="number" id="todoEditAufwand" min="0" step="0.5" placeholder="0" inputmode="decimal" oninput="todoAufwandInfo()">
<span class="pd-einheit">Std.</span>
<button type="button" class="pd-stepper-knopf" onclick="todoAufwandAendern(1)" aria-label="Eine Stunde mehr">+</button>
</div>
<div class="pd-aufwand-schnell" id="todoAufwandSchnell" role="group" aria-label="Schnellwahl">
<button type="button" data-h="2" onclick="todoAufwandSetzen(2)">2 h</button>
<button type="button" data-h="4" onclick="todoAufwandSetzen(4)">½ Tag</button>
<button type="button" data-tage="1" onclick="todoAufwandSetzen('tag', 1)">1 Tag</button>
<button type="button" data-tage="2" onclick="todoAufwandSetzen('tag', 2)">2 Tage</button>
<button type="button" data-tage="5" onclick="todoAufwandSetzen('tag', 5)">1 Woche</button>
</div>
</div>
<span class="pd-aufwand-info" id="todoAufwandInfoText"></span>
</div>
</section>
<section class="pd-abs">
<h3>Stand</h3>
@@ -1983,7 +2017,7 @@
Tab-Ordner, keine neue Abhängigkeit nach vorn. -->
<script src="/js/wand/wand.js?v=21"></script>
<script src="/js/settings/settings.js?v=14"></script>
<script src="/js/admin/admin.js?v=21"></script>
<script src="/js/admin/admin.js?v=22"></script>
<script src="/js/admin-users.js?v=10"></script>
<script src="/js/admin/rollen.js?v=1"></script>
<script src="/js/einfuehrung.js?v=2"></script>
@@ -2002,7 +2036,7 @@
<script src="/js/plan/plan-build-gantt.js?v=11"></script>
<script src="/js/plan/plan-gantt.js?v=21"></script>
<script src="/js/plan/plan-drag.js?v=8"></script>
<script src="/js/plan/plan-modals.js?v=15"></script>
<script src="/js/plan/plan-modals.js?v=18"></script>
<script src="/js/plan/plan-tasks.js?v=4"></script>
<script src="/js/plan/plan-phasen.js?v=2"></script>
<script src="/js/plan/plan-files.js?v=4"></script>
+3 -3
View File
@@ -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
View File
@@ -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;
+55 -5
View File
@@ -1522,11 +1522,51 @@ html[data-theme="dark"] .modal-bg.als-panel { background: rgba(0,0,0,.32); }
.pd-loeschen:hover { background: color-mix(in srgb, var(--red) 10%, transparent); }
.pd-loeschen[hidden] { display: none; }
/* Aufwand einer Aufgabe in Stunden (zählt zur Auslastung). */
.pd-aufwand { display: flex; align-items: center; gap: 10px; font-size: 13px; font-weight: 400; margin-top: 4px; }
.pd-aufwand > span:first-child { font-weight: 600; color: var(--text-muted); font-size: 12px; }
.pd-aufwand > span:last-child { color: var(--text-muted); font-size: 12px; }
.pd-aufwand input { width: 96px; padding: 7px 10px; border: 1px solid var(--line); border-radius: var(--r-md); font: inherit; font-size: 13px; color: var(--text); background: var(--surface); text-align: right; }
.pd-aufwand input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(0,113,227,.18); }
/* Aufwand im Aufgaben-Panel: Kopf, Stepper (−, Zahl, „Std.“, +),
Schnellwahl als kleine Kapseln, Umrechnung in Arbeitstage darunter.
Knöpfe tragen eigene Klassen bzw. stehen in eigenen Containern, damit die
allgemeine (Glas-)Knopfregel sie nicht akzentblau färbt. */
.pd-aufwand { display: grid; gap: 8px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line-soft); }
.pd-aufwand-kopf { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.pd-aufwand-kopf label { font-size: 12px; font-weight: 600; color: var(--text-muted); }
.pd-aufwand-kopf span { font-size: 11.5px; color: var(--text-subtle); }
.pd-aufwand-zeile { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.pd-stepper {
display: inline-flex; align-items: center; height: 34px; border: 1px solid var(--line);
border-radius: var(--r-pill); background: var(--surface-sunk); overflow: hidden;
transition: border-color var(--dur, 150ms), box-shadow var(--dur, 150ms);
}
.pd-stepper:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent); }
.pd-stepper .pd-stepper-knopf {
all: unset; box-sizing: border-box; width: 32px; height: 100%; display: grid; place-items: center;
font-size: 16px; color: var(--text-muted); cursor: pointer;
}
.pd-stepper .pd-stepper-knopf:hover { background: var(--seg-hover); color: var(--text); }
.pd-stepper .pd-stepper-knopf:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.pd-stepper input {
width: 52px; border: 0; background: transparent; padding: 0; text-align: right;
font: inherit; font-size: 15px; font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums;
-moz-appearance: textfield; appearance: textfield;
}
.pd-stepper input::-webkit-outer-spin-button, .pd-stepper input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.pd-stepper input:focus { outline: none; }
/* Glas-/Dialog-Regeln für Eingabefelder geben dem Feld sonst eine eigene
Fläche mitten im Stepper — hier bewusst durchsichtig, der Rahmen ist die Kapsel. */
.pd-stepper input#todoEditAufwand { background: transparent !important; border: 0 !important; border-radius: 0 !important; box-shadow: none !important; }
.pd-einheit { font-size: 12px; color: var(--text-muted); padding: 0 6px 0 4px; }
.pd-aufwand-schnell { display: flex; gap: 6px; flex-wrap: wrap; }
.pd-aufwand-schnell button {
all: unset; box-sizing: border-box; padding: 5px 11px; border-radius: var(--r-pill); cursor: pointer;
font-size: 12px; color: var(--text-muted); border: 1px solid var(--line); background: transparent;
transition: background var(--dur, 150ms), color var(--dur, 150ms), border-color var(--dur, 150ms);
}
.pd-aufwand-schnell button:hover { background: var(--seg-hover); color: var(--text); }
.pd-aufwand-schnell button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.pd-aufwand-schnell button[aria-pressed="true"] {
background: var(--accent-soft); color: var(--accent); font-weight: 600;
border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}
.pd-aufwand-info { font-size: 12px; color: var(--text-subtle); }
/* Terminplan aufgeräumt (Eriks Wahl "A", plan-gantt.js / plan-build-gantt.js):
eine Leiste statt zwei, Menüs "Mehr" und "+ Neu", Gliederung nach
@@ -1809,3 +1849,13 @@ html[data-theme="dark"] { --gantt-phase: #a59ef2; }
.mail-auswahl-zeile { grid-template-columns: auto 1fr; }
.mail-auswahl-art { grid-column: 2; justify-self: start; }
}
/* ---------- Aufgaben-Panel: „Externe Personen“ (Kollegen außerhalb des
Projekts, todoExterneSetzen() in plan-modals.js) ---------- */
.pd-externe { margin-top: 12px; padding-top: 10px; border-top: 1px dashed var(--line); display: grid; gap: 8px; }
.pd-externe-kopf { font-size: 12px; font-weight: 600; color: var(--text-muted); }
.pd-externe-kopf .hint { font-weight: 400; margin: 0 0 0 4px; }
#todoEditExterne:empty { display: none; }
#todoEditExterneWahl { max-width: 100%; }
.person-chip-x { margin-left: 4px; opacity: .6; }
.person-chip:hover .person-chip-x { opacity: 1; }