fix(absences): team filter reads worker ID from <input> value, not data-attr

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 <input name='workers'> 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.
This commit is contained in:
Konrad du Plessis 2026-05-15 00:08:09 +02:00
parent 7d4d7b1f8b
commit 4368e53d95

View File

@ -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
// <input name="workers"> 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';
});
}