def reschedule_series(
self, task_id: str, scheduled_date: str, *, op_id: str | None = None
) -> dict[str, Any]:
"""Shift every occurrence of a recurrence by the same day offset.
The dragged occurrence defines the delta between its current date and
the drop target; all sibling occurrences (same ``template_id``) move by
that delta. Occurrences without a date are left untouched, and the
recurrence rule itself is edited separately on the Récurrences page.
"""
scheduled = _validate_iso_date(scheduled_date)
if not scheduled:
raise SuccesError("Une date valide est requise pour reporter la série.")
ts = now_ms()
with self._transaction() as conn:
anchor = self._load_task(conn, task_id)
if anchor is None:
raise SuccesNotFound("Cette tâche n'existe pas ou a été supprimée.")
template_id = str(anchor.get("templateId") or "")
if not template_id:
raise SuccesError("Cette tâche ne provient pas d'une récurrence.")
current = str(anchor.get("date") or "")
if not current:
raise SuccesError("La tâche de référence n'a pas encore de date.")
delta = (date.fromisoformat(scheduled) - date.fromisoformat(current)).days
rows = conn.execute(
"SELECT id, scheduled_date FROM succes_tasks "
"WHERE template_id=? AND deleted_at_ms IS NULL",
(template_id,),
).fetchall()
updated = 0
for row in rows:
old = str(row["scheduled_date"] or "")
if not old:
continue
new_date = (date.fromisoformat(old) + timedelta(days=delta)).isoformat()
if delta != 0:
conn.execute(
"UPDATE succes_tasks SET scheduled_date=?, updated_at_ms=? "
"WHERE id=?",
(new_date, ts, row["id"]),
)
task = self._load_task(conn, row["id"])
assert task is not None
self._record_op(
conn,
entity="tasks",
entity_id=row["id"],
kind="upsert",
payload=task,
request={
"action": "reschedule_series",
"taskId": row["id"],
"templateId": template_id,
"date": new_date,
},
timestamp_ms=ts,
)
updated += 1
return {"templateId": template_id, "deltaDays": delta, "updated": updated}