diff --git a/djangorota/djangorota/templates/rota/leaves_global.html b/djangorota/djangorota/templates/rota/leaves_global.html
new file mode 100644
index 0000000..f9ab44b
--- /dev/null
+++ b/djangorota/djangorota/templates/rota/leaves_global.html
@@ -0,0 +1,70 @@
+{% extends 'base.html' %}
+{% load crispy_forms_tags static %}
+{% block content %}
+
+
All leave requests
+
+
+
Global leave calendar
+ {% include 'rota/partials/flatpickr_includes.html' %}
+
+
+
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).
+ {% if request.GET.rota_id %}
+
Showing leaves for rota id {{ request.GET.rota_id }}.
+ {% endif %}
+
+
+
+ Legend:
+ Existing leave
+ Selected range
+
+
+
+
+
+
+
+ {% if request.GET.rota_id %}
+
+
Workers on this rota
+
+
+
+ {% endif %}
+{% endblock %}
diff --git a/djangorota/djangorota/templates/rota/rota_detail.html b/djangorota/djangorota/templates/rota/rota_detail.html
index beed00a..e13454c 100644
--- a/djangorota/djangorota/templates/rota/rota_detail.html
+++ b/djangorota/djangorota/templates/rota/rota_detail.html
@@ -12,7 +12,10 @@
Period: {{ rota.start_date }} → {{ rota.end_date }}
-
+
+
+ All leave requests
+
{% include 'rota/partials/worker_list.html' %}
diff --git a/djangorota/rota/urls.py b/djangorota/rota/urls.py
index e6fec72..0797bb8 100644
--- a/djangorota/rota/urls.py
+++ b/djangorota/rota/urls.py
@@ -20,6 +20,8 @@ urlpatterns = [
path("leave/request/", views.request_leave, name="request_leave"),
path("leave/request/token//", views.request_leave, name="request_leave_token"),
path("worker/settings/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//delete/", views.leave_delete, name="leave_delete"),
path("worker//leaves/delete_all/", views.leave_delete_all, name="leave_delete_all"),
path("worker//regenerate_token/", views.regenerate_worker_token, name="regenerate_worker_token"),
diff --git a/djangorota/rota/views.py b/djangorota/rota/views.py
index b9eb57f..defb184 100644
--- a/djangorota/rota/views.py
+++ b/djangorota/rota/views.py
@@ -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."""
diff --git a/static/js/calendar.js b/static/js/calendar.js
index 7651ec4..45fc27f 100644
--- a/static/js/calendar.js
+++ b/static/js/calendar.js
@@ -29,7 +29,11 @@
let viewMonth = new Date().getMonth();
let selStart = null; let selEnd = null; let previewEnd = null;
- function inLeaves(isoStr){ for(const L of leaves){ if(!L.start) continue; if(L.start<=isoStr && isoStr<=L.end) return true } return false }
+ function inLeaves(isoStr){
+ const matches = [];
+ for(const L of leaves){ if(!L.start) continue; if(L.start<=isoStr && isoStr<=L.end) matches.push(L); }
+ return matches;
+ }
function buildControls(){
let controls = document.getElementById('calendar-controls');
@@ -84,7 +88,31 @@
else{
const isoStr = iso(viewYear, viewMonth, cur);
dayDiv.textContent = cur;
- if(inLeaves(isoStr)) dayDiv.classList.add('leave');
+ const matches = inLeaves(isoStr);
+ if(matches && matches.length){
+ dayDiv.classList.add('leave');
+ try{
+ // collect distinct colors for this day's matches
+ const colors = Array.from(new Set(matches.map(x=>x.color).filter(Boolean)));
+ if(colors.length===1){
+ dayDiv.style.backgroundImage = ''; // clear any previous gradient
+ dayDiv.style.backgroundColor = colors[0];
+ } else if(colors.length>1){
+ // build an equal-split horizontal gradient (cap to 6 colours)
+ const n = Math.min(colors.length, 6);
+ const seg = 100 / n;
+ const stops = colors.slice(0,n).map((c,i)=>{
+ const start = Math.round(i*seg);
+ const end = Math.round((i+1)*seg);
+ return `${c} ${start}% ${end}%`;
+ }).join(', ');
+ dayDiv.style.backgroundColor = '';
+ dayDiv.style.backgroundImage = `linear-gradient(90deg, ${stops})`;
+ }
+ }catch(e){ /* defensive: leave default styling */ }
+ // set tooltip to show reasons / worker names for overlapping leaves
+ try{ dayDiv.title = matches.map(x => x.reason || '').filter(Boolean).join(', '); }catch(e){}
+ }
const rangeEnd = selEnd || previewEnd;
if(selStart && rangeEnd){ if(selStart<=isoStr && isoStr<=rangeEnd) dayDiv.classList.add('in-range'); }
else if(selStart && selStart===isoStr) dayDiv.classList.add('selected');
@@ -113,7 +141,7 @@
if(res.ok){
const data = await res.json();
if(data && Array.isArray(data.leaves)){
- setLeaves(data.leaves.map(l=>({start:l.start, end:l.end, reason:l.reason}))); return;
+ setLeaves(data.leaves.map(l=>({start:l.start, end:l.end, reason:l.reason, color:l.color, worker_id:l.worker_id}))); return;
}
}
}catch(e){ console.error('refreshLeaves failed', e); }