Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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) {
Expand All @@ -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") {
Expand All @@ -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);
Expand All @@ -86,4 +101,6 @@
hoursInput.addEventListener("change", render);
minutesInput.addEventListener("input", render);
minutesInput.addEventListener("change", render);
vacationInput.addEventListener("input", render);
vacationInput.addEventListener("change", render);
})();
12 changes: 12 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ <h1>Horas laborales</h1>
</div>
</div>
</div>
<div class="field">
<label for="unused-vacation">Vacaciones no disfrutadas (días)</label>
<input
id="unused-vacation"
name="unusedVacation"
type="number"
inputmode="numeric"
min="0"
step="1"
autocomplete="off"
/>
</div>
<button type="submit">Calcular</button>
<p id="form-error" class="error" hidden></p>
</form>
Expand Down
25 changes: 22 additions & 3 deletions worktime.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 };
Expand All @@ -131,6 +149,7 @@
expectedHours: expectedHours,
parseHmm: parseHmm,
parseDurationParts: parseDurationParts,
parseNonNegativeInt: parseNonNegativeInt,
formatHmm: formatHmm,
calculate: calculate,
};
Expand Down
32 changes: 32 additions & 0 deletions worktime.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
Loading