diff --git a/app/controllers/buyer/public_markets_controller.rb b/app/controllers/buyer/public_markets_controller.rb index db342ea0..33bdf5d3 100644 --- a/app/controllers/buyer/public_markets_controller.rb +++ b/app/controllers/buyer/public_markets_controller.rb @@ -13,7 +13,7 @@ class PublicMarketsController < ApplicationController def show case step - when :setup, :summary, :lot_config + when :setup, :summary, :lot_config, :form_config render_wizard else @current_category = step.to_s @@ -66,6 +66,8 @@ def step_params lot_limit_enabled: params[:lot_limit_enabled], lot_limit: params[:lot_limit] } + when :form_config + {} else # Category step - collect selected optional fields { selected_attribute_keys: params[:selected_attribute_keys] || [] } diff --git a/app/presenters/public_market_presenter.rb b/app/presenters/public_market_presenter.rb index d4c120d9..51008bb5 100644 --- a/app/presenters/public_market_presenter.rb +++ b/app/presenters/public_market_presenter.rb @@ -6,6 +6,7 @@ class PublicMarketPresenter INITIAL_WIZARD_STEP = :setup LOT_CONFIG_STEP = :lot_config + FORM_CONFIG_STEP = :form_config FINAL_WIZARD_STEP = :summary def initialize(public_market) @@ -14,7 +15,10 @@ def initialize(public_market) def wizard_steps steps = [INITIAL_WIZARD_STEP] - steps << LOT_CONFIG_STEP if @public_market.lots.any? + if @public_market.lots.any? + steps << LOT_CONFIG_STEP + steps << FORM_CONFIG_STEP + end steps + available_category_keys.map(&:to_sym) + [FINAL_WIZARD_STEP] end @@ -100,6 +104,17 @@ def market_types_label_with_source "#{market_types_label} (#{I18n.t('market_types.source.platform')})" end + def lots_by_effective_type + lots_for_config.group_by(&:effective_market_type) + end + + def lot_effective_type_label(lot) + type = lot.effective_market_type + return nil unless type + + I18n.t("market_types.#{type.code}", default: type.code.humanize) + end + def lots @lots ||= @public_market.lots.ordered.to_a end diff --git a/app/services/buyer/public_market_wizard_service.rb b/app/services/buyer/public_market_wizard_service.rb index 04062936..ee57f40a 100644 --- a/app/services/buyer/public_market_wizard_service.rb +++ b/app/services/buyer/public_market_wizard_service.rb @@ -14,6 +14,8 @@ def call handle_setup_step when :lot_config handle_lot_config_step + when :form_config + public_market when :summary complete_market else diff --git a/app/views/buyer/public_markets/form_config.html.erb b/app/views/buyer/public_markets/form_config.html.erb new file mode 100644 index 00000000..88f288d6 --- /dev/null +++ b/app/views/buyer/public_markets/form_config.html.erb @@ -0,0 +1,80 @@ +<% content_for :title, "#{@public_market.editor.name} - #{t('buyer.public_markets.form_config.title')}" %> + +

<%= t('buyer.public_markets.form_config.title') %>

+

<%= t('buyer.public_markets.form_config.subtitle') %>

+ +<%= render 'shared/notice', notice_text: t('buyer.public_markets.form_config.banner') %> + +
+
+
+

+ <%= t('buyer.shared.market_info_title') %> +

+
+ <%= render 'shared/sidebar_market_info', + buyer_label: t('buyer.public_markets.form_config.sidebar_buyer_label'), + market_name: @public_market.buyer_display_name, + typology_label: t('buyer.public_markets.form_config.sidebar_typology_label'), + market_types_label: @public_market.market_type_codes.any? ? @presenter.market_types_label : nil, + deadline_label: t('buyer.shared.deadline_short'), + deadline: l(@public_market.deadline, format: '%d/%m/%Y %H:%M') %> +
+ +
+
+

+ <%= t('buyer.public_markets.form_config.lots_section_title') %> +

+ <%= link_to step_buyer_public_market_path(identifier: @public_market.identifier, id: :lot_config), + class: 'fr-btn fr-btn--tertiary-no-outline fr-btn--sm fr-icon-edit-line', + title: t('buyer.public_markets.form_config.edit_lots_label'), + aria: { label: t('buyer.public_markets.form_config.edit_lots_label') } do %> + <% end %> +
+
+
+ <% @presenter.lots_for_config.each do |lot| %> + + <% type_label = @presenter.lot_effective_type_label(lot) %> + <%= type_label ? "#{lot.name} - #{type_label.downcase}" : lot.name %> + + <% end %> +
+
+
+ +
+
+
+ <% @presenter.lots_by_effective_type.each do |market_type, type_lots| %> + <% if market_type %> + + <%= t("market_types.#{market_type.code}", default: market_type.code.humanize) %> + + <% end %> + + <%= t('buyer.public_markets.form_config.lots_count', count: type_lots.size) %> + + <% end %> +
+ +

<%= t('buyer.public_markets.form_config.form_block_title') %>

+ + <%= form_with url: wizard_path, method: :put, local: true, data: { turbo: false } do |_form| %> +
+ +
+ <% end %> +
+ + +
+
diff --git a/app/views/buyer/public_markets/lot_config.html.erb b/app/views/buyer/public_markets/lot_config.html.erb index 3d4984c0..a695f527 100644 --- a/app/views/buyer/public_markets/lot_config.html.erb +++ b/app/views/buyer/public_markets/lot_config.html.erb @@ -1,7 +1,36 @@ <% content_for :title, "#{@public_market.editor.name} - #{t('buyer.public_markets.lot_config.title')}" %>
-
+
+
+

+ <%= t('buyer.shared.market_info_title') %> +

+
+ <%= render 'shared/sidebar_market_info', + buyer_label: t('buyer.public_markets.lot_config.sidebar_buyer_label'), + market_name: @public_market.buyer_display_name, + typology_label: t('buyer.public_markets.lot_config.sidebar_typology_label'), + market_types_label: @public_market.market_type_codes.any? ? @presenter.market_types_label : nil, + deadline_label: t('buyer.shared.deadline_short'), + deadline: l(@public_market.deadline, format: '%d/%m/%Y %H:%M') %> +
+ +
+

+ <%= t('buyer.public_markets.lot_config.how_it_works_title') %> +

+
+
    +
  1. <%= t('buyer.public_markets.lot_config.how_it_works_step_1') %>
  2. +
  3. <%= t('buyer.public_markets.lot_config.how_it_works_step_2') %>
  4. +
  5. <%= t('buyer.public_markets.lot_config.how_it_works_step_3') %>
  6. +
  7. <%= t('buyer.public_markets.lot_config.how_it_works_step_4') %>
  8. +
+
+
+ +

<%= t('buyer.public_markets.lot_config.title') %>

<%= t('buyer.public_markets.lot_config.subtitle') %>

@@ -25,6 +54,7 @@ + @@ -32,6 +62,14 @@ <% @presenter.lots_for_config.each do |lot| %> + @@ -102,33 +140,4 @@ <% end %> - -
-
-

- <%= t('buyer.shared.market_info_title') %> -

-
- <%= render 'shared/sidebar_market_info', - buyer_label: t('buyer.public_markets.lot_config.sidebar_buyer_label'), - market_name: @public_market.name, - typology_label: t('buyer.public_markets.lot_config.sidebar_typology_label'), - market_types_label: @public_market.market_type_codes.any? ? @presenter.market_types_label : nil, - deadline_label: t('buyer.shared.deadline_short'), - deadline: l(@public_market.deadline, format: '%d/%m/%Y %H:%M') %> -
- -
-

- <%= t('buyer.public_markets.lot_config.how_it_works_title') %> -

-
-
    -
  1. <%= t('buyer.public_markets.lot_config.how_it_works_step_1') %>
  2. -
  3. <%= t('buyer.public_markets.lot_config.how_it_works_step_2') %>
  4. -
  5. <%= t('buyer.public_markets.lot_config.how_it_works_step_3') %>
  6. -
  7. <%= t('buyer.public_markets.lot_config.how_it_works_step_4') %>
  8. -
-
-
diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 57f43ea8..15ff19bb 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -368,7 +368,7 @@ fr: title_suffix: "Synthèse de ma candidature" market_info_title: "Informations du marché" no_fields_configured: "Aucun champ configuré." - finalize: "Finaliser la configuration" + finalize: "Transmettre la configuration" lot_limit: "Limite de lots par candidat" lot_limit_value: one: "%{count} lot maximum" @@ -403,13 +403,13 @@ fr: capacites_techniques_professionnelles: Sélections des renseignements complémentaires (partie 2) summary: "Synthèse" lot_config: - title: "Configurer le type de vos lots" + title: "Configurez le type de vos lots" subtitle: "Vérifiez et ajustez le type de chaque lot avant de configurer les pièces justificatives." lots_section_title: "Vérifiez les types transmis" lots_section_subtitle: "Assurez-vous que les types transmis par la plateforme de marché sont exacts et modifiez-les si besoin." - platform_banner_global_type: "La plateforme %{platform} a transmis le type %{type} pour l'ensemble des lots. Sélectionnez les lots dont le type est incorrect pour les modifier." - platform_banner_per_lot_type: "La plateforme %{platform} a transmis un type distinct pour chaque lot. Sélectionnez les lots dont le type est incorrect pour les modifier." - platform_source: "Plateforme" + platform_banner_global_type: "Votre plateforme d'achat nous a transmis une typologie de marché pour les lots, si le type d'un lot est erroné vous pouvez le sélectionner pour lui appliquer la bonne typologie (travaux, services, fournitures)" + platform_banner_per_lot_type: "Votre plateforme d'achat nous a transmis une typologie de marché pour les lots, si le type d'un lot est erroné vous pouvez le sélectionner pour lui appliquer la bonne typologie (travaux, services, fournitures)" + platform_source: "PLATEFORME" lot_number: "Lot %{number}" lot_limit_section_title: "Indiquez si vous souhaitez limiter le nombre de lots" lot_limit_section_subtitle: "Si vous le souhaitez, vous pouvez ajuster le nombre de lots auxquels une entreprise peut candidater." @@ -424,7 +424,22 @@ fr: how_it_works_step_2: "Modifiez les types de lots si nécessaires" how_it_works_step_3: "Limitez le nombre de lots si nécessaire" how_it_works_step_4: "Passez à la configuration des candidatures" + select_lot: "Sélectionner le lot %{name}" submit: "Configurer les candidatures" + form_config: + title: "Configurez le formulaire de candidature" + subtitle: "Configurez les pièces justificatives attendues dans le cadre de la candidature à votre marché." + banner: "Vous n'avez qu'un seul formulaire de candidature à configurer, Passe Marché se charge de transmettre votre configuration pour les différents lots." + form_block_title: "Formulaire de candidature à configurer" + configure_button: "Configurer" + transmit_button: "Transmettre la configuration" + lots_section_title: "Liste des lots" + lots_count: + one: "1 lot" + other: "%{count} lots" + edit_lots_label: "Modifier la sélection des lots" + sidebar_buyer_label: "Acheteur" + sidebar_typology_label: "Typologie" generic_step: optional_question: Souhaitez-vous demander des renseignements complémentaires ? yes_option: Oui diff --git a/features/buyer_flow.feature b/features/buyer_flow.feature index e4518487..1868c76b 100644 --- a/features/buyer_flow.feature +++ b/features/buyer_flow.feature @@ -22,13 +22,13 @@ Feature: Buyer Configuration Flow And I should see "Lot 1 - Ordinateurs portables" When I click on "Débuter l'activation de" - Then I should see "Configurer le type de vos lots" + Then I should see "Configurez le type de vos lots" When I navigate through all category steps to summary Then I should be on the summary page And I should see "Synthèse des paramètres de la candidature" And I should see "Informations du marché" - And I should see a button "Finaliser la configuration" + And I should see a button "Transmettre la configuration" Scenario: Navigation arrière avec les boutons Précédent Given I am on the summary page for my public market diff --git a/features/buyer_lot_config.feature b/features/buyer_lot_config.feature index fb6554ff..9f4c0ed5 100644 --- a/features/buyer_lot_config.feature +++ b/features/buyer_lot_config.feature @@ -12,7 +12,7 @@ Feature: Buyer Lot Configuration Step Scenario: L'acheteur accède à la page de configuration des lots When I visit the lot_config page for my public market - Then I should see "Configurer le type de vos lots" + Then I should see "Configurez le type de vos lots" And I should see "Lot 1" And I should see "Lot 2" And I should see "Lot 3" @@ -22,7 +22,7 @@ Feature: Buyer Lot Configuration Step When I visit the setup page for my public market And I click on "Débuter l'activation de" Then I should be on the lot_config page - And I should see "Configurer le type de vos lots" + And I should see "Configurez le type de vos lots" Scenario: L'acheteur choisit de ne pas limiter les lots When I visit the lot_config page for my public market @@ -61,4 +61,22 @@ Feature: Buyer Lot Configuration Step Then I should be on the lot_config page When I choose "Non" for lot limit And I submit the lot_config form + Then I should be on the form_config page + And I should see "Configurez le formulaire de candidature" + + Scenario: La page form_config affiche la liste des lots et le bouton Configurer + When I visit the form_config page for my public market + Then I should see "Configurez le formulaire de candidature" + And I should see "Formulaire de candidature à configurer" + And I should see "Configurer" + And I should see "Transmettre la configuration" + + Scenario: Le bouton crayon sur form_config renvoie vers lot_config + When I visit the form_config page for my public market + And I click the edit lots button + Then I should be on the lot_config page + + Scenario: Depuis form_config, Configurer mène à la première étape de catégorie + When I visit the form_config page for my public market + And I click on "Configurer" Then I should be on the first category page diff --git a/features/step_definitions/buyer_flow_steps.rb b/features/step_definitions/buyer_flow_steps.rb index 8a6f77c4..2677adcf 100644 --- a/features/step_definitions/buyer_flow_steps.rb +++ b/features/step_definitions/buyer_flow_steps.rb @@ -9,7 +9,7 @@ @market_identifier ||= @last_api_response['identifier'] public_market = PublicMarket.find_by!(identifier: @market_identifier) presenter = PublicMarketPresenter.new(public_market) - special_steps = %i[setup lot_config summary] + special_steps = %i[setup lot_config form_config summary] @first_category = presenter.wizard_steps.find { |s| special_steps.exclude?(s) } visit step_buyer_public_market_path(identifier: @market_identifier, id: @first_category) end @@ -34,7 +34,7 @@ visit step_buyer_public_market_path(identifier: @market_identifier, id: :summary) end -SPECIAL_WIZARD_STEPS = %i[setup lot_config summary].freeze +SPECIAL_WIZARD_STEPS = %i[setup lot_config form_config summary].freeze When('I navigate through all category steps to summary') do public_market = PublicMarket.find_by!(identifier: @market_identifier) @@ -46,6 +46,11 @@ find('button[type="submit"]').click end + if presenter.wizard_steps.include?(:form_config) && + page.current_path == step_buyer_public_market_path(@market_identifier, :form_config) + find('button[type="submit"]').click + end + category_steps = presenter.wizard_steps.reject { |s| SPECIAL_WIZARD_STEPS.include?(s) } category_steps.each do @@ -66,7 +71,7 @@ Then('I should be on the first category page') do public_market = PublicMarket.find_by!(identifier: @market_identifier) presenter = PublicMarketPresenter.new(public_market) - special_steps = %i[setup lot_config summary] + special_steps = %i[setup lot_config form_config summary] first_category = presenter.wizard_steps.find { |s| special_steps.exclude?(s) } expect(page).to have_current_path(step_buyer_public_market_path(@market_identifier, first_category)) end diff --git a/features/step_definitions/buyer_lot_config_steps.rb b/features/step_definitions/buyer_lot_config_steps.rb index 74ec8909..75e6b459 100644 --- a/features/step_definitions/buyer_lot_config_steps.rb +++ b/features/step_definitions/buyer_lot_config_steps.rb @@ -49,3 +49,17 @@ public_market = PublicMarket.find_by!(identifier: @market_identifier) public_market.update!(lot_limit: limit) end + +When('I visit the form_config page for my public market') do + @market_identifier ||= @last_api_response['identifier'] + visit step_buyer_public_market_path(identifier: @market_identifier, id: :form_config) +end + +Then('I should be on the form_config page') do + @market_identifier ||= @last_api_response['identifier'] + expect(page).to have_current_path(step_buyer_public_market_path(@market_identifier, :form_config)) +end + +When('I click the edit lots button') do + find("a[aria-label='#{I18n.t('buyer.public_markets.form_config.edit_lots_label')}']").click +end diff --git a/spec/presenters/public_market_presenter_spec.rb b/spec/presenters/public_market_presenter_spec.rb index 80cc5fd3..e0a5ff0d 100644 --- a/spec/presenters/public_market_presenter_spec.rb +++ b/spec/presenters/public_market_presenter_spec.rb @@ -306,8 +306,8 @@ it 'returns the global type banner' do text = presenter.platform_type_banner_text - expect(text).to include(public_market.editor.name) - expect(text).to include(I18n.t("market_types.#{market_type.code}")) + expect(text).to be_present + expect(text).to include('plateforme') end end @@ -321,9 +321,8 @@ it 'returns the per-lot type banner' do text = presenter.platform_type_banner_text - expect(text).to include(public_market.editor.name) - expect(text).not_to include(I18n.t("market_types.#{market_type.code}")) - expect(text).not_to include(I18n.t("market_types.#{other_type.code}")) + expect(text).to be_present + expect(text).to include('plateforme') end end end
<%= t('buyer.shared.lot_name') %> <%= t('buyer.shared.market_type_short') %>
+ <%= label_tag "lot_#{lot.id}", class: 'fr-sr-only' do %> + <%= t('buyer.public_markets.lot_config.select_lot', name: lot.name) %> + <% end %> + <%= check_box_tag 'selected_lot_ids[]', lot.id, false, + id: "lot_#{lot.id}", + style: 'width: 1.125rem; height: 1.125rem; cursor: pointer; accent-color: var(--blue-france-sun-113-625);' %> + <%= lot.name %> <%= render 'buyer/public_markets/lot_type_badge', lot: lot %>