diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index 765b4b19b..856c110da 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -5,7 +5,7 @@ module Users layout 'procedure_context', only: [:identite, :update_identite, :siret, :update_siret] ACTIONS_ALLOWED_TO_ANY_USER = [:index, :recherche, :new, :transferer_all] - ACTIONS_ALLOWED_TO_OWNER_OR_INVITE = [:show, :demande, :messagerie, :brouillon, :update_brouillon, :modifier, :update, :create_commentaire] + ACTIONS_ALLOWED_TO_OWNER_OR_INVITE = [:show, :demande, :messagerie, :brouillon, :update_brouillon, :modifier, :update, :create_commentaire, :papertrail] ACTIONS_ALLOWED_TO_OWNER_OR_INVITE_HIDDEN = [:restore] before_action :ensure_ownership!, except: ACTIONS_ALLOWED_TO_ANY_USER + ACTIONS_ALLOWED_TO_OWNER_OR_INVITE + ACTIONS_ALLOWED_TO_OWNER_OR_INVITE_HIDDEN @@ -69,6 +69,11 @@ module Users end end + def papertrail + raise ActionController::BadRequest if dossier.brouillon? + @dossier = dossier + end + def identite @dossier = dossier @user = current_user diff --git a/app/graphql/api/v2/schema.rb b/app/graphql/api/v2/schema.rb index 1b599ab86..697a7bdd9 100644 --- a/app/graphql/api/v2/schema.rb +++ b/app/graphql/api/v2/schema.rb @@ -9,7 +9,11 @@ class API::V2::Schema < GraphQL::Schema context_class API::V2::Context def self.id_from_object(object, type_definition, ctx) - object.to_typed_id + if object.is_a?(Hash) + object[:id] + else + object.to_typed_id + end end def self.object_from_id(id, query_ctx) diff --git a/app/graphql/types/dossier_type.rb b/app/graphql/types/dossier_type.rb index ff4dbd404..b800cecfe 100644 --- a/app/graphql/types/dossier_type.rb +++ b/app/graphql/types/dossier_type.rb @@ -69,7 +69,7 @@ module Types def usager if object.user_deleted? - { email: object.user_email_for(:display), id: -1 } + { email: object.user_email_for(:display), id: '' } else Loaders::Record.for(User).load(object.user_id) end diff --git a/app/helpers/papertrail_helper.rb b/app/helpers/papertrail_helper.rb new file mode 100644 index 000000000..bbe8845d3 --- /dev/null +++ b/app/helpers/papertrail_helper.rb @@ -0,0 +1,19 @@ +module PapertrailHelper + def papertrail_requester_identity(dossier) + if dossier.etablissement.present? + raison_sociale_or_name(dossier.etablissement) + else + [dossier.individual.prenom, dossier.individual.nom.upcase].join(' ') + end + end + + def papertrail_dossier_state(dossier) + raise "Dossiers in 'brouillon' state are not supported" if dossier.brouillon? + # i18n-tasks-use t('users.dossiers.papertrail.dossier_state.en_construction') + # i18n-tasks-use t('users.dossiers.papertrail.dossier_state.en_instruction') + # i18n-tasks-use t('users.dossiers.papertrail.dossier_state.accepte') + # i18n-tasks-use t('users.dossiers.papertrail.dossier_state.refuse') + # i18n-tasks-use t('users.dossiers.papertrail.dossier_state.sans_suite') + I18n.t("users.dossiers.papertrail.states.#{dossier.state}") + end +end diff --git a/app/helpers/turbo_stream_helper.rb b/app/helpers/turbo_stream_helper.rb index 40699c66e..fa14dc358 100644 --- a/app/helpers/turbo_stream_helper.rb +++ b/app/helpers/turbo_stream_helper.rb @@ -31,5 +31,13 @@ module TurboStreamHelper def focus_all(targets) dispatch('dom:mutation', { action: :focus, targets: targets }) end + + def disable(target) + dispatch('dom:mutation', { action: :disable, target: target }) + end + + def enable(target) + dispatch('dom:mutation', { action: :enable, target: target }) + end end end diff --git a/app/javascript/components/MapEditor/hooks.ts b/app/javascript/components/MapEditor/hooks.ts index d292d5e1b..a20391c96 100644 --- a/app/javascript/components/MapEditor/hooks.ts +++ b/app/javascript/components/MapEditor/hooks.ts @@ -38,7 +38,7 @@ export function useFeatureCollection( features: callback(features) })); httpRequest(url) - .js() + .turbo() .catch(() => null); }, [url, setFeatureCollection] diff --git a/app/javascript/controllers/application_controller.ts b/app/javascript/controllers/application_controller.ts index 3ef5e014f..ca753e1f4 100644 --- a/app/javascript/controllers/application_controller.ts +++ b/app/javascript/controllers/application_controller.ts @@ -22,4 +22,19 @@ export class ApplicationController extends Controller { target: document.documentElement }); } + + protected on( + eventName: string, + handler: (event: HandlerEvent) => void + ): void { + const disconnect = this.disconnect; + const callback = (event: Event): void => { + handler(event as HandlerEvent); + }; + this.element.addEventListener(eventName, callback); + this.disconnect = () => { + this.element.removeEventListener(eventName, callback); + disconnect.call(this); + }; + } } diff --git a/app/javascript/controllers/turbo_event_controller.ts b/app/javascript/controllers/turbo_event_controller.ts index f66b63ffd..c42250d18 100644 --- a/app/javascript/controllers/turbo_event_controller.ts +++ b/app/javascript/controllers/turbo_event_controller.ts @@ -18,7 +18,7 @@ export class TurboEventController extends ApplicationController { } } -const MutationAction = z.enum(['show', 'hide', 'focus']); +const MutationAction = z.enum(['show', 'hide', 'focus', 'enable', 'disable']); type MutationAction = z.infer; const Mutation = z.union([ z.object({ @@ -55,6 +55,16 @@ const Mutations: Record void> = { for (const element of findElements(mutation)) { element.focus(); } + }, + disable: (mutation) => { + for (const element of findElements(mutation)) { + element.disabled = true; + } + }, + enable: (mutation) => { + for (const element of findElements(mutation)) { + element.disabled = false; + } } }; diff --git a/app/javascript/controllers/turbo_input_controller.tsx b/app/javascript/controllers/turbo_input_controller.tsx new file mode 100644 index 000000000..d194a7f3c --- /dev/null +++ b/app/javascript/controllers/turbo_input_controller.tsx @@ -0,0 +1,22 @@ +import { httpRequest } from '@utils'; + +import { ApplicationController } from './application_controller'; + +export class TurboInputController extends ApplicationController { + static values = { + url: String + }; + + declare readonly urlValue: string; + + connect(): void { + this.on('input', () => this.debounce(this.load, 200)); + } + + private load(): void { + const target = this.element as HTMLInputElement; + const url = new URL(this.urlValue, document.baseURI); + url.searchParams.append(target.name, target.value); + httpRequest(url.toString()).turbo(); + } +} diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index e34fa2a8c..945d83857 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -8,7 +8,6 @@ import * as Turbo from '@hotwired/turbo'; import '../shared/activestorage/ujs'; import '../shared/remote-poller'; import '../shared/safari-11-file-xhr-workaround'; -import '../shared/remote-input'; import '../shared/franceconnect'; import '../shared/toggle-target'; import '../shared/ujs-error-handling'; @@ -19,6 +18,7 @@ import { } from '../controllers/react_controller'; import { TurboEventController } from '../controllers/turbo_event_controller'; import { GeoAreaController } from '../controllers/geo_area_controller'; +import { TurboInputController } from '../controllers/turbo_input_controller'; import '../new_design/dropdown'; import '../new_design/form-validation'; @@ -96,6 +96,7 @@ const Stimulus = Application.start(); Stimulus.register('react', ReactController); Stimulus.register('turbo-event', TurboEventController); Stimulus.register('geo-area', GeoAreaController); +Stimulus.register('turbo-input', TurboInputController); // Expose globals window.DS = window.DS || DS; diff --git a/app/javascript/shared/remote-input.js b/app/javascript/shared/remote-input.js deleted file mode 100644 index e265a1835..000000000 --- a/app/javascript/shared/remote-input.js +++ /dev/null @@ -1,21 +0,0 @@ -import { delegate, fire, debounce } from '@utils'; - -const remote = 'data-remote'; -const inputChangeSelector = `input[${remote}], textarea[${remote}]`; - -// This is a patch for ujs remote handler. Its purpose is to add -// a debounced input listener. -function handleRemote(event) { - const element = this; - - if (isRemote(element)) { - fire(element, 'change', event); - } -} - -function isRemote(element) { - const value = element.getAttribute(remote); - return value && value !== 'false'; -} - -delegate('input', inputChangeSelector, debounce(handleRemote, 200)); diff --git a/app/models/champ.rb b/app/models/champ.rb index da1919a83..505bfaa97 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -148,7 +148,7 @@ class Champ < ApplicationRecord # predictable input name. def input_name if parent_id - "#{parent.input_name}[#{champs_attributes_accessor}][#{id}]" + "#{parent.input_name}[champs_attributes][#{id}]" else "dossier[#{champs_attributes_accessor}][#{id}]" end diff --git a/app/models/dossier_operation_log.rb b/app/models/dossier_operation_log.rb index 709ae4e2c..131a29009 100644 --- a/app/models/dossier_operation_log.rb +++ b/app/models/dossier_operation_log.rb @@ -12,7 +12,6 @@ # updated_at :datetime not null # bill_signature_id :bigint # dossier_id :bigint -# instructeur_id :bigint # class DossierOperationLog < ApplicationRecord self.ignored_columns = [:instructeur_id] diff --git a/app/views/champs/carte/index.js.erb b/app/views/champs/carte/index.js.erb deleted file mode 100644 index 1cd381cd7..000000000 --- a/app/views/champs/carte/index.js.erb +++ /dev/null @@ -1,9 +0,0 @@ -<%= render_flash(timeout: 5000, fixed: true) %> - -<%= render_to_element("##{@champ.input_group_id} .geo-areas", - partial: 'shared/champs/carte/geo_areas', - locals: { champ: @champ, editing: true }) %> - -<% if @focus %> - <%= fire_event('map:feature:focus', { bbox: @champ.bounding_box }.to_json) %> -<% end %> diff --git a/app/views/champs/carte/index.turbo_stream.haml b/app/views/champs/carte/index.turbo_stream.haml new file mode 100644 index 000000000..e5e4080a4 --- /dev/null +++ b/app/views/champs/carte/index.turbo_stream.haml @@ -0,0 +1,4 @@ += turbo_stream.update dom_id(@champ, :geo_areas), partial: 'shared/champs/carte/geo_areas', locals: { champ: @champ, editing: true } + +- if @focus + = turbo_stream.dispatch 'map:feature:focus', bbox: @champ.bounding_box diff --git a/app/views/champs/dossier_link/show.js.erb b/app/views/champs/dossier_link/show.js.erb deleted file mode 100644 index cdf84195a..000000000 --- a/app/views/champs/dossier_link/show.js.erb +++ /dev/null @@ -1,3 +0,0 @@ -<%= render_to_element("##{@champ.input_group_id} .help-block", - partial: 'shared/champs/dossier_link/help_block', - locals: { id: @linked_dossier_id }) %> diff --git a/app/views/champs/dossier_link/show.turbo_stream.haml b/app/views/champs/dossier_link/show.turbo_stream.haml new file mode 100644 index 000000000..6cb304fca --- /dev/null +++ b/app/views/champs/dossier_link/show.turbo_stream.haml @@ -0,0 +1 @@ += turbo_stream.update dom_id(@champ, :help_block), partial: 'shared/champs/dossier_link/help_block', locals: { id: @linked_dossier_id } diff --git a/app/views/champs/siret/show.js.erb b/app/views/champs/siret/show.js.erb deleted file mode 100644 index 85d3c51c9..000000000 --- a/app/views/champs/siret/show.js.erb +++ /dev/null @@ -1,3 +0,0 @@ -<%= render_to_element("##{@champ.input_group_id} .siret-info", - partial: 'shared/champs/siret/etablissement', - locals: { siret: @siret, etablissement: @etablissement }) %> diff --git a/app/views/champs/siret/show.turbo_stream.haml b/app/views/champs/siret/show.turbo_stream.haml new file mode 100644 index 000000000..be1c3a1c8 --- /dev/null +++ b/app/views/champs/siret/show.turbo_stream.haml @@ -0,0 +1 @@ += turbo_stream.update dom_id(@champ, :siret_info), partial: 'shared/champs/siret/etablissement', locals: { siret: @siret, etablissement: @etablissement } diff --git a/app/views/password_complexity/_field.html.haml b/app/views/password_complexity/_field.html.haml index eaf29e700..2e031f574 100644 --- a/app/views/password_complexity/_field.html.haml +++ b/app/views/password_complexity/_field.html.haml @@ -1,4 +1,4 @@ -= form.password_field :password, autofocus: true, autocomplete: 'off', placeholder: 'Mot de passe', data: { remote: test_complexity, url: show_password_complexity_path } += form.password_field :password, autofocus: true, autocomplete: 'off', placeholder: 'Mot de passe', data: { controller: test_complexity ? 'turbo-input' : false, turbo_input_url_value: show_password_complexity_path } - if test_complexity #complexity-bar.password-complexity diff --git a/app/views/password_complexity/show.js.erb b/app/views/password_complexity/show.js.erb deleted file mode 100644 index 1d83ac45a..000000000 --- a/app/views/password_complexity/show.js.erb +++ /dev/null @@ -1,3 +0,0 @@ -<%= render_to_element('#complexity-label', partial: 'label', outer: true) %> -<%= render_to_element('#complexity-bar', partial: 'bar', outer: true) %> -<%= raw("document.querySelector('#submit-password').disabled = #{@score < @min_complexity || @length < @min_length};") %> diff --git a/app/views/password_complexity/show.turbo_stream.haml b/app/views/password_complexity/show.turbo_stream.haml new file mode 100644 index 000000000..3fd3648f6 --- /dev/null +++ b/app/views/password_complexity/show.turbo_stream.haml @@ -0,0 +1,6 @@ += turbo_stream.replace 'complexity-label', partial: 'label' += turbo_stream.replace 'complexity-bar', partial: 'bar' +- if @score < @min_complexity || @length < @min_length + = turbo_stream.disable 'submit-password' +- else + = turbo_stream.enable 'submit-password' diff --git a/app/views/shared/dossiers/editable_champs/_carte.html.haml b/app/views/shared/dossiers/editable_champs/_carte.html.haml index 41fa07ac7..6677e6dc4 100644 --- a/app/views/shared/dossiers/editable_champs/_carte.html.haml +++ b/app/views/shared/dossiers/editable_champs/_carte.html.haml @@ -1,4 +1,4 @@ = react_component("MapEditor", featureCollection: champ.to_feature_collection, url: champs_carte_features_path(champ), options: champ.render_options) -.geo-areas +.geo-areas{ id: dom_id(champ, :geo_areas) } = render partial: 'shared/champs/carte/geo_areas', locals: { champ: champ, editing: true } diff --git a/app/views/shared/dossiers/editable_champs/_dossier_link.html.haml b/app/views/shared/dossiers/editable_champs/_dossier_link.html.haml index f3a72c1ed..4e0ad4832 100644 --- a/app/views/shared/dossiers/editable_champs/_dossier_link.html.haml +++ b/app/views/shared/dossiers/editable_champs/_dossier_link.html.haml @@ -5,7 +5,7 @@ placeholder: "Numéro de dossier", autocomplete: 'off', required: champ.mandatory?, - data: { remote: true, url: champs_dossier_link_path(champ.id) } + data: { controller: 'turbo-input', turbo_input_url_value: champs_dossier_link_path(champ.id) } - .help-block + .help-block{ id: dom_id(champ, :help_block) } = render partial: 'shared/champs/dossier_link/help_block', locals: { id: champ.value } diff --git a/app/views/shared/dossiers/editable_champs/_siret.html.haml b/app/views/shared/dossiers/editable_champs/_siret.html.haml index 402a20428..fd8681112 100644 --- a/app/views/shared/dossiers/editable_champs/_siret.html.haml +++ b/app/views/shared/dossiers/editable_champs/_siret.html.haml @@ -2,11 +2,11 @@ id: champ.input_id, aria: { describedby: champ.describedby_id }, placeholder: champ.libelle, - data: { remote: true, debounce: true, url: champs_siret_path(champ.id), spinner: true }, + data: { controller: 'turbo-input', turbo_input_url_value: champs_siret_path(champ.id) }, required: champ.mandatory?, pattern: "[0-9]{14}", title: "Le numéro de SIRET doit comporter exactement 14 chiffres" .spinner.right.hidden -.siret-info +.siret-info{ id: dom_id(champ, :siret_info) } - if champ.etablissement.present? = render partial: 'shared/dossiers/editable_champs/etablissement_titre', locals: { etablissement: champ.etablissement } diff --git a/app/views/users/dossiers/papertrail.pdf.prawn b/app/views/users/dossiers/papertrail.pdf.prawn new file mode 100644 index 000000000..937c86b2b --- /dev/null +++ b/app/views/users/dossiers/papertrail.pdf.prawn @@ -0,0 +1,99 @@ +require 'prawn/measurement_extensions' + +#----- A4 page size +page_size = 'A4' +page_width = 595 + +#----- margins +top_margin = 20 +right_margin = 20 +bottom_margin = 20 +left_margin = 20 + +header_width = page_width - left_margin - right_margin +body_width = 400 + +body_left_margin = (page_width - body_width - left_margin - right_margin) / 2 + +prawn_document(margin: [top_margin, right_margin, bottom_margin, left_margin], page_size: page_size) do |pdf| + pdf.font_families.update('marianne' => { + normal: Rails.root.join('lib/prawn/fonts/marianne/marianne-regular.ttf'), + bold: Rails.root.join('lib/prawn/fonts/marianne/marianne-bold.ttf') + }) + pdf.font 'marianne' + + grey = '555555' + black = '333333' + + pdf.float do + pdf.svg IO.read(DOSSIER_DEPOSIT_RECEIPT_LOGO_SRC), height: 64 + end + + pdf.bounding_box([110, pdf.cursor - 18], width: header_width - 200) do + pdf.fill_color black + pdf.text APPLICATION_NAME, size: 20, style: :bold + + pdf.fill_color grey + pdf.text t('.receipt'), size: 14 + end + + pdf.bounding_box([body_left_margin, pdf.cursor - 20], width: body_width) do + pdf.fill_color black + pdf.pad_top(40) { pdf.text @dossier.procedure.libelle, size: 14, character_spacing: -0.2, align: :center } + + pdf.fill_color grey + description = t('.description', user_name: papertrail_requester_identity(@dossier), procedure: @dossier.procedure.libelle, date: l(@dossier.created_at, format: '%e %B %Y')) + pdf.pad_top(30) { pdf.text description, size: 10, character_spacing: -0.2, align: :left } + + pdf.fill_color black + pdf.pad_top(30) { pdf.text t('views.shared.dossiers.demande.requester_identity'), size: 14, character_spacing: -0.2, align: :justify } + + if @dossier.individual.present? + pdf.pad_top(7) do + pdf.fill_color grey + pdf.text "#{Individual.human_attribute_name(:prenom)} : #{@dossier.individual.prenom}", size: 10, character_spacing: -0.2, align: :justify + pdf.text "#{Individual.human_attribute_name(:nom)} : #{@dossier.individual.nom.upcase}", size: 10, character_spacing: -0.2, align: :justify + end + end + + if @dossier.etablissement.present? + pdf.pad_top(7) do + pdf.fill_color grey + pdf.text "Dénomination : " + raison_sociale_or_name(@dossier.etablissement), size: 10, character_spacing: -0.2, align: :justify + pdf.text "SIRET : " + @dossier.etablissement.siret, size: 10, character_spacing: -0.2, align: :justify + end + end + + pdf.fill_color black + pdf.pad_top(30) { pdf.text Dossier.model_name.human, size: 14, character_spacing: -0.2, align: :justify } + + pdf.fill_color grey + pdf.pad_top(7) do + pdf.text "#{Dossier.human_attribute_name(:id)} : #{@dossier.id.to_s}", size: 10, character_spacing: -0.2, align: :justify + pdf.text t('.file_submitted_at') + ' : ' + l(@dossier.en_construction_at, format: '%e %B %Y'), size: 10, character_spacing: -0.2, align: :justify + pdf.text t('.dossier_state') + ' : ' + papertrail_dossier_state(@dossier), size: 10, character_spacing: -0.2, align: :justify + end + + service = @dossier.procedure.service + if service.present? + pdf.fill_color black + pdf.pad_top(30) { pdf.text t('.administrative_service'), size: 14, character_spacing: -0.2, align: :justify } + + pdf.fill_color grey + pdf.pad_top(7) do + pdf.text "#{Service.model_name.human} : " + [service.nom, service.organisme].join(", "), size: 10, character_spacing: -0.2, align: :justify + pdf.text "#{Service.human_attribute_name(:adresse)} : #{service.adresse}", size: 10, character_spacing: -0.2, align: :justify + pdf.text "#{Service.human_attribute_name(:email)} : #{service.email}", size: 10, character_spacing: -0.2, align: :justify + if service.telephone.present? + pdf.text "#{Service.human_attribute_name(:telephone)} : #{service.telephone}", size: 10, character_spacing: -0.2, align: :justify + end + end + end + + pdf.fill_color black + pdf.pad_top(100) do + pdf.text t('.generated_at', date: l(Time.zone.now.to_date, format: :long)), size: 10, character_spacing: -0.2, align: :right + pdf.text t('.signature', app_name: APPLICATION_NAME), size: 10, character_spacing: -0.2, align: :right + end + end +end diff --git a/app/views/users/dossiers/show.html.haml b/app/views/users/dossiers/show.html.haml index 2ec8ab29d..deeb7cd3b 100644 --- a/app/views/users/dossiers/show.html.haml +++ b/app/views/users/dossiers/show.html.haml @@ -9,5 +9,8 @@ .container = render partial: 'users/dossiers/show/status_overview', locals: { dossier: @dossier } + - if @dossier.procedure.feature_enabled?(:procedure_dossier_papertrail) + = render partial: 'users/dossiers/show/papertrail', locals: { dossier: @dossier } + - if !@dossier.termine? = render partial: 'users/dossiers/show/latest_message', locals: { dossier: @dossier } diff --git a/app/views/users/dossiers/show/_papertrail.html.haml b/app/views/users/dossiers/show/_papertrail.html.haml new file mode 100644 index 000000000..9146f6e89 --- /dev/null +++ b/app/views/users/dossiers/show/_papertrail.html.haml @@ -0,0 +1,4 @@ +.papertrail.mb-2 + = link_to papertrail_dossier_url(dossier, format: :pdf), class: "button", download: t('.filename'), target: "_blank" do + %span.icon.justificatif + = t('.get_papertrail') diff --git a/config/env.example.optional b/config/env.example.optional index e19e0d6b5..2504fe14f 100644 --- a/config/env.example.optional +++ b/config/env.example.optional @@ -68,6 +68,9 @@ DS_ENV="staging" # Instance customization: Procedure default logo ---> to be put in "app/assets/images" # PROCEDURE_DEFAULT_LOGO_SRC="republique-francaise-logo.svg" +# Instance customization: Deposit receipt logo ---> to be put in "app/assets/images" +# DOSSIER_DEPOSIT_RECEIPT_LOGO_SRC="app/assets/images/republique-francaise-logo.svg" + # Instance customization: PDF export logo ---> to be put in "app/assets/images" # DOSSIER_PDF_EXPORT_LOGO_SRC="app/assets/images/header/logo-ds-wide.svg" diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb index f335c7ce3..624931613 100644 --- a/config/initializers/flipper.rb +++ b/config/initializers/flipper.rb @@ -32,7 +32,8 @@ features = [ :hide_instructeur_email, :procedure_revisions, :procedure_routage_api, - :procedure_process_expired_dossiers_termine + :procedure_process_expired_dossiers_termine, + :procedure_dossier_papertrail ] def database_exists? diff --git a/config/initializers/images.rb b/config/initializers/images.rb index c488aa279..bfa9e34f3 100644 --- a/config/initializers/images.rb +++ b/config/initializers/images.rb @@ -16,5 +16,8 @@ MAILER_FOOTER_LOGO_SRC = ENV.fetch("MAILER_FOOTER_LOGO_SRC", "mailer/instructeur # Default logo of a procedure PROCEDURE_DEFAULT_LOGO_SRC = ENV.fetch("PROCEDURE_DEFAULT_LOGO_SRC", "republique-francaise-logo.svg") +# Deposit receipt logo +DOSSIER_DEPOSIT_RECEIPT_LOGO_SRC = ENV.fetch("DOSSIER_DEPOSIT_RECEIPT_LOGO_SRC", "app/assets/images/republique-francaise-logo.svg") + # Logo in PDF export of a "Dossier" DOSSIER_PDF_EXPORT_LOGO_SRC = ENV.fetch("DOSSIER_PDF_EXPORT_LOGO_SRC", "app/assets/images/header/logo-ds-wide.svg") diff --git a/config/locales/en.yml b/config/locales/en.yml index ee9040130..f6c355c28 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -453,6 +453,24 @@ en: identity_saved: "Identity data is registred" attestation: no_longer_available: "The certificate is no longer available on this file." + show: + papertrail: + get_papertrail: "Get a deposit receipt" + filename: "deposit-receipt.pdf" + papertrail: + receipt: "Deposit receipt" + description: "This document attests that on the %{date}, %{user_name} submitted a file on the procedure “%{procedure}”." + file_submitted_at: "File submission date" + dossier_state: "File status" + states: + en_construction: "submitted, pending processing" + en_instruction: "processing" + accepte: "accepted" + refuse: "declined" + sans suite: "closed, no further action" + administrative_service: "Administrative department" + generated_at: "Made on %{date}," + signature: "%{app_name}" instructeurs: dossiers: deleted_by_instructeur: "The folder has been deleted" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 177b44dd4..d01aa509c 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -461,6 +461,24 @@ fr: identity_saved: "Identité enregistrée" attestation: no_longer_available: "L’attestation n'est plus disponible sur ce dossier." + papertrail: + receipt: "Accusé de dépôt" + description: "Ce document atteste que %{user_name} a déposé le %{date} un dossier sur la démarche « %{procedure} »." + file_submitted_at: "Dossier déposé le" + dossier_state: "État du dossier" + states: + en_construction: "déposé, en attente d’examen par l’administration" + en_instruction: "en cours d’instruction par l’administration" + accepte: "accepté" + refuse: "refusé" + sans suite: "classé sans suite" + administrative_service: "Service administratif" + generated_at: "Fait le %{date}," + signature: "La direction de %{app_name}" + show: + papertrail: + get_papertrail: "Obtenir une attestation de dépôt de dossier" + filename: "attestation-de-depot.pdf" instructeurs: dossiers: deleted_by_instructeur: "Le dossier a bien été supprimé de votre interface" diff --git a/config/locales/models/dossier/en.yml b/config/locales/models/dossier/en.yml index c9bb7d046..19fe8ff63 100644 --- a/config/locales/models/dossier/en.yml +++ b/config/locales/models/dossier/en.yml @@ -6,6 +6,7 @@ en: other: "Files" attributes: dossier: + id: "File number" state: "State" dossier/state: &state brouillon: "Draft" diff --git a/config/locales/models/dossier/fr.yml b/config/locales/models/dossier/fr.yml index 1fb503dff..7f9f119c7 100644 --- a/config/locales/models/dossier/fr.yml +++ b/config/locales/models/dossier/fr.yml @@ -6,6 +6,7 @@ fr: other: "Dossiers" attributes: dossier: + id: "Numéro de dossier" montant_projet: 'Le montant du projet' montant_aide_demande: "Le montant d’aide demandée" date_previsionnelle: "La date de début prévisionnelle" diff --git a/config/locales/models/service/en.yml b/config/locales/models/service/en.yml new file mode 100644 index 000000000..5d3012f12 --- /dev/null +++ b/config/locales/models/service/en.yml @@ -0,0 +1,11 @@ +en: + activerecord: + models: + service: + one: 'Service' + other: 'Services' + attributes: + service: + adresse: 'Mail address' + email: 'Email' + telephone: 'Phone' diff --git a/config/locales/models/service/fr.yml b/config/locales/models/service/fr.yml index c9792d965..c1e7fed72 100644 --- a/config/locales/models/service/fr.yml +++ b/config/locales/models/service/fr.yml @@ -1,4 +1,14 @@ fr: + activerecord: + models: + service: + one: 'Service' + other: 'Services' + attributes: + service: + adresse: 'Adresse postale' + email: 'Email de contact' + telephone: 'Téléphone' type_organisme: administration_centrale: 'Administration centrale' association: 'Association' diff --git a/config/routes.rb b/config/routes.rb index 34400251f..24d75f346 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -273,6 +273,7 @@ Rails.application.routes.draw do patch 'restore', to: 'dossiers#restore' get 'attestation' get 'transferer', to: 'dossiers#transferer' + get 'papertrail', format: :pdf end collection do diff --git a/db/migrate/20220406144202_remove_column_instructeur_id_from_dossier_operation_log.rb b/db/migrate/20220406144202_remove_column_instructeur_id_from_dossier_operation_log.rb new file mode 100644 index 000000000..5cb117776 --- /dev/null +++ b/db/migrate/20220406144202_remove_column_instructeur_id_from_dossier_operation_log.rb @@ -0,0 +1,5 @@ +class RemoveColumnInstructeurIdFromDossierOperationLog < ActiveRecord::Migration[6.1] + def change + safety_assured { remove_column :dossier_operation_logs, :instructeur_id } + end +end diff --git a/db/schema.rb b/db/schema.rb index f28167ade..b8a86f85d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -267,13 +267,11 @@ ActiveRecord::Schema.define(version: 2022_04_26_140107) do t.text "digest" t.bigint "dossier_id" t.datetime "executed_at" - t.bigint "instructeur_id" t.datetime "keep_until" t.string "operation", null: false t.datetime "updated_at", null: false t.index ["bill_signature_id"], name: "index_dossier_operation_logs_on_bill_signature_id" t.index ["dossier_id"], name: "index_dossier_operation_logs_on_dossier_id" - t.index ["instructeur_id"], name: "index_dossier_operation_logs_on_instructeur_id" t.index ["keep_until"], name: "index_dossier_operation_logs_on_keep_until" end @@ -874,7 +872,6 @@ ActiveRecord::Schema.define(version: 2022_04_26_140107) do add_foreign_key "commentaires", "dossiers" add_foreign_key "commentaires", "experts" add_foreign_key "dossier_operation_logs", "bill_signatures" - add_foreign_key "dossier_operation_logs", "instructeurs" add_foreign_key "dossier_transfer_logs", "dossiers" add_foreign_key "dossiers", "dossier_transfers" add_foreign_key "dossiers", "groupe_instructeurs" diff --git a/spec/controllers/champs/carte_controller_spec.rb b/spec/controllers/champs/carte_controller_spec.rb index 621f8f4f8..99bf6a74e 100644 --- a/spec/controllers/champs/carte_controller_spec.rb +++ b/spec/controllers/champs/carte_controller_spec.rb @@ -117,7 +117,7 @@ describe Champs::CarteController, type: :controller do render_views before do - get :index, params: params, format: :js, xhr: true + get :index, params: params, format: :turbo_stream end context 'without focus' do @@ -126,7 +126,7 @@ describe Champs::CarteController, type: :controller do end it 'updates the list' do - expect(response.body).not_to include("DS.fire('map:feature:focus'") + expect(response.body).not_to include("map:feature:focus") expect(response.status).to eq 200 end end @@ -140,7 +140,8 @@ describe Champs::CarteController, type: :controller do end it 'updates the list and focuses the map' do - expect(response.body).to include("DS.fire('map:feature:focus'") + expect(response.body).to include(ActionView::RecordIdentifier.dom_id(champ, :geo_areas)) + expect(response.body).to include("map:feature:focus") expect(response.status).to eq 200 end end diff --git a/spec/controllers/champs/dossier_link_controller_spec.rb b/spec/controllers/champs/dossier_link_controller_spec.rb index 319fad897..59a0fc29f 100644 --- a/spec/controllers/champs/dossier_link_controller_spec.rb +++ b/spec/controllers/champs/dossier_link_controller_spec.rb @@ -27,33 +27,33 @@ describe Champs::DossierLinkController, type: :controller do context 'when the dossier exist' do before do - get :show, params: params, format: :js, xhr: true + get :show, params: params, format: :turbo_stream end it 'renders the procedure name' do expect(response.body).to include('Dossier en brouillon') expect(response.body).to include(procedure.libelle) expect(response.body).to include(procedure.organisation) - expect(response.body).to include("##{champ.input_group_id} .help-block") + expect(response.body).to include(ActionView::RecordIdentifier.dom_id(champ, :help_block)) end end context 'when the dossier does not exist' do let(:dossier_id) { '13' } before do - get :show, params: params, format: :js, xhr: true + get :show, params: params, format: :turbo_stream end it 'renders error message' do expect(response.body).to include('Ce dossier est inconnu') - expect(response.body).to include("##{champ.input_group_id} .help-block") + expect(response.body).to include(ActionView::RecordIdentifier.dom_id(champ, :help_block)) end end end context 'when user is not connected' do before do - get :show, params: { champ_id: champ.id }, format: :js, xhr: true + get :show, params: { champ_id: champ.id }, format: :turbo_stream end it { expect(response.code).to eq('401') } diff --git a/spec/controllers/champs/siret_controller_spec.rb b/spec/controllers/champs/siret_controller_spec.rb index 820d4703d..f7ae6a832 100644 --- a/spec/controllers/champs/siret_controller_spec.rb +++ b/spec/controllers/champs/siret_controller_spec.rb @@ -37,7 +37,7 @@ describe Champs::SiretController, type: :controller do end context 'when the SIRET is empty' do - subject! { get :show, params: params, format: :js, xhr: true } + subject! { get :show, params: params, format: :turbo_stream } it 'clears the etablissement and SIRET on the model' do champ.reload @@ -46,15 +46,14 @@ describe Champs::SiretController, type: :controller do end it 'clears any information or error message' do - expect(response.body).to include("##{champ.input_group_id} .siret-info") - expect(response.body).to include('innerHTML = ""') + expect(response.body).to include(ActionView::RecordIdentifier.dom_id(champ, :siret_info)) end end context 'when the SIRET is invalid' do let(:siret) { '1234' } - subject! { get :show, params: params, format: :js, xhr: true } + subject! { get :show, params: params, format: :turbo_stream } it 'clears the etablissement and SIRET on the model' do champ.reload @@ -71,7 +70,7 @@ describe Champs::SiretController, type: :controller do let(:siret) { '82161143100015' } let(:api_etablissement_status) { 503 } - subject! { get :show, params: params, format: :js, xhr: true } + subject! { get :show, params: params, format: :turbo_stream } it 'clears the etablissement and SIRET on the model' do champ.reload @@ -88,7 +87,7 @@ describe Champs::SiretController, type: :controller do let(:siret) { '00000000000000' } let(:api_etablissement_status) { 404 } - subject! { get :show, params: params, format: :js, xhr: true } + subject! { get :show, params: params, format: :turbo_stream } it 'clears the etablissement and SIRET on the model' do champ.reload @@ -106,7 +105,7 @@ describe Champs::SiretController, type: :controller do let(:api_etablissement_status) { 200 } let(:api_etablissement_body) { File.read('spec/fixtures/files/api_entreprise/etablissements.json') } - subject! { get :show, params: params, format: :js, xhr: true } + subject! { get :show, params: params, format: :turbo_stream } it 'populates the etablissement and SIRET on the model' do champ.reload @@ -119,7 +118,7 @@ describe Champs::SiretController, type: :controller do end context 'when user is not signed in' do - subject! { get :show, params: { champ_id: champ.id }, format: :js, xhr: true } + subject! { get :show, params: { champ_id: champ.id }, format: :turbo_stream } it { expect(response.code).to eq('401') } end diff --git a/spec/controllers/password_complexity_controller_spec.rb b/spec/controllers/password_complexity_controller_spec.rb index 7c60de3fc..b04985806 100644 --- a/spec/controllers/password_complexity_controller_spec.rb +++ b/spec/controllers/password_complexity_controller_spec.rb @@ -4,7 +4,7 @@ describe PasswordComplexityController, type: :controller do { user: { password: 'moderately complex password' } } end - subject { get :show, format: :js, params: params, xhr: true } + subject { get :show, format: :turbo_stream, params: params } it 'computes a password score' do subject @@ -27,8 +27,8 @@ describe PasswordComplexityController, type: :controller do it 'renders Javascript that updates the password complexity meter' do subject - expect(response.body).to include('#complexity-label') - expect(response.body).to include('#complexity-bar') + expect(response.body).to include('complexity-label') + expect(response.body).to include('complexity-bar') end end end diff --git a/spec/controllers/users/dossiers_controller_spec.rb b/spec/controllers/users/dossiers_controller_spec.rb index f8a1faf9e..8eb4013d3 100644 --- a/spec/controllers/users/dossiers_controller_spec.rb +++ b/spec/controllers/users/dossiers_controller_spec.rb @@ -1011,6 +1011,31 @@ describe Users::DossiersController, type: :controller do end end + describe "#papertrail" do + before { sign_in(user) } + + subject do + get :papertrail, format: :pdf, params: { id: dossier.id } + end + + context 'when the dossier has been submitted' do + let(:dossier) { create(:dossier, :en_construction, user: user) } + + it 'renders a PDF document' do + subject + expect(response).to render_template(:papertrail) + end + end + + context 'when the dossier is still a draft' do + let(:dossier) { create(:dossier, :brouillon, user: user) } + + it 'raises an error' do + expect { subject }.to raise_error(ActionController::BadRequest) + end + end + end + describe '#delete_dossier' do before { sign_in(user) } diff --git a/spec/graphql/dossier_spec.rb b/spec/graphql/dossier_spec.rb index 6aba97515..4f7a5dfa8 100644 --- a/spec/graphql/dossier_spec.rb +++ b/spec/graphql/dossier_spec.rb @@ -56,6 +56,31 @@ RSpec.describe Types::DossierType, type: :graphql do it { expect(data[:dossier][:champs][1][:__typename]).to eq "AddressChamp" } end + describe 'dossier with user' do + let(:dossier) { create(:dossier, :en_construction) } + let(:query) { DOSSIER_WITH_USAGER_QUERY } + let(:variables) { { number: dossier.id } } + + it { expect(data[:dossier][:usager]).not_to be_nil } + end + + describe 'dossier with deleted user' do + let(:dossier) { create(:dossier, :en_construction) } + let(:query) { DOSSIER_WITH_USAGER_QUERY } + let(:variables) { { number: dossier.id } } + let(:email) { dossier.user.email } + + before do + dossier.update(user_id: nil, deleted_user_email_never_send: email) + end + + it { + expect(data[:dossier][:usager]).not_to be_nil + expect(data[:dossier][:usager][:email]).to eq(email) + expect(data[:dossier][:usager][:id]).to eq('') + } + end + DOSSIER_QUERY = <<-GRAPHQL query($number: Int!) { dossier(number: $number) { @@ -65,6 +90,19 @@ RSpec.describe Types::DossierType, type: :graphql do } GRAPHQL + DOSSIER_WITH_USAGER_QUERY = <<-GRAPHQL + query($number: Int!) { + dossier(number: $number) { + id + number + usager { + id + email + } + } + } + GRAPHQL + DOSSIER_WITH_ATTESTATION_QUERY = <<-GRAPHQL query($number: Int!) { dossier(number: $number) { diff --git a/spec/models/champ_spec.rb b/spec/models/champ_spec.rb index b256a04d9..3391ec4f0 100644 --- a/spec/models/champ_spec.rb +++ b/spec/models/champ_spec.rb @@ -602,5 +602,25 @@ describe Champ do expect(champ.reload.data).to eq data end end + + context "#input_name" do + let(:champ) { create(:champ_text) } + it { expect(champ.input_name).to eq "dossier[champs_attributes][#{champ.id}]" } + + context "when private" do + let(:champ) { create(:champ_text, private: true) } + it { expect(champ.input_name).to eq "dossier[champs_private_attributes][#{champ.id}]" } + end + + context "when has parent" do + let(:champ) { create(:champ_text, parent: create(:champ_text)) } + it { expect(champ.input_name).to eq "dossier[champs_attributes][#{champ.parent_id}][champs_attributes][#{champ.id}]" } + end + + context "when has private parent" do + let(:champ) { create(:champ_text, private: true, parent: create(:champ_text, private: true)) } + it { expect(champ.input_name).to eq "dossier[champs_private_attributes][#{champ.parent_id}][champs_attributes][#{champ.id}]" } + end + end end end diff --git a/spec/system/users/dossier_details_spec.rb b/spec/system/users/dossier_details_spec.rb index b2346120d..135309ccd 100644 --- a/spec/system/users/dossier_details_spec.rb +++ b/spec/system/users/dossier_details_spec.rb @@ -16,6 +16,16 @@ describe 'Dossier details:' do expect(page).to have_text(dossier.commentaires.last.body) end + context 'when the deposit receipt feature is enabled' do + before { Flipper.enable(:procedure_dossier_papertrail, procedure) } + after { Flipper.disable(:procedure_dossier_papertrail, procedure) } + + it 'displays a link to download a deposit receipt' do + visit dossier_path(dossier) + expect(page).to have_link("Obtenir une attestation de dépôt de dossier", href: %r{dossiers/#{dossier.id}/papertrail.pdf}) + end + end + describe "the user can see the mean time they are expected to wait" do let(:other_dossier) { create(:dossier, :accepte, :with_individual, procedure: procedure, depose_at: 10.days.ago, en_instruction_at: 9.days.ago, processed_at: Time.zone.now) } diff --git a/spec/views/users/dossiers/papertrail.pdf.prawl_spec.rb b/spec/views/users/dossiers/papertrail.pdf.prawl_spec.rb new file mode 100644 index 000000000..c0b5483a5 --- /dev/null +++ b/spec/views/users/dossiers/papertrail.pdf.prawl_spec.rb @@ -0,0 +1,25 @@ +describe 'users/dossiers/papertrail.pdf.prawn', type: :view do + before do + assign(:dossier, dossier) + end + + subject { render } + + context 'for a dossier with an individual' do + let(:dossier) { create(:dossier, :en_construction, :with_service, :with_individual) } + + it 'renders a PDF document with the dossier state' do + subject + expect(rendered).to be_present + end + end + + context 'for a dossier with a SIRET' do + let(:dossier) { create(:dossier, :en_construction, :with_service, :with_entreprise) } + + it 'renders a PDF document with the dossier state' do + subject + expect(rendered).to be_present + end + end +end