import { pool } from '../db.js'; import { displayName, hourlyRate } from './projects.js'; import { userFilter } from './access.js'; /* * Buchungen fuer Auswertungen laden - mit allem, was Dashboard, Reports, * Kundenansicht und Exporte brauchen, und mit dem Betrag je Buchung: * * amount abrechenbarer Betrag (Stunden x Stundensatz des Topfs); * nur fuer Projekte vom Typ "billable" * fixed_amount Festpreisprojekte: Stunden x virtueller Stundensatz * (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 } = {}) { // Administratoren sind keine Mitarbeiter - ihre Zeiten zaehlen nirgends mit. const where = ["u.role <> 'admin'"]; const params = []; if (from) { where.push('e.entry_date >= ?'); params.push(from); } if (to) { where.push('e.entry_date <= ?'); params.push(to); } const users = userFilter(userIds); where.push(users.sql); params.push(...users.params); if (customerId) { where.push('p.customer_id = ?'); params.push(Number(customerId)); } if (projectId) { where.push('e.project_id = ?'); params.push(Number(projectId)); } if (taskId) { 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, e.created_at, e.updated_at, u.name AS user_name, u.color AS user_color, u.avatar AS user_avatar, u.is_external, u.company AS user_company, u.weekly_hours, u.manager_id, p.name AS project_name, p.code AS project_code, p.type AS project_type, p.billing_unit, p.rate AS project_rate, c.id AS customer_id, c.name AS customer_name, c.short_name AS customer_short, c.color AS customer_color, t.name AS task_name, t.rate AS task_rate, b.label AS pot_label, b.rate AS pot_rate, b.budget_amount AS pot_amount, b.order_number AS pot_order_number FROM pulse_time_entries e JOIN pulse_users u ON u.id = e.user_id JOIN pulse_projects p ON p.id = e.project_id JOIN pulse_customers c ON c.id = p.customer_id LEFT JOIN pulse_tasks t ON t.id = e.task_id LEFT JOIN pulse_budget_pots b ON b.id = e.pot_id WHERE ${where.join(' AND ')} ORDER BY e.entry_date, e.id`, params ); // Virtueller Stundensatz braucht die Stunden des ganzen Topfs, nicht nur des Zeitraums. const fixedPots = [...new Set(rows.filter((r) => r.project_type === 'fixed' && r.pot_id).map((r) => r.pot_id))]; const potMinutes = new Map(); if (fixedPots.length) { const [usage] = await pool.query( `SELECT pot_id, SUM(minutes) AS minutes FROM pulse_time_entries WHERE pot_id IN (${fixedPots.map(() => '?').join(',')}) GROUP BY pot_id`, fixedPots ); for (const u of usage) potMinutes.set(u.pot_id, Number(u.minutes)); } return rows.map((r) => { const project = { name: r.project_name, code: r.project_code, billing_unit: r.billing_unit, type: r.project_type, rate: r.project_rate }; const hours = r.minutes / 60; let amount = 0; let fixedAmount = 0; // Billable rechnet ueber den Satz des Topfs ab, Abruf ueber den am Projekt. // Traegt der Task einen eigenen Satz (z. B. "vor Ort"), gewinnt dieser. if (r.billable && (r.project_type === 'billable' || r.project_type === 'abruf')) { const rate = hourlyRate( r.project_type === 'abruf' ? null : { rate: r.pot_rate }, project, settings, r.task_rate == null ? null : { rate: r.task_rate } ); amount = rate ? hours * rate : 0; } if (r.billable && r.project_type === 'fixed' && r.pot_amount && potMinutes.get(r.pot_id)) { fixedAmount = (Number(r.pot_amount) / (potMinutes.get(r.pot_id) / 60)) * hours; } return { ...r, billable: Boolean(r.billable), is_external: Boolean(r.is_external), project_display: displayName(settings, project, { name: r.customer_name, short_name: r.customer_short }), amount: Math.round(amount * 100) / 100, fixed_amount: Math.round(fixedAmount * 100) / 100, }; }); }