.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags static %}
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h2 class="title">All leave requests</h2>
|
||||
|
||||
<div class="box">
|
||||
<h3 class="subtitle">Global leave calendar</h3>
|
||||
{% include 'rota/partials/flatpickr_includes.html' %}
|
||||
<link rel="stylesheet" href="{% static 'css/calendar.css' %}">
|
||||
<div id="calendar" class="calendar" data-mode="inline"></div>
|
||||
<p class="help">View all leave requests across workers. Click and drag to select dates to open the leave form (you'll need to choose a worker in the modal).</p>
|
||||
{% if request.GET.rota_id %}
|
||||
<p class="help">Showing leaves for rota id {{ request.GET.rota_id }}.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div style="margin-top:0.5rem;">
|
||||
<strong>Legend:</strong>
|
||||
<span style="display:inline-block;width:12px;height:12px;background:#ffecec;border:1px solid #ddd;margin-left:0.5rem;vertical-align:middle"></span> Existing leave
|
||||
<span style="display:inline-block;width:12px;height:12px;background:#cdeffd;border:1px solid #2b8fd6;margin-left:0.75rem;vertical-align:middle"></span> Selected range
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="worker_leaves_json" type="application/json">{{ leaves_json|safe }}</script>
|
||||
<script src="{% static 'js/calendar.js' %}"></script>
|
||||
<script>
|
||||
(function(){
|
||||
const container = document.getElementById('calendar');
|
||||
if(!container) return;
|
||||
let leaves = [];
|
||||
try {
|
||||
const raw = document.getElementById('worker_leaves_json');
|
||||
if (raw) {
|
||||
leaves = JSON.parse(raw.textContent || raw.innerText || '[]');
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug('Unable to parse embedded leaves JSON', e);
|
||||
}
|
||||
// Provide a leavesUrl so the calendar will fetch the authoritative all-leaves list
|
||||
// Pass rota_id if present in the querystring so the server filters to rota workers.
|
||||
const rotaParam = new URLSearchParams(window.location.search).get('rota_id');
|
||||
let leavesUrl = '{% url "rota:leaves_global_json" %}';
|
||||
if(rotaParam) leavesUrl += '?rota_id=' + encodeURIComponent(rotaParam);
|
||||
window.initCalendar('calendar', { leaves: leaves, leavesUrl: leavesUrl, requestUrl: '{% url "rota:request_leave" %}', mode: container.dataset.mode || 'inline' });
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% if request.GET.rota_id %}
|
||||
<div class="box" style="margin-top:0.75rem;">
|
||||
<h4 class="subtitle is-6">Workers on this rota</h4>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:0.5rem;">
|
||||
<div id="rota-worker-legend">Loading legend…</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
const legend = document.getElementById('rota-worker-legend');
|
||||
const rotaParam = new URLSearchParams(window.location.search).get('rota_id');
|
||||
if(!rotaParam) return;
|
||||
fetch('{% url "rota:leaves_global_json" %}?rota_id=' + encodeURIComponent(rotaParam), {credentials:'same-origin'}).then(r=>r.json()).then(data=>{
|
||||
const byWorker = {};
|
||||
(data.leaves||[]).forEach(l=>{ if(l.worker_id){ byWorker[l.worker_id] = {name:l.reason, color:l.color}; } });
|
||||
if(Object.keys(byWorker).length===0){ legend.innerText = 'No workers with leaves in this rota.'; return; }
|
||||
legend.innerHTML = Object.entries(byWorker).map(([id,info])=> `<div style="display:inline-flex;align-items:center;gap:0.5rem;margin-right:0.5rem"><span style="width:14px;height:14px;background:${info.color};border:1px solid rgba(0,0,0,0.1);display:inline-block"></span><span>${info.name}</span></div>`).join('');
|
||||
}).catch(e=>{ legend.innerText = 'Unable to load legend'; console.debug(e); });
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -12,7 +12,10 @@
|
||||
<p>Period: {{ rota.start_date }} → {{ rota.end_date }}</p>
|
||||
|
||||
<div class="mb-4">
|
||||
<p><button class="button is-link" hx-get="{% url 'rota:worker_add' %}?rota_id={{ rota.id }}" hx-target="#modal" hx-swap="innerHTML">Add worker</button></p>
|
||||
<p>
|
||||
<button class="button is-link" hx-get="{% url 'rota:worker_add' %}?rota_id={{ rota.id }}" hx-target="#modal" hx-swap="innerHTML">Add worker</button>
|
||||
<a class="button is-info is-light is-small" href="{% url 'rota:leaves_global' %}?rota_id={{ rota.id }}" style="margin-left:0.5rem;">All leave requests</a>
|
||||
</p>
|
||||
{% include 'rota/partials/worker_list.html' %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ urlpatterns = [
|
||||
path("leave/request/", views.request_leave, name="request_leave"),
|
||||
path("leave/request/token/<uuid:token>/", views.request_leave, name="request_leave_token"),
|
||||
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/<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"),
|
||||
|
||||
@@ -1039,6 +1039,52 @@ def leaves_json(request, worker_id):
|
||||
return JsonResponse({'leaves': data})
|
||||
|
||||
|
||||
def leaves_global_json(request):
|
||||
"""Return JSON list of all leave ranges across all workers."""
|
||||
from .models import Leave
|
||||
# Optionally filter by rota_id (provided as query param) so the global
|
||||
# calendar can be scoped to a particular rota's workers.
|
||||
rota_id = request.GET.get('rota_id')
|
||||
qs = Leave.objects.select_related('worker')
|
||||
if rota_id:
|
||||
try:
|
||||
rota_id_i = int(rota_id)
|
||||
qs = qs.filter(worker__rotas=rota_id_i)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Build a worker->color map for workers present in the query so the
|
||||
# client can color-code leaves by worker.
|
||||
worker_colors = {}
|
||||
def color_for_worker(pk):
|
||||
# deterministic hue based on id
|
||||
h = (int(pk) * 37) % 360
|
||||
return f'hsl({h} 65% 85%)'
|
||||
|
||||
data = []
|
||||
for leave in qs.all().order_by('start_date'):
|
||||
worker = getattr(leave, 'worker', None)
|
||||
wid = getattr(worker, 'id', None)
|
||||
wname = getattr(worker, 'name', '') if worker else ''
|
||||
if wid not in worker_colors and wid is not None:
|
||||
worker_colors[wid] = color_for_worker(wid)
|
||||
data.append({
|
||||
'start': leave.start_date.isoformat(),
|
||||
'end': leave.end_date.isoformat(),
|
||||
'reason': wname or (leave.reason or ''),
|
||||
'worker_id': wid,
|
||||
'color': worker_colors.get(wid),
|
||||
})
|
||||
return JsonResponse({'leaves': data})
|
||||
|
||||
|
||||
def leaves_global(request):
|
||||
"""Page showing a global calendar with all leave requests."""
|
||||
# Provide an empty JSON payload initially; the calendar will fetch the authoritative list
|
||||
leaves_json = '[]'
|
||||
return render(request, 'rota/leaves_global.html', {'leaves_json': leaves_json})
|
||||
|
||||
|
||||
@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."""
|
||||
|
||||
Reference in New Issue
Block a user