diff --git a/src/__tests__/__snapshots__/attendance-print-html.test.ts.snap b/src/__tests__/__snapshots__/attendance-print-html.test.ts.snap index ba8fdaf..1ae94c1 100644 --- a/src/__tests__/__snapshots__/attendance-print-html.test.ts.snap +++ b/src/__tests__/__snapshots__/attendance-print-html.test.ts.snap @@ -337,7 +337,7 @@ exports[`buildUserSectionHtml > produces a stable per-user section (full output
Odpracováno včetně svátků a přesčasů: - 16,00h + 8,00h
Dovolená: @@ -345,7 +345,7 @@ exports[`buildUserSectionHtml > produces a stable per-user section (full output
Přesčas: - 8,00h + 0,00h
Svátek: diff --git a/src/__tests__/attendance-mzda-summary.test.ts b/src/__tests__/attendance-mzda-summary.test.ts new file mode 100644 index 0000000..7d4b97a --- /dev/null +++ b/src/__tests__/attendance-mzda-summary.test.ts @@ -0,0 +1,176 @@ +import { describe, it, expect } from "vitest"; +import { computeMzdaSummary } from "../admin/utils/attendanceHelpers"; +import { buildUserSectionHtml } from "../admin/hooks/useAttendanceAdmin"; + +/** + * Payroll recap spec (accountant notes, confirmed by the owner 2026-07): + * - Odpracováno = all worked hours EXCEPT hours worked on holidays + * - Vč. svátků a přesčasů = ALL worked hours (so line2 − line1 = Svátek) + * - Dovolená = Σ vacation leave_hours (hour-based) + * - Přesčas = max(0, worked − fond) — month-level, never negative + * - Svátek = hours actually WORKED on public-holiday dates + * - So/Ne = worked hours on Sat/Sun + * - Fond = Mon–Fri (holidays included) × 8 h + * June 2026: 22 workdays, no holidays → fond 176. + * July 2026: 23 Mon–Fri days (6.7. Monday holiday counted; 5.7. is Sunday) → fond 184. + */ + +const work = ( + day: string, + from: string, + to: string, + brk?: [string, string], +) => ({ + shift_date: `${day}T02:00:00`, + arrival_time: `${day}T${from}:00`, + departure_time: `${day}T${to}:00`, + break_start: brk ? `${day}T${brk[0]}:00` : null, + break_end: brk ? `${day}T${brk[1]}:00` : null, + leave_type: "work", +}); +const vacation = (day: string, hours: number) => ({ + shift_date: `${day}T02:00:00`, + leave_type: "vacation", + leave_hours: hours, + arrival_time: null, + departure_time: null, + break_start: null, + break_end: null, +}); + +const JUNE_WORKDAYS = [ + "01", + "02", + "03", + "04", + "05", + "08", + "09", + "10", + "11", + "12", + "15", + "16", + "17", + "18", + "19", + "22", + "23", + "24", + "25", + "26", + "29", + "30", +].map((d) => `2026-06-${d}`); + +describe("computeMzdaSummary — accountant spec", () => { + it("single 3h day: worked 3, no overtime (173h below fond)", () => { + const s = computeMzdaSummary( + [work("2026-06-02", "09:00", "12:00")], + "2026-06", + ); + expect(s.odpracovano).toBe(3); + expect(s.vcetneSvatku).toBe(3); + expect(s.prescas).toBe(0); + expect(s.fund).toBe(176); + }); + + it("accountant's Přesčas example: +3h one day, −2h elsewhere → 1h overtime", () => { + const recs = JUNE_WORKDAYS.slice(0, 20).map((d) => + work(d, "08:00", "16:30", ["12:00", "12:30"]), + ); // 20 × 8h + recs.push(work("2026-06-29", "08:00", "14:00")); // 6h (2h short) + recs.push(work("2026-06-30", "08:00", "19:30", ["12:00", "12:30"])); // 11h (+3h) + const s = computeMzdaSummary(recs, "2026-06"); + expect(s.vcetneSvatku).toBe(177); + expect(s.prescas).toBe(1); + }); + + it("accountant's Svátek example: 10h worked on 6.7. → Svátek 10; excluded from Odpracováno", () => { + const julyWorkdays = [ + "01", + "02", + "03", + "07", + "08", + "09", + "10", + "13", + "14", + "15", + "16", + "17", + "20", + "21", + "22", + "23", + "24", + "27", + "28", + "29", + "30", + "31", + ].map((d) => `2026-07-${d}`); + const recs = julyWorkdays.map((d) => + work(d, "08:00", "16:30", ["12:00", "12:30"]), + ); // 22 × 8h + recs.push(work("2026-07-06", "08:00", "18:30", ["12:00", "12:30"])); // holiday, 10h + const s = computeMzdaSummary(recs, "2026-07"); + expect(s.fund).toBe(184); // 23 Mon–Fri days incl. the Monday holiday + expect(s.svatekHours).toBe(10); + expect(s.vcetneSvatku).toBe(186); // all worked hours + expect(s.odpracovano).toBe(176); // minus the holiday-worked 10h + expect(s.prescas).toBe(2); // max(0, 186 − 184) — never negative + }); + + it("vacation does NOT flow into Přesčas (worked-hours-only rule)", () => { + const recs: ReturnType[] = + JUNE_WORKDAYS.slice(0, 21).map((d) => + work(d, "08:00", "16:30", ["12:00", "12:30"]), + ); // 21 × 8 = 168 + recs.push(vacation("2026-06-30", 8)); + const s = computeMzdaSummary(recs, "2026-06"); + expect(s.vacationHours).toBe(8); + expect(s.prescas).toBe(0); // 168 worked < 176 fond — vacation doesn't count + }); + + it("partial-day vacation next to a work shift on the same day (5h work + 3h vacation)", () => { + const recs = [ + work("2026-06-02", "08:00", "13:00"), // 5h + vacation("2026-06-02", 3), + ]; + const s = computeMzdaSummary(recs, "2026-06"); + expect(s.odpracovano).toBe(5); + expect(s.vacationHours).toBe(3); + }); + + it("weekend hours count into So/Ne and into worked totals", () => { + const s = computeMzdaSummary( + [work("2026-06-06", "08:00", "12:00")], // Saturday 4h + "2026-06", + ); + expect(s.weekendHours).toBe(4); + expect(s.odpracovano).toBe(4); + }); +}); + +describe("print day table — multiple records on one day", () => { + it("renders one row per record (two shifts are both visible)", () => { + const records = [ + work("2026-06-02", "06:00", "10:00"), + work("2026-06-02", "14:00", "18:00"), + ]; + const totalMins = 8 * 60; + const html = buildUserSectionHtml( + "1", + { name: "Test", minutes: totalMins, vacation_hours: 0, records } as never, + { month: "2026-06", month_name: "Červen 2026", user_totals: {} } as never, + ); + // Both shifts appear, each with its own arrival time. + expect(html).toContain("06:00"); + expect(html).toContain("14:00"); + // Two rows dated 2. 6. + const rows = html.match(/2\. 6\. 2026<\/td>/g) || []; + expect(rows.length).toBe(2); + }); +}); diff --git a/src/admin/hooks/useAttendanceAdmin.ts b/src/admin/hooks/useAttendanceAdmin.ts index b97a360..89f1f9b 100644 --- a/src/admin/hooks/useAttendanceAdmin.ts +++ b/src/admin/hooks/useAttendanceAdmin.ts @@ -18,10 +18,8 @@ import { getDaysInMonth, shiftDateForMonth, formatHoursDecimal, - calculateNightMinutes, normalizeDateStr, - holidaysInMonth, - calculateFreeHolidayHours, + computeMzdaSummary, getCzechWeekday, } from "../utils/attendanceHelpers"; import type { @@ -216,116 +214,35 @@ function computeUserTotals( } } - // Add fund data per user (matching PHP addFundDataToUserTotals) - const [yearStr, monthStr] = month.split("-"); - const yr = parseInt(yearStr, 10); - const mo = parseInt(monthStr, 10) - 1; - - // Count Mon-Fri business days in month (INCLUDING holidays). - // The fund is rawBizDays × 8 — holidays stay in the count so the - // user is paid for them via the fund. Svátek (free holiday hours) is - // computed separately as "8h per holiday the user did not work". - let rawBizDays = 0; - const cur = new Date(yr, mo, 1); - while (cur.getMonth() === mo) { - const dow = cur.getDay(); - if (dow !== 0 && dow !== 6) rawBizDays++; - cur.setDate(cur.getDate() + 1); - } - - const holidayDates = holidaysInMonth(yr, mo + 1); - + // Rekapitulace per user — the SAME shared computation the print uses + // (computeMzdaSummary), so the screen and the printed sheet can never + // disagree. Accountant spec 2026-07: Odpracováno = worked minus + // holiday-worked; Vč. svátků = ALL worked; Přesčas = max(0, worked − fond); + // Svátek = hours actually worked on holidays. for (const uid of Object.keys(totals)) { const t = totals[uid]; const userRecords = recordsByUser.get(Number(uid)) || []; - - // Odpracováno: floor(worked / 8) × 8 — round to whole days, drop remainder. - const workedHoursRaw = t.minutes / 60; - const odpracovano = Math.floor(workedHoursRaw / 8) * 8; - - // So/Ne: sum of worked hours on Sat/Sun (raw, not rounded). - const weekendHours = userRecords - .filter( - (r) => - (r.leave_type || "work") === "work" && isWeekendDate(r.shift_date), - ) - .reduce((sum, r) => sum + calculateWorkMinutesPrint(r) / 60, 0); - - // Práce v noci: minutes in 22:00-06:00 across all work records. - const nightMinutes = userRecords - .filter((r) => (r.leave_type || "work") === "work") - .reduce((sum, r) => sum + calculateNightMinutes(r), 0); - - // Svátek (free): 8h per holiday the user did not work. - const freeHolidayHours = calculateFreeHolidayHours( - userRecords, - holidayDates, - ); - - // Worked holidays: holidays the user DID work (counted toward - // Odpracováno, but their 8h must not flow into Přesčas). - const workedDatesSet = new Set( - userRecords - .filter((r) => (r.leave_type || "work") === "work") - .map((r) => normalizeDateStr(r.shift_date)) - .filter(Boolean), - ); - let workedHolidayCount = 0; - let workedHolidayHours = 0; - for (const hd of holidayDates) { - if (workedDatesSet.has(hd)) workedHolidayCount++; - } - // Sum the actual hours worked on holiday dates (for the KPI card's - // "fond used" total = real_work + vacation + worked_holiday + free_holiday). - for (const r of userRecords) { - if ((r.leave_type || "work") !== "work") continue; - const d = normalizeDateStr(r.shift_date); - if (d && holidayDates.includes(d)) { - workedHolidayHours += calculateWorkMinutesPrint(r) / 60; - } - } - - // Fund = Mon-Fri × 8h. Holidays count as work days (already in rawBizDays). - const fund = rawBizDays * 8; - - // Včetně svátků a přesčasů (mzda formula): - // odpracovano + vacation + remainder − min(freeHols, workedHols × 8) - // - // Two-way logic matching the print: - // • If the user worked at least one holiday, that worked holiday's - // 8h is in Odpracováno and shouldn't also count as Přesčas. We - // subtract one 8h per worked holiday (capped at the total free - // holiday hours — can't go negative). - // • If the user did NOT work any holiday, no adjustment is needed: - // all unworked holidays are already paid via the Svátek line and - // don't flow into Přesčas, so vcetne_svatku just equals the - // work + vacation + remainder. - const remainder = workedHoursRaw - odpracovano; - const holidayAdj = Math.min(freeHolidayHours, workedHolidayCount * 8); - const vcetneSv = odpracovano - holidayAdj + t.vacation_hours + remainder; - - // Přesčas = vcetneSv − odpracovano (derived). - const prescas = vcetneSv - odpracovano; + const s = computeMzdaSummary(userRecords, month); // Legacy fields (kept so existing UI doesn't break). - const workedHours = Math.round(workedHoursRaw * 10) / 10; + const workedHours = Math.round(s.workedHoursRaw * 10) / 10; const covered = Math.round((workedHours + t.vacation_hours) * 10) / 10; - t.fund = fund; - t.business_days = rawBizDays; + t.fund = s.fund; + t.business_days = s.businessDays; t.worked_hours = workedHours; t.covered = covered; - t.missing = Math.max(0, Math.round((fund - covered) * 10) / 10); - t.overtime = Math.max(0, Math.round((covered - fund) * 10) / 10); + t.missing = Math.max(0, Math.round((s.fund - covered) * 10) / 10); + t.overtime = Math.max(0, Math.round((covered - s.fund) * 10) / 10); - t.worked_hours_raw = workedHoursRaw; - t.odpracovano = odpracovano; - t.vcetne_svatku = vcetneSv; - t.prescas = prescas; - t.svatek = freeHolidayHours; - t.weekend_hours = weekendHours; - t.night_minutes = nightMinutes; - t.worked_holiday_hours = workedHolidayHours; + t.worked_hours_raw = s.workedHoursRaw; + t.odpracovano = s.odpracovano; + t.vcetne_svatku = s.vcetneSvatku; + t.prescas = s.prescas; + t.svatek = s.svatekHours; + t.weekend_hours = s.weekendHours; + t.night_minutes = s.nightMinutes; + t.worked_holiday_hours = s.svatekHours; } return totals; @@ -390,10 +307,23 @@ export function buildUserSectionHtml( userData: UserTotal, printData: PrintData, ): string { - // Build a date-keyed lookup of the user's records. - const recordsByDate = new Map(); + // Build a date-keyed lookup of the user's records. A day may hold MULTIPLE + // records (two shifts, or work + partial-day vacation) — keep them ALL; a + // single-record map silently dropped every shift but the last one. + const recordsByDate = new Map(); for (const r of userData.records || []) { - recordsByDate.set(normalizeDateStr(r.shift_date), r); + const key = normalizeDateStr(r.shift_date); + const list = recordsByDate.get(key); + if (list) list.push(r); + else recordsByDate.set(key, [r]); + } + // Stable in-day order: by arrival time (leave records without times last). + for (const list of recordsByDate.values()) { + list.sort((a, b) => + String(a.arrival_time || "9999").localeCompare( + String(b.arrival_time || "9999"), + ), + ); } // Iterate one row per day of the month. @@ -407,19 +337,14 @@ export function buildUserSectionHtml( const rows: string[] = []; for (let d = 1; d <= daysInMonth; d++) { const dateStr = shiftDateForMonth(yr, mo, d); - const record = recordsByDate.get(dateStr); + const dayRecords = recordsByDate.get(dateStr); const weekend = isWeekendDate(dateStr); const holiday = isHoliday(dateStr); - const leaveType = (record?.leave_type as string) || "work"; - const rowClasses = [ - weekend && "weekend", - holiday && "holiday", - leaveType === "vacation" && "vacation", - ] - .filter(Boolean) - .join(" "); - if (!record) { + if (!dayRecords || dayRecords.length === 0) { + const rowClasses = [weekend && "weekend", holiday && "holiday"] + .filter(Boolean) + .join(" "); rows.push(` ${escapeHtml(formatDate(dateStr))} ${escapeHtml(getCzechWeekday(dateStr))} @@ -434,25 +359,37 @@ export function buildUserSectionHtml( continue; } - const isLeave = leaveType !== "work"; - const workMinutes = calculateWorkMinutesPrint(record); - const hours = Math.floor(workMinutes / 60); - const mins = workMinutes % 60; - const breakCell = - isLeave || !record.break_start || !record.break_end - ? "—" - : `${escapeHtml(formatTimeOrDatetimePrint(record.break_start, record.shift_date))} - ${escapeHtml(formatTimeOrDatetimePrint(record.break_end, record.shift_date))}`; + // One row PER RECORD — a day can hold two shifts, or work + partial-day + // vacation. (A single row per day silently dropped the extra shifts.) + for (const record of dayRecords) { + const leaveType = (record.leave_type as string) || "work"; + const rowClasses = [ + weekend && "weekend", + holiday && "holiday", + leaveType === "vacation" && "vacation", + ] + .filter(Boolean) + .join(" "); - let hoursCell: string; - if (workMinutes > 0 && !isLeave) { - hoursCell = `${hours}:${String(mins).padStart(2, "0")} (${formatHoursDecimal(workMinutes)})`; - } else if (isLeave) { - hoursCell = formatHoursDecimal((Number(record.leave_hours) || 8) * 60); - } else { - hoursCell = "—"; - } + const isLeave = leaveType !== "work"; + const workMinutes = calculateWorkMinutesPrint(record); + const hours = Math.floor(workMinutes / 60); + const mins = workMinutes % 60; + const breakCell = + isLeave || !record.break_start || !record.break_end + ? "—" + : `${escapeHtml(formatTimeOrDatetimePrint(record.break_start, record.shift_date))} - ${escapeHtml(formatTimeOrDatetimePrint(record.break_end, record.shift_date))}`; - rows.push(` + let hoursCell: string; + if (workMinutes > 0 && !isLeave) { + hoursCell = `${hours}:${String(mins).padStart(2, "0")} (${formatHoursDecimal(workMinutes)})`; + } else if (isLeave) { + hoursCell = formatHoursDecimal((Number(record.leave_hours) || 8) * 60); + } else { + hoursCell = "—"; + } + + rows.push(` ${escapeHtml(formatDate(dateStr))} ${escapeHtml(getCzechWeekday(dateStr))} ${escapeHtml(getLeaveTypeName(leaveType))} @@ -463,110 +400,48 @@ export function buildUserSectionHtml( ${buildProjectLogsHtml(record)} ${escapeHtml(record.notes || "")} `); - } - - // ----- mzda-style summary numbers (computed here, not from userData, - // because the backend's getPrintData only returns the legacy fields). ----- - - // Worked minutes: sum of calculateWorkMinutesPrint across work records. - let workedMinutes = 0; - let weekendHoursRaw = 0; - let nightMinutes = 0; - for (const r of userRecords) { - const leaveType = r.leave_type || "work"; - if (leaveType !== "work") continue; - const m = calculateWorkMinutesPrint(r); - workedMinutes += m; - if (isWeekendDate(r.shift_date)) { - weekendHoursRaw += m / 60; } - nightMinutes += calculateNightMinutes(r); } - // Sum of vacation/sick/unpaid hours (in hours, not minutes). The - // "holiday" leave_type is auto-computed from Czech holidays further down - // and no longer selectable in the UI, so it's deliberately omitted here. - let vacationHours = 0; - for (const r of userRecords) { - const lt = r.leave_type || "work"; - if (lt === "work") continue; - const h = Number(r.leave_hours) || 8; - if (lt === "vacation") vacationHours += h; - } - - // Odpracováno: floor(worked / 8) × 8. - const workedHoursRaw = workedMinutes / 60; - const odpracovano = Math.floor(workedHoursRaw / 8) * 8; - - // Free Svátek: 8h per holiday date the user did not work. - const holidayDates = holidaysInMonth(yr, mo); - const workedDates = new Set( - userRecords - .filter((r) => (r.leave_type || "work") === "work") - .map((r) => normalizeDateStr(r.shift_date)) - .filter(Boolean), - ); - let freeHolidayHours = 0; - let workedHolidayCount = 0; - for (const hd of holidayDates) { - if (workedDates.has(hd)) workedHolidayCount++; - else freeHolidayHours += 8; - } - - // Fund: count Mon-Fri (incl. holidays) × 8h. - let rawBizDays = 0; - const cur = new Date(yr, mo - 1, 1); - while (cur.getMonth() === mo - 1) { - const dow = cur.getDay(); - if (dow !== 0 && dow !== 6) rawBizDays++; - cur.setDate(cur.getDate() + 1); - } - const businessDays = rawBizDays; - const fund = businessDays * 8; - - // Včetně svátků a přesčasů: floor(worked/8)*8 − worked_holiday*8 + vacation + remainder. - // (A worked holiday counts toward Odpracováno, but its 8h should not flow - // into Přesčas — so we subtract it here. Unworked holidays are paid via - // the fund and don't affect this line.) - const remainder = workedHoursRaw - odpracovano; - const vcetneSv = - odpracovano - workedHolidayCount * 8 + vacationHours + remainder; - - // Přesčas = #2 - #1. - const prescas = vcetneSv - odpracovano; + // Rekapitulace — single shared implementation (accountant spec 2026-07): + // Odpracováno = all worked hours EXCEPT hours worked on holidays + // Vč. svátků a přesč. = ALL worked hours (line2 − line1 = Svátek) + // Přesčas = max(0, worked − fond) + // Svátek = hours actually worked on holiday dates + const s = computeMzdaSummary(userRecords, printData.month); const summaryHtml = `
Odpracováno: - ${formatHoursDecimal(odpracovano * 60)}h + ${formatHoursDecimal(s.odpracovano * 60)}h
Odpracováno včetně svátků a přesčasů: - ${formatHoursDecimal(vcetneSv * 60)}h + ${formatHoursDecimal(s.vcetneSvatku * 60)}h
Dovolená: - ${formatHoursDecimal(vacationHours * 60)}h + ${formatHoursDecimal(s.vacationHours * 60)}h
Přesčas: - ${formatHoursDecimal(prescas * 60)}h + ${formatHoursDecimal(s.prescas * 60)}h
Svátek: - ${formatHoursDecimal(freeHolidayHours * 60)}h + ${formatHoursDecimal(s.svatekHours * 60)}h
So/Ne: - ${formatHoursDecimal(weekendHoursRaw * 60)}h + ${formatHoursDecimal(s.weekendHours * 60)}h
Práce v noci: - ${formatHoursDecimal(nightMinutes)}h + ${formatHoursDecimal(s.nightMinutes)}h
Fond měsíce: - ${formatHoursDecimal(fund * 60)}h (${businessDays} dnů) + ${formatHoursDecimal(s.fund * 60)}h (${s.businessDays} dnů)
Víkend (So/Ne) diff --git a/src/admin/utils/attendanceHelpers.ts b/src/admin/utils/attendanceHelpers.ts index b058ddd..9a17f66 100644 --- a/src/admin/utils/attendanceHelpers.ts +++ b/src/admin/utils/attendanceHelpers.ts @@ -370,3 +370,94 @@ export const calculateFreeHolidayHours = ( } return free; }; + +// --------------------------------------------------------------------------- +// Mzda rekapitulace (single source — the print AND the admin screen consume +// this, so the two can never drift again; spec confirmed with the accountant +// 2026-07, see the summary lines below) +// --------------------------------------------------------------------------- + +export interface MzdaSummary { + /** All worked hours in the month: arrival→departure minus recorded breaks. */ + workedHoursRaw: number; + /** Odpracováno: all worked hours EXCEPT hours worked on public holidays. */ + odpracovano: number; + /** Odpracováno včetně svátků a přesčasů: ALL worked hours (line2 − line1 = Svátek). */ + vcetneSvatku: number; + /** Dovolená: sum of vacation leave_hours (hour-based, partial days allowed). */ + vacationHours: number; + /** Přesčas: worked hours above the monthly fund — max(0, worked − fond). */ + prescas: number; + /** Svátek: hours actually WORKED on public-holiday dates. */ + svatekHours: number; + /** So/Ne: worked hours on Saturdays and Sundays. */ + weekendHours: number; + /** Práce v noci: worked minutes in the 22:00–06:00 window. */ + nightMinutes: number; + /** Fond měsíce: Mon–Fri (public holidays included) × 8 h. */ + fund: number; + businessDays: number; + /** 8 h per holiday the user did NOT work (fond is covered by these). */ + freeHolidayHours: number; +} + +/** Compute the payroll recap for one user's month of records. */ +export const computeMzdaSummary = ( + records: AttendanceRecord[], + monthStr: string, +): MzdaSummary => { + const [yearStr, monthNumStr] = monthStr.split("-"); + const yr = parseInt(yearStr, 10); + const mo = parseInt(monthNumStr, 10); + + const holidayDates = holidaysInMonth(yr, mo); + const holidaySet = new Set(holidayDates); + + let workedMinutes = 0; + let svatekMinutes = 0; + let weekendMinutes = 0; + let nightMinutes = 0; + let vacationHours = 0; + + for (const r of records) { + const lt = r.leave_type || "work"; + if (lt !== "work") { + if (lt === "vacation") vacationHours += Number(r.leave_hours) || 8; + continue; + } + const m = calculateWorkMinutesPrint(r); + workedMinutes += m; + const day = normalizeDateStr(r.shift_date); + if (holidaySet.has(day)) svatekMinutes += m; + if (isWeekendDate(day)) weekendMinutes += m; + nightMinutes += calculateNightMinutes(r); + } + + // Fond: Mon–Fri × 8 h; public holidays stay in the count (they are paid via + // the fond — an unworked holiday earns its 8 h, see freeHolidayHours). + let businessDays = 0; + const cur = new Date(yr, mo - 1, 1); + while (cur.getMonth() === mo - 1) { + const dow = cur.getDay(); + if (dow !== 0 && dow !== 6) businessDays++; + cur.setDate(cur.getDate() + 1); + } + const fund = businessDays * 8; + + const workedHoursRaw = workedMinutes / 60; + const svatekHours = svatekMinutes / 60; + + return { + workedHoursRaw, + odpracovano: workedHoursRaw - svatekHours, + vcetneSvatku: workedHoursRaw, + vacationHours, + prescas: Math.max(0, workedHoursRaw - fund), + svatekHours, + weekendHours: weekendMinutes / 60, + nightMinutes, + fund, + businessDays, + freeHolidayHours: calculateFreeHolidayHours(records, holidayDates), + }; +};