demarches-simplifiees / demarches-simplifiees.fr

Dématérialiser et simplifier les démarches administratives
https://www.demarches-simplifiees.fr
GNU Affero General Public License v3.0
193 stars 87 forks source link

ETQ instructeur, faire une même action sur plusieurs dossiers (passer en instruction, clôturer, demander un avis, ...) #7805

Closed dzc34 closed 3 weeks ago

dzc34 commented 1 year ago

Résumé

ETQ instructeur, je souhaite faire une même action sur plusieurs dossiers (passer en instruction, clôturer, ...)

voir en commentaire la liste des actions (déjà implémentées, à implémenter ou qui ne seront pas implémentées) : --------> https://github.com/demarches-simplifiees/demarches-simplifiees.fr/issues/7805#issuecomment-1386967187

Cadre de cette contribution

  • L'ADULLACT est intéressé pour proposer cette évolution sur son instance DS.
  • Notre souhait est d'intégrer cette fonctionnalité sous forme de contribution au code source du logiciel DS.
  • Nous avons mandaté le prestataire @synbioz pour développer cette fonctionnalité dans l'optique d'une contribution.
  • Si c'est nécessaire, @akarzim et @jobygoude de @synbioz pourront interagir avec l'équipe betagouv sur ce ticket et sur les PRs (répondre aux commentaires, pousser des correctifs, ...).

Demande initiale

via l'ARNIA (août 2022) à l'Adullact

Sur une démarche, les instructeurs reçoivent 30 dossiers par jour, et souhaite clôturer en un coup, les 30 demandes reçues le lundi, puis en un coup les 30 reçues le mardi.

Une mairie traite plusieurs dizaines de demande d’acte de naissance par jour, elle doit se rendre dans chaque démarche pour accepter chaque demande une à une.

Demandes similaires

Suggestion-Votes / Instructeur - passer plusieurs dossiers à la fois en instruction (2021)

Nous pourrions gagner du temps en sélectionnant plusieurs dossiers à la fois, afin de les passer en instruction, acceptation, etc. Aujourd'hui, il faut les sélectionner un par un, ce qui peut être fastidieux.

Suggestion-Votes / Instructeur - Faciliter l'archivage des dossiers (2021)

Il sera plus facile et rapide d'avoir une case à cocher pour définir les dossiers à passer en archivage

Github Milestone - Traitement multiple de dossiers (instructeurs)

Github #5468 - ETQ Instructeur, je peux faire une même action sur des dossiers multiples

Exemples d'actions demandées :

  • Accepter ou refuser des dossiers en masse
  • Envoyer un même message à plusieurs dossiers

Github #2715 - ETQ Instructeur, j'aimerais passer plusieurs dossiers en archivés en même temps

actions demandée : passer plusieurs dossiers en archivés en même temps

Github #2013 - ETQ accompagnateur je veux demander des avis par batch de dossiers

actions demandée : demander un avis à la même personne pour des dossiers sélectionnés

Github #1617 - ETQ Instructeur je veux effectuer des actions de masse sur les dossiers

Exemples d'actions demandées :

  • passage de plusieurs dossiers en "archivés"
  • demander un avis à la même personne pour des dossiers sélectionnés

Actuellement

Si l'instructeur doit appliquer la même action sur plusieurs dossiers, il doit ouvrir un par un chaque dossier et appliquer à chaque fois la même action (Passer en instruction, Accepter, Archiver, ...).

Comportement attendu

L'instructeur peut sélectionner plusieurs dossiers et appliquer en même temps la même action (1) sur les dossiers sélectionnés.

voir en commentaire la liste des actions (déjà implémentées, à implémenter ou qui ne seront pas implémentées) : --------> https://github.com/demarches-simplifiees/demarches-simplifiees.fr/issues/7805#issuecomment-1386967187


Cahier des charges

User story

Format : “En tant que” (ETQ), “je souhaite”, “afin de”

ETQ instructeur, 
Je souhaite, sélectionner plusieurs dossiers 
             et appliquer en même temps la même action (1) sur ces dossiers 
Afin de pouvoir traiter rapidement ces dossiers 

User story par type d'action

Suivre plusieurs dossiers

ETQ instructeur, 
Je souhaite, sélectionner plusieurs dossiers dans la liste des dossiers à suivre,
             et les suivre en même temps
Afin de pouvoir traiter rapidement ces dossiers 

Ne plus suivre plusieurs dossiers

ETQ instructeur, 
Je souhaite, sélectionner plusieurs dossiers dans la liste des dossiers suivis, 
             et ne plus les suivre en même temps
Afin de pouvoir traiter rapidement ces dossiers 

Passer "en instruction" plusieurs dossiers

ETQ instructeur, 
Je souhaite, sélectionner plusieurs dossiers dont le status est "en construction", 
             et les passer "en instruction" en même temps
Afin de pouvoir traiter rapidement ces dossiers 

Accepter, refuser ou "classer sans suite" plusieurs dossiers

ETQ instructeur, 
Je souhaite, sélectionner plusieurs dossiers dont le status est "en instruction",
             et rendre une décision identique ("accepter", "refuser" ou "classer sans suite") 
             sur ces dossiers en même temps
Afin de pouvoir traiter rapidement ces dossiers 

Archiver plusieurs dossiers :

ETQ instructeur, 
Je souhaite, sélectionner plusieurs dossiers 
             dont le status est "accepté", "refusé" ou "classer sans suite",
             et archiver  sur ces dossiers en même temps
Afin de pouvoir traiter rapidement ces dossiers 

Demander un avis à la même personne sur plusieurs dossiers :

ETQ instructeur, 
Je souhaite, sélectionner plusieurs dossiers 
             et demander un avis à la même personne pour ces dossiers sélectionnés
Afin de pouvoir traiter rapidement ces dossiers 

Envoyer un message identique aux usagers de plusieurs dossiers :

ETQ instructeur, 
Je souhaite, sélectionner plusieurs dossiers 
             et envoyer en même temps le même message aux usagers des dossiers sélectionnées
Afin de pouvoir traiter rapidement ces dossiers 

...

Tests d'acceptation

TODO : à compléter en fonction des choix UX

Passer "en instruction" plusieurs dossiers

ETQ instructeur
Quand je sélectionne plusieurs dossiers en construction
Alors je peux les passer en instruction en même temps

Accepter, refuser ou "classer sans suite" plusieurs dossiers

ETQ instructeur
Quand je sélectionne plusieurs dossiers en instruction
Alors je peux rendre une décision identique ("accepter", "refuser" ou "classer sans suite") sur ces dossiers

Problématique UX

TODO : planifier un échange UX

  • comment gérer les actions sur des dossiers ayants des status/flags différents ?
  • utiliser un système à la Gitlab avec un bouton "Modfier les dossiers" ?
  • utiliser une page dédiée au traitement par lot, pour présélectionner le type de dossier à manipuler (dossiers en construction, dossiers en instruction, dossiers suivis, ...)

Status possibles des dossiers en fonction du type liste (onglet) :

Onglet Status possibles des dossiers URL
à suivre en instruction
en construction
(2) /procedures/<idProcedure>?statut=a-suivre
suivis en instruction
en construction
(3) /procedures/<idProcedure>?statut=suivis
traité accepté
refusé
classé sans suite
(4) /procedures/<idProcedure>?statut=traites
au total en instruction
en construction
accepté
refusé
classer sans suite
(5) /procedures/<idProcedure>?statut=tous
supprimés accepté
refusé
classer sans suite
(6) /procedures/<idProcedure>?statut=supprimes_recemment
archivés accepté
refusé
classer sans suite
(7) /procedures/<idProcedure>?statut=archives

Actions possibles en fonction du status du dossier et des flags

Status du dossier Flags Action possible
en construction suivis ne plus suivre
envoyer un message
demander un avis
passer en instruction
en construction à suivre suivre
envoyer un message
demander un avis
passer en instruction
en instruction suivis ne plus suivre
envoyer un message
demander un avis
re-passer en construction
classer sans suite
accepté
refusé
en instruction à suivre suivre
envoyer un message
demander un avis
re-passer en construction
classer sans suite
accepté
refusé
classer sans suite
accepté
refusé
- Archiver
Supprimer
classer sans suite
accepté
refusé
archivé Désarchiver
classer sans suite
accepté
refusé
supprimé Restaurer

trackingAdullactContrib trackingAdullactARNiaContrib

dzc34 commented 1 year ago

@tchak @krichtof ticket complété

mfo commented 1 year ago

@dzc34 ; on commence a se demander si on ouvrirait pas ce sujet de notre coté. je me demande ou vous en êtes :-)

dzc34 commented 1 year ago

Avec un peu de retard, le résumé de la réunion de cadrage du 26/09/2022 entre équipe DS (@emsnytech @krichtof @tchak), Synbioz (@akarzim) et l'Adullact (@dzc34).


Avec un peu de retard, le résumé de la réunion de cadrage technique du 29/09/2022 entre équipe DS (@krichtof @tchak), Synbioz (@akarzim) et l'Adullact (@dzc34).

1 action sur un dossier, c'est plusieurs traitements

1 action sur X dossier

Analyse des problématiques

UI en cas du traitement en cours et d'erreur ---> voir avec l'UX de l'équipe DS

Principes

Planification

(7)(6)(5)(4)(3)(2)(1) ----> créer un ticket dédié à chaque fois

Remarques

pour "Envoyer un message identique sur plusieurs dossiers"

pour "Demander un avis aux même expert"

mfo commented 1 year ago

@tchak ; l'impression que le préambule a tous ça c'est un refacto pour isoler les appels db/side effects. t'avais une cible la dessous ou pas ?

dzc34 commented 1 year ago

remarque de @akarzim sur le refacto

refacto à faire pour isoler les "side effects"

La refacto proposée devrait porter sur plusieurs axes :

  • encapsuler les requêtes DB dans des transactions ;
  • logguer de façon asynchrone ;
  • optimisation de certaines requêtes (?) ;
  • extraire la logique dans des services pour soulager le modèle dossier qui fait près de 1300 lignes (?) ;
  • consolider les tests.
dzc34 commented 1 year ago

notes techniques de @akarzim pour l'étape 1 et 1 question :


Opérations possibles sur un dossier par un instructeur

# app/models/dossier_operation_log.rb
enum operation: {                                          
  changer_groupe_instructeur: 'changer_groupe_instructeur', # par l'administrateur uniquement
  passer_en_instruction: 'passer_en_instruction',          
  repasser_en_construction: 'repasser_en_construction',    
  repasser_en_instruction: 'repasser_en_instruction',      
  accepter: 'accepter',                                    
  refuser: 'refuser',                                      
  classer_sans_suite: 'classer_sans_suite',                
  supprimer: 'supprimer',                                  
  restaurer: 'restaurer',                                  
  modifier_annotation: 'modifier_annotation',              
  demander_un_avis: 'demander_un_avis',                    
  archiver: 'archiver',                                    
  desarchiver: 'desarchiver'                               
}                                                          

1ère étape (facile) -> pas de mail

Archivage

# app/controllers/instructeurs/dossiers_controller.rb
def archive                                                              
  dossier.archiver!(current_instructeur)                                 
  redirect_back(fallback_location: instructeur_procedure_path(procedure))
end                                                                      

# app/models/dossier.rb
def archiver!(author)                     
  update!(archived: true)                 
  log_dossier_operation(author, :archiver)
end                                       
# app/controllers/instructeurs/dossiers_controller.rb
def unarchive                                                            
  dossier.desarchiver!(current_instructeur)                              
  redirect_back(fallback_location: instructeur_procedure_path(procedure))
end                                                                      

# app/models/dossier.rb
def desarchiver!(author)                     
  update!(archived: false)                   
  log_dossier_operation(author, :desarchiver)
end                                          

Suivre un dossier

# app/controllers/instructeurs/dossiers_controller.rb
def follow                                                               
  current_instructeur.follow(dossier)                                    
  flash.notice = 'Dossier suivi'                                         
  redirect_back(fallback_location: instructeur_procedure_path(procedure))
end                                                                      

# app/models/instructeur.rb
def follow(dossier)                                                               
  begin                                                                           
    followed_dossiers << dossier                                                  
    # If the user tries to follow a dossier she already follows,                  
    # we just fail silently: it means the goal is already reached.                
  rescue ActiveRecord::RecordNotUnique                                            
    # Database uniqueness constraint                                              
  rescue ActiveRecord::RecordInvalid => e                                         
    # ActiveRecord validation                                                     
    raise unless e.record.errors.details.dig(:instructeur_id, 0, :error) == :taken
  end                                                                             
end                                                                               
# app/controllers/instructeurs/dossiers_controller.rb
def unfollow                                                             
  current_instructeur.unfollow(dossier)                                  
  flash.notice = "Vous ne suivez plus le dossier nº #{dossier.id}"       

  redirect_back(fallback_location: instructeur_procedure_path(procedure))
end                                                                      

# app/models/instructeur.rb
def unfollow(dossier)                     
  f = follows.find_by(dossier: dossier)   
  if f.present?                           
    f.update(unfollowed_at: Time.zone.now)
  end                                     
end                                       

Question : pas de log pour l'action de (ne plus) suivre un dossier ?

dzc34 commented 1 year ago

notes techniques de @akarzim pour l'étape 2 et 2 questions / remarques


2ème étape ---> mails envoyés dans certains cas

Suppression / Restauration

# app/controllers/instructeurs/dossiers_controller.rb
def destroy                                                                
  if dossier.termine?                                                      
    dossier.hide_and_keep_track!(current_instructeur, :instructeur_request)
    flash.notice = t('instructeurs.dossiers.deleted_by_instructeur')       
  else                                                                     
    flash.alert = t('instructeurs.dossiers.impossible_deletion')           
  end                                                                      
  redirect_back(fallback_location: instructeur_procedure_path(procedure))  
end                                                                        

# app/models/dossier.rb
def hide_and_keep_track!(author, reason)                                                                                                  
  transaction do                                                                                                                          
    if author_is_administration(author) && can_be_deleted_by_administration?(reason)                                                      
      update(hidden_by_administration_at: Time.zone.now, hidden_by_reason: reason)                                                        
    elsif author_is_user(author) && can_be_deleted_by_user?                                                                               
      update(hidden_by_user_at: Time.zone.now, dossier_transfer_id: nil, hidden_by_reason: reason)                                        
    else                                                                                                                                  
      raise "Unauthorized dossier hide attempt Dossier##{id} by #{author} for reason #{reason}"                                           
    end                                                                                                                                   

    log_dossier_operation(author, :supprimer, self)                                                                                       
  end                                                                                                                                     

  if en_construction? && !hidden_by_administration?                                                                                       
    administration_emails = followers_instructeurs.present? ? followers_instructeurs.map(&:email) : procedure.administrateurs.map(&:email)
    administration_emails.each do |email|                                                                                                 
      DossierMailer.notify_en_construction_deletion_to_administration(self, email).deliver_later                                          
    end                                                                                                                                   
  end                                                                                                                                     
end                                                                                                                                       
# app/controllers/instructeurs/dossiers_controller.rb
def restore                                                                 
  dossier = current_instructeur.dossiers.find(params[:dossier_id])          
  dossier.restore(current_instructeur)                                      
  flash.notice = t('instructeurs.dossiers.restore')                         

  if dossier.termine?                                                       
    redirect_to instructeur_procedure_path(procedure, statut: :traites)     
  else                                                                      
    redirect_back(fallback_location: instructeur_procedure_path(procedure)) 
  end                                                                       
end                                                                         

# app/models/dossier.rb
def restore(author)                                  
  transaction do                                     
    if author_is_administration(author)              
      update(hidden_by_administration_at: nil)       
    elsif author_is_user(author)                     
      update(hidden_by_user_at: nil)                 
    end                                              

    if !hidden_by_user? && !hidden_by_administration?
      update(hidden_by_reason: nil)                  
    end                                              

    log_dossier_operation(author, :restaurer, self)  
  end                                                
end                                                  

Questions / Remarques :

  • Les transactions c'est bien, mais ne devrait-on pas envisager d'en exclure les logs ? Dit autrement, si le log crash, faut-il annuler la mise à jour ?
  • Pourquoi n'envoie-t-on pas d'email aux administrateurs quand un dossier est restauré ?
dzc34 commented 1 year ago

notes techniques de @akarzim pour l'étape 3

3ème étape ---> mails envoyés

# app/controllers/instructeurs/dossiers_controller.rb
def passer_en_instruction                                             
  begin                                                               
    dossier.passer_en_instruction!(instructeur: current_instructeur)  
    flash.notice = 'Dossier passé en instruction.'                    
  rescue AASM::InvalidTransition => e                                 
    flash.alert = aasm_error_message(e, target_state: :en_instruction)
  end                                                                 

  @dossier = dossier                                                  
  render :change_state                                                
end

# app/models/dossier.rb
aasm whiny_persistence: true, column: :state, enum: true do           
  state :brouillon, initial: true                                     
  state :en_construction                                              
  state :en_instruction                                               
  state :accepte                                                      
  state :refuse                                                       
  state :sans_suite                                                   

  event :passer_en_instruction, after: :after_passer_en_instruction do
    transitions from: :en_construction, to: :en_instruction           
  end                                                                 
end

def after_passer_en_instruction(h)                                         
  instructeur = h[:instructeur]                                            
  disable_notification = h.fetch(:disable_notification, false)             

  instructeur.follow(self)                                                 

  self.en_construction_close_to_expiration_notice_sent_at = nil            
  self.conservation_extension = 0.days                                     
  self.en_instruction_at = self.traitements                                
    .passer_en_instruction(instructeur: instructeur)                       
    .processed_at                                                          
  save!                                                                    

  if !procedure.declarative_accepte? && !disable_notification              
    NotificationMailer.send_en_instruction_notification(self).deliver_later
  end                                                                      
  log_dossier_operation(instructeur, :passer_en_instruction)               
end                                                                        

Remarques :

  • Les autres actions sont très similaires ;
  • Plus complexe ici, on passe par une machine à états sur le dossier ;
  • On touche à plusieurs objets ici (dossier, instructeur, traitements), il faudra penser à encapsuler ça dans une transaction ;
  • La notification mail est faite de manière asynchrone ;
  • Nous devrions très certainement faire de même pour les logs.
dzc34 commented 1 year ago

notes techniques de @akarzim pour l'étape 4

4ème étape --> mails envoyés + éventuellement un fichier

# app/controllers/instructeurs/dossiers_controller.rb
def terminer                                                                                  
  motivation = params[:dossier] && params[:dossier][:motivation]                              
  justificatif = params[:dossier] && params[:dossier][:justificatif_motivation]               

  h = { instructeur: current_instructeur, motivation: motivation, justificatif: justificatif }

  begin                                                                                       
    case params[:process_action]                                                              
    when "refuser"                                                                            
      target_state = :refuse                                                                  
      dossier.refuser!(h)                                                                     
      flash.notice = "Dossier considéré comme refusé."                                        
    when "classer_sans_suite"                                                                 
      target_state = :sans_suite                                                              
      dossier.classer_sans_suite!(h)                                                          
      flash.notice = "Dossier considéré comme sans suite."                                    
    when "accepter"                                                                           
      target_state = :accepte                                                                 
      dossier.accepter!(h)                                                                    
      flash.notice = "Dossier traité avec succès."                                            
    end                                                                                       
  rescue AASM::InvalidTransition => e                                                         
    flash.alert = aasm_error_message(e, target_state: target_state)                           
  end                                                                                         

  @dossier = dossier                                                                          
  render :change_state                                                                        
end

# app/models/dossier.rb
def after_accepter(h)                                               
  instructeur = h[:instructeur]                                     
  motivation = h[:motivation]                                       
  justificatif = h[:justificatif]                                   
  disable_notification = h.fetch(:disable_notification, false)      

  self.processed_at = self.traitements                              
    .accepter(motivation: motivation, instructeur: instructeur)     
    .processed_at                                                   
  save!                                                             

  if justificatif                                                   
    self.justificatif_motivation.attach(justificatif)               
  end                                                               

  if attestation.nil?                                               
    self.attestation = build_attestation                            
  end                                                               

  save!                                                             
  remove_titres_identite!                                           
  if !disable_notification                                          
    NotificationMailer.send_accepte_notification(self).deliver_later
  end                                                               
  send_dossier_decision_to_experts(self)                            
  log_dossier_operation(instructeur, :accepter, self)               
end

Remarques :

  • Les autres actions sont très similaires ;
  • On passe ici aussi par une machine à états sur le dossier ;
  • On attache des fichiers ;
  • On envoie des notifications mail en asynchrone ;
  • On transmet la décision aux experts, en asynchrone, mais avec des requêtes supplémentaire sur les avis ;
  • On nettoie certaines données de manière asynchrone ;
  • Ici encore, les logs devraient être fait de manière asynchrone ;
  • Et encapsuler le tout (hors async) dans une transaction serait bienvenu.
mfo commented 1 year ago

notes techniques de @akarzim pour l'étape 1

Question : pas de log pour l'action de (ne plus) suivre un dossier ?

@akarzim / @dzc34 : pas de log c'est pas une info importante (au point qu'on penser arreter de logguer sur archive

mfo commented 1 year ago

notes techniques de @akarzim pour l'étape 2

Questions / Remarques :

* Les transactions c'est bien, mais ne devrait-on pas envisager d'en exclure les logs ? Dit autrement, si le log crash, faut-il annuler la mise à jour ?

Pour des raisons de sécurité/traçabilité, il nous faut les logs, il me semble que ca va forcement de pair. @LeSim / @tchak une confirmation serait top

* Pourquoi n'envoie-t-on pas d'email aux administrateurs quand un dossier est restauré ?

Je ne sais pas, p-e que le besoin n'a jamais été remonté

mfo commented 1 year ago

notes techniques de @akarzim pour l'étape 3

Remarques :

  • Les autres actions sont très similaires ;
  • Plus complexe ici, on passe par une machine à états sur le dossier ;
  • On touche à plusieurs objets ici (dossier, instructeur, traitements), il faudra penser à encapsuler ça dans une transaction ;
  • La notification mail est faite de manière asynchrone ;
  • Nous devrions très certainement faire de même pour les logs.

Remarque sur les logs en asynchrone, de ma comprehension le dossier est serialisé au moment du logs, le passage en asynchrone n'est pas impossible, mais il faudrait serializer l'etat du dossier au moment du passage en async. @LeSim / @tchak, pas enorme visibilité la dessus, possible que vous confirmiez (ou pas?)

LeSim commented 1 year ago

Questions / Remarques :

* Les transactions c'est bien, mais ne devrait-on pas envisager d'en exclure les logs ? Dit autrement, si le log crash, faut-il annuler la mise à jour ?

Pour des raisons de sécurité/traçabilité, il nous faut les logs, il me semble que ca va forcement de pair. @LeSim / @tchak une confirmation serait top

Oui, je confirme, ces logs sont importants. S'ils ne sont pas écrit, on ne veut pas réaliser l'action.

LeSim commented 1 year ago

Remarque sur les logs en asynchrone, de ma comprehension le dossier est serialisé au moment du logs, le passage en asynchrone n'est pas impossible, mais il faudrait serializer l'etat du dossier au moment du passage en async. @LeSim / @tchak, pas enorme visibilité la dessus, possible que vous confirmiez (ou pas?)

je n'ai pas très bien compris le coup de l'asynchrone.

Tout ce que je peux vous dire c'est que le log représente l'état du dossier au moment ou l'action est prise. Si on ne fait pas les 2 en mm temps (passage_en_instruction, log) on ne peut pas prouver que notre log est correct.

dzc34 commented 1 year ago

Commentaire pour le suivi Adullact (@cmayran @dzc34)

voir


Actions par lot : :hourglass_flowing_sand:


:warning: (1) ---> :stop_sign: ne sera pas implémenté


:warning: (2) ---> :stop_sign: ne sera pas implémenté