.
This commit is contained in:
@@ -8,11 +8,19 @@
|
|||||||
<h3 class="subtitle">Global leave calendar</h3>
|
<h3 class="subtitle">Global leave calendar</h3>
|
||||||
{% include 'rota/partials/flatpickr_includes.html' %}
|
{% include 'rota/partials/flatpickr_includes.html' %}
|
||||||
<link rel="stylesheet" href="{% static 'css/calendar.css' %}">
|
<link rel="stylesheet" href="{% static 'css/calendar.css' %}">
|
||||||
|
|
||||||
|
<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:0.5rem;">
|
||||||
|
<label for="rota-selector" style="margin:0;">Rota:</label>
|
||||||
|
<select id="rota-selector">
|
||||||
|
<option value="">All rotas</option>
|
||||||
|
{% for r in rotas %}
|
||||||
|
<option value="{{ r.id }}" {% if selected_rota_id == r.id %}selected{% endif %}>{{ r.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="calendar" class="calendar" data-mode="inline"></div>
|
<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>
|
<p class="help">View all leave requests across workers. Choose a rota to scope the calendar, or select "All rotas" to see everything. Click and drag to select dates to open the leave form (you'll need to choose workers 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>
|
||||||
|
|
||||||
<div style="margin-top:0.5rem;">
|
<div style="margin-top:0.5rem;">
|
||||||
@@ -37,34 +45,43 @@
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.debug('Unable to parse embedded leaves JSON', e);
|
console.debug('Unable to parse embedded leaves JSON', e);
|
||||||
}
|
}
|
||||||
// Provide a leavesUrl so the calendar will fetch the authoritative all-leaves list
|
// Base URL for leaves JSON
|
||||||
// Pass rota_id if present in the querystring so the server filters to rota workers.
|
let leavesUrlBase = '{% url "rota:leaves_global_json" %}';
|
||||||
const rotaParam = new URLSearchParams(window.location.search).get('rota_id');
|
// Read initial rota selection provided by the view (if any)
|
||||||
let leavesUrl = '{% url "rota:leaves_global_json" %}';
|
const initialRota = {% if selected_rota_id %} '{{ selected_rota_id }}' {% else %} null {% endif %};
|
||||||
if(rotaParam) leavesUrl += '?rota_id=' + encodeURIComponent(rotaParam);
|
window.initCalendar('calendar', { leaves: leaves, leavesUrl: leavesUrlBase, rotaId: initialRota, requestUrl: '{% url "rota:request_leave" %}', mode: container.dataset.mode || 'inline' });
|
||||||
window.initCalendar('calendar', { leaves: leaves, leavesUrl: leavesUrl, requestUrl: '{% url "rota:request_leave" %}', mode: container.dataset.mode || 'inline' });
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% if request.GET.rota_id %}
|
// Wire rota selector to update the calendar scope
|
||||||
<div class="box" style="margin-top:0.75rem;">
|
const selector = document.getElementById('rota-selector');
|
||||||
<h4 class="subtitle is-6">Workers on this rota</h4>
|
function refreshLegendFor(id){
|
||||||
<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 legend = document.getElementById('rota-worker-legend');
|
||||||
const rotaParam = new URLSearchParams(window.location.search).get('rota_id');
|
if(!legend) return;
|
||||||
if(!rotaParam) return;
|
if(!id){ legend.innerText = 'Showing leave for all rotas.'; return; }
|
||||||
fetch('{% url "rota:leaves_global_json" %}?rota_id=' + encodeURIComponent(rotaParam), {credentials:'same-origin'}).then(r=>r.json()).then(data=>{
|
legend.innerText = 'Loading legend…';
|
||||||
|
fetch(leavesUrlBase + '?rota_id=' + encodeURIComponent(id), {credentials:'same-origin'}).then(r=>r.json()).then(data=>{
|
||||||
const byWorker = {};
|
const byWorker = {};
|
||||||
(data.leaves||[]).forEach(l=>{ if(l.worker_id){ byWorker[l.worker_id] = {name:l.reason, color:l.color}; } });
|
(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; }
|
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('');
|
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); });
|
}).catch(e=>{ legend.innerText = 'Unable to load legend'; console.debug(e); });
|
||||||
|
}
|
||||||
|
if(selector){
|
||||||
|
selector.addEventListener('change', function(){
|
||||||
|
const v = selector.value || null;
|
||||||
|
try{ const cal = window._calendars && window._calendars['calendar']; if(cal && typeof cal.setRotaId === 'function'){ cal.setRotaId(v); } }
|
||||||
|
catch(e){ console.debug('Unable to set rotaId on calendar', e); }
|
||||||
|
refreshLegendFor(v);
|
||||||
|
});
|
||||||
|
// initial legend
|
||||||
|
refreshLegendFor(initialRota);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
|
||||||
|
<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">Select a rota to show its workers.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<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>
|
<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>
|
<a class="button is-info is-light is-small" href="{% url 'rota:leaves_global_rota' rota.id %}" style="margin-left:0.5rem;">All leave requests</a>
|
||||||
</p>
|
</p>
|
||||||
{% include 'rota/partials/worker_list.html' %}
|
{% include 'rota/partials/worker_list.html' %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ urlpatterns = [
|
|||||||
path("worker/settings/token/<uuid:token>/", views.worker_settings_token, name="worker_settings_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/", views.leaves_global, name="leaves_global"),
|
||||||
path("leaves/all.json", views.leaves_global_json, name="leaves_global_json"),
|
path("leaves/all.json", views.leaves_global_json, name="leaves_global_json"),
|
||||||
|
# Rota-scoped variants which render the same views but with rota_id supplied
|
||||||
|
path("rota/<int:rota_id>/leaves/all/", views.leaves_global, name="leaves_global_rota"),
|
||||||
|
path("rota/<int:rota_id>/leaves/all.json", views.leaves_global_json, name="leaves_global_json_rota"),
|
||||||
|
path("rota/<int:rota_id>/leave/select_workers/", views.select_workers_modal, name="select_workers_modal_rota"),
|
||||||
path("leave/select_workers/", views.select_workers_modal, name="select_workers_modal"),
|
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/apply_multi/", views.apply_leave_multi, name="apply_leave_multi"),
|
||||||
path("leave/<int:leave_id>/delete/", views.leave_delete, name="leave_delete"),
|
path("leave/<int:leave_id>/delete/", views.leave_delete, name="leave_delete"),
|
||||||
|
|||||||
@@ -1039,12 +1039,12 @@ def leaves_json(request, worker_id):
|
|||||||
return JsonResponse({'leaves': data})
|
return JsonResponse({'leaves': data})
|
||||||
|
|
||||||
|
|
||||||
def leaves_global_json(request):
|
def leaves_global_json(request, rota_id=None):
|
||||||
"""Return JSON list of all leave ranges across all workers."""
|
"""Return JSON list of all leave ranges across all workers."""
|
||||||
from .models import Leave
|
from .models import Leave
|
||||||
# Optionally filter by rota_id (provided as query param) so the global
|
# Optionally filter by rota_id (provided as query param) so the global
|
||||||
# calendar can be scoped to a particular rota's workers.
|
# calendar can be scoped to a particular rota's workers.
|
||||||
rota_id = request.GET.get('rota_id')
|
rota_id = request.GET.get('rota_id') if rota_id is None else rota_id
|
||||||
qs = Leave.objects.select_related('worker')
|
qs = Leave.objects.select_related('worker')
|
||||||
if rota_id:
|
if rota_id:
|
||||||
try:
|
try:
|
||||||
@@ -1078,22 +1078,23 @@ def leaves_global_json(request):
|
|||||||
return JsonResponse({'leaves': data})
|
return JsonResponse({'leaves': data})
|
||||||
|
|
||||||
|
|
||||||
def leaves_global(request):
|
def leaves_global(request, rota_id=None):
|
||||||
"""Page showing a global calendar with all leave requests."""
|
"""Page showing a global calendar with all leave requests."""
|
||||||
# Provide an empty JSON payload initially; the calendar will fetch the authoritative list
|
# Provide an empty JSON payload initially; the calendar will fetch the authoritative list
|
||||||
leaves_json = '[]'
|
leaves_json = '[]'
|
||||||
return render(request, 'rota/leaves_global.html', {'leaves_json': leaves_json})
|
selected = rota_id if rota_id is not None else request.GET.get('rota_id')
|
||||||
|
return render(request, 'rota/leaves_global.html', {'leaves_json': leaves_json, 'rotas': RotaSchedule.objects.all().order_by('name'), 'selected_rota_id': selected})
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def select_workers_modal(request):
|
def select_workers_modal(request, rota_id=None):
|
||||||
"""Return an HTMX modal partial that lists workers to apply a selected leave range to.
|
"""Return an HTMX modal partial that lists workers to apply a selected leave range to.
|
||||||
|
|
||||||
Query params: start_date, end_date, rota_id (optional)
|
Query params: start_date, end_date, rota_id (optional)
|
||||||
"""
|
"""
|
||||||
start_date = request.GET.get('start_date')
|
start_date = request.GET.get('start_date')
|
||||||
end_date = request.GET.get('end_date')
|
end_date = request.GET.get('end_date')
|
||||||
rota_id = request.GET.get('rota_id')
|
rota_id = request.GET.get('rota_id') if rota_id is None else rota_id
|
||||||
workers = Worker.objects.all()
|
workers = Worker.objects.all()
|
||||||
if rota_id:
|
if rota_id:
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user