diff --git a/app.js b/app.js
index cb86587..7ee2e53 100644
--- a/app.js
+++ b/app.js
@@ -6,6 +6,7 @@
var endInput = document.getElementById("end-date");
var hoursInput = document.getElementById("worked-h");
var minutesInput = document.getElementById("worked-m");
+ var vacationInput = document.getElementById("unused-vacation");
var errorEl = document.getElementById("form-error");
var daysEl = document.getElementById("working-days");
var expectedEl = document.getElementById("expected-hours");
@@ -29,6 +30,7 @@
if (!endInput.value) endInput.value = todayIso();
if (!hoursInput.value) hoursInput.value = "0";
if (!minutesInput.value) minutesInput.value = "0";
+ if (!vacationInput.value) vacationInput.value = "0";
}
function showError(message) {
@@ -38,10 +40,13 @@
function render() {
var parts = Worktime.parseDurationParts(hoursInput.value, minutesInput.value);
+ var vacation = Worktime.parseNonNegativeInt(vacationInput.value);
var result = Worktime.calculate(
startInput.value,
endInput.value,
- parts ? parts.hmm : "invalid"
+ parts ? parts.hmm : "invalid",
+ undefined,
+ vacation == null ? "invalid" : vacation
);
if (!result.ok && result.error === "range") {
@@ -64,6 +69,16 @@
return;
}
+ if (vacation == null || (!result.ok && result.error === "vacation")) {
+ showError("Usa un número entero de días, sin negativos.");
+ daysEl.textContent = result.days != null ? String(result.days) : "—";
+ expectedEl.textContent = Worktime.formatHmm(result.expected);
+ differenceEl.textContent = "—";
+ differenceEl.classList.remove("is-behind");
+ dailyEl.textContent = "—";
+ return;
+ }
+
showError("");
daysEl.textContent = String(result.days);
expectedEl.textContent = Worktime.formatHmm(result.expected);
@@ -86,4 +101,6 @@
hoursInput.addEventListener("change", render);
minutesInput.addEventListener("input", render);
minutesInput.addEventListener("change", render);
+ vacationInput.addEventListener("input", render);
+ vacationInput.addEventListener("change", render);
})();
diff --git a/index.html b/index.html
index 3e6466e..301b936 100644
--- a/index.html
+++ b/index.html
@@ -59,6 +59,18 @@
Horas laborales
+
+
+
+
diff --git a/worktime.js b/worktime.js
index 64a5b01..80143e6 100644
--- a/worktime.js
+++ b/worktime.js
@@ -70,6 +70,19 @@
return days * perDay;
}
+ /** Non-negative integer field. Empty → 0. Rejects decimals and negatives. */
+ function parseNonNegativeInt(raw) {
+ if (raw == null || raw === "") return 0;
+ if (typeof raw === "number") {
+ if (!isFinite(raw) || raw < 0 || Math.floor(raw) !== raw) return null;
+ return raw;
+ }
+ var s = String(raw).trim();
+ if (s === "") return 0;
+ if (!/^\d+$/.test(s)) return null;
+ return Number(s);
+ }
+
/** Hours + minutes fields (Android numeric keypad). Empty → 0. Minutes must be 0–59. */
function parseDurationParts(hoursRaw, minutesRaw) {
function asInt(raw) {
@@ -106,18 +119,23 @@
return sign + h + ":" + String(min).padStart(2, "0");
}
- function calculate(startIso, endIso, workedText, holidays) {
+ function calculate(startIso, endIso, workedText, holidays, unusedVacationDays) {
var days = countWorkingDays(startIso, endIso, holidays);
var worked = parseHmm(workedText);
+ var vacation = parseNonNegativeInt(unusedVacationDays);
if (days == null) {
return { ok: false, error: "range", days: 0, expected: null, worked: worked, difference: null, daily: null };
}
if (worked == null) {
- return { ok: false, error: "hours", days: days, expected: days * HOURS_PER_DAY, worked: null, difference: null, daily: null };
+ var expectedOnHoursError = vacation == null ? days * HOURS_PER_DAY : (days - vacation) * HOURS_PER_DAY;
+ return { ok: false, error: "hours", days: days, expected: expectedOnHoursError, worked: null, difference: null, daily: null };
+ }
+ if (vacation == null) {
+ return { ok: false, error: "vacation", days: days, expected: days * HOURS_PER_DAY, worked: worked, difference: null, daily: null };
}
- var expected = days * HOURS_PER_DAY;
+ var expected = (days - vacation) * HOURS_PER_DAY;
var difference = worked - expected;
var daily = days === 0 ? null : worked / days;
return { ok: true, error: null, days: days, expected: expected, worked: worked, difference: difference, daily: daily };
@@ -131,6 +149,7 @@
expectedHours: expectedHours,
parseHmm: parseHmm,
parseDurationParts: parseDurationParts,
+ parseNonNegativeInt: parseNonNegativeInt,
formatHmm: formatHmm,
calculate: calculate,
};
diff --git a/worktime.test.js b/worktime.test.js
index e0f271a..924c961 100644
--- a/worktime.test.js
+++ b/worktime.test.js
@@ -97,3 +97,35 @@ test("zero working days does not divide by zero", () => {
assert.equal(r.days, 0);
assert.equal(r.daily, null);
});
+
+test("unused vacation days default to 0 and subtract 8 h each from expected", () => {
+ const none = W.calculate("2025-01-02", "2025-01-03", "16:00");
+ assert.equal(none.ok, true);
+ assert.equal(none.expected, 16);
+ assert.equal(none.difference, 0);
+
+ const one = W.calculate("2025-01-02", "2025-01-03", "16:00", undefined, 1);
+ assert.equal(one.ok, true);
+ assert.equal(one.days, 2);
+ assert.equal(one.expected, 8);
+ assert.equal(one.difference, 8);
+ assert.equal(one.daily, 8);
+
+ const empty = W.calculate("2025-01-02", "2025-01-03", "16:00", undefined, "");
+ assert.equal(empty.ok, true);
+ assert.equal(empty.expected, 16);
+});
+
+test("unused vacation days reject negatives and non-integers", () => {
+ assert.equal(W.parseNonNegativeInt(""), 0);
+ assert.equal(W.parseNonNegativeInt("0"), 0);
+ assert.equal(W.parseNonNegativeInt("3"), 3);
+ assert.equal(W.parseNonNegativeInt(-1), null);
+ assert.equal(W.parseNonNegativeInt("-1"), null);
+ assert.equal(W.parseNonNegativeInt("1.5"), null);
+ assert.equal(W.parseNonNegativeInt("abc"), null);
+
+ const r = W.calculate("2025-01-02", "2025-01-03", "16:00", undefined, "-1");
+ assert.equal(r.ok, false);
+ assert.equal(r.error, "vacation");
+});