Créneaux à la main + grille des besoins en axe des temps (30 min)

Un poste ne génère plus de créneaux à sa création : la coordination crée
elle-même ceux dont elle a besoin, avec ses propres horaires de début et
de fin (ConfigGrid.tsx). La génération depuis les créneaux types reste
disponible en option (bouton « Générer les créneaux »).

La grille des besoins devient un vrai axe des temps : une colonne = 30 min,
bornée par jour à la plage réellement utilisée, avec les cases fusionnées
pour correspondre aux horaires de chaque créneau (buildTimeGrid /
placeInTimeGrid). La « Vue bénévoles » garde son ancien modèle (une colonne
par créneau distinct), plus adapté à son tableau bénévole × créneau.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-09-22 18:44:20 +02:00
co-authored by Claude Sonnet 5
parent f1d88302a4
commit 15109acfe5
9 changed files with 474 additions and 179 deletions
+3 -3
View File
@@ -58,9 +58,9 @@ def create_position(
_check_lead_account(session, edition, payload.lead_account_id)
position = Position(edition_id=edition.id, **payload.model_dump())
session.add(position)
session.flush()
# Un créneau à besoin 0/0 par jour et par créneau type de l'édition.
generate_missing_shifts(session, edition, position)
# Pas de créneaux générés automatiquement : la coordination choisit
# elle-même ceux dont elle a besoin (créés à la main, ou en repartant des
# créneaux types via `generate_shifts` ci-dessous).
session.commit()
session.refresh(position)
return position
+31 -13
View File
@@ -1,4 +1,9 @@
"""Génération des créneaux d'un poste à partir des créneaux types."""
"""Génération des créneaux d'un poste à partir des créneaux types.
Depuis le passage à la création de créneaux à la main (voir
`test_shifts_api.py`), un `Position` ne génère plus rien automatiquement à sa
création : `generate-shifts` reste l'outil optionnel pour repartir des
créneaux types de l'édition."""
import datetime as dt
@@ -13,12 +18,26 @@ def _create_position(client, edition, name="Bar"):
).json()
def test_creating_a_position_generates_one_shift_per_day_and_type(client, edition):
def _generate(client, edition, position):
return client.post(f"{_positions_url(edition)}/{position['id']}/generate-shifts")
def test_creating_a_position_generates_no_shifts(client, edition):
position = _create_position(client, edition)
shifts = client.get(
f"/editions/{edition.id}/shifts", params={"position_id": position["id"]}
).json()
assert shifts == []
def test_generate_shifts_creates_one_per_day_and_type(client, edition):
position = _create_position(client, edition)
resp = _generate(client, edition, position)
# 4 jours (2026-07-09..12) x 3 créneaux types.
assert resp.json()["created"] == 12
shifts = client.get(
f"/editions/{edition.id}/shifts", params={"position_id": position["id"]}
).json()
assert len(shifts) == 12
assert all(s["min_staff"] == 0 and s["ideal_staff"] == 0 for s in shifts)
assert all(s["day_part_id"] is not None for s in shifts)
@@ -26,20 +45,18 @@ def test_creating_a_position_generates_one_shift_per_day_and_type(client, editio
def test_generate_is_idempotent(client, edition):
position = _create_position(client, edition)
resp = client.post(
f"{_positions_url(edition)}/{position['id']}/generate-shifts"
)
_generate(client, edition, position)
resp = _generate(client, edition, position)
assert resp.status_code == 200
assert resp.json()["created"] == 0
def test_extending_the_edition_then_generating_adds_the_new_days(client, edition):
position = _create_position(client, edition)
_generate(client, edition, position)
client.patch(f"/editions/{edition.id}", json={"end_date": "2026-07-14"})
resp = client.post(
f"{_positions_url(edition)}/{position['id']}/generate-shifts"
)
resp = _generate(client, edition, position)
# 2 jours de plus x 3 créneaux types.
assert resp.json()["created"] == 6
shifts = client.get(
@@ -50,13 +67,12 @@ def test_extending_the_edition_then_generating_adds_the_new_days(client, edition
def test_adding_a_day_part_then_generating_adds_it_everywhere(client, edition):
position = _create_position(client, edition)
_generate(client, edition, position)
client.post(
f"/editions/{edition.id}/day-parts",
json={"label": "Nuit", "start_time": "02:00", "end_time": "05:00"},
)
resp = client.post(
f"{_positions_url(edition)}/{position['id']}/generate-shifts"
)
resp = _generate(client, edition, position)
assert resp.json()["created"] == 4 # 1 par jour
@@ -67,11 +83,12 @@ def test_day_part_before_the_day_boundary_rolls_to_the_next_calendar_day(client,
suite de `Soirée` 19 h–00 h) est donc la fin de la nuit du jour
*précédent* : il doit être généré sur le jour suivant, pas sur `day`."""
position = _create_position(client, edition)
_generate(client, edition, position)
client.post(
f"/editions/{edition.id}/day-parts",
json={"label": "Nuit", "start_time": "02:00", "end_time": "05:00", "order": 3},
)
client.post(f"{_positions_url(edition)}/{position['id']}/generate-shifts")
_generate(client, edition, position)
shifts = client.get(
f"/editions/{edition.id}/shifts", params={"position_id": position["id"]}
@@ -89,6 +106,7 @@ def test_day_part_before_the_day_boundary_rolls_to_the_next_calendar_day(client,
def test_generation_does_not_touch_staffing_already_set(client, edition):
position = _create_position(client, edition)
_generate(client, edition, position)
shift = client.get(
f"/editions/{edition.id}/shifts", params={"position_id": position["id"]}
).json()[0]
@@ -96,6 +114,6 @@ def test_generation_does_not_touch_staffing_already_set(client, edition):
f"/editions/{edition.id}/shifts/{shift['id']}",
json={"min_staff": 3, "ideal_staff": 5},
)
client.post(f"{_positions_url(edition)}/{position['id']}/generate-shifts")
_generate(client, edition, position)
again = client.get(f"/editions/{edition.id}/shifts/{shift['id']}").json()
assert again["min_staff"] == 3 and again["ideal_staff"] == 5