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
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user