feat(reports): nicht abrechenbare Projekte ausblenden

Im Zeitreport stehen interne Projekte und Abwesenheiten zwischen den
Kundenzahlen: 2025 sind das 19.451 von 45.762 Stunden, und die Auslastung
zeigt 146 Prozent, weil Urlaub und Krankheit mitzaehlen.

Ein Haken neben der Billable-Aufteilung blendet Projekte vom Typ
"nicht abrechenbar" aus. Er wirkt auf den ganzen Report - Summen,
Auslastung, alle vier Gruppierungen - und auf die Exporte, die denselben
Report bauen; im PDF-Kopf steht dann "Nicht abrechenbare Projekte:
ausgeblendet". Gefiltert wird in loadEntries, damit Kunden-, Projekt- und
Taskliste dieselbe Grundmenge sehen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
schakal00783
2026-09-21 17:07:37 +02:00
co-authored by Claude Opus 5
parent 2710baa615
commit 80e46910fd
5 changed files with 43 additions and 7 deletions
+24 -4
View File
@@ -75,6 +75,9 @@ const TimeReport: React.FC = () => {
const [customerId, setCustomerId] = useState<number | ''>('');
const [team, setTeam] = useState<number | ''>('');
const [includeFixed, setIncludeFixed] = useState(false);
// Blendet interne Projekte und Abwesenheiten aus - der Report zeigt dann
// nur das Kundengeschaeft, inklusive Auslastung und Export.
const [hideNonBillable, setHideNonBillable] = useState(false);
const [customers, setCustomers] = useState<Customer[]>([]);
const [leads, setLeads] = useState<DirectoryUser[]>([]);
// Klick auf einen Mitarbeiter oeffnet seine Buchungen des Zeitraums als Liste.
@@ -90,8 +93,16 @@ const TimeReport: React.FC = () => {
const range = periodRange(period, anchor, custom);
const report = useLoader(
() => reportsApi.time({ from: range.from, to: range.to, customer_id: customerId || null, team: team || null, include_fixed: includeFixed }),
[range.from, range.to, customerId, team, includeFixed]
() =>
reportsApi.time({
from: range.from,
to: range.to,
customer_id: customerId || null,
team: team || null,
include_fixed: includeFixed,
hide_non_billable: hideNonBillable,
}),
[range.from, range.to, customerId, team, includeFixed, hideNonBillable]
);
const shift = (step: number) => {
@@ -101,7 +112,7 @@ const TimeReport: React.FC = () => {
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 };
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(
() => [
@@ -113,7 +124,7 @@ const TimeReport: React.FC = () => {
{ label: 'CSV – alle Buchungen', path: `/exports/report${qs({ ...exportParams, group: 'detail', format: 'csv' })}`, filename: 'Zeitreport-Detail.csv' },
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[tab, range.from, range.to, customerId, team, includeFixed]
[tab, range.from, range.to, customerId, team, includeFixed, hideNonBillable]
);
const data = report.data;
@@ -197,6 +208,15 @@ const TimeReport: React.FC = () => {
</span>
<b className="tabular text-ink-900">{formatHours(s!.non_billable_minutes)}</b>
</p>
<label className="flex cursor-pointer items-center gap-2 pt-1 text-xs font-semibold text-ink-700">
<input
type="checkbox"
checked={hideNonBillable}
onChange={(e) => setHideNonBillable(e.target.checked)}
className="h-4 w-4 accent-[var(--color-brand-500)]"
/>
Nicht abrechenbare Projekte ausblenden
</label>
</div>
</div>
<div>
+9 -2
View File
@@ -403,6 +403,7 @@ export interface GroupRow {
export interface TimeReport {
range: { from: string; to: string };
include_fixed: boolean;
hide_non_billable: boolean;
summary: {
minutes: number;
billable_minutes: number;
@@ -447,8 +448,14 @@ export interface CustomerReport {
}
export const reportsApi = {
time: (params: { from: string; to: string; customer_id?: number | null; team?: number | null; include_fixed?: boolean }) =>
get<TimeReport>(`/reports/time${qs({ ...params, include_fixed: params.include_fixed ? 1 : undefined })}`),
time: (params: { from: string; to: string; customer_id?: number | null; team?: number | null; include_fixed?: boolean; hide_non_billable?: boolean }) =>
get<TimeReport>(
`/reports/time${qs({
...params,
include_fixed: params.include_fixed ? 1 : undefined,
hide_non_billable: params.hide_non_billable ? 1 : undefined,
})}`
),
project: (id: number) => get<ProjectDetail>(`/reports/project/${id}`),
customer: (id: number) => get<CustomerReport>(`/reports/customer/${id}`),
};