From 4368e53d953d3bc3b7c8d8bbceb7d317c51d3c54 Mon Sep 17 00:00:00 2001 From: Konrad du Plessis Date: Fri, 15 May 2026 00:08:09 +0200 Subject: [PATCH] fix(absences): team filter reads worker ID from value, not data-attr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Konrad reported that selecting a team on /absences/log/ hid ALL workers, not just non-team. Root cause: the JS read row.dataset.workerId to filter, which depends on how Django renders choice_value for ModelMultipleChoiceField iteration — not reliable. Switched to read the actual value attribute, matching the attendance_log's proven pattern. Same UX intent (hide non-team workers); more robust implementation. Also uses an O(1) object lookup instead of array.indexOf, and adds defensive fallback for both string and numeric team-id keys. --- core/templates/core/absences/log.html | 44 +++++++++++++++++++++------ 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/core/templates/core/absences/log.html b/core/templates/core/absences/log.html index d4931eb..94708d2 100644 --- a/core/templates/core/absences/log.html +++ b/core/templates/core/absences/log.html @@ -156,6 +156,17 @@ them from the WorkLog). // re-runs the combined visibility pass via applyWorkerFilters(). // // Empty selection in either filter = "no restriction" for that filter. +// +// IMPLEMENTATION NOTE: we read the worker's id from the inner +// value attribute, NOT from data-worker-id on +// the row div. This matches the attendance form's proven pattern and +// avoids brittleness around how Django renders choice_value for +// ModelMultipleChoiceField iteration. +// +// REGRESSION: this used to read data-worker-id, which silently hid +// all workers in some renders. Reading from input[name="workers"] +// value is the same pattern attendance_log.html uses and is proven. +// See 7d4d7b1+ Konrad's bug report on production. (function() { var searchInput = document.getElementById('workerSearch'); var teamSelect = document.querySelector('[name="team"]'); @@ -166,18 +177,33 @@ them from the WorkLog). function applyWorkerFilters() { var q = searchInput ? searchInput.value.toLowerCase() : ''; var teamId = teamSelect ? teamSelect.value : ''; - // Workers allowed by the team filter — null means "no team filter". - var allowedWorkerIds = null; - if (teamId && teamWorkersMap[teamId]) { - // Stringify so we can compare against data-worker-id (which is a string). - allowedWorkerIds = teamWorkersMap[teamId].map(function(id) { return String(id); }); + + // Build allowed set as STRINGS (matching DOM input.value strings). + // null means "no team filter applied" (show everyone). + var allowedSet = null; + if (teamId) { + // Defensive fallback: object keys are always strings in JS, but + // be explicit about handling both raw and stringified keys just + // in case the JSON payload ever changes shape. + var teamWorkers = teamWorkersMap[teamId] || teamWorkersMap[String(teamId)]; + if (teamWorkers) { + allowedSet = {}; + teamWorkers.forEach(function(id) { allowedSet[String(id)] = true; }); + } else { + // Team key not in map — show no one (defensive) + allowedSet = {}; + } } + document.querySelectorAll('.worker-row').forEach(function(row) { var name = row.dataset.name || ''; - var wid = row.dataset.workerId || ''; - // Visible only if BOTH conditions pass. - var matchesSearch = name.indexOf(q) > -1; - var matchesTeam = (allowedWorkerIds === null) || (allowedWorkerIds.indexOf(wid) > -1); + // Read worker id from the actual checkbox input — proven reliable. + var input = row.querySelector('input[name="workers"]'); + var wid = input ? String(input.value) : ''; + + var matchesSearch = q === '' || name.indexOf(q) > -1; + var matchesTeam = (allowedSet === null) || (allowedSet[wid] === true); + row.style.display = (matchesSearch && matchesTeam) ? '' : 'none'; }); }