diff --git a/working-hours-calculator/app.js b/working-hours-calculator/app.js
index fe870c5..cb1fd7c 100644
--- a/working-hours-calculator/app.js
+++ b/working-hours-calculator/app.js
@@ -1,458 +1,388 @@
-const STORAGE_KEY = "working-hours-calculator:v1";
+const STORAGE_KEY = 'whc-state-v1';
const DAY_LABELS = [
- "Week 1 - Monday",
- "Week 1 - Tuesday",
- "Week 1 - Wednesday",
- "Week 1 - Thursday",
- "Week 1 - Friday",
- "Week 2 - Monday",
- "Week 2 - Tuesday",
- "Week 2 - Wednesday",
- "Week 2 - Thursday",
- "Week 2 - Friday",
+ 'Week 1 - Mon',
+ 'Week 1 - Tue',
+ 'Week 1 - Wed',
+ 'Week 1 - Thu',
+ 'Week 1 - Fri',
+ 'Week 2 - Mon',
+ 'Week 2 - Tue',
+ 'Week 2 - Wed',
+ 'Week 2 - Thu',
+ 'Week 2 - Fri'
];
-const defaultSettings = () => ({
- hoursPerWeek: 37.5,
- targetFortnightHours: 75,
- defaultBreakMinutes: 60,
-});
-
-const defaultDay = (breakMinutes) => ({
- start: "09:00",
- end: "17:30",
- breakMinutes,
-});
-
-const defaultPattern = (settings) => ({
- id: crypto.randomUUID(),
- name: "Standard",
- days: Array.from({ length: 10 }, () => defaultDay(settings.defaultBreakMinutes)),
-});
-
-let state = loadState();
+const settingsInputs = {
+ hoursPerWeek: document.getElementById('hours-per-week'),
+ targetFortnight: document.getElementById('target-fortnight'),
+ defaultBreak: document.getElementById('default-break'),
+ resetTarget: document.getElementById('reset-target')
+};
+const heroStats = {
+ weekly: document.getElementById('stat-weekly'),
+ target: document.getElementById('stat-target'),
+ break: document.getElementById('stat-break'),
+ patterns: document.getElementById('stat-patterns')
+};
+const addPatternButton = document.getElementById('add-pattern');
+const patternsContainer = document.getElementById('patterns');
+const comparisonBody = document.getElementById('comparison-body');
function loadState() {
+ const defaults = {
+ settings: {
+ hoursPerWeek: 37.5,
+ targetFortnightHours: 75,
+ defaultBreak: 60,
+ autoTarget: true
+ },
+ patterns: []
+ };
+
try {
- const raw = localStorage.getItem(STORAGE_KEY);
- if (!raw) {
- const settings = defaultSettings();
- return { settings, patterns: [defaultPattern(settings)] };
+ const stored = localStorage.getItem(STORAGE_KEY);
+ if (!stored) {
+ return defaults;
}
- const parsed = JSON.parse(raw);
- // Basic shape validation and defaults
- const settings = {
- ...defaultSettings(),
- ...(parsed.settings || {}),
+ const parsed = JSON.parse(stored);
+ return {
+ settings: {
+ ...defaults.settings,
+ ...parsed.settings,
+ autoTarget: parsed.settings?.autoTarget ?? true
+ },
+ patterns: parsed.patterns?.length ? parsed.patterns : []
};
- const patterns = Array.isArray(parsed.patterns) && parsed.patterns.length
- ? parsed.patterns
- : [defaultPattern(settings)];
- return { settings, patterns };
- } catch (err) {
- console.error("Failed to load state, using defaults", err);
- const settings = defaultSettings();
- return { settings, patterns: [defaultPattern(settings)] };
+ } catch (error) {
+ console.warn('Falling back to defaults due to load error', error);
+ return defaults;
}
}
-function persist() {
+function saveState() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
-function parseTimeToMinutes(value) {
- if (typeof value !== "string") return null;
- const match = /^([01]?\d|2[0-3]):([0-5]\d)$/.exec(value.trim());
- if (!match) return null;
- const hours = Number(match[1]);
- const minutes = Number(match[2]);
- return hours * 60 + minutes;
+function minutesToDecimalHours(minutes) {
+ return Math.round((minutes / 60) * 100) / 100;
}
-function minutesToHoursDecimal(minutes) {
- return (minutes / 60).toFixed(2);
+function parseTimeToMinutes(timeString) {
+ if (!timeString || !/^\d{2}:\d{2}$/.test(timeString)) return null;
+ const [hours, minutes] = timeString.split(':').map(Number);
+ return hours * 60 + minutes;
}
-function calcDaily(day) {
- const start = parseTimeToMinutes(day.start);
- const end = parseTimeToMinutes(day.end);
+function calculateDayHours(day) {
+ const startMinutes = parseTimeToMinutes(day.start);
+ const endMinutes = parseTimeToMinutes(day.end);
const breakMinutes = Number(day.breakMinutes);
- if (Number.isNaN(breakMinutes) || breakMinutes < 0) {
- return { valid: false, hours: null, message: "Break must be non-negative" };
- }
- // Treat a day with no times as a zero-hour (valid) day off
- if (start === null && end === null) {
- return { valid: true, hours: 0 };
- }
- if (start === null || end === null) {
- return { valid: false, hours: null, message: "Enter start and end time" };
- }
- if (end <= start) {
- return { valid: false, hours: null, message: "End must be after start" };
- }
- const totalMinutes = end - start;
- if (breakMinutes >= totalMinutes) {
- return { valid: false, hours: null, message: "Break too long for shift" };
+ if (startMinutes === null || endMinutes === null || Number.isNaN(breakMinutes)) {
+ return { valid: false, hours: null, message: 'Add start, end, and break' };
}
- const workMinutes = totalMinutes - breakMinutes;
- return { valid: true, hours: Number(minutesToHoursDecimal(workMinutes)) };
-}
-function calcPattern(pattern, settings) {
- const dayResults = pattern.days.map(calcDaily);
+ if (endMinutes <= startMinutes) {
+ return { valid: false, hours: null, message: 'End must be after start' };
+ }
- const week1 = dayResults.slice(0, 5).reduce((sum, d) => (d.valid ? sum + d.hours : sum), 0);
- const week2 = dayResults.slice(5).reduce((sum, d) => (d.valid ? sum + d.hours : sum), 0);
- const fortnight = week1 + week2;
- const gap = Number((fortnight - settings.targetFortnightHours).toFixed(2));
- const hasInvalid = dayResults.some((d) => !d.valid);
+ const duration = endMinutes - startMinutes;
+ if (breakMinutes < 0 || breakMinutes >= duration) {
+ return { valid: false, hours: null, message: 'Break must be less than shift length' };
+ }
- return { dayResults, week1, week2, fortnight, gap, hasInvalid };
+ const workMinutes = duration - breakMinutes;
+ return {
+ valid: true,
+ hours: minutesToDecimalHours(workMinutes)
+ };
}
-function updateSettings(key, value) {
- const newSettings = { ...state.settings, [key]: value };
+function calculatePatternTotals(pattern, settings) {
+ let week1 = 0;
+ let week2 = 0;
+ let hasInvalid = false;
- // If hoursPerWeek changes and target matches prior derived, update target too
- if (key === "hoursPerWeek") {
- const priorDerived = Number((state.settings.hoursPerWeek * 2).toFixed(2));
- const currentTarget = Number(state.settings.targetFortnightHours);
- const newDerived = Number((value * 2).toFixed(2));
- if (currentTarget === priorDerived) {
- newSettings.targetFortnightHours = newDerived;
+ const dailyHours = pattern.days.map((day, index) => {
+ const result = calculateDayHours(day);
+ if (result.valid && typeof result.hours === 'number') {
+ if (index < 5) {
+ week1 += result.hours;
+ } else {
+ week2 += result.hours;
+ }
+ } else if (day.start || day.end || day.breakMinutes !== '') {
+ hasInvalid = true;
}
- }
-
- state = { ...state, settings: newSettings };
- persist();
- render();
-}
-
-function addPattern(prefill = true) {
- const settings = state.settings;
- const pattern = prefill
- ? defaultPattern(settings)
- : {
- id: crypto.randomUUID(),
- name: "New pattern",
- days: Array.from({ length: 10 }, () => ({
- start: "",
- end: "",
- breakMinutes: settings.defaultBreakMinutes,
- })),
- };
+ return result;
+ });
- state = { ...state, patterns: [...state.patterns, pattern] };
- persist();
- render();
- showToast("Pattern added");
-}
+ const fortnight = Math.round((week1 + week2) * 100) / 100;
+ const gap = Math.round((fortnight - Number(settings.targetFortnightHours || 0)) * 100) / 100;
-function duplicatePattern(id) {
- const existing = state.patterns.find((p) => p.id === id);
- if (!existing) return;
- const copy = {
- ...existing,
- id: crypto.randomUUID(),
- name: `${existing.name} (copy)`,
- days: existing.days.map((d) => ({ ...d })),
+ return {
+ week1: Math.round(week1 * 100) / 100,
+ week2: Math.round(week2 * 100) / 100,
+ fortnight,
+ gap,
+ dailyHours,
+ hasInvalid
};
- state = { ...state, patterns: [...state.patterns, copy] };
- persist();
- render();
- showToast("Pattern duplicated");
}
-function deletePattern(id) {
- const pattern = state.patterns.find((p) => p.id === id);
- if (!pattern) return;
- if (!confirm(`Delete pattern "${pattern.name}"?`)) return;
- state = { ...state, patterns: state.patterns.filter((p) => p.id !== id) };
- if (state.patterns.length === 0) {
- state.patterns.push(defaultPattern(state.settings));
- }
- persist();
- render();
- showToast("Pattern deleted");
-}
+function createDefaultPattern(name) {
+ const defaultDay = () => ({
+ start: '09:00',
+ end: '17:30',
+ breakMinutes: state.settings.defaultBreak
+ });
-function renamePattern(id, name) {
- state = {
- ...state,
- patterns: state.patterns.map((p) => (p.id === id ? { ...p, name } : p)),
+ return {
+ id: crypto.randomUUID ? crypto.randomUUID() : `pattern-${Date.now()}-${Math.random()}`,
+ name,
+ days: Array.from({ length: 10 }, defaultDay)
};
- persist();
- render();
}
-function updateDayField(patternId, dayIndex, field, value) {
- state = {
- ...state,
- patterns: state.patterns.map((p) =>
- p.id === patternId
- ? {
- ...p,
- days: p.days.map((d, idx) =>
- idx === dayIndex ? { ...d, [field]: field === "breakMinutes" ? Number(value) : value } : d
- ),
- }
- : p
- ),
- };
- persist();
- render();
+const state = loadState();
+if (state.patterns.length === 0) {
+ state.patterns.push(createDefaultPattern('Pattern 1'));
+ saveState();
}
function renderSettings() {
- const { hoursPerWeek, targetFortnightHours, defaultBreakMinutes } = state.settings;
- const hoursInput = document.querySelector("#hoursPerWeek");
- const targetInput = document.querySelector("#targetFortnightHours");
- const breakInput = document.querySelector("#defaultBreakMinutes");
- const errorsEl = document.querySelector("#settings-errors");
-
- hoursInput.value = hoursPerWeek;
- targetInput.value = targetFortnightHours;
- breakInput.value = defaultBreakMinutes;
-
- const errors = [];
- if (!(hoursPerWeek > 0)) errors.push("Hours per week must be greater than 0");
- if (!(targetFortnightHours > 0)) errors.push("Target fortnight hours must be greater than 0");
- if (!(defaultBreakMinutes >= 0)) errors.push("Default break must be non-negative");
- errorsEl.textContent = errors.join(" • ");
+ settingsInputs.hoursPerWeek.value = state.settings.hoursPerWeek;
+ settingsInputs.targetFortnight.value = state.settings.targetFortnightHours;
+ settingsInputs.defaultBreak.value = state.settings.defaultBreak;
+ updateHeroStats();
}
-function renderPatterns() {
- const container = document.querySelector("#patterns-container");
- container.innerHTML = "";
-
- state.patterns.forEach((pattern) => {
- const { dayResults, week1, week2, fortnight, gap, hasInvalid } = calcPattern(
- pattern,
- state.settings
- );
-
- const card = document.createElement("article");
- card.className = "pattern-card";
- card.dataset.patternId = pattern.id;
+function updateTargetFromWeekly() {
+ state.settings.autoTarget = true;
+ state.settings.targetFortnightHours = Math.round((Number(state.settings.hoursPerWeek) * 2) * 100) / 100;
+}
- const header = document.createElement("div");
- header.className = "pattern-header";
-
- header.innerHTML = `
-
-
- ${hasInvalid ? 'Pattern has invalid days' : ""}
-
-
-
-
-
- `;
+function renderPatterns() {
+ patternsContainer.innerHTML = '';
+ updateHeroStats();
+
+ state.patterns.forEach((pattern, patternIndex) => {
+ const metrics = calculatePatternTotals(pattern, state.settings);
+ const patternTemplate = document.getElementById('pattern-template');
+ const node = patternTemplate.content.cloneNode(true);
+ const card = node.querySelector('.pattern-card');
+
+ const nameInput = node.querySelector('.pattern-name');
+ nameInput.value = pattern.name;
+ nameInput.addEventListener('input', (event) => {
+ state.patterns[patternIndex].name = event.target.value;
+ saveAndRender();
+ });
- const tableWrapper = document.createElement("div");
- tableWrapper.className = "table-wrapper";
-
- const table = document.createElement("table");
- table.className = "day-grid";
- const tbody = document.createElement("tbody");
-
- // Header
- table.innerHTML = `
-
-
- | Day |
- Start |
- End |
- Break (min) |
- Hours |
-
-
- `;
+ const duplicateBtn = node.querySelector('.duplicate');
+ duplicateBtn.addEventListener('click', () => {
+ const copy = JSON.parse(JSON.stringify(pattern));
+ copy.id = crypto.randomUUID ? crypto.randomUUID() : `pattern-${Date.now()}-${Math.random()}`;
+ copy.name = `${pattern.name} (copy)`;
+ state.patterns.splice(patternIndex + 1, 0, copy);
+ saveAndRender();
+ });
- pattern.days.forEach((day, idx) => {
- const result = dayResults[idx];
- const row = document.createElement("tr");
- if (!result.valid) row.classList.add("invalid-row");
-
- row.innerHTML = `
- ${DAY_LABELS[idx]} |
- |
- |
- |
- ${result.valid ? result.hours.toFixed(2) : "—"} |
- `;
-
- if (!result.valid && result.message) {
- const msgRow = document.createElement("tr");
- msgRow.className = "invalid-row";
- msgRow.innerHTML = `${result.message} | `;
- tbody.appendChild(row);
- tbody.appendChild(msgRow);
- } else {
- tbody.appendChild(row);
- }
+ const deleteBtn = node.querySelector('.delete');
+ deleteBtn.disabled = state.patterns.length === 1;
+ deleteBtn.addEventListener('click', () => {
+ if (state.patterns.length === 1) return;
+ state.patterns.splice(patternIndex, 1);
+ saveAndRender();
});
- const summaryRow = document.createElement("tr");
- summaryRow.className = "summary-row";
- summaryRow.innerHTML = `
- Totals |
- Week 1: ${week1.toFixed(2)} |
- Week 2: ${week2.toFixed(2)} |
- Fortnight: ${fortnight.toFixed(2)} |
- ${formatGap(gap)} |
- `;
- tbody.appendChild(summaryRow);
+ const dayRowsContainer = node.querySelector('.day-rows');
+ const dayRowTemplate = document.getElementById('day-row-template');
+
+ pattern.days.forEach((day, dayIndex) => {
+ const dayNode = dayRowTemplate.content.cloneNode(true);
+ const dayLabel = dayNode.querySelector('.day-label');
+ dayLabel.textContent = DAY_LABELS[dayIndex];
+
+ const startInput = dayNode.querySelector('.day-start');
+ const endInput = dayNode.querySelector('.day-end');
+ const breakInput = dayNode.querySelector('.day-break');
+ const hoursCell = dayNode.querySelector('.day-hours');
+
+ startInput.value = day.start;
+ endInput.value = day.end;
+ breakInput.value = day.breakMinutes;
+
+ const renderDaily = () => {
+ const result = metrics.dailyHours[dayIndex];
+ if (result.valid) {
+ hoursCell.textContent = `${result.hours.toFixed(2)} h`;
+ hoursCell.classList.remove('invalid');
+ } else {
+ hoursCell.textContent = result.message || 'Invalid';
+ hoursCell.classList.add('invalid');
+ }
+ };
- table.appendChild(tbody);
- tableWrapper.appendChild(table);
+ startInput.addEventListener('change', (event) => {
+ pattern.days[dayIndex].start = event.target.value;
+ saveAndRender();
+ });
- card.appendChild(header);
- const body = document.createElement("div");
- body.className = "panel-body";
- body.appendChild(tableWrapper);
- card.appendChild(body);
- container.appendChild(card);
- });
+ endInput.addEventListener('change', (event) => {
+ pattern.days[dayIndex].end = event.target.value;
+ saveAndRender();
+ });
- bindPatternEvents();
-}
-
-function formatGap(gap) {
- const cls = gap === 0 ? "gap-zero" : gap > 0 ? "gap-positive" : "gap-negative";
- const sign = gap > 0 ? "+" : "";
- return `${sign}${gap.toFixed(2)}`;
-}
+ breakInput.addEventListener('change', (event) => {
+ const value = Number(event.target.value);
+ pattern.days[dayIndex].breakMinutes = Number.isNaN(value) ? '' : value;
+ saveAndRender();
+ });
-function bindPatternEvents() {
- document.querySelectorAll("[data-role='pattern-name']").forEach((input) => {
- input.addEventListener("input", (e) => {
- const patternId = e.target.closest(".pattern-card").dataset.patternId;
- renamePattern(patternId, e.target.value);
+ renderDaily();
+ dayRowsContainer.appendChild(dayNode);
});
- });
- document.querySelectorAll("[data-role='duplicate']").forEach((btn) => {
- btn.addEventListener("click", (e) => {
- const patternId = e.target.closest(".pattern-card").dataset.patternId;
- duplicatePattern(patternId);
- });
- });
+ const summary = node.querySelector('.summary-values');
+ summary.innerHTML = `
+ Week 1: ${metrics.week1.toFixed(2)} hrs
+ Week 2: ${metrics.week2.toFixed(2)} hrs
+ Fortnight: ${metrics.fortnight.toFixed(2)} hrs
+ Gap: ${formatGap(metrics.gap)}
+ `;
- document.querySelectorAll("[data-role='delete']").forEach((btn) => {
- btn.addEventListener("click", (e) => {
- const patternId = e.target.closest(".pattern-card").dataset.patternId;
- deletePattern(patternId);
- });
- });
+ const statusText = node.querySelector('.status-text');
+ const badges = node.querySelector('.badges');
+ badges.innerHTML = '';
+
+ if (metrics.hasInvalid) {
+ statusText.textContent = 'Contains invalid days (excluded from totals).';
+ const badge = document.createElement('span');
+ badge.className = 'badge info';
+ badge.textContent = 'Fix entries to include them in totals';
+ badges.appendChild(badge);
+ } else {
+ statusText.textContent = 'All days valid and counted in totals.';
+ }
- document.querySelectorAll(".pattern-card input[type='time']").forEach((input) => {
- input.addEventListener("change", (e) => {
- const patternId = e.target.closest(".pattern-card").dataset.patternId;
- const dayIndex = Number(e.target.dataset.idx);
- const field = e.target.dataset.role;
- updateDayField(patternId, dayIndex, field, e.target.value);
- });
- });
+ const gapBadge = document.createElement('span');
+ gapBadge.classList.add('badge', classifyGap(metrics.gap));
+ gapBadge.textContent = `Gap: ${formatGap(metrics.gap)}`;
+ badges.appendChild(gapBadge);
- document.querySelectorAll(".pattern-card input[type='number']").forEach((input) => {
- input.addEventListener("change", (e) => {
- const patternId = e.target.closest(".pattern-card").dataset.patternId;
- const dayIndex = Number(e.target.dataset.idx);
- updateDayField(patternId, dayIndex, "breakMinutes", Number(e.target.value));
- });
+ card.dataset.patternId = pattern.id;
+ patternsContainer.appendChild(node);
});
}
function renderComparison() {
- const tbody = document.querySelector("#comparison-table tbody");
- tbody.innerHTML = "";
-
- const rows = state.patterns.map((p) => {
- const { week1, week2, fortnight, gap, hasInvalid } = calcPattern(p, state.settings);
- return { id: p.id, name: p.name, week1, week2, fortnight, gap, hasInvalid };
- });
+ comparisonBody.innerHTML = '';
+ const totals = state.patterns.map((pattern) => calculatePatternTotals(pattern, state.settings));
+ const bestIndex = totals.reduce((best, current, idx) => {
+ if (best === -1) return idx;
+ const currentGap = Math.abs(current.gap);
+ const bestGap = Math.abs(totals[best].gap);
+ return currentGap < bestGap ? idx : best;
+ }, totals.length ? 0 : -1);
+
+ state.patterns.forEach((pattern, idx) => {
+ const metrics = totals[idx];
+ const row = document.createElement('tr');
+ if (idx === bestIndex) {
+ row.classList.add('best-match');
+ }
- const validRows = rows.filter((r) => !r.hasInvalid);
- let highlightId = null;
- if (validRows.length) {
- highlightId = validRows.reduce((best, r) => {
- const bestAbs = Math.abs(rows.find((x) => x.id === best)?.gap ?? Infinity);
- const currentAbs = Math.abs(r.gap);
- return currentAbs < bestAbs ? r.id : best;
- }, validRows[0].id);
- }
+ const cells = [
+ pattern.name,
+ metrics.week1.toFixed(2),
+ metrics.week2.toFixed(2),
+ metrics.fortnight.toFixed(2),
+ formatGap(metrics.gap)
+ ];
+
+ cells.forEach((value, cellIndex) => {
+ const td = document.createElement('td');
+ td.textContent = value;
+ if (cellIndex === 4) {
+ td.classList.add(classifyGap(metrics.gap));
+ }
+ row.appendChild(td);
+ });
- rows.forEach((r) => {
- const tr = document.createElement("tr");
- if (r.id === highlightId) tr.classList.add("highlight");
- tr.innerHTML = `
- ${r.name} |
- ${r.hasInvalid ? "—" : r.week1.toFixed(2)} |
- ${r.hasInvalid ? "—" : r.week2.toFixed(2)} |
- ${r.hasInvalid ? "—" : r.fortnight.toFixed(2)} |
- ${r.hasInvalid ? 'Invalid' : formatGap(r.gap)} |
- `;
- tbody.appendChild(tr);
+ comparisonBody.appendChild(row);
});
}
-function showToast(message) {
- const toast = document.querySelector("#toast");
- toast.textContent = message;
- toast.classList.add("toast-visible");
- clearTimeout(showToast._timer);
- showToast._timer = setTimeout(() => toast.classList.remove("toast-visible"), 1800);
+function classifyGap(gap) {
+ if (gap === 0) return 'gap-zero';
+ return gap > 0 ? 'gap-positive' : 'gap-negative';
}
-function bindSettingsEvents() {
- document.querySelector("#hoursPerWeek").addEventListener("change", (e) => {
- updateSettings("hoursPerWeek", Number(e.target.value));
- });
- document.querySelector("#targetFortnightHours").addEventListener("change", (e) => {
- updateSettings("targetFortnightHours", Number(e.target.value));
- });
- document.querySelector("#defaultBreakMinutes").addEventListener("change", (e) => {
- updateSettings("defaultBreakMinutes", Number(e.target.value));
- });
+function formatGap(gap) {
+ if (gap === 0) return '0.00 hrs';
+ return `${gap > 0 ? '+' : ''}${gap.toFixed(2)} hrs`;
}
-function bindGlobalActions() {
- document.querySelector("#addPatternBtn").addEventListener("click", () => addPattern(true));
+function updateHeroStats() {
+ if (heroStats.weekly) {
+ heroStats.weekly.textContent = `${Number(state.settings.hoursPerWeek || 0).toFixed(2)} hrs`;
+ }
+ if (heroStats.target) {
+ heroStats.target.textContent = `${Number(state.settings.targetFortnightHours || 0).toFixed(2)} hrs`;
+ }
+ if (heroStats.break) {
+ heroStats.break.textContent = `${Number(state.settings.defaultBreak || 0)} min`;
+ }
+ if (heroStats.patterns) {
+ heroStats.patterns.textContent = state.patterns.length.toString();
+ }
}
-function render() {
+function saveAndRender() {
+ saveState();
renderSettings();
renderPatterns();
renderComparison();
}
-function runInlineChecks() {
- // 37.5h per week pattern totals
- const pattern = defaultPattern(defaultSettings());
- const res = calcPattern(pattern, defaultSettings());
- if (res.fortnight !== 75) {
- console.warn("Check failed: expected 75 hours fortnight, got", res.fortnight);
- }
+function bindSettingsListeners() {
+ settingsInputs.hoursPerWeek.addEventListener('change', (event) => {
+ const value = Number(event.target.value);
+ state.settings.hoursPerWeek = Number.isNaN(value) ? 0 : value;
+ if (state.settings.autoTarget) {
+ updateTargetFromWeekly();
+ }
+ saveAndRender();
+ });
- // Week 2 Friday off scenario
- const custom = defaultPattern(defaultSettings());
- custom.days[9] = { ...custom.days[9], start: "", end: "", breakMinutes: 60 };
- const res2 = calcPattern(custom, defaultSettings());
- if (res2.hasInvalid || res2.fortnight !== 67.5) {
- console.warn("Check: Week2 Friday off expected 67.5 hours, got", res2.fortnight);
- }
+ settingsInputs.targetFortnight.addEventListener('change', (event) => {
+ const value = Number(event.target.value);
+ state.settings.targetFortnightHours = Number.isNaN(value) ? 0 : value;
+ state.settings.autoTarget = false;
+ saveAndRender();
+ });
+
+ settingsInputs.defaultBreak.addEventListener('change', (event) => {
+ const value = Number(event.target.value);
+ state.settings.defaultBreak = Number.isNaN(value) ? 0 : value;
+ saveAndRender();
+ });
+
+ settingsInputs.resetTarget.addEventListener('click', () => {
+ updateTargetFromWeekly();
+ saveAndRender();
+ });
}
-function init() {
- bindSettingsEvents();
- bindGlobalActions();
- render();
- runInlineChecks();
+function bindGlobalActions() {
+ addPatternButton.addEventListener('click', () => {
+ const newIndex = state.patterns.length + 1;
+ state.patterns.push(createDefaultPattern(`Pattern ${newIndex}`));
+ saveAndRender();
+ });
}
-init();
+bindSettingsListeners();
+bindGlobalActions();
+saveAndRender();
diff --git a/working-hours-calculator/index.html b/working-hours-calculator/index.html
index 417fc3b..17a6cec 100644
--- a/working-hours-calculator/index.html
+++ b/working-hours-calculator/index.html
@@ -4,79 +4,161 @@
Working Hours Calculator
-
+
+
+
+
-