From a2d22c34fd652cf0e32527571e84e1cb0a6c4cc7 Mon Sep 17 00:00:00 2001 From: Ross Date: Wed, 17 Dec 2025 15:55:42 +0000 Subject: [PATCH] Add tooltip for shift constraints and enhance weekday checkbox handling in worker form --- .../templates/rota/partials/shift_list.html | 10 +++ .../rota/partials/worker_form_modal.html | 88 ++++++++++++++++--- 2 files changed, 86 insertions(+), 12 deletions(-) diff --git a/djangorota/djangorota/templates/rota/partials/shift_list.html b/djangorota/djangorota/templates/rota/partials/shift_list.html index e33bc38..8a4e7ad 100644 --- a/djangorota/djangorota/templates/rota/partials/shift_list.html +++ b/djangorota/djangorota/templates/rota/partials/shift_list.html @@ -5,6 +5,16 @@ {% for s in rota.shifts %}
  • {{ s.name }} — sites: {{ s.sites|join:", " }} — days: {{ s.days|join:", " }} — workers: {{ s.workers_required }} + {% if s.constraints %} +
    + + {% for c in s.constraints %} + {# Build a simple tooltip showing key: value pairs for the constraint on hover #} + {{ c.name }} + {% endfor %} + +
    + {% endif %} {% if s.balance_offset is defined and s.balance_offset %} — balance offset: {{ s.balance_offset }} {% endif %} diff --git a/djangorota/djangorota/templates/rota/partials/worker_form_modal.html b/djangorota/djangorota/templates/rota/partials/worker_form_modal.html index cdfb306..32504f7 100644 --- a/djangorota/djangorota/templates/rota/partials/worker_form_modal.html +++ b/djangorota/djangorota/templates/rota/partials/worker_form_modal.html @@ -268,6 +268,7 @@ if(textarea) textarea.style.display='none'; const listEl = document.getElementById(cfg.container); const addBtn = document.getElementById(cfg.addButton); + const WEEKDAYS = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']; function parse(){ let v = textarea ? textarea.value : ''; if(!v) return []; @@ -284,16 +285,48 @@ const field = document.createElement('div'); field.className='field'; const label = document.createElement('label'); label.className='label'; label.textContent=f.label; field.appendChild(label); const control = document.createElement('div'); control.className='control'; - let input; - if(f.type==='date'){ - input = document.createElement('input'); input.type='date'; input.className='datepicker input'; input.setAttribute('autocomplete','off'); input.value = item[f.name] || ''; - } else if(f.type==='int'){ - input = document.createElement('input'); input.type='number'; input.className='input'; input.value = item[f.name] || ''; - } else { - input = document.createElement('input'); input.type='text'; input.className='input'; input.value = item[f.name] || ''; + + // Special-case: render weekday checkboxes when the field name is 'days' + if(f.name === 'days'){ + const box = document.createElement('div'); box.className='buttons'; box.setAttribute('data-field', f.name); + WEEKDAYS.forEach(d => { + const btn = document.createElement('label'); btn.className='button is-small is-light'; btn.style.marginRight='0.25rem'; + const chk = document.createElement('input'); chk.type='checkbox'; chk.value = d; chk.style.marginRight='0.35rem'; + // mark checked if item[f.name] contains this day (accept set/list/string) + try{ + const cur = item[f.name]; + if(cur){ + if(Array.isArray(cur)){ + if(cur.indexOf(d)!==-1) chk.checked = true; + } else if(typeof cur === 'string'){ + const parts = cur.split(',').map(s=>s.trim()); if(parts.indexOf(d)!==-1) chk.checked = true; + } else if(typeof cur === 'object'){ + // set-like + if(cur.hasOwnProperty(d) || (Array.from(Object.values(cur)).indexOf(d)!==-1)) chk.checked = true; + } + } + }catch(e){} + chk.onchange = ()=>{ sync(idx); }; + btn.appendChild(chk); + const span = document.createElement('span'); span.textContent = d; btn.appendChild(span); + box.appendChild(btn); + }); + control.appendChild(box); + field.appendChild(control); col.appendChild(field); row.appendChild(col); + } else { + let input; + if(f.type==='date'){ + input = document.createElement('input'); input.type='date'; input.className='datepicker input'; input.setAttribute('autocomplete','off'); input.value = item[f.name] || ''; + } else if(f.type==='int'){ + input = document.createElement('input'); input.type='number'; input.className='input'; input.value = item[f.name] || ''; + } else { + input = document.createElement('input'); input.type='text'; input.className='input'; input.value = item[f.name] || ''; + } + // tag this control with the field name so sync() can find it reliably + input.setAttribute('data-field', f.name); + input.onchange = ()=>{ sync(idx); }; + control.appendChild(input); field.appendChild(control); col.appendChild(field); row.appendChild(col); } - input.onchange = ()=>{ sync(idx); }; - control.appendChild(input); field.appendChild(control); col.appendChild(field); row.appendChild(col); }); // remove const colRm = document.createElement('div'); colRm.className='column is-narrow'; const fr = document.createElement('div'); fr.className='field'; const ctr = document.createElement('div'); ctr.className='control'; const rm = document.createElement('button'); rm.type='button'; rm.className='button is-danger is-light is-small'; rm.textContent='Remove'; rm.onclick = ()=>{ removeAt(idx); }; ctr.appendChild(rm); fr.appendChild(ctr); colRm.appendChild(fr); row.appendChild(colRm); @@ -302,7 +335,38 @@ return wrap; } - function sync(i){ const data = parse(); const el = listEl.children[i]; if(!el) return; const inputs = el.querySelectorAll('input'); const obj = {}; cfg.fields.forEach((f,j)=>{ const val = inputs[j].value; if(val!=='') obj[f.name]= f.type==='int'?parseInt(val,10):val; }); data[i]=obj; if(textarea) textarea.value = JSON.stringify(data); } + function sync(i){ + const data = parse(); + const el = listEl.children[i]; if(!el) return; + const obj = {}; + cfg.fields.forEach((f)=>{ + const fld = el.querySelector('[data-field="'+f.name+'"]'); + if(!fld){ + // fallback: try to find an input by label text + const inp = el.querySelector('input'); + if(inp && inp.value!=='') obj[f.name] = f.type==='int'?parseInt(inp.value,10):inp.value; + return; + } + // if this is a checkbox group (for days), fld will be a container + if(f.name === 'days'){ + const checks = fld.querySelectorAll('input[type=checkbox]'); + const vals = Array.from(checks).filter(c=>c.checked).map(c=>c.value); + if(vals.length) obj[f.name] = vals; + } else { + // single input control + if(fld.tagName === 'INPUT'){ + const v = fld.value; + if(v !== '') obj[f.name] = f.type==='int'?parseInt(v,10):v; + } else { + // container wrapping an input + const inp = fld.querySelector('input'); + if(inp && inp.value !== '') obj[f.name] = f.type==='int'?parseInt(inp.value,10):inp.value; + } + } + }); + data[i]=obj; + if(textarea) textarea.value = JSON.stringify(data); + } function removeAt(i){ const data = parse(); data.splice(i,1); if(textarea) textarea.value = JSON.stringify(data); render(); } @@ -321,8 +385,8 @@ makeSimpleListEditor({textarea:'id_allowed_multi_shift_sets', container:'ams-list', addButton:'add-ams', fields:[{name:'shifts',label:'Shifts (comma-separated)',type:'text'}]}); // Hard day deps/exclusions need days/weeks/start/end - makeSimpleListEditor({textarea:'id_hard_day_dependencies', container:'hdd-list', addButton:'add-hdd', fields:[{name:'days',label:'Days (comma-separated)',type:'text'},{name:'weeks',label:'Weeks (comma-separated ints)',type:'text'},{name:'start_date',label:'Start date',type:'date'},{name:'end_date',label:'End date',type:'date'}]}); - makeSimpleListEditor({textarea:'id_hard_day_exclusions', container:'hde-list', addButton:'add-hde', fields:[{name:'days',label:'Days (comma-separated)',type:'text'},{name:'weeks',label:'Weeks (comma-separated ints)',type:'text'},{name:'start_date',label:'Start date',type:'date'},{name:'end_date',label:'End date',type:'date'}]}); + makeSimpleListEditor({textarea:'id_hard_day_dependencies', container:'hdd-list', addButton:'add-hdd', fields:[{name:'days',label:'Days (weekdays)',type:'text'},{name:'weeks',label:'Weeks (comma-separated ints)',type:'text'},{name:'start_date',label:'Start date',type:'date'},{name:'end_date',label:'End date',type:'date'}]}); + makeSimpleListEditor({textarea:'id_hard_day_exclusions', container:'hde-list', addButton:'add-hde', fields:[{name:'days',label:'Days (weekdays)',type:'text'},{name:'weeks',label:'Weeks (comma-separated ints)',type:'text'},{name:'start_date',label:'Start date',type:'date'},{name:'end_date',label:'End date',type:'date'}]}); })();