feat(kunden): Kundennachweis ueber alle Berater exportieren
Bisher ging ein Stundennachweis nur von einer Person aus. Fuer die Rechnung an den Kunden ist aber der Auftrag die Klammer: wer alles auf Auxil gebucht hat, steht nirgends auf einem Blatt. Auf der Kundenseite oeffnet ein Knopf je Karte und Zeile den Kundennachweis: Monat, Format und wahlweise einzelne Projekte des Kunden; ohne Auswahl sind alle enthalten. Das Dokument zeigt je Zeile den Berater und fasst am Ende nach Berater (Tage, Dauer, Stunden, optional Betrag) und nach Projekt und Task zusammen. Dafuer nimmt /exports/timesheet ein ?project_ids fuer mehrere Projekte, und loadEntries filtert entsprechend. Die Berater-Zusammenfassung erscheint nur, wenn mehr als eine Person beteiligt war. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A8ZiAB96iSmQhDrK4Jpi2R
This commit is contained in:
co-authored by
Claude Opus 5
parent
dd42fba1cb
commit
8a8e433397
@@ -12,7 +12,7 @@ import { userFilter } from './access.js';
|
||||
* (Topfbudget / alle auf dem Topf gebuchten Stunden). Nur
|
||||
* intern, in Reports per Schalter zuschaltbar.
|
||||
*/
|
||||
export async function loadEntries(settings, { from, to, userIds = null, customerId, projectId, taskId, skipNonBillableProjects = false } = {}) {
|
||||
export async function loadEntries(settings, { from, to, userIds = null, customerId, projectId, projectIds, taskId, skipNonBillableProjects = false } = {}) {
|
||||
// Administratoren sind keine Mitarbeiter - ihre Zeiten zaehlen nirgends mit.
|
||||
const where = ["u.role <> 'admin'"];
|
||||
const params = [];
|
||||
@@ -35,6 +35,12 @@ export async function loadEntries(settings, { from, to, userIds = null, customer
|
||||
where.push('e.project_id = ?');
|
||||
params.push(Number(projectId));
|
||||
}
|
||||
// Mehrere Projekte: fuer den Kundennachweis, der eine Auswahl abdecken soll.
|
||||
const ids = (Array.isArray(projectIds) ? projectIds : []).map(Number).filter(Number.isFinite);
|
||||
if (ids.length) {
|
||||
where.push(`e.project_id IN (${ids.map(() => '?').join(',')})`);
|
||||
params.push(...ids);
|
||||
}
|
||||
if (taskId) {
|
||||
where.push('e.task_id = ?');
|
||||
params.push(Number(taskId));
|
||||
|
||||
@@ -42,12 +42,20 @@ router.get('/timesheet', async (req, res, next) => {
|
||||
}
|
||||
const showAmounts = req.query.amounts === '1' && hasRole(req.user, 'lead');
|
||||
const settings = await getSettings();
|
||||
// ?project_ids=1,2,3 fuer den Kundennachweis ueber eine Projektauswahl.
|
||||
const projectIds = String(req.query.project_ids || '')
|
||||
.split(',')
|
||||
.map((v) => Number(v.trim()))
|
||||
// Ohne Angabe bleibt die Liste leer - aus '' wuerde sonst eine 0 und
|
||||
// damit ein Filter auf ein Projekt, das es nicht gibt.
|
||||
.filter((v) => Number.isFinite(v) && v > 0);
|
||||
const entries = await loadEntries(settings, {
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
userIds,
|
||||
customerId: req.query.customer_id,
|
||||
projectId: req.query.project_id,
|
||||
projectIds,
|
||||
});
|
||||
|
||||
const [y, m] = req.query.month.split('-');
|
||||
@@ -110,6 +118,20 @@ router.get('/timesheet', async (req, res, next) => {
|
||||
summary.set(key, item);
|
||||
}
|
||||
|
||||
/*
|
||||
* Wer war beteiligt? Auf einem Kundennachweis ueber mehrere Berater ist das
|
||||
* die Frage, die als Erstes kommt - die Spalte je Zeile beantwortet sie
|
||||
* erst nach dem Durchlesen.
|
||||
*/
|
||||
const perPerson = new Map();
|
||||
for (const e of entries) {
|
||||
const item = perPerson.get(e.user_id) || { user_name: e.user_name, days: new Set(), minutes: 0, amount: 0 };
|
||||
item.days.add(e.entry_date);
|
||||
item.minutes += e.minutes;
|
||||
item.amount += e.amount;
|
||||
perPerson.set(e.user_id, item);
|
||||
}
|
||||
|
||||
const orderNumbers = [...new Set(entries.map((e) => e.pot_order_number).filter(Boolean))];
|
||||
|
||||
const doc = {
|
||||
@@ -138,6 +160,24 @@ router.get('/timesheet', async (req, res, next) => {
|
||||
rows,
|
||||
totals: { entry_date: null, weekday: 'Summe', duration: durationDe(total), minutes: total, ...(showAmounts ? { amount } : {}) },
|
||||
},
|
||||
...(multiUser && perPerson.size > 1
|
||||
? [
|
||||
{
|
||||
title: 'Zusammenfassung nach Berater',
|
||||
columns: [
|
||||
{ key: 'user_name', label: 'Berater', xlsWidth: 28 },
|
||||
{ key: 'days', label: 'Tage', align: 'right', width: 60 },
|
||||
{ key: 'duration', label: 'Dauer', align: 'right', width: 60 },
|
||||
{ key: 'minutes', label: 'Stunden', type: 'hours', align: 'right', width: 60 },
|
||||
...(showAmounts ? [{ key: 'amount', label: 'Betrag', type: 'money', align: 'right', width: 80 }] : []),
|
||||
],
|
||||
rows: [...perPerson.values()]
|
||||
.sort((a, b) => b.minutes - a.minutes)
|
||||
.map((r) => ({ ...r, days: String(r.days.size), duration: durationDe(r.minutes) })),
|
||||
totals: { user_name: 'Summe', days: String(days), duration: durationDe(total), minutes: total, ...(showAmounts ? { amount } : {}) },
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: 'Zusammenfassung nach Projekt und Task',
|
||||
columns: [
|
||||
|
||||
Reference in New Issue
Block a user