.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Apply leave to workers</p>
|
||||
<button class="delete" aria-label="close" onclick="(function(){ if(window.closeProcRotaModal) window.closeProcRotaModal(); })()"></button>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<p class="help">Select one or more workers to apply the selected leave range to.</p>
|
||||
<form id="apply-multi-form" method="post" action="{% url 'rota:apply_leave_multi' %}" hx-post="{% url 'rota:apply_leave_multi' %}" hx-swap="none">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="start_date" value="{{ start_date }}" />
|
||||
<input type="hidden" name="end_date" value="{{ end_date }}" />
|
||||
<div style="max-height:300px; overflow:auto; margin-top:0.5rem;">
|
||||
{% for w in workers %}
|
||||
<div class="field">
|
||||
<label class="checkbox"><input type="checkbox" name="worker_id" value="{{ w.id }}"> {{ w.name }}{% if w.site %} — {{ w.site }}{% endif %}</label>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="help">No workers available.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button id="apply-multi-submit" class="button is-success" type="submit" disabled>Apply leave</button>
|
||||
<button class="button" type="button" onclick="(function(){ if(window.closeProcRotaModal) window.closeProcRotaModal(); })()">Cancel</button>
|
||||
</footer>
|
||||
<script>
|
||||
(function(){
|
||||
const form = document.getElementById('apply-multi-form');
|
||||
const submitBtn = document.getElementById('apply-multi-submit');
|
||||
if(!form) return;
|
||||
|
||||
function updateBtn(){
|
||||
const any = Array.from(form.querySelectorAll('input[type=checkbox][name="worker_id"]')).some(cb=>cb.checked);
|
||||
submitBtn.disabled = !any;
|
||||
}
|
||||
|
||||
// Attach change handlers to checkboxes
|
||||
Array.from(form.querySelectorAll('input[type=checkbox][name="worker_id"]')).forEach(cb=> cb.addEventListener('change', updateBtn));
|
||||
updateBtn();
|
||||
|
||||
// Make the external Apply button trigger the form submit (button is outside the <form>)
|
||||
submitBtn.addEventListener('click', function(ev){
|
||||
ev.preventDefault();
|
||||
if(typeof form.requestSubmit === 'function'){
|
||||
form.requestSubmit();
|
||||
} else {
|
||||
// older browsers
|
||||
const submitEvt = new Event('submit', {bubbles: true, cancelable: true});
|
||||
form.dispatchEvent(submitEvt);
|
||||
}
|
||||
});
|
||||
|
||||
// Fallback submit handler using fetch so the modal doesn't navigate away
|
||||
form.addEventListener('submit', function(ev){
|
||||
ev.preventDefault();
|
||||
// collect form data
|
||||
const fd = new FormData(form);
|
||||
const url = form.action;
|
||||
// fetch with CSRF from cookie if present
|
||||
function getCookie(name){
|
||||
const v = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)');
|
||||
return v ? v.pop() : '';
|
||||
}
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken'),
|
||||
'HX-Request': 'true'
|
||||
},
|
||||
body: fd
|
||||
}).then(resp=>{
|
||||
// Respect HX-Trigger header if present
|
||||
const trig = resp.headers.get('HX-Trigger') || resp.headers.get('Hx-Trigger');
|
||||
if(trig){
|
||||
const parts = trig.split(',').map(s=>s.trim()).filter(Boolean);
|
||||
if(parts.includes('closeModal')){
|
||||
if(window.closeProcRotaModal) window.closeProcRotaModal();
|
||||
}
|
||||
if(parts.includes('leaveUpdated')){
|
||||
// dispatch a custom event that other code listens to
|
||||
try{ window.dispatchEvent(new Event('leaveUpdated')); }catch(e){}
|
||||
}
|
||||
}
|
||||
if(!resp.ok){
|
||||
resp.text().then(t=>{ alert('Failed to apply leave: ' + (t||resp.statusText)); });
|
||||
}
|
||||
}).catch(err=>{ console.error('apply-multi submit failed', err); alert('Failed to apply leave'); });
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
@@ -22,6 +22,8 @@ urlpatterns = [
|
||||
path("worker/settings/token/<uuid:token>/", views.worker_settings_token, name="worker_settings_token"),
|
||||
path("leaves/all/", views.leaves_global, name="leaves_global"),
|
||||
path("leaves/all.json", views.leaves_global_json, name="leaves_global_json"),
|
||||
path("leave/select_workers/", views.select_workers_modal, name="select_workers_modal"),
|
||||
path("leave/apply_multi/", views.apply_leave_multi, name="apply_leave_multi"),
|
||||
path("leave/<int:leave_id>/delete/", views.leave_delete, name="leave_delete"),
|
||||
path("worker/<int:worker_id>/leaves/delete_all/", views.leave_delete_all, name="leave_delete_all"),
|
||||
path("worker/<int:worker_id>/regenerate_token/", views.regenerate_worker_token, name="regenerate_worker_token"),
|
||||
|
||||
@@ -1085,6 +1085,59 @@ def leaves_global(request):
|
||||
return render(request, 'rota/leaves_global.html', {'leaves_json': leaves_json})
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
def select_workers_modal(request):
|
||||
"""Return an HTMX modal partial that lists workers to apply a selected leave range to.
|
||||
|
||||
Query params: start_date, end_date, rota_id (optional)
|
||||
"""
|
||||
start_date = request.GET.get('start_date')
|
||||
end_date = request.GET.get('end_date')
|
||||
rota_id = request.GET.get('rota_id')
|
||||
workers = Worker.objects.all()
|
||||
if rota_id:
|
||||
try:
|
||||
workers = workers.filter(rotas__id=int(rota_id))
|
||||
except Exception:
|
||||
pass
|
||||
workers = workers.order_by('name')
|
||||
modal_html = render_to_string(
|
||||
'rota/partials/select_workers_modal.html',
|
||||
{'workers': workers, 'start_date': start_date, 'end_date': end_date},
|
||||
request=request,
|
||||
)
|
||||
return HttpResponse(modal_html)
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
def apply_leave_multi(request):
|
||||
"""Apply a leave range to multiple workers. Expects POST with worker_ids (list), start_date and end_date."""
|
||||
from .models import Leave
|
||||
|
||||
worker_ids = request.POST.getlist('worker_id')
|
||||
start_date = request.POST.get('start_date')
|
||||
end_date = request.POST.get('end_date')
|
||||
if not worker_ids or not start_date:
|
||||
return HttpResponseBadRequest('Missing parameters')
|
||||
created = []
|
||||
for wid in worker_ids:
|
||||
try:
|
||||
w = Worker.objects.get(pk=int(wid))
|
||||
except Exception:
|
||||
continue
|
||||
try:
|
||||
leave = Leave(worker=w, start_date=start_date, end_date=end_date)
|
||||
leave.save()
|
||||
created.append(leave)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Return simple HTMX response: close modal and trigger calendar refresh
|
||||
response = HttpResponse('')
|
||||
response['HX-Trigger'] = 'closeModal,leaveUpdated'
|
||||
return response
|
||||
|
||||
|
||||
@require_POST
|
||||
def leave_delete(request, leave_id):
|
||||
"""Delete a Leave record. Allowed if the requester is staff, or if a valid token matches the worker, or the authenticated user's email matches the worker email."""
|
||||
|
||||
@@ -168,10 +168,22 @@
|
||||
previewEnd = null;
|
||||
render();
|
||||
if(selStart && selEnd){
|
||||
// If a specific workerId is supplied, use existing single-worker modal flow.
|
||||
if(workerId){
|
||||
const url = requestUrl + '?worker_id=' + encodeURIComponent(workerId) + '&start_date=' + selStart + '&end_date=' + selEnd;
|
||||
setTimeout(function(){
|
||||
try{ if(window.htmx && typeof window.htmx.ajax === 'function'){ window.htmx.ajax('GET', url, {target:'#modal', swap:'innerHTML'}); } else { htmx.ajax('GET', url, {target:'#modal', swap:'innerHTML'}); } }catch(e){ window.location = url; }
|
||||
}, 80);
|
||||
} else {
|
||||
// Global calendar: prompt for workers to apply this leave to.
|
||||
// Use an HTMX GET to render the select-workers modal.
|
||||
let url = '/leave/select_workers/?start_date=' + encodeURIComponent(selStart) + '&end_date=' + encodeURIComponent(selEnd);
|
||||
// Include rota_id if present in the page URL
|
||||
try{ const rotaParam = new URLSearchParams(window.location.search).get('rota_id'); if(rotaParam){ url += '&rota_id=' + encodeURIComponent(rotaParam); } }catch(e){}
|
||||
setTimeout(function(){
|
||||
try{ if(window.htmx && typeof window.htmx.ajax === 'function'){ window.htmx.ajax('GET', url, {target:'#modal', swap:'innerHTML'}); } else { htmx.ajax('GET', url, {target:'#modal', swap:'innerHTML'}); } }catch(e){ window.location = url; }
|
||||
}, 80);
|
||||
}
|
||||
}
|
||||
}catch(e){ console.error('calendar pointer handler', e); }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user