diff --git a/README.md b/README.md
index 7a9edc7..197f644 100644
--- a/README.md
+++ b/README.md
@@ -88,6 +88,25 @@ Ein Klick auf eine Zeile öffnet dieselbe Detailansicht wie eine Kachel, hier un
mindestens Vorgesetzter ist.
+## Kundenreport: Zeitraum
+
+*Reports → Kundenreport* hat neben der Kundenauswahl eine Zeitraumwahl: **Gesamt** (Standard, die ganze
+Kundenbeziehung wie bisher), **Woche**, **Monat**, **Quartal**, **Jahr** mit Vor/Zurück-Pfeilen oder ein freier
+**Zeitraum** (von–bis). Die Wahl merkt sich der Browser (`pulse_customer_report_period`) und bleibt beim
+Kundenwechsel stehen, damit sich Kunden im selben Zeitraum vergleichen lassen.
+
+- Auf den Zeitraum beziehen sich: Gesamtstunden (billable/non-billable), die Stunden je Projekt, die Tabs *Team*
+ und *Tasks* sowie die Exporte *PDF – Projekte* und *CSV – alle Buchungen*. Der Stundennachweis nimmt den Monat,
+ in dem der Zeitraum endet.
+- Die zweite Kachel zeigt mit Zeitraum den **Umsatz im Zeitraum** (Buchungen inkl. Festpreis-Anteil), darunter
+ *Erbracht gesamt*. **Umsatz offen**, **Budget gesamt** und die Budgetbalken bleiben Stand heute – sie hängen am
+ Projekt, nicht am Zeitraum.
+- Das Diagramm wählt die Einheit nach der Länge: bis 31 Tage je Tag, bis 184 Tage je Woche (KW), darüber je Monat.
+ Ohne Zeitraum wie bisher Monate mit Auswahl 6/12/24.
+- API: `GET /api/reports/customer/:id?from=YYYY-MM-DD&to=YYYY-MM-DD` – nur beide Werte zusammen schränken ein,
+ `from > to` gibt 400. Die Antwort enthält `range`, `series_unit` (`day`/`week`/`month`) und `series` mit `key`/`start`.
+- Die Zeitraumwahl ist `src/components/reports/PeriodPicker.tsx` und wird auch vom Zeitreport benutzt.
+
## Auslastung
Die Auslastung vergleicht **gebuchte Stunden bis heute** mit der **Kapazität bis heute**
@@ -335,6 +354,7 @@ Aus Auftrag und Call-Mitschrift.
| Kunden anlegen/bearbeiten, wer hat welche Projekte mit welchen Mitarbeitern, Umsatz erbracht/offen, animiert | ✅ | Kunden |
| Reports wie Harvest (Report.png), interaktiv, Export Excel/PDF/CSV | ✅ | Reports → Zeitreport |
| Report für einzelne Kunden / Projekt wie Report-Kunde.png | ✅ | Reports → Kunden-/Projektreport |
+| Kundenreport nach Woche, Monat, Jahr oder freiem Zeitraum | ✅ zusätzlich Quartal und Gesamt | Reports → Kundenreport |
| Einstellungen (nur Admin): SSO-Credentials, Userverwaltung, Kopf/Fuß der Exporte | ✅ inkl. PDF-/Excel-Vorschau | Einstellungen |
| Arbeitsstunden pro Woche (40) | ✅ | Einstellungen → Allgemein |
| Projektbezeichnungen nach Muster | ✅ | Einstellungen → Allgemein |
diff --git a/server/src/routes/reports.js b/server/src/routes/reports.js
index 578da54..8e579b9 100644
--- a/server/src/routes/reports.js
+++ b/server/src/routes/reports.js
@@ -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 };
diff --git a/src/components/reports/CustomerReport.tsx b/src/components/reports/CustomerReport.tsx
index 354afc1..1ca8cb8 100644
--- a/src/components/reports/CustomerReport.tsx
+++ b/src/components/reports/CustomerReport.tsx
@@ -7,8 +7,9 @@ import { ExportMenu } from './ExportMenu';
import { ReportKpi, ReportRow } from './ProjectReport';
import { useAuth } from '../../lib/auth';
import { customersApi, qs, reportsApi, type CustomerReport as CustomerReportData } from '../../lib/api';
-import { useLoader } from '../../lib/hooks';
-import { MONTHS, colorFor, formatEuro, formatHours, formatNumber, todayIso } from '../../lib/format';
+import { useLoader, usePersistentState } from '../../lib/hooks';
+import { PeriodPicker, periodRange, type Period } from './PeriodPicker';
+import { MONTHS, colorFor, formatDate, formatEuro, formatHours, formatNumber, isoWeek, monthStart, todayIso } from '../../lib/format';
import { useRouter } from '../../lib/router';
import { TYPE_TONE } from '../projects/Projects';
import { PROJECT_TYPE_LABELS } from '../../types';
@@ -16,7 +17,16 @@ import { PROJECT_TYPE_LABELS } from '../../types';
export const CustomerReport: React.FC<{ id: number | null }> = ({ id }) => {
const { navigate } = useRouter();
const customers = useLoader(() => customersApi.list(), []);
- const report = useLoader(() => (id ? reportsApi.customer(id) : Promise.resolve(null)), [id]);
+ // Zeitraum bleibt beim Kundenwechsel erhalten - so lassen sich Kunden im
+ // selben Monat vergleichen. 'all' zeigt die ganze Kundenbeziehung.
+ const [period, setPeriod] = usePersistentState('pulse_customer_report_period', 'all');
+ const [anchor, setAnchor] = useState(todayIso());
+ const [custom, setCustom] = useState({ from: monthStart(todayIso()), to: todayIso() });
+ const range = period === 'all' ? null : periodRange(period, anchor, custom);
+ const report = useLoader(
+ () => (id ? reportsApi.customer(id, range) : Promise.resolve(null)),
+ [id, range?.from, range?.to]
+ );
const [status, setStatus] = useState<'active' | 'inactive' | 'all'>('active');
// Der gewaehlte Kunde bleibt in der Liste, auch wenn der Filter ihn sonst
@@ -47,6 +57,7 @@ export const CustomerReport: React.FC<{ id: number | null }> = ({ id }) => {
{ value: 'all', label: 'Alle' },
]}
/>
+
{!id ? (
@@ -55,13 +66,15 @@ export const CustomerReport: React.FC<{ id: number | null }> = ({ id }) => {
) : !report.data ? (
) : (
-
+
+
+
)}
);
};
-const Body: React.FC<{ data: CustomerReportData }> = ({ data }) => {
+const Body: React.FC<{ data: CustomerReportData; rangeLabel: string | null }> = ({ data, rangeLabel }) => {
const { settings } = useAuth();
const { navigate } = useRouter();
const [tab, setTab] = useState<'projects' | 'team' | 'tasks'>('projects');
@@ -69,14 +82,26 @@ const Body: React.FC<{ data: CustomerReportData }> = ({ data }) => {
const c = data.customer;
const t = data.totals;
const color = colorFor(c.id, c.color);
- const month = todayIso().slice(0, 7);
- const series = useMemo(() => data.series.slice(-months), [data.series, months]);
+ const ranged = Boolean(data.range.from && data.range.to);
+ // Stundennachweis bezieht sich auf einen Monat: Monat des Zeitraumendes,
+ // sonst der laufende.
+ const month = (data.range.to ?? todayIso()).slice(0, 7);
+ const from = data.range.from ?? `${month.slice(0, 4)}-01-01`;
+ const to = data.range.to ?? todayIso();
+ const series = useMemo(() => (ranged ? data.series : data.series.slice(-months)), [data.series, months, ranged]);
+ const pointLabel = (start: string) => {
+ if (data.series_unit === 'day') return formatDate(start, 'day');
+ if (data.series_unit === 'week') return `KW ${isoWeek(start)}`;
+ return `${MONTHS[Number(start.slice(5, 7)) - 1].slice(0, 3)} ${start.slice(2, 4)}`;
+ };
+ const chartTitle = { day: 'Stunden je Tag', week: 'Stunden je Woche', month: 'Stunden je Monat' }[data.series_unit];
+ const rangeName = ranged ? 'Zeitraum' : 'Jahr';
const exportOptions = [
{ label: 'PDF – Stundennachweis (Monat)', hint: 'Vorlage für den Kunden, alle Berater', path: `/exports/timesheet${qs({ month, format: 'pdf', user_id: 'all', customer_id: c.id })}`, filename: `${c.short_name}-${month}.pdf` },
{ label: 'Excel – Stundennachweis mit Beträgen', hint: 'Für die Rechnungsstellung', path: `/exports/timesheet${qs({ month, format: 'xlsx', user_id: 'all', customer_id: c.id, amounts: 1 })}`, filename: `${c.short_name}-${month}.xlsx` },
- { label: 'PDF – Projekte (Jahr)', path: `/exports/report${qs({ from: `${month.slice(0, 4)}-01-01`, to: todayIso(), customer_id: c.id, group: 'projects', format: 'pdf', include_fixed: 1 })}`, filename: `${c.short_name}-projekte.pdf` },
- { label: 'CSV – alle Buchungen (Jahr)', path: `/exports/report${qs({ from: `${month.slice(0, 4)}-01-01`, to: todayIso(), customer_id: c.id, group: 'detail', format: 'csv', include_fixed: 1 })}`, filename: `${c.short_name}.csv` },
+ { label: `PDF – Projekte (${rangeName})`, path: `/exports/report${qs({ from, to, customer_id: c.id, group: 'projects', format: 'pdf', include_fixed: 1 })}`, filename: `${c.short_name}-projekte.pdf` },
+ { label: `CSV – alle Buchungen (${rangeName})`, path: `/exports/report${qs({ from, to, customer_id: c.id, group: 'detail', format: 'csv', include_fixed: 1 })}`, filename: `${c.short_name}.csv` },
];
return (
@@ -88,6 +113,7 @@ const Body: React.FC<{ data: CustomerReportData }> = ({ data }) => {
{c.name}
+ {rangeLabel && {rangeLabel} · }
{data.projects.length} Projekte · {data.team.length} Mitarbeiter
{c.contact_person && ` · Ansprechpartner: ${c.contact_person}`}
@@ -101,10 +127,17 @@ const Body: React.FC<{ data: CustomerReportData }> = ({ data }) => {
-
- formatEuro(v, 2)} className="text-3xl font-extrabold text-ink-900" />
-
-
+ {ranged ? (
+
+ formatEuro(v, 2)} className="text-3xl font-extrabold text-ink-900" />
+
+
+ ) : (
+
+ formatEuro(v, 2)} className="text-3xl font-extrabold text-ink-900" />
+
+
+ )}
formatEuro(v, 2)} className="text-3xl font-extrabold text-ink-900" />
s + p.project.pots.filter((x) => x.status === 'active').length, 0)}`} />
@@ -118,25 +151,27 @@ const Body: React.FC<{ data: CustomerReportData }> = ({ data }) => {
-
Stunden je Monat
+ {chartTitle}
-
+ {!ranged && (
+
+ )}
- {series.length ? (
+ {series.some((m) => m.minutes > 0) ? (
({ key: m.month, label: `${MONTHS[Number(m.month.slice(5)) - 1].slice(0, 3)} ${m.month.slice(2, 4)}`, a: m.billable_minutes, b: m.minutes - m.billable_minutes }))}
+ points={series.map((m) => ({ key: m.key, label: pointLabel(m.start), a: m.billable_minutes, b: m.minutes - m.billable_minutes }))}
format={(v) => `${formatHours(v, 1)} h`}
axisFormat={(v) => formatNumber(v / 60, 0)}
height={220}
/>
) : (
- Noch keine Buchungen.
+ {ranged ? 'Keine Buchungen im Zeitraum.' : 'Noch keine Buchungen.'}
)}
diff --git a/src/components/reports/PeriodPicker.tsx b/src/components/reports/PeriodPicker.tsx
new file mode 100644
index 0000000..e4e31d4
--- /dev/null
+++ b/src/components/reports/PeriodPicker.tsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import { inputClass } from '../ui/controls';
+import { Icon } from '../ui/Icon';
+import { MONTHS, addDays, addMonths, formatDate, isoWeek, mondayOf, monthEnd, monthStart } from '../../lib/format';
+
+export type Period = 'week' | 'month' | 'quarter' | 'year' | 'custom';
+
+export function periodRange(period: Period, anchor: string, custom: { from: string; to: string }) {
+ if (period === 'week') {
+ const from = mondayOf(anchor);
+ return { from, to: addDays(from, 6), label: `KW ${isoWeek(from)} · ${formatDate(from)} – ${formatDate(addDays(from, 6))}` };
+ }
+ if (period === 'month') return { from: monthStart(anchor), to: monthEnd(anchor), label: `${MONTHS[Number(anchor.slice(5, 7)) - 1]} ${anchor.slice(0, 4)}` };
+ if (period === 'quarter') {
+ const q = Math.floor((Number(anchor.slice(5, 7)) - 1) / 3);
+ const from = `${anchor.slice(0, 4)}-${String(q * 3 + 1).padStart(2, '0')}-01`;
+ return { from, to: monthEnd(addMonths(from, 2)), label: `Q${q + 1} ${anchor.slice(0, 4)}` };
+ }
+ if (period === 'year') return { from: `${anchor.slice(0, 4)}-01-01`, to: `${anchor.slice(0, 4)}-12-31`, label: anchor.slice(0, 4) };
+ return { ...custom, label: `${formatDate(custom.from)} – ${formatDate(custom.to)}` };
+}
+
+/** Verschiebt den Bezugstag um eine Periode vor oder zurueck. */
+export function shiftAnchor(period: Period, anchor: string, step: number) {
+ if (period === 'week') return addDays(anchor, 7 * step);
+ if (period === 'month') return addMonths(anchor, step);
+ if (period === 'quarter') return addMonths(anchor, 3 * step);
+ if (period === 'year') return `${Number(anchor.slice(0, 4)) + step}-01-01`;
+ return anchor;
+}
+
+/**
+ * Auswahl Woche/Monat/Quartal/Jahr/Zeitraum mit Blaettern. `allLabel` blendet
+ * zusaetzlich eine Option ohne Einschraenkung ein (Wert 'all').
+ */
+export function PeriodPicker({
+ period,
+ onPeriod,
+ anchor,
+ onAnchor,
+ custom,
+ onCustom,
+ allLabel,
+}: {
+ period: P;
+ onPeriod: (p: P) => void;
+ anchor: string;
+ onAnchor: (a: string) => void;
+ custom: { from: string; to: string };
+ onCustom: (c: { from: string; to: string }) => void;
+ allLabel?: string;
+}) {
+ const label = period === 'all' ? null : periodRange(period as Period, anchor, custom).label;
+ return (
+ <>
+
+ {period === 'all' ? null : period === 'custom' ? (
+
+ e.target.value && onCustom({ ...custom, from: e.target.value })} />
+ –
+ e.target.value && onCustom({ ...custom, to: e.target.value })} />
+
+ ) : (
+
+
+
+
+ {label}
+
+
+
+ )}
+ >
+ );
+}
diff --git a/src/components/reports/Reports.tsx b/src/components/reports/Reports.tsx
index d8733f9..25240bf 100644
--- a/src/components/reports/Reports.tsx
+++ b/src/components/reports/Reports.tsx
@@ -1,38 +1,22 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Avatar, Card, ErrorBox, IconButton, Spinner, inputClass } from '../ui/controls';
import { Modal } from '../ui/Modal';
-import { Icon } from '../ui/Icon';
import { CountUp, Donut, BILLABLE, NON_BILLABLE } from '../ui/charts';
import { ReportTable } from './ReportTable';
import { EntryList } from '../booking/EntryList';
import { ExportMenu } from './ExportMenu';
import { CustomerReport } from './CustomerReport';
import { ProjectReport } from './ProjectReport';
+import { PeriodPicker, periodRange, type Period } from './PeriodPicker';
import { useAuth } from '../../lib/auth';
import { customersApi, entriesApi, qs, reportsApi, usersApi, type GroupRow } from '../../lib/api';
import { useLoader, usePersistentState } from '../../lib/hooks';
-import { MONTHS, addDays, addMonths, formatDate, formatEuro, formatHours, isoWeek, mondayOf, monthEnd, monthStart, todayIso } from '../../lib/format';
+import { formatEuro, formatHours, monthStart, todayIso } from '../../lib/format';
import { useRouter } from '../../lib/router';
import { atLeast, type Customer, type DirectoryUser } from '../../types';
-type Period = 'week' | 'month' | 'quarter' | 'year' | 'custom';
type Tab = 'clients' | 'projects' | 'tasks' | 'users';
-function periodRange(period: Period, anchor: string, custom: { from: string; to: string }) {
- if (period === 'week') {
- const from = mondayOf(anchor);
- return { from, to: addDays(from, 6), label: `KW ${isoWeek(from)} · ${formatDate(from)} – ${formatDate(addDays(from, 6))}` };
- }
- if (period === 'month') return { from: monthStart(anchor), to: monthEnd(anchor), label: `${MONTHS[Number(anchor.slice(5, 7)) - 1]} ${anchor.slice(0, 4)}` };
- if (period === 'quarter') {
- const q = Math.floor((Number(anchor.slice(5, 7)) - 1) / 3);
- const from = `${anchor.slice(0, 4)}-${String(q * 3 + 1).padStart(2, '0')}-01`;
- return { from, to: monthEnd(addMonths(from, 2)), label: `Q${q + 1} ${anchor.slice(0, 4)}` };
- }
- if (period === 'year') return { from: `${anchor.slice(0, 4)}-01-01`, to: `${anchor.slice(0, 4)}-12-31`, label: anchor.slice(0, 4) };
- return { ...custom, label: `${formatDate(custom.from)} – ${formatDate(custom.to)}` };
-}
-
export const Reports: React.FC = () => {
const { route, navigate } = useRouter();
const [kind, id] = route.params;
@@ -105,13 +89,6 @@ const TimeReport: React.FC = () => {
[range.from, range.to, customerId, team, includeFixed, hideNonBillable]
);
- const shift = (step: number) => {
- if (period === 'week') setAnchor(addDays(anchor, 7 * step));
- if (period === 'month') setAnchor(addMonths(anchor, step));
- if (period === 'quarter') setAnchor(addMonths(anchor, 3 * step));
- if (period === 'year') setAnchor(`${Number(anchor.slice(0, 4)) + step}-01-01`);
- };
-
const exportParams = { from: range.from, to: range.to, customer_id: customerId || undefined, team: team || undefined, include_fixed: includeFixed ? 1 : undefined, hide_non_billable: hideNonBillable ? 1 : undefined };
const groupName = { clients: 'Kunden', projects: 'Projekte', tasks: 'Tasks', users: 'Mitarbeiter' }[tab];
const exportOptions = useMemo(
@@ -165,33 +142,7 @@ const TimeReport: React.FC = () => {
)}
-
- {period === 'custom' ? (
-
- setCustom({ ...custom, from: e.target.value })} />
- –
- setCustom({ ...custom, to: e.target.value })} />
-
- ) : (
-
-
-
-
- {range.label}
-
-
-
- )}
+
{report.error && }
diff --git a/src/lib/api.ts b/src/lib/api.ts
index c3b7d41..fa0708e 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -489,6 +489,8 @@ export interface ProjectDetail {
export interface CustomerReport {
customer: Customer;
+ range: { from: string | null; to: string | null };
+ series_unit: 'day' | 'week' | 'month';
totals: {
minutes: number;
billable_minutes: number;
@@ -498,7 +500,7 @@ export interface CustomerReport {
open: number;
budget_value: number;
};
- series: Array<{ month: string; minutes: number; billable_minutes: number; amount: number }>;
+ series: Array<{ key: string; start: string; minutes: number; billable_minutes: number; amount: number }>;
projects: Array>;
team: GroupRow[];
tasks: GroupRow[];
@@ -514,5 +516,5 @@ export const reportsApi = {
})}`
),
project: (id: number) => get(`/reports/project/${id}`),
- customer: (id: number) => get(`/reports/customer/${id}`),
+ customer: (id: number, range?: { from: string; to: string } | null) => get(`/reports/customer/${id}${qs({ from: range?.from, to: range?.to })}`),
};