feat(reports): Kundenreport nach Woche, Monat, Quartal, Jahr oder Zeitraum
Zeitraumwahl im Kundenreport (Standard Gesamt). Stunden, Team, Tasks, Projektstunden und Exporte beziehen sich auf den Zeitraum; Diagramm je Tag/Woche/Monat nach Laenge. Zeitraumwahl als PeriodPicker ausgelagert, auch im Zeitreport genutzt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5275f7c03d
commit
876ef24c75
@@ -4,7 +4,7 @@ import { teamOf, userFilter, visibleUserIds } from '../lib/access.js';
|
||||
import { getSettings } from '../lib/settings.js';
|
||||
import { loadEntries } from '../lib/entries.js';
|
||||
import { loadProjects } from '../lib/projects.js';
|
||||
import { addDays, mondayOf, parseDate, today, isoWeek } from '../lib/dates.js';
|
||||
import { addDays, daysBetween, mondayOf, parseDate, today, isoWeek } from '../lib/dates.js';
|
||||
import { capacityMinutes } from './dashboard.js';
|
||||
|
||||
const router = express.Router();
|
||||
@@ -196,6 +196,53 @@ function buildProjectDetail(project, entries) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeitreihe fuer den Kundenreport. Ohne Zeitraum: Monate ueber die ganze
|
||||
* Kundenbeziehung (hoechstens 24). Mit Zeitraum: die Einheit richtet sich nach
|
||||
* der Laenge - bis 31 Tage je Tag, bis ein halbes Jahr je Woche, sonst je Monat.
|
||||
*/
|
||||
function customerSeries(entries, from, to) {
|
||||
const sum = (key, start, own) => ({
|
||||
key,
|
||||
start,
|
||||
minutes: own.reduce((s, e) => s + e.minutes, 0),
|
||||
billable_minutes: own.filter((e) => e.billable).reduce((s, e) => s + e.minutes, 0),
|
||||
amount: r2(own.reduce((s, e) => s + e.amount + e.fixed_amount, 0)),
|
||||
});
|
||||
const monthly = (first, last, limit) => {
|
||||
const points = [];
|
||||
const cursor = new Date(`${first}-01T00:00:00Z`);
|
||||
while (cursor.toISOString().slice(0, 7) <= last && points.length < limit) {
|
||||
const month = cursor.toISOString().slice(0, 7);
|
||||
points.push(sum(month, `${month}-01`, entries.filter((e) => e.entry_date.startsWith(month))));
|
||||
cursor.setUTCMonth(cursor.getUTCMonth() + 1);
|
||||
}
|
||||
return points;
|
||||
};
|
||||
|
||||
if (!from) {
|
||||
const months = [...new Set(entries.map((e) => e.entry_date.slice(0, 7)))].sort();
|
||||
const last = today().slice(0, 7);
|
||||
return { unit: 'month', points: monthly(months[0] || last, last, 36).slice(-24) };
|
||||
}
|
||||
|
||||
const days = daysBetween(from, to) + 1;
|
||||
if (days <= 31) {
|
||||
const points = [];
|
||||
for (let d = from; d <= to; d = addDays(d, 1)) points.push(sum(d, d, entries.filter((e) => e.entry_date === d)));
|
||||
return { unit: 'day', points };
|
||||
}
|
||||
if (days <= 184) {
|
||||
const points = [];
|
||||
for (let monday = mondayOf(from); monday <= to; monday = addDays(monday, 7)) {
|
||||
const sunday = addDays(monday, 6);
|
||||
points.push(sum(monday, monday, entries.filter((e) => e.entry_date >= monday && e.entry_date <= sunday)));
|
||||
}
|
||||
return { unit: 'week', points };
|
||||
}
|
||||
return { unit: 'month', points: monthly(from.slice(0, 7), to.slice(0, 7), 120) };
|
||||
}
|
||||
|
||||
/** Kundenreport: alle Projekte eines Kunden mit Budget, Verlauf und Team. */
|
||||
router.get('/customer/:id', async (req, res, next) => {
|
||||
try {
|
||||
@@ -204,31 +251,20 @@ router.get('/customer/:id', async (req, res, next) => {
|
||||
if (!customer) return res.status(404).json({ error: 'Kunde nicht gefunden.' });
|
||||
const projects = await loadProjects(settings, { where: 'p.customer_id = ?', params: [customer.id] });
|
||||
const userIds = await visibleUserIds(req.user);
|
||||
const entries = await loadEntries(settings, { customerId: customer.id, userIds, from: req.query.from, to: req.query.to });
|
||||
// Zeitraum nur vollstaendig: von und bis, sonst die ganze Kundenbeziehung.
|
||||
const ranged = parseDate(req.query.from) && parseDate(req.query.to);
|
||||
const from = ranged ? req.query.from : null;
|
||||
const to = ranged ? req.query.to : null;
|
||||
if (ranged && from > to) return res.status(400).json({ error: 'Zeitraum ungültig.' });
|
||||
const entries = await loadEntries(settings, { customerId: customer.id, userIds, from, to });
|
||||
const series = customerSeries(entries, from, to);
|
||||
|
||||
// Monatsreihe ueber die ganze Kundenbeziehung, hoechstens 24 Monate.
|
||||
const months = [...new Set(entries.map((e) => e.entry_date.slice(0, 7)))].sort();
|
||||
const first = months[0] || today().slice(0, 7);
|
||||
const series = [];
|
||||
const cursor = new Date(`${first}-01T00:00:00Z`);
|
||||
const last = today().slice(0, 7);
|
||||
while (cursor.toISOString().slice(0, 7) <= last && series.length < 36) {
|
||||
const month = cursor.toISOString().slice(0, 7);
|
||||
const own = entries.filter((e) => e.entry_date.startsWith(month));
|
||||
series.push({
|
||||
month,
|
||||
minutes: own.reduce((s, e) => s + e.minutes, 0),
|
||||
billable_minutes: own.filter((e) => e.billable).reduce((s, e) => s + e.minutes, 0),
|
||||
amount: r2(own.reduce((s, e) => s + e.amount + e.fixed_amount, 0)),
|
||||
});
|
||||
cursor.setUTCMonth(cursor.getUTCMonth() + 1);
|
||||
}
|
||||
|
||||
|
||||
const minutes = entries.reduce((s, e) => s + e.minutes, 0);
|
||||
const minutes = entries.reduce((s, e) => s + e.minutes, 0);
|
||||
const billable = entries.filter((e) => e.billable).reduce((s, e) => s + e.minutes, 0);
|
||||
res.json({
|
||||
customer,
|
||||
range: { from, to },
|
||||
series_unit: series.unit,
|
||||
totals: {
|
||||
minutes,
|
||||
billable_minutes: billable,
|
||||
@@ -238,7 +274,7 @@ router.get('/customer/:id', async (req, res, next) => {
|
||||
open: r2(projects.reduce((s, p) => s + p.open, 0)),
|
||||
budget_value: r2(projects.reduce((s, p) => s + p.budget_value, 0)),
|
||||
},
|
||||
series: series.slice(-24),
|
||||
series: series.points,
|
||||
projects: projects.map((p) => {
|
||||
const own = entries.filter((e) => e.project_id === p.id);
|
||||
return { ...buildProjectDetail(p, own), weekly: undefined };
|
||||
|
||||
Reference in New Issue
Block a user