fix(absences): dropdown z-index + clearer Confirm Absences copy

- /absences/ Reasons multi-checkbox dropdown: z-index 1050 so it
  renders above the table rows (was hiding the bottom 4 options).
- /absences/log/confirm/: action-oriented copy, pre-checked
  'remove from work log' (the common case), explicit Cancel button.
  Was confusing: 'Also remove from WorkLog' didn't read as the
  natural fix for the conflict. New language explains both branches
  in plain English. +1 regression test for the new copy.
This commit is contained in:
Konrad du Plessis 2026-05-14 23:32:45 +02:00
parent a6cf766394
commit 27fe05e3b6
3 changed files with 45 additions and 25 deletions

View File

@ -78,7 +78,7 @@ has an inline delete form. CSV export button only shows for admin.
<button class="btn btn-outline-secondary btn-sm dropdown-toggle form-select-sm" type="button" data-bs-toggle="dropdown" style="min-width: 140px; text-align: left;"> <button class="btn btn-outline-secondary btn-sm dropdown-toggle form-select-sm" type="button" data-bs-toggle="dropdown" style="min-width: 140px; text-align: left;">
{% if filter_reasons %}{{ filter_reasons|length }} selected{% else %}All{% endif %} {% if filter_reasons %}{{ filter_reasons|length }} selected{% else %}All{% endif %}
</button> </button>
<ul class="dropdown-menu p-2" style="min-width: 220px;" onclick="event.stopPropagation();"> <ul class="dropdown-menu p-2" style="min-width: 220px; z-index: 1050;" onclick="event.stopPropagation();">
{% for key, label in reason_choices %} {% for key, label in reason_choices %}
<li> <li>
<label class="form-check d-block mb-0"> <label class="form-check d-block mb-0">

View File

@ -7,51 +7,58 @@
{% comment %} {% comment %}
Conflict-confirmation page. Shown when at least one (worker, date) Conflict-confirmation page. Shown when at least one (worker, date)
pair on the absence-log form already has a WorkLog. The admin can pair on the absence-log form already has a WorkLog. The admin can
tick "Also remove from WorkLog" per conflict before committing — keep or untick the pre-checked "Remove from work log" box per
useful when correcting an earlier mistake (worker was clocked in conflict — the common case (clocked-in but actually absent) is
but in fact was absent that day). Untouched rows keep their fixed by removing them from the work log AND creating the absence,
WorkLog intact, so partial-day cases work too. so we pre-tick the box. Unticking keeps the WorkLog and SKIPS the
absence creation contradiction by still creating it (untouched
rows keep their WorkLog intact, so partial-day cases work too).
{% endcomment %} {% endcomment %}
<div class="container py-4"> <div class="container py-4">
<h1 class="page-title mb-3"> <h1 class="page-title mb-3">
<i class="fas fa-exclamation-triangle me-2" style="color: var(--accent);"></i> <i class="fas fa-triangle-exclamation me-2" style="color: var(--accent);"></i>
Confirm Absences Confirm Absences
</h1> </h1>
<div class="alert alert-warning"> <div class="alert alert-warning">
<strong>{{ conflicts|length }} worker(s) already have work logs on these dates.</strong><br> <strong>{{ conflicts|length }} worker(s) were already logged as <em>working</em> on these dates.</strong>
Tick the boxes below to also remove them from those work logs (recommended if you're correcting a mistake). <p class="mb-0 mt-2">
You can't be both <em>working</em> and <em>absent</em> on the same day — pick which is correct below.
The usual fix is to remove them from the work log (their attendance was recorded by mistake), so we've
pre-selected that for you. Untick the box if you want to keep the work log entry instead (their absence
won't be recorded in that case — you'll have to delete the absence manually after this saves).
</p>
</div> </div>
<form method="post" class="card"> <form method="post" class="card">
{% csrf_token %} {% csrf_token %}
<div class="card-body"> <div class="card-body">
<h6 class="text-uppercase mb-3" style="font-size: 0.75rem; color: var(--text-secondary);"> <h6 class="text-uppercase mb-3" style="font-size: 0.75rem; color: var(--text-secondary);">
Conflicts Worker &times; Date
</h6> </h6>
<table class="table table-sm"> <table class="table table-sm align-middle">
<thead> <thead>
<tr> <tr>
<th>Worker</th> <th>Worker</th>
<th>Date</th> <th>Date</th>
<th>WorkLog</th> <th>Existing Work Log</th>
<th>Action</th> <th style="min-width: 240px;">What should happen?</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for c in conflicts %} {% for c in conflicts %}
<tr> <tr>
<td>{{ c.worker_name }}</td> <td><strong>{{ c.worker_name }}</strong></td>
<td>{{ c.date }}</td> <td>{{ c.date }}</td>
<td>{{ c.project_name }} (WorkLog #{{ c.work_log_id }})</td> <td><span class="text-muted">{{ c.project_name }}</span> (Log #{{ c.work_log_id }})</td>
<td> <td>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" <input class="form-check-input" type="checkbox"
name="remove_from_worklog_{{ c.work_log_id }}_{{ c.worker_id }}" name="remove_from_worklog_{{ c.work_log_id }}_{{ c.worker_id }}"
id="remove_{{ forloop.counter }}"> id="remove_{{ forloop.counter }}" checked>
<label class="form-check-label" for="remove_{{ forloop.counter }}"> <label class="form-check-label" for="remove_{{ forloop.counter }}">
Also remove from WorkLog Remove from work log <span class="text-muted">(recommended &mdash; they were absent, not working)</span>
</label> </label>
</div> </div>
</td> </td>
@ -61,18 +68,18 @@ WorkLog intact, so partial-day cases work too.
</table> </table>
<hr> <hr>
<p class="mb-2"><strong>{{ absence_count }} absence(s) will be created:</strong></p> <div class="alert alert-secondary mb-0">
<p> <strong>{{ absence_count }} absence record(s) will be created.</strong>
Reason: <strong>{{ reason }}</strong>. Reason: <strong>{{ reason }}</strong>.
Paid: <strong>{% if is_paid %}Yes{% else %}No{% endif %}</strong>. Paid: <strong>{% if is_paid %}Yes (Bonus payroll adjustment){% else %}No{% endif %}</strong>.
{# Project line is conditional — older flows / non-project absences leave it blank. #} </div>
{% if project_name %}Project: <strong>{{ project_name }}</strong>.{% endif %}
</p>
<div class="d-flex justify-content-end gap-2 mt-3"> <div class="d-flex justify-content-between gap-2 mt-3">
<a href="{% url 'absence_log' %}" class="btn btn-outline-secondary">&larr; Back to form</a> <a href="{% url 'absence_log' %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-1"></i> Cancel
</a>
<button type="submit" class="btn btn-accent"> <button type="submit" class="btn btn-accent">
<i class="fas fa-check me-1"></i> Confirm &amp; Create Absences <i class="fas fa-check me-1"></i> Save Absences
</button> </button>
</div> </div>
</div> </div>

View File

@ -2241,6 +2241,19 @@ class AbsenceConfirmViewTests(TestCase):
self.assertEqual(Absence.objects.count(), 1) # Still creates absence self.assertEqual(Absence.objects.count(), 1) # Still creates absence
self.assertEqual(resp.status_code, 302) # No 500 self.assertEqual(resp.status_code, 302) # No 500
def test_confirm_page_renders_new_copy(self):
"""Round D-followup — confirm page uses action-oriented copy + Cancel button."""
resp = self.client.get('/absences/log/confirm/')
self.assertEqual(resp.status_code, 200)
# New banner copy
self.assertContains(resp, "were already logged")
# Cancel button (was 'Back to form')
self.assertContains(resp, "Cancel")
# Save button (was 'Confirm &amp; Create Absences')
self.assertContains(resp, "Save Absences")
# Recommended hint on the checkbox
self.assertContains(resp, "recommended")
# === ABSENCE LIST / EDIT / DELETE / EXPORT VIEW TESTS ============================ # === ABSENCE LIST / EDIT / DELETE / EXPORT VIEW TESTS ============================
# Covers Task 5 of the Worker Absences feature: browsing absences via /absences/ # Covers Task 5 of the Worker Absences feature: browsing absences via /absences/