diff --git a/app/controllers/champs/carte_controller.rb b/app/controllers/champs/carte_controller.rb index 9a22d821a..84f5de313 100644 --- a/app/controllers/champs/carte_controller.rb +++ b/app/controllers/champs/carte_controller.rb @@ -40,6 +40,54 @@ class Champs::CarteController < ApplicationController response.status = 503 end + def index + @selector = ".carte-#{params[:champ_id]}" + @champ = policy_scope(Champ).find(params[:champ_id]) + @update_cadastres = params[:cadastres] + + if @champ.cadastres? && @update_cadastres + @champ.geo_areas.cadastres.destroy_all + @champ.geo_areas += GeoArea.from_feature_collection(cadastres_features_collection(@champ.to_feature_collection)) + @champ.save! + end + rescue ApiCarto::API::ResourceNotFound + flash.alert = 'Les données cartographiques sont temporairement indisponibles. Réessayez dans un instant.' + response.status = 503 + end + + def create + champ = policy_scope(Champ).find(params[:champ_id]) + geo_area = champ.geo_areas.selections_utilisateur.new + save_geometry!(geo_area, params_feature) + + render json: { feature: geo_area.to_feature }, status: :created + end + + def update + champ = policy_scope(Champ).find(params[:champ_id]) + geo_area = champ.geo_areas.selections_utilisateur.find(params[:id]) + save_geometry!(geo_area, params_feature) + + head :no_content + end + + def destroy + champ = policy_scope(Champ).find(params[:champ_id]) + champ.geo_areas.selections_utilisateur.find(params[:id]).destroy! + + head :no_content + end + + def import + champ = policy_scope(Champ).find(params[:champ_id]) + params_features.each do |feature| + geo_area = champ.geo_areas.selections_utilisateur.new + save_geometry!(geo_area, feature) + end + + render json: champ.to_feature_collection, status: :created + end + private def populate_cadastres(feature_collection) @@ -61,4 +109,45 @@ class Champs::CarteController < ApplicationController end end end + + def params_feature + params[:feature] + end + + def params_features + params[:features] + end + + def save_geometry!(geo_area, feature) + geo_area.geometry = feature[:geometry] + geo_area.save! + end + + def cadastres_features_collection(feature_collection) + coordinates = feature_collection[:features].filter do |feature| + feature[:properties][:source] == GeoArea.sources.fetch(:selection_utilisateur) && feature[:geometry]['type'] == 'Polygon' + end.map do |feature| + feature[:geometry]['coordinates'][0].map { |(lng, lat)| { 'lng' => lng, 'lat' => lat } } + end + + if coordinates.present? + cadastres = ApiCartoService.generate_cadastre(coordinates) + + { + type: 'FeatureCollection', + features: cadastres.map do |cadastre| + { + type: 'Feature', + geometry: cadastre.delete(:geometry), + properties: cadastre.merge(source: GeoArea.sources.fetch(:cadastre)) + } + end + } + else + { + type: 'FeatureCollection', + features: [] + } + end + end end diff --git a/app/controllers/instructeurs/avis_controller.rb b/app/controllers/instructeurs/avis_controller.rb index 5e2e0c8a8..d213beac7 100644 --- a/app/controllers/instructeurs/avis_controller.rb +++ b/app/controllers/instructeurs/avis_controller.rb @@ -72,6 +72,14 @@ module Instructeurs end end + def bilans_bdf + if avis.dossier.etablissement&.entreprise_bilans_bdf_to_csv.present? + render csv: avis.dossier.etablissement.entreprise_bilans_bdf_to_csv + else + redirect_to instructeur_avis_path(avis) + end + end + def sign_up @email = params[:email] @dossier = Avis.includes(:dossier).find(params[:id]).dossier diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index c1dea88f4..6106c8107 100644 --- a/app/graphql/schema.graphql +++ b/app/graphql/schema.graphql @@ -692,6 +692,8 @@ type Effectif { } type Entreprise { + attestationFiscaleAttachment: File + attestationSocialeAttachment: File capitalSocial: BigInt! codeEffectifEntreprise: String! dateCreation: ISO8601Date! diff --git a/app/graphql/types/personne_morale_type.rb b/app/graphql/types/personne_morale_type.rb index 627145cb0..769554ce6 100644 --- a/app/graphql/types/personne_morale_type.rb +++ b/app/graphql/types/personne_morale_type.rb @@ -21,6 +21,16 @@ module Types field :nom, String, null: false field :prenom, String, null: false field :inline_adresse, String, null: false + field :attestation_sociale_attachment, Types::File, null: true + field :attestation_fiscale_attachment, Types::File, null: true + + def attestation_sociale_attachment + load_attachment_for(:entreprise_attestation_sociale_attachment) + end + + def attestation_fiscale_attachment + load_attachment_for(:entreprise_attestation_fiscale_attachment) + end def effectif_mensuel if object.effectif_mensuel.present? @@ -39,6 +49,15 @@ module Types } end end + + private + + def load_attachment_for(key) + Loaders::Association.for( + Etablissement, + key => :blob + ).load(object.etablissement) + end end class AssociationType < Types::BaseObject diff --git a/app/javascript/components/MapEditor/MapEditor.js b/app/javascript/components/MapEditor/MapEditor.js index 6a219fa62..904cb8c05 100644 --- a/app/javascript/components/MapEditor/MapEditor.js +++ b/app/javascript/components/MapEditor/MapEditor.js @@ -3,116 +3,122 @@ import PropTypes from 'prop-types'; import mapboxgl from 'mapbox-gl'; import ReactMapboxGl, { GeoJSONLayer, ZoomControl } from 'react-mapbox-gl'; import DrawControl from 'react-mapbox-gl-draw'; -import area from '@turf/area'; import SwitchMapStyle from './SwitchMapStyle'; import SearchInput from './SearchInput'; -import { fire } from '@utils'; +import { getJSON, ajax } from '@utils'; +import { gpx } from '@tmcw/togeojson/dist/togeojson.es.js'; import ortho from './styles/ortho.json'; import vector from './styles/vector.json'; -import { - createFeatureCollection, - polygonCadastresFill, - polygonCadastresLine, - ERROR_GEO_JSON -} from './utils'; +import { polygonCadastresFill, polygonCadastresLine } from './utils'; import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'; const Map = ReactMapboxGl({}); -const MapEditor = ({ featureCollection: { features, bbox, id } }) => { +function filterFeatureCollection(featureCollection, source) { + return { + type: 'FeatureCollection', + features: featureCollection.features.filter( + feature => feature.properties.source === source + ) + }; +} + +const MapEditor = ({ featureCollection, url }) => { const drawControl = useRef(null); const [style, setStyle] = useState('ortho'); const [coords, setCoords] = useState([1.7, 46.9]); const [zoom, setZoom] = useState([5]); const [currentMap, setCurrentMap] = useState({}); - let input = document.querySelector( - `input[data-feature-collection-id="${id}"]` - ); - - let userSelections = features.filter( - feature => feature.properties.source === 'selection_utilisateur' - ); - - let cadastresFeatureCollection = { - type: 'FeatureCollection', - features: [] - }; - - const constructCadastresFeatureCollection = features => { - for (let feature of features) { - switch (feature.properties.source) { - case 'cadastre': - cadastresFeatureCollection.features.push(feature); - break; - } - } - }; - constructCadastresFeatureCollection(features); - + const [bbox, setBbox] = useState(featureCollection.bbox); const mapStyle = style === 'ortho' ? ortho : vector; + const cadastresFeatureCollection = filterFeatureCollection( + featureCollection, + 'cadastre' + ); - const saveFeatureCollection = featuresToSave => { - const featuresCollection = createFeatureCollection(featuresToSave); - if (area(featuresCollection) < 300000) { - input.value = JSON.stringify(featuresCollection); - } else { - input.value = ERROR_GEO_JSON; - } - fire(input, 'change'); - }; - - const onDrawCreate = ({ features }) => { - const draw = drawControl.current.draw; - const featureId = features[0].id; - draw.setFeatureProperty(featureId, 'id', featureId); - draw.setFeatureProperty(featureId, 'source', 'selection_utilisateur'); - userSelections.push(draw.get(featureId)); - saveFeatureCollection(userSelections); - }; - - const onDrawUpdate = ({ features }) => { - let featureId = features[0].properties.id; - userSelections = userSelections.map(selection => { - if (selection.properties.id === featureId) { - selection = features[0]; - } - return selection; - }); - saveFeatureCollection(userSelections); - }; - - const onDrawDelete = ({ features }) => { - userSelections = userSelections.filter( - selection => selection.properties.id !== features[0].properties.id + function updateFeaturesList(features) { + const cadastres = features.find( + ({ geometry }) => geometry.type === 'Polygon' ); - saveFeatureCollection(userSelections); - }; + ajax({ url, type: 'get', data: cadastres ? 'cadastres=update' : '' }); + } + + function setFeatureId(lid, feature) { + const draw = drawControl.current.draw; + draw.setFeatureProperty(lid, 'id', feature.properties.id); + } + + async function onDrawCreate({ features }) { + for (const feature of features) { + const data = await getJSON(url, { feature }, 'post'); + setFeatureId(feature.id, data.feature); + } + + updateFeaturesList(features); + } + + async function onDrawUpdate({ features }) { + for (const feature of features) { + let { id } = feature.properties; + await getJSON(`${url}/${id}`, { feature }, 'patch'); + } + + updateFeaturesList(features); + } + + async function onDrawDelete({ features }) { + for (const feature of features) { + const { id } = feature.properties; + await getJSON(`${url}/${id}`, null, 'delete'); + } + + updateFeaturesList(features); + } const onMapLoad = map => { setCurrentMap(map); - if (userSelections.length > 0) { - userSelections.map((selection, index) => { - selection.properties.id = index + 1; - drawControl.current.draw.add(selection); - }); + + drawControl.current.draw.set( + filterFeatureCollection(featureCollection, 'selection_utilisateur') + ); + }; + + const onCadastresUpdate = evt => { + if (currentMap) { + currentMap + .getSource('cadastres-layer') + .setData( + filterFeatureCollection(evt.detail.featureCollection, 'cadastre') + ); } }; - const onMapUpdate = evt => { - if (currentMap) { - cadastresFeatureCollection.features = []; - constructCadastresFeatureCollection( - evt.detail.featureCollection.features + const onGpxImport = e => { + let reader = new FileReader(); + reader.readAsText(e.target.files[0], 'UTF-8'); + reader.onload = async event => { + const featureCollection = gpx( + new DOMParser().parseFromString(event.target.result, 'text/xml') ); - currentMap - .getSource('cadastres-layer') - .setData(cadastresFeatureCollection); - } + const resultFeatureCollection = await getJSON( + `${url}/import`, + featureCollection, + 'post' + ); + drawControl.current.draw.set( + filterFeatureCollection( + resultFeatureCollection, + 'selection_utilisateur' + ) + ); + updateFeaturesList(resultFeatureCollection.features); + setBbox(resultFeatureCollection.bbox); + }; }; useEffect(() => { - addEventListener('map:update', onMapUpdate); - return () => removeEventListener('map:update', onMapUpdate); + addEventListener('cadastres:update', onCadastresUpdate); + return () => removeEventListener('cadastres:update', onCadastresUpdate); }); if (!mapboxgl.supported()) { @@ -127,6 +133,16 @@ const MapEditor = ({ featureCollection: { features, bbox, id } }) => { return ( <> +
+
+

+ Importer un fichier GPX +

+
+
+ +
+
{ return ( { - return { - type: 'FeatureCollection', - features: selectionsUtilisateur - }; -}; - export const polygonCadastresFill = { 'fill-color': '#EC3323', 'fill-opacity': 0.3 diff --git a/app/javascript/shared/utils.js b/app/javascript/shared/utils.js index d360af45f..ac1d3246f 100644 --- a/app/javascript/shared/utils.js +++ b/app/javascript/shared/utils.js @@ -68,7 +68,7 @@ export function ajax(options) { } export function getJSON(url, data, method = 'get') { - data = method !== 'get' ? JSON.stringify(data) : data; + data = method !== 'get' && data ? JSON.stringify(data) : data; return Promise.resolve( $.ajax({ method, diff --git a/app/models/entreprise.rb b/app/models/entreprise.rb index 5808b91e9..3b471cbaf 100644 --- a/app/models/entreprise.rb +++ b/app/models/entreprise.rb @@ -3,6 +3,7 @@ class Entreprise < Hashie::Dash self[attribute] end + property :etablissement property :siren property :capital_social property :numero_tva_intracommunautaire diff --git a/app/models/etablissement.rb b/app/models/etablissement.rb index 50585c6a2..8dce7b4fa 100644 --- a/app/models/etablissement.rb +++ b/app/models/etablissement.rb @@ -96,6 +96,7 @@ class Etablissement < ApplicationRecord def entreprise Entreprise.new( + etablissement: self, siren: entreprise_siren, capital_social: entreprise_capital_social, numero_tva_intracommunautaire: entreprise_numero_tva_intracommunautaire, diff --git a/app/views/champs/carte/index.js.erb b/app/views/champs/carte/index.js.erb new file mode 100644 index 000000000..43157bbf2 --- /dev/null +++ b/app/views/champs/carte/index.js.erb @@ -0,0 +1,9 @@ +<%= render_flash(timeout: 5000, fixed: true) %> + +<%= render_to_element("#{@selector} + .geo-areas", + partial: 'shared/champs/carte/geo_areas', + locals: { champ: @champ, error: @error }) %> + +<% if @update_cadastres %> + <%= fire_event('cadastres:update', { featureCollection: @champ.to_feature_collection }.to_json) %> +<% end %> diff --git a/app/views/champs/carte/show.js.erb b/app/views/champs/carte/show.js.erb index f36252efc..7b80491fe 100644 --- a/app/views/champs/carte/show.js.erb +++ b/app/views/champs/carte/show.js.erb @@ -4,8 +4,4 @@ partial: 'shared/champs/carte/geo_areas', locals: { champ: @champ, error: @error }) %> -<% if feature_enabled?(:new_map_editor) %> - <%= fire_event('map:update', { featureCollection: @champ.to_feature_collection }.to_json) %> -<% else %> - <%= fire_event('carte:update', { selector: @selector, data: @champ.to_render_data }.to_json) %> -<% end %> +<%= fire_event('carte:update', { selector: @selector, data: @champ.to_render_data }.to_json) %> diff --git a/app/views/dossiers/show.pdf.prawn b/app/views/dossiers/show.pdf.prawn index 29343bb26..50f9b7431 100644 --- a/app/views/dossiers/show.pdf.prawn +++ b/app/views/dossiers/show.pdf.prawn @@ -57,7 +57,11 @@ def render_identite_etablissement(pdf, etablissement) pdf.text " - Libellé NAF : #{etablissement.libelle_naf}" pdf.text " - Code NAF : #{etablissement.naf}" pdf.text " - Date de création : #{try_format_date(etablissement.entreprise.date_creation)}" - pdf.text " - Effectif de l'organisation : #{effectif(etablissement)}" + if @include_infos_administration + pdf.text " - Effectif mensuel #{try_format_mois_effectif(etablissement)} (URSSAF) : #{etablissement.entreprise_effectif_mensuel}" + pdf.text " - Effectif moyen annuel #{etablissement.entreprise_effectif_annuel_annee} (URSSAF) : #{etablissement.entreprise_effectif_annuel}" + end + pdf.text " - Effectif de l'organisation (INSEE) : #{effectif(etablissement)}" pdf.text " - Code effectif : #{etablissement.entreprise.code_effectif_entreprise}" pdf.text " - Numéro de TVA intracommunautaire : #{etablissement.entreprise.numero_tva_intracommunautaire}" pdf.text " - Adresse : #{etablissement.adresse}" diff --git a/app/views/shared/dossiers/_identite_entreprise.html.haml b/app/views/shared/dossiers/_identite_entreprise.html.haml index 714769597..b22f27335 100644 --- a/app/views/shared/dossiers/_identite_entreprise.html.haml +++ b/app/views/shared/dossiers/_identite_entreprise.html.haml @@ -83,7 +83,10 @@ %th.libelle Bilans Banque de France = "en #{etablissement.entreprise_bilans_bdf_monnaie}" - %td= link_to "Consulter les bilans", bilans_bdf_instructeur_dossier_path + - if controller.is_a?(Instructeurs::AvisController) + %td= link_to "Consulter les bilans", bilans_bdf_instructeur_avis_path(@avis.id) + - else + %td= link_to "Consulter les bilans", bilans_bdf_instructeur_dossier_path(procedure_id: @dossier.procedure.id, dossier_id: @dossier.id) - if etablissement.association? %tr diff --git a/app/views/shared/dossiers/editable_champs/_carte.html.haml b/app/views/shared/dossiers/editable_champs/_carte.html.haml index c7c058056..4464306f9 100644 --- a/app/views/shared/dossiers/editable_champs/_carte.html.haml +++ b/app/views/shared/dossiers/editable_champs/_carte.html.haml @@ -1,13 +1,13 @@ - - if feature_enabled?(:new_map_editor) - = react_component("MapEditor", { featureCollection: champ.to_feature_collection }, class: "carte-#{form.index}") + = react_component("MapEditor", { featureCollection: champ.to_feature_collection, url: champs_carte_features_path(champ) }, class: "carte-#{champ.id}") - else .toolbar %button.button.primary.new-area Ajouter une zone %select.select2.adresse{ data: { address: true }, placeholder: 'Saisissez une adresse ou positionner la carte' } .carte.edit{ data: { geo: geo_data(champ) }, class: "carte-#{form.index}" } + + = form.hidden_field :value, + data: { remote: true, feature_collection_id: champ.stable_id, url: champs_carte_path(form.index), params: champ_carte_params(champ).to_query, method: 'post' } + .geo-areas = render partial: 'shared/champs/carte/geo_areas', locals: { champ: champ, error: false } - -= form.hidden_field :value, - data: { remote: true, feature_collection_id: champ.stable_id, url: champs_carte_path(form.index), params: champ_carte_params(champ).to_query, method: 'post' } diff --git a/config/routes.rb b/config/routes.rb index c84893b20..bdbcc31a3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -120,6 +120,13 @@ Rails.application.routes.draw do get ':position/siret', to: 'siret#show', as: :siret get ':position/dossier_link', to: 'dossier_link#show', as: :dossier_link post ':position/carte', to: 'carte#show', as: :carte + + get ':champ_id/carte/features', to: 'carte#index', as: :carte_features + post ':champ_id/carte/features', to: 'carte#create' + post ':champ_id/carte/features/import', to: 'carte#import' + patch ':champ_id/carte/features/:id', to: 'carte#update' + delete ':champ_id/carte/features/:id', to: 'carte#destroy' + post ':position/repetition', to: 'repetition#show', as: :repetition put 'piece_justificative/:champ_id', to: 'piece_justificative#update', as: :piece_justificative end @@ -347,6 +354,7 @@ Rails.application.routes.draw do get 'messagerie' post 'commentaire' => 'avis#create_commentaire' post 'avis' => 'avis#create_avis' + get 'bilans_bdf' get 'sign_up/email/:email' => 'avis#sign_up', constraints: { email: /.*/ }, as: 'sign_up' post 'sign_up/email/:email' => 'avis#create_instructeur', constraints: { email: /.*/ } diff --git a/package.json b/package.json index de8edb1a8..459e72faa 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@rails/webpacker": "4.2.2", "@reach/combobox": "^0.10.0", "@sentry/browser": "^5.11.2", + "@tmcw/togeojson": "^4.0.0", "@turf/area": "^6.0.1", "babel-plugin-macros": "^2.8.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", diff --git a/spec/controllers/champs/carte_controller_spec.rb b/spec/controllers/champs/carte_controller_spec.rb index 69493fd05..b257fa613 100644 --- a/spec/controllers/champs/carte_controller_spec.rb +++ b/spec/controllers/champs/carte_controller_spec.rb @@ -18,6 +18,126 @@ describe Champs::CarteController, type: :controller do cadastres: true }).champ.create(dossier: dossier) end + describe 'features' do + let(:feature) { attributes_for(:geo_area, :polygon) } + let(:geo_area) { create(:geo_area, :selection_utilisateur, :polygon, champ: champ) } + let(:params) do + { + champ_id: champ.id, + feature: feature + } + end + + before do + sign_in user + request.accept = "application/json" + request.content_type = "application/json" + end + + describe 'POST #create' do + before do + post :create, params: params + end + + it { expect(response.status).to eq 201 } + end + + describe 'PATCH #update' do + let(:params) do + { + champ_id: champ.id, + id: geo_area.id, + feature: feature + } + end + + before do + patch :update, params: params + end + + it { expect(response.status).to eq 204 } + end + + describe 'DELETE #destroy' do + let(:params) do + { + champ_id: champ.id, + id: geo_area.id + } + end + + before do + delete :destroy, params: params + end + + it { expect(response.status).to eq 204 } + end + + describe 'POST #import' do + render_views + + let(:params) do + { + champ_id: champ.id, + features: [feature] + + } + end + + before do + post :import, params: params + end + + it { + expect(response.status).to eq 201 + expect(response.body).to include("bbox") + } + end + + describe 'GET #index' do + render_views + + before do + request.accept = "application/javascript" + request.content_type = "application/javascript" + end + + context 'with cadastres update' do + let(:params) do + { + champ_id: champ.id, + cadastres: 'update' + } + end + + before do + get :index, params: params + end + + it { + expect(response.status).to eq 200 + expect(response.body).to include("DS.fire('cadastres:update'") + } + end + + context 'without cadastres update' do + let(:params) do + { + champ_id: champ.id + } + end + + before do + get :index, params: params + end + + it { + expect(response.status).to eq 200 + expect(response.body).not_to include("DS.fire('cadastres:update'") + } + end + end + end describe 'POST #show' do render_views diff --git a/spec/controllers/instructeurs/avis_controller_spec.rb b/spec/controllers/instructeurs/avis_controller_spec.rb index 52c1f5d5d..c4d4462cb 100644 --- a/spec/controllers/instructeurs/avis_controller_spec.rb +++ b/spec/controllers/instructeurs/avis_controller_spec.rb @@ -50,6 +50,12 @@ describe Instructeurs::AvisController, type: :controller do it { expect(assigns(:dossier)).to eq(dossier) } end + describe '#bilans_bdf' do + before { get :bilans_bdf, params: { id: avis_without_answer.id } } + + it { expect(response).to redirect_to(instructeur_avis_path(avis_without_answer)) } + end + describe '#update' do describe 'without attachment' do before do diff --git a/spec/services/dossier_search_service_spec.rb b/spec/services/dossier_search_service_spec.rb index 16d613acc..52f855c27 100644 --- a/spec/services/dossier_search_service_spec.rb +++ b/spec/services/dossier_search_service_spec.rb @@ -133,13 +133,13 @@ describe DossierSearchService do context 'when the user owns the dossier' do let(:terms) { dossier_0.id.to_s } - it { expect(subject.size).to eq(1) } + it { expect(subject.map(&:id)).to include(dossier_0.id) } end context 'when the user does not own the dossier' do let(:terms) { dossier_0b.id.to_s } - it { expect(subject.size).to eq(0) } + it { expect(subject.map(&:id)).not_to include(dossier_0b.id) } end end diff --git a/yarn.lock b/yarn.lock index 8003cd132..df4585998 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1263,6 +1263,11 @@ "@sentry/types" "5.15.4" tslib "^1.9.3" +"@tmcw/togeojson@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@tmcw/togeojson/-/togeojson-4.0.0.tgz#ee111e4e5b2b5d498d43e27a6c9bf546ce4041cc" + integrity sha512-JZXGC1myBPPYb/moq03cYPtErqZKzVR74Cv9C85IuqATHCxHCNOxw4D45vVcYHQnnxG2TQTIR+igzpbFiu/O6Q== + "@turf/area@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@turf/area/-/area-6.0.1.tgz#50ed63c70ef2bdb72952384f1594319d94f3b051"