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

+33 -16
View File
@@ -4679,13 +4679,19 @@ app.post('/save-week', requireAuth, (req, res) => {
}
});
// ---------- Teams: Nachricht bei neuen Aufgaben ----------
// ---------- Teams: persönliche Nachricht bei neuen Aufgaben ----------
//
// Eriks Wunsch: Wird jemandem eine Aufgabe zugewiesen, erscheint in einem
// Teams-Kanal eine Nachricht mit Link direkt auf die Aufgabe im Projekt.
// Eingerichtet in Teams über „Workflows“ → „Beim Empfang einer
// Webhook-Anfrage in einem Kanal posten“; die dort erzeugte Adresse trägt
// ein Admin unter Admin → Teams ein (Recht a.teams). Die Adresse ist ein
// Eriks Wunsch: Wird jemandem eine Aufgabe zugewiesen, bekommt GENAU DIESE
// Person in Teams eine Nachricht (Einzelchat mit dem Flow-Bot) mit Link
// direkt auf die Aufgabe im Projekt — nicht der ganze Kanal (Eriks Wahl
// „nur persönlich“). Dafür legt man in Teams einen Workflow an: Auslöser
// „Bei Empfang einer Teams-Webhookanforderung“, Aktion „Karte in einem Chat
// oder Kanal veröffentlichen“ als Flow-Bot im Chat, Empfänger
// triggerBody()?['empfaenger'], Karte string(triggerBody()?['karte']). Die
// Adresse des Auslösers trägt ein Admin unter Admin → Teams ein (Recht
// a.teams). Die App schickt je Person {empfaenger: E-Mail, karte: Adaptive
// Card}; Empfänger ist users.email (muss dem Teams-Anmeldenamen
// entsprechen). Personen ohne Konto oder E-Mail bekommen nichts. Die Adresse ist ein
// Geheimnis (wer sie kennt, kann in den Kanal schreiben) — sie steht in
// `integrationen`, nicht in app_settings (das jedes Konto lesen darf), und
// geht nie an den Browser zurück. Nicht in IMPORT_TABLES.
@@ -4707,17 +4713,16 @@ function integrationSchreiben(k, v){
.run(k, String(v));
}
// teamsKarte: Adaptive Card im Umschlag, den der Teams-Workflow erwartet.
// teamsKarte: die Adaptive Card selbst (der Workflow legt sie in den Chat).
function teamsKarte(titel, zeilen, fakten, link, linkText){
const body = [{ type: 'TextBlock', text: titel, weight: 'Bolder', size: 'Medium', wrap: true }];
zeilen.forEach(z => body.push({ type: 'TextBlock', text: z, wrap: true, spacing: 'Small' }));
if(fakten.length) body.push({ type: 'FactSet', facts: fakten.map(([title, value]) => ({ title, value })) });
const content = {
return {
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
type: 'AdaptiveCard', version: '1.4', body,
actions: link ? [{ type: 'Action.OpenUrl', title: linkText || 'Öffnen', url: link }] : []
};
return { type: 'message', attachments: [{ contentType: 'application/vnd.microsoft.card.adaptive', contentUrl: null, content }] };
}
// teamsSenden: an die eingetragene Adresse schicken. Rückgabe: Promise —
@@ -4756,22 +4761,31 @@ function teamsDatum(iso){
// teamsNeueAufgaben: je Aufgabe mit neu Zuständigen EINE Nachricht. neu =
// Map todoId -> { p, t, personen:[ids] }. Läuft im Hintergrund.
// teamsNeueAufgaben: je neu zuständiger PERSON eine Nachricht an sie selbst.
// neu = Map todoId -> { p, t, personen:[person_ids] }. Läuft im Hintergrund.
function teamsNeueAufgaben(weekLabel, neu, user, req){
if(!neu.size || integrationLesen('teams_neue_aufgaben') !== '1' || !integrationLesen('teams_webhook')) return;
const namen = new Map(db.prepare('SELECT person_id, name, full_name FROM week_people WHERE week_label = ?').all(weekLabel)
.map(r => [r.person_id, r.full_name || r.name || r.person_id]));
const konto = db.prepare(`SELECT email FROM users WHERE person_id = ? AND person_id != '' AND active = 1 AND email != '' AND ${sqlOhneWandtafel()}`);
const von = (user && (user.displayName || user.username)) || '';
neu.forEach(({ p, t, personen }) => {
const wer = personen.map(id => namen.get(id) || id).join(', ');
const zeitraum = t.start && t.end && t.start !== t.end ? teamsDatum(t.start) + ' – ' + teamsDatum(t.end) : '';
const fakten = [['Projekt', [p.nr, p.name].filter(Boolean).join(' ')], ['Zuständig', wer]];
const fakten = [['Projekt', [p.nr, p.name].filter(Boolean).join(' ')]];
if(personen.length > 1) fakten.push(['Zuständig', wer]);
if(t.end) fakten.push(['Fällig', teamsDatum(t.end)]);
if(zeitraum) fakten.push(['Zeitraum', zeitraum]);
if(Number(t.aufwand) > 0) fakten.push(['Aufwand', String(t.aufwand).replace('.', ',') + ' h']);
if(von) fakten.push(['Zugewiesen von', von]);
const karte = teamsKarte('Neue Aufgabe: ' + (t.title || 'Aufgabe'), [], fakten,
const karte = teamsKarte('Neue Aufgabe für Sie: ' + (t.title || 'Aufgabe'), [], fakten,
aufgabenLink(req, weekLabel, p, t.id), 'Aufgabe öffnen');
teamsSenden(karte).catch(err => console.warn('Teams-Nachricht nicht gesendet:', err.message));
personen.forEach(pid => {
const k = konto.get(pid);
if(!k){ console.log('Teams: keine E-Mail für', namen.get(pid) || pid, '— keine Nachricht'); return; }
teamsSenden({ empfaenger: k.email, karte })
.catch(err => console.warn('Teams-Nachricht an', k.email, 'nicht gesendet:', err.message));
});
});
}
@@ -4805,13 +4819,16 @@ app.put('/admin/teams', requireRecht('a.teams'), (req, res) => {
// POST /admin/teams/test — Testnachricht; Fehler als {ok:false, error} mit
// HTTP 200 (ein fehlgeschlagener Test ist ein erwartetes Ergebnis).
// Die Testnachricht geht an den Admin selbst (seine E-Mail aus dem Konto).
app.post('/admin/teams/test', requireRecht('a.teams'), async (req, res) => {
try {
const email = (db.prepare('SELECT email FROM users WHERE id = ?').get(req.user.id) || {}).email || '';
if(!email) return res.json({ ok: false, error: 'Ihr Konto hat keine E-Mail-Adresse — die braucht Teams, um Sie zu finden (Einstellungen → Mein Konto).' });
const link = req.protocol + '://' + req.get('host') + '/';
await teamsSenden(teamsKarte('Projektübersicht ist mit Teams verbunden',
['Ab jetzt erscheinen hier neue Aufgaben mit einem Link direkt zur Aufgabe im Projekt.'],
[['Eingerichtet von', req.user.displayName || req.user.username]], link, 'Projektübersicht öffnen'));
res.json({ ok: true });
await teamsSenden({ empfaenger: email, karte: teamsKarte('Projektübersicht ist mit Teams verbunden',
['Ab jetzt bekommt jede Person hier eine Nachricht, wenn ihr eine Aufgabe zugewiesen wird — mit Link direkt zur Aufgabe.'],
[['Eingerichtet von', req.user.displayName || req.user.username]], link, 'Projektübersicht öffnen') });
res.json({ ok: true, an: email });
} catch(err){
res.json({ ok: false, error: err.message });
}