Add worker overview and leave management features with modals for enhanced usability

This commit is contained in:
Ross
2025-12-17 16:26:11 +00:00
parent 86201ec1d7
commit 105b37feae
6 changed files with 153 additions and 4 deletions
@@ -0,0 +1,22 @@
{% load crispy_forms_tags %}
<div class="modal is-active" id="leave-modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Add leave for {{ worker.name }}</p>
<button class="delete" aria-label="close" onclick="document.getElementById('modal').innerHTML='' "></button>
</header>
<section class="modal-card-body">
<form method="post" hx-post="{{ form_action }}" hx-target="#modal" hx-swap="innerHTML">
{% csrf_token %}
{{ form|crispy }}
<div class="field" style="margin-top:0.75rem;">
<div class="control">
<button class="button is-primary" type="submit">Save</button>
<button type="button" class="button" onclick="document.getElementById('modal').innerHTML=''">Cancel</button>
</div>
</div>
</form>
</section>
</div>
</div>
@@ -20,6 +20,19 @@
{% include 'rota/partials/worker_token_area.html' with worker=worker token_link=token_link %}
{% endif %}
{% if worker %}
<div class="box" id="worker-leaves">
<h4 class="title is-6">Leaves</h4>
<div>
{% include 'rota/partials/leave_list.html' with leaves=worker.leaves.all token=None %}
</div>
<div style="margin-top:0.5rem; display:flex; gap:0.5rem;">
<a class="button is-small" href="{% url 'rota:worker_detail' worker.id %}">Open full page</a>
<button class="button is-small" hx-get="{% url 'rota:worker_leave_add' worker.id %}" hx-target="#modal" hx-swap="innerHTML">Add leave</button>
</div>
</div>
{% endif %}
<!-- Structured Non-working days editor (keeps #id_nwds in sync) -->
<div class="box" id="nwds-editor">
<h4 class="title is-6">Non-working days</h4>
@@ -6,7 +6,7 @@
<li>
<div style="display:flex; align-items:center; justify-content:space-between; gap:0.75rem;">
<div style="display:flex; align-items:center; gap:0.5rem;">
<a href="{% url 'rota:worker_detail' w.id %}">{{ w.name }}</a>
<a href="javascript:void(0)" hx-get="{% url 'rota:worker_overview' rota.id w.id %}" hx-target="#modal" hx-swap="innerHTML">{{ w.name }}</a>
<small style="color:#666">&mdash; {{ w.site }}{% if w.grade %} · grade {{ w.grade }}{% endif %} · FTE {{ w.fte }}</small>
{# show non-working days stored in options.nwds if present; render JSON safely and let JS format it #}
{% if w.options.nwds %}
@@ -38,9 +38,10 @@
</div>
{% endif %}
</div>
<div style="flex:0 0 auto;">
<button class="button is-small is-light ml-2" hx-get="{% url 'rota:worker_edit' rota.id w.id %}" hx-target="#modal" hx-swap="innerHTML">Edit</button>
<button class="button is-small is-danger ml-2" hx-get="{% url 'rota:worker_delete' rota.id w.id %}" hx-target="#modal" hx-swap="innerHTML">Delete</button>
<div style="flex:0 0 auto; display:flex; gap:0.5rem;">
<button class="button is-small is-light" hx-get="{% url 'rota:worker_edit' rota.id w.id %}" hx-target="#modal" hx-swap="innerHTML">Edit</button>
<a class="button is-small" href="{% url 'rota:worker_detail' w.id %}">Leave</a>
<button class="button is-small is-danger" hx-get="{% url 'rota:worker_delete' rota.id w.id %}" hx-target="#modal" hx-swap="innerHTML">Delete</button>
</div>
</div>
</li>
@@ -0,0 +1,58 @@
{% load crispy_forms_tags %}
<div class="modal is-active" id="worker-overview-modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Worker: {{ worker.name }}</p>
<button class="delete" aria-label="close" onclick="document.getElementById('modal').innerHTML='' "></button>
</header>
<section class="modal-card-body">
<div class="content">
<h4 class="title is-6">Basic info</h4>
<p><strong>Site:</strong> {{ worker.site }}</p>
<p><strong>Grade:</strong> {{ worker.grade }}</p>
<p><strong>FTE:</strong> {{ worker.fte }}</p>
<p><strong>Email:</strong> {{ worker.email }}</p>
<h4 class="title is-6">Leaves</h4>
<div>
{% include 'rota/partials/leave_list.html' with leaves=worker.leaves.all token=None %}
</div>
<h4 class="title is-6">Configured constraints & options</h4>
<p class="help">Below are the worker's configured options. Values are shown read-only.</p>
<div class="box" style="white-space:pre-wrap; font-family:monospace; font-size:0.9rem;">
{{ pretty_options }}
</div>
<h4 class="title is-6">Quick summary</h4>
<div class="tags">
{% for it in worker.options.hard_day_exclusions %}
<span class="tag is-light" title="{{ it|escape }}">HDE</span>
{% empty %}{% endfor %}
{% for it in worker.options.hard_day_dependencies %}
<span class="tag is-light" title="{{ it|escape }}">HDD</span>
{% empty %}{% endfor %}
{% for it in worker.options.forced_assignments %}
<span class="tag is-light" title="{{ it|escape }}">FA</span>
{% empty %}{% endfor %}
{% for it in worker.options.forced_assignments_by_date %}
<span class="tag is-light" title="{{ it|escape }}">FAD</span>
{% empty %}{% endfor %}
{% for it in worker.options.allowed_multi_shift_sets %}
<span class="tag is-light" title="{{ it|escape }}">AMS</span>
{% empty %}{% endfor %}
</div>
</div>
<div style="margin-top:1rem; display:flex; gap:0.5rem;">
<button class="button is-small is-light" hx-get="{% url 'rota:worker_edit' rota.id worker.id %}" hx-target="#modal" hx-swap="innerHTML">Edit</button>
<a class="button is-small" href="{% url 'rota:worker_detail' worker.id %}">Open full page</a>
<button type="button" class="button" onclick="document.getElementById('modal').innerHTML=''">Close</button>
</div>
</section>
</div>
</div>
+2
View File
@@ -14,7 +14,9 @@ urlpatterns = [
path("rota/<int:rota_id>/edit/", views.rota_edit, name="rota_edit"),
path("worker/add/", views.worker_create, name="worker_add"),
path("worker/<int:rota_id>/<int:worker_id>/edit/", views.worker_edit, name="worker_edit"),
path("worker/<int:rota_id>/<int:worker_id>/overview/", views.worker_overview, name="worker_overview"),
path("worker/<int:rota_id>/<int:worker_id>/delete/", views.worker_delete, name="worker_delete"),
path("worker/<int:worker_id>/leave/add/", views.worker_leave_add, name="worker_leave_add"),
path("worker/<int:worker_id>/", views.worker_detail, name="worker_detail"),
path("worker/<int:worker_id>/leaves.json", views.leaves_json, name="worker_leaves_json"),
path("leave/request/", views.request_leave, name="request_leave"),
+53
View File
@@ -14,6 +14,7 @@ from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.shortcuts import HttpResponseRedirect
import traceback
def _get_constraint_description(key: str):
@@ -345,6 +346,58 @@ def worker_detail(request, worker_id):
return render(request, "rota/worker_detail.html", {"worker": worker, "form": form, "leaves": leaves, "leaves_json": leaves_json})
@require_http_methods(["GET", "POST"])
def worker_leave_add(request, worker_id):
"""HTMX endpoint: show a leave form modal for a worker, or create the leave on POST."""
worker = get_object_or_404(Worker, pk=worker_id)
is_hx = request.headers.get("HX-Request", "false").lower() == "true"
if request.method == "POST":
form = LeaveForm(request.POST)
if form.is_valid():
leave = form.save(commit=False)
leave.worker = worker
leave.save()
if is_hx:
# Close modal and optionally trigger an update; leave list can be refreshed separately
response = HttpResponse("")
response["HX-Trigger"] = "closeModal"
return response
return redirect("rota:worker_detail", worker_id=worker.id)
# invalid: return modal with errors
if is_hx:
modal_html = render_to_string("rota/partials/leave_form_modal.html", {"form": form, "worker": worker, "form_action": request.path}, request=request)
return HttpResponse(modal_html)
# non-HTMX fallthrough
# GET: render modal partial
form = LeaveForm()
if is_hx:
modal_html = render_to_string("rota/partials/leave_form_modal.html", {"form": form, "worker": worker, "form_action": request.path}, request=request)
return HttpResponse(modal_html)
return redirect("rota:worker_detail", worker_id=worker.id)
@require_http_methods(["GET"])
def worker_overview(request, rota_id, worker_id):
"""Return a read-only modal overview of the worker (HTMX GET)."""
rota = get_object_or_404(RotaSchedule, pk=rota_id)
worker = get_object_or_404(Worker, pk=worker_id)
try:
pretty_options = json.dumps(worker.options or {}, indent=2, default=str)
except Exception:
pretty_options = str(worker.options or {})
modal_html = render_to_string(
"rota/partials/worker_overview_modal.html",
{"worker": worker, "rota": rota, "pretty_options": pretty_options},
request=request,
)
return HttpResponse(modal_html)
@require_http_methods(["GET", "POST"])
def worker_edit(request, rota_id, worker_id):
rota = get_object_or_404(RotaSchedule, pk=rota_id)