Enhance textarea synchronization and improve WorkerForm save logic

This commit is contained in:
Ross
2025-12-14 22:28:17 +00:00
parent 008c28f68e
commit 0db4881d6f
3 changed files with 93 additions and 43 deletions
@@ -84,6 +84,27 @@ function _close_shift_modal_targets(){
data.forEach((c, idx)=>{
listEl.appendChild(renderConstraintRow(c, idx));
});
// Ensure textarea is synchronized with the rendered rows. Some
// inputs update the textarea via their handlers; this function
// triggers those handlers or falls back to reconstructing the
// JSON from DOM if needed.
function syncTextarea(){
try{
const children = listEl.children;
for(let i=0;i<children.length;i++){
const ch = children[i];
// dispatch change/input events on controls so their handlers
// update the textarea via syncToData
const controls = ch.querySelectorAll('select, input, textarea');
controls.forEach(ctrl=>{
try{
if(ctrl.tagName==='SELECT') ctrl.dispatchEvent(new Event('change', {bubbles:true}));
else ctrl.dispatchEvent(new Event('input', {bubbles:true}));
}catch(e){}
});
}
}catch(e){ /* ignore */ }
}
syncTextarea();
}
@@ -43,7 +43,68 @@
function parseInitial(){
let val = textarea ? textarea.value : '';
if(!val) return [];
try{ const p = JSON.parse(val); return Array.isArray(p)?p:[]; }catch(e){ return []; }
let raw;
try{ raw = JSON.parse(val); }catch(e){ return []; }
if(!Array.isArray(raw)) return [];
// Normalize entries: the stored shape may be a list of ints (0=Mon),
// strings, or objects {day: 'Mon', start_date, end_date}.
return raw.map(item=>{
if(item === null || item === undefined) return {};
if(typeof item === 'number'){
return { day: DAYS[item] || '' };
}
if(typeof item === 'string'){
// Accept 'monday' / 'Mon' / 'mon' / '0' etc.
const n = parseInt(item,10);
if(!Number.isNaN(n)) return { day: DAYS[n] || '' };
const s = item.trim();
// try matching first 3 letters ignoring case
const low = s.toLowerCase();
for(let d of DAYS){ if(d.toLowerCase().startsWith(low.slice(0,3))) return { day: d }; }
// fallback: return as-day string
return { day: s };
}
if(typeof item === 'object'){
// If object has numeric day, convert
const copy = Object.assign({}, item);
if(copy.day !== undefined && typeof copy.day === 'number'){
copy.day = DAYS[copy.day] || '';
}
// If day is a short name, normalise to 3-letter capitalised
if(copy.day && typeof copy.day === 'string'){
const s = copy.day.trim();
const low = s.toLowerCase();
for(let d of DAYS){ if(d.toLowerCase().startsWith(low.slice(0,3))) { copy.day = d; break; } }
}
return copy;
}
return {};
});
}
function syncTextarea(){
// Ensure each row's inputs trigger their onchange handlers so the
// hidden textarea is kept in sync. This is resilient whether rows
// expose a `_sync` helper or rely on input events.
try{
const children = listEl.children;
for(let i=0;i<children.length;i++){
const ch = children[i];
if(ch && typeof ch._sync === 'function'){
try{ ch._sync(); }catch(e){}
} else {
// trigger change/input events on contained controls
const controls = ch.querySelectorAll('select, input, textarea');
controls.forEach(ctrl=>{
try{
if(ctrl.tagName==='SELECT') ctrl.dispatchEvent(new Event('change', {bubbles:true}));
else ctrl.dispatchEvent(new Event('input', {bubbles:true}));
}catch(e){}
});
}
}
}catch(e){ /* ignore */ }
}
function render(){
+10 -42
View File
@@ -444,57 +444,25 @@ class WorkerSelfServiceForm(forms.ModelForm):
super().__init__(*args, **kwargs)
def save(self, commit=True):
"""Persist FTE and non-working days (nwds) into the Worker instance.
The form exposes only `fte` and `nwds`. `fte` is handled by the
ModelForm machinery; we need to ensure `nwds` is persisted inside
`instance.options['nwds']` as a list of ints.
"""
instance = super().save(commit=False)
# persist non-working days into instance.options['nwds'] as list of ints
# Persist non-working days into instance.options['nwds'] as list of ints
opts = instance.options or {}
nwds = self.cleaned_data.get('nwds') or []
try:
opts['nwds'] = [int(x) for x in nwds]
except Exception:
# fallback: keep as-is
opts['nwds'] = nwds
instance.options = opts
if commit:
instance.save()
return instance
def save(self, commit=True):
instance = super().save(commit=False)
end_date = self.cleaned_data.get("end_date")
if end_date:
instance.end_date = end_date
# Collect option fields and persist into instance.options
opts = instance.options or {}
# constraint-based option fields
for key in getattr(self, "_constraint_defaults", {}).keys():
field_name = f"opt__{key}"
if field_name in self.cleaned_data:
val = self.cleaned_data[field_name]
default = self._constraint_defaults.get(key)
# Parse JSON for list/dict fields
if isinstance(default, (list, dict)):
try:
parsed = json.loads(val) if val is not None and val != "" else []
except Exception:
parsed = val
opts[key] = parsed
else:
opts[key] = val
# builder args (prefixed with opt__)
builder_keys = [
"balance_offset_modifier",
"ltft_balance_offset",
"use_previous_shifts",
"use_shift_balance_extra",
"use_bank_holiday_extra",
"allow_force_assignment_with_leave_conflict",
]
for k in builder_keys:
field_name = f"opt__{k}"
if field_name in self.cleaned_data:
opts[k] = self.cleaned_data[field_name]
instance.options = opts
if commit:
instance.save()
try: