From 0db4881d6ff1ca37b5969d4c0c70036966cf612c Mon Sep 17 00:00:00 2001 From: Ross Date: Sun, 14 Dec 2025 22:28:17 +0000 Subject: [PATCH] Enhance textarea synchronization and improve WorkerForm save logic --- .../rota/partials/shift_form_modal.html | 21 +++++++ .../rota/partials/worker_form_modal.html | 63 ++++++++++++++++++- djangorota/rota/forms.py | 52 +++------------ 3 files changed, 93 insertions(+), 43 deletions(-) diff --git a/djangorota/djangorota/templates/rota/partials/shift_form_modal.html b/djangorota/djangorota/templates/rota/partials/shift_form_modal.html index 13a2a32..873120a 100644 --- a/djangorota/djangorota/templates/rota/partials/shift_form_modal.html +++ b/djangorota/djangorota/templates/rota/partials/shift_form_modal.html @@ -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{ + 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(); } diff --git a/djangorota/djangorota/templates/rota/partials/worker_form_modal.html b/djangorota/djangorota/templates/rota/partials/worker_form_modal.html index aeb7a89..cdfb306 100644 --- a/djangorota/djangorota/templates/rota/partials/worker_form_modal.html +++ b/djangorota/djangorota/templates/rota/partials/worker_form_modal.html @@ -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{ + 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(){ diff --git a/djangorota/rota/forms.py b/djangorota/rota/forms.py index 68240ec..72eb453 100644 --- a/djangorota/rota/forms.py +++ b/djangorota/rota/forms.py @@ -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: