diff --git a/app/assets/stylesheets/02_utils.scss b/app/assets/stylesheets/02_utils.scss index c445acbfa..891a06575 100644 --- a/app/assets/stylesheets/02_utils.scss +++ b/app/assets/stylesheets/02_utils.scss @@ -37,7 +37,7 @@ } .text-right { - text-align: right; + text-align: right !important; } .text-sm { diff --git a/app/assets/stylesheets/badges.scss b/app/assets/stylesheets/badges.scss deleted file mode 100644 index f1e19da3a..000000000 --- a/app/assets/stylesheets/badges.scss +++ /dev/null @@ -1,34 +0,0 @@ -@import "colors"; -@import "constants"; - -.badge { - padding: 0 5px; - font-size: 14px; - font-weight: bold; - text-align: center; - white-space: nowrap; - border-radius: 100px; - background-color: rgba(0, 0, 0, 0.08); - vertical-align: top; - - &.baseline { - vertical-align: baseline; - } - - &.warning { - background-color: $orange; - color: #FFFFFF; - } -} - -.badge-group { - display: flex; - - .fr-badge { - margin-right: $default-spacer; - } - - .fr-badge:last-child { - margin-right: 0; - } -} diff --git a/app/assets/stylesheets/dsfr.scss b/app/assets/stylesheets/dsfr.scss index a2ca49e60..fb1ecc189 100644 --- a/app/assets/stylesheets/dsfr.scss +++ b/app/assets/stylesheets/dsfr.scss @@ -262,3 +262,13 @@ button.fr-tag-bug { .fr-badge--lowercase { text-transform: lowercase; } + +// We don't want badge to split in two lines +.fr-tag { + white-space: nowrap; +} + +// We remove the line height because it creates unharmonized spaces - most of all in table +.fr-tags-group > li { + line-height: inherit; +} diff --git a/app/assets/stylesheets/instructeur.scss b/app/assets/stylesheets/instructeur.scss index 9227d5063..8d95fdd70 100644 --- a/app/assets/stylesheets/instructeur.scss +++ b/app/assets/stylesheets/instructeur.scss @@ -48,6 +48,10 @@ width: 450px; } +.dropdown-label.dropdown-content { + min-width: 390px; +} + .print-menu { display: none; position: absolute; diff --git a/app/assets/stylesheets/tags.scss b/app/assets/stylesheets/tags.scss new file mode 100644 index 000000000..5d45bfb1e --- /dev/null +++ b/app/assets/stylesheets/tags.scss @@ -0,0 +1,30 @@ +@import "colors"; +@import "constants"; + +$colors: "green-tilleul-verveine", + "green-bourgeon", + "green-emeraude", + "green-menthe", + "blue-ecume", + "purple-glycine", + "pink-macaron", + "yellow-tournesol", + "brown-cafe-creme", + "beige-gris-galet"; + + +@each $color in $colors { + .fr-tag--#{$color}, + a.fr-tag--#{$color}, + button.fr-tag--#{$color}, + input[type=button].fr-tag--#{$color}, + input[type=image].fr-tag--#{$color}, + input[type=reset].fr-tag--#{$color}, + input[type=submit].fr-tag--#{$color} { + --idle: transparent; + --hover: var(--background-action-low-#{$color}-hover); + --active: var(--background-action-low-#{$color}-active); + background-color: var(--background-action-low-#{$color}); + color: var(--text-action-high-#{$color}); + } +} diff --git a/app/components/instructeurs/column_filter_value_component.rb b/app/components/instructeurs/column_filter_value_component.rb index d83a239ea..848c89b76 100644 --- a/app/components/instructeurs/column_filter_value_component.rb +++ b/app/components/instructeurs/column_filter_value_component.rb @@ -53,6 +53,10 @@ class Instructeurs::ColumnFilterValueComponent < ApplicationComponent [_1.label, _1.id] end end + elsif column.table == 'dossier_labels' + Procedure.find(procedure_id).labels.filter_map do + [_1.name, _1.id] + end else find_type_de_champ(column.column).options_for_select(column) end diff --git a/app/components/instructeurs/filter_buttons_component.rb b/app/components/instructeurs/filter_buttons_component.rb index 950adee9b..3a78bf9c5 100644 --- a/app/components/instructeurs/filter_buttons_component.rb +++ b/app/components/instructeurs/filter_buttons_component.rb @@ -59,6 +59,8 @@ class Instructeurs::FilterButtonsComponent < ApplicationComponent elsif column.groupe_instructeur? current_instructeur.groupe_instructeurs .find { _1.id == filter.to_i }&.label || filter + elsif column.dossier_labels? + Label.find(filter)&.name || filter elsif column.type == :date helpers.try_parse_format_date(filter) else diff --git a/app/components/main_navigation/instructeur_expert_navigation_component/instructeur_expert_navigation_component.html.haml b/app/components/main_navigation/instructeur_expert_navigation_component/instructeur_expert_navigation_component.html.haml index ee0d7dbb6..47c160cf1 100644 --- a/app/components/main_navigation/instructeur_expert_navigation_component/instructeur_expert_navigation_component.html.haml +++ b/app/components/main_navigation/instructeur_expert_navigation_component/instructeur_expert_navigation_component.html.haml @@ -9,6 +9,6 @@ = link_to expert_all_avis_path, class: 'fr-nav__link', aria: aria_current_for(:avis) do = Avis.model_name.human(count: 10) - if helpers.current_expert.avis_summary[:unanswered] > 0 - %span.badge.warning= helpers.current_expert.avis_summary[:unanswered] + %span.fr-badge.fr-badge--new.fr-badge--no-icon= helpers.current_expert.avis_summary[:unanswered] = render MainNavigation::AnnouncesLinkComponent.new diff --git a/app/components/procedure/card/labels_component.rb b/app/components/procedure/card/labels_component.rb new file mode 100644 index 000000000..4b484db35 --- /dev/null +++ b/app/components/procedure/card/labels_component.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class Procedure::Card::LabelsComponent < ApplicationComponent + def initialize(procedure:) + @procedure = procedure + end +end diff --git a/app/components/procedure/card/labels_component/labels_component.fr.yml b/app/components/procedure/card/labels_component/labels_component.fr.yml new file mode 100644 index 000000000..31f598407 --- /dev/null +++ b/app/components/procedure/card/labels_component/labels_component.fr.yml @@ -0,0 +1,3 @@ +--- +fr: + title: Labels diff --git a/app/components/procedure/card/labels_component/labels_component.html.haml b/app/components/procedure/card/labels_component/labels_component.html.haml new file mode 100644 index 000000000..d18982afe --- /dev/null +++ b/app/components/procedure/card/labels_component/labels_component.html.haml @@ -0,0 +1,17 @@ +.fr-col-6.fr-col-md-4.fr-col-lg-3 + = link_to [:admin, @procedure, :labels], class: 'fr-tile fr-enlarge-link' do + .fr-tile__body.flex.column.align-center.justify-between + - if @procedure.labels.present? + %p.fr-badge.fr-badge--info + Configuré + %div + .line-count.fr-my-1w + %p.fr-tag= @procedure.labels.size + - else + %p.fr-badge + Non configuré + + %h3.fr-h6 + = t('.title') + %p.fr-tile-subtitle Gérer les labels utilisables par les instructeurs + %p.fr-btn.fr-btn--tertiary= t('views.shared.actions.edit') diff --git a/app/controllers/administrateurs/labels_controller.rb b/app/controllers/administrateurs/labels_controller.rb new file mode 100644 index 000000000..e5b987bc8 --- /dev/null +++ b/app/controllers/administrateurs/labels_controller.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Administrateurs + class LabelsController < AdministrateurController + before_action :retrieve_procedure + before_action :retrieve_label, only: [:edit, :update, :destroy] + before_action :set_colors_collection, only: [:edit, :new, :create, :update] + + def index + @labels = @procedure.labels + end + + def edit + end + + def new + @label = Label.new + end + + def create + @label = @procedure.labels.build(label_params) + + if @label.save + flash.notice = 'Le label a bien été créé' + redirect_to [:admin, @procedure, :labels] + else + flash.alert = @label.errors.full_messages + render :new + end + end + + def update + if @label.update(label_params) + flash.notice = 'Le label a bien été modifié' + redirect_to [:admin, @procedure, :labels] + else + flash.alert = @label.errors.full_messages + render :edit + end + end + + def destroy + @label.destroy! + flash.notice = 'Le label a bien été supprimé' + redirect_to [:admin, @procedure, :labels] + end + + private + + def label_params + params.require(:label).permit(:name, :color) + end + + def retrieve_label + @label = @procedure.labels.find(params[:id]) + end + + def set_colors_collection + @colors_collection = Label.colors.keys + end + end +end diff --git a/app/controllers/administrateurs/procedures_controller.rb b/app/controllers/administrateurs/procedures_controller.rb index 371ee4d3b..b29493517 100644 --- a/app/controllers/administrateurs/procedures_controller.rb +++ b/app/controllers/administrateurs/procedures_controller.rb @@ -108,6 +108,7 @@ module Administrateurs flash.now.alert = @procedure.errors.full_messages render 'new' else + @procedure.create_generic_labels flash.notice = 'Démarche enregistrée.' current_administrateur.instructeur.assign_to_procedure(@procedure) diff --git a/app/controllers/instructeurs/dossiers_controller.rb b/app/controllers/instructeurs/dossiers_controller.rb index 0b7eb3a74..a5ac2c0db 100644 --- a/app/controllers/instructeurs/dossiers_controller.rb +++ b/app/controllers/instructeurs/dossiers_controller.rb @@ -63,6 +63,19 @@ module Instructeurs end end + def dossier_labels + labels = params[:label_id]&.map(&:to_i) || [] + + @dossier = dossier + labels.each { |params_label| DossierLabel.find_or_create_by(dossier_id: @dossier.id, label_id: params_label) } + + all_labels = DossierLabel.where(dossier_id: @dossier.id).pluck(:label_id) + + (all_labels - labels).each { DossierLabel.find_by(dossier_id: @dossier.id, label_id: _1).destroy } + + render :change_state + end + def messagerie @commentaire = Commentaire.new @messagerie_seen_at = current_instructeur.follows.find_by(dossier: dossier)&.messagerie_seen_at diff --git a/app/helpers/dossier_helper.rb b/app/helpers/dossier_helper.rb index bc9e86127..ab60b1222 100644 --- a/app/helpers/dossier_helper.rb +++ b/app/helpers/dossier_helper.rb @@ -118,6 +118,21 @@ module DossierHelper tag.span(Dossier.human_attribute_name("pending_correction.resolved"), class: ['fr-badge fr-badge--sm fr-badge--success super', html_class], role: 'status') end + def tags_label(tags) + if tags.count > 1 + tag.ul(class: 'fr-tags-group') do + safe_join(tags.map { |t| tag.li(tag_label(t[1], t[2])) }) + end + else + tag = tags.first + tag_label(tag[1], tag[2]) + end + end + + def tag_label(name, color) + tag.span(name, class: "fr-tag fr-tag--sm fr-tag--#{Label.class_name(color)}") + end + def demandeur_dossier(dossier) if dossier.procedure.for_individual? && dossier.for_tiers? return t('shared.dossiers.beneficiaire', mandataire: dossier.mandataire_full_name, beneficiaire: "#{dossier&.individual&.prenom} #{dossier&.individual&.nom}") diff --git a/app/models/column.rb b/app/models/column.rb index b9c223fe8..c9968b826 100644 --- a/app/models/column.rb +++ b/app/models/column.rb @@ -40,6 +40,7 @@ class Column def notifications? = [table, column] == ['notifications', 'notifications'] def dossier_state? = [table, column] == ['self', 'state'] def groupe_instructeur? = [table, column] == ['groupe_instructeur', 'id'] + def dossier_labels? = [table, column] == ['dossier_labels', 'label_id'] def type_de_champ? = table == TYPE_DE_CHAMP_TABLE def self.find(h_id) diff --git a/app/models/concerns/columns_concern.rb b/app/models/concerns/columns_concern.rb index 1a9ecab1a..c30074c68 100644 --- a/app/models/concerns/columns_concern.rb +++ b/app/models/concerns/columns_concern.rb @@ -91,6 +91,8 @@ module ColumnsConcern def user_france_connected_column = Columns::DossierColumn.new(procedure_id: id, table: 'self', column: 'user_from_france_connect?', filterable: false, displayable: false) + def dossier_labels_column = Columns::DossierColumn.new(procedure_id: id, table: 'dossier_labels', column: 'label_id', type: :enum) + def procedure_chorus_columns ['domaine_fonctionnel', 'referentiel_prog', 'centre_de_cout'] .map { |column| Columns::DossierColumn.new(procedure_id: id, table: 'procedure', column:, displayable: false, filterable: false) } @@ -127,7 +129,8 @@ module ColumnsConcern followers_instructeurs_email_column, groupe_instructeurs_id_column, Columns::DossierColumn.new(procedure_id: id, table: 'avis', column: 'question_answer', filterable: false), - user_france_connected_column + user_france_connected_column, + dossier_labels_column ] end diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 2f9a469aa..a07393b05 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -132,6 +132,8 @@ class Dossier < ApplicationRecord belongs_to :transfer, class_name: 'DossierTransfer', foreign_key: 'dossier_transfer_id', optional: true, inverse_of: :dossiers has_many :transfer_logs, class_name: 'DossierTransferLog', dependent: :destroy + has_many :dossier_labels, dependent: :destroy + has_many :labels, through: :dossier_labels after_destroy_commit :log_destroy diff --git a/app/models/dossier_label.rb b/app/models/dossier_label.rb new file mode 100644 index 000000000..6e9cc96f5 --- /dev/null +++ b/app/models/dossier_label.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class DossierLabel < ApplicationRecord + belongs_to :dossier + belongs_to :label +end diff --git a/app/models/label.rb b/app/models/label.rb new file mode 100644 index 000000000..aaeeebf0d --- /dev/null +++ b/app/models/label.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class Label < ApplicationRecord + belongs_to :procedure + has_many :dossier_labels, dependent: :destroy + + NAME_MAX_LENGTH = 30 + GENERIC_LABELS = [ + { name: 'À examiner', color: 'purple_glycine' }, + { name: 'À relancer', color: 'green_tilleul_verveine' }, + { name: 'Complet', color: 'green_emeraude' }, + { name: 'À signer', color: 'blue_ecume' }, + { name: 'Urgent', color: 'pink_macaron' } + ] + + enum color: { + green_tilleul_verveine: "green-tilleul-verveine", + green_bourgeon: "green-bourgeon", + green_emeraude: "green-emeraude", + green_menthe: "green-menthe", + blue_ecume: "blue-ecume", + purple_glycine: "purple-glycine", + pink_macaron: "pink-macaron", + yellow_tournesol: "yellow-tournesol", + brown_cafe_creme: "brown-cafe-creme", + beige_gris_galet: "beige-gris-galet" + } + + validates :name, :color, presence: true + validates :name, length: { maximum: NAME_MAX_LENGTH } + + def self.class_name(color) + Label.colors.fetch(color.underscore) + end +end diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 1d87dd735..3aebfec72 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -60,6 +60,7 @@ class Procedure < ApplicationRecord has_and_belongs_to_many :procedure_tags has_many :bulk_messages, dependent: :destroy + has_many :labels, dependent: :destroy def active_dossier_submitted_message published_dossier_submitted_message || draft_dossier_submitted_message @@ -528,6 +529,7 @@ class Procedure < ApplicationRecord procedure.closing_notification_en_cours = false procedure.template = false procedure.monavis_embed = nil + procedure.labels = labels.map(&:dup) if !procedure.valid? procedure.errors.attribute_names.each do |attribute| @@ -935,6 +937,12 @@ class Procedure < ApplicationRecord end end + def create_generic_labels + Label::GENERIC_LABELS.each do |label| + Label.create(name: label[:name], color: label[:color], procedure_id: self.id) + end + end + def stable_ids_used_by_routing_rules @stable_ids_used_by_routing_rules ||= groupe_instructeurs.flat_map { _1.routing_rule&.sources }.compact end diff --git a/app/services/dossier_filter_service.rb b/app/services/dossier_filter_service.rb index d43dae082..54e700965 100644 --- a/app/services/dossier_filter_service.rb +++ b/app/services/dossier_filter_service.rb @@ -56,6 +56,11 @@ class DossierFilterService .order("#{sanitized_column(table, column)} #{order}") .pluck(:id) .uniq + when 'dossier_labels' + dossiers.includes(table) + .order("#{self.class.sanitized_column(table, column)} #{order}") + .pluck(:id) + .uniq when 'self', 'user', 'individual', 'etablissement', 'groupe_instructeur' (table == 'self' ? dossiers : dossiers.includes(table)) .order("#{sanitized_column(table, column)} #{order}") @@ -122,6 +127,11 @@ class DossierFilterService dossiers .includes(table) .filter_ilike(table, column, values) # ilike or where column == 'value' are both valid, we opted for ilike + when 'dossier_labels' + assert_supported_column(table, column) + dossiers + .joins(:dossier_labels) + .where(dossier_labels: { label_id: values }) when 'groupe_instructeur' assert_supported_column(table, column) diff --git a/app/services/dossier_projection_service.rb b/app/services/dossier_projection_service.rb index dfe9039b7..a79aeacba 100644 --- a/app/services/dossier_projection_service.rb +++ b/app/services/dossier_projection_service.rb @@ -123,6 +123,18 @@ class DossierProjectionService fields[0][:id_value_h] = id_value_h + when 'dossier_labels' + columns = fields.map { _1[COLUMN].to_sym } + + id_value_h = + DossierLabel + .includes(:label) + .where(dossier_id: dossiers_ids) + .pluck('dossier_id, labels.name, labels.color') + .group_by { |dossier_id, _| dossier_id } + + fields[0][:id_value_h] = id_value_h.transform_values { |v| { value: v, type: :label } } + when 'procedure' Dossier .joins(:procedure) diff --git a/app/tasks/maintenance/backfill_labels_for_procedures_task.rb b/app/tasks/maintenance/backfill_labels_for_procedures_task.rb new file mode 100644 index 000000000..b207454f1 --- /dev/null +++ b/app/tasks/maintenance/backfill_labels_for_procedures_task.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Maintenance + class BackfillLabelsForProceduresTask < MaintenanceTasks::Task + # Cette tâche permet de créer un jeu de labels génériques pour les anciennes procédures + # Plus d'informations sur l'implémentation des labels ici : https://github.com/demarches-simplifiees/demarches-simplifiees.fr/issues/9787 + # 2024-10-15 + + include RunnableOnDeployConcern + + run_on_first_deploy + + def collection + Procedure + .includes(:labels) + .where(labels: { id: nil }) + end + + def process(procedure) + Label::GENERIC_LABELS.each do |label| + Label.create(name: label[:name], color: label[:color], procedure_id: procedure.id) + end + end + end +end diff --git a/app/views/administrateurs/labels/_form.html.haml b/app/views/administrateurs/labels/_form.html.haml new file mode 100644 index 000000000..6742b1d35 --- /dev/null +++ b/app/views/administrateurs/labels/_form.html.haml @@ -0,0 +1,15 @@ += form_with model: label, url: [:admin, @procedure, @label], local: true do |f| + = render Dsfr::InputComponent.new(form: f, attribute: :name, input_type: :text_field, opts: { maxlength: Label::NAME_MAX_LENGTH}) + + %fieldset.fr-fieldset + %legend.fr-fieldset__legend.fr-fieldset__legend--regular + = t('activerecord.attributes.label.color') + = asterisk + + - @colors_collection.each do |color| + .fr-fieldset__element.fr-fieldset__element--inline + .fr-radio-group + = f.radio_button :color, color, checked: (label.color == color) + = f.label :color, t("activerecord.attributes.label/color.#{color}"), value: color, class: "fr-label fr-tag fr-tag--sm fr-tag--#{Label.class_name(color)}" + + = render Procedure::FixedFooterComponent.new(procedure: @procedure, form: f) diff --git a/app/views/administrateurs/labels/edit.html.haml b/app/views/administrateurs/labels/edit.html.haml new file mode 100644 index 000000000..8f3784ca4 --- /dev/null +++ b/app/views/administrateurs/labels/edit.html.haml @@ -0,0 +1,20 @@ +- content_for :title, "Modifier le label" + += render partial: 'administrateurs/breadcrumbs', + locals: { steps: [['Démarches', admin_procedures_path], + [@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)], + ['gestion des labels', [:admin, @procedure, :labels]], + ['Modifier le label']] } + + +.fr-container + .fr-mb-3w + = link_to "Liste de tous les labels", + [:admin, @procedure, :labels], + class: "fr-link fr-icon-arrow-left-line fr-link--icon-left" + + %h1.fr-h2 + Modifier le label + + = render partial: 'form', + locals: { label: @label, procedure_id: @procedure.id } diff --git a/app/views/administrateurs/labels/index.html.haml b/app/views/administrateurs/labels/index.html.haml new file mode 100644 index 000000000..f0dc512f5 --- /dev/null +++ b/app/views/administrateurs/labels/index.html.haml @@ -0,0 +1,43 @@ +- content_for :title, "Labels" + += render partial: 'administrateurs/breadcrumbs', + locals: { steps: [['Démarches', admin_procedures_path], + [@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)], + ['Labels']] } + +.fr-container + %h1.fr-h2 Labels + + = link_to "Nouveau label", + [:new, :admin, @procedure, :label], + class: "fr-btn fr-btn--primary fr-btn--icon-left fr-icon-add-circle-line mb-3" + + - if @procedure.labels.present? + .fr-table.fr-table--layout-fixed.fr-table--bordered + %table + %caption Liste des labels + %thead + %tr + %th{ scope: "col" } + Nom + %th.change{ scope: "col" } + Actions + + %tbody + - @labels.each do |label| + %tr + %td + = tag_label(label.name, label.color) + %td.change + + = link_to 'Modifier', + [:edit, :admin, @procedure, label], + class: 'fr-btn fr-btn--sm fr-btn--secondary fr-btn--icon-left fr-icon-pencil-line' + + = link_to 'Supprimer', + [:admin, @procedure, label], + method: :delete, + data: { confirm: "Confirmez vous la suppression de #{label.name}" }, + class: 'fr-btn fr-btn--sm fr-btn--secondary fr-btn--icon-left fr-icon-delete-line fr-ml-1w' + += render Procedure::FixedFooterComponent.new(procedure: @procedure) diff --git a/app/views/administrateurs/labels/new.html.haml b/app/views/administrateurs/labels/new.html.haml new file mode 100644 index 000000000..fc4713cd3 --- /dev/null +++ b/app/views/administrateurs/labels/new.html.haml @@ -0,0 +1,20 @@ +- content_for :title, "Nouveau label" + += render partial: 'administrateurs/breadcrumbs', + locals: { steps: [['Démarches', admin_procedures_path], + [@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)], + ['gestion des labels', [:admin, @procedure, :labels]], + ['Nouveau label']] } + + +.fr-container + .fr-mb-3w + = link_to "Liste de tous les labels", + [:admin, @procedure, :labels], + class: "fr-link fr-icon-arrow-left-line fr-link--icon-left" + + %h1.fr-h2 + Créer un nouveau label + + = render partial: 'form', + locals: { label: @label, procedure_id: @procedure.id } diff --git a/app/views/administrateurs/procedures/show.html.haml b/app/views/administrateurs/procedures/show.html.haml index 4414f921f..877f4ef98 100644 --- a/app/views/administrateurs/procedures/show.html.haml +++ b/app/views/administrateurs/procedures/show.html.haml @@ -98,3 +98,4 @@ = render Procedure::Card::DossierSubmittedMessageComponent.new(procedure: @procedure) = render Procedure::Card::ChorusComponent.new(procedure: @procedure) = render Procedure::Card::AccuseLectureComponent.new(procedure: @procedure) + = render Procedure::Card::LabelsComponent.new(procedure: @procedure) diff --git a/app/views/instructeurs/dossiers/_header_top.html.haml b/app/views/instructeurs/dossiers/_header_top.html.haml index 765abe36b..7fe1fb2f8 100644 --- a/app/views/instructeurs/dossiers/_header_top.html.haml +++ b/app/views/instructeurs/dossiers/_header_top.html.haml @@ -1,11 +1,11 @@ #header-top.fr-container - .flex.fr-mb-3w + .flex %div %h1.fr-h3.fr-mb-1w = "Dossier nº #{dossier.id}" = link_to dossier.procedure.libelle.truncate_words(10), instructeur_procedure_path(dossier.procedure), title: dossier.procedure.libelle, class: "fr-link" - .fr-mt-2w.badge-group + .fr-mt-2w.fr-badge-group = procedure_badge(dossier.procedure) = status_badge(dossier.state) @@ -16,7 +16,6 @@ = render Instructeurs::SVASVRDecisionBadgeComponent.new(projection_or_dossier: dossier, procedure: dossier.procedure, with_label: true) - .header-actions.fr-ml-auto = render partial: 'instructeurs/dossiers/header_actions', locals: { dossier: } = render partial: 'instructeurs/dossiers/print_and_export_actions', locals: { dossier: } @@ -26,3 +25,30 @@ - if dossier.user_deleted? %p.fr-mb-1w %small L’usager a supprimé son compte. Vous pouvez archiver puis supprimer le dossier. + + - if dossier.procedure.labels.present? + .fr-mb-3w + - if dossier.labels.present? + - dossier.labels.each do |label| + = tag_label(label.name, label.color) + + = render Dropdown::MenuComponent.new(wrapper: :span, button_options: { class: ['fr-btn--sm fr-btn--tertiary-no-outline fr-pl-1v']}, menu_options: { class: ['dropdown-label left-aligned'] }) do |menu| + - if dossier.labels.empty? + - menu.with_button_inner_html do + Ajouter un label + + - menu.with_form do + = form_with(url: dossier_labels_instructeur_dossier_path(dossier_id: dossier.id, procedure_id: dossier.procedure.id), method: :post, class: 'fr-p-3w', data: { controller: 'autosubmit', turbo: 'true' }) do |f| + %fieldset.fr-fieldset.fr-mt-2w.fr-mb-0 + = f.collection_check_boxes :label_id, dossier.procedure.labels, :id, :name, include_hidden: false do |b| + .fr-fieldset__element + .fr-checkbox-group.fr-checkbox-group--sm.fr-mb-1w + = b.check_box(checked: DossierLabel.find_by(dossier_id: dossier.id, label_id: b.value).present? ) + = b.label(class: "fr-label fr-tag fr-tag--sm fr-tag--#{Label.colors.fetch(b.object.color)}") { b.text } + + %hr + %p.fr-text--sm.fr-text-mention--grey.fr-mb-0 + %b Besoin d'autres labels ? + %br + Contactez les + = link_to 'administrateurs de la démarche', administrateurs_instructeur_procedure_path(dossier.procedure), class: 'fr-link fr-link--sm', **external_link_attributes diff --git a/app/views/instructeurs/procedures/show.html.haml b/app/views/instructeurs/procedures/show.html.haml index e36560e88..8c6063308 100644 --- a/app/views/instructeurs/procedures/show.html.haml +++ b/app/views/instructeurs/procedures/show.html.haml @@ -132,12 +132,12 @@ %td - if p.hidden_by_administration_at.present? %span.cell-link - = column + = column.is_a?(Hash) ? tags_label(column[:value]) : column - if p.hidden_by_user_at.present? = "- #{t("views.instructeurs.dossiers.deleted_reason.#{p.hidden_by_reason}")}" - else %a.cell-link{ href: path } - = column + = column.is_a?(Hash) ? tags_label(column[:value]) : column = "- #{t("views.instructeurs.dossiers.deleted_reason.#{p.hidden_by_reason}")}" if p.hidden_by_user_at.present? %td.status-col diff --git a/app/views/invites/_dropdown.html.haml b/app/views/invites/_dropdown.html.haml index 3d7de5a12..e2da2ffab 100644 --- a/app/views/invites/_dropdown.html.haml +++ b/app/views/invites/_dropdown.html.haml @@ -5,7 +5,7 @@ = dsfr_icon('fr-icon-user-add-fill', :sm, :mr) - if invites.present? = t('views.invites.dropdown.view_invited_people') - %span.badge= invites.size + %span.fr-badge.fr-ml-1v= invites.size - else - if dossier.read_only? = t('views.invites.dropdown.invite_to_view') diff --git a/app/views/root/patron.html.haml b/app/views/root/patron.html.haml index 968b94d70..e6b2dd10e 100644 --- a/app/views/root/patron.html.haml +++ b/app/views/root/patron.html.haml @@ -153,11 +153,6 @@ %span.label.refused .label.refused %span.label.without-continuation .label.without-continuation - %h1 Badges - - %span.badge 1 - %span.badge.warning 1 - %h1 Cards .card diff --git a/app/views/shared/_tab_item.html.haml b/app/views/shared/_tab_item.html.haml index cd4103ef8..a9de4a2a7 100644 --- a/app/views/shared/_tab_item.html.haml +++ b/app/views/shared/_tab_item.html.haml @@ -3,5 +3,5 @@ %span.notifications{ 'aria-label': 'notifications' } = link_to(url, 'aria-selected': active ? true : nil, class: 'fr-tabs__tab', role: 'tab' ) do - if badge.present? - %span.badge.fr-mr-1w= badge + %span.fr-badge.fr-badge--blue-ecume.fr-mr-1w= badge = label diff --git a/config/locales/models/label/fr.yml b/config/locales/models/label/fr.yml new file mode 100644 index 000000000..ca20608be --- /dev/null +++ b/config/locales/models/label/fr.yml @@ -0,0 +1,17 @@ +fr: + activerecord: + attributes: + label: + color: Couleur + name: Nom + label/color: &color + green_tilleul_verveine: 'tilleul' + green_bourgeon: 'bourgeon' + green_emeraude: 'émeraude' + green_menthe: 'menthe' + blue_ecume: 'écume' + purple_glycine: 'glycine' + pink_macaron: 'macaron' + yellow_tournesol: 'tournesol' + brown_cafe_creme: 'café' + beige_gris_galet: 'galet' diff --git a/config/locales/models/procedure_presentation/en.yml b/config/locales/models/procedure_presentation/en.yml index 78957508d..95ff9d0ff 100644 --- a/config/locales/models/procedure_presentation/en.yml +++ b/config/locales/models/procedure_presentation/en.yml @@ -81,3 +81,5 @@ en: association_date_creation: 'Association date de création' association_date_declaration: 'Association date de déclaration' association_date_publication: 'Association date de publication' + dossier_labels: + label_id: Labels diff --git a/config/locales/models/procedure_presentation/fr.yml b/config/locales/models/procedure_presentation/fr.yml index b46c82a37..f02ba070f 100644 --- a/config/locales/models/procedure_presentation/fr.yml +++ b/config/locales/models/procedure_presentation/fr.yml @@ -85,3 +85,5 @@ fr: association_date_creation: 'Association date de création' association_date_declaration: 'Association date de déclaration' association_date_publication: 'Association date de publication' + dossier_labels: + label_id: Labels diff --git a/config/routes.rb b/config/routes.rb index d194eb920..ed06d5b7b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -512,6 +512,7 @@ Rails.application.routes.draw do resources :commentaires, only: [:destroy] post 'repousser-expiration' => 'dossiers#extend_conservation' post 'repousser-expiration-and-restore' => 'dossiers#extend_conservation_and_restore' + post 'dossier_labels' => 'dossiers#dossier_labels' get 'geo_data' get 'apercu_attestation' get 'bilans_bdf' @@ -707,6 +708,8 @@ Rails.application.routes.draw do get 'preview', on: :member end + resources :labels, controller: 'labels' + resource :attestation_template, only: [:show, :edit, :update, :create] do get 'preview', on: :member end diff --git a/db/migrate/20240924151336_create_labels.rb b/db/migrate/20240924151336_create_labels.rb new file mode 100644 index 000000000..b6897db2a --- /dev/null +++ b/db/migrate/20240924151336_create_labels.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class CreateLabels < ActiveRecord::Migration[7.0] + def change + create_table :labels do |t| + t.string :name + t.string :color + t.references :procedure, null: false, foreign_key: true + t.timestamps + end + end +end diff --git a/db/migrate/20240925133719_create_dossier_labels.rb b/db/migrate/20240925133719_create_dossier_labels.rb new file mode 100644 index 000000000..5b4ed3ac9 --- /dev/null +++ b/db/migrate/20240925133719_create_dossier_labels.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class CreateDossierLabels < ActiveRecord::Migration[7.0] + def change + create_table :dossier_labels do |t| + t.references :dossier, null: false, foreign_key: true + t.references :label, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 93aea3e2b..bdb19f2c4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -416,6 +416,15 @@ ActiveRecord::Schema[7.0].define(version: 2024_10_14_084333) do t.index ["resolved_at"], name: "index_dossier_corrections_on_resolved_at", where: "((resolved_at IS NULL) OR (resolved_at IS NOT NULL))" end + create_table "dossier_labels", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "dossier_id", null: false + t.bigint "label_id", null: false + t.datetime "updated_at", null: false + t.index ["dossier_id"], name: "index_dossier_labels_on_dossier_id" + t.index ["label_id"], name: "index_dossier_labels_on_label_id" + end + create_table "dossier_operation_logs", force: :cascade do |t| t.boolean "automatic_operation", default: false, null: false t.bigint "bill_signature_id" @@ -820,6 +829,15 @@ ActiveRecord::Schema[7.0].define(version: 2024_10_14_084333) do t.index ["email", "dossier_id"], name: "index_invites_on_email_and_dossier_id", unique: true end + create_table "labels", force: :cascade do |t| + t.string "color" + t.datetime "created_at", null: false + t.string "name" + t.bigint "procedure_id", null: false + t.datetime "updated_at", null: false + t.index ["procedure_id"], name: "index_labels_on_procedure_id" + end + create_table "maintenance_tasks_runs", force: :cascade do |t| t.text "arguments" t.text "backtrace" @@ -1280,6 +1298,8 @@ ActiveRecord::Schema[7.0].define(version: 2024_10_14_084333) do add_foreign_key "dossier_batch_operations", "dossiers" add_foreign_key "dossier_corrections", "commentaires" add_foreign_key "dossier_corrections", "dossiers" + add_foreign_key "dossier_labels", "dossiers" + add_foreign_key "dossier_labels", "labels" add_foreign_key "dossier_operation_logs", "bill_signatures" add_foreign_key "dossier_transfer_logs", "dossiers" add_foreign_key "dossiers", "batch_operations" @@ -1300,6 +1320,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_10_14_084333) do add_foreign_key "groupe_instructeurs", "procedures" add_foreign_key "initiated_mails", "procedures" add_foreign_key "instructeurs", "users" + add_foreign_key "labels", "procedures" add_foreign_key "merge_logs", "users" add_foreign_key "procedure_presentations", "assign_tos" add_foreign_key "procedure_revision_types_de_champ", "procedure_revision_types_de_champ", column: "parent_id" diff --git a/spec/components/main_navigation/instructeur_expert_navigation_component_spec.rb b/spec/components/main_navigation/instructeur_expert_navigation_component_spec.rb index 486af1473..309430144 100644 --- a/spec/components/main_navigation/instructeur_expert_navigation_component_spec.rb +++ b/spec/components/main_navigation/instructeur_expert_navigation_component_spec.rb @@ -68,7 +68,7 @@ describe MainNavigation::InstructeurExpertNavigationComponent, type: :component it 'renders a link to expert all avis with current page class' do expect(subject).to have_link('Avis', href: component.helpers.expert_all_avis_path) expect(subject).to have_selector('a[aria-current="true"]', text: 'Avis') - expect(subject).not_to have_selector('span.badge') + expect(subject).not_to have_selector('span.fr-badge') end it 'does not have Démarches link' do @@ -79,7 +79,7 @@ describe MainNavigation::InstructeurExpertNavigationComponent, type: :component let(:unanswered) { 2 } it 'renders an unanswered avis badge for the expert' do - expect(subject).to have_selector('span.badge.warning', text: '2') + expect(subject).to have_selector('span.fr-badge', text: '2') end end diff --git a/spec/controllers/administrateurs/labels_controller_spec.rb b/spec/controllers/administrateurs/labels_controller_spec.rb new file mode 100644 index 000000000..55b68a9f3 --- /dev/null +++ b/spec/controllers/administrateurs/labels_controller_spec.rb @@ -0,0 +1,168 @@ +# frozen_string_literal: true + +describe Administrateurs::LabelsController, type: :controller do + let(:admin) { administrateurs(:default_admin) } + let(:procedure) { create(:procedure, administrateur: admin) } + let(:admin_2) { create(:administrateur) } + let(:procedure_2) { create(:procedure, administrateur: admin_2) } + + describe '#index' do + render_views + let!(:label_1) { create(:label, procedure:) } + let!(:label_2) { create(:label, procedure:) } + let!(:label_3) { create(:label, procedure:) } + + before do + sign_in(admin.user) + end + + subject { get :index, params: { procedure_id: procedure.id } } + + it 'displays all procedure labels' do + subject + expect(response.body).to have_link("Nouveau label") + expect(response.body).to have_link("Modifier", count: 3) + expect(response.body).to have_link("Supprimer", count: 3) + end + end + + describe '#create' do + before do + sign_in(admin.user) + end + + subject { post :create, params: params } + + context 'when submitting a new label' do + let(:params) do + { + label: { + name: 'Nouveau label', + color: 'green-bourgeon' + }, + procedure_id: procedure.id + } + end + + it { expect { subject }.to change { Label.count } .by(1) } + + it 'creates a new label' do + subject + expect(flash.alert).to be_nil + expect(flash.notice).to eq('Le label a bien été créé') + expect(Label.last.name).to eq('Nouveau label') + expect(Label.last.color).to eq('green_bourgeon') + expect(procedure.labels.last).to eq(Label.last) + end + end + + context 'when submitting an invalid label' do + let(:params) { { label: { name: 'Nouveau label' }, procedure_id: procedure.id } } + + it { expect { subject }.not_to change { Label.count } } + + it 'does not create a new label' do + subject + expect(flash.alert).to eq(["Le champ « Couleur » doit être rempli"]) + expect(response).to render_template(:new) + expect(assigns(:label).name).to eq('Nouveau label') + end + end + + context 'when submitting a label for a not own procedure' do + let(:params) do + { + label: { + name: 'Nouveau label', + color: 'green-bourgeon' + }, + procedure_id: procedure_2.id + } + end + + it { expect { subject }.not_to change { Label.count } } + + it 'does not create a new label' do + subject + expect(flash.alert).to eq("Démarche inexistante") + expect(response.status).to eq(404) + end + end + end + + describe '#update' do + let!(:label) { create(:label, procedure:) } + let(:label_params) { { name: 'Nouveau nom' } } + let(:params) { { id: label.id, label: label_params, procedure_id: procedure.id } } + + before do + sign_in(admin.user) + end + + subject { patch :update, params: } + + context 'when updating a label' do + it 'updates correctly' do + subject + expect(flash.alert).to be_nil + expect(flash.notice).to eq('Le label a bien été modifié') + expect(label.reload.name).to eq('Nouveau nom') + expect(label.reload.color).to eq('green_bourgeon') + expect(label.reload.updated_at).not_to eq(label.reload.created_at) + expect(response).to redirect_to(admin_procedure_labels_path(procedure_id: procedure.id)) + end + end + + context 'when updating a service with invalid data' do + let(:label_params) { { name: '' } } + + it 'does not update' do + subject + expect(flash.alert).not_to be_nil + expect(response).to render_template(:edit) + expect(label.reload.updated_at).to eq(label.reload.created_at) + end + end + + context 'when updating a label for a not own procedure' do + let(:params) { { id: label.id, label: label_params, procedure_id: procedure_2.id } } + + it 'does not update' do + subject + expect(label.reload.updated_at).to eq(label.reload.created_at) + end + end + end + + describe '#destroy' do + let(:label) { create(:label, procedure:) } + + before do + sign_in(admin.user) + end + + subject { delete :destroy, params: } + + context "when deleting a label" do + let(:params) { { id: label.id, procedure_id: procedure.id } } + + it "delete the label" do + subject + expect { label.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect(flash.notice).to eq('Le label a bien été supprimé') + expect(response).to redirect_to(admin_procedure_labels_path(procedure_id: procedure.id)) + end + end + + context 'when deleting a label for a not own procedure' do + let(:params) { { id: label.id, procedure_id: procedure_2.id } } + + it 'does not delete' do + subject + expect(flash.alert).to eq("Démarche inexistante") + expect(response.status).to eq(404) + expect { label.reload }.not_to raise_error(ActiveRecord::RecordNotFound) + end + end + end +end diff --git a/spec/controllers/administrateurs/procedures_controller_spec.rb b/spec/controllers/administrateurs/procedures_controller_spec.rb index 72b097a5d..469fc8b2f 100644 --- a/spec/controllers/administrateurs/procedures_controller_spec.rb +++ b/spec/controllers/administrateurs/procedures_controller_spec.rb @@ -513,6 +513,11 @@ describe Administrateurs::ProceduresController, type: :controller do expect(response).to redirect_to(champs_admin_procedure_path(Procedure.last)) expect(flash[:notice]).to be_present end + + it "create generic labels" do + expect(subject.labels.size).to eq(5) + expect(subject.labels.first.name).to eq('À examiner') + end end describe "procedure is saved with custom retention period" do @@ -657,7 +662,7 @@ describe Administrateurs::ProceduresController, type: :controller do end describe 'PUT #clone' do - let(:procedure) { create(:procedure, :with_notice, :with_deliberation, administrateur: admin) } + let(:procedure) { create(:procedure, :with_notice, :with_deliberation, :with_labels, administrateur: admin) } let(:params) { { procedure_id: procedure.id } } subject { put :clone, params: params } @@ -679,6 +684,10 @@ describe Administrateurs::ProceduresController, type: :controller do expect(Procedure.last.cloned_from_library).to be_falsey expect(Procedure.last.notice.attached?).to be_truthy expect(Procedure.last.deliberation.attached?).to be_truthy + expect(Procedure.last.labels.present?).to be_truthy + expect(Procedure.last.labels.first.procedure_id).to eq(Procedure.last.id) + expect(procedure.labels.first.procedure_id).to eq(procedure.id) + expect(flash[:notice]).to have_content 'Démarche clonée. Pensez à vérifier la présentation et choisir le service à laquelle cette démarche est associée.' end @@ -700,6 +709,7 @@ describe Administrateurs::ProceduresController, type: :controller do it 'creates a new procedure and redirect to it' do expect(response).to redirect_to admin_procedure_path(id: Procedure.last.id) + expect(Procedure.last.labels.present?).to be_truthy expect(flash[:notice]).to have_content 'Démarche clonée. Pensez à vérifier la présentation et choisir le service à laquelle cette démarche est associée.' end end diff --git a/spec/controllers/instructeurs/dossiers_controller_spec.rb b/spec/controllers/instructeurs/dossiers_controller_spec.rb index 4ad9070b2..3ec50f3cc 100644 --- a/spec/controllers/instructeurs/dossiers_controller_spec.rb +++ b/spec/controllers/instructeurs/dossiers_controller_spec.rb @@ -1518,4 +1518,38 @@ describe Instructeurs::DossiersController, type: :controller do expect([Champs::PieceJustificativeChamp, Champs::TitreIdentiteChamp, Commentaire]).to include(*assigns(:gallery_attachments).map { _1.record.class }) end end + + describe 'dossier_labels' do + let(:procedure) { create(:procedure, :with_labels, instructeurs: [instructeur]) } + let!(:dossier) { create(:dossier, :en_construction, procedure:) } + context 'it create dossier labels' do + subject { post :dossier_labels, params: { procedure_id: procedure.id, dossier_id: dossier.id, label_id: [Label.first.id] }, format: :turbo_stream } + it 'works' do + subject + dossier.reload + + expect(dossier.dossier_labels.count).to eq(1) + expect(subject.body).to include('fr-tag--purple-glycine') + expect(subject.body).not_to include('Ajouter un label') + end + end + + context 'it remove dossier labels' do + before do + DossierLabel.create(dossier_id: dossier.id, label_id: dossier.procedure.labels.first.id) + end + + subject { post :dossier_labels, params: { procedure_id: procedure.id, dossier_id: dossier.id, label_id: [] }, format: :turbo_stream } + + it 'works' do + expect(dossier.dossier_labels.count).to eq(1) + + subject + dossier.reload + + expect(dossier.dossier_labels.count).to eq(0) + expect(subject.body).to include('Ajouter un label') + end + end + end end diff --git a/spec/controllers/instructeurs/procedures_controller_spec.rb b/spec/controllers/instructeurs/procedures_controller_spec.rb index 1f3ea9acb..8fcd18be9 100644 --- a/spec/controllers/instructeurs/procedures_controller_spec.rb +++ b/spec/controllers/instructeurs/procedures_controller_spec.rb @@ -637,6 +637,38 @@ describe Instructeurs::ProceduresController, type: :controller do it { expect(assigns(:last_export)).to eq(nil) } end end + + context 'dossier labels' do + let(:procedure) { create(:procedure, :with_labels, instructeurs: [instructeur]) } + let!(:dossier) { create(:dossier, :en_construction, procedure:, groupe_instructeur: gi_2) } + let!(:dossier_2) { create(:dossier, :en_construction, procedure:, groupe_instructeur: gi_2) } + let(:statut) { 'tous' } + let(:label_id) { procedure.find_column(label: 'Labels') } + let!(:procedure_presentation) do + ProcedurePresentation.create!(assign_to: AssignTo.first) + end + render_views + + before do + DossierLabel.create(dossier_id: dossier.id, label_id: dossier.procedure.labels.first.id) + DossierLabel.create(dossier_id: dossier.id, label_id: dossier.procedure.labels.second.id) + DossierLabel.create(dossier_id: dossier_2.id, label_id: dossier.procedure.labels.last.id) + + procedure_presentation.update(displayed_columns: [ + label_id.id + ]) + + subject + end + + it 'displays correctly labels in instructeur table' do + expect(response.body).to include("Labels") + expect(response.body).to have_selector('ul.fr-tags-group li span.fr-tag', text: 'À examiner') + expect(response.body).to have_selector('ul.fr-tags-group li span.fr-tag', text: 'À relancer') + expect(response.body).not_to have_selector('ul li span.fr-tag', text: 'Urgent') + expect(response.body).to have_selector('span.fr-tag', text: 'Urgent') + end + end end end diff --git a/spec/factories/label.rb b/spec/factories/label.rb new file mode 100644 index 000000000..feeae49d1 --- /dev/null +++ b/spec/factories/label.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :label do + name { 'Un label' } + color { 'green-bourgeon' } + association :procedure + end +end diff --git a/spec/factories/procedure.rb b/spec/factories/procedure.rb index 13db75fb4..672aa4d2a 100644 --- a/spec/factories/procedure.rb +++ b/spec/factories/procedure.rb @@ -291,6 +291,12 @@ FactoryBot.define do trait :accuse_lecture do accuse_lecture { true } end + + trait :with_labels do + after(:create) do |procedure, _evaluator| + procedure.create_generic_labels + end + end end end diff --git a/spec/models/concerns/columns_concern_spec.rb b/spec/models/concerns/columns_concern_spec.rb index 44ca9f1d3..1f6f0b98b 100644 --- a/spec/models/concerns/columns_concern_spec.rb +++ b/spec/models/concerns/columns_concern_spec.rb @@ -51,6 +51,7 @@ describe ColumnsConcern do { label: 'Groupe instructeur', table: 'groupe_instructeur', column: 'id', displayable: true, type: :enum, scope: '', value_column: :value, filterable: true }, { label: 'Avis oui/non', table: 'avis', column: 'question_answer', displayable: true, type: :text, scope: '', value_column: :value, filterable: false }, { label: 'France connecté ?', table: 'self', column: 'user_from_france_connect?', displayable: false, type: :text, scope: '', value_column: :value, filterable: false }, + { label: "Labels", table: "dossier_labels", column: "label_id", displayable: true, scope: '', value_column: :value, filterable: true }, { label: 'SIREN', table: 'etablissement', column: 'entreprise_siren', displayable: true, type: :text, scope: '', value_column: :value, filterable: true }, { label: 'Forme juridique', table: 'etablissement', column: 'entreprise_forme_juridique', displayable: true, type: :text, scope: '', value_column: :value, filterable: true }, { label: 'Nom commercial', table: 'etablissement', column: 'entreprise_nom_commercial', displayable: true, type: :text, scope: '', value_column: :value, filterable: true }, diff --git a/spec/system/experts/expert_spec.rb b/spec/system/experts/expert_spec.rb index 7ad4e7685..433bead97 100644 --- a/spec/system/experts/expert_spec.rb +++ b/spec/system/experts/expert_spec.rb @@ -72,7 +72,7 @@ describe 'Inviting an expert:', js: true do expect(page).to have_text('1 avis à donner') expect(page).to have_text('0 avis donnés') - expect(page).to have_selector('.badge', text: 1) + expect(page).to have_selector('.fr-badge', text: 1) expect(page).to have_selector('.notifications') click_on '1 avis à donner' @@ -93,7 +93,7 @@ describe 'Inviting an expert:', js: true do expect(page).to have_text('0 avis à donner') expect(page).to have_text('1 avis donné') - expect(page).not_to have_selector('.badge', text: 1) + expect(page).not_to have_selector('.fr-badge', text: 1) expect(page).not_to have_selector('.notifications') end diff --git a/spec/system/instructeurs/instruction_spec.rb b/spec/system/instructeurs/instruction_spec.rb index 62390e084..098878767 100644 --- a/spec/system/instructeurs/instruction_spec.rb +++ b/spec/system/instructeurs/instruction_spec.rb @@ -272,6 +272,39 @@ describe 'Instructing a dossier:', js: true do after { DownloadHelpers.clear_downloads } end + context 'An instructeur can add labels' do + let(:procedure) { create(:procedure, :with_labels, :published, instructeurs: [instructeur]) } + + scenario 'An instructeur can add and remove labels to a dossier' do + log_in(instructeur.email, password) + + visit instructeur_dossier_path(procedure, dossier) + click_on 'Ajouter un label' + + check 'À relancer', allow_label_click: true + expect(page).to have_css('.fr-tag', text: "À relancer", count: 2) + expect(dossier.dossier_labels.count).to eq(1) + + expect(page).not_to have_text('Ajouter un label') + find('span.dropdown button.dropdown-button').click + + expect(page).to have_checked_field('À relancer') + check 'Complet', allow_label_click: true + + expect(page).to have_css('.fr-tag', text: "Complet", count: 2) + expect(dossier.dossier_labels.count).to eq(2) + + find('span.dropdown button.dropdown-button').click + uncheck 'À relancer', allow_label_click: true + + expect(page).to have_unchecked_field('À relancer') + expect(page).to have_checked_field('Complet') + expect(page).to have_css('.fr-tag', text: "À relancer", count: 1) + expect(page).to have_css('.fr-tag', text: "Complet", count: 2) + expect(dossier.dossier_labels.count).to eq(1) + end + end + def log_in(email, password, check_email: true) visit new_user_session_path expect(page).to have_current_path(new_user_session_path) diff --git a/spec/system/instructeurs/procedure_filters_spec.rb b/spec/system/instructeurs/procedure_filters_spec.rb index c105775f4..1cd23ea13 100644 --- a/spec/system/instructeurs/procedure_filters_spec.rb +++ b/spec/system/instructeurs/procedure_filters_spec.rb @@ -2,7 +2,7 @@ describe "procedure filters" do let(:instructeur) { create(:instructeur) } - let(:procedure) { create(:procedure, :published, types_de_champ_public:, instructeurs: [instructeur]) } + let(:procedure) { create(:procedure, :published, :with_labels, types_de_champ_public:, instructeurs: [instructeur]) } let(:types_de_champ_public) { [{ type: :text }] } let!(:type_de_champ) { procedure.active_revision.types_de_champ_public.first } let!(:new_unfollow_dossier) { create(:dossier, procedure: procedure, state: Dossier.states.fetch(:en_instruction)) } @@ -94,6 +94,7 @@ describe "procedure filters" do expect(page).to have_link(new_unfollow_dossier_2.user.email) end end + describe 'with dropdown' do let(:types_de_champ_public) { [{ type: :drop_down_list }] } @@ -171,6 +172,15 @@ describe "procedure filters" do end end + describe 'dossier labels' do + scenario "should be able to filter by dossier labels", js: true do + DossierLabel.create!(dossier_id: new_unfollow_dossier.id, label_id: procedure.labels.first.id) + add_filter('Labels', procedure.labels.first.name, type: :enum) + expect(page).to have_link(new_unfollow_dossier.id.to_s) + expect(page).not_to have_link(new_unfollow_dossier_2.id.to_s) + end + end + scenario "should be able to add and remove two filters for the same field", js: true do add_filter(type_de_champ.libelle, champ.value) add_filter(type_de_champ.libelle, champ_2.value) diff --git a/spec/views/instructeur/dossiers/show.html.haml_spec.rb b/spec/views/instructeur/dossiers/show.html.haml_spec.rb index bb7163332..3a567c551 100644 --- a/spec/views/instructeur/dossiers/show.html.haml_spec.rb +++ b/spec/views/instructeur/dossiers/show.html.haml_spec.rb @@ -217,4 +217,44 @@ describe 'instructeurs/dossiers/show', type: :view do expect(subject).to have_selector('a.fr-sidemenu__link', text: 'l1') end end + + describe "Dossier labels" do + let(:procedure) { create(:procedure, :with_labels) } + let(:dossier) { create(:dossier, :en_construction, procedure:) } + + context "Procedure without labels" do + let(:procedure_without_labels) { create(:procedure) } + let(:dossier) { create(:dossier, :en_construction, procedure: procedure_without_labels) } + it 'does not display button to add label or dropdown' do + expect(subject).not_to have_text("Ajouter un label") + expect(subject).not_to have_text("À examiner") + end + end + + context "Dossier without labels" do + it 'displays button with text to add label' do + expect(subject).to have_text("Ajouter un label") + expect(subject).to have_selector("button.dropdown-button") + expect(subject).to have_text("À examiner", count: 1) + within('.dropdown') do + expect(subject).to have_text("À examiner", count: 1) + end + end + end + + context "Dossier with labels" do + before do + DossierLabel.create(dossier_id: dossier.id, label_id: dossier.procedure.labels.first.id) + end + + it 'displays labels and button without text to add label' do + expect(subject).not_to have_text("Ajouter un label") + expect(subject).to have_selector("button.dropdown-button") + expect(subject).to have_text("À examiner", count: 2) + within('.dropdown') do + expect(subject).to have_text("À examiner", count: 1) + end + end + end + end end