Compare commits
2 Commits
3cd7cfcc34
...
3530dd247c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3530dd247c | ||
|
|
996012e3d3 |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "app-ts",
|
||||
"version": "2.4.45",
|
||||
"version": "2.4.46",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "app-ts",
|
||||
"version": "2.4.45",
|
||||
"version": "2.4.46",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.102.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-ts",
|
||||
"version": "2.4.45",
|
||||
"version": "2.4.46",
|
||||
"description": "",
|
||||
"main": "dist/server.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -347,6 +347,10 @@ exports[`buildUserSectionHtml > produces a stable per-user section (full output
|
||||
<span class="summary-label">Přesčas:</span>
|
||||
<span class="summary-value">0,00h</span>
|
||||
</div>
|
||||
<div class="summary-row summary-alert">
|
||||
<span class="summary-label">Chybějící hodiny:</span>
|
||||
<span class="summary-value">152,00h</span>
|
||||
</div>
|
||||
<div class="summary-row">
|
||||
<span class="summary-label">Svátek:</span>
|
||||
<span class="summary-value">0,00h</span>
|
||||
|
||||
@@ -133,6 +133,70 @@ describe("computeMzdaSummary — built-in acceptance dataset (Dominik 5/2026)",
|
||||
it("Práce v noci = 36,75 — VČETNĚ směny 31.5.→1.6. (výplatnice s 31 h se mýlí)", () => {
|
||||
expect(s.nightMinutes / 60).toBe(36.75);
|
||||
});
|
||||
|
||||
it("Chybějící hodiny = 0 (145,25 + 24 pokrývá fond 152)", () => {
|
||||
expect(s.manko).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeMzdaSummary — Chybějící hodiny (Dominik 6/2026, reálná data)", () => {
|
||||
// Real production month exposing the gap the paušál hides: 21 attended days
|
||||
// × 8 = 168 + 8h dovolená = 176 = fond exactly (weekend shifts nominally
|
||||
// compensate 3 missing weekdays 1.6./11.6./15.6.), yet REAL worked time is
|
||||
// only 136,5h → 31,5h below the fond. Přesčas must stay 0 (formulas are
|
||||
// binding), manko must surface the 31,5.
|
||||
const JUNE = [
|
||||
W("2026-06-02", "06:15", "14:00", ["10:00", "10:30"]),
|
||||
W("2026-06-03", "06:30", "13:30", ["10:00", "10:30"]),
|
||||
W("2026-06-04", "06:00", "13:45", ["09:45", "10:15"]),
|
||||
W("2026-06-05", "06:15", "13:45", ["10:00", "10:30"]),
|
||||
W("2026-06-07", "07:15", "13:00", ["09:30", "10:00"]), // Sunday
|
||||
W("2026-06-08", "07:15", "13:30", ["10:15", "10:45"]),
|
||||
W("2026-06-09", "06:00", "11:15", ["09:30", "10:00"]),
|
||||
W("2026-06-10", "06:00", "11:45", ["09:30", "10:00"]),
|
||||
W("2026-06-12", "06:00", "13:45", ["09:45", "10:15"]),
|
||||
W("2026-06-14", "17:30", "2026-06-15 04:15:00", ["22:45", "23:15"]), // Sun→Mon
|
||||
W("2026-06-16", "07:30", "13:30", ["10:00", "10:30"]),
|
||||
W("2026-06-17", "10:45", "13:15"),
|
||||
W("2026-06-18", "06:00", "13:15", ["09:30", "10:00"]),
|
||||
W("2026-06-19", "07:00", "13:15", ["10:00", "10:30"]),
|
||||
W("2026-06-22", "06:00", "13:30", ["09:45", "10:15"]),
|
||||
W("2026-06-23", "06:00", "13:45", ["09:45", "10:15"]),
|
||||
W("2026-06-24", "05:30", "13:45", ["09:30", "10:00"]),
|
||||
V("2026-06-25", 8),
|
||||
W("2026-06-26", "05:45", "14:45", ["10:15", "10:45"]),
|
||||
W("2026-06-27", "05:45", "17:00", ["11:15", "11:45"]), // Saturday
|
||||
W("2026-06-29", "11:45", "14:30"),
|
||||
W("2026-06-30", "06:30", "09:15"),
|
||||
W("2026-06-30", "10:45", "13:30"),
|
||||
];
|
||||
const s = computeMzdaSummary(JUNE as never, "2026-06");
|
||||
|
||||
it("verified rows stay exactly as before (fond 176, odpracováno 168, přesčas 0)", () => {
|
||||
expect(s.fund).toBe(176);
|
||||
expect(s.businessDays).toBe(22);
|
||||
expect(s.odpracovano).toBe(168); // 21 days × 8 flat
|
||||
expect(s.vacationHours).toBe(8);
|
||||
expect(s.workedHoursRaw).toBe(136.5);
|
||||
expect(s.prescas).toBe(0); // (136,5 + 8) − 176 < 0 — unchanged formula
|
||||
expect(s.vcetneSvatku).toBe(168);
|
||||
expect(s.svatekHours).toBe(0);
|
||||
expect(s.weekendHours).toBe(26.25); // shifts starting 7.6., 14.6., 27.6.
|
||||
// 14.6. shift (22:00–04:15 minus break = 5:45) + early starts before
|
||||
// 06:00: 24.6. 05:30 (0:30), 26.6. 05:45 (0:15), 27.6. 05:45 (0:15).
|
||||
expect(s.nightMinutes / 60).toBe(6.75);
|
||||
});
|
||||
|
||||
it("Chybějící hodiny = 31,5 — the gap the paušál nominally hides", () => {
|
||||
expect(s.manko).toBe(31.5); // 176 − (136,5 + 8)
|
||||
});
|
||||
|
||||
it("nemoc a neplacené volno kryjí fond stejně jako dovolená", () => {
|
||||
const withSick = [...JUNE, { ...V("2026-06-01", 8), leave_type: "sick" }];
|
||||
const s2 = computeMzdaSummary(withSick as never, "2026-06");
|
||||
expect(s2.sickHours).toBe(8);
|
||||
expect(s2.manko).toBe(23.5); // 31,5 − 8
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeMzdaSummary — formula edge cases", () => {
|
||||
|
||||
@@ -97,6 +97,7 @@ interface UserTotal {
|
||||
weekend_hours?: number;
|
||||
night_minutes?: number;
|
||||
worked_holiday_hours?: number; // sum of hours worked ON a holiday
|
||||
manko?: number; // Chybějící hodiny: real clock time below the fond
|
||||
records?: AttendanceRecord[];
|
||||
}
|
||||
|
||||
@@ -243,6 +244,7 @@ function computeUserTotals(
|
||||
t.weekend_hours = s.weekendHours;
|
||||
t.night_minutes = s.nightMinutes;
|
||||
t.worked_holiday_hours = s.svatekHours;
|
||||
t.manko = s.manko;
|
||||
}
|
||||
|
||||
return totals;
|
||||
@@ -427,6 +429,10 @@ export function buildUserSectionHtml(
|
||||
<span class="summary-label">Přesčas:</span>
|
||||
<span class="summary-value">${formatHoursDecimal(s.prescas * 60)}h</span>
|
||||
</div>
|
||||
<div class="summary-row${s.manko > 0 ? " summary-alert" : ""}">
|
||||
<span class="summary-label">Chybějící hodiny:</span>
|
||||
<span class="summary-value">${formatHoursDecimal(s.manko * 60)}h</span>
|
||||
</div>
|
||||
<div class="summary-row">
|
||||
<span class="summary-label">Svátek:</span>
|
||||
<span class="summary-value">${formatHoursDecimal(s.svatekHours * 60)}h</span>
|
||||
@@ -547,6 +553,9 @@ export function buildPrintHtml(
|
||||
}
|
||||
.user-summary .summary-label { color: #555; }
|
||||
.user-summary .summary-value { font-weight: 600; }
|
||||
/* Chybějící hodiny > 0 — must jump out of the sheet, not hide in it. */
|
||||
.user-summary .summary-alert .summary-label,
|
||||
.user-summary .summary-alert .summary-value { color: #b00020; font-weight: 700; }
|
||||
.user-summary .summary-fund {
|
||||
border-top: 1px solid #ddd; margin-top: 4px; padding-top: 6px;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ interface UserTotalData {
|
||||
prescas?: number;
|
||||
svatek?: number;
|
||||
worked_holiday_hours?: number;
|
||||
/** Chybějící hodiny: real clock time below the fond (approved absences count). */
|
||||
manko?: number;
|
||||
}
|
||||
|
||||
/** Fond "used" total mirrors the Fond footer line below:
|
||||
@@ -293,6 +295,13 @@ export default function AttendanceAdmin() {
|
||||
label={`Nep: ${ut.unpaid_hours}h`}
|
||||
/>
|
||||
)}
|
||||
{ut.manko !== undefined && ut.manko > 0 && (
|
||||
<StatusChip
|
||||
color="error"
|
||||
label={`Chybí: ${String(Math.round(ut.manko * 100) / 100).replace(".", ",")}h`}
|
||||
title="Skutečně odpracované hodiny + schválené absence nedosahují fondu měsíce"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Fond usage */}
|
||||
|
||||
@@ -411,6 +411,20 @@ export interface MzdaSummary {
|
||||
fund: number;
|
||||
/** Mon–Fri non-holiday day count backing the fond. */
|
||||
businessDays: number;
|
||||
/** Nemoc: sum of sick leave_hours (evidence records). */
|
||||
sickHours: number;
|
||||
/** Neplacené volno: sum of unpaid leave_hours (evidence records). */
|
||||
unpaidHours: number;
|
||||
/**
|
||||
* Chybějící hodiny (manko):
|
||||
* max(0, fond − (worked_total + dovolená + nemoc + neplacené volno))
|
||||
* — REAL clock hours below the fond. A supplement next to Přesčas, never a
|
||||
* replacement: the paušál Odpracováno can nominally cover the fond (weekend
|
||||
* work compensating missing weekdays) while dozens of real hours are
|
||||
* missing — e.g. Dominik 6/2026: paušál 168 + 8 dovolená = fond 176, yet
|
||||
* real worked was 136,5 → manko 31,5. This line makes that visible.
|
||||
*/
|
||||
manko: number;
|
||||
}
|
||||
|
||||
/** Compute the payroll recap for one user's month of records. */
|
||||
@@ -429,6 +443,8 @@ export const computeMzdaSummary = (
|
||||
let weekendMinutes = 0;
|
||||
let nightMinutes = 0;
|
||||
let vacationHours = 0;
|
||||
let sickHours = 0;
|
||||
let unpaidHours = 0;
|
||||
// Distinct non-holiday calendar days (start day) with a "Práce" record —
|
||||
// two shifts on one day still count that day once.
|
||||
const workDays = new Set<string>();
|
||||
@@ -438,6 +454,8 @@ export const computeMzdaSummary = (
|
||||
if (lt !== "work") {
|
||||
// "holiday" placeholder records are evidence-only — skip everywhere.
|
||||
if (lt === "vacation") vacationHours += Number(r.leave_hours) || 8;
|
||||
else if (lt === "sick") sickHours += Number(r.leave_hours) || 8;
|
||||
else if (lt === "unpaid") unpaidHours += Number(r.leave_hours) || 8;
|
||||
continue;
|
||||
}
|
||||
const m = calculateWorkMinutesPrint(r);
|
||||
@@ -468,6 +486,12 @@ export const computeMzdaSummary = (
|
||||
const workedHoursRaw = workedMinutes / 60;
|
||||
const odpracovano = workDays.size * 8;
|
||||
const prescas = Math.max(0, workedHoursRaw + vacationHours - fund);
|
||||
// Chybějící hodiny: REAL clock time (not the paušál) vs the fond — approved
|
||||
// absences (dovolená/nemoc/neplacené volno) cover the fond.
|
||||
const manko = Math.max(
|
||||
0,
|
||||
fund - (workedHoursRaw + vacationHours + sickHours + unpaidHours),
|
||||
);
|
||||
|
||||
return {
|
||||
workedHoursRaw,
|
||||
@@ -480,5 +504,8 @@ export const computeMzdaSummary = (
|
||||
nightMinutes,
|
||||
fund,
|
||||
businessDays,
|
||||
sickHours,
|
||||
unpaidHours,
|
||||
manko,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user