Repository: betagouv/service-national-universel
Pull request #4824: feat(api, admin): 42, 298 - Changement de centre sur une ligne et évolutions des notifs de changements de PDR
Author: login: nicobret
Labels: name: deploy
description: Deploy custom env
Created: 2025-03-10T16:24:05Z | Merged: 2025-03-13T10:11:09Z

Merge des 2 PR sur l'édition des PDR et des centre sur les lignes de bus.


Diff:
diff --git a/admin/src/scenes/plan-transport/lib/useSessionsByCohort.ts b/admin/src/scenes/plan-transport/lib/useSessionsByCohort.ts
new file mode 100644
index 0000000000..04dc630010
--- /dev/null
+++ b/admin/src/scenes/plan-transport/lib/useSessionsByCohort.ts
@@ -0,0 +1,11 @@
+import { useQuery } from "@tanstack/react-query";
+import { SessionPhase1Service } from "../../../services/sessionPhase1Service";
+
+export default function useSessionsByCohort(cohortName: string) {
+  const filters = { cohort: [cohortName] };
+  return useQuery({
+    queryKey: ["session", cohortName],
+    queryFn: () => SessionPhase1Service.getSessions(filters),
+    refetchOnWindowFocus: false,
+  });
+}
diff --git a/admin/src/scenes/plan-transport/lib/useUpdateSessionSurLigneDeBus.ts b/admin/src/scenes/plan-transport/lib/useUpdateSessionSurLigneDeBus.ts
new file mode 100644
index 0000000000..90c9b1cd79
--- /dev/null
+++ b/admin/src/scenes/plan-transport/lib/useUpdateSessionSurLigneDeBus.ts
@@ -0,0 +1,12 @@
+import { useMutation } from "@tanstack/react-query";
+import { toastr } from "react-redux-toastr";
+import { LigneBusType, translate } from "snu-lib";
+import { SessionPhase1Service } from "../../../services/sessionPhase1Service";
+
+export default function useUpdateSessionSurLigneDeBus(busId: string) {
+  return useMutation({
+    mutationFn: (payload: Partial<LigneBusType>) => SessionPhase1Service.updateSessionSurLigneDeBus(busId, payload),
+    onSuccess: () => toastr.success("Succès", "Votre modification a été prise en compte."),
+    onError: (error: Error) => toastr.error("Oups, une erreur est survenue lors de la modification des informations du centre", translate(error.message)),
+  });
+}
diff --git a/admin/src/scenes/plan-transport/ligne-bus/View/authorization.ts b/admin/src/scenes/plan-transport/ligne-bus/View/authorization.ts
new file mode 100644
index 0000000000..4067793a30
--- /dev/null
+++ b/admin/src/scenes/plan-transport/ligne-bus/View/authorization.ts
@@ -0,0 +1,25 @@
+import { canEditLigneBusCenter, canEditLigneBusPointDeRassemblement, CohortType, isBusEditionOpen, ROLES, UserDto } from "snu-lib";
+
+export function getAuthorizationToUpdatePdrOnLine(user: UserDto, cohort?: CohortType | null): { isAuthorized: boolean; message: string } {
+  if (!cohort) return { isAuthorized: false, message: "Impossible de récupérer les informations de la cohorte" };
+  if (user.role === ROLES.TRANSPORTER) {
+    if (isBusEditionOpen(user, cohort)) return { isAuthorized: true, message: "" };
+    return { isAuthorized: false, message: "La modification des lignes de transport est fermée pour ce séjour." };
+  }
+  if (!canEditLigneBusPointDeRassemblement(user)) {
+    return { isAuthorized: false, message: "Vous n'avez pas l'autorisation de modifier le point de rassemblement." };
+  }
+  return { isAuthorized: true, message: "" };
+}
+
+export function getAuthorizationToUpdateCenterOnLine(user: UserDto, cohort?: CohortType | null): { isAuthorized: boolean; message: string } {
+  if (!cohort) return { isAuthorized: false, message: "Impossible de récupérer les informations de la cohorte" };
+  if (user.role === ROLES.TRANSPORTER) {
+    if (isBusEditionOpen(user, cohort)) return { isAuthorized: true, message: "" };
+    return { isAuthorized: false, message: "La modification des lignes de transport est fermée pour ce séjour." };
+  }
+  if (!canEditLigneBusCenter(user)) {
+    return { isAuthorized: false, message: "Vous n'avez pas l'autorisation de modifier la destination d'une ligne de transport." };
+  }
+  return { isAuthorized: true, message: "" };
+}
diff --git a/admin/src/scenes/plan-transport/ligne-bus/View/components/Centre.jsx b/admin/src/scenes/plan-transport/ligne-bus/View/components/Centre.jsx
deleted file mode 100644
index af8905c5c0..0000000000
--- a/admin/src/scenes/plan-transport/ligne-bus/View/components/Centre.jsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import React from "react";
-import { useSelector } from "react-redux";
-import { toastr } from "react-redux-toastr";
-import { Link } from "react-router-dom";
-import { canEditLigneBusCenter, isBusEditionOpen, ROLES, translate } from "snu-lib";
-import ExternalLink from "../../../../../assets/icons/ExternalLink";
-import Pencil from "../../../../../assets/icons/Pencil";
-import { capture } from "../../../../../sentry";
-import api from "../../../../../services/api";
-import Field from "../../components/Field";
-import Iceberg from "../../components/Icons/Iceberg";
-
-export default function Centre({ bus, setBus, cohort }) {
-  const user = useSelector((state) => state.Auth.user);
-  const [editCenter, setEditCenter] = React.useState(false);
-  const [isLoading, setIsLoading] = React.useState(false);
-  const [errors, setErrors] = React.useState({});
-  const [data, setData] = React.useState({
-    centerArrivalTime: bus.centerArrivalTime || "",
-    centerDepartureTime: bus.centerDepartureTime || "",
-  });
-
-  React.useEffect(() => {
-    setData({
-      centerArrivalTime: bus.centerArrivalTime || "",
-      centerDepartureTime: bus.centerDepartureTime || "",
-    });
-    setErrors({});
-  }, [editCenter]);
-
-  const onSubmitInfo = async () => {
-    try {
-      setIsLoading(true);
-      setErrors({});
-      let errors = {};
-
-      const regex = new RegExp("^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$");
-      if (!regex.test(data.centerArrivalTime)) errors.centerArrivalTime = "Format invalide (hh:mm)";
-      if (!regex.test(data.centerDepartureTime)) errors.centerDepartureTime = "Format invalide (hh:mm)";
-
-      if (Object.keys(errors).length > 0) {
-        setErrors(errors);
-        setIsLoading(false);
-        return;
-      }
-      // Save data
-      const { ok, code, data: ligneInfo } = await api.put(`/ligne-de-bus/${bus._id}/centre`, data);
-
-      if (!ok) {
-        toastr.error("Oups, une erreur est survenue lors de la modification des informations du centre", translate(code));
-        return setIsLoading(false);
-      }
-      setBus(ligneInfo);
-      setEditCenter(false);
-      setIsLoading(false);
-    } catch (e) {
-      capture(e);
-      toastr.error("Oups, une erreur est survenue lors de la modification des informations du centre");
-      setIsLoading(false);
-    }
-  };
-  return (
-    <div className="w-1/2 rounded-xl bg-white p-8">
-      <div className="flex items-center justify-between">
-        <div className="text-lg leading-7 text-gray-900 font-bold">Centre de cohésion</div>
-        {canEditLigneBusCenter(user) || isBusEditionOpen(user, cohort) ? (
-          <>
-            {!editCenter ? (
-              <button
-                className="flex cursor-pointer items-center gap-2 rounded-full border-[1px] border-blue-100 bg-blue-100 px-3 py-2 text-xs leading-5 text-blue-600 hover:border-blue-600 disabled:cursor-not-allowed disabled:opacity-50"
-                onClick={() => setEditCenter(true)}
-                disabled={isLoading}>
-                <Pencil stroke="#2563EB" className="h-[12px] w-[12px]" />
-                Modifier
-              </button>
-            ) : (
-              <div className="flex items-center gap-2">
-                <button
-                  className="flex cursor-pointer items-center gap-2 rounded-full border-[1px] border-gray-100 bg-gray-100 px-3 py-2 text-xs leading-5 text-gray-700 hover:border-gray-700 disabled:cursor-not-allowed disabled:opacity-50"
-                  onClick={() => setEditCenter(false)}
-                  disabled={isLoading}>
-                  Annuler
-                </button>
-                <button
-                  className="flex cursor-pointer items-center gap-2 rounded-full  border-[1px] border-blue-100 bg-blue-100 px-3 py-2 text-xs leading-5 text-blue-600 hover:border-blue-600 disabled:cursor-not-allowed disabled:opacity-50"
-                  onClick={onSubmitInfo}
-                  disabled={isLoading}>
-                  <Pencil stroke="#2563EB" className="mr-[6px] h-[12px] w-[12px]" />
-                  Enregistrer les changements
-                </button>
-              </div>
-            )}
-          </>
-        ) : null}
-      </div>
-      <div className="mt-8 flex flex-col">
-        <div className="flex flex-col">
-          <div className="flex items-center gap-2">
-            <p className="text-[15px] font-medium leading-6 text-[#242526]">
-              {bus.centerDetail.department} • {bus.centerDetail.region}
-            </p>
-            {user.role !== ROLES.TRANSPORTER && (
-              <Link
-                to={`/centre/${bus.centerDetail._id}`}
-                onClick={(e) => {
-                  e.stopPropagation();
-                }}>
-                <ExternalLink className="text-[#9CA3AF]" />
-              </Link>
-            )}
-          </div>
-          <p className="text-xs font-light leading-4 text-[#738297]">{bus.centerDetail.name}</p>
-          <p className="text-xs font-light leading-4 text-[#738297]">
-            {bus.centerDetail.address}, {bus.centerDetail.zip}, {bus.centerDetail.city}
-          </p>
-        </div>
-        <div>{/* TODO V2 modif de centre de destination */}</div>
-        <div className="mt-8 flex items-center gap-4">
-          <Field
-            label="Heure d’arrivée"
-            onChange={(e) => setData({ ...data, centerArrivalTime: e.target.value })}
-            placeholder="hh:mm"
-            value={data.centerArrivalTime}
-            error={errors?.centerArrivalTime}
-            readOnly={!editCenter}
-          />
-          <Field
-            label="Heure de départ"
-            onChange={(e) => setData({ ...data, centerDepartureTime: e.target.value })}
-            placeholder="hh:mm"
-            value={data.centerDepartureTime}
-            error={errors?.centerDepartureTime}
-            readOnly={!editCenter}
-          />
-        </div>
-        <div className="mt-8 flex justify-end">
-          <Iceberg />
-        </div>
-      </div>
-    </div>
-  );
-}
diff --git a/admin/src/scenes/plan-transport/ligne-bus/View/components/Centre/CentreLabel.tsx b/admin/src/scenes/plan-transport/ligne-bus/View/components/Centre/CentreLabel.tsx
new file mode 100644
index 0000000000..b5cee38a6d
--- /dev/null
+++ b/admin/src/scenes/plan-transport/ligne-bus/View/components/Centre/CentreLabel.tsx
@@ -0,0 +1,39 @@
+import React from "react";
+import { Link } from "react-router-dom";
+import { CenterDetailDto, CohesionCenterType } from "snu-lib";
+import ExternalLink from "@/assets/icons/ExternalLink";
+
+interface CentreLabelProps {
+  centre: Partial<CenterDetailDto | CohesionCenterType>;
+  showLink?: boolean;
+  className?: string;
+}
+
+const CentreLabel: React.FC<CentreLabelProps> = ({ centre, showLink = false, className = "" }) => {
+  return (
+    <div className={className}>
+      <div className="flex items-center gap-2">
+        <p className="text-[15px] font-medium leading-6 text-gray-800]">
+          {centre.department} • {centre.region}
+        </p>
+        {showLink && (
+          <Link
+            to={`/point-de-rassemblement/${centre._id}`}
+            onClick={(e) => {
+              e.stopPropagation();
+            }}>
+            <ExternalLink className="text-[#9CA3AF]" />
+          </Link>
+        )}
+      </div>
+      <div className="text-left">
+        <p className="text-xs font-light leading-4 text-gray-500">{centre.name}</p>
+        <p className="text-xs font-light leading-4 text-gray-500">
+          {centre.address}, {centre.zip}, {centre.city}
+        </p>
+      </div>
+    </div>
+  );
+};
+
+export default CentreLabel;
diff --git a/admin/src/scenes/plan-transport/ligne-bus/View/components/Centre/CentreModal.tsx b/admin/src/scenes/plan-transport/ligne-bus/View/components/Centre/CentreModal.tsx
new file mode 100644
index 0000000000..fa88a29aaa
--- /dev/null
+++ b/admin/src/scenes/plan-transport/ligne-bus/View/components/Centre/CentreModal.tsx
@@ -0,0 +1,118 @@
+import React from "react";
+import { Button, Modal } from "@snu/ds/admin";
+import XCircleFull from "@/assets/icons/XCircleFull";
+import CheckCircleFull from "@/assets/icons/CheckCircleFull";
+import { useToggle } from "react-use";
+import { LigneBusDto, SENDINBLUE_TEMPLATES } from "snu-lib";
+import CentreLabel from "./CentreLabel";
+import { TfiReload } from "react-icons/tfi";
+import { HiExternalLink } from "react-icons/hi";
+import useSessions from "@/scenes/plan-transport/lib/useSessionsByCohort";
+import { CentreFormValues } from ".";
+
+interface ConfirmChangesModalProps {
+  isOpen: boolean;
+  count: number;
+  initialData: LigneBusDto;
+  formData: { sessionId: string; centerArrivalTime: string; centerDepartureTime: string };
+  onConfirm: (data: CentreFormValues, emailing: boolean) => void;
+  onCancel: () => void;
+  isLoading?: boolean;
+}
+
+export default function CentreModal({ isOpen, initialData, formData, count, onConfirm, onCancel, isLoading }: ConfirmChangesModalProps) {
+  const [isEmailing, toggleEmailing] = useToggle(false);
+  const templateId = SENDINBLUE_TEMPLATES.young.PHASE_1_CHANGEMENT_CENTRE;
+  const templateIdReferent = SENDINBLUE_TEMPLATES.CLE.PHASE_1_MODIFICATION_LIGNE;
+  const isCLE = initialData.cohort.includes("CLE");
+  const { data: sessions } = useSessions(initialData.cohort);
+  const initialSession = sessions?.find((s) => s._id === initialData.sessionId);
+  const newSession = sessions?.find((s) => s._id === formData.sessionId);
+
+  const handleConfirm = () => {
+    onConfirm(formData, isEmailing);
+  };
+
+  return (
+    <Modal
+      isOpen={isOpen}
+      onClose={onCancel}
+      className="max-w-3xl"
+      content={
+        <div className="scroll-y-auto overflow-y-auto">
+          <div className="flex flex-col items-center text-center gap-6">
+            <div className="flex items-center justify-center w-12 h-12 rounded-full bg-gray-50">
+              <TfiReload className="w-5 h-5 text-gray-800" />
+            </div>
+            <h1 className="font-medium text-xl max-w-sm">Êtes-vous sûr(e) de vouloir changer de centre de cohésion&nbsp;?</h1>
+          </div>
+
+          <div className="mt-8 grid grid-cols-2 gap-3">
+            <div>
+              <p className="text-xs font-medium">Ancien centre</p>
+              <CenterCard
+                centerDetail={initialSession?.cohesionCenter}
+                centerArrivalTime={initialData.centerArrivalTime}
+                centerDepartureTime={initialData.centerDepartureTime}
+                icon={<XCircleFull className="text-red-500" />}
+              />
+            </div>
+
+            <div>
+              <p className="text-xs font-medium">Nouveau centre</p>
+              <CenterCard
+                centerDetail={newSession?.cohesionCenter}
+                centerArrivalTime={formData.centerArrivalTime!}
+                centerDepartureTime={formData.centerDepartureTime!}
+                icon={<CheckCircleFull className="text-green-500" />}
+              />
+            </div>
+          </div>
+
+          <div className="mt-8 border-t border-gray-200 w-full pt-8">
+            <label className="flex items-start space-x-2">
+              <input type="checkbox" className="mt-1" checked={isEmailing} onChange={toggleEmailing} />
+              <div className="leading-relaxed">
+                <p>Envoyer une campagne d’emailing aux volontaires ({count}) et à leurs représentants légaux.</p>
+                {isCLE && <p>Les référent de classe, chef d’établissement et coordinateurs seront également prévenus.</p>}
+                <div className="mt-2">
+                  <a href={`/email-preview/${templateId}`} target="_blank" rel="noreferrer" className="text-gray-500 hover:text-gray-800 underline underline-offset-2">
+                    Visualiser l'aperçu pour les volontaires
+                    <HiExternalLink className="inline-block w-4 h-4 ml-1" />
+                  </a>
+                  <br />
+                  {isCLE && (
+                    <a href={`/email-preview/${templateIdReferent}`} target="_blank" rel="noreferrer" className="text-gray-500 hover:text-gray-800 underline underline-offset-2">
+                      Visualiser l'aperçu pour les référents CLE
+                      <HiExternalLink className="inline-block w-4 h-4 ml-1" />
+                    </a>
+                  )}
+                </div>
+              </div>
+            </label>
+          </div>
+          <br />
+        </div>
+      }
+      footer={
+        <div className="grid grid-cols-2 gap-3">
+          <Button title="Annuler" type="secondary" onClick={onCancel} className="w-full" />
+          <Button title="Oui, valider" loading={isLoading} onClick={handleConfirm} className="w-full" />
+        </div>
+      }
+    />
+  );
+}
+
+function CenterCard({ centerDetail, centerArrivalTime, centerDepartureTime, icon }) {
+  return (
+    <div className="mt-2 bg-gray-50 p-3 rounded-xl">
+      <div className="mb-2">{icon}</div>
+      <CentreLabel centre={centerDetail} showLink={false} />
+      <div className="mt-2 font-light text-xs text-gray-500">
+        <p>Heure d'arrivée : {centerArrivalTime}</p>
+        <p>Heure de départ : {centerDepartureTime}</p>
+      </div>
+    </div>
+  );
+}
diff --git a/admin/src/scenes/plan-transport/ligne-bus/View/components/Centre/SessionSelector.tsx b/admin/src/scenes/plan-transport/ligne-bus/View/components/Centre/SessionSelector.tsx
new file mode 100644
index 0000000000..df50d38350
--- /dev/null
+++ b/admin/src/scenes/plan-transport/ligne-bus/View/components/Centre/SessionSelector.tsx
@@ -0,0 +1,119 @@
+import { AuthState } from "@/redux/auth/reducer";
+import React from "react";
+import { useSelector } from "react-redux";
+import CentreLabel from "./CentreLabel";
+import { isSuperAdmin, LigneBusDto, ROLES } from "snu-lib";
+import { BsSearch } from "react-icons/bs";
+import useSessions from "@/scenes/plan-transport/lib/useSessionsByCohort";
+import { AiOutlineStop } from "react-icons/ai";
+import Loader from "@/components/Loader";
+import { SessionPhase1PopulatedWithCenter } from "@/services/sessionPhase1Service";
+
+type Props = {
+  sessionId: string;
+  setSessionId: (string) => void;
+  ligne: LigneBusDto;
+  disabled: boolean;
+};
+
+function filterResults(data: SessionPhase1PopulatedWithCenter[], search: string) {
+  if (search === "") return data;
+  const searchFields = ["name", "address", "region", "department", "code", "city", "zip", "matricule"];
+  return data.filter((item) => {
+    return searchFields.some((field) => item.cohesionCenter[field]?.toString().toLowerCase().includes(search.toLowerCase()));
+  });
+}
+
+export default function SessionSelector({ sessionId, setSessionId, ligne, disabled }: Props) {
+  const user = useSelector((state: AuthState) => state.Auth.user);
+  const [search, setSearch] = React.useState("");
+  const { data: sessions, isPending, isError } = useSessions(ligne.cohort);
+  const [open, setOpen] = React.useState(false);
+  const refSelect = React.useRef<HTMLDivElement>(null);
+  const refInput = React.useRef<HTMLInputElement>(null);
+  const refButtonChangesPDR = React.useRef<HTMLButtonElement>(null);
+
+  React.useEffect(() => {
+    const handleClickOutside = (event) => {
+      if (refButtonChangesPDR.current && refButtonChangesPDR.current.contains(event.target)) {
+        setOpen((open) => !open);
+      } else if (refSelect.current && !refSelect.current.contains(event.target)) {
+        setOpen(false);
+      }
+    };
+    document.addEventListener("click", handleClickOutside, true);
+    return () => {
+      document.removeEventListener("click", handleClickOutside, true);
+    };
+  }, []);
+
+  if (isPending) return <Loader />;
+  if (isError) return <div>Erreur</div>;
+
+  const selectedSession = sessions?.find((s) => s._id === sessionId);
+  const options = sessions.filter((s) => s._id !== sessionId);
+  const filteredOptions = filterResults(options, search);
+
+  function handleSelect(session: SessionPhase1PopulatedWithCenter) {
+    setSessionId(session._id);
+    setOpen(false);
+  }
+
+  return (
+    <>
+      <div className="flex flex-col border border-gray-300 rounded-lg py-2 px-2.5">
+        <div className="flex justify-between">
+          <CentreLabel centre={selectedSession!.cohesionCenter} showLink={user.role !== ROLES.TRANSPORTER} />
+          {isSuperAdmin(user) && !disabled && (
+            <button type="button" ref={refButtonChangesPDR} className="text-xs font-normal leading-6 text-blue-500">
+              Changer de lieu
+            </button>
+          )}
+        </div>
+      </div>
+
+      {open && (
+        <div className="relative">
+          <div ref={refSelect} className="max-h-[500px] overflow-y-auto absolute left-0 z-50 w-full rounded-lg border border-gray-300 bg-white px-3 shadow-lg">
+            <div className="sticky top-0 z-10 bg-white pt-3">
+              <div className="flex flex-row items-center gap-2">
+                <BsSearch className="text-gray-400" />
+                <input
+                  ref={refInput}
+                  type="text"
+                  placeholder="Rechercher un centre"
+                  onChange={(e) => setSearch(e.target.value)}
+                  className="w-full text-[13px] leading-3 text-gray-800"
+                />
+              </div>
+              <hr className="my-2" />
+            </div>
+            <div className="pb-2">
+              {filteredOptions.length === 0 ? (
+                <div className="text-xs leading-4 text-gray-900">Aucun centre de cohésion trouvé</div>
+              ) : (
+                filteredOptions.map((session) => <CenterButton key={session._id} session={session} ligne={ligne} onClick={() => handleSelect(session)} />)
+              )}
+            </div>
+          </div>
+        </div>
+      )}
+    </>
+  );
+}
+
+function CenterButton({ session, ligne, onClick }) {
+  const disabled = session.placesLeft === 0 || session.placesLeft < ligne.youngSeatsTaken;
+  return (
+    <>
+      <button type="button" onClick={onClick} disabled={disabled} className="my-2 w-full gap-4 rounded-lg py-1 px-2 hover:bg-gray-50 disabled:cursor-not-allowed group">
+        <CentreLabel centre={session.cohesionCenter} showLink={false} className={disabled ? "text-gray-400" : ""} />
+        <p className="mt-1 text-xs text-gray-600 text-left group-disabled:text-gray-400">
+          {disabled && <AiOutlineStop className="inline-block mr-1 mb-1 text-gray-400" />}
+          {session.placesLeft} places restantes
+        </p>
+      </button>
+      <hr />
+    </>
+  );
+}
diff --git a/admin/src/scenes/plan-transport/ligne-bus/View/components/Centre/index.tsx b/admin/src/scenes/plan-transport/ligne-bus/View/components/Centre/index.tsx
new file mode 100644
index 0000000000..25d14bce61
--- /dev/null
+++ b/admin/src/scenes/plan-transport/ligne-bus/View/components/Centre/index.tsx
@@ -0,0 +1,112 @@
+import React, { useState } from "react";
+import { useSelector } from "react-redux";
+import { CohortType, LigneBusDto } from "snu-lib";
+import Field from "../../../components/Field";
+import Iceberg from "../../../components/Icons/Iceberg";
+import { AuthState } from "@/redux/auth/reducer";
+import SessionSelector from "./SessionSelector";
+import useUpdateSessionSurLigneDeBus from "@/scenes/plan-transport/lib/useUpdateSessionSurLigneDeBus";
+import CentreModal from "./CentreModal";
+import { Controller, SubmitHandler, useForm } from "react-hook-form";
+import { EditButton } from "@snu/ds/admin";
+import { getAuthorizationToUpdateCenterOnLine } from "../../authorization";
+
+type Props = {
+  bus: LigneBusDto;
+  setBus: (data: LigneBusDto) => void;
+  cohort?: CohortType | null;
+};
+
+export type CentreFormValues = {
+  centerArrivalTime: string;
+  centerDepartureTime: string;
+  sessionId: string;
+};
+
+const pattern = new RegExp("^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$");
+
+export default function Centre({ bus, setBus, cohort }: Props) {
+  const user = useSelector((state: AuthState) => state.Auth.user);
+  const { mutate: updateSessionSurLigneDeBus, isPending } = useUpdateSessionSurLigneDeBus(bus._id);
+  const [isEditing, setIsEditing] = useState(false);
+  const [openModal, setOpenModal] = useState(false);
+  const { control, handleSubmit, reset, getValues } = useForm({
+    defaultValues: {
+      centerArrivalTime: bus.centerArrivalTime,
+      centerDepartureTime: bus.centerDepartureTime,
+      sessionId: bus.sessionId,
+    },
+  });
+
+  const { isAuthorized, message } = getAuthorizationToUpdateCenterOnLine(user, cohort);
+
+  const onSubmit: SubmitHandler<CentreFormValues> = (data) => {
+    if (data.sessionId !== bus.sessionId) {
+      setOpenModal(true);
+    } else {
+      handleConfirm(data, false);
+    }
+  };
+
+  function handleConfirm(data: CentreFormValues, sendCampaign?: boolean) {
+    const payload = { ...data, sendCampaign };
+    updateSessionSurLigneDeBus(payload, {
+      onSuccess: (ligneInfo) => {
+        setOpenModal(false);
+        setBus(ligneInfo);
+        setIsEditing(false);
+      },
+    });
+  }
+
+  return (
+    <form onSubmit={handleSubmit(onSubmit)} className="w-1/2 rounded-xl bg-white p-8">
+      <div className="flex items-center justify-between">
+        <p className="text-lg leading-7 text-gray-900 font-bold">Centre de cohésion</p>
+        <EditButton isEditing={isEditing} setIsEditing={setIsEditing} disabled={!isAuthorized} onReset={reset} tooltipMessage={message} isLoading={isPending} />
+      </div>
+
+      <div className="mt-8 grid grid-cols-2 gap-x-4 gap-y-8">
+        <Controller
+          name="sessionId"
+          control={control}
+          render={({ field }) => (
+            <div className="col-span-2">
+              <SessionSelector sessionId={field.value} setSessionId={field.onChange} ligne={bus} disabled={!isEditing} />
+            </div>
+          )}
+        />
+        <Controller
+          name="centerArrivalTime"
+          control={control}
+          rules={{ required: isEditing && "L'heure d'arrivée est requise", pattern: { value: pattern, message: "Format attendu: hh:mm" } }}
+          render={({ field, fieldState }) => (
+            <Field label="Heure d’arrivée" value={field.value} onChange={field.onChange} placeholder="hh:mm" readOnly={!isEditing} error={fieldState.error?.message} />
+          )}
+        />
+        <Controller
+          name="centerDepartureTime"
+          control={control}
+          rules={{ required: isEditing && "L'heure de départ est requise", pattern: { value: pattern, message: "Format attendu: hh:mm" } }}
+          render={({ field, fieldState }) => (
+            <Field label="Heure de départ" value={field.value} onChange={field.onChange} placeholder="hh:mm" readOnly={!isEditing} error={fieldState.error?.message} />
+          )}
+        />
+      </div>
+
+      <div className="mt-8 flex justify-end">
+        <Iceberg />
+      </div>
+
+      <CentreModal
+        isOpen={openModal}
+        onCancel={() => setOpenModal(false)}
+        onConfirm={handleConfirm}
+        initialData={bus}
+        formData={getValues()}
+        count={bus.youngSeatsTaken}
+        isLoading={isPending}
+      />
+    </form>
+  );
+}
diff --git a/admin/src/scenes/plan-transport/ligne-bus/View/components/ConfirmChangesModal.tsx b/admin/src/scenes/plan-transport/ligne-bus/View/components/ConfirmChangesModal.tsx
index 6863f3e82f..71f79e90eb 100644
--- a/admin/src/scenes/plan-transport/ligne-bus/View/components/ConfirmChangesModal.tsx
+++ b/admin/src/scenes/plan-transport/ligne-bus/View/components/ConfirmChangesModal.tsx
@@ -13,11 +13,12 @@ interface ConfirmChangesModalProps {
   youngsCount: number;
   beforeChangeFormData: PointDeRassemblementFormValueChanges;
   afterChangeFormData: PointDeRassemblementFormValueChanges;
+  loading: boolean;
   onConfirm: (emailing: boolean) => void;
   onCancel: () => void;
 }
 
-export default function ConfirmChangesModal({ isOpen, cohort, beforeChangeFormData, afterChangeFormData, onConfirm, onCancel, youngsCount }: ConfirmChangesModalProps) {
+export default function ConfirmChangesModal({ isOpen, cohort, beforeChangeFormData, afterChangeFormData, onConfirm, onCancel, youngsCount, loading }: ConfirmChangesModalProps) {
   const [isEmailing, toggleEmailing] = useToggle(false);
 
   const isBeforeDeparture = cohort ? new Date() < new Date(cohort.dateStart) : false;
@@ -33,7 +34,7 @@ export default function ConfirmChangesModal({ isOpen, cohort, beforeChangeFormDa
     templateId = SENDINBLUE_TEMPLATES.young.CHANGE_PDR_BEFORE_RETURN;
   }
 
-  const templateIdForCLE = SENDINBLUE_TEMPLATES.CLE.PHASE_1_CHANGEMENT_PDR;
+  const templateIdForCLE = SENDINBLUE_TEMPLATES.CLE.PHASE_1_MODIFICATION_LIGNE;
 
   const handleConfirm = () => {
     onConfirm(isEmailing);
@@ -156,7 +157,7 @@ export default function ConfirmChangesModal({ isOpen, cohort, beforeChangeFormDa
       footer={
         <div className="flex items-center justify-between gap-6">
           <Button title="Annuler" type="secondary" className="flex-1 justify-center" onClick={onCancel} />
-          <Button title="Oui, valider" onClick={handleConfirm} className="flex-1" />
+          <Button title="Oui, valider" onClick={handleConfirm} className="flex-1" loading={loading} />
         </div>
       }
     />
diff --git a/admin/src/scenes/plan-transport/ligne-bus/View/components/PointDeRassemblement.tsx b/admin/src/scenes/plan-transport/ligne-bus/View/components/PointDeRassemblement.tsx
index ed2ce06e53..0655b24753 100644
--- a/admin/src/scenes/plan-transport/ligne-bus/View/components/PointDeRassemblement.tsx
+++ b/admin/src/scenes/plan-transport/ligne-bus/View/components/PointDeRassemblement.tsx
@@ -5,22 +5,22 @@ import { BsSearch } from "react-icons/bs";
 import { toastr } from "react-redux-toastr";
 import { useToggle } from "react-use";
 
-import { canEditLigneBusPointDeRassemblement, CohortType, isBusEditionOpen, isSuperAdmin, LigneBusDto, PointDeRassemblementType, ROLES, translate } from "snu-lib";
+import { CohortType, isSuperAdmin, LigneBusDto, PointDeRassemblementType, ROLES, translate } from "snu-lib";
 
 import { AuthState } from "@/redux/auth/reducer";
 import api from "@/services/api";
-import Pencil from "@/assets/icons/Pencil";
 import { capture } from "@/sentry";
 import Loader from "@/components/Loader";
 
 import Select from "../../components/Select";
 import Field from "../../components/Field";
 import PDRIcon from "../../components/Icons/PDR";
-import { Button } from "@snu/ds/admin";
+import { Button, EditButton } from "@snu/ds/admin";
 import { checkTime } from "snu-lib";
 
 import PointDeRassemblementLabel from "./PointDeRassemblementLabel";
 import ConfirmChangesModal from "./ConfirmChangesModal";
+import { getAuthorizationToUpdatePdrOnLine } from "../authorization";
 
 const options = [
   { label: "Bus", value: "bus" },
@@ -28,8 +28,6 @@ const options = [
   { label: "Avion", value: "avion" },
 ];
 
-const keys = ["code", "name", "city", "zip", "department", "region"];
-
 interface FormErrors {
   meetingHour?: string;
   busArrivalHour?: string;
@@ -195,9 +193,13 @@ export default function PointDeRassemblement({ bus, onBusChange, index, pdr, vol
     }
   };
 
-  const handleConfirmChangesPDR = useCallback(async () => {
-    toggleConfirmChangesModal(true);
-  }, [toggleConfirmChangesModal]);
+  const handleConfirmChangesPDR = useCallback(
+    async (e) => {
+      e.preventDefault();
+      toggleConfirmChangesModal(true);
+    },
+    [toggleConfirmChangesModal],
+  );
 
   if (!volume) {
     return (
@@ -207,48 +209,24 @@ export default function PointDeRassemblement({ bus, onBusChange, index, pdr, vol
     );
   }
 
+  const { isAuthorized, message } = getAuthorizationToUpdatePdrOnLine(user, cohort);
+
   return (
     <>
-      <div className="w-full rounded-xl bg-white p-8">
+      <form onSubmit={handleConfirmChangesPDR} className="w-full rounded-xl bg-white p-8">
         <div className="relative flex items-start justify-between flex-wrap-reverse gap-y-4">
           <div className="flex items-center gap-4">
             <div className="text-xl leading-6 text-[#242526]">Point de rassemblement</div>
             <div className="flex h-7 w-7 items-center justify-center rounded-full bg-gray-200 text-sm">{index}</div>
           </div>
-          {canEditLigneBusPointDeRassemblement(user) || isBusEditionOpen(user, cohort) ? (
-            !editPdr ? (
-              <button
-                className="flex cursor-pointer items-center gap-2 rounded-full border-[1px] border-blue-100 bg-blue-100 px-3 py-2 text-xs leading-5 text-blue-600 hover:border-blue-600 disabled:cursor-not-allowed disabled:opacity-50"
-                onClick={() => setEditPdr(true)}
-                disabled={isLoading}>
-                <Pencil stroke="#2563EB" className="h-[12px] w-[12px]" />
-                Modifier
-              </button>
-            ) : (
-              <div className="flex flex-rows items-end justify-end gap-2 flex-row-reverse ml-auto">
-                <button
-                  className="flex cursor-pointer items-center gap-2 rounded-full  border-[1px] border-blue-100 bg-blue-100 px-3 py-2 text-xs leading-5 text-blue-600 hover:border-blue-600 disabled:cursor-not-allowed disabled:opacity-50"
-                  onClick={handleConfirmChangesPDR}
-                  disabled={isLoading}>
-                  <Pencil stroke="#2563EB" className="mr-[6px] h-[12px] w-[12px]" />
-                  Enregistrer les changements
-                </button>
-                <button
-                  className="flex cursor-pointer items-center gap-2 rounded-full border-[1px] border-gray-100 bg-gray-100 px-3 py-2 text-xs leading-5 text-gray-700 hover:border-gray-700 disabled:cursor-not-allowed disabled:opacity-50"
-                  onClick={handleCancelChanges}
-                  disabled={isLoading}>
-                  Annuler
-                </button>
-              </div>
-            )
-          ) : null}
+          <EditButton isEditing={editPdr} setIsEditing={setEditPdr} isLoading={isLoading} onReset={handleCancelChanges} disabled={!isAuthorized} tooltipMessage={message} />
         </div>
         <div className="mt-8 flex flex-col">
           <div className="flex flex-col border border-gray-300 rounded-lg py-2 px-2.5">
             <div className="flex justify-between flex-row items-center">
               <PointDeRassemblementLabel pdr={selectedPDR} showLink={user.role !== ROLES.TRANSPORTER} />
               {isSuperAdmin(user) && editPdr && (
-                <button ref={refButtonChangesPDR} className="text-xs font-normal leading-6 text-blue-500">
+                <button type="button" ref={refButtonChangesPDR} className="text-xs font-normal leading-6 text-blue-500">
                   Changer de lieu
                 </button>
               )}
@@ -362,7 +340,7 @@ export default function PointDeRassemblement({ bus, onBusChange, index, pdr, vol
             <PDRIcon />
           </div>
         </div>
-      </div>
+      </form>
       <ConfirmChangesModal
         isOpen={showConfirmChangesModal}
         onCancel={handleCancelChanges}
@@ -378,6 +356,7 @@ export default function PointDeRassemblement({ bus, onBusChange, index, pdr, vol
           returnHour: data.returnHour || "",
         }}
         youngsCount={youngsCount}
+        loading={isLoading}
       />
     </>
   );
diff --git a/admin/src/services/sessionPhase1Service.ts b/admin/src/services/sessionPhase1Service.ts
new file mode 100644
index 0000000000..2756ae1943
--- /dev/null
+++ b/admin/src/services/sessionPhase1Service.ts
@@ -0,0 +1,20 @@
+import API from "@/services/api";
+import { CohesionCenterType, LigneBusDto, LigneBusType, SessionPhase1Type } from "snu-lib";
+
+export type SessionPhase1PopulatedWithCenter = SessionPhase1Type & { cohesionCenter: CohesionCenterType };
+
+const SessionPhase1Service = {
+  async getSessions(filters: Record<string, string[]>): Promise<SessionPhase1PopulatedWithCenter[]> {
+    const body = { filters, size: 100 };
+    const { responses } = await API.post("/elasticsearch/sessionphase1/search?needCohesionCenterInfo=true", body);
+    return responses[0].hits.hits.map((hit) => ({ _id: hit._id, ...hit._source }));
+  },
+  async updateSessionSurLigneDeBus(busId: string, payload: Partial<LigneBusType>): Promise<LigneBusDto> {
+    const { ok, code, data } = await API.put(`/ligne-de-bus/${busId}/centre`, payload);
+    if (!ok) throw new Error(code);
+    if (!data) throw new Error("No data returned");
+    return data;
+  },
+};
+
+export { SessionPhase1Service };
diff --git a/api/src/__tests__/ligne-de-bus.test.ts b/api/src/__tests__/ligne-de-bus.test.ts
index 9fbb212363..ab9ea38587 100644
--- a/api/src/__tests__/ligne-de-bus.test.ts
+++ b/api/src/__tests__/ligne-de-bus.test.ts
@@ -2,23 +2,40 @@ import { Types } from "mongoose";
 import request from "supertest";
 import getAppHelper, { resetAppAuth } from "./helpers/app";
 import { mockEsClient } from "./helpers/es";
-import { SchemaDeRepartitionModel, LigneToPointModel, CohesionCenterModel, YoungModel, CohortModel, LigneBusModel, PlanTransportModel, PointDeRassemblementModel } from "../models";
-import { dbConnect, dbClose } from "./helpers/db";
+import {
+  SchemaDeRepartitionModel,
+  LigneToPointModel,
+  CohesionCenterModel,
+  YoungModel,
+  CohortModel,
+  LigneBusModel,
+  PlanTransportModel,
+  PointDeRassemblementModel,
+  SessionPhase1Model,
+} from "../models";
+import { dbConnect, dbClose, mockTransaction } from "./helpers/db";
 import { createYoungHelper } from "./helpers/young";
 import getNewYoungFixture from "./fixtures/young";
 import { createPointDeRassemblementWithBus } from "./helpers/PlanDeTransport/pointDeRassemblement";
 import getNewPointDeRassemblementFixture from "./fixtures/PlanDeTransport/pointDeRassemblement";
 import getNewLigneBusFixture from "./fixtures/PlanDeTransport/ligneBus";
 import getBusTeamFixture from "./fixtures/busTeam";
-import { updatePDRForLine } from "../services/LigneDeBusService";
 import { ERRORS, ROLES } from "snu-lib";
 import { sendTemplate } from "../brevo";
+import getNewLigneToPointFixture from "./fixtures/PlanDeTransport/ligneToPoint";
+import { getNewSessionPhase1Fixture } from "./fixtures/sessionPhase1";
+import { getNewCohesionCenterFixture } from "./fixtures/cohesionCenter";
+import getNewCohortFixture from "./fixtures/cohort";
+import getPlanDeTransportFixture from "./fixtures/PlanDeTransport/planDeTransport";
+import * as ligneDeBusService from "../planDeTransport/ligneDeBus/ligneDeBusService";
 
 const ObjectId = Types.ObjectId;
 
 mockEsClient({
   lignebus: [{ _id: "ligneId" }],
 });
+// disable transaction for this test
+mockTransaction();
 
 const mockModelMethodWithError = (model, method) => {
   jest.spyOn(model, method).mockImplementation(() => {
@@ -26,7 +43,10 @@ const mockModelMethodWithError = (model, method) => {
   });
 };
 
-jest.mock("../services/LigneDeBusService");
+// jest.mock("../planDeTransport/ligneDeBus/ligneDeBusService", () => ({
+//   updatePDRForLine: jest.fn(),
+// }));
+
 jest.mock("../brevo", () => ({
   sendTemplate: jest.fn(),
 }));
@@ -162,6 +182,7 @@ describe("LigneDeBus", () => {
 
       expect(res.status).toBe(200);
       expect(res.body.ok).toBe(true);
+      expect(res.body.data).toBeDefined();
       // expect(res.body.data.name).toBe(ligneBus.name);
       expect(res.body.data.centerDepartureTime).toBe(ligneBus.centerDepartureTime);
       expect(res.body.data.centerArrivalTime).toBe(ligneBus.centerArrivalTime);
@@ -647,20 +668,60 @@ describe("LigneDeBus", () => {
   });
 
   describe("PUT /:id/updatePDRForLine", () => {
-    it("should return 403 if user is not a super admin", async () => {
-      const ligneBus = await LigneBusModel.create(getNewLigneBusFixture());
-
-      const res = await request(getAppHelper(userAdmin)).put(`/ligne-de-bus/${ligneBus._id}/updatePDRForLine`).send({
+    let cohort;
+    let center;
+    let sessionPhase1;
+    let meetingPoint1;
+    let meetingPoint2;
+    let ligneBus;
+    let ligneToPoint;
+    let planDeTransport;
+    let payload;
+
+    beforeAll(async () => {
+      cohort = await CohortModel.create(getNewCohortFixture());
+      center = await CohesionCenterModel.create(getNewCohesionCenterFixture());
+      sessionPhase1 = await SessionPhase1Model.create(getNewSessionPhase1Fixture({ cohortId: cohort._id, cohesionCenterId: center._id }));
+      meetingPoint1 = await PointDeRassemblementModel.create(getNewPointDeRassemblementFixture());
+      meetingPoint2 = await PointDeRassemblementModel.create(getNewPointDeRassemblementFixture());
+      ligneBus = await LigneBusModel.create(
+        getNewLigneBusFixture({ cohort: cohort.name, cohortId: cohort._id, centerId: center._id, sessionId: sessionPhase1._id, meetingPointsIds: [meetingPoint1._id] }),
+      );
+      ligneToPoint = await LigneToPointModel.create({ ...getNewLigneToPointFixture(), lineId: ligneBus._id, meetingPointId: meetingPoint1._id });
+      planDeTransport = await PlanTransportModel.create({
+        ...getPlanDeTransportFixture({
+          busId: ligneBus._id,
+          centerId: center._id,
+          cohortId: cohort._id,
+          cohort: cohort.name,
+        }),
+        _id: new ObjectId(ligneBus._id),
+      });
+      planDeTransport.pointDeRassemblements.push({
+        meetingPointId: meetingPoint1._id,
+        ...meetingPoint1._doc,
+        busArrivalHour: ligneToPoint.busArrivalHour,
+        meetingHour: ligneToPoint.meetingHour,
+        departureHour: ligneToPoint.departureHour,
+        returnHour: ligneToPoint.returnHour,
+        transportType: ligneToPoint.transportType,
+      });
+      await planDeTransport.save();
+      payload = {
         id: ligneBus._id,
         transportType: "bus",
         meetingHour: "08:00",
         busArrivalHour: "07:30",
         departureHour: "08:30",
         returnHour: "18:00",
-        meetingPointId: "oldMeetingPointId",
-        newMeetingPointId: "newMeetingPointId",
+        meetingPointId: meetingPoint1._id,
+        newMeetingPointId: meetingPoint2._id,
         sendEmailCampaign: false,
-      });
+      };
+    });
+
+    it("should return 403 if user is not a super admin", async () => {
+      const res = await request(getAppHelper(userAdmin)).put(`/ligne-de-bus/${ligneBus._id}/updatePDRForLine`).send(payload);
 
       expect(res.status).toBe(403);
       expect(res.body.ok).toBe(false);
@@ -668,156 +729,67 @@ describe("LigneDeBus", () => {
     });
 
     it("should return 404 if ligne bus is not found", async () => {
-      const ligneBus = await LigneBusModel.create(getNewLigneBusFixture());
-      (updatePDRForLine as jest.Mock).mockRejectedValue(new Error(ERRORS.NOT_FOUND));
-
-      const res = await request(getAppHelper(userSuperAdmin)).put(`/ligne-de-bus/${ligneBus._id}/updatePDRForLine`).send({
-        id: ligneBus._id,
-        transportType: "bus",
-        meetingHour: "08:00",
-        busArrivalHour: "07:30",
-        departureHour: "08:30",
-        returnHour: "18:00",
-        meetingPointId: "oldMeetingPointId",
-        newMeetingPointId: "newMeetingPointId",
-        sendEmailCampaign: false,
-      });
+      const fakeLigneBus = new LigneBusModel(getNewLigneBusFixture());
+      const res = await request(getAppHelper(userSuperAdmin))
+        .put(`/ligne-de-bus/${fakeLigneBus._id}/updatePDRForLine`)
+        .send({ ...payload, id: fakeLigneBus._id });
 
       expect(res.status).toBe(404);
       expect(res.body.ok).toBe(false);
       expect(res.body.code).toBe("NOT_FOUND");
     });
 
-    it("should update PDR for line successfully", async () => {
-      const ligneBus = await LigneBusModel.create(getNewLigneBusFixture());
-      (updatePDRForLine as jest.Mock).mockResolvedValue(ligneBus);
-
-      const res = await request(getAppHelper(userSuperAdmin)).put(`/ligne-de-bus/${ligneBus._id}/updatePDRForLine`).send({
-        id: ligneBus._id,
-        transportType: "bus",
-        meetingHour: "08:00",
-        busArrivalHour: "07:30",
-        departureHour: "08:30",
-        returnHour: "18:00",
-        meetingPointId: "oldMeetingPointId",
-        newMeetingPointId: "newMeetingPointId",
-        sendEmailCampaign: false,
-      });
-
-      expect(res.status).toBe(200);
-      expect(res.body.ok).toBe(true);
-      expect(res.body.data).toBeDefined();
-      expect(updatePDRForLine).toHaveBeenCalledWith(
-        ligneBus._id.toString(),
-        "bus",
-        "08:00",
-        "07:30",
-        "08:30",
-        "18:00",
-        "oldMeetingPointId",
-        "newMeetingPointId",
-        false,
-        expect.objectContaining(userSuperAdmin),
-      );
-    });
-
     it("should return 400 if meeting hour is after departure hour", async () => {
-      const ligneBus = await LigneBusModel.create(getNewLigneBusFixture());
-      (updatePDRForLine as jest.Mock).mockRejectedValue(new Error(ERRORS.INVALID_BODY));
-
-      const res = await request(getAppHelper(userSuperAdmin)).put(`/ligne-de-bus/${new ObjectId()}/updatePDRForLine`).send({
-        id: ligneBus._id,
-        transportType: "bus",
-        meetingHour: "09:00",
-        busArrivalHour: "07:30",
-        departureHour: "08:30",
-        returnHour: "18:00",
-        meetingPointId: "oldMeetingPointId",
-        newMeetingPointId: "newMeetingPointId",
-        sendEmailCampaign: false,
-      });
+      const res = await request(getAppHelper(userSuperAdmin))
+        .put(`/ligne-de-bus/${new ObjectId()}/updatePDRForLine`)
+        .send({ ...payload, meetingHour: "09:00", departureHour: "08:30" });
 
       expect(res.status).toBe(400);
       expect(res.body.ok).toBe(false);
       expect(res.body.code).toBe("INVALID_BODY");
     });
 
-    it("should return 500 when there's an error", async () => {
-      const ligneBus = await LigneBusModel.create(getNewLigneBusFixture());
-      (updatePDRForLine as jest.Mock).mockRejectedValue(new Error("Test error"));
-
-      const res = await request(getAppHelper(userSuperAdmin)).put(`/ligne-de-bus/${ligneBus._id}/updatePDRForLine`).send({
-        id: ligneBus._id,
-        transportType: "bus",
-        meetingHour: "08:00",
-        busArrivalHour: "07:30",
-        departureHour: "08:30",
-        returnHour: "18:00",
-        meetingPointId: "oldMeetingPointId",
-        newMeetingPointId: "newMeetingPointId",
-        sendEmailCampaign: false,
-      });
-
-      expect(res.status).toBe(500);
-      expect(res.body.ok).toBe(false);
-      expect(res.body.code).toBe("SERVER_ERROR");
-    });
-
     it("should return 400 if bus arrival hour is after departure hour", async () => {
-      const ligneBus = await LigneBusModel.create(getNewLigneBusFixture());
-      (updatePDRForLine as jest.Mock).mockRejectedValue(new Error(ERRORS.INVALID_BODY));
-
-      const res = await request(getAppHelper(userSuperAdmin)).put(`/ligne-de-bus/${ligneBus._id}/updatePDRForLine`).send({
-        id: ligneBus._id,
-        transportType: "bus",
-        meetingHour: "07:00",
-        busArrivalHour: "09:00",
-        departureHour: "08:30",
-        returnHour: "18:00",
-        meetingPointId: "oldMeetingPointId",
-        newMeetingPointId: "newMeetingPointId",
-        sendEmailCampaign: false,
-      });
+      const res = await request(getAppHelper(userSuperAdmin))
+        .put(`/ligne-de-bus/${ligneBus._id}/updatePDRForLine`)
+        .send({ ...payload, busArrivalHour: "09:00", departureHour: "08:30" });
 
       expect(res.status).toBe(400);
       expect(res.body.ok).toBe(false);
       expect(res.body.code).toBe("INVALID_BODY");
-      expect(updatePDRForLine).toHaveBeenCalled();
     });
 
-    it("should send email campaign when sendEmailCampaign is true", async () => {
-      const ligneBus = await LigneBusModel.create(getNewLigneBusFixture());
-      const mockUpdatedLigneBus = { ...ligneBus.toObject(), _id: ligneBus._id.toString() };
+    it("should update PDR for line successfully", async () => {
+      const res = await request(getAppHelper(userSuperAdmin)).put(`/ligne-de-bus/${ligneBus._id}/updatePDRForLine`).send(payload);
 
-      (updatePDRForLine as jest.Mock).mockResolvedValue(mockUpdatedLigneBus);
-      (sendTemplate as jest.Mock).mockResolvedValue({});
+      expect(res.status).toBe(200);
+      expect(res.body.ok).toBe(true);
+      expect(res.body.data).toBeDefined();
+      expect(res.body.data.meetingPointsIds).toStrictEqual([meetingPoint2._id.toString()]);
+    });
 
-      const res = await request(getAppHelper(userSuperAdmin)).put(`/ligne-de-bus/${ligneBus._id}/updatePDRForLine`).send({
-        id: ligneBus._id,
-        transportType: "bus",
-        meetingHour: "08:00",
-        busArrivalHour: "07:30",
-        departureHour: "08:30",
-        returnHour: "18:00",
-        meetingPointId: "oldMeetingPointId",
-        newMeetingPointId: "newMeetingPointId",
-        sendEmailCampaign: true,
-      });
+    it("should send email campaign when sendEmailCampaign is true", async () => {
+      const updatePDRForLineSpy = jest.spyOn(ligneDeBusService, "updatePDRForLine").mockResolvedValue(ligneBus);
+      (sendTemplate as jest.Mock).mockResolvedValue({});
+      const res = await request(getAppHelper(userSuperAdmin))
+        .put(`/ligne-de-bus/${ligneBus._id}/updatePDRForLine`)
+        .send({ ...payload, sendEmailCampaign: true });
 
       expect(res.status).toBe(200);
       expect(res.body.ok).toBe(true);
-      expect(updatePDRForLine).toHaveBeenCalledWith(
+      expect(updatePDRForLineSpy).toHaveBeenCalledWith(
         ligneBus._id.toString(),
         "bus",
         "08:00",
         "07:30",
         "08:30",
         "18:00",
-        "oldMeetingPointId",
-        "newMeetingPointId",
+        meetingPoint1._id.toString(),
+        meetingPoint2._id.toString(),
         true,
         expect.objectContaining(userSuperAdmin),
       );
+      updatePDRForLineSpy.mockRestore();
     });
   });
 });
diff --git a/api/src/planDeTransport/ligneDeBus/ligneDeBusController.ts b/api/src/planDeTransport/ligneDeBus/ligneDeBusController.ts
index 860f3dca65..d1bdec0376 100644
--- a/api/src/planDeTransport/ligneDeBus/ligneDeBusController.ts
+++ b/api/src/planDeTransport/ligneDeBus/ligneDeBusController.ts
@@ -30,31 +30,15 @@ import {
   SchemaDeRepartitionModel,
   ReferentModel,
   CohortModel,
+  SessionPhase1Model,
 } from "../../models";
 import { capture } from "../../sentry";
 import { sendTemplate } from "../../brevo";
 import { ERRORS } from "../../utils";
 import { validateId } from "../../utils/validator";
 import { UserRequest } from "../../controllers/request";
-import { getInfoBus } from "./ligneDeBusService";
-import { updatePDRForLine } from "../../services/LigneDeBusService";
-
-interface MeetingPointResult {
-  youngsCount: number;
-  meetingPointId: string;
-}
-
-interface Patch {
-  modelName: string;
-  date: Date;
-  ref: string;
-  refName: string;
-  op: string;
-  path: string;
-  value: any;
-  originalValue: any;
-  user: any;
-}
+import { getInfoBus, updatePDRForLine, updateSessionForLine } from "./ligneDeBusService";
+import { notifyTransporteurLineWasUpdated } from "./ligneDeBusNotificationService";
 
 const router = express.Router();
 
@@ -181,7 +165,7 @@ router.put("/:id/info", passport.authenticate("referent", { session: false, fail
 
     const infoBus = await getInfoBus(ligne);
 
-    await notifyTranporteurs(ligne, "Informations générales");
+    await notifyTransporteurLineWasUpdated(ligne, "Informations générales");
 
     return res.status(200).send({ ok: true, data: infoBus });
   } catch (error) {
@@ -247,7 +231,7 @@ router.put("/:id/team", passport.authenticate("referent", { session: false, fail
 
     const infoBus = await getInfoBus(ligne);
 
-    await notifyTranporteurs(ligne, "Équipe");
+    await notifyTransporteurLineWasUpdated(ligne, "Équipe");
 
     return res.status(200).send({ ok: true, data: infoBus });
   } catch (error) {
@@ -293,7 +277,7 @@ router.put("/:id/teamDelete", passport.authenticate("referent", { session: false
 
     const infoBus = await getInfoBus(ligne);
 
-    await notifyTranporteurs(ligne, "Équipe");
+    await notifyTransporteurLineWasUpdated(ligne, "Équipe");
 
     res.status(200).send({ ok: true, data: infoBus });
   } catch (error) {
@@ -308,11 +292,13 @@ router.put("/:id/centre", passport.authenticate("referent", { session: false, fa
       id: Joi.string().required(),
       centerArrivalTime: Joi.string().required(),
       centerDepartureTime: Joi.string().required(),
+      sessionId: Joi.string(),
+      sendCampaign: Joi.boolean(),
     }).validate({ ...req.params, ...req.body });
 
     if (error) return res.status(400).send({ ok: false, code: ERRORS.INVALID_BODY });
 
-    let { id, centerArrivalTime, centerDepartureTime } = value;
+    let { id, centerArrivalTime, centerDepartureTime, sessionId, sendCampaign } = value;
 
     const ligne = await LigneBusModel.findById(id);
     if (!ligne) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND });
@@ -345,9 +331,20 @@ router.put("/:id/centre", passport.authenticate("referent", { session: false, fa
 
     await planDeTransport.save({ fromUser: req.user });
 
+    if (sessionId && sessionId !== ligne.sessionId) {
+      const session = await SessionPhase1Model.findById(sessionId);
+      if (!session) throw new Error(ERRORS.NOT_FOUND);
+      await updateSessionForLine({
+        ligne,
+        session,
+        user: req.user,
+        sendCampaign,
+      });
+    }
+
     const infoBus = await getInfoBus(ligne);
 
-    await notifyTranporteurs(ligne, "Centre de cohésion");
+    await notifyTransporteurLineWasUpdated(ligne, "Centre de cohésion");
 
     return res.status(200).send({ ok: true, data: infoBus });
   } catch (error) {
@@ -442,7 +439,7 @@ router.put("/:id/pointDeRassemblement", passport.authenticate("referent", { sess
 
     const infoBus = await getInfoBus(ligne);
 
-    await notifyTranporteurs(ligne, "Point de rassemblement");
+    await notifyTransporteurLineWasUpdated(ligne, "Point de rassemblement");
 
     return res.status(200).send({ ok: true, data: infoBus });
   } catch (error) {
@@ -1187,22 +1184,4 @@ function mergeArrayItems(array: any[], subProperty?: string | null | undefined)
   return Object.values(set);
 }
 
-async function notifyTranporteurs(ligne, type) {
-  const usersToNotify = await ReferentModel.find({ role: ROLES.TRANSPORTER });
-
-  await sendTemplate(SENDINBLUE_TEMPLATES.PLAN_TRANSPORT.NOTIF_TRANSPORTEUR_MODIF, {
-    emailTo: usersToNotify.map((referent) => ({
-      name: `${referent.firstName} ${referent.lastName}`,
-      email: referent.email,
-    })),
-    params: {
-      type,
-      cohort: ligne.cohort,
-      ID: ligne._id.toString(),
-      lineName: ligne.busId,
-      cta: `${config.ADMIN_URL}/ligne-de-bus/${ligne._id.toString()}`,
-    },
-  });
-}
-
 export default router;
diff --git a/api/src/planDeTransport/ligneDeBus/ligneDeBusNotificationService.ts b/api/src/planDeTransport/ligneDeBus/ligneDeBusNotificationService.ts
new file mode 100644
index 0000000000..ab843656c7
--- /dev/null
+++ b/api/src/planDeTransport/ligneDeBus/ligneDeBusNotificationService.ts
@@ -0,0 +1,78 @@
+import { config } from "../../config";
+import { CohortType, ERRORS, ROLES, SENDINBLUE_TEMPLATES } from "snu-lib";
+import { ClasseDocument, EtablissementModel, LigneBusDocument, ReferentModel, YoungDocument } from "../../models";
+import { sendTemplate } from "../../brevo";
+
+export async function notifyYoungsAndRlsPDRWasUpdated(youngs: YoungDocument[], cohort: CohortType) {
+  let isBeforeDeparture = false;
+  let templateId: string | null = null;
+  if (new Date() < new Date(cohort.dateStart)) {
+    isBeforeDeparture = true;
+    templateId = SENDINBLUE_TEMPLATES.young.CHANGE_PDR_BEFORE_DEPARTURE;
+  } else if (new Date() < new Date(cohort.dateEnd)) {
+    templateId = SENDINBLUE_TEMPLATES.young.CHANGE_PDR_BEFORE_RETURN;
+  }
+
+  if (!templateId) throw new Error("Modification date is out of range, no email sent.");
+
+  for (const young of youngs) {
+    if (isBeforeDeparture) {
+      const contacts = [young.email];
+      if (young.parent1Email) contacts.push(young.parent1Email);
+      if (young.parent2Email) contacts.push(young.parent2Email);
+
+      for (const contact of contacts) {
+        await sendTemplate(templateId, { emailTo: [{ email: contact }] });
+      }
+    } else {
+      if (young.cohesionStayPresence !== "false" && young.departInform !== "true") {
+        if (young.parent1Email) {
+          await sendTemplate(templateId, { emailTo: [{ email: young.parent1Email }], cc: [{ email: young.email }] });
+        }
+        if (young.parent2Email) {
+          await sendTemplate(templateId, { emailTo: [{ email: young.parent2Email }], cc: [{ email: young.email }] });
+        }
+      }
+    }
+  }
+}
+
+export async function notifyYoungsAndRlsSessionWasUpdated(youngs: YoungDocument[]) {
+  const templateId = SENDINBLUE_TEMPLATES.young.PHASE_1_CHANGEMENT_CENTRE;
+  for (const young of youngs) {
+    await sendTemplate(templateId, { emailTo: [{ email: young.email }] });
+    if (young.parent1Email) await sendTemplate(templateId, { emailTo: [{ email: young.parent1Email }] });
+    if (young.parent2Email) await sendTemplate(templateId, { emailTo: [{ email: young.parent2Email }] });
+  }
+}
+
+export async function notifyReferentsCLELineWasUpdated(classe: ClasseDocument) {
+  const etablissementId = classe.etablissementId;
+  const etablissement = await EtablissementModel.findById(etablissementId);
+  if (!etablissement) throw new Error(ERRORS.NOT_FOUND);
+  const referentsCLEIds = [...classe.referentClasseIds, ...etablissement.referentEtablissementIds, ...etablissement.coordinateurIds];
+
+  const referentsCLE = await ReferentModel.find({ _id: { $in: referentsCLEIds } }).select("email");
+
+  for (const referent of referentsCLE) {
+    await sendTemplate(SENDINBLUE_TEMPLATES.CLE.PHASE_1_MODIFICATION_LIGNE, { emailTo: [{ email: referent.email }] });
+  }
+}
+
+export async function notifyTransporteurLineWasUpdated(ligne: LigneBusDocument, type: "Informations générales" | "Équipe" | "Centre de cohésion" | "Point de rassemblement") {
+  const usersToNotify = await ReferentModel.find({ role: ROLES.TRANSPORTER });
+
+  await sendTemplate(SENDINBLUE_TEMPLATES.PLAN_TRANSPORT.NOTIF_TRANSPORTEUR_MODIF, {
+    emailTo: usersToNotify.map((referent) => ({
+      name: `${referent.firstName} ${referent.lastName}`,
+      email: referent.email,
+    })),
+    params: {
+      type,
+      cohort: ligne.cohort,
+      ID: ligne._id.toString(),
+      lineName: ligne.busId,
+      cta: `${config.ADMIN_URL}/ligne-de-bus/${ligne._id.toString()}`,
+    },
+  });
+}
diff --git a/api/src/planDeTransport/ligneDeBus/ligneDeBusService.test.ts b/api/src/planDeTransport/ligneDeBus/ligneDeBusService.test.ts
index 9a737febd0..6247255a46 100644
--- a/api/src/planDeTransport/ligneDeBus/ligneDeBusService.test.ts
+++ b/api/src/planDeTransport/ligneDeBus/ligneDeBusService.test.ts
@@ -1,7 +1,8 @@
+import { ERRORS, UserDto } from "snu-lib";
 import { dbConnect, dbClose } from "../../__tests__/helpers/db";
-import { LigneBusModel } from "../../models";
+import { LigneBusModel, SessionPhase1Document, SessionPhase1Model } from "../../models";
 
-import { syncMergedBus } from "./ligneDeBusService";
+import { syncMergedBus, updateSessionForLine } from "./ligneDeBusService";
 
 beforeAll(dbConnect);
 afterAll(dbClose);
@@ -10,8 +11,9 @@ afterEach(() => {
 });
 
 const findOneBusSpy = jest.spyOn(LigneBusModel, "findOne");
+const sessionPhase1ModelFindByIdSpy = jest.spyOn(SessionPhase1Model, "findById");
 
-describe("ligneDeBusService", () => {
+describe("ligneDeBusService.syncMergedBus", () => {
   it("should syncMergedBus all bus line", async () => {
     const ligneBus = { busId: "#1", cohort: "cohort", set: () => null, save: () => Promise.resolve() } as any;
     const busSaveSpy = jest.spyOn(ligneBus, "save");
@@ -34,3 +36,20 @@ describe("ligneDeBusService", () => {
     expect(findOneBusSpy).toHaveBeenCalledTimes(2);
   });
 });
+
+describe("ligneDeBusService.updateSessionForLine", () => {
+  const sessionPhase1 = { _id: "oldSessionId", placesLeft: 10 };
+  sessionPhase1ModelFindByIdSpy.mockResolvedValue(sessionPhase1);
+
+  it("should throw an error if the new session does not have enough places left", async () => {
+    const user = { _id: "userId" } as UserDto;
+    const newSession = { _id: "newSessionId", placesLeft: 5 } as SessionPhase1Document;
+    const ligne = { _id: "busId", sessionId: "oldSessionId", youngSeatsTaken: 23 } as any;
+
+    try {
+      await updateSessionForLine({ ligne, session: newSession, user, sendCampaign: false });
+    } catch (e) {
+      expect(e.message).toBe(ERRORS.OPERATION_NOT_ALLOWED);
+    }
+  });
+});
diff --git a/api/src/planDeTransport/ligneDeBus/ligneDeBusService.ts b/api/src/planDeTransport/ligneDeBus/ligneDeBusService.ts
index a7f6042c77..057baf297f 100644
--- a/api/src/planDeTransport/ligneDeBus/ligneDeBusService.ts
+++ b/api/src/planDeTransport/ligneDeBus/ligneDeBusService.ts
@@ -1,11 +1,23 @@
-import { BusTeamDto } from "snu-lib/src/dto";
-import { LigneBusDocument } from "../../models";
-import { LigneToPointModel, PointDeRassemblementModel, LigneBusModel, CohesionCenterModel, PlanTransportModel, YoungModel, CohortModel } from "../../models";
+import {
+  ClasseModel,
+  LigneBusDocument,
+  SessionPhase1Document,
+  SessionPhase1Model,
+  LigneToPointModel,
+  PointDeRassemblementModel,
+  LigneBusModel,
+  CohesionCenterModel,
+  PlanTransportModel,
+  YoungModel,
+  CohortModel,
+} from "../../models";
 import { mapBusTeamToUpdate } from "./ligneDeBusMapper";
 import { Types, ClientSession } from "mongoose";
-
-import { CohortType, SENDINBLUE_TEMPLATES, UserDto, checkTime } from "snu-lib";
-import { sendTemplate } from "../../brevo";
+import { ERRORS, UserDto, checkTime, BusTeamDto, YOUNG_STATUS_PHASE1, YOUNG_STATUS, COHORT_TYPE } from "snu-lib";
+import { updatePlacesSessionPhase1 } from "../../utils";
+import { endSession, startSession, withTransaction } from "../../mongo";
+import { notifyReferentsCLELineWasUpdated, notifyYoungsAndRlsPDRWasUpdated, notifyYoungsAndRlsSessionWasUpdated } from "./ligneDeBusNotificationService";
+import { logger } from "../../logger";
 
 const { ObjectId } = Types;
 
@@ -46,7 +58,6 @@ export async function getInfoBus(line: LigneBusDocument) {
       youngCapacity: ligneBus.youngCapacity,
     }))[0];
   }
-
   return { ...line._doc, meetingsPointsDetail, centerDetail, mergedBusDetails, mirrorBusDetails };
 }
 
@@ -113,100 +124,190 @@ export const updatePDRForLine = async (
   user: UserDto,
 ) => {
   const ligneBus = await LigneBusModel.findById(ligneBusId);
-  if (!ligneBus) throw new Error("NOT_FOUND");
+  if (!ligneBus) {
+    logger.error(`LigneBus not found ${ligneBusId}`);
+    throw new Error("NOT_FOUND");
+  }
 
   const cohort = await CohortModel.findOne({ name: ligneBus.cohort });
-  if (!cohort) throw new Error("NOT_FOUND");
+  if (!cohort) {
+    logger.error(`Cohort not found ${ligneBus.cohort}`);
+    throw new Error("NOT_FOUND");
+  }
 
   if (checkTime(departureHour, meetingHour) || checkTime(departureHour, busArrivalHour)) {
     throw new Error("INVALID_BODY");
   }
 
   const ligneToPoint = await LigneToPointModel.findOne({ lineId: ligneBusId, meetingPointId });
-  if (!ligneToPoint) throw new Error("NOT_FOUND");
-
-  ligneToPoint.set({
-    transportType,
-    meetingHour,
-    busArrivalHour,
-    departureHour,
-    returnHour,
-    ...(meetingPointId !== newMeetingPointId && { meetingPointId: newMeetingPointId }),
-  });
-
-  await ligneToPoint.save({ fromUser: user });
-
-  if (meetingPointId !== newMeetingPointId) {
-    const meetingPointsIds = ligneBus.meetingPointsIds.filter((id) => id !== meetingPointId);
-    meetingPointsIds.push(newMeetingPointId);
-    ligneBus.set({ meetingPointsIds });
-    await ligneBus.save({ fromUser: user });
+  if (!ligneToPoint) {
+    logger.error(`LigneToPoint not found ${ligneBusId} ${meetingPointId}`);
+    throw new Error("NOT_FOUND");
   }
 
-  const planDeTransport = await PlanTransportModel.findById(ligneBusId);
-  if (!planDeTransport) throw new Error("NOT_FOUND");
-
-  const pointDeRassemblement = await PointDeRassemblementModel.findById(new ObjectId(newMeetingPointId));
-  if (!pointDeRassemblement) throw new Error("NOT_FOUND");
-
-  const meetingPoint = planDeTransport.pointDeRassemblements.find((mp) => mp.meetingPointId === meetingPointId);
-  if (!meetingPoint) throw new Error("NOT_FOUND");
-
-  meetingPoint.set({
-    meetingPointId: newMeetingPointId,
-    ...pointDeRassemblement._doc,
-    busArrivalHour: ligneToPoint.busArrivalHour,
-    meetingHour: ligneToPoint.meetingHour,
-    departureHour: ligneToPoint.departureHour,
-    returnHour: ligneToPoint.returnHour,
-    transportType: ligneToPoint.transportType,
-  });
-
-  await planDeTransport.save({ fromUser: user });
-
-  const youngUpdateResult = await YoungModel.updateMany(
-    {
-      ligneId: ligneBusId,
-      meetingPointId: meetingPointId,
-    },
-    {
-      $set: {
+  const transaction = await startSession();
+  try {
+    await withTransaction(transaction, async () => {
+      ligneToPoint.set({
+        transportType,
+        meetingHour,
+        busArrivalHour,
+        departureHour,
+        returnHour,
+        ...(meetingPointId !== newMeetingPointId && { meetingPointId: newMeetingPointId }),
+      });
+
+      await ligneToPoint.save({ fromUser: user, session: transaction });
+
+      if (meetingPointId !== newMeetingPointId) {
+        const meetingPointsIds = ligneBus.meetingPointsIds.filter((id) => id !== meetingPointId);
+        meetingPointsIds.push(newMeetingPointId);
+        ligneBus.set({ meetingPointsIds });
+        await ligneBus.save({ fromUser: user, session: transaction });
+      }
+
+      const planDeTransport = await PlanTransportModel.findById(ligneBusId);
+      if (!planDeTransport) {
+        logger.error(`PlanTransport not found ${ligneBusId}`);
+        throw new Error("NOT_FOUND");
+      }
+
+      const pointDeRassemblement = await PointDeRassemblementModel.findById(new ObjectId(newMeetingPointId));
+      if (!pointDeRassemblement) {
+        logger.error(`PointDeRassemblement not found ${newMeetingPointId}`);
+        throw new Error("NOT_FOUND");
+      }
+
+      const meetingPoint = planDeTransport.pointDeRassemblements.find((mp) => mp.meetingPointId === meetingPointId);
+      if (!meetingPoint) {
+        logger.error(`MeetingPoint not found ${meetingPointId}`);
+        throw new Error("NOT_FOUND");
+      }
+
+      meetingPoint.set({
         meetingPointId: newMeetingPointId,
-      },
-    },
-    { fromUser: user },
-  );
-
-  if (shouldSendEmailCampaign && youngUpdateResult.modifiedCount > 0) {
-    await sendEmailCampaign(ligneBusId, newMeetingPointId, cohort);
+        ...pointDeRassemblement._doc,
+        busArrivalHour: ligneToPoint.busArrivalHour,
+        meetingHour: ligneToPoint.meetingHour,
+        departureHour: ligneToPoint.departureHour,
+        returnHour: ligneToPoint.returnHour,
+        transportType: ligneToPoint.transportType,
+      });
+
+      await planDeTransport.save({ fromUser: user, session: transaction });
+
+      await YoungModel.updateMany(
+        {
+          ligneId: ligneBusId,
+          meetingPointId: meetingPointId,
+        },
+        {
+          $set: {
+            meetingPointId: newMeetingPointId,
+          },
+        },
+        { fromUser: user, session: transaction },
+      );
+
+      if (shouldSendEmailCampaign) {
+        const updatedYoungs = await YoungModel.find({
+          ligneId: ligneBusId,
+          meetingPointId: meetingPointId,
+          status: YOUNG_STATUS.VALIDATED,
+          statusPhase1: { $in: [YOUNG_STATUS_PHASE1.AFFECTED, YOUNG_STATUS_PHASE1.DONE] },
+        });
+        await notifyYoungsAndRlsPDRWasUpdated(updatedYoungs, cohort);
+
+        if (cohort.type === COHORT_TYPE.CLE) {
+          const classes = await ClasseModel.find({ ligneId: ligneBusId });
+          for (const classe of classes) {
+            await notifyReferentsCLELineWasUpdated(classe);
+          }
+        }
+      }
+    });
+  } finally {
+    await endSession(transaction);
   }
 
   return ligneBus;
 };
 
-const sendEmailCampaign = async (ligneBusId: string, newMeetingPointId: string, cohort: CohortType) => {
-  const updatedYoungs = await YoungModel.find({
-    ligneId: ligneBusId,
-    meetingPointId: newMeetingPointId,
-  }).select("email parent1Email parent2Email");
-
-  let templateId: string | null = null;
-  if (new Date() < new Date(cohort.dateStart)) {
-    templateId = SENDINBLUE_TEMPLATES.young.CHANGE_PDR_BEFORE_DEPARTURE;
-  } else if (new Date() < new Date(cohort.dateEnd)) {
-    templateId = SENDINBLUE_TEMPLATES.young.CHANGE_PDR_BEFORE_RETURN;
+export const updateSessionForLine = async ({
+  ligne,
+  session,
+  user,
+  sendCampaign,
+}: {
+  ligne: LigneBusDocument;
+  session: SessionPhase1Document;
+  user: UserDto;
+  sendCampaign?: boolean;
+}) => {
+  const currentSessionPhase1 = await SessionPhase1Model.findById(ligne.sessionId);
+  if (!currentSessionPhase1) throw new Error(ERRORS.NOT_FOUND);
+
+  if (ligne.youngSeatsTaken > (session.placesLeft || 0)) {
+    throw new Error(ERRORS.OPERATION_NOT_ALLOWED);
   }
 
-  if (!templateId) throw new Error("Modification date is out of range, no email sent.");
-
-  for (const young of updatedYoungs) {
-    const cc: { email: string }[] = [];
-    if (young.parent1Email) cc.push({ email: young.parent1Email });
-    if (young.parent2Email) cc.push({ email: young.parent2Email });
-
-    await sendTemplate(templateId, {
-      emailTo: [{ email: young.email }],
-      cc,
+  const transaction = await startSession();
+
+  try {
+    await withTransaction(transaction, async () => {
+      // Ligne
+      ligne.set({
+        sessionId: session.id,
+        centerId: session.cohesionCenterId,
+      });
+      await ligne.save({ fromUser: user, session: transaction });
+
+      // Plan de transport
+      const planDeTransport = await PlanTransportModel.findById(ligne._id);
+      if (!planDeTransport) throw new Error(ERRORS.NOT_FOUND);
+      planDeTransport.set({
+        centerId: session.cohesionCenterId,
+        centerRegion: session.region,
+        centerDepartment: session.department,
+        centerAddress: session.cityCentre,
+        centerZip: session.zipCentre,
+        centerName: session.nameCentre,
+        centerCode: session.codeCentre,
+      });
+      await planDeTransport.save({ fromUser: user, session: transaction });
+
+      // Jeunes
+      const filter = { ligneId: ligne._id };
+      const updateDoc = { $set: { sessionPhase1Id: session.id, cohesionCenterId: session.cohesionCenterId } };
+      await YoungModel.updateMany(filter, updateDoc, { fromUser: user, session: transaction });
+
+      // Classes
+      const classes = await ClasseModel.find({ ligneId: ligne._id });
+      for (const classe of classes) {
+        classe.set({
+          sessionId: session.id,
+          cohesionCenterId: session.cohesionCenterId,
+        });
+        await classe.save({ fromUser: user, session: transaction });
+      }
     });
+
+    await updatePlacesSessionPhase1(currentSessionPhase1, user);
+    await updatePlacesSessionPhase1(session, user);
+
+    if (sendCampaign) {
+      const updatedYoungs = await YoungModel.find({
+        ligneId: ligne._id,
+        status: YOUNG_STATUS.VALIDATED,
+        statusPhase1: { $in: [YOUNG_STATUS_PHASE1.AFFECTED, YOUNG_STATUS_PHASE1.DONE] },
+      });
+      await notifyYoungsAndRlsSessionWasUpdated(updatedYoungs);
+      const classes = await ClasseModel.find({ ligneId: ligne._id });
+      for (const classe of classes) {
+        await notifyReferentsCLELineWasUpdated(classe);
+      }
+    }
+  } finally {
+    await endSession(transaction);
   }
 };
diff --git a/api/src/services/LigneDeBusService.ts b/api/src/services/LigneDeBusService.ts
deleted file mode 100644
index 41256f45b2..0000000000
--- a/api/src/services/LigneDeBusService.ts
+++ /dev/null
@@ -1,159 +0,0 @@
-import { Types } from "mongoose";
-import {
-  LigneBusModel,
-  LigneToPointModel,
-  PlanTransportModel,
-  YoungModel,
-  CohortModel,
-  PointDeRassemblementModel,
-  ClasseModel,
-  ClasseDocument,
-  EtablissementModel,
-  ReferentModel,
-} from "../models";
-import { COHORT_TYPE, CohortType, ERRORS, SENDINBLUE_TEMPLATES, UserDto, checkTime } from "snu-lib";
-import { sendTemplate } from "../brevo";
-import { YoungDocument } from "../models/young";
-
-const { ObjectId } = Types;
-
-export const updatePDRForLine = async (
-  ligneBusId: string,
-  transportType: string,
-  meetingHour: string,
-  busArrivalHour: string,
-  departureHour: string,
-  returnHour: string,
-  meetingPointId: string,
-  newMeetingPointId: string,
-  shouldSendEmailCampaign: boolean,
-  user: UserDto,
-) => {
-  const ligneBus = await LigneBusModel.findById(ligneBusId);
-  if (!ligneBus) throw new Error(ERRORS.NOT_FOUND);
-
-  const cohort = await CohortModel.findOne({ name: ligneBus.cohort });
-  if (!cohort) throw new Error(ERRORS.NOT_FOUND);
-
-  if (checkTime(departureHour, meetingHour) || checkTime(departureHour, busArrivalHour)) {
-    throw new Error(ERRORS.INVALID_BODY);
-  }
-
-  const ligneToPoint = await LigneToPointModel.findOne({ lineId: ligneBusId, meetingPointId });
-  if (!ligneToPoint) throw new Error(ERRORS.NOT_FOUND);
-
-  ligneToPoint.set({
-    transportType,
-    meetingHour,
-    busArrivalHour,
-    departureHour,
-    returnHour,
-    ...(meetingPointId !== newMeetingPointId && { meetingPointId: newMeetingPointId }),
-  });
-
-  await ligneToPoint.save({ fromUser: user });
-
-  if (meetingPointId !== newMeetingPointId) {
-    const meetingPointsIds = ligneBus.meetingPointsIds.filter((id) => id !== meetingPointId);
-    meetingPointsIds.push(newMeetingPointId);
-    ligneBus.set({ meetingPointsIds });
-    await ligneBus.save({ fromUser: user });
-  }
-
-  const planDeTransport = await PlanTransportModel.findById(ligneBusId);
-  if (!planDeTransport) throw new Error(ERRORS.NOT_FOUND);
-
-  const pointDeRassemblement = await PointDeRassemblementModel.findById(new ObjectId(newMeetingPointId));
-  if (!pointDeRassemblement) throw new Error(ERRORS.NOT_FOUND);
-
-  const meetingPoint = planDeTransport.pointDeRassemblements.find((mp) => mp.meetingPointId === meetingPointId);
-  if (!meetingPoint) throw new Error(ERRORS.NOT_FOUND);
-
-  meetingPoint.set({
-    meetingPointId: newMeetingPointId,
-    ...pointDeRassemblement._doc,
-    busArrivalHour: ligneToPoint.busArrivalHour,
-    meetingHour: ligneToPoint.meetingHour,
-    departureHour: ligneToPoint.departureHour,
-    returnHour: ligneToPoint.returnHour,
-    transportType: ligneToPoint.transportType,
-  });
-
-  await planDeTransport.save({ fromUser: user });
-
-  const youngsToUpdate = await YoungModel.find({
-    ligneId: ligneBusId,
-    meetingPointId: meetingPointId,
-  });
-
-  await Promise.all(
-    youngsToUpdate.map(async (young) => {
-      young.meetingPointId = newMeetingPointId;
-      await young.save({ fromUser: user });
-    }),
-  );
-
-  if (shouldSendEmailCampaign) {
-    const updatedYoungs = await YoungModel.find({
-      ligneId: ligneBusId,
-      meetingPointId: newMeetingPointId,
-    });
-
-    await sendEmailCampaignToYoungAndRL(updatedYoungs, cohort);
-
-    if (cohort.type === COHORT_TYPE.CLE) {
-      const classe = await ClasseModel.findById(updatedYoungs[0].classeId);
-      if (!classe) throw new Error(ERRORS.NOT_FOUND);
-      await sendEmailCampaignToRefCLE(classe);
-    }
-  }
-
-  return ligneBus;
-};
-
-const sendEmailCampaignToYoungAndRL = async (youngs: YoungDocument[], cohort: CohortType) => {
-  let isBeforeDeparture = false;
-  let templateId: string | null = null;
-  if (new Date() < new Date(cohort.dateStart)) {
-    isBeforeDeparture = true;
-    templateId = SENDINBLUE_TEMPLATES.young.CHANGE_PDR_BEFORE_DEPARTURE;
-  } else if (new Date() < new Date(cohort.dateEnd)) {
-    templateId = SENDINBLUE_TEMPLATES.young.CHANGE_PDR_BEFORE_RETURN;
-  }
-
-  if (!templateId) throw new Error("Modification date is out of range, no email sent.");
-
-  for (const young of youngs) {
-    if (isBeforeDeparture) {
-      const contacts = [young.email];
-      if (young.parent1Email) contacts.push(young.parent1Email);
-      if (young.parent2Email) contacts.push(young.parent2Email);
-
-      for (const contact of contacts) {
-        await sendTemplate(templateId, { emailTo: [{ email: contact }] });
-      }
-    } else {
-      if (young.cohesionStayPresence !== "false" && young.departInform !== "true") {
-        if (young.parent1Email) {
-          await sendTemplate(templateId, { emailTo: [{ email: young.parent1Email }], cc: [{ email: young.email }] });
-        }
-        if (young.parent2Email) {
-          await sendTemplate(templateId, { emailTo: [{ email: young.parent2Email }], cc: [{ email: young.email }] });
-        }
-      }
-    }
-  }
-};
-
-const sendEmailCampaignToRefCLE = async (classe: ClasseDocument) => {
-  const etablissementId = classe.etablissementId;
-  const etablissement = await EtablissementModel.findById(etablissementId);
-  if (!etablissement) throw new Error(ERRORS.NOT_FOUND);
-  const referentsCLEIds = [...classe.referentClasseIds, ...etablissement.referentEtablissementIds, ...etablissement.coordinateurIds];
-
-  const referentsCLE = await ReferentModel.find({ _id: { $in: referentsCLEIds } }).select("email");
-
-  for (const referent of referentsCLE) {
-    await sendTemplate(SENDINBLUE_TEMPLATES.CLE.PHASE_1_CHANGEMENT_PDR, { emailTo: [{ email: referent.email }] });
-  }
-};
diff --git a/packages/ds/src/admin/form/EditButton.tsx b/packages/ds/src/admin/form/EditButton.tsx
new file mode 100644
index 0000000000..00f09b29a6
--- /dev/null
+++ b/packages/ds/src/admin/form/EditButton.tsx
@@ -0,0 +1,79 @@
+import React, { useId } from "react";
+import { HiOutlinePencil } from "react-icons/hi";
+import ReactTooltip from "react-tooltip";
+
+type Props = {
+  form?: string;
+  isEditing: boolean;
+  setIsEditing: (value: boolean) => void;
+  disabled: boolean;
+  isLoading: boolean;
+  onReset: () => void;
+  tooltipMessage?: string;
+};
+
+export default function EditButton({
+  form,
+  isEditing,
+  setIsEditing,
+  disabled,
+  isLoading,
+  onReset,
+  tooltipMessage = "Modification impossible",
+}: Props) {
+  const tooltipId = useId();
+  const className =
+    "flex items-center gap-2 rounded-full text-xs font-medium px-3 py-2.5 border-[1px] disabled:opacity-50";
+
+  if (isEditing)
+    return (
+      <div className="flex items-center gap-2">
+        <button
+          type="reset"
+          onClick={() => {
+            setIsEditing(false);
+            onReset();
+          }}
+          disabled={isLoading}
+          className={`${className} border-gray-100 text-gray-600 bg-gray-100 hover:border-gray-600`}
+        >
+          Annuler
+        </button>
+        <button
+          form={form}
+          type="submit"
+          disabled={isLoading}
+          className={`${className} border-blue-100 text-blue-600 bg-blue-100 hover:border-blue-600`}
+        >
+          <HiOutlinePencil />
+          Enregistrer les changements
+        </button>
+      </div>
+    );
+
+  return (
+    <>
+      {disabled && (
+        <ReactTooltip
+          id={tooltipId}
+          className="bg-white shadow-xl"
+          arrowColor="white"
+          disable={false}
+        >
+          <p className="text-gray-800">{tooltipMessage}</p>
+        </ReactTooltip>
+      )}
+      <button
+        type="button"
+        onClick={() => setIsEditing(true)}
+        disabled={disabled}
+        data-tip
+        data-for={tooltipId}
+        className={`${className} border-blue-100 text-blue-600 bg-blue-100 hover:border-blue-600 disabled:hover:border-blue-100`}
+      >
+        <HiOutlinePencil />
+        Modifier
+      </button>
+    </>
+  );
+}
diff --git a/packages/ds/src/admin/index.ts b/packages/ds/src/admin/index.ts
index f3eb76fec9..3712c32370 100644
--- a/packages/ds/src/admin/index.ts
+++ b/packages/ds/src/admin/index.ts
@@ -9,6 +9,7 @@ import Label from "./form/Label";
 import Select, { SelectOption } from "./form/Select/Select";
 export { default as CollapsableSelectSwitcher } from "./form/CollapsableSelectSwitcher";
 export { default as SectionSwitcher } from "./form/SectionSwitcher";
+import EditButton from "./form/EditButton";
 
 // Layout
 import Container from "./layout/Container";
@@ -37,6 +38,7 @@ export {
   InputNumber,
   InputPhone,
   Label,
+  EditButton,
 
   // Layout
   Container,
diff --git a/packages/lib/src/constants/email.ts b/packages/lib/src/constants/email.ts
index 58dc8d1caf..a1c3b6032c 100644
--- a/packages/lib/src/constants/email.ts
+++ b/packages/lib/src/constants/email.ts
@@ -120,6 +120,7 @@ const SENDINBLUE_TEMPLATES = {
     CHANGE_PDR_BEFORE_DEPARTURE: "2270",
     CHANGE_PDR_BEFORE_RETURN: "2307",
     PHASE1_AGREEMENT: "2576",
+    PHASE_1_CHANGEMENT_CENTRE: "2276",
 
     // MIG
     REFUSE_APPLICATION: "1253",
@@ -213,7 +214,7 @@ const SENDINBLUE_TEMPLATES = {
     INVITATION_REFERENT_CLASSE_TO_INSCRIPTION_TEMPLATE: "1391",
     INVITATION_REFERENT_CLASSE_TO_CONFIRMATION_TEMPLATE: "1427",
     SUPPRESSION_ANCIEN_REFERENT_CLASSE_TEMPLATE: "",
-    PHASE_1_CHANGEMENT_PDR: "2614",
+    PHASE_1_MODIFICATION_LIGNE: "2614",
   },
   // Imports
   IMPORT_AUTO: "2324",
diff --git a/packages/lib/src/dto/ligneDeBus.ts b/packages/lib/src/dto/ligneDeBus.ts
index bc8f923b09..f1295ecebd 100644
--- a/packages/lib/src/dto/ligneDeBus.ts
+++ b/packages/lib/src/dto/ligneDeBus.ts
@@ -1,6 +1,6 @@
 import { PointDeRassemblementType } from "../mongoSchema";
 
-type CenterDetailDto = {
+export type CenterDetailDto = {
   cohorts: string[];
   waitingList: any[];
   sessionStatus: any[];


Reviews:
user: login: achorein
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T08:41:17Z
user: login: achorein
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T08:43:40Z
user: login: nicobret
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T09:28:43Z
user: login: nicobret
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T09:31:08Z
user: login: nicobret
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T10:01:16Z
user: login: nicobret
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T10:08:10Z
user: login: nicobret
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T10:10:46Z
user: login: achorein
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T10:22:16Z
user: login: achorein
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T10:23:55Z
user: login: nicobret
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T10:39:16Z
user: login: nicobret
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T10:42:39Z
user: login: achorein
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T10:52:25Z
user: login: achorein
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T10:54:45Z

Review comments:
diff_hunk: @@ -0,0 +1,118 @@
+import React from "react";
+import { Button, Modal } from "@snu/ds/admin";
+import XCircleFull from "@/assets/icons/XCircleFull";
+import CheckCircleFull from "@/assets/icons/CheckCircleFull";
+import { useToggle } from "react-use";
+import { LigneBusDto, SENDINBLUE_TEMPLATES } from "snu-lib";
+import CentreLabel from "./CentreLabel";
+import { TfiReload } from "react-icons/tfi";
+import { HiExternalLink } from "react-icons/hi";
+import useSessions from "@/scenes/plan-transport/lib/useSessionsByCohort";
+import { CentreFormValues } from ".";
+
+interface ConfirmChangesModalProps {
+  isOpen: boolean;
+  count: number;
+  initialData: LigneBusDto;
+  formData: { sessionId: string; centerArrivalTime: string; centerDepartureTime: string };
+  onConfirm: (data: CentreFormValues, emailing: boolean) => void;
+  onCancel: () => void;
+  isLoading?: boolean;
+}
+
+export default function CentreModal({ isOpen, initialData, formData, count, onConfirm, onCancel, isLoading }: ConfirmChangesModalProps) {
+  const [isEmailing, toggleEmailing] = useToggle(false);
+  const templateId = SENDINBLUE_TEMPLATES.young.PHASE_1_CHANGEMENT_CENTRE;
+  const templateIdReferent = SENDINBLUE_TEMPLATES.CLE.PHASE_1_MODIFICATION_LIGNE;
+  const isCLE = initialData.cohort.includes("CLE");
+  const { data } = useSessions(initialData.cohort);
+  const initialSession = data?.find((s) => s._id === initialData.sessionId);
+  const newSession = data?.find((s) => s._id === formData.sessionId);
+
+  const handleConfirm = () => {
+    onConfirm(formData, isEmailing);
+  };
+
+  return (
+    <Modal
+      isOpen={isOpen}
+      onClose={onCancel}
+      className="max-w-3xl"
+      content={
+        <div className="scroll-y-auto overflow-y-auto">
+          <div className="flex flex-col items-center text-center gap-6">
+            <div className="flex items-center justify-center w-12 h-12 rounded-full bg-gray-50">
+              <TfiReload className="w-5 h-5 text-gray-800" />
+            </div>
+            <h1 className="font-medium text-xl max-w-sm">Êtes-vous sûr(e) de vouloir changer de centre de cohésion&nbsp;?</h1>
+          </div>
+
+          <div className="mt-8 grid grid-cols-2 gap-3">
+            <div>
+              <p className="text-xs font-medium">Ancien centre</p>
+              <CenterCard
+                centerDetail={initialSession?.cohesionCenter}
+                centerArrivalTime={initialData.centerArrivalTime}
+                centerDepartureTime={initialData.centerDepartureTime}
+                icon={<XCircleFull className="text-red-500" />}
+              />
+            </div>
+
+            <div>
+              <p className="text-xs font-medium">Nouveau centre</p>
+              <CenterCard
+                centerDetail={newSession?.cohesionCenter}
+                centerArrivalTime={formData.centerArrivalTime!}
+                centerDepartureTime={formData.centerDepartureTime!}
+                icon={<CheckCircleFull className="text-green-500" />}
+              />
+            </div>
+          </div>
+
+          <div className="mt-8 border-t border-gray-200 w-full pt-8">
+            <label className="flex items-start space-x-2">
+              <input type="checkbox" className="mt-1" checked={isEmailing} onChange={toggleEmailing} />
+              <div className="leading-relaxed">
+                <p>Envoyer une campagne d’emailing aux volontaires ({count}) et à leurs représentants légaux.</p>
+                {isCLE && <p>Les référent de classe, chef d’établissement et coordinateurs seront également prévenus.</p>}
+                <div className="mt-2">
+                  <a href={`/email-preview/${templateId}`} target="_blank" rel="noreferrer" className="text-gray-500 hover:text-gray-800 underline underline-offset-2">
+                    Visualiser l'aperçu pour les volontaires
+                    <HiExternalLink className="inline-block w-4 h-4 ml-1" />
+                  </a>
+                  <br />
+                  {isCLE && (
+                    <a href={`/email-preview/${templateIdReferent}`} target="_blank" rel="noreferrer" className="text-gray-500 hover:text-gray-800 underline underline-offset-2">
+                      Visualiser l'aperçu pour les référents CLE
+                      <HiExternalLink className="inline-block w-4 h-4 ml-1" />
+                    </a>
+                  )}
+                </div>
+              </div>
+            </label>
+          </div>
+          <br />
+        </div>
+      }
+      footer={
+        <div className="grid grid-cols-2 gap-3">
+          <Button title="Annuler" type="secondary" onClick={onCancel} className="w-full" />
+          <Button title={isLoading ? "Modification en cours" : "Oui, valider"} onClick={handleConfirm} disabled={isLoading} className="w-full" />
path: admin/src/scenes/plan-transport/ligne-bus/View/components/Centre/CentreModal.tsx
user: login: achorein
body: remplacer `disabled={isLoading}` par `loading={isLoading}`, ça le desactive en même temps et ça fait l'animation de loader !
created_at: 2025-03-12T08:31:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,11 @@
+import { useQuery } from "@tanstack/react-query";
+import { SessionPhase1Service } from "../../../services/sessionPhase1Service";
+
+export default function useSessionsByCohort(cohortName: string) {
path: admin/src/scenes/plan-transport/lib/useSessionsByCohort.ts
user: login: achorein
body: Je ne pense pas que ça soit utile du côté de admin, on passe par le selecteur Redux (useSelector).

created_at: 2025-03-12T08:33:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,11 @@
+import { useQuery } from "@tanstack/react-query";
+import { SessionPhase1Service } from "../../../services/sessionPhase1Service";
+
+export default function useSessionsByCohort(cohortName: string) {
+  const filters = { cohort: [cohortName] };
+  return useQuery({
path: admin/src/scenes/plan-transport/lib/useSessionsByCohort.ts
user: login: achorein
body: il faudrait revoir la sortie de function pour qu'elle retourne directement une cohort plutôt qu'un tableau avec un seul élément.

```
return {
  cohort: data?.[0],
  isLoading: isFetching || isPending,
  error: error;
} 
```
  
  
created_at: 2025-03-12T08:35:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,118 @@
+import React from "react";
+import { Button, Modal } from "@snu/ds/admin";
+import XCircleFull from "@/assets/icons/XCircleFull";
+import CheckCircleFull from "@/assets/icons/CheckCircleFull";
+import { useToggle } from "react-use";
+import { LigneBusDto, SENDINBLUE_TEMPLATES } from "snu-lib";
+import CentreLabel from "./CentreLabel";
+import { TfiReload } from "react-icons/tfi";
+import { HiExternalLink } from "react-icons/hi";
+import useSessions from "@/scenes/plan-transport/lib/useSessionsByCohort";
+import { CentreFormValues } from ".";
+
+interface ConfirmChangesModalProps {
+  isOpen: boolean;
+  count: number;
+  initialData: LigneBusDto;
+  formData: { sessionId: string; centerArrivalTime: string; centerDepartureTime: string };
+  onConfirm: (data: CentreFormValues, emailing: boolean) => void;
+  onCancel: () => void;
+  isLoading?: boolean;
+}
+
+export default function CentreModal({ isOpen, initialData, formData, count, onConfirm, onCancel, isLoading }: ConfirmChangesModalProps) {
+  const [isEmailing, toggleEmailing] = useToggle(false);
+  const templateId = SENDINBLUE_TEMPLATES.young.PHASE_1_CHANGEMENT_CENTRE;
+  const templateIdReferent = SENDINBLUE_TEMPLATES.CLE.PHASE_1_MODIFICATION_LIGNE;
+  const isCLE = initialData.cohort.includes("CLE");
+  const { data } = useSessions(initialData.cohort);
path: admin/src/scenes/plan-transport/ligne-bus/View/components/Centre/CentreModal.tsx
user: login: achorein
body: ```suggestion
  const { data: sessions } = useSessions(initialData.cohort);
```
created_at: 2025-03-12T08:36:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,118 @@
+import React from "react";
+import { Button, Modal } from "@snu/ds/admin";
+import XCircleFull from "@/assets/icons/XCircleFull";
+import CheckCircleFull from "@/assets/icons/CheckCircleFull";
+import { useToggle } from "react-use";
+import { LigneBusDto, SENDINBLUE_TEMPLATES } from "snu-lib";
+import CentreLabel from "./CentreLabel";
+import { TfiReload } from "react-icons/tfi";
+import { HiExternalLink } from "react-icons/hi";
+import useSessions from "@/scenes/plan-transport/lib/useSessionsByCohort";
+import { CentreFormValues } from ".";
+
+interface ConfirmChangesModalProps {
+  isOpen: boolean;
+  count: number;
+  initialData: LigneBusDto;
+  formData: { sessionId: string; centerArrivalTime: string; centerDepartureTime: string };
+  onConfirm: (data: CentreFormValues, emailing: boolean) => void;
+  onCancel: () => void;
+  isLoading?: boolean;
+}
+
+export default function CentreModal({ isOpen, initialData, formData, count, onConfirm, onCancel, isLoading }: ConfirmChangesModalProps) {
+  const [isEmailing, toggleEmailing] = useToggle(false);
+  const templateId = SENDINBLUE_TEMPLATES.young.PHASE_1_CHANGEMENT_CENTRE;
+  const templateIdReferent = SENDINBLUE_TEMPLATES.CLE.PHASE_1_MODIFICATION_LIGNE;
+  const isCLE = initialData.cohort.includes("CLE");
+  const { data } = useSessions(initialData.cohort);
+  const initialSession = data?.find((s) => s._id === initialData.sessionId);
+  const newSession = data?.find((s) => s._id === formData.sessionId);
+
+  const handleConfirm = () => {
+    onConfirm(formData, isEmailing);
+  };
+
+  return (
+    <Modal
+      isOpen={isOpen}
+      onClose={onCancel}
+      className="max-w-3xl"
+      content={
+        <div className="scroll-y-auto overflow-y-auto">
+          <div className="flex flex-col items-center text-center gap-6">
+            <div className="flex items-center justify-center w-12 h-12 rounded-full bg-gray-50">
+              <TfiReload className="w-5 h-5 text-gray-800" />
+            </div>
+            <h1 className="font-medium text-xl max-w-sm">Êtes-vous sûr(e) de vouloir changer de centre de cohésion&nbsp;?</h1>
+          </div>
+
+          <div className="mt-8 grid grid-cols-2 gap-3">
+            <div>
+              <p className="text-xs font-medium">Ancien centre</p>
+              <CenterCard
+                centerDetail={initialSession?.cohesionCenter}
+                centerArrivalTime={initialData.centerArrivalTime}
+                centerDepartureTime={initialData.centerDepartureTime}
+                icon={<XCircleFull className="text-red-500" />}
+              />
+            </div>
+
+            <div>
+              <p className="text-xs font-medium">Nouveau centre</p>
+              <CenterCard
+                centerDetail={newSession?.cohesionCenter}
+                centerArrivalTime={formData.centerArrivalTime!}
+                centerDepartureTime={formData.centerDepartureTime!}
+                icon={<CheckCircleFull className="text-green-500" />}
+              />
+            </div>
+          </div>
+
+          <div className="mt-8 border-t border-gray-200 w-full pt-8">
+            <label className="flex items-start space-x-2">
+              <input type="checkbox" className="mt-1" checked={isEmailing} onChange={toggleEmailing} />
+              <div className="leading-relaxed">
+                <p>Envoyer une campagne d’emailing aux volontaires ({count}) et à leurs représentants légaux.</p>
+                {isCLE && <p>Les référent de classe, chef d’établissement et coordinateurs seront également prévenus.</p>}
+                <div className="mt-2">
+                  <a href={`/email-preview/${templateId}`} target="_blank" rel="noreferrer" className="text-gray-500 hover:text-gray-800 underline underline-offset-2">
+                    Visualiser l'aperçu pour les volontaires
+                    <HiExternalLink className="inline-block w-4 h-4 ml-1" />
+                  </a>
+                  <br />
+                  {isCLE && (
+                    <a href={`/email-preview/${templateIdReferent}`} target="_blank" rel="noreferrer" className="text-gray-500 hover:text-gray-800 underline underline-offset-2">
+                      Visualiser l'aperçu pour les référents CLE
+                      <HiExternalLink className="inline-block w-4 h-4 ml-1" />
+                    </a>
+                  )}
+                </div>
+              </div>
+            </label>
+          </div>
+          <br />
+        </div>
+      }
+      footer={
+        <div className="grid grid-cols-2 gap-3">
+          <Button title="Annuler" type="secondary" onClick={onCancel} className="w-full" />
+          <Button title={isLoading ? "Modification en cours" : "Oui, valider"} onClick={handleConfirm} disabled={isLoading} className="w-full" />
+        </div>
+      }
+    />
+  );
+}
+
+function CenterCard({ centerDetail, centerArrivalTime, centerDepartureTime, icon }) {
path: admin/src/scenes/plan-transport/ligne-bus/View/components/Centre/CentreModal.tsx
user: login: achorein
body: un seul composant par fichier
created_at: 2025-03-12T08:36:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,119 @@
+import { AuthState } from "@/redux/auth/reducer";
+import React from "react";
+import { useSelector } from "react-redux";
+import CentreLabel from "./CentreLabel";
+import { isSuperAdmin, LigneBusDto, ROLES } from "snu-lib";
+import { BsSearch } from "react-icons/bs";
+import useSessions from "@/scenes/plan-transport/lib/useSessionsByCohort";
+import { AiOutlineStop } from "react-icons/ai";
+import Loader from "@/components/Loader";
+import { SessionPhase1PopulatedWithCenter } from "@/services/sessionPhase1Service";
+
+type Props = {
+  sessionId: string;
+  setSessionId: (string) => void;
+  ligne: LigneBusDto;
+  disabled: boolean;
+};
+
+function filterResults(data: SessionPhase1PopulatedWithCenter[], search: string) {
+  if (search === "") return data;
+  const searchFields = ["name", "address", "region", "department", "code", "city", "zip", "matricule"];
+  return data.filter((item) => {
+    return searchFields.some((field) => item.cohesionCenter[field]?.toString().toLowerCase().includes(search.toLowerCase()));
+  });
+}
+
+export default function SessionSelector({ sessionId, setSessionId, ligne, disabled }: Props) {
+  const user = useSelector((state: AuthState) => state.Auth.user);
+  const [search, setSearch] = React.useState("");
+  const { data: sessions, isPending, isError } = useSessions(ligne.cohort);
+  const [open, setOpen] = React.useState(false);
+  const refSelect = React.useRef<HTMLDivElement>(null);
+  const refInput = React.useRef<HTMLInputElement>(null);
+  const refButtonChangesPDR = React.useRef<HTMLButtonElement>(null);
+
+  React.useEffect(() => {
path: admin/src/scenes/plan-transport/ligne-bus/View/components/Centre/SessionSelector.tsx
user: login: achorein
body: tu peux utiliser `useClickAway` qui simplifie beaucoup la chose : https://github.com/streamich/react-use/blob/master/docs/useClickAway.md
created_at: 2025-03-12T08:37:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -178,35 +207,80 @@ export const updatePDRForLine = async (
   );
 
   if (shouldSendEmailCampaign && youngUpdateResult.modifiedCount > 0) {
-    await sendEmailCampaign(ligneBusId, newMeetingPointId, cohort);
+    const updatedYoungs = await YoungModel.find({ ligneId: ligneBusId, meetingPointId: newMeetingPointId });
+    await notifyYoungsAndRlsPDRWasUpdated(updatedYoungs, cohort);
   }
 
   return ligneBus;
 };
 
-const sendEmailCampaign = async (ligneBusId: string, newMeetingPointId: string, cohort: CohortType) => {
-  const updatedYoungs = await YoungModel.find({
-    ligneId: ligneBusId,
-    meetingPointId: newMeetingPointId,
-  }).select("email parent1Email parent2Email");
-
-  let templateId: string | null = null;
-  if (new Date() < new Date(cohort.dateStart)) {
-    templateId = SENDINBLUE_TEMPLATES.young.CHANGE_PDR_BEFORE_DEPARTURE;
-  } else if (new Date() < new Date(cohort.dateEnd)) {
-    templateId = SENDINBLUE_TEMPLATES.young.CHANGE_PDR_BEFORE_RETURN;
+export const updateSessionForLine = async ({
+  ligne,
+  session,
+  user,
+  sendCampaign,
+}: {
+  ligne: LigneBusDocument;
+  session: SessionPhase1Document;
+  user: UserDto;
+  sendCampaign?: boolean;
+}) => {
+  const currentSessionPhase1 = await SessionPhase1Model.findById(ligne.sessionId);
+  if (!currentSessionPhase1) throw new Error(ERRORS.NOT_FOUND);
+
+  if (ligne.youngSeatsTaken > (session.placesLeft || 0)) {
+    throw new Error(ERRORS.OPERATION_NOT_ALLOWED);
   }
 
-  if (!templateId) throw new Error("Modification date is out of range, no email sent.");
-
-  for (const young of updatedYoungs) {
-    const cc: { email: string }[] = [];
-    if (young.parent1Email) cc.push({ email: young.parent1Email });
-    if (young.parent2Email) cc.push({ email: young.parent2Email });
-
-    await sendTemplate(templateId, {
-      emailTo: [{ email: young.email }],
-      cc,
+  const transaction = await startSession();
+
+  try {
+    await withTransaction(transaction, async () => {
path: api/src/planDeTransport/ligneDeBus/ligneDeBusService.ts
user: login: achorein
body: 👍 
created_at: 2025-03-12T08:39:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -178,35 +207,80 @@ export const updatePDRForLine = async (
   );
 
   if (shouldSendEmailCampaign && youngUpdateResult.modifiedCount > 0) {
-    await sendEmailCampaign(ligneBusId, newMeetingPointId, cohort);
+    const updatedYoungs = await YoungModel.find({ ligneId: ligneBusId, meetingPointId: newMeetingPointId });
+    await notifyYoungsAndRlsPDRWasUpdated(updatedYoungs, cohort);
   }
 
   return ligneBus;
 };
 
-const sendEmailCampaign = async (ligneBusId: string, newMeetingPointId: string, cohort: CohortType) => {
-  const updatedYoungs = await YoungModel.find({
-    ligneId: ligneBusId,
-    meetingPointId: newMeetingPointId,
-  }).select("email parent1Email parent2Email");
-
-  let templateId: string | null = null;
-  if (new Date() < new Date(cohort.dateStart)) {
-    templateId = SENDINBLUE_TEMPLATES.young.CHANGE_PDR_BEFORE_DEPARTURE;
-  } else if (new Date() < new Date(cohort.dateEnd)) {
-    templateId = SENDINBLUE_TEMPLATES.young.CHANGE_PDR_BEFORE_RETURN;
+export const updateSessionForLine = async ({
+  ligne,
+  session,
+  user,
+  sendCampaign,
+}: {
+  ligne: LigneBusDocument;
+  session: SessionPhase1Document;
+  user: UserDto;
+  sendCampaign?: boolean;
+}) => {
+  const currentSessionPhase1 = await SessionPhase1Model.findById(ligne.sessionId);
+  if (!currentSessionPhase1) throw new Error(ERRORS.NOT_FOUND);
+
+  if (ligne.youngSeatsTaken > (session.placesLeft || 0)) {
+    throw new Error(ERRORS.OPERATION_NOT_ALLOWED);
   }
 
-  if (!templateId) throw new Error("Modification date is out of range, no email sent.");
-
-  for (const young of updatedYoungs) {
-    const cc: { email: string }[] = [];
-    if (young.parent1Email) cc.push({ email: young.parent1Email });
-    if (young.parent2Email) cc.push({ email: young.parent2Email });
-
-    await sendTemplate(templateId, {
-      emailTo: [{ email: young.email }],
-      cc,
+  const transaction = await startSession();
+
+  try {
+    await withTransaction(transaction, async () => {
+      // Ligne
+      ligne.set({
+        sessionId: session.id,
+        centerId: session.cohesionCenterId,
+      });
+      await ligne.save({ fromUser: user });
+
+      // Plan de transport
+      const planDeTransport = await PlanTransportModel.findById(ligne._id);
+      if (!planDeTransport) throw new Error(ERRORS.NOT_FOUND);
+      planDeTransport.set({
+        centerId: session.cohesionCenterId,
+        centerRegion: session.region,
+        centerDepartment: session.department,
+        centerAddress: session.cityCentre,
+        centerZip: session.zipCentre,
+        centerName: session.nameCentre,
+        centerCode: session.codeCentre,
+      });
+      await planDeTransport.save({ fromUser: user });
+
+      // Jeunes
+      const filter = { ligneId: ligne._id };
+      const updateDoc = { $set: { sessionPhase1Id: session.id, cohesionCenterId: session.cohesionCenterId } };
+      await YoungModel.updateMany(filter, updateDoc, { fromUser: user });
+      if (sendCampaign) {
+        const updatedYoungs = await YoungModel.find(filter);
+        await notifyYoungsAndRlsSessionWasUpdated(updatedYoungs);
+      }
+
+      // Classes
+      const classes = await ClasseModel.find({ ligneId: ligne._id });
+      for (const classe of classes) {
+        classe.set({
+          sessionId: session.id,
+          cohesionCenterId: session.cohesionCenterId,
+        });
+        await classe.save({ fromUser: user });
path: api/src/planDeTransport/ligneDeBus/ligneDeBusService.ts
user: login: achorein
body: ```suggestion
        await classe.save({ fromUser: user, session: transaction });
```
created_at: 2025-03-12T08:42:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -178,35 +207,80 @@ export const updatePDRForLine = async (
   );
 
   if (shouldSendEmailCampaign && youngUpdateResult.modifiedCount > 0) {
-    await sendEmailCampaign(ligneBusId, newMeetingPointId, cohort);
+    const updatedYoungs = await YoungModel.find({ ligneId: ligneBusId, meetingPointId: newMeetingPointId });
+    await notifyYoungsAndRlsPDRWasUpdated(updatedYoungs, cohort);
   }
 
   return ligneBus;
 };
 
-const sendEmailCampaign = async (ligneBusId: string, newMeetingPointId: string, cohort: CohortType) => {
-  const updatedYoungs = await YoungModel.find({
-    ligneId: ligneBusId,
-    meetingPointId: newMeetingPointId,
-  }).select("email parent1Email parent2Email");
-
-  let templateId: string | null = null;
-  if (new Date() < new Date(cohort.dateStart)) {
-    templateId = SENDINBLUE_TEMPLATES.young.CHANGE_PDR_BEFORE_DEPARTURE;
-  } else if (new Date() < new Date(cohort.dateEnd)) {
-    templateId = SENDINBLUE_TEMPLATES.young.CHANGE_PDR_BEFORE_RETURN;
+export const updateSessionForLine = async ({
+  ligne,
+  session,
+  user,
+  sendCampaign,
+}: {
+  ligne: LigneBusDocument;
+  session: SessionPhase1Document;
+  user: UserDto;
+  sendCampaign?: boolean;
+}) => {
+  const currentSessionPhase1 = await SessionPhase1Model.findById(ligne.sessionId);
+  if (!currentSessionPhase1) throw new Error(ERRORS.NOT_FOUND);
+
+  if (ligne.youngSeatsTaken > (session.placesLeft || 0)) {
+    throw new Error(ERRORS.OPERATION_NOT_ALLOWED);
   }
 
-  if (!templateId) throw new Error("Modification date is out of range, no email sent.");
-
-  for (const young of updatedYoungs) {
-    const cc: { email: string }[] = [];
-    if (young.parent1Email) cc.push({ email: young.parent1Email });
-    if (young.parent2Email) cc.push({ email: young.parent2Email });
-
-    await sendTemplate(templateId, {
-      emailTo: [{ email: young.email }],
-      cc,
+  const transaction = await startSession();
+
+  try {
+    await withTransaction(transaction, async () => {
+      // Ligne
+      ligne.set({
+        sessionId: session.id,
+        centerId: session.cohesionCenterId,
+      });
+      await ligne.save({ fromUser: user });
+
+      // Plan de transport
+      const planDeTransport = await PlanTransportModel.findById(ligne._id);
+      if (!planDeTransport) throw new Error(ERRORS.NOT_FOUND);
+      planDeTransport.set({
+        centerId: session.cohesionCenterId,
+        centerRegion: session.region,
+        centerDepartment: session.department,
+        centerAddress: session.cityCentre,
+        centerZip: session.zipCentre,
+        centerName: session.nameCentre,
+        centerCode: session.codeCentre,
+      });
+      await planDeTransport.save({ fromUser: user });
+
+      // Jeunes
+      const filter = { ligneId: ligne._id };
+      const updateDoc = { $set: { sessionPhase1Id: session.id, cohesionCenterId: session.cohesionCenterId } };
+      await YoungModel.updateMany(filter, updateDoc, { fromUser: user });
path: api/src/planDeTransport/ligneDeBus/ligneDeBusService.ts
user: login: achorein
body: il faut passer la transaction à chaque save ou update :
```suggestion
      await YoungModel.updateMany(filter, updateDoc, { fromUser: user, session: transaction });
```
created_at: 2025-03-12T08:43:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -178,35 +207,80 @@ export const updatePDRForLine = async (
   );
 
   if (shouldSendEmailCampaign && youngUpdateResult.modifiedCount > 0) {
-    await sendEmailCampaign(ligneBusId, newMeetingPointId, cohort);
+    const updatedYoungs = await YoungModel.find({ ligneId: ligneBusId, meetingPointId: newMeetingPointId });
+    await notifyYoungsAndRlsPDRWasUpdated(updatedYoungs, cohort);
   }
 
   return ligneBus;
 };
 
-const sendEmailCampaign = async (ligneBusId: string, newMeetingPointId: string, cohort: CohortType) => {
-  const updatedYoungs = await YoungModel.find({
-    ligneId: ligneBusId,
-    meetingPointId: newMeetingPointId,
-  }).select("email parent1Email parent2Email");
-
-  let templateId: string | null = null;
-  if (new Date() < new Date(cohort.dateStart)) {
-    templateId = SENDINBLUE_TEMPLATES.young.CHANGE_PDR_BEFORE_DEPARTURE;
-  } else if (new Date() < new Date(cohort.dateEnd)) {
-    templateId = SENDINBLUE_TEMPLATES.young.CHANGE_PDR_BEFORE_RETURN;
+export const updateSessionForLine = async ({
+  ligne,
+  session,
+  user,
+  sendCampaign,
+}: {
+  ligne: LigneBusDocument;
+  session: SessionPhase1Document;
+  user: UserDto;
+  sendCampaign?: boolean;
+}) => {
+  const currentSessionPhase1 = await SessionPhase1Model.findById(ligne.sessionId);
+  if (!currentSessionPhase1) throw new Error(ERRORS.NOT_FOUND);
+
+  if (ligne.youngSeatsTaken > (session.placesLeft || 0)) {
+    throw new Error(ERRORS.OPERATION_NOT_ALLOWED);
   }
 
-  if (!templateId) throw new Error("Modification date is out of range, no email sent.");
-
-  for (const young of updatedYoungs) {
-    const cc: { email: string }[] = [];
-    if (young.parent1Email) cc.push({ email: young.parent1Email });
-    if (young.parent2Email) cc.push({ email: young.parent2Email });
-
-    await sendTemplate(templateId, {
-      emailTo: [{ email: young.email }],
-      cc,
+  const transaction = await startSession();
+
+  try {
+    await withTransaction(transaction, async () => {
+      // Ligne
+      ligne.set({
+        sessionId: session.id,
+        centerId: session.cohesionCenterId,
+      });
+      await ligne.save({ fromUser: user });
+
+      // Plan de transport
+      const planDeTransport = await PlanTransportModel.findById(ligne._id);
+      if (!planDeTransport) throw new Error(ERRORS.NOT_FOUND);
+      planDeTransport.set({
+        centerId: session.cohesionCenterId,
+        centerRegion: session.region,
+        centerDepartment: session.department,
+        centerAddress: session.cityCentre,
+        centerZip: session.zipCentre,
+        centerName: session.nameCentre,
+        centerCode: session.codeCentre,
+      });
+      await planDeTransport.save({ fromUser: user });
+
+      // Jeunes
+      const filter = { ligneId: ligne._id };
+      const updateDoc = { $set: { sessionPhase1Id: session.id, cohesionCenterId: session.cohesionCenterId } };
+      await YoungModel.updateMany(filter, updateDoc, { fromUser: user });
+      if (sendCampaign) {
path: api/src/planDeTransport/ligneDeBus/ligneDeBusService.ts
user: login: achorein
body: il faudrait envoyer les notifications après tous les updates (au cas où ça rollback avec la transaction)
created_at: 2025-03-12T08:43:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -178,35 +207,80 @@ export const updatePDRForLine = async (
   );
 
   if (shouldSendEmailCampaign && youngUpdateResult.modifiedCount > 0) {
-    await sendEmailCampaign(ligneBusId, newMeetingPointId, cohort);
+    const updatedYoungs = await YoungModel.find({ ligneId: ligneBusId, meetingPointId: newMeetingPointId });
+    await notifyYoungsAndRlsPDRWasUpdated(updatedYoungs, cohort);
   }
 
   return ligneBus;
 };
 
-const sendEmailCampaign = async (ligneBusId: string, newMeetingPointId: string, cohort: CohortType) => {
-  const updatedYoungs = await YoungModel.find({
-    ligneId: ligneBusId,
-    meetingPointId: newMeetingPointId,
-  }).select("email parent1Email parent2Email");
-
-  let templateId: string | null = null;
-  if (new Date() < new Date(cohort.dateStart)) {
-    templateId = SENDINBLUE_TEMPLATES.young.CHANGE_PDR_BEFORE_DEPARTURE;
-  } else if (new Date() < new Date(cohort.dateEnd)) {
-    templateId = SENDINBLUE_TEMPLATES.young.CHANGE_PDR_BEFORE_RETURN;
+export const updateSessionForLine = async ({
+  ligne,
+  session,
+  user,
+  sendCampaign,
+}: {
+  ligne: LigneBusDocument;
+  session: SessionPhase1Document;
+  user: UserDto;
+  sendCampaign?: boolean;
+}) => {
+  const currentSessionPhase1 = await SessionPhase1Model.findById(ligne.sessionId);
+  if (!currentSessionPhase1) throw new Error(ERRORS.NOT_FOUND);
+
+  if (ligne.youngSeatsTaken > (session.placesLeft || 0)) {
+    throw new Error(ERRORS.OPERATION_NOT_ALLOWED);
   }
 
-  if (!templateId) throw new Error("Modification date is out of range, no email sent.");
-
-  for (const young of updatedYoungs) {
-    const cc: { email: string }[] = [];
-    if (young.parent1Email) cc.push({ email: young.parent1Email });
-    if (young.parent2Email) cc.push({ email: young.parent2Email });
-
-    await sendTemplate(templateId, {
-      emailTo: [{ email: young.email }],
-      cc,
+  const transaction = await startSession();
+
+  try {
+    await withTransaction(transaction, async () => {
+      // Ligne
+      ligne.set({
+        sessionId: session.id,
+        centerId: session.cohesionCenterId,
+      });
+      await ligne.save({ fromUser: user });
path: api/src/planDeTransport/ligneDeBus/ligneDeBusService.ts
user: login: achorein
body: ```suggestion
      await ligne.save({ fromUser: user, session: transaction });
```
created_at: 2025-03-12T08:43:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,11 @@
+import { useQuery } from "@tanstack/react-query";
+import { SessionPhase1Service } from "../../../services/sessionPhase1Service";
+
+export default function useSessionsByCohort(cohortName: string) {
path: admin/src/scenes/plan-transport/lib/useSessionsByCohort.ts
user: login: nicobret
body: Ce hook sert à obtenir et mettre en cache les sessionPhase1 et non les cohortes (toujours ce problème de naming). Il permet dans cette PR d'accéder aux sessions dans le select de centre et dans la modale de confirmation.
created_at: 2025-03-12T09:28:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,119 @@
+import { AuthState } from "@/redux/auth/reducer";
+import React from "react";
+import { useSelector } from "react-redux";
+import CentreLabel from "./CentreLabel";
+import { isSuperAdmin, LigneBusDto, ROLES } from "snu-lib";
+import { BsSearch } from "react-icons/bs";
+import useSessions from "@/scenes/plan-transport/lib/useSessionsByCohort";
+import { AiOutlineStop } from "react-icons/ai";
+import Loader from "@/components/Loader";
+import { SessionPhase1PopulatedWithCenter } from "@/services/sessionPhase1Service";
+
+type Props = {
+  sessionId: string;
+  setSessionId: (string) => void;
+  ligne: LigneBusDto;
+  disabled: boolean;
+};
+
+function filterResults(data: SessionPhase1PopulatedWithCenter[], search: string) {
+  if (search === "") return data;
+  const searchFields = ["name", "address", "region", "department", "code", "city", "zip", "matricule"];
+  return data.filter((item) => {
+    return searchFields.some((field) => item.cohesionCenter[field]?.toString().toLowerCase().includes(search.toLowerCase()));
+  });
+}
+
+export default function SessionSelector({ sessionId, setSessionId, ligne, disabled }: Props) {
+  const user = useSelector((state: AuthState) => state.Auth.user);
+  const [search, setSearch] = React.useState("");
+  const { data: sessions, isPending, isError } = useSessions(ligne.cohort);
+  const [open, setOpen] = React.useState(false);
+  const refSelect = React.useRef<HTMLDivElement>(null);
+  const refInput = React.useRef<HTMLInputElement>(null);
+  const refButtonChangesPDR = React.useRef<HTMLButtonElement>(null);
+
+  React.useEffect(() => {
path: admin/src/scenes/plan-transport/ligne-bus/View/components/Centre/SessionSelector.tsx
user: login: nicobret
body: J'ai repris le select des PDR. Il faudrait effectivement l'améliorer et le factoriser mais c'est hors scope (cette PR traîne depuis deux semaines parce qu'on n'arrête pas d'ajouter des refacto, là c'est stop 😄  ).
created_at: 2025-03-12T09:31:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -178,35 +207,80 @@ export const updatePDRForLine = async (
   );
 
   if (shouldSendEmailCampaign && youngUpdateResult.modifiedCount > 0) {
-    await sendEmailCampaign(ligneBusId, newMeetingPointId, cohort);
+    const updatedYoungs = await YoungModel.find({ ligneId: ligneBusId, meetingPointId: newMeetingPointId });
+    await notifyYoungsAndRlsPDRWasUpdated(updatedYoungs, cohort);
   }
 
   return ligneBus;
 };
 
-const sendEmailCampaign = async (ligneBusId: string, newMeetingPointId: string, cohort: CohortType) => {
-  const updatedYoungs = await YoungModel.find({
-    ligneId: ligneBusId,
-    meetingPointId: newMeetingPointId,
-  }).select("email parent1Email parent2Email");
-
-  let templateId: string | null = null;
-  if (new Date() < new Date(cohort.dateStart)) {
-    templateId = SENDINBLUE_TEMPLATES.young.CHANGE_PDR_BEFORE_DEPARTURE;
-  } else if (new Date() < new Date(cohort.dateEnd)) {
-    templateId = SENDINBLUE_TEMPLATES.young.CHANGE_PDR_BEFORE_RETURN;
+export const updateSessionForLine = async ({
+  ligne,
+  session,
+  user,
+  sendCampaign,
+}: {
+  ligne: LigneBusDocument;
+  session: SessionPhase1Document;
+  user: UserDto;
+  sendCampaign?: boolean;
+}) => {
+  const currentSessionPhase1 = await SessionPhase1Model.findById(ligne.sessionId);
+  if (!currentSessionPhase1) throw new Error(ERRORS.NOT_FOUND);
+
+  if (ligne.youngSeatsTaken > (session.placesLeft || 0)) {
+    throw new Error(ERRORS.OPERATION_NOT_ALLOWED);
   }
 
-  if (!templateId) throw new Error("Modification date is out of range, no email sent.");
-
-  for (const young of updatedYoungs) {
-    const cc: { email: string }[] = [];
-    if (young.parent1Email) cc.push({ email: young.parent1Email });
-    if (young.parent2Email) cc.push({ email: young.parent2Email });
-
-    await sendTemplate(templateId, {
-      emailTo: [{ email: young.email }],
-      cc,
+  const transaction = await startSession();
+
+  try {
+    await withTransaction(transaction, async () => {
+      // Ligne
+      ligne.set({
+        sessionId: session.id,
+        centerId: session.cohesionCenterId,
+      });
+      await ligne.save({ fromUser: user });
+
+      // Plan de transport
+      const planDeTransport = await PlanTransportModel.findById(ligne._id);
+      if (!planDeTransport) throw new Error(ERRORS.NOT_FOUND);
+      planDeTransport.set({
+        centerId: session.cohesionCenterId,
+        centerRegion: session.region,
+        centerDepartment: session.department,
+        centerAddress: session.cityCentre,
+        centerZip: session.zipCentre,
+        centerName: session.nameCentre,
+        centerCode: session.codeCentre,
+      });
+      await planDeTransport.save({ fromUser: user });
+
+      // Jeunes
+      const filter = { ligneId: ligne._id };
+      const updateDoc = { $set: { sessionPhase1Id: session.id, cohesionCenterId: session.cohesionCenterId } };
+      await YoungModel.updateMany(filter, updateDoc, { fromUser: user });
+      if (sendCampaign) {
path: api/src/planDeTransport/ligneDeBus/ligneDeBusService.ts
user: login: nicobret
body: Bien vu.
created_at: 2025-03-12T10:01:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,118 @@
+import React from "react";
+import { Button, Modal } from "@snu/ds/admin";
+import XCircleFull from "@/assets/icons/XCircleFull";
+import CheckCircleFull from "@/assets/icons/CheckCircleFull";
+import { useToggle } from "react-use";
+import { LigneBusDto, SENDINBLUE_TEMPLATES } from "snu-lib";
+import CentreLabel from "./CentreLabel";
+import { TfiReload } from "react-icons/tfi";
+import { HiExternalLink } from "react-icons/hi";
+import useSessions from "@/scenes/plan-transport/lib/useSessionsByCohort";
+import { CentreFormValues } from ".";
+
+interface ConfirmChangesModalProps {
+  isOpen: boolean;
+  count: number;
+  initialData: LigneBusDto;
+  formData: { sessionId: string; centerArrivalTime: string; centerDepartureTime: string };
+  onConfirm: (data: CentreFormValues, emailing: boolean) => void;
+  onCancel: () => void;
+  isLoading?: boolean;
+}
+
+export default function CentreModal({ isOpen, initialData, formData, count, onConfirm, onCancel, isLoading }: ConfirmChangesModalProps) {
+  const [isEmailing, toggleEmailing] = useToggle(false);
+  const templateId = SENDINBLUE_TEMPLATES.young.PHASE_1_CHANGEMENT_CENTRE;
+  const templateIdReferent = SENDINBLUE_TEMPLATES.CLE.PHASE_1_MODIFICATION_LIGNE;
+  const isCLE = initialData.cohort.includes("CLE");
+  const { data } = useSessions(initialData.cohort);
+  const initialSession = data?.find((s) => s._id === initialData.sessionId);
+  const newSession = data?.find((s) => s._id === formData.sessionId);
+
+  const handleConfirm = () => {
+    onConfirm(formData, isEmailing);
+  };
+
+  return (
+    <Modal
+      isOpen={isOpen}
+      onClose={onCancel}
+      className="max-w-3xl"
+      content={
+        <div className="scroll-y-auto overflow-y-auto">
+          <div className="flex flex-col items-center text-center gap-6">
+            <div className="flex items-center justify-center w-12 h-12 rounded-full bg-gray-50">
+              <TfiReload className="w-5 h-5 text-gray-800" />
+            </div>
+            <h1 className="font-medium text-xl max-w-sm">Êtes-vous sûr(e) de vouloir changer de centre de cohésion&nbsp;?</h1>
+          </div>
+
+          <div className="mt-8 grid grid-cols-2 gap-3">
+            <div>
+              <p className="text-xs font-medium">Ancien centre</p>
+              <CenterCard
+                centerDetail={initialSession?.cohesionCenter}
+                centerArrivalTime={initialData.centerArrivalTime}
+                centerDepartureTime={initialData.centerDepartureTime}
+                icon={<XCircleFull className="text-red-500" />}
+              />
+            </div>
+
+            <div>
+              <p className="text-xs font-medium">Nouveau centre</p>
+              <CenterCard
+                centerDetail={newSession?.cohesionCenter}
+                centerArrivalTime={formData.centerArrivalTime!}
+                centerDepartureTime={formData.centerDepartureTime!}
+                icon={<CheckCircleFull className="text-green-500" />}
+              />
+            </div>
+          </div>
+
+          <div className="mt-8 border-t border-gray-200 w-full pt-8">
+            <label className="flex items-start space-x-2">
+              <input type="checkbox" className="mt-1" checked={isEmailing} onChange={toggleEmailing} />
+              <div className="leading-relaxed">
+                <p>Envoyer une campagne d’emailing aux volontaires ({count}) et à leurs représentants légaux.</p>
+                {isCLE && <p>Les référent de classe, chef d’établissement et coordinateurs seront également prévenus.</p>}
+                <div className="mt-2">
+                  <a href={`/email-preview/${templateId}`} target="_blank" rel="noreferrer" className="text-gray-500 hover:text-gray-800 underline underline-offset-2">
+                    Visualiser l'aperçu pour les volontaires
+                    <HiExternalLink className="inline-block w-4 h-4 ml-1" />
+                  </a>
+                  <br />
+                  {isCLE && (
+                    <a href={`/email-preview/${templateIdReferent}`} target="_blank" rel="noreferrer" className="text-gray-500 hover:text-gray-800 underline underline-offset-2">
+                      Visualiser l'aperçu pour les référents CLE
+                      <HiExternalLink className="inline-block w-4 h-4 ml-1" />
+                    </a>
+                  )}
+                </div>
+              </div>
+            </label>
+          </div>
+          <br />
+        </div>
+      }
+      footer={
+        <div className="grid grid-cols-2 gap-3">
+          <Button title="Annuler" type="secondary" onClick={onCancel} className="w-full" />
+          <Button title={isLoading ? "Modification en cours" : "Oui, valider"} onClick={handleConfirm} disabled={isLoading} className="w-full" />
path: admin/src/scenes/plan-transport/ligne-bus/View/components/Centre/CentreModal.tsx
user: login: nicobret
body: Ah c'est top
created_at: 2025-03-12T10:08:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,11 @@
+import { useQuery } from "@tanstack/react-query";
+import { SessionPhase1Service } from "../../../services/sessionPhase1Service";
+
+export default function useSessionsByCohort(cohortName: string) {
+  const filters = { cohort: [cohortName] };
+  return useQuery({
path: admin/src/scenes/plan-transport/lib/useSessionsByCohort.ts
user: login: nicobret
body: Il y a bien plusieurs sessions par cohorte.
created_at: 2025-03-12T10:10:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,79 @@
+import React, { useId } from "react";
+import { HiOutlinePencil } from "react-icons/hi";
+import ReactTooltip from "react-tooltip";
+
+type Props = {
+  form?: string;
+  isEditing: boolean;
+  setIsEditing: (value: boolean) => void;
+  disabled: boolean;
+  isLoading: boolean;
+  reset: () => void;
+  tooltipMessage?: string;
+};
+
+export default function EditButton({
path: packages/ds/src/admin/form/EditButton.tsx
user: login: achorein
body: pour moi ce composant ne doit pas être dans le DS, dans le DS il y a déjà  le `Button` classique qui gère tout les design, avec une propriété `leftIcon` etc.
Sinon il devrait plutôt se nommer `<EditButtonGroup>`, ou 
d'ailleurs ici les composant `<button>` natif sont à éviter si on a déjà `<Button>` du DS qui gère tout, ça fait de la duplication de code.
created_at: 2025-03-12T10:19:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,79 @@
+import React, { useId } from "react";
+import { HiOutlinePencil } from "react-icons/hi";
+import ReactTooltip from "react-tooltip";
+
+type Props = {
+  form?: string;
+  isEditing: boolean;
+  setIsEditing: (value: boolean) => void;
path: packages/ds/src/admin/form/EditButton.tsx
user: login: achorein
body: ```suggestion
  onChange: (value: boolean) => void;
```
created_at: 2025-03-12T10:21:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,79 @@
+import React, { useId } from "react";
+import { HiOutlinePencil } from "react-icons/hi";
+import ReactTooltip from "react-tooltip";
+
+type Props = {
+  form?: string;
+  isEditing: boolean;
+  setIsEditing: (value: boolean) => void;
+  disabled: boolean;
+  isLoading: boolean;
+  reset: () => void;
path: packages/ds/src/admin/form/EditButton.tsx
user: login: achorein
body: ```suggestion
  onReset: () => void;
```
created_at: 2025-03-12T10:22:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,79 @@
+import React, { useId } from "react";
+import { HiOutlinePencil } from "react-icons/hi";
+import ReactTooltip from "react-tooltip";
+
+type Props = {
+  form?: string;
+  isEditing: boolean;
+  setIsEditing: (value: boolean) => void;
+  disabled: boolean;
+  isLoading: boolean;
+  reset: () => void;
+  tooltipMessage?: string;
+};
+
+export default function EditButton({
path: packages/ds/src/admin/form/EditButton.tsx
user: login: nicobret
body: C'est toi qui m'a suggéré de le mettre dans le DS 😄 
Encore une fois le composant Button du DS est différent, à la fois au niveau de l'usage et du design.

EditButton :

![Screenshot 2025-03-12 at 11 32 23](https://github.com/user-attachments/assets/15e8c3cc-76a5-49c0-8058-1414fd885416)

Button :

![Screenshot 2025-03-12 at 11 32 17](https://github.com/user-attachments/assets/bf12316f-234d-4e2e-af5d-ba4b110fde80)

created_at: 2025-03-12T10:39:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,79 @@
+import React, { useId } from "react";
+import { HiOutlinePencil } from "react-icons/hi";
+import ReactTooltip from "react-tooltip";
+
+type Props = {
+  form?: string;
+  isEditing: boolean;
+  setIsEditing: (value: boolean) => void;
path: packages/ds/src/admin/form/EditButton.tsx
user: login: nicobret
body: onChange suggère qu'il faut fournir un callback pour gérer la validation des changements, mais ce n'est pas ce qui est attendu ici. Ce bouton gère le mode édition ouverte/fermée du composant dont il fait partie.
created_at: 2025-03-12T10:42:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,79 @@
+import React, { useId } from "react";
+import { HiOutlinePencil } from "react-icons/hi";
+import ReactTooltip from "react-tooltip";
+
+type Props = {
+  form?: string;
+  isEditing: boolean;
+  setIsEditing: (value: boolean) => void;
path: packages/ds/src/admin/form/EditButton.tsx
user: login: achorein
body: onChange indique que l'état du composant à changer rien de plus, si c'est ambigu on peut presiser `onEditChange`
created_at: 2025-03-12T10:52:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,79 @@
+import React, { useId } from "react";
+import { HiOutlinePencil } from "react-icons/hi";
+import ReactTooltip from "react-tooltip";
+
+type Props = {
+  form?: string;
+  isEditing: boolean;
+  setIsEditing: (value: boolean) => void;
+  disabled: boolean;
+  isLoading: boolean;
+  reset: () => void;
+  tooltipMessage?: string;
+};
+
+export default function EditButton({
path: packages/ds/src/admin/form/EditButton.tsx
user: login: achorein
body: pour moi le bouton "Annuler" et le bouton "Enregistrer les changements" utilise le même composant DS.

en fait le bleu clair est legacy pour moi et devrait reprendre le blue plus foncer du DS en ajoutant l'icon à gauche. l'idée est justement de ne pas séparpiller.

mais sinon c'est rajouté un "type" spécifique au DS pour gérer le bleu clair.
created_at: 2025-03-12T10:54:45Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-11T10:17:33Z
body: Application endpoints :

- https://snuevolutionnotifschwdqf9qqw-admin.functions.fnc.fr-par.scw.cloud
- https://snuevolutionnotifschwdqf9qqw-app.functions.fnc.fr-par.scw.cloud
- https://snuevolutionnotifschwdqf9qqw-api.functions.fnc.fr-par.scw.cloud
- https://snuevolutionnotifschwdqf9qqw-apiv2.functions.fnc.fr-par.scw.cloud
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-12T08:09:59Z
body: Application endpoints :

- https://snuevolutionnotifschplujxver-admin.functions.fnc.fr-par.scw.cloud
- https://snuevolutionnotifschplujxver-app.functions.fnc.fr-par.scw.cloud
- https://snuevolutionnotifschplujxver-api.functions.fnc.fr-par.scw.cloud
- https://snuevolutionnotifschplujxver-apiv2.functions.fnc.fr-par.scw.cloud
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-12T10:36:06Z
body: Application endpoints :

- https://snuevolutionnotifschplujxver-admin.functions.fnc.fr-par.scw.cloud
- https://snuevolutionnotifschplujxver-app.functions.fnc.fr-par.scw.cloud
- https://snuevolutionnotifschplujxver-api.functions.fnc.fr-par.scw.cloud
- https://snuevolutionnotifschplujxver-apiv2.functions.fnc.fr-par.scw.cloud
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-12T13:21:26Z
body: Application endpoints :

- https://snuevolutionnotifschokfmx1iw-admin.functions.fnc.fr-par.scw.cloud
- https://snuevolutionnotifschokfmx1iw-app.functions.fnc.fr-par.scw.cloud
- https://snuevolutionnotifschokfmx1iw-api.functions.fnc.fr-par.scw.cloud
- https://snuevolutionnotifschokfmx1iw-apiv2.functions.fnc.fr-par.scw.cloud
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-13T08:09:03Z
body: Application endpoints :

- https://snuevolutionnotifschpzowvrq1-admin.functions.fnc.fr-par.scw.cloud
- https://snuevolutionnotifschpzowvrq1-app.functions.fnc.fr-par.scw.cloud
- https://snuevolutionnotifschpzowvrq1-api.functions.fnc.fr-par.scw.cloud
- https://snuevolutionnotifschpzowvrq1-apiv2.functions.fnc.fr-par.scw.cloud
author_association: NONE