Add rota-builder summary to detail view with weeks, shifts, and workers count
This commit is contained in:
@@ -11,6 +11,19 @@
|
|||||||
<p class="subtitle">{{ rota.description }}</p>
|
<p class="subtitle">{{ rota.description }}</p>
|
||||||
<p>Period: {{ rota.start_date }} → {{ rota.end_date }}</p>
|
<p>Period: {{ rota.start_date }} → {{ rota.end_date }}</p>
|
||||||
|
|
||||||
|
<div class="box" style="margin-top:0.75rem;">
|
||||||
|
<h2 class="subtitle is-6">RotaBuilder summary</h2>
|
||||||
|
<div class="columns is-mobile is-multiline" style="margin-bottom:0.5rem;">
|
||||||
|
<div class="column is-narrow"><strong>Weeks</strong></div>
|
||||||
|
<div class="column">{{ weeks_to_rota|default:'—' }}</div>
|
||||||
|
<div class="column is-narrow"><strong>Shifts</strong></div>
|
||||||
|
<div class="column">{{ shift_count|default:'—' }}</div>
|
||||||
|
<div class="column is-narrow"><strong>Workers</strong></div>
|
||||||
|
<div class="column">{{ worker_count|default:'—' }}</div>
|
||||||
|
</div>
|
||||||
|
{# constraints sample intentionally omitted here; keep summary compact #}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<p>
|
<p>
|
||||||
<button class="button is-link" hx-get="{% url 'rota:worker_add' %}?rota_id={{ rota.id }}" hx-target="#modal" hx-swap="innerHTML">Add worker</button>
|
<button class="button is-link" hx-get="{% url 'rota:worker_add' %}?rota_id={{ rota.id }}" hx-target="#modal" hx-swap="innerHTML">Add worker</button>
|
||||||
|
|||||||
@@ -441,6 +441,77 @@ class RotaScheduleForm(forms.ModelForm):
|
|||||||
self.fields["end_date"].initial = end_date
|
self.fields["end_date"].initial = end_date
|
||||||
return cleaned
|
return cleaned
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
"""Save the RotaSchedule and persist dynamic `opt__*` fields into
|
||||||
|
`instance.options`.
|
||||||
|
|
||||||
|
This persists both typed constraint option fields (created from
|
||||||
|
RotaConstraintOptions) and the small set of builder-argument fields
|
||||||
|
(created in __init__). For structured fields we attempt to parse JSON
|
||||||
|
textareas into Python objects.
|
||||||
|
"""
|
||||||
|
instance = super().save(commit=False)
|
||||||
|
|
||||||
|
opts = instance.options or {}
|
||||||
|
|
||||||
|
# Persist typed constraint option fields using inferred defaults map
|
||||||
|
try:
|
||||||
|
defaults = getattr(self, "_constraint_defaults", {}) or {}
|
||||||
|
except Exception:
|
||||||
|
defaults = {}
|
||||||
|
|
||||||
|
for key, default in defaults.items():
|
||||||
|
field_name = f"opt__{key}"
|
||||||
|
if field_name in self.cleaned_data:
|
||||||
|
val = self.cleaned_data[field_name]
|
||||||
|
if isinstance(default, (list, dict)):
|
||||||
|
# JSON textarea
|
||||||
|
try:
|
||||||
|
parsed = json.loads(val) if val is not None and val != "" else []
|
||||||
|
except Exception:
|
||||||
|
parsed = val
|
||||||
|
opts[key] = parsed
|
||||||
|
else:
|
||||||
|
if val == "" or val is None:
|
||||||
|
opts[key] = None
|
||||||
|
else:
|
||||||
|
opts[key] = val
|
||||||
|
|
||||||
|
# Persist any remaining opt__ fields (builder args and others)
|
||||||
|
for fname, val in self.cleaned_data.items():
|
||||||
|
if not fname.startswith("opt__"):
|
||||||
|
continue
|
||||||
|
key = fname[5:]
|
||||||
|
if key in defaults:
|
||||||
|
# already handled
|
||||||
|
continue
|
||||||
|
v = val
|
||||||
|
# Try to parse JSON-like strings into structures
|
||||||
|
if isinstance(v, str):
|
||||||
|
s = v.strip()
|
||||||
|
if s.startswith("[") or s.startswith("{"):
|
||||||
|
try:
|
||||||
|
v = json.loads(v)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
opts[key] = v
|
||||||
|
|
||||||
|
# Persist the top-level 'weeks' value from the form into options so
|
||||||
|
# the builder can prefer this explicit value instead of deriving it
|
||||||
|
# from end_date. Do not persist end_date itself here.
|
||||||
|
try:
|
||||||
|
weeks_val = self.cleaned_data.get("weeks")
|
||||||
|
if weeks_val is not None:
|
||||||
|
opts["weeks"] = int(weeks_val)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
instance.options = opts
|
||||||
|
|
||||||
|
if commit:
|
||||||
|
instance.save()
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
class WorkerSelfServiceForm(forms.ModelForm):
|
class WorkerSelfServiceForm(forms.ModelForm):
|
||||||
"""Small form exposed to workers via tokenized links so they can set
|
"""Small form exposed to workers via tokenized links so they can set
|
||||||
|
|||||||
@@ -55,14 +55,34 @@ class RotaSchedule(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# compute weeks_to_rota (integer number of weeks)
|
# compute weeks_to_rota (integer number of weeks)
|
||||||
delta = self.end_date - self.start_date
|
# Prefer an explicit value stored in `self.options['weeks']` (set by
|
||||||
weeks_to_rota = max(1, int(delta.days // 7))
|
# the rota edit form). If absent, fall back to deriving from the
|
||||||
|
# stored end_date (backwards compatible).
|
||||||
|
opts_preview = dict(self.options or {})
|
||||||
|
weeks_opt = None
|
||||||
|
if "weeks" in opts_preview:
|
||||||
|
try:
|
||||||
|
weeks_opt = int(opts_preview.get("weeks"))
|
||||||
|
except Exception:
|
||||||
|
weeks_opt = None
|
||||||
|
if weeks_opt is not None:
|
||||||
|
weeks_to_rota = max(1, weeks_opt)
|
||||||
|
else:
|
||||||
|
delta = self.end_date - self.start_date
|
||||||
|
weeks_to_rota = max(1, int(delta.days // 7))
|
||||||
|
|
||||||
# Build kwargs for RotaBuilder from any explicit builder args stored in
|
# Build kwargs for RotaBuilder from any explicit builder args stored in
|
||||||
# `self.options` (kept for backward compatibility), and treat the
|
# `self.options` (kept for backward compatibility), and treat the
|
||||||
# remaining keys as constraint options which should map to
|
# remaining keys as constraint options which should map to
|
||||||
# `RotaConstraintOptions`.
|
# `RotaConstraintOptions`.
|
||||||
opts = dict(self.options or {})
|
opts = dict(self.options or {})
|
||||||
|
# Remove 'weeks' from opts before handing to constraint options so
|
||||||
|
# it doesn't cause validation failures in RotaConstraintOptions.
|
||||||
|
if "weeks" in opts:
|
||||||
|
try:
|
||||||
|
opts.pop("weeks")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
builder_keys = [
|
builder_keys = [
|
||||||
"balance_offset_modifier",
|
"balance_offset_modifier",
|
||||||
"ltft_balance_offset",
|
"ltft_balance_offset",
|
||||||
|
|||||||
@@ -191,6 +191,31 @@ def rota_detail(request, rota_id):
|
|||||||
except Exception:
|
except Exception:
|
||||||
configured_constraints = {}
|
configured_constraints = {}
|
||||||
|
|
||||||
|
# Compute a small rota-builder summary for display in the template
|
||||||
|
try:
|
||||||
|
# Prefer explicit value stored in rota.options['weeks'] when present.
|
||||||
|
opts = rota.options or {}
|
||||||
|
if "weeks" in opts:
|
||||||
|
try:
|
||||||
|
weeks_to_rota = max(1, int(opts.get("weeks")))
|
||||||
|
except Exception:
|
||||||
|
weeks_to_rota = None
|
||||||
|
else:
|
||||||
|
delta = rota.end_date - rota.start_date
|
||||||
|
weeks_to_rota = max(1, int(delta.days // 7))
|
||||||
|
except Exception:
|
||||||
|
weeks_to_rota = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
shift_count = len(rota.shifts or [])
|
||||||
|
except Exception:
|
||||||
|
shift_count = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
worker_count = rota.workers.count()
|
||||||
|
except Exception:
|
||||||
|
worker_count = None
|
||||||
|
|
||||||
# legacy RotaOptionsForm not used here; we provide a typed RotaScheduleForm below
|
# legacy RotaOptionsForm not used here; we provide a typed RotaScheduleForm below
|
||||||
|
|
||||||
# Provide a typed RotaScheduleForm instance so the template can render
|
# Provide a typed RotaScheduleForm instance so the template can render
|
||||||
@@ -205,7 +230,15 @@ def rota_detail(request, rota_id):
|
|||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"rota/rota_detail.html",
|
"rota/rota_detail.html",
|
||||||
{"rota": rota, "options_form": typed_form, "available_constraints": available_constraints_rich, "configured_constraints": configured_constraints},
|
{
|
||||||
|
"rota": rota,
|
||||||
|
"options_form": typed_form,
|
||||||
|
"available_constraints": available_constraints_rich,
|
||||||
|
"configured_constraints": configured_constraints,
|
||||||
|
"weeks_to_rota": weeks_to_rota,
|
||||||
|
"shift_count": shift_count,
|
||||||
|
"worker_count": worker_count,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user