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'}]});
})();