Add tooltip for shift constraints and enhance weekday checkbox handling in worker form

This commit is contained in:
Ross
2025-12-17 15:55:42 +00:00
parent ba154f680a
commit a2d22c34fd
2 changed files with 86 additions and 12 deletions
@@ -5,6 +5,16 @@
{% for s in rota.shifts %}
<li>
<strong>{{ s.name }}</strong> — sites: {{ s.sites|join:", " }} — days: {{ s.days|join:", " }} — workers: {{ s.workers_required }}
{% if s.constraints %}
<div style="margin-top:0.25rem;">
<span class="tags">
{% for c in s.constraints %}
{# Build a simple tooltip showing key: value pairs for the constraint on hover #}
<span class="tag is-light" title="{% for k,v in c.items %}{{ k }}: {{ v }}{% if not forloop.last %}; {% endif %}{% endfor %}">{{ c.name }}</span>
{% endfor %}
</span>
</div>
{% endif %}
{% if s.balance_offset is defined and s.balance_offset %}
— balance offset: {{ s.balance_offset }}
{% endif %}
@@ -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'}]});
})();
</script>