Repository: MTES-MCT/histologe
Pull request #3926: [BO - Visites] Intervenant Visite - Externe
Author: login: numew
Created: 2025-03-31T10:35:09Z | Merged: 2025-04-09T10:00:24Z

## Ticket

#3742

## Description
Ajout de la possibilité de sélectionner un "Opérateur externe" lors de la création/modification d'une visite

## Changements apportés
* Ajout d'une colonne sur la table intervention pour enregistrer le nom de l'opérateur externe de visite
* Mise au propre du html des modale d'ajout/édition de visites
* Corrections et ajout de tests sur les visites

## Pré-requis
`make execute-migration name=Version20250328162438 direction=up`
`make npm-build`

## Tests
- [ ] Faire quelques tests arbitraire sur ce qui concerne les visites 



Diff:
diff --git a/assets/scripts/vanilla/controllers/form_visite.js b/assets/scripts/vanilla/controllers/form_visite.js
index 83a4f2b960..baf0d0b7b8 100644
--- a/assets/scripts/vanilla/controllers/form_visite.js
+++ b/assets/scripts/vanilla/controllers/form_visite.js
@@ -12,14 +12,32 @@ dateFields.forEach(dateField => {
   dateField.addEventListener('change', evt => {
     const fieldToToggle = dateField.dataset.fields
 
-    if (dateField.value <= localDateString) {
+    if (dateField.value && dateField.value <= localDateString) {
       document.querySelector('#' + fieldToToggle).classList.remove('fr-hidden')
     } else {
       document.querySelector('#' + fieldToToggle).classList.add('fr-hidden')
     }
   })
+  dateField.dispatchEvent(new Event('change'))
 })
 
+const selectVisitePartner = document.querySelectorAll('.visite-partner-select')
+selectVisitePartner.forEach(partnerSelect => {
+  const operatorExtern = partnerSelect.parentElement.nextElementSibling
+  const operatorExternField = operatorExtern.querySelector('input')
+  partnerSelect.addEventListener('change', evt => {
+    if (evt.currentTarget.value === 'extern') {
+      operatorExtern.classList.remove('fr-hidden')
+      operatorExternField.setAttribute('required', 'required')
+    }else {
+      operatorExtern.classList.add('fr-hidden')
+      operatorExternField.removeAttribute('required')
+    }
+  })
+  partnerSelect.dispatchEvent(new Event('change'))
+})
+
+
 histoCheckVisiteForms('add')
 histoCheckVisiteForms('reschedule')
 histoCheckVisiteForms('confirm')
@@ -49,7 +67,14 @@ function histoCheckVisiteForms (formType) {
       if (selectVisitePartner) {
         const selectVisitePartnerError = visiteForm.querySelector('#signalement-' + formType + '-visite-partner-double-error')
         selectVisitePartnerError.classList.add('fr-hidden')
-        if (selectVisitePartner.selectedOptions[0].classList.contains('alert-partner')) {
+        if(selectVisitePartner.value === 'extern') {
+          const operatorExternField = visiteForm.querySelector('.visite-external-operator')
+          const operatorNames = JSON.parse(document.getElementById('list-pending-visite-external-operator-names').dataset.list)
+          if(operatorNames.includes(operatorExternField.value)) {
+            selectVisitePartnerError.classList.remove('fr-hidden')
+            stopSubmit = true
+          }
+        }else if (selectVisitePartner.selectedOptions[0].classList.contains('alert-partner')) {
           selectVisitePartnerError.classList.remove('fr-hidden')
           stopSubmit = true
         }
diff --git a/assets/styles/histologe.scss b/assets/styles/histologe.scss
index f8e6c1cc1a..4ca883cbab 100644
--- a/assets/styles/histologe.scss
+++ b/assets/styles/histologe.scss
@@ -175,7 +175,7 @@ pre {
 }
 
 legend.required::after, label.required::after {
-    color: #F60700;
+    color: var(--text-default-error);
     content: '*';
     position: relative;
     left: 3px;
diff --git a/migrations/Version20250328162438.php b/migrations/Version20250328162438.php
new file mode 100644
index 0000000000..d30ad845ed
--- /dev/null
+++ b/migrations/Version20250328162438.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace DoctrineMigrations;
+
+use Doctrine\DBAL\Schema\Schema;
+use Doctrine\Migrations\AbstractMigration;
+
+final class Version20250328162438 extends AbstractMigration
+{
+    public function getDescription(): string
+    {
+        return 'Add external_operator column to intervention table';
+    }
+
+    public function up(Schema $schema): void
+    {
+        $this->addSql('ALTER TABLE intervention ADD external_operator VARCHAR(255) DEFAULT NULL');
+    }
+
+    public function down(Schema $schema): void
+    {
+        $this->addSql('ALTER TABLE intervention DROP external_operator');
+    }
+}
diff --git a/src/Controller/Back/SignalementVisitesController.php b/src/Controller/Back/SignalementVisitesController.php
index e6c55d4e34..2cea06d785 100755
--- a/src/Controller/Back/SignalementVisitesController.php
+++ b/src/Controller/Back/SignalementVisitesController.php
@@ -92,12 +92,14 @@ public function addVisiteToSignalement(
         $fileName = $this->getUploadedFile($request, 'visite-add', $uploadHandler, $filenameGenerator);
 
         $requestAddData = $request->get('visite-add');
+        $idPartner = 'extern' === $requestAddData['partner'] ? null : $requestAddData['partner'];
         $visiteRequest = new VisiteRequest(
             idIntervention: $requestAddData['intervention'] ?? null,
             date: $requestAddData['date'],
             time: $requestAddData['time'],
             timezone: $timezoneProvider->getTimezone(),
-            idPartner: $requestAddData['partner'],
+            idPartner: $idPartner,
+            externalOperator: $requestAddData['externalOperator'],
             details: $requestAddData['details'] ?? null,
             concludeProcedure: $requestAddData['concludeProcedure'] ?? null,
             isVisiteDone: $requestAddData['visiteDone'] ?? null,
@@ -210,12 +212,14 @@ public function rescheduleVisiteFromSignalement(
         $previousDate = $intervention->getScheduledAt();
         $fileName = $this->getUploadedFile($request, 'visite-reschedule', $uploadHandler, $filenameGenerator);
 
+        $idPartner = 'extern' === $requestRescheduleData['partner'] ? null : $requestRescheduleData['partner'];
         $visiteRequest = new VisiteRequest(
             idIntervention: $requestRescheduleData['intervention'],
             date: $requestRescheduleData['date'],
             time: $requestRescheduleData['time'],
             timezone: $timezoneProvider->getTimezone(),
-            idPartner: $requestRescheduleData['partner'] ?? $intervention->getPartner()?->getId(),
+            idPartner: $idPartner,
+            externalOperator: $requestRescheduleData['externalOperator'],
             details: $requestRescheduleData['details'] ?? null,
             concludeProcedure: $requestRescheduleData['concludeProcedure'] ?? null,
             isVisiteDone: $requestRescheduleData['visiteDone'] ?? null,
diff --git a/src/Dto/Request/Signalement/VisiteRequest.php b/src/Dto/Request/Signalement/VisiteRequest.php
index 39a65ff755..2992f01044 100644
--- a/src/Dto/Request/Signalement/VisiteRequest.php
+++ b/src/Dto/Request/Signalement/VisiteRequest.php
@@ -4,6 +4,7 @@
 
 use App\Service\TimezoneProvider;
 use Symfony\Component\Validator\Constraints as Assert;
+use Symfony\Component\Validator\Context\ExecutionContextInterface;
 
 class VisiteRequest
 {
@@ -18,8 +19,8 @@ public function __construct(
         #[Assert\DateTime('H:i')]
         private readonly ?string $time = null,
         private readonly ?string $timezone = TimezoneProvider::TIMEZONE_EUROPE_PARIS,
-        #[Assert\NotBlank]
         private readonly ?int $idPartner = null,
+        private readonly ?string $externalOperator = null,
         private readonly ?string $details = null,
         private readonly ?array $concludeProcedure = [],
         private readonly ?bool $isVisiteDone = null,
@@ -30,6 +31,14 @@ public function __construct(
     ) {
     }
 
+    #[Assert\Callback]
+    public function validate(ExecutionContextInterface $context, $payload)
+    {
+        if (null === $this->idPartner && empty(trim((string) $this->externalOperator))) {
+            $context->buildViolation('Le nom de l\'opérateur externe est requis')->atPath('externalOperator')->addViolation();
+        }
+    }
+
     public function getIntervention(): ?int
     {
         return $this->idIntervention;
@@ -76,6 +85,11 @@ public function getPartner(): ?int
         return $this->idPartner;
     }
 
+    public function getExternalOperator(): ?string
+    {
+        return $this->externalOperator;
+    }
+
     public function getDetails(): ?string
     {
         return $this->details;
diff --git a/src/Entity/Intervention.php b/src/Entity/Intervention.php
index ee3afe7792..aa8a3d094a 100644
--- a/src/Entity/Intervention.php
+++ b/src/Entity/Intervention.php
@@ -8,6 +8,7 @@
 use App\Entity\Enum\DocumentType;
 use App\Entity\Enum\HistoryEntryEvent;
 use App\Entity\Enum\InterventionType;
+use App\Entity\Enum\PartnerType;
 use App\Entity\Enum\ProcedureType;
 use App\Repository\InterventionRepository;
 use App\Service\TimezoneProvider;
@@ -89,6 +90,9 @@ class Intervention implements EntityHistoryInterface, EntitySanitizerInterface
     #[ORM\OneToMany(mappedBy: 'intervention', targetEntity: File::class, cascade: ['persist'])]
     private Collection $files;
 
+    #[ORM\Column(length: 255, nullable: true)]
+    private ?string $externalOperator = null;
+
     public function __construct()
     {
         $this->files = new ArrayCollection();
@@ -161,6 +165,14 @@ public function setSignalement(?Signalement $signalement): self
 
     public function getPartner(): ?Partner
     {
+        if (!$this->partner && $this->externalOperator) {
+            $externalPartner = new Partner();
+            $externalPartner->setNom($this->externalOperator);
+            $externalPartner->setType(PartnerType::OPERATEUR_VISITES_ET_TRAVAUX);
+
+            return $externalPartner;
+        }
+
         return $this->partner;
     }
 
@@ -371,4 +383,16 @@ public function getDescription(): string
     {
         return $this->getDetails() ?? '';
     }
+
+    public function getExternalOperator(): ?string
+    {
+        return $this->externalOperator;
+    }
+
+    public function setExternalOperator(?string $externalOperator): static
+    {
+        $this->externalOperator = $externalOperator;
+
+        return $this;
+    }
 }
diff --git a/src/Manager/InterventionManager.php b/src/Manager/InterventionManager.php
index 505696da51..f1be288eaf 100644
--- a/src/Manager/InterventionManager.php
+++ b/src/Manager/InterventionManager.php
@@ -49,22 +49,26 @@ public function __construct(
      */
     public function createVisiteFromRequest(Signalement $signalement, VisiteRequest $visiteRequest): ?Intervention
     {
-        if (!$visiteRequest->getDate() || !$visiteRequest->getPartner()) {
+        if (!$visiteRequest->getDate()) {
             return null;
         }
 
-        $partnerFound = $this->partnerManager->getPartnerIfQualification(
-            $visiteRequest->getPartner(),
-            Qualification::VISITES,
-            $signalement->getTerritory()
-        );
-        if (!$partnerFound) {
-            return null;
+        $partnerFound = null;
+        if ($visiteRequest->getPartner()) {
+            $partnerFound = $this->partnerManager->getPartnerIfQualification(
+                $visiteRequest->getPartner(),
+                Qualification::VISITES,
+                $signalement->getTerritory()
+            );
+            if (!$partnerFound) {
+                return null;
+            }
         }
 
         $intervention = new Intervention();
         $intervention->setSignalement($signalement)
             ->setPartner($partnerFound)
+            ->setExternalOperator($visiteRequest->getExternalOperator())
             ->setScheduledAt(new \DateTimeImmutable($visiteRequest->getDateTimeUTC()))
             ->setType(InterventionType::VISITE)
             ->setStatus(Intervention::STATUS_PLANNED);
@@ -107,7 +111,7 @@ public function cancelVisiteFromRequest(VisiteRequest $visiteRequest): ?Interven
      */
     public function rescheduleVisiteFromRequest(Signalement $signalement, VisiteRequest $visiteRequest): ?Intervention
     {
-        if (!$visiteRequest->getIntervention() || !$visiteRequest->getDate() || !$visiteRequest->getPartner()) {
+        if (!$visiteRequest->getIntervention() || !$visiteRequest->getDate()) {
             return null;
         }
 
@@ -116,17 +120,21 @@ public function rescheduleVisiteFromRequest(Signalement $signalement, VisiteRequ
             return null;
         }
 
-        $partnerFound = $this->partnerManager->getPartnerIfQualification(
-            $visiteRequest->getPartner(),
-            Qualification::VISITES,
-            $signalement->getTerritory()
-        );
-        if (!$partnerFound) {
-            return null;
+        $partnerFound = null;
+        if ($visiteRequest->getPartner()) {
+            $partnerFound = $this->partnerManager->getPartnerIfQualification(
+                $visiteRequest->getPartner(),
+                Qualification::VISITES,
+                $signalement->getTerritory()
+            );
+            if (!$partnerFound) {
+                return null;
+            }
         }
 
         $intervention
             ->setPartner($partnerFound)
+            ->setExternalOperator($visiteRequest->getExternalOperator())
             ->setScheduledAt(new \DateTimeImmutable($visiteRequest->getDateTimeUTC()));
         $this->save($intervention);
 
diff --git a/templates/back/signalement/view/visites/modals/visites-modal-add.html.twig b/templates/back/signalement/view/visites/modals/visites-modal-add.html.twig
index cf0eb1caed..0af0fd67be 100755
--- a/templates/back/signalement/view/visites/modals/visites-modal-add.html.twig
+++ b/templates/back/signalement/view/visites/modals/visites-modal-add.html.twig
@@ -31,45 +31,26 @@
 
                             <div class="fr-grid-row fr-grid-row--gutters fr-grid-row--middle fr-my-3w">
                                 <div class="fr-col-12 fr-mb-3v">
-                                    <fieldset class="fr-fieldset fr-fieldset--inline fr-mb-5v">
-                                        <legend class="fr-fieldset__legend fr-text--regular required">
-                                            Date de la visite
-                                        </legend>
-                                        <p id="signalement-add-visite-date-error" class="fr-error-text fr-hidden fr-my-3v">
-                                            Veuillez préciser la date de la visite.
-                                        </p>
+                                    <div class="fr-input-group">
+                                        <label class="fr-label" for="visite-add[date]">Date de la visite <span class="fr-text-default--error">*</span></label>
                                         <input type="date"
                                                class="fr-input add-fields-if-past-date"
                                                data-fields="visite-add-past-date-complementary-fields"
                                                name="visite-add[date]" required>
-                                    </fieldset>
+                                    </div>
 
-                                    <fieldset class="fr-fieldset fr-fieldset--inline fr-mb-5v">
-                                        <legend class="fr-fieldset__legend fr-text--regular">
-                                            Heure de la visite
-                                        </legend>
-                                        <p id="signalement-add-visite-date-error" class="fr-error-text fr-hidden fr-my-3v">
-                                            Veuillez préciser l'heure de la visite.
-                                        </p>
+                                    <div class="fr-input-group">
+                                        <label class="fr-label" for="visite-add[time]">Heure de la visite</label>
                                         <input type="time"
                                                class="fr-input"
                                                data-territory-timezone="{{ territory_timezone }}"
                                                data-fields="visite-add-past-date-complementary-fields"
                                                name="visite-add[time]">
-                                    </fieldset>
+                                    </div>
                                     
                                     {% if is_granted('ROLE_ADMIN_TERRITORY') %}
-                                        <fieldset class="fr-fieldset fr-fieldset--inline fr-mb-5v">
-                                            <legend class="fr-fieldset__legend fr-text--regular required">
-                                                Opérateur de visite
-                                            </legend>
-                                            <p id="signalement-add-visite-partner-error" class="fr-error-text fr-hidden fr-my-3v">
-                                                Veuillez sélectionner le partenaire en charge de la visite
-                                            </p>
-                                            <p id="signalement-add-visite-partner-double-error" class="fr-error-text fr-hidden fr-my-3v">
-                                                Ce partenaire a déjà une visite en cours.
-                                                Veuillez terminer ou annuler la visite ou sélectionner un autre partenaire.
-                                            </p>
+                                        <div class="fr-select-group">
+                                            <label class="fr-label" for="visite-add[partner]">Opérateur de visite <span class="fr-text-default--error">*</span></label>
                                             <select name="visite-add[partner]" class="fr-select visite-partner-select" required>
                                                 <option value=""></option>
                                                 {% for partner in partnersCanVisite %}
@@ -86,8 +67,17 @@
                                                         {{ partner.nom|upper }}
                                                     </option>
                                                 {% endfor %}
+                                                <option value="extern">Opérateur Externe</option>
                                             </select>
-                                        </fieldset>
+                                            <p id="signalement-add-visite-partner-double-error" class="fr-error-text fr-hidden">
+                                                Ce partenaire a déjà une visite en cours.
+                                                Veuillez terminer ou annuler la visite ou sélectionner un autre partenaire.
+                                            </p>
+                                        </div>
+                                        <div class="fr-input-group fr-hidden">
+                                            <label class="fr-label" for="visite-add[externalOperator]">Nom de l'opérateur externe <span class="fr-text-default--error">*</span></label>
+                                            <input type="text" name="visite-add[externalOperator]" maxlength="255" class="fr-input visite-external-operator">
+                                        </div>
                                     {% else %}
                                         <input type="hidden" name="visite-add[partner]" value="{{ app.user.partnerInTerritory(signalement.territory).id }}">
                                     {% endif %}
diff --git a/templates/back/signalement/view/visites/modals/visites-modal-reschedule.html.twig b/templates/back/signalement/view/visites/modals/visites-modal-reschedule.html.twig
index 78e651e294..a211431592 100755
--- a/templates/back/signalement/view/visites/modals/visites-modal-reschedule.html.twig
+++ b/templates/back/signalement/view/visites/modals/visites-modal-reschedule.html.twig
@@ -23,38 +23,19 @@
 
                             <div class="fr-grid-row fr-grid-row--gutters fr-grid-row--middle fr-my-3w">
                                 <div class="fr-col-12 fr-mb-3v">
-                                    <fieldset class="fr-fieldset fr-fieldset--inline fr-mb-5v">
-                                        <legend class="fr-fieldset__legend fr-text--regular required">
-                                            Date de la visite
-                                        </legend>
-                                        <p id="signalement-reschedule-visite-date-error" class="fr-error-text fr-hidden fr-my-3v">
-                                            Veuillez préciser la date de la visite.
-                                        </p>
+                                    <div class="fr-input-group">
+                                        <label class="fr-label" for="visite-reschedule[date]">Date de la visite <span class="fr-text-default--error">*</span></label>
                                         <input type="date" class="fr-input add-fields-if-past-date" data-fields="visite-reschedule-{{intervention.id}}-past-date-complementary-fields" name="visite-reschedule[date]" value="{{ intervention.scheduledAt.format('Y-m-d') }}" required>
-                                    </fieldset>
+                                    </div>
 
-                                    <fieldset class="fr-fieldset fr-fieldset--inline fr-mb-5v">
-                                        <legend class="fr-fieldset__legend fr-text--regular">
-                                            Heure de la visite
-                                        </legend>
-                                        <p id="signalement-reschedule-visite-date-error" class="fr-error-text fr-hidden fr-my-3v">
-                                            Veuillez préciser l'heure de la visite.
-                                        </p>
+                                    <div class="fr-input-group">
+                                        <label class="fr-label" for="visite-reschedule[time]">Heure de la visite</label>
                                         <input type="time" class="fr-input" data-fields="visite-reschedule-past-date-complementary-fields" name="visite-reschedule[time]" value="{{ (intervention.scheduledAt.format('H')) > 0 ? intervention.scheduledAt|date('H:i')  : '' }}">
-                                    </fieldset>
+                                    </div>
                                     
                                     {% if is_granted('ROLE_ADMIN_TERRITORY') %}
-                                        <fieldset class="fr-fieldset fr-fieldset--inline fr-mb-5v">
-                                            <legend class="fr-fieldset__legend fr-text--regular required">
-                                                Opérateur de visite
-                                            </legend>
-                                            <p id="signalement-reschedule-visite-partner-error" class="fr-error-text fr-hidden fr-my-3v">
-                                                Veuillez sélectionner le partenaire en charge de la visite
-                                            </p>
-                                            <p id="signalement-reschedule-visite-partner-double-error" class="fr-error-text fr-hidden fr-my-3v">
-                                                Ce partenaire a déjà une visite en cours.
-                                                Veuillez terminer ou annuler la visite ou sélectionner un autre partenaire.
-                                            </p>
+                                        <div class="fr-select-group">
+                                            <label class="fr-label" for="visite-reschedule[partner]">Opérateur de visite <span class="fr-text-default--error">*</span></label>
                                             <select name="visite-reschedule[partner]" class="fr-select visite-partner-select">
                                                 {% for partner in partnersCanVisite %}
                                                     {% set alertPartner = false %}
@@ -75,8 +56,17 @@
                                                         {{ partner.nom|upper }}
                                                     </option>
                                                 {% endfor %}
+                                                <option value="extern" {% if intervention.partner.id is null %}selected="selected"{% endif %}>Opérateur Externe</option>
                                             </select>
-                                        </fieldset>
+                                            <p id="signalement-reschedule-visite-partner-double-error" class="fr-error-text fr-hidden fr-my-3v">
+                                                Ce partenaire a déjà une visite en cours.
+                                                Veuillez terminer ou annuler la visite ou sélectionner un autre partenaire.
+                                            </p>
+                                        </div>
+                                        <div class="fr-input-group fr-hidden" id="visite-reschedule-external-operator">
+                                            <label class="fr-label" for="visite-reschedule[externalOperator]">Nom de l'opérateur externe <span class="fr-text-default--error">*</span></label>
+                                            <input type="text" name="visite-reschedule[externalOperator]" value="{{intervention.externalOperator}}" maxlength="255" class="fr-input visite-external-operator">
+                                        </div>
                                     {% else %}
                                         <input type="hidden" name="visite-reschedule[partner]" value="{{ app.user.partnerInTerritory(signalement.territory).id }}">
                                     {% endif %}
diff --git a/templates/back/signalement/view/visites/visite-item.html.twig b/templates/back/signalement/view/visites/visite-item.html.twig
index 87dcca9733..bdca1ac8b4 100755
--- a/templates/back/signalement/view/visites/visite-item.html.twig
+++ b/templates/back/signalement/view/visites/visite-item.html.twig
@@ -57,8 +57,10 @@
                 Non renseigné
             {% else %}
                 {{ intervention.partner.nom }}
-                {% if intervention.partner.territory is not same as signalement.territory or intervention.partner.isArchive %}
-                    (partenaire supprimé)
+                {% if intervention.externalOperator %}
+                    <i>(opérateur externe)</i>
+                {% elseif intervention.partner.territory is not same as signalement.territory or intervention.partner.isArchive %}
+                    <i>(partenaire supprimé)</i>
                 {% endif %}
             {% endif %}
         </div>
diff --git a/templates/back/signalement/view/visites/visites-form-confirm-fields.html.twig b/templates/back/signalement/view/visites/visites-form-confirm-fields.html.twig
index 9f11006949..882cdd7ced 100755
--- a/templates/back/signalement/view/visites/visites-form-confirm-fields.html.twig
+++ b/templates/back/signalement/view/visites/visites-form-confirm-fields.html.twig
@@ -1,59 +1,68 @@
 {% if formType is not same as 'edit' %}
-    <fieldset class="fr-fieldset fr-fieldset--inline fr-mb-5v">
+    <fieldset class="fr-fieldset fr-fieldset--inline">
         <legend class="fr-fieldset__legend fr-text--regular required">
             La visite a-t-elle eu lieu ?
         </legend>
-        <p id="signalement-confirm-visite-done-error" class="fr-error-text fr-hidden fr-my-3v">
-            Veuillez préciser si la visite a eu lieu.
-        </p>
-        <div class="fr-radio-group">
-            <input type="radio" id="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-confirm-1" name="visite-{{ formType }}[visiteDone]" value="1">
-            <label for="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-confirm-1">Visite effectuée</label>
+        <div class="fr-fieldset__element fr-fieldset__element--inline">
+            <div class="fr-radio-group">
+                <input type="radio" id="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-confirm-1" name="visite-{{ formType }}[visiteDone]" value="1">
+                <label for="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-confirm-1">Visite effectuée</label>
+            </div>
         </div>
-        <div class="fr-radio-group">
-            <input type="radio" id="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-confirm-0" name="visite-{{ formType }}[visiteDone]" value="0">
-            <label for="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-confirm-0">Visite non effectuée</label>
+        <div class="fr-fieldset__element fr-fieldset__element--inline">
+            <div class="fr-radio-group">
+                <input type="radio" id="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-confirm-0" name="visite-{{ formType }}[visiteDone]" value="0">
+                <label for="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-confirm-0">Visite non effectuée</label>
+            </div>
         </div>
+        <p id="signalement-confirm-visite-done-error" class="fr-error-text fr-hidden fr-messages-group fr-mt-n1w">
+            Veuillez préciser si la visite a eu lieu.
+        </p>
     </fieldset>
-    <fieldset class="fr-fieldset fr-fieldset--inline fr-mb-5v">
+    <fieldset class="fr-fieldset fr-fieldset--inline">
         <legend class="fr-fieldset__legend fr-text--regular required">
             L'occupant était-il présent ?
         </legend>
-        <p id="signalement-confirm-visite-occupant-present-error" class="fr-error-text fr-hidden fr-my-3v">
-            Veuillez préciser si l'occupant était présent.
-        </p>
-        <div class="fr-radio-group">
-            <input type="radio" id="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-occupant-present-1" name="visite-{{ formType }}[occupantPresent]" value="1">
-            <label for="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-occupant-present-1">Oui</label>
+        <div class="fr-fieldset__element fr-fieldset__element--inline">
+            <div class="fr-radio-group">
+                <input type="radio" id="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-occupant-present-1" name="visite-{{ formType }}[occupantPresent]" value="1">
+                <label for="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-occupant-present-1">Oui</label>
+            </div>
         </div>
-        <div class="fr-radio-group">
-            <input type="radio" id="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-occupant-present-0" name="visite-{{ formType }}[occupantPresent]" value="0">
-            <label for="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-occupant-present-0">Non</label>
+        <div class="fr-fieldset__element fr-fieldset__element--inline">
+            <div class="fr-radio-group">
+                <input type="radio" id="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-occupant-present-0" name="visite-{{ formType }}[occupantPresent]" value="0">
+                <label for="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-occupant-present-0">Non</label>
+            </div>
         </div>
+        <p id="signalement-confirm-visite-occupant-present-error" class="fr-error-text fr-messages-group fr-hidden fr-mt-n1w">
+            Veuillez préciser si l'occupant était présent.
+        </p>
     </fieldset>
-    <fieldset class="fr-fieldset fr-fieldset--inline fr-mb-5v">
+    <fieldset class="fr-fieldset fr-fieldset--inline">
         <legend class="fr-fieldset__legend fr-text--regular required">
             Le propriétaire était-il présent ?
         </legend>
-        <p id="signalement-confirm-visite-proprietaire-present-error" class="fr-error-text fr-hidden fr-my-3v">
-            Veuillez préciser si le propriétaire était présent.
-        </p>
-        <div class="fr-radio-group">
-            <input type="radio" id="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-proprietaire-present-1" name="visite-{{ formType }}[proprietairePresent]" value="1">
-            <label for="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-proprietaire-present-1">Oui</label>
+         <div class="fr-fieldset__element fr-fieldset__element--inline">
+            <div class="fr-radio-group">
+                <input type="radio" id="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-proprietaire-present-1" name="visite-{{ formType }}[proprietairePresent]" value="1">
+                <label for="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-proprietaire-present-1">Oui</label>
+            </div>
         </div>
-        <div class="fr-radio-group">
-            <input type="radio" id="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-proprietaire-present-0" name="visite-{{ formType }}[proprietairePresent]" value="0">
-            <label for="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-proprietaire-present-0">Non</label>
+        <div class="fr-fieldset__element fr-fieldset__element--inline">
+            <div class="fr-radio-group">
+                <input type="radio" id="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-proprietaire-present-0" name="visite-{{ formType }}[proprietairePresent]" value="0">
+                <label for="visite-{{ formType }}-{{ intervention is defined ? intervention.id : '' }}-proprietaire-present-0">Non</label>
+            </div>
         </div>
+        <p id="signalement-confirm-visite-proprietaire-present-error" class="fr-error-text fr-messages-group fr-hidden fr-mt-n1w">
+            Veuillez préciser si le propriétaire était présent.
+        </p>
     </fieldset>
     <fieldset id="fieldset-conclude-procedure" class="fr-fieldset fr-fieldset--inline fr-mb-5v fr-w-100-left fr-hidden">
         <legend class="fr-fieldset__legend fr-text--regular required">
             Quelle est la conclusion de la visite ?
         </legend>
-        <p id="signalement-confirm-visite-procedure-error" class="fr-error-text fr-hidden fr-my-3v">
-            Veuillez préciser la conclusion de la visite.
-        </p>
         <div class="search-checkbox-container fr-input-group fr-w-100">
             <input id="visite-{{ formType }}[concludeProcedure]_input" type="text" placeholder="Sélectionner une ou plusieurs conclusions" class="fr-input">
             <div class="search-checkbox" class="fr-hidden">
@@ -70,6 +79,9 @@
                 {% endfor %}
             </div>
         </div>
+        <p id="signalement-confirm-visite-procedure-error" class="fr-error-text fr-hidden fr-messages-group fr-mt-n1w">
+            Veuillez préciser la conclusion de la visite.
+        </p>
     </fieldset>
 {% endif %}
 
@@ -94,17 +106,17 @@
 </div>
 {% endif %}
 
-<fieldset class="fr-fieldset fr-fieldset--inline fr-mb-5v">
-    <legend class="fr-fieldset__legend fr-text--regular required">
-        Commentaire de visite
-    </legend>
-    <p id="signalement-confirm-visite-details-error" class="fr-error-text fr-hidden fr-my-3v">
-        Veuillez saisir un commentaire pour la visite.
-    </p>
+<div class="fr-input-group">
+    <label class="fr-label" for="visite-{{ formType }}[details]">
+        Commentaire de visite <span class="fr-text-default--error">*</span>
+    </label>
     <textarea class="fr-input fr-input--no-resize editor field-visite-details" id="visite-{{ formType }}[details]" name="visite-{{ formType }}[details]" minlength="10">{{
         intervention is defined and intervention.details ? intervention.details : ''
     }}</textarea>
-</fieldset>
+    <p id="signalement-confirm-visite-details-error" class="fr-error-text fr-messages-group fr-hidden">
+        Veuillez saisir un commentaire pour la visite.
+    </p>
+</div>
 
 {% if not intervention is defined or intervention.files is empty %}
 <div class="fr-upload-group fr-mb-5v">
diff --git a/templates/back/signalement/view/visites/visites-list.html.twig b/templates/back/signalement/view/visites/visites-list.html.twig
index 0c2513a175..85097deb7a 100755
--- a/templates/back/signalement/view/visites/visites-list.html.twig
+++ b/templates/back/signalement/view/visites/visites-list.html.twig
@@ -12,12 +12,17 @@
         {% if is_granted('SIGN_ADD_VISITE', signalement) %}
             {% include 'back/signalement/view/visites/modals/visites-modal-add.html.twig' %}
             {% set displayAddVisiteButton = true %}
+            {% set listPendingVisiteExternalOperatorNames = [] %}
             {% for pendingVisite in pendingVisites %}
                 {% set partner = app.user.partnerInTerritoryOrFirstOne(signalement.territory) %}
                 {% if pendingVisite.partner and pendingVisite.partner.id is same as partner.id %}
                     {% set displayAddVisiteButton = false %}
                 {% endif %}
+                {% if pendingVisite.externalOperator and pendingVisite.partner.id is null %}
+                    {% set listPendingVisiteExternalOperatorNames = listPendingVisiteExternalOperatorNames|merge([pendingVisite.externalOperator]) %}
+                {% endif %}
             {% endfor %}
+            <span id="list-pending-visite-external-operator-names" data-list="{{ listPendingVisiteExternalOperatorNames|json_encode}}"></span>
 
             <div class="fr-my-5v fr-text--right">
                 {% if not signalement.territory.isGrilleVisiteDisabled %}
diff --git a/tests/Functional/Controller/Back/SignalementVisitesControllerTest.php b/tests/Functional/Controller/Back/SignalementVisitesControllerTest.php
index df7316d680..5942fa5192 100644
--- a/tests/Functional/Controller/Back/SignalementVisitesControllerTest.php
+++ b/tests/Functional/Controller/Back/SignalementVisitesControllerTest.php
@@ -49,13 +49,57 @@ public function testAddFutureVisite(): void
             'POST',
             $route,
             [
-                'visite-add[date]' => '2123-01-01',
-                'visite-add[partner]' => $partner->getId(),
-                '_token' => 'signalement_add_visit_'.$signalement->getId(),
+                'visite-add' => [
+                    'date' => '2123-01-01',
+                    'time' => '',
+                    'partner' => $partner->getId(),
+                    'externalOperator' => '',
+                ],
+                '_token' => $this->generateCsrfToken(
+                    $this->client,
+                    'signalement_add_visit_'.$signalement->getId()
+                ),
             ]
         );
 
         $this->assertResponseRedirects('/bo/signalements/'.$signalement->getUuid());
+
+        $flashBag = $this->client->getRequest()->getSession()->getFlashBag(); // @phpstan-ignore-line
+        $this->assertTrue($flashBag->has('success'));
+        $successMessages = $flashBag->get('success');
+        $this->assertEquals('La date de visite a bien été définie.', $successMessages[0]);
+    }
+
+    public function testAddFutureVisiteOnExternalOperator(): void
+    {
+        $signalement = $this->signalementRepository->findOneBy(['uuid' => '00000000-0000-0000-2022-000000000001']);
+
+        $route = $this->router->generate('back_signalement_visite_add', ['uuid' => $signalement->getUuid()]);
+        $this->client->request('GET', $route);
+
+        $this->client->request(
+            'POST',
+            $route,
+            [
+                'visite-add' => [
+                    'date' => '2123-01-01',
+                    'time' => '10:00',
+                    'partner' => 'extern',
+                    'externalOperator' => 'Opérateur externe',
+                ],
+                '_token' => $this->generateCsrfToken(
+                    $this->client,
+                    'signalement_add_visit_'.$signalement->getId()
+                ),
+            ]
+        );
+
+        $this->assertResponseRedirects('/bo/signalements/'.$signalement->getUuid());
+
+        $flashBag = $this->client->getRequest()->getSession()->getFlashBag(); // @phpstan-ignore-line
+        $this->assertTrue($flashBag->has('success'));
+        $successMessages = $flashBag->get('success');
+        $this->assertEquals('La date de visite a bien été définie.', $successMessages[0]);
     }
 
     public function testAddPastVisite(): void
@@ -71,14 +115,25 @@ public function testAddPastVisite(): void
             'POST',
             $route,
             [
-                'visite-add[date]' => '2022-01-01',
-                'visite-add[partner]' => $partner->getId(),
-                'visite-add[details]' => 'Lorem Ipsum',
-                '_token' => 'signalement_add_visit_'.$signalement->getId(),
+                'visite-add' => [
+                    'date' => '2022-01-01',
+                    'time' => '10:00',
+                    'partner' => $partner->getId(),
+                    'externalOperator' => '',
+                    'details' => 'Lorem Ipsum',
+                ],
+                '_token' => $this->generateCsrfToken(
+                    $this->client,
+                    'signalement_add_visit_'.$signalement->getId()
+                ),
             ]
         );
 
         $this->assertResponseRedirects('/bo/signalements/'.$signalement->getUuid());
+        $flashBag = $this->client->getRequest()->getSession()->getFlashBag(); // @phpstan-ignore-line
+        $this->assertTrue($flashBag->has('success'));
+        $successMessages = $flashBag->get('success');
+        $this->assertEquals('Les informations de la visite ont bien été renseignées.', $successMessages[0]);
     }
 
     public function testAddPastVisiteNotDone(): void
@@ -101,6 +156,7 @@ public function testAddPastVisiteNotDone(): void
                     'date' => '2023-01-01',
                     'time' => '10:00',
                     'partner' => $partner->getId(),
+                    'externalOperator' => '',
                     'visiteDone' => '0',
                     'occupantPresent' => '0',
                     'proprietairePresent' => '0',
diff --git a/tests/Functional/Manager/InterventionManagerTest.php b/tests/Functional/Manager/InterventionManagerTest.php
index a19cd5ce1a..676979be04 100644
--- a/tests/Functional/Manager/InterventionManagerTest.php
+++ b/tests/Functional/Manager/InterventionManagerTest.php
@@ -138,6 +138,30 @@ public function testCreateFutureVisiteFromRequest(): void
         $this->assertEmailCount(0);
     }
 
+    /**
+     * @throws \Exception
+     */
+    public function testCreateFutureVisiteFromRequestOnExternalOperator(): void
+    {
+        $signalement = $this->signalementRepository->findOneBy(['reference' => '2023-10']);
+
+        $visiteRequest = new VisiteRequest(
+            date: (new \DateTimeImmutable())->modify('+ 1 month')->format('Y-m-d'),
+            time: '10:00',
+            externalOperator: 'Lala la',
+        );
+
+        $intervention = $this->interventionManager->createVisiteFromRequest($signalement, $visiteRequest);
+        $this->assertInstanceOf(Intervention::class, $intervention);
+        $this->assertEquals(Intervention::STATUS_PLANNED, $intervention->getStatus());
+        $this->assertTrue($intervention->getScheduledAt() > new \DateTimeImmutable());
+        $this->assertEquals('Lala la', $intervention->getPartner()->getNom());
+        $this->assertNull($intervention->getPartner()->getId());
+        $this->assertEquals('OPERATEUR_VISITES_ET_TRAVAUX', $intervention->getPartner()->getType()->name);
+        $this->assertEquals('Lala la', $intervention->getExternalOperator());
+        $this->assertEmailCount(0);
+    }
+
     /**
      * @throws \Exception
      */


Reviews:
user: login: hmeneuvrier
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-01T09:46:20Z
user: login: emilschn
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-01T10:22:04Z
user: login: numew
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-01T15:14:09Z
user: login: numew
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-01T15:20:09Z
user: login: numew
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-01T15:22:27Z
user: login: numew
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-01T15:23:39Z
user: login: numew
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-01T15:32:51Z
user: login: hmeneuvrier
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T07:18:46Z
user: login: hmeneuvrier
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T07:19:44Z
user: login: hmeneuvrier
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T07:23:49Z
user: login: emilschn
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T07:29:30Z
user: login: numew
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T12:15:45Z
user: login: numew
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T12:16:03Z
user: login: sfinx13
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T15:20:18Z
user: login: hmeneuvrier
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T10:06:22Z
user: login: hmeneuvrier
body: OK pour moi
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-04T10:23:49Z

Review comments:
diff_hunk: @@ -161,6 +165,14 @@ public function setSignalement(?Signalement $signalement): self
 
     public function getPartner(): ?Partner
     {
+        if (!$this->partner && $this->externalOperator) {
+            $externalPartner = new Partner();
+            $externalPartner->setNom($this->externalOperator);
+            $externalPartner->setType(PartnerType::AUTRE);
path: src/Entity/Intervention.php
user: login: hmeneuvrier
body: Plutôt `PartnerType::OPERATEUR_VISITES_ET_TRAVAUX` ?
created_at: 2025-04-01T09:26:31Z
author_association: COLLABORATOR
diff_hunk: @@ -116,17 +120,21 @@ public function rescheduleVisiteFromRequest(Signalement $signalement, VisiteRequ
             return null;
         }
 
-        $partnerFound = $this->partnerManager->getPartnerIfQualification(
-            $visiteRequest->getPartner(),
-            Qualification::VISITES,
-            $signalement->getTerritory()
-        );
-        if (!$partnerFound) {
-            return null;
+        $partnerFound = null;
+        if ($visiteRequest->getPartner()) {
+            $partnerFound = $this->partnerManager->getPartnerIfQualification(
+                $visiteRequest->getPartner(),
+                Qualification::VISITES,
+                $signalement->getTerritory()
+            );
+            if (!$partnerFound) {
+                return null;
+            }
path: src/Manager/InterventionManager.php
user: login: hmeneuvrier
body: factoriser ?
created_at: 2025-04-01T09:34:46Z
author_association: COLLABORATOR
diff_hunk: @@ -31,45 +31,26 @@
 
                             <div class="fr-grid-row fr-grid-row--gutters fr-grid-row--middle fr-my-3w">
                                 <div class="fr-col-12 fr-mb-3v">
-                                    <fieldset class="fr-fieldset fr-fieldset--inline fr-mb-5v">
-                                        <legend class="fr-fieldset__legend fr-text--regular required">
-                                            Date de la visite
-                                        </legend>
-                                        <p id="signalement-add-visite-date-error" class="fr-error-text fr-hidden fr-my-3v">
-                                            Veuillez préciser la date de la visite.
-                                        </p>
+                                    <div class="fr-input-group">
path: templates/back/signalement/view/visites/modals/visites-modal-add.html.twig
user: login: hmeneuvrier
body: on a les messages d'erreur navigateurs (donc non accessibles), c'était peut-être déjà le cas avant, ne pas tenir compte dans ce cas

![image](https://github.com/user-attachments/assets/575cb19c-fd53-4bb7-9c4d-ba91f1a7b1d2)

created_at: 2025-04-01T09:36:22Z
author_association: COLLABORATOR
diff_hunk: @@ -86,8 +67,17 @@
                                                         {{ partner.nom|upper }}
                                                     </option>
                                                 {% endfor %}
+                                                <option value="extern">Opérateur Externe</option>
                                             </select>
-                                        </fieldset>
+                                            <p id="signalement-add-visite-partner-double-error" class="fr-error-text fr-hidden">
+                                                Ce partenaire a déjà une visite en cours.
+                                                Veuillez terminer ou annuler la visite ou sélectionner un autre partenaire.
path: templates/back/signalement/view/visites/modals/visites-modal-add.html.twig
user: login: hmeneuvrier
body: Chipotage, mais on peut définir 2 visites pour le même opérateur externe 
![image](https://github.com/user-attachments/assets/9e5edbdf-9f2c-4d20-ae62-3f73e786d133)

created_at: 2025-04-01T09:37:38Z
author_association: COLLABORATOR
diff_hunk: @@ -23,38 +23,19 @@
 
                             <div class="fr-grid-row fr-grid-row--gutters fr-grid-row--middle fr-my-3w">
                                 <div class="fr-col-12 fr-mb-3v">
-                                    <fieldset class="fr-fieldset fr-fieldset--inline fr-mb-5v">
-                                        <legend class="fr-fieldset__legend fr-text--regular required">
-                                            Date de la visite
-                                        </legend>
-                                        <p id="signalement-reschedule-visite-date-error" class="fr-error-text fr-hidden fr-my-3v">
-                                            Veuillez préciser la date de la visite.
-                                        </p>
+                                    <div class="fr-input-group">
+                                        <label class="fr-label" for="visite-reschedule[date]">Date de la visite <span class="fr-text-default--error">*</span></label>
path: templates/back/signalement/view/visites/modals/visites-modal-reschedule.html.twig
user: login: hmeneuvrier
body: idem pour les messages d'erreur 
![image](https://github.com/user-attachments/assets/e7110c7f-6a4d-45c9-b7c8-a0a6d648a8a0)

created_at: 2025-04-01T09:38:25Z
author_association: COLLABORATOR
diff_hunk: @@ -75,8 +56,17 @@
                                                         {{ partner.nom|upper }}
                                                     </option>
                                                 {% endfor %}
+                                                <option value="extern" {% if intervention.partner.id is null %}selected="selected"{% endif %}>Opérateur Externe</option>
                                             </select>
-                                        </fieldset>
+                                            <p id="signalement-reschedule-visite-partner-double-error" class="fr-error-text fr-hidden fr-my-3v">
+                                                Ce partenaire a déjà une visite en cours.
+                                                Veuillez terminer ou annuler la visite ou sélectionner un autre partenaire.
path: templates/back/signalement/view/visites/modals/visites-modal-reschedule.html.twig
user: login: hmeneuvrier
body: et idem pour le même opérateur externe
created_at: 2025-04-01T09:39:23Z
author_association: COLLABORATOR
diff_hunk: @@ -138,6 +138,30 @@ public function testCreateFutureVisiteFromRequest(): void
         $this->assertEmailCount(0);
     }
 
+    /**
+     * @throws \Exception
+     */
+    public function testCreateFutureVisiteFromRequestOnExternalOperator(): void
+    {
+        $signalement = $this->signalementRepository->findOneBy(['reference' => '2023-10']);
+
+        $visiteRequest = new VisiteRequest(
+            date: (new \DateTimeImmutable())->modify('+ 1 month')->format('Y-m-d'),
+            time: '10:00',
+            externalOperator: 'Lala la',
+        );
+
+        $intervention = $this->interventionManager->createVisiteFromRequest($signalement, $visiteRequest);
+        $this->assertInstanceOf(Intervention::class, $intervention);
+        $this->assertEquals(Intervention::STATUS_PLANNED, $intervention->getStatus());
+        $this->assertTrue($intervention->getScheduledAt() > new \DateTimeImmutable());
+        $this->assertEquals('Lala la', $intervention->getPartner()->getNom());
+        $this->assertNull($intervention->getPartner()->getId());
+        $this->assertEquals('AUTRE', $intervention->getPartner()->getType()->name);
path: tests/Functional/Manager/InterventionManagerTest.php
user: login: hmeneuvrier
body: si on change de type de partner (cf autre commentaire) à changer ici aussi
created_at: 2025-04-01T09:44:31Z
author_association: COLLABORATOR
diff_hunk: @@ -161,6 +165,14 @@ public function setSignalement(?Signalement $signalement): self
 
     public function getPartner(): ?Partner
     {
+        if (!$this->partner && $this->externalOperator) {
path: src/Entity/Intervention.php
user: login: emilschn
body: C'est pas un peu trompeur de faire ça ici ?
On s'attend quand même à un vrai partenaire lié, plutôt qu'une entité créée en temps réel ?
(mais ça a peut-être d'autres impacts)
created_at: 2025-04-01T10:22:04Z
author_association: COLLABORATOR
diff_hunk: @@ -161,6 +165,14 @@ public function setSignalement(?Signalement $signalement): self
 
     public function getPartner(): ?Partner
     {
+        if (!$this->partner && $this->externalOperator) {
path: src/Entity/Intervention.php
user: login: numew
body: Comme mis sur mattermost, c'est discutable, je comprends que c'est pas bien conventionnel mais ca permet d'éviter de modifier les 40 appels à cette méthode un peu partout dans le code. J'ai pas vraiment d'autre argument mais j'ai checké les différente utilisation ca ne devrait pas provoquer d'effet de bord
created_at: 2025-04-01T15:14:09Z
author_association: COLLABORATOR
diff_hunk: @@ -116,17 +120,21 @@ public function rescheduleVisiteFromRequest(Signalement $signalement, VisiteRequ
             return null;
         }
 
-        $partnerFound = $this->partnerManager->getPartnerIfQualification(
-            $visiteRequest->getPartner(),
-            Qualification::VISITES,
-            $signalement->getTerritory()
-        );
-        if (!$partnerFound) {
-            return null;
+        $partnerFound = null;
+        if ($visiteRequest->getPartner()) {
+            $partnerFound = $this->partnerManager->getPartnerIfQualification(
+                $visiteRequest->getPartner(),
+                Qualification::VISITES,
+                $signalement->getTerritory()
+            );
+            if (!$partnerFound) {
+                return null;
+            }
path: src/Manager/InterventionManager.php
user: login: numew
body: Je vois pas trop comment tourner le truc pour factoriser ça ?
created_at: 2025-04-01T15:20:09Z
author_association: COLLABORATOR
diff_hunk: @@ -31,45 +31,26 @@
 
                             <div class="fr-grid-row fr-grid-row--gutters fr-grid-row--middle fr-my-3w">
                                 <div class="fr-col-12 fr-mb-3v">
-                                    <fieldset class="fr-fieldset fr-fieldset--inline fr-mb-5v">
-                                        <legend class="fr-fieldset__legend fr-text--regular required">
-                                            Date de la visite
-                                        </legend>
-                                        <p id="signalement-add-visite-date-error" class="fr-error-text fr-hidden fr-my-3v">
-                                            Veuillez préciser la date de la visite.
-                                        </p>
+                                    <div class="fr-input-group">
path: templates/back/signalement/view/visites/modals/visites-modal-add.html.twig
user: login: numew
body: Oui c'était déjà le cas (et j'ai un doute sur la question de l’accessibilité les concernant)
created_at: 2025-04-01T15:22:27Z
author_association: COLLABORATOR
diff_hunk: @@ -86,8 +67,17 @@
                                                         {{ partner.nom|upper }}
                                                     </option>
                                                 {% endfor %}
+                                                <option value="extern">Opérateur Externe</option>
                                             </select>
-                                        </fieldset>
+                                            <p id="signalement-add-visite-partner-double-error" class="fr-error-text fr-hidden">
+                                                Ce partenaire a déjà une visite en cours.
+                                                Veuillez terminer ou annuler la visite ou sélectionner un autre partenaire.
path: templates/back/signalement/view/visites/modals/visites-modal-add.html.twig
user: login: numew
body: Tu ferais quoi ? Un contrôle si une visite sur un opérateur extérieur est déjà présente ou, sur un opérateur extérieur du même nom ?
created_at: 2025-04-01T15:23:39Z
author_association: COLLABORATOR
diff_hunk: @@ -161,6 +165,14 @@ public function setSignalement(?Signalement $signalement): self
 
     public function getPartner(): ?Partner
     {
+        if (!$this->partner && $this->externalOperator) {
+            $externalPartner = new Partner();
+            $externalPartner->setNom($this->externalOperator);
+            $externalPartner->setType(PartnerType::AUTRE);
path: src/Entity/Intervention.php
user: login: numew
body: Ok
created_at: 2025-04-01T15:32:51Z
author_association: COLLABORATOR
diff_hunk: @@ -86,8 +67,17 @@
                                                         {{ partner.nom|upper }}
                                                     </option>
                                                 {% endfor %}
+                                                <option value="extern">Opérateur Externe</option>
                                             </select>
-                                        </fieldset>
+                                            <p id="signalement-add-visite-partner-double-error" class="fr-error-text fr-hidden">
+                                                Ce partenaire a déjà une visite en cours.
+                                                Veuillez terminer ou annuler la visite ou sélectionner un autre partenaire.
path: templates/back/signalement/view/visites/modals/visites-modal-add.html.twig
user: login: hmeneuvrier
body: Sur un opérateur extérieur du même nom
created_at: 2025-04-03T07:18:46Z
author_association: COLLABORATOR
diff_hunk: @@ -31,45 +31,26 @@
 
                             <div class="fr-grid-row fr-grid-row--gutters fr-grid-row--middle fr-my-3w">
                                 <div class="fr-col-12 fr-mb-3v">
-                                    <fieldset class="fr-fieldset fr-fieldset--inline fr-mb-5v">
-                                        <legend class="fr-fieldset__legend fr-text--regular required">
-                                            Date de la visite
-                                        </legend>
-                                        <p id="signalement-add-visite-date-error" class="fr-error-text fr-hidden fr-my-3v">
-                                            Veuillez préciser la date de la visite.
-                                        </p>
+                                    <div class="fr-input-group">
path: templates/back/signalement/view/visites/modals/visites-modal-add.html.twig
user: login: hmeneuvrier
body: ok si c'était le cas avant je ferme, on verra ça en temps voulu. 
created_at: 2025-04-03T07:19:44Z
author_association: COLLABORATOR
diff_hunk: @@ -116,17 +120,21 @@ public function rescheduleVisiteFromRequest(Signalement $signalement, VisiteRequ
             return null;
         }
 
-        $partnerFound = $this->partnerManager->getPartnerIfQualification(
-            $visiteRequest->getPartner(),
-            Qualification::VISITES,
-            $signalement->getTerritory()
-        );
-        if (!$partnerFound) {
-            return null;
+        $partnerFound = null;
+        if ($visiteRequest->getPartner()) {
+            $partnerFound = $this->partnerManager->getPartnerIfQualification(
+                $visiteRequest->getPartner(),
+                Qualification::VISITES,
+                $signalement->getTerritory()
+            );
+            if (!$partnerFound) {
+                return null;
+            }
path: src/Manager/InterventionManager.php
user: login: hmeneuvrier
body: mouais nan, effectivement, c'est pas vraiment utile
created_at: 2025-04-03T07:23:49Z
author_association: COLLABORATOR
diff_hunk: @@ -161,6 +165,14 @@ public function setSignalement(?Signalement $signalement): self
 
     public function getPartner(): ?Partner
     {
+        if (!$this->partner && $this->externalOperator) {
path: src/Entity/Intervention.php
user: login: emilschn
body: J'arrive pas à me faire un avis, dans un sens ou dans l'autre :)
created_at: 2025-04-03T07:29:30Z
author_association: COLLABORATOR
diff_hunk: @@ -86,8 +67,17 @@
                                                         {{ partner.nom|upper }}
                                                     </option>
                                                 {% endfor %}
+                                                <option value="extern">Opérateur Externe</option>
                                             </select>
-                                        </fieldset>
+                                            <p id="signalement-add-visite-partner-double-error" class="fr-error-text fr-hidden">
+                                                Ce partenaire a déjà une visite en cours.
+                                                Veuillez terminer ou annuler la visite ou sélectionner un autre partenaire.
path: templates/back/signalement/view/visites/modals/visites-modal-add.html.twig
user: login: numew
body: Ok c'est fait
created_at: 2025-04-03T12:15:45Z
author_association: COLLABORATOR
diff_hunk: @@ -161,6 +165,14 @@ public function setSignalement(?Signalement $signalement): self
 
     public function getPartner(): ?Partner
     {
+        if (!$this->partner && $this->externalOperator) {
path: src/Entity/Intervention.php
user: login: numew
body: Dans ce cas je propose de garder en l'état :D
created_at: 2025-04-03T12:16:03Z
author_association: COLLABORATOR
diff_hunk: @@ -161,6 +165,14 @@ public function setSignalement(?Signalement $signalement): self
 
     public function getPartner(): ?Partner
     {
+        if (!$this->partner && $this->externalOperator) {
path: src/Entity/Intervention.php
user: login: sfinx13
body: C'est bizarre en effet pourquoi ne pas créer ce partenaire externe s'il a un type ? (et faire en sorte que ce partenaire externe soit non affectable sans user) Ça serait plus cohérent que le hack

Autre question, le type est-il vraiment utile ? Y'a une règle de gestion derrière ?

SI on doit coller son fonctionnement au partenaire autant qu'il devienne un partenaire avec un type à spécifier.
created_at: 2025-04-03T15:20:18Z
author_association: COLLABORATOR
diff_hunk: @@ -161,6 +165,14 @@ public function setSignalement(?Signalement $signalement): self
 
     public function getPartner(): ?Partner
     {
+        if (!$this->partner && $this->externalOperator) {
path: src/Entity/Intervention.php
user: login: hmeneuvrier
body: Après discussion on a décidé de laisser comme ça. (cf ADR)
created_at: 2025-04-04T10:06:22Z
author_association: COLLABORATOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-03T12:16:33Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=MTES-MCT_histologe&pullRequest=3926) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [12.3% Duplication on New Code](https://sonarcloud.io/component_measures?id=MTES-MCT_histologe&pullRequest=3926&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=MTES-MCT_histologe&pullRequest=3926)


author_association: NONE