Add worker overview and leave management features with modals for enhanced usability
This commit is contained in:
@@ -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">— {{ 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>
|
||||
@@ -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"),
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user