diff --git a/server/src/lib/entries.js b/server/src/lib/entries.js index d16785f..c3258dd 100644 --- a/server/src/lib/entries.js +++ b/server/src/lib/entries.js @@ -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 } = {}) { +export async function loadEntries(settings, { from, to, userIds = null, customerId, projectId, taskId, skipNonBillableProjects = false } = {}) { // Administratoren sind keine Mitarbeiter - ihre Zeiten zaehlen nirgends mit. const where = ["u.role <> 'admin'"]; const params = []; @@ -39,6 +39,9 @@ export async function loadEntries(settings, { from, to, userIds = null, customer where.push('e.task_id = ?'); params.push(Number(taskId)); } + // Fuer Auswertungen, die nur das Kundengeschaeft zeigen sollen: interne + // Projekte und Abwesenheiten bleiben draussen. + if (skipNonBillableProjects) where.push("p.type <> 'non_billable'"); const [rows] = await pool.query( `SELECT e.id, e.user_id, e.project_id, e.task_id, e.pot_id, e.entry_date, e.minutes, e.note, e.billable, diff --git a/server/src/routes/exports.js b/server/src/routes/exports.js index 23f4484..b0b4d94 100644 --- a/server/src/routes/exports.js +++ b/server/src/routes/exports.js @@ -244,6 +244,7 @@ router.get('/report', async (req, res, next) => { info: [ ['Kunde', customerName || 'Alle Kunden'], ['Festpreisprojekte', report.include_fixed ? 'im Betrag enthalten' : 'nicht im Betrag enthalten'], + ...(report.hide_non_billable ? [['Nicht abrechenbare Projekte', 'ausgeblendet']] : []), ], kpis: [ ['Gesamtstunden', hoursDe(s.minutes)], diff --git a/server/src/routes/reports.js b/server/src/routes/reports.js index 4cb2053..578da54 100644 --- a/server/src/routes/reports.js +++ b/server/src/routes/reports.js @@ -39,6 +39,9 @@ export async function buildTimeReport(viewer, query) { const from = query.from; const to = query.to; const includeFixed = query.include_fixed === '1' || query.include_fixed === 'true'; + // Schalter im Report: nicht abrechenbare Projekte (intern, Abwesenheiten) + // ausblenden - dann zeigt die Auswertung nur das Kundengeschaeft. + const hideNonBillable = query.hide_non_billable === '1' || query.hide_non_billable === 'true'; const userIds = await reportUserIds(viewer, query.team); const entries = await loadEntries(settings, { from, @@ -46,6 +49,7 @@ export async function buildTimeReport(viewer, query) { userIds, customerId: query.customer_id, projectId: query.project_id, + skipNonBillableProjects: hideNonBillable, }); const filter = userFilter(userIds, 'id'); @@ -91,6 +95,7 @@ export async function buildTimeReport(viewer, query) { return { range: { from, to }, include_fixed: includeFixed, + hide_non_billable: hideNonBillable, summary: { minutes, billable_minutes: billable, diff --git a/src/components/reports/Reports.tsx b/src/components/reports/Reports.tsx index d182c06..0d418eb 100644 --- a/src/components/reports/Reports.tsx +++ b/src/components/reports/Reports.tsx @@ -75,6 +75,9 @@ const TimeReport: React.FC = () => { const [customerId, setCustomerId] = useState(''); const [team, setTeam] = useState(''); 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([]); const [leads, setLeads] = useState([]); // 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 = () => { {formatHours(s!.non_billable_minutes)}

+
diff --git a/src/lib/api.ts b/src/lib/api.ts index 2348d76..ff8c3fe 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -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(`/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( + `/reports/time${qs({ + ...params, + include_fixed: params.include_fixed ? 1 : undefined, + hide_non_billable: params.hide_non_billable ? 1 : undefined, + })}` + ), project: (id: number) => get(`/reports/project/${id}`), customer: (id: number) => get(`/reports/customer/${id}`), };