Enhance textarea synchronization and improve WorkerForm save logic
This commit is contained in:
@@ -84,6 +84,27 @@ function _close_shift_modal_targets(){
|
|||||||
data.forEach((c, idx)=>{
|
data.forEach((c, idx)=>{
|
||||||
listEl.appendChild(renderConstraintRow(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();
|
syncTextarea();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,68 @@
|
|||||||
function parseInitial(){
|
function parseInitial(){
|
||||||
let val = textarea ? textarea.value : '';
|
let val = textarea ? textarea.value : '';
|
||||||
if(!val) return [];
|
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(){
|
function render(){
|
||||||
|
|||||||
+10
-42
@@ -444,57 +444,25 @@ class WorkerSelfServiceForm(forms.ModelForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def save(self, commit=True):
|
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)
|
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 {}
|
opts = instance.options or {}
|
||||||
nwds = self.cleaned_data.get('nwds') or []
|
nwds = self.cleaned_data.get('nwds') or []
|
||||||
try:
|
try:
|
||||||
opts['nwds'] = [int(x) for x in nwds]
|
opts['nwds'] = [int(x) for x in nwds]
|
||||||
except Exception:
|
except Exception:
|
||||||
|
# fallback: keep as-is
|
||||||
opts['nwds'] = nwds
|
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
|
instance.options = opts
|
||||||
|
|
||||||
if commit:
|
if commit:
|
||||||
instance.save()
|
instance.save()
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user