From b237d015948c7a263a11daf27ff96ad8318ebf46 Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Fri, 24 Feb 2023 14:59:09 +0100 Subject: [PATCH] Feat/crud/gp sites components (#38) * feat(front): wip sites_groups component and svc * WIP feat(front): DataTable sites_groups - Table with specific data value (OK) - Table with sort column (OK) - Datatable , select row event and change color (wip). Reviewed-by: andriac [Refs ticket]: #4 * feat(front): Datatable format and selected row - Get and display data from group_site database (OK) - Selecting row and get id of row table (OK) Improve : - improve assign colname table outside the component (maybe into the class folder ?) Todo/next: - Filtering table - Add action column to table - refactor code by creating component for the ngx-datable in order to reuse component for other data Reviewed-by: andriac [Refs ticket]: #4 * feat(front): Filtering table (OK) Function filtering is working on each column Button filtering hide/display filter inputs (OK) TODO: - check for "help" icon and description for each column if description present - Check if multiple rows and pages if it's working Reviewed-by: andriac [Refs ticket]: #4 * feat(front): wip server pagination & filtering * feat(front): add sorting capability and fixes Fix: keep filters (sort and filters) on when changing pages Feat: remove useless id column Feat: add sorting * fix(db): change trigger to constraint (migrations) Delete the trigger and create check constraint on id_nomenclature column Reviewed-by: andriacap [Refs ticket]: #3 * refactor: Custom type Geojson and group properties Create custom type geojson and build properties into marshmallow schema according to this type of Geojson [Refs ticket]: #3 * refactor: site component with site-service add function to site.services.ts [Refs ticket]: #4 * refactor: create datatable component and service Separate of concern for all about table (service and component) and all about sites (service and component) reviewed_by: andriacap [Refs ticket]: #3 * fix: change offset to page change offset name to page for paginated Reviewed-by: andriacap [Refs ticket]: #4 * merge: merge interface and type merge site_group.service.ts delete site.service.ts add interfaces re arrange code from branch front-site-interface Reviewed-by: andriacap [Refs ticket]: #4 * feat: details properties sites groups Create properties components to display properties of sites_groups Adding route with id according to the site group selected Reviewed-by: andriacap [Refs ticket]: #4 * feat: display groups sites's child Display group site child into table and uder properties Use routing and id params to display property of groups site Create service site Add logic to check routing and child route to display reactive component properties and table Reviewed-by: andriacap [Refs ticket]: #4 * refactor(front): rename interfaces, remove classes Better types * feat(front): get all geometries For sites and sites_groups * feat(front): WIP: geojson service to create layers And features groups since geojson component accumulated layers without cleaning them... * feat(front): implemented select capability Need to refact a lot! * feat(api): add route to get one site_group by id * fix(front): too much / * feat(front): add get sites_group from id * fix(front): add possibility to provide Geometry To setMapData * refactor(front): sites and sites_groups component To extend a base component to gather the same methods at one place To use route children and router-outlet properly To add some rxjs operators To move some common interfaces/functions * fix(front): fix filters by adding baseFilters * feat: edit sitegroups Create edit-service Create observable to update object to edit according to form component Adapt function service of monitoring object to sites and sitesgroups WIP: - adapt remaining function of monitoring object inside form-component-g - create an object which inherit of patch/post/delete method with - think about object with ObjectType of sites_group , sites, observation ? , visit ? - reload properties or update data of properties table after update data Reviewed-by:andriacap [Refs_ticket]: #4 * feat: edit sitegroups Improving route 'patch' Adding reload properties when sitegroups is updated Refactoring EditService for more readability Reviewed-by: andriacap [Ref_ticket]: #4 * feat: improve edit Add method to define objectType according to service used (site or gpsite) Rewied-by: andriacap [Refs_ticket]: #4 * feat: improve rendering front "edit" and "add" Adding global object service to set object type and variable "add" or "edit" button Reviewed-by: andriacap [Refs_ticket]: #4 * feat: improving patch method object Adding service to share request patch,get in order to to re-use the service in form component according to the type pass inside the formcomponent Adding decorator errorhandler for blueprint routes Reviewed-by:andriacap [Refs_ticket]: #4 * feat: create site group method with form Route create with form component Using location pacakage to choose between edit form or init sitegroup (see to improve that) Rewiewed-by: andriacap [Refs ticket]: #4 * feat: delete site_group component Add route back Add site_group from front form component Reviewed-by:andriac [Refs_ticket]: #4 * chore(api): removed unused code * style(config): apply formatter * chore(front): removed unused code & console.log * feat(front): removed display map button As it was ugly * refactor(front): remove Object for keys As there is an Angular pipe to get keys and values Also removed console.log Removed unused code * style(front): reformat routes * refactor(front): add create component To isolate functionnalities * chore(front): remove unused services * chore(front): removed usused code * chore(api): remove string package And fix if that cannot be reached * chore(api): removed unused comment * chore(front): removed console.log and comments * chore(api): removed unused code and log --------- Co-authored-by: Andria Capai --- .../config/generic/config.json | 14 +- .../config/generic/sites_group.json | 15 - .../gn_module_monitoring/monitoring/models.py | 43 +- .../monitoring/schemas.py | 23 +- backend/gn_module_monitoring/routes/site.py | 3 +- .../routes/sites_groups.py | 51 +++ .../tests/test_routes/test_sites_groups.py | 10 + .../utils/errors/__init__.py | 0 .../utils/errors/errorHandler.py | 19 + .../app/class/monitoring-geom-component.ts | 30 ++ frontend/app/class/monitoring-site.ts | 13 + frontend/app/class/monitoring-sites-group.ts | 12 + .../components/modules/modules.component.html | 2 +- .../components/modules/modules.component.ts | 10 +- .../monitoring-datatable-g.component.css | 48 +++ .../monitoring-datatable-g.component.html | 121 ++++++ .../monitoring-datatable-g.component.spec.ts | 25 ++ .../monitoring-datatable-g.component.ts | 204 +++++++++ .../monitoring-form.component-g.css | 31 ++ .../monitoring-form.component-g.html | 124 ++++++ .../monitoring-form.component-g.spec.ts | 25 ++ .../monitoring-form.component-g.ts | 396 ++++++++++++++++++ .../monitoring-form.component.ts | 4 +- .../monitoring-map-list.component.css | 78 ++++ .../monitoring-map-list.component.html | 13 + .../monitoring-map-list.component.ts | 11 + .../monitoring-properties-g.component.css | 33 ++ .../monitoring-properties-g.component.html | 114 +++++ .../monitoring-properties-g.component.spec.ts | 25 ++ .../monitoring-properties-g.component.ts | 49 +++ .../monitoring-sites.component.css | 0 .../monitoring-sites.component.html | 25 ++ .../monitoring-sites.component.ts | 138 ++++++ ...onitoring-sitesgroups-create.component.css | 0 ...nitoring-sitesgroups-create.component.html | 4 + ...monitoring-sitesgroups-create.component.ts | 23 + .../monitoring-sitesgroups.component.css | 0 .../monitoring-sitesgroups.component.html | 6 + .../monitoring-sitesgroups.component.ts | 129 ++++++ frontend/app/functions/popup.ts | 14 + frontend/app/gnModule.module.ts | 86 +++- frontend/app/interfaces/column.ts | 5 + frontend/app/interfaces/form.ts | 7 + frontend/app/interfaces/geom.ts | 50 +++ frontend/app/interfaces/page.ts | 17 + frontend/app/interfaces/response.ts | 6 + frontend/app/services/api-geom.service.ts | 121 ++++++ frontend/app/services/cache.service.ts | 16 +- frontend/app/services/config.service.ts | 1 - frontend/app/services/data-table.service.ts | 90 ++++ frontend/app/services/edit-object.service.ts | 58 +++ frontend/app/services/geojson.service.ts | 112 +++++ frontend/app/services/object.service.ts | 23 + frontend/app/types/jsondata.ts | 1 + frontend/app/types/response.ts | 4 + 55 files changed, 2413 insertions(+), 69 deletions(-) create mode 100644 backend/gn_module_monitoring/utils/errors/__init__.py create mode 100644 backend/gn_module_monitoring/utils/errors/errorHandler.py create mode 100644 frontend/app/class/monitoring-geom-component.ts create mode 100644 frontend/app/class/monitoring-site.ts create mode 100644 frontend/app/class/monitoring-sites-group.ts create mode 100644 frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css create mode 100644 frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html create mode 100644 frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.spec.ts create mode 100644 frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts create mode 100644 frontend/app/components/monitoring-form-g/monitoring-form.component-g.css create mode 100644 frontend/app/components/monitoring-form-g/monitoring-form.component-g.html create mode 100644 frontend/app/components/monitoring-form-g/monitoring-form.component-g.spec.ts create mode 100644 frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts create mode 100644 frontend/app/components/monitoring-map-list/monitoring-map-list.component.css create mode 100644 frontend/app/components/monitoring-map-list/monitoring-map-list.component.html create mode 100644 frontend/app/components/monitoring-map-list/monitoring-map-list.component.ts create mode 100644 frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.css create mode 100644 frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html create mode 100644 frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.spec.ts create mode 100644 frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts create mode 100644 frontend/app/components/monitoring-sites/monitoring-sites.component.css create mode 100644 frontend/app/components/monitoring-sites/monitoring-sites.component.html create mode 100644 frontend/app/components/monitoring-sites/monitoring-sites.component.ts create mode 100644 frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.css create mode 100644 frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.html create mode 100644 frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts create mode 100644 frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.css create mode 100644 frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.html create mode 100644 frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts create mode 100644 frontend/app/functions/popup.ts create mode 100644 frontend/app/interfaces/column.ts create mode 100644 frontend/app/interfaces/form.ts create mode 100644 frontend/app/interfaces/geom.ts create mode 100644 frontend/app/interfaces/page.ts create mode 100644 frontend/app/interfaces/response.ts create mode 100644 frontend/app/services/api-geom.service.ts create mode 100644 frontend/app/services/data-table.service.ts create mode 100644 frontend/app/services/edit-object.service.ts create mode 100644 frontend/app/services/geojson.service.ts create mode 100644 frontend/app/services/object.service.ts create mode 100644 frontend/app/types/jsondata.ts create mode 100644 frontend/app/types/response.ts diff --git a/backend/gn_module_monitoring/config/generic/config.json b/backend/gn_module_monitoring/config/generic/config.json index f2dafb345..dc748a66e 100644 --- a/backend/gn_module_monitoring/config/generic/config.json +++ b/backend/gn_module_monitoring/config/generic/config.json @@ -1,20 +1,22 @@ { "tree": { "module": { - "site": { - "visit": { - "observation": null + "sites_group": { + "site": { + "visit": { + "observation": null + } } } } }, - "synthese" : "__MODULE.B_SYNTHESE", + "synthese": "__MODULE.B_SYNTHESE", "default_display_field_names": { "user": "nom_complet", "nomenclature": "label_fr", "dataset": "dataset_name", "observer_list": "nom_liste", - "taxonomy" : "__MODULE.TAXONOMY_DISPLAY_FIELD_NAME", + "taxonomy": "__MODULE.TAXONOMY_DISPLAY_FIELD_NAME", "taxonomy_list": "nom_liste", "sites_group": "sites_group_name", "habitat": "lb_hab_fr", @@ -22,4 +24,4 @@ "municipality": "nom_com_dept", "site": "base_site_name" } -} +} \ No newline at end of file diff --git a/backend/gn_module_monitoring/config/generic/sites_group.json b/backend/gn_module_monitoring/config/generic/sites_group.json index 68bdf820d..db1f1d5ed 100644 --- a/backend/gn_module_monitoring/config/generic/sites_group.json +++ b/backend/gn_module_monitoring/config/generic/sites_group.json @@ -17,16 +17,6 @@ {"prop": "sites_group_code"} ], "generic": { - "id_sites_group": { - "type_widget": "text", - "attribut_label": "Id site", - "hidden": true - }, - "id_module": { - "type_widget": "text", - "attribut_label": "ID Module", - "hidden": true - }, "sites_group_name": { "type_widget": "text", "attribut_label": "Nom", @@ -53,11 +43,6 @@ }, "nb_visits": { "attribut_label": "Nombre de visites" - }, - "medias": { - "type_widget": "medias", - "attribut_label": "Médias", - "schema_dot_table": "gn_monitoring.t_sites_groups" } } } diff --git a/backend/gn_module_monitoring/monitoring/models.py b/backend/gn_module_monitoring/monitoring/models.py index 65160fbac..9ea917069 100644 --- a/backend/gn_module_monitoring/monitoring/models.py +++ b/backend/gn_module_monitoring/monitoring/models.py @@ -2,7 +2,7 @@ Modèles SQLAlchemy pour les modules de suivi """ from sqlalchemy import select, func, and_ -from sqlalchemy.orm import column_property +from sqlalchemy.orm import column_property, ColumnProperty, RelationshipProperty, class_mapper from sqlalchemy.dialects.postgresql import JSONB, UUID from uuid import uuid4 @@ -10,6 +10,7 @@ from utils_flask_sqla_geo.serializers import geoserializable from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.ext.declarative import declared_attr from pypnnomenclature.models import TNomenclatures, BibNomenclaturesTypes from geonature.core.gn_commons.models import TMedias @@ -21,6 +22,42 @@ from geonature.core.gn_monitoring.models import corVisitObserver from gn_module_monitoring.monitoring.queries import Query as MonitoringQuery + +class GenericModel: + @declared_attr + def __tablename__(cls): + return cls.__name__.lower() + + @classmethod + def set_id(cls) -> None: + pk_string = class_mapper(cls).primary_key[0].name + if hasattr(cls,"id_g") ==False: + pk_value= getattr(cls,pk_string) + setattr(cls,"id_g",pk_value) + + @classmethod + def get_id(cls) -> None: + pk_string = class_mapper(cls).primary_key[0].name + # print('======= ==>', pk_string) + if hasattr(cls,"id_g") ==False: + pk_value= getattr(cls,pk_string) + setattr(cls,"id_g",pk_value) + return pk_string + + @classmethod + def find_by_id(cls, _id: int) -> "GenericModel": + cls.set_id() + return cls.query.get_or_404(_id) + + @classmethod + def attribute_names(cls): + return [ + prop.key + for prop in class_mapper(cls).iterate_properties + if isinstance(prop, ColumnProperty) + ] + + cor_module_type = DB.Table( "cor_module_type", DB.Column( @@ -53,7 +90,7 @@ @serializable -class BibTypeSite(DB.Model): +class BibTypeSite(DB.Model, GenericModel): __tablename__ = "bib_type_site" __table_args__ = {"schema": "gn_monitoring"} query_class = MonitoringQuery @@ -238,7 +275,7 @@ class TMonitoringSites(TBaseSites): @serializable -class TMonitoringSitesGroups(DB.Model): +class TMonitoringSitesGroups(DB.Model, GenericModel): __tablename__ = 't_sites_groups' __table_args__ = {'schema': 'gn_monitoring'} query_class = MonitoringQuery diff --git a/backend/gn_module_monitoring/monitoring/schemas.py b/backend/gn_module_monitoring/monitoring/schemas.py index f34066c55..1816d677e 100644 --- a/backend/gn_module_monitoring/monitoring/schemas.py +++ b/backend/gn_module_monitoring/monitoring/schemas.py @@ -1,9 +1,9 @@ import json import geojson -from marshmallow import Schema, fields -from marshmallow_sqlalchemy import SQLAlchemyAutoSchema -from pypnnomenclature.schemas import NomenclatureSchema +from geonature.utils.env import MA +from marshmallow import Schema, fields, validate +from geonature.core.gn_commons.schemas import MediaSchema from gn_module_monitoring.monitoring.models import ( BibTypeSite, @@ -22,19 +22,30 @@ class PaginationSchema(Schema): return PaginationSchema -class MonitoringSitesGroupsSchema(SQLAlchemyAutoSchema): +class MonitoringSitesGroupsSchema(MA.SQLAlchemyAutoSchema): + + sites_group_name = fields.String( + validate=validate.Length(min=3,error="Length must be greater than 3"),) + class Meta: model = TMonitoringSitesGroups exclude = ("geom_geojson",) + load_instance = True + medias = MA.Nested(MediaSchema) + pk = fields.Method("set_pk",dump_only=True) geometry = fields.Method("serialize_geojson", dump_only=True) + + def set_pk(self,obj): + return self.Meta.model.get_id() def serialize_geojson(self, obj): if obj.geom_geojson is not None: return json.loads(obj.geom_geojson) + -class MonitoringSitesSchema(SQLAlchemyAutoSchema): +class MonitoringSitesSchema(MA.SQLAlchemyAutoSchema): class Meta: model = TMonitoringSites exclude = ("geom_geojson", "geom") @@ -46,7 +57,7 @@ def serialize_geojson(self, obj): return geojson.dumps(obj.as_geofeature().get("geometry")) -class BibTypeSiteSchema(SQLAlchemyAutoSchema): +class BibTypeSiteSchema(MA.SQLAlchemyAutoSchema): label = fields.Method("get_label_from_type_site") # See if useful in the future: # type_site = fields.Nested(NomenclatureSchema(only=("label_fr",)), dump_only=True) diff --git a/backend/gn_module_monitoring/routes/site.py b/backend/gn_module_monitoring/routes/site.py index b4c8cc673..d4ee778aa 100644 --- a/backend/gn_module_monitoring/routes/site.py +++ b/backend/gn_module_monitoring/routes/site.py @@ -36,8 +36,7 @@ def get_types_site(): @blueprint.route("/sites/types/", methods=["GET"]) def get_type_site_by_id(id_type_site): - query = BibTypeSite.query.filter_by(id_nomenclature_type_site=id_type_site) - res = query.first() + res = BibTypeSite.find_by_id(id_type_site) schema = BibTypeSiteSchema() return schema.dump(res) diff --git a/backend/gn_module_monitoring/routes/sites_groups.py b/backend/gn_module_monitoring/routes/sites_groups.py index e04b27c65..8ebb8d3fd 100644 --- a/backend/gn_module_monitoring/routes/sites_groups.py +++ b/backend/gn_module_monitoring/routes/sites_groups.py @@ -1,5 +1,6 @@ from flask import jsonify, request from geonature.utils.env import db +from marshmallow import ValidationError from sqlalchemy import func from werkzeug.datastructures import MultiDict @@ -14,6 +15,7 @@ paginate, sort, ) +from gn_module_monitoring.utils.errors.errorHandler import InvalidUsage @blueprint.route("/sites_groups", methods=["GET"]) @@ -34,6 +36,13 @@ def get_sites_groups(): ) +@blueprint.route("/sites_groups/", methods=["GET"]) +def get_sites_group_by_id(id_sites_group: int): + schema = MonitoringSitesGroupsSchema() + result = TMonitoringSitesGroups.find_by_id(id_sites_group) + return jsonify(schema.dump(result)) + + @blueprint.route("/sites_groups/geometries", methods=["GET"]) def get_sites_group_geometries(): subquery = ( @@ -53,3 +62,45 @@ def get_sites_group_geometries(): result = geojson_query(subquery) return jsonify(result) + + +@blueprint.route("/sites_groups/", methods=["PATCH"]) +def patch(_id): + item_schema = MonitoringSitesGroupsSchema() + item_json = request.get_json() + item = TMonitoringSitesGroups.find_by_id(_id) + fields = TMonitoringSitesGroups.attribute_names() + for field in item_json: + if field in fields: + setattr(item, field, item_json[field]) + item_schema.load(item_json) + db.session.add(item) + + db.session.commit() + return item_schema.dump(item), 201 + + +@blueprint.route("/sites_groups/", methods=["DELETE"]) +def delete(_id): + item_schema = MonitoringSitesGroupsSchema() + item = TMonitoringSitesGroups.find_by_id(_id) + TMonitoringSitesGroups.query.filter_by(id_g=_id).delete() + db.session.commit() + return item_schema.dump(item), 201 + + +@blueprint.route("/sites_groups", methods=["POST"]) +def post(): + item_schema = MonitoringSitesGroupsSchema() + item_json = request.get_json() + item = item_schema.load(item_json) + db.session.add(item) + db.session.commit() + return item_schema.dump(item), 201 + + +@blueprint.errorhandler(ValidationError) +def handle_validation_error(error): + return InvalidUsage( + "Fields cannot be validated, message : {}".format(error.messages), status_code=422, payload=error.data + ).to_dict() diff --git a/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py b/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py index f459d22ce..0d5d4f2e3 100644 --- a/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py +++ b/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py @@ -7,6 +7,16 @@ @pytest.mark.usefixtures("client_class", "temporary_transaction") class TestSitesGroups: + def test_get_sites_group_by_id(self, sites_groups): + sites_group = list(sites_groups.values())[0] + id_sites_group = sites_group.id_sites_group + r = self.client.get( + url_for("monitorings.get_sites_group_by_id", id_sites_group=id_sites_group) + ) + + assert r.json["id_sites_group"] == id_sites_group + assert r.json["sites_group_name"] == sites_group.sites_group_name + def test_get_sites_groups(self, sites_groups): r = self.client.get(url_for("monitorings.get_sites_groups")) diff --git a/backend/gn_module_monitoring/utils/errors/__init__.py b/backend/gn_module_monitoring/utils/errors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/utils/errors/errorHandler.py b/backend/gn_module_monitoring/utils/errors/errorHandler.py new file mode 100644 index 000000000..a10b13987 --- /dev/null +++ b/backend/gn_module_monitoring/utils/errors/errorHandler.py @@ -0,0 +1,19 @@ +from geonature.utils.errors import GeonatureApiError + + +class InvalidUsage(GeonatureApiError): + status_code = 400 + + def __init__(self, message, status_code=None, payload=None): + GeonatureApiError.__init__(self, message, status_code) + self.message = message + if status_code is not None: + self.status_code = status_code + self.payload = payload + + def to_dict(self): + rv = {} + rv["payload"] = self.payload + rv["message"] = self.message + rv["status_code"] = self.status_code + return (rv, self.status_code) diff --git a/frontend/app/class/monitoring-geom-component.ts b/frontend/app/class/monitoring-geom-component.ts new file mode 100644 index 000000000..acad9b05b --- /dev/null +++ b/frontend/app/class/monitoring-geom-component.ts @@ -0,0 +1,30 @@ +import { PageInfo } from "../interfaces/page"; +import { JsonData } from "../types/jsondata"; + +const LIMIT = 10; + +type callbackFunction = (pageNumber: number, filters: JsonData) => void; + +export class MonitoringGeomComponent { + protected getAllItemsCallback: callbackFunction; + protected limit = LIMIT; + public filters = {}; + public baseFilters = {}; + + constructor() {} + + setPage(page: PageInfo) { + this.getAllItemsCallback(page.offset + 1, this.filters); + } + + setSort(filters: JsonData) { + this.filters = { ...this.baseFilters, ...filters }; + const pageNumber = 1; + this.getAllItemsCallback(pageNumber, this.filters); + } + + setFilter(filters) { + this.filters = { ...this.baseFilters, ...filters }; + this.getAllItemsCallback(1, this.filters); + } +} diff --git a/frontend/app/class/monitoring-site.ts b/frontend/app/class/monitoring-site.ts new file mode 100644 index 000000000..d33650386 --- /dev/null +++ b/frontend/app/class/monitoring-site.ts @@ -0,0 +1,13 @@ +export enum columnNameSite { + base_site_name = "Nom", + last_visit = "Dernière visite", + nb_visits = "Nb. visites", + base_site_code = "Code", + altitude_max = "Alt.max", + altitude_min = "Alt.min", +} + +export const extendedDetailsSite = { + ...columnNameSite, + base_site_description: "Description", +}; diff --git a/frontend/app/class/monitoring-sites-group.ts b/frontend/app/class/monitoring-sites-group.ts new file mode 100644 index 000000000..96fab297d --- /dev/null +++ b/frontend/app/class/monitoring-sites-group.ts @@ -0,0 +1,12 @@ +export enum columnNameSiteGroup { + sites_group_name = "Nom", + nb_sites = "Nb. sites", + nb_visits = "Nb. visites", + sites_group_code = "Code", +} + +export const extendedDetailsSiteGroup = { + ...columnNameSiteGroup, + comments: "Commentaires", + sites_group_description: "Description", +}; diff --git a/frontend/app/components/modules/modules.component.html b/frontend/app/components/modules/modules.component.html index 3519e09b6..b382502c2 100644 --- a/frontend/app/components/modules/modules.component.html +++ b/frontend/app/components/modules/modules.component.html @@ -14,7 +14,7 @@

{{titleModule}}

{{description}}
-
diff --git a/frontend/app/components/modules/modules.component.ts b/frontend/app/components/modules/modules.component.ts index a0d411dbe..febaee23b 100644 --- a/frontend/app/components/modules/modules.component.ts +++ b/frontend/app/components/modules/modules.component.ts @@ -14,8 +14,6 @@ import { AuthService, User } from "@geonature/components/auth/auth.service"; styleUrls: ['./modules.component.css'], }) export class ModulesComponent implements OnInit { - - currentUser: User; description: string; @@ -34,7 +32,7 @@ export class ModulesComponent implements OnInit { private _auth: AuthService, private _dataMonitoringObjectService: DataMonitoringObjectService, private _configService: ConfigService - ) { } + ) {} ngOnInit() { this.bLoading = true; @@ -64,10 +62,4 @@ export class ModulesComponent implements OnInit { this.currentUser["cruved"] = {}; this.currentUser["cruved_objects"] = {}; } - - onAccessSitesClick(modules) { - console.log("accès aux sites avec droits ") - console.log(modules) - } - } diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css new file mode 100644 index 000000000..6017b44ab --- /dev/null +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css @@ -0,0 +1,48 @@ +.cell-link { + cursor: pointer; +} + +:host::ng-deep .datatable-body-row.active .datatable-row-group { + background-color: rgb(117, 227, 118) !important; +} + +.link:hover { + background-color: rgba(0, 0, 0, 0.2) !important; + transition: background-color 0.5; +} + +.link { + display: inline; + transition: background-color 0.5s; + border-radius: 5px; +} + +.header-filter-span > input { + width: 100%; +} + +.header-sort-span { + /* width: 100%; */ + cursor: pointer; + text-overflow: ellipsis; + overflow: hidden; + white-space:nowrap +} + + +.header-sort-span:hover { + background-color: rgb(245, 245, 245); +} + +.icon-sort { + font-size: 1.2em; + float: right; +} + +:host::ng-deep .sort-btn { + display: none !important; +} + +.custom-dt { + box-shadow: none !important; +} diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html new file mode 100644 index 000000000..17e0a03ed --- /dev/null +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html @@ -0,0 +1,121 @@ + +
+ + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + +
+ + + help + + {{ column.name }} +
+
+ +
+
+
+ diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.spec.ts b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.spec.ts new file mode 100644 index 000000000..deaea12fe --- /dev/null +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MonitoringDatatableComponent } from './monitoring-datatable-g.component'; + +describe('MonitoringDatatableComponent', () => { + let component: MonitoringDatatableComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MonitoringDatatableComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MonitoringDatatableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts new file mode 100644 index 000000000..b152c85cd --- /dev/null +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts @@ -0,0 +1,204 @@ +import { DatatableComponent } from "@swimlane/ngx-datatable"; +import { + Component, + OnInit, + Input, + Output, + EventEmitter, + ViewChild, + SimpleChanges, + TemplateRef, +} from "@angular/core"; +import { Router, ActivatedRoute } from "@angular/router"; +import { Subject } from "rxjs"; +import { debounceTime } from "rxjs/operators"; +import { DataTableService } from "../../services/data-table.service"; +import { IColumn } from "../../interfaces/column"; +import { IPage } from "../../interfaces/page"; +import { ObjectService } from "../../services/object.service"; + +interface ItemObjectTable { + id: number | null; + selected: boolean; + visible: boolean; + current: boolean; +} +type ItemsObjectTable = { [key: string]: ItemObjectTable }; + +@Component({ + selector: "pnx-monitoring-datatable-g", + templateUrl: "./monitoring-datatable-g.component.html", + styleUrls: ["./monitoring-datatable-g.component.css"], +}) +export class MonitoringDatatableGComponent implements OnInit { + @Input() rows; + @Input() colsname: IColumn[]; + @Input() page: IPage = { count: 0, limit: 0, page: 0 }; + @Input() obj; + + @Input() rowStatus: Array; + @Output() rowStatusChange = new EventEmitter(); + + @Output() bEditChanged = new EventEmitter(); + + @Input() currentUser; + + @Output() onSort = new EventEmitter(); + @Output() onFilter = new EventEmitter(); + @Output() onSetPage = new EventEmitter(); + @Output() onDetailsRow = new EventEmitter(); + @Output() addEvent = new EventEmitter(); + + private filterSubject: Subject = new Subject(); + displayFilter: boolean = false; + objectsStatus: ItemsObjectTable; + + objectType: string = ""; + columns; + row_save; + selected = []; + filters = {}; + + @ViewChild(DatatableComponent) table: DatatableComponent; + @ViewChild("actionsTemplate") actionsTemplate: TemplateRef; + @ViewChild("hdrTpl") hdrTpl: TemplateRef; + + constructor( + private _dataTableService: DataTableService, + private _objService: ObjectService, + private router: Router, + private _Activatedroute: ActivatedRoute + ) {} + + ngOnInit() { + this.initDatatable(); + } + + initDatatable() { + // IF prefered observable compare to ngOnChanges uncomment this: + // this._dataTableService.currentCols.subscribe(newCols => { this.columns = newCols }) + this._objService.currentObjectType.subscribe((newObjType) => { + this.objectType = newObjType; + }); + + this.filters = {}; + this.filterSubject.pipe(debounceTime(500)).subscribe(() => { + this.filter(); + }); + } + + onSortEvent($event) { + this.filters = { + ...this.filters, + sort: $event.column.prop, + sort_dir: $event.newValue, + }; + this.onSort.emit(this.filters); + } + + setPage($event) { + this.onSetPage.emit($event); + } + + filterInput($event) { + this.filterSubject.next(); + } + + filter(bInitFilter = false) { + // filter all + const oldFilters = this.filters; + this.filters = Object.keys(oldFilters).reduce(function (r, e) { + if (![undefined, "", null].includes(oldFilters[e])) r[e] = oldFilters[e]; + return r; + }, {}); + this.onFilter.emit(this.filters); + } + + onSelectEvent({ selected }) { + const id = selected[0].id_group_site; + + if (!this.rowStatus) { + return; + } + + this.rowStatus.forEach((status) => { + const bCond = status.id === id; + status["selected"] = bCond && !status["selected"]; + }); + + this.setSelected(); + this.rowStatusChange.emit(this.rowStatus); + } + + setSelected() { + // this.table._internalRows permet d'avoir les ligne triées et d'avoir les bons index + + if (!this.rowStatus) { + return; + } + + const status_selected = this.rowStatus.find((status) => status.selected); + if (!status_selected) { + return; + } + + const index_row_selected = this.table._internalRows.findIndex( + (row) => row.id === status_selected.id + ); + if (index_row_selected === -1) { + return; + } + + this.selected = [this.table._internalRows[index_row_selected]]; + this.table.offset = Math.floor(index_row_selected / this.table._limit); + } + + ngOnDestroy() { + this.filterSubject.unsubscribe(); + } + + // tooltip(column) { + // return this.child0.template.fieldDefinitions[column.prop] + // ? column.name + " : " + this.child0.template.fieldDefinitions[column.prop] + // : column.name; + // } + + ngOnChanges(changes: SimpleChanges) { + // IF prefered ngOnChanges compare to observable uncomment this: + if (changes["rows"] && this.rows && this.rows.length > 0) { + this.columns = this._dataTableService.colsTable( + this.colsname, + this.rows[0] + ); + } + + if (changes["colsname"]) { + this.filters = {}; + } + + if (changes["obj"] && this.obj) { + this.objectsStatus, + (this.rowStatus = this._dataTableService.initObjectsStatus( + this.obj, + "sites_groups" + )); + } + for (const propName of Object.keys(changes)) { + switch (propName) { + case "rowStatus": + this.setSelected(); + break; + } + } + } + navigateToAddChildren(_, rowId) { + this.addEvent.emit(rowId); + this.router.navigate(["create"], { + relativeTo: this._Activatedroute, + }); + } + navigateToDetail(row) { + row["id"] = row.pk; + this.onDetailsRow.emit(row); + } +} diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.css b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.css new file mode 100644 index 000000000..a5c26b9ca --- /dev/null +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.css @@ -0,0 +1,31 @@ +:host ::ng-deep .obj-form { + margin: 0; + margin-bottom: 10px; + padding: 0; +} + +.hide-spinner { + display: none; +} + +.btn-height { + height: 39px; +} + +.float-right { +margin-left: 5px; +} + + +.float-left { + margin-right: 10px; + float: left; +} + +form:invalid { + outline: none; +} + +form.ng-invalid { + border: 0px !important; +} \ No newline at end of file diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html new file mode 100644 index 000000000..389e9284b --- /dev/null +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html @@ -0,0 +1,124 @@ + +

Attention

+

+ + Vous êtes sur le point de supprimer le groupe de site + Description du groupe de site +

+ + +
+ +
+
+ + + + + + + +
+ + + + + + + + +
+ + + + + + +
+
+
+
+
diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.spec.ts b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.spec.ts new file mode 100644 index 000000000..1cfc81be0 --- /dev/null +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MonitoringFormComponent } from './monitoring-form.component'; + +describe('MonitoringFormComponent', () => { + let component: MonitoringFormComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MonitoringFormComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MonitoringFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts new file mode 100644 index 000000000..c993c5026 --- /dev/null +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts @@ -0,0 +1,396 @@ +import { + Component, + OnInit, + Input, + Output, + EventEmitter, + SimpleChanges, +} from "@angular/core"; +import { FormGroup, FormBuilder } from "@angular/forms"; +import { MonitoringObject } from "../../class/monitoring-object"; +import { ConfigService } from "../../services/config.service"; +import { CommonService } from "@geonature_common/service/common.service"; +import { DynamicFormService } from "@geonature_common/form/dynamic-form-generator/dynamic-form.service"; +import { ActivatedRoute } from "@angular/router"; +import { EditObjectService } from "../../services/edit-object.service"; +import { Router } from "@angular/router"; +import { IDataForm } from "../../interfaces/form"; +import { ApiGeomService } from "../../services/api-geom.service"; +@Component({ + selector: "pnx-monitoring-form-g", + templateUrl: "./monitoring-form.component-g.html", + styleUrls: ["./monitoring-form.component-g.css"], +}) +export class MonitoringFormComponentG implements OnInit { + @Input() currentUser; + + @Input() objForm: FormGroup; + + // @Input() obj: any; + @Output() objChanged = new EventEmitter(); + + @Input() objectsStatus; + @Output() objectsStatusChange = new EventEmitter(); + + @Input() bEdit: boolean; + @Output() bEditChange = new EventEmitter(); + + @Input() sites: {}; + dataForm: IDataForm; + searchSite = ""; + + obj: any; + objFormsDefinition; + + meta: {}; + + public bSaveSpinner = false; + public bSaveAndAddChildrenSpinner = false; + public bDeleteSpinner = false; + public bDeleteModal = false; + public bChainInput = false; + public bAddChildren = false; + public chainShow = []; + + public queryParams = {}; + + constructor( + private _formBuilder: FormBuilder, + private _route: ActivatedRoute, + private _configService: ConfigService, + private _commonService: CommonService, + private _dynformService: DynamicFormService, + private _editService: EditObjectService, + private _apiGeomService: ApiGeomService, + private _router: Router + ) {} + + ngOnInit() { + this._editService.currentData.subscribe((dataToEdit) => { + this.obj = dataToEdit; + this.obj.bIsInitialized = true; + this._configService + .init(this.obj.moduleCode) + .pipe() + .subscribe(() => { + // return this._route.queryParamMap; + // }) + // .subscribe((queryParams) => { + this.queryParams = this._route.snapshot.queryParams || {}; + this.bChainInput = + this._configService.frontendParams()["bChainInput"]; + + const schema = this._configService.schema( + this.obj.moduleCode, + this.obj.objectType + ); + this.obj[this.obj.moduleCode] = schema; + // const schema = this.obj.schema(); + + // init objFormsDefinition + + // meta pour les parametres dynamiques + // ici pour avoir acces aux nomenclatures + this.meta = { + // nomenclatures: this._dataUtilsService.getDataUtil('nomenclature'), + // dataset: this._dataUtilsService.getDataUtil('dataset'), + // id_role: this.currentUser.id_role, + bChainInput: this.bChainInput, + parents: this.obj.parents, + }; + + this.objFormsDefinition = this._dynformService + .formDefinitionsdictToArray(schema, this.meta) + .filter((formDef) => formDef.type_widget) + .sort((a, b) => { + // medias à la fin + return a.attribut_name === "medias" + ? +1 + : b.attribut_name === "medias" + ? -1 + : 0; + }); + + // display_form pour customiser l'ordre dans le formulaire + // les éléments de display form sont placé en haut dans l'ordre du tableau + // tous les éléments non cachés restent affichés + + let displayProperties = [ + ...(this._configService.configModuleObjectParam( + this.obj.moduleCode, + this.obj.objectType, + "display_properties" + ) || []), + ]; + if (displayProperties && displayProperties.length) { + displayProperties.reverse(); + this.objFormsDefinition.sort((a, b) => { + let indexA = displayProperties.findIndex( + (e) => e == a.attribut_name + ); + let indexB = displayProperties.findIndex( + (e) => e == b.attribut_name + ); + return indexB - indexA; + }); + } + + // champs patch pour simuler un changement de valeur et déclencher le recalcul des propriété + // par exemple quand bChainInput change + this.objForm.addControl("patch_update", this._formBuilder.control(0)); + + // this._configService.configModuleObject(this.obj.moduleCode, this.obj.objectType); + // set geometry + // if (this.obj.config["geometry_type"]) { + // this.objForm.addControl( + // "geometry", + // this._formBuilder.control("", Validators.required) + // ); + // } + + // pour donner la valeur de idParent + + this.initForm(); + }); + }); + } + + /** pour réutiliser des paramètres déjà saisis */ + keepDefinitions() { + return this.objFormsDefinition.filter((def) => + this.obj.configParam("keep").includes(def.attribut_name) + ); + } + + setQueryParams() { + // par le biais des parametre query de route on donne l'id du ou des parents + // permet d'avoir un 'tree' ou un objet est sur plusieurs branches + // on attend des ids d'où test avec parseInt + for (const key of Object.keys(this.queryParams)) { + const strToInt = parseInt(this.queryParams[key]); + if (strToInt != NaN) { + this.obj.properties[key] = strToInt; + } + } + } + + /** initialise le formulaire quand le formulaire est prêt ou l'object est prêt */ + initForm() { + if (!(this.objForm && this.obj.bIsInitialized)) { + return; + } + + this.setQueryParams(); + // pour donner la valeur de l'objet au formulaire + this._editService.formValues(this.obj).subscribe((formValue) => { + this.objForm.patchValue(formValue); + this.setDefaultFormValue(); + this.dataForm = formValue; + // reset geom ? + }); + } + + keepNames() { + return this.obj.configParam("keep") || []; + } + + resetObjForm() { + // quand on enchaine les relevés + const chainShow = this.obj.configParam("chain_show"); + if (chainShow) { + this.chainShow.push( + chainShow.map((key) => this.obj.resolvedProperties[key]) + ); + this.chainShow.push(this.obj.resolvedProperties); + } + + // les valeur que l'on garde d'une saisie à l'autre + const keep = {}; + for (const key of this.keepNames()) { + keep[key] = this.obj.properties[key]; + } + + // nouvel object + this.obj = new MonitoringObject( + this.obj.moduleCode, + this.obj.objectType, + null, + this.obj.monitoringObjectService() + ); + this.obj.init({}); + + this.obj.properties[this.obj.configParam("id_field_Name")] = null; + + // pq get ????? + // this.obj.get(0).subscribe(() => { + this.obj.bIsInitialized = true; + for (const key of this.keepNames()) { + this.obj.properties[key] = keep[key]; + } + + this.objChanged.emit(this.obj); + this.objForm.patchValue({ geometry: null }); + this.initForm(); + // }); + } + + /** Pour donner des valeurs par defaut si la valeur n'est pas définie + * id_digitiser => current_user.id_role + * id_inventor => current_user.id_role + * date => today + */ + setDefaultFormValue() { + const value = this.objForm.value; + const date = new Date(); + const defaultValue = { + // id_digitiser: value["id_digitiser"] || this.currentUser.id_role, + // id_inventor: value["id_inventor"] || this.currentUser.id_role, + first_use_date: value["first_use_date"] || { + year: date.getUTCFullYear(), + month: date.getUTCMonth() + 1, + day: date.getUTCDate(), + }, + }; + this.objForm.patchValue(defaultValue); + } + + /** + * TODO faire des fonctions dans monitoring objet (ou moniotring objet service pour naviguer + */ + + /** + * Valider et renseigner les enfants + */ + navigateToAddChildren() { + this.bEditChange.emit(false); + this.obj.navigateToAddChildren(); + } + + /** + * Valider et aller à la page de l'objet + */ + navigateToDetail(id, objectType, queryParams) { + // patch bug navigation + this._router.navigate( + ["monitorings", objectType, id].filter((s) => !!s), + { + queryParams, + } + ); + this.bEditChange.emit(false); + } + + /** + * Valider et aller à la page de l'objet + */ + navigateToParent() { + this.bEditChange.emit(false); // patch bug navigation + this._router.navigateByUrl("/monitorings/sites_group"); + + // this.obj.navigateToParent(); + } + + msgToaster(action) { + // return `${action} ${this.obj.labelDu()} ${this.obj.description()} effectuée`.trim(); + return `${action} effectuée`.trim(); + } + + /** TODO améliorer site etc.. */ + onSubmit() { + const { patch_update, ...sendValue } = this.dataForm; + const action = this.obj.id + ? // ? this.obj.patch(this.objForm.value) + // : this.obj.post(this.objForm.value); + this._apiGeomService.patch(this.obj.id, sendValue) + : this._apiGeomService.create(sendValue); + const actionLabel = this.obj.id ? "Modification" : "Création"; + action.subscribe((objData) => { + this._commonService.regularToaster( + "success", + this.msgToaster(actionLabel) + ); + this.bSaveSpinner = this.bSaveAndAddChildrenSpinner = false; + this.objChanged.emit(this.obj); + + /** si c'est un module : reset de la config */ + if (this.obj.objectType === "module") { + this._configService.loadConfig(this.obj.moduleCode).subscribe(); + } + + if (this.bChainInput) { + this.resetObjForm(); + } else if (this.bAddChildren) { + this.navigateToAddChildren(); + } else { + if ( + this._configService.configModuleObjectParam( + this.obj.moduleCode, + this.obj.objectType, + "redirect_to_parent" + ) + ) { + this.navigateToParent(); + } else { + this.navigateToDetail( + this.obj.id, + this.obj.objectType, + this.queryParams + ); + } + } + }); + } + + onCancelEdit() { + if (this.obj.id) { + this.bEditChange.emit(false); + } else { + this.navigateToParent(); + } + } + + onDelete() { + this.bDeleteSpinner = true; + this._commonService.regularToaster("info", this.msgToaster("Suppression")); + // : this.obj.post(this.objForm.value); + this._apiGeomService.delete(this.obj.id).subscribe((del) => { + this.bDeleteSpinner = this.bDeleteModal = false; + this.objChanged.emit(this.obj); + setTimeout(() => { + this.navigateToParent(); + }, 100); + }); + } + + onObjFormValueChange(event) { + // let {id_module,medias, ...rest} = this.objForm.value; + // this.dataForm = rest + this.dataForm = this.objForm.value; + const change = this._configService.change( + this.obj.moduleCode, + this.obj.objectType + ); + if (!change) { + return; + } + setTimeout(() => { + change({ objForm: this.objForm, meta: this.meta }); + }, 100); + } + + procesPatchUpdateForm() { + this.objForm.patchValue({ + patch_update: this.objForm.value.patch_update + 1, + }); + } + + /** bChainInput gardé dans config service */ + bChainInputChanged() { + for (const formDef of this.objFormsDefinition) { + formDef.meta.bChainInput = this.bChainInput; + } + this._configService.setFrontendParams("bChainInput", this.bChainInput); + // patch pour recalculers + this.procesPatchUpdateForm(); + } +} diff --git a/frontend/app/components/monitoring-form/monitoring-form.component.ts b/frontend/app/components/monitoring-form/monitoring-form.component.ts index 3e0ae5634..5e3ca239a 100644 --- a/frontend/app/components/monitoring-form/monitoring-form.component.ts +++ b/frontend/app/components/monitoring-form/monitoring-form.component.ts @@ -281,13 +281,11 @@ export class MonitoringFormComponent implements OnInit { onDelete() { this.bDeleteSpinner = true; - this._commonService.regularToaster('info', this.msgToaster('Suppression')); - this.obj.delete().subscribe((objData) => { this.bDeleteSpinner = this.bDeleteModal = false; this.obj.deleted = true; this.objChanged.emit(this.obj); - + this._commonService.regularToaster('info', this.msgToaster('Suppression')); setTimeout(() => { this.navigateToParent(); }, 100); diff --git a/frontend/app/components/monitoring-map-list/monitoring-map-list.component.css b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.css new file mode 100644 index 000000000..4c1e5a93e --- /dev/null +++ b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.css @@ -0,0 +1,78 @@ +.flex-container { + background-color: rgb(240, 240, 240); + display: flex; + } + + .flex-container > div { + width: 50%; + padding: 10px; + margin: 10px; + } + + .flex-container > div:first-child { + margin-right: 0; + } + + .scroll { + overflow-y: scroll; + } + + :host ::ng-deep .cadre { + background-color: white; + /* border: 1px solid grey;*/ + border-radius: 5px; + padding: 5px; + margin: 5px; + /* display: inline-block; */ + } + + /* TABLE */ + + .cell-link { + cursor: pointer; + } + + :host::ng-deep .datatable-body-row.active .datatable-row-group { + background-color: rgb(117, 227, 118) !important; + } + + .link:hover { + background-color: rgba(0, 0, 0, 0.2) !important; + transition: background-color 0.5; + } + + .link { + display: inline; + transition: background-color 0.5s; + border-radius: 5px; + } + + .header-filter-span > input { + width: 100%; + } + + .header-sort-span { + /* width: 100%; */ + cursor: pointer; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .header-sort-span:hover { + background-color: rgb(245, 245, 245); + } + + .icon-sort { + font-size: 1.2em; + float: right; + } + + :host::ng-deep .sort-btn { + display: none !important; + } + + .custom-dt { + box-shadow: none !important; + } + \ No newline at end of file diff --git a/frontend/app/components/monitoring-map-list/monitoring-map-list.component.html b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.html new file mode 100644 index 000000000..ee93b5280 --- /dev/null +++ b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.html @@ -0,0 +1,13 @@ +
+
+
+ +
+
+
+
+ + +
+
+
\ No newline at end of file diff --git a/frontend/app/components/monitoring-map-list/monitoring-map-list.component.ts b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.ts new file mode 100644 index 000000000..65bf3f50d --- /dev/null +++ b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.ts @@ -0,0 +1,11 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "monitoring-map-list.component", + templateUrl: "./monitoring-map-list.component.html", + styleUrls: ["./monitoring-map-list.component.css"], +}) +export class MonitoringMapListComponent { + displayMap: boolean = true; + constructor() {} +} diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.css b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.css new file mode 100644 index 000000000..c2fc53c60 --- /dev/null +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.css @@ -0,0 +1,33 @@ +table { + font-size: small; +} + +th { + text-align: right; +} + +.key { + font-weight: bold; +} + +td { + padding-left: 20px; +} + +.small-icon { + font-size: 18px; +} + +.medias-tab { + margin: 10px; +} + +.hide-spinner { + display: none; +} + + +::ng-deep .cdk-global-overlay-wrapper, +::ng-deep .cdk-overlay-container { + z-index: 99999 !important; +} diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html new file mode 100644 index 000000000..ebab4aca9 --- /dev/null +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html @@ -0,0 +1,114 @@ +
+
+ + +
+
diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.spec.ts b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.spec.ts new file mode 100644 index 000000000..dd9bf0fc3 --- /dev/null +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MonitoringPropertiesComponent } from './monitoring-properties-g.component'; + +describe('MonitoringPropertiesGComponent', () => { + let component: MonitoringPropertiesComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MonitoringPropertiesComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MonitoringPropertiesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts new file mode 100644 index 000000000..d47a3ef60 --- /dev/null +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts @@ -0,0 +1,49 @@ +import { + Component, + OnInit, + Input, + Output, + EventEmitter, + SimpleChanges, +} from "@angular/core"; +import { FormControl } from "@angular/forms"; +import { extendedDetailsSiteGroup } from "../../class/monitoring-sites-group"; +import { ISitesGroup } from "../../interfaces/geom"; +import { EditObjectService } from "../../services/edit-object.service"; +import { ObjectService } from "../../services/object.service"; + +@Component({ + selector: "pnx-monitoring-properties-g", + templateUrl: "./monitoring-properties-g.component.html", + styleUrls: ["./monitoring-properties-g.component.css"], +}) +export class MonitoringPropertiesGComponent implements OnInit { + @Input() selectedObj: ISitesGroup; + @Input() bEdit: boolean; + @Output() bEditChange = new EventEmitter(); + @Input() objectType: string; + + infosColsSiteGroups: typeof extendedDetailsSiteGroup = + extendedDetailsSiteGroup; + color: string = "white"; + dataDetails: ISitesGroup; + + datasetForm = new FormControl(); + + constructor( + private _editService: EditObjectService, + private _objService: ObjectService + ) {} + + ngOnInit() { + this._objService.currentObjectTypeParent.subscribe((newObjType) => { + this.objectType = newObjType; + }); + } + + onEditClick() { + this.bEditChange.emit(true); + this.selectedObj["id"] = this.selectedObj[this.selectedObj.pk]; + this._editService.changeDataSub(this.selectedObj); + } +} diff --git a/frontend/app/components/monitoring-sites/monitoring-sites.component.css b/frontend/app/components/monitoring-sites/monitoring-sites.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/app/components/monitoring-sites/monitoring-sites.component.html b/frontend/app/components/monitoring-sites/monitoring-sites.component.html new file mode 100644 index 000000000..4c9a60e1c --- /dev/null +++ b/frontend/app/components/monitoring-sites/monitoring-sites.component.html @@ -0,0 +1,25 @@ + + + + diff --git a/frontend/app/components/monitoring-sites/monitoring-sites.component.ts b/frontend/app/components/monitoring-sites/monitoring-sites.component.ts new file mode 100644 index 000000000..00b9cd752 --- /dev/null +++ b/frontend/app/components/monitoring-sites/monitoring-sites.component.ts @@ -0,0 +1,138 @@ +import { Component, OnInit, Input } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { forkJoin } from "rxjs"; +import { tap, map, mergeMap } from "rxjs/operators"; +import * as L from "leaflet"; +import { ISite, ISitesGroup } from "../../interfaces/geom"; +import { IPage, IPaginated } from "../../interfaces/page"; +import { columnNameSite } from "../../class/monitoring-site"; +import { MonitoringGeomComponent } from "../../class/monitoring-geom-component"; +import { setPopup } from "../../functions/popup"; +import { GeoJSONService } from "../../services/geojson.service"; +import { FormGroup, FormBuilder } from "@angular/forms"; +import { + SitesService, + SitesGroupService, +} from "../../services/api-geom.service"; +import { ObjectService } from "../../services/object.service"; + +const LIMIT = 10; + +@Component({ + selector: "monitoring-sites", + templateUrl: "./monitoring-sites.component.html", + styleUrls: ["./monitoring-sites.component.css"], +}) +export class MonitoringSitesComponent + extends MonitoringGeomComponent + implements OnInit +{ + siteGroupId: number; + sites: ISite[]; + sitesGroup: ISitesGroup; + colsName: typeof columnNameSite = columnNameSite; + page: IPage; + filters = {}; + siteGroupLayer: L.FeatureGroup; + @Input() bEdit: boolean; + objForm: FormGroup; + objectType: string; + + constructor( + private _sitesGroupService: SitesGroupService, + private _siteService: SitesService, + private _objService: ObjectService, + private router: Router, + private _Activatedroute: ActivatedRoute, + private _geojsonService: GeoJSONService, + private _formBuilder: FormBuilder + ) { + super(); + this.getAllItemsCallback = this.getSitesFromSiteGroupId; + } + + ngOnInit() { + this.objForm = this._formBuilder.group({}); + this._objService.changeObjectType(this._siteService.addObjectType()); + this.initSite(); + } + + initSite() { + this._Activatedroute.params + .pipe( + map((params) => params["id"] as number), + tap((id: number) => { + this._geojsonService.getSitesGroupsChildGeometries( + this.onEachFeatureSite(), + { id_sites_group: id } + ); + }), + mergeMap((id: number) => + forkJoin({ + sitesGroup: this._sitesGroupService.getById(id), + sites: this._sitesGroupService.getSitesChild(1, this.limit, { + id_sites_group: id, + }), + }) + ) + ) + .subscribe( + (data: { sitesGroup: ISitesGroup; sites: IPaginated }) => { + this.sitesGroup = data.sitesGroup; + this.sites = data.sites.items; + this.page = { + count: data.sites.count, + page: data.sites.page, + limit: data.sites.limit, + }; + this.siteGroupLayer = this._geojsonService.setMapData( + data.sitesGroup.geometry, + () => {} + ); + this.baseFilters = { id_sites_group: this.sitesGroup.id_sites_group }; + } + ); + } + ngOnDestroy() { + this._geojsonService.removeFeatureGroup( + this._geojsonService.sitesFeatureGroup + ); + this._geojsonService.removeFeatureGroup(this.siteGroupLayer); + } + + onEachFeatureSite() { + const baseUrl = this.router.url; + return (feature, layer) => { + const popup = setPopup( + baseUrl, + feature.properties.id_base_site, + "Site :" + feature.properties.base_site_name + ); + layer.bindPopup(popup); + }; + } + + getSitesFromSiteGroupId(page, params) { + this._sitesGroupService + .getSitesChild(page, LIMIT, params) + .subscribe((data: IPaginated) => { + this.sites = data.items; + this.page = { + count: data.count, + limit: data.limit, + page: data.page - 1, + }; + }); + } + + seeDetails($event) { + this._objService.changeObjectTypeParent(this._siteService.editObjectType()); + this.router.navigate([`sites/${$event.id_base_site}`], { + relativeTo: this._Activatedroute, + }); + } + + onObjChanged($event) { + this.initSite(); + } +} diff --git a/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.css b/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.html b/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.html new file mode 100644 index 000000000..69379ea77 --- /dev/null +++ b/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.html @@ -0,0 +1,4 @@ + +
+ +
\ No newline at end of file diff --git a/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts b/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts new file mode 100644 index 000000000..4138a321b --- /dev/null +++ b/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts @@ -0,0 +1,23 @@ +import { Component, OnInit } from "@angular/core"; +import { EditObjectService } from "../../services/edit-object.service"; +import { FormGroup, FormBuilder } from "@angular/forms"; +import { ISitesGroup } from "../../interfaces/geom"; + +@Component({ + selector: "monitoring-sitesgroups-create", + templateUrl: "./monitoring-sitesgroups-create.component.html", + styleUrls: ["./monitoring-sitesgroups-create.component.css"], +}) +export class MonitoringSitesGroupsCreateComponent implements OnInit { + siteGroup: ISitesGroup; + form: FormGroup; + constructor( + private _editService: EditObjectService, + private _formBuilder: FormBuilder + ) {} + + ngOnInit() { + this._editService.changeDataSub({}); + this.form = this._formBuilder.group({}); + } +} diff --git a/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.css b/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.html b/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.html new file mode 100644 index 000000000..2ddf3aae0 --- /dev/null +++ b/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.html @@ -0,0 +1,6 @@ + +
+ +
\ No newline at end of file diff --git a/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts b/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts new file mode 100644 index 000000000..88473a04f --- /dev/null +++ b/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts @@ -0,0 +1,129 @@ +import { Component, OnInit, Input } from "@angular/core"; +import { SitesGroupService } from "../../services/api-geom.service"; +import { columnNameSiteGroup } from "../../class/monitoring-sites-group"; +import { IPaginated, IPage } from "../../interfaces/page"; +import { Router, ActivatedRoute } from "@angular/router"; +import { columnNameSite } from "../../class/monitoring-site"; +import { ISite, ISitesGroup } from "../../interfaces/geom"; +import { GeoJSONService } from "../../services/geojson.service"; +import { MonitoringGeomComponent } from "../../class/monitoring-geom-component"; +import { setPopup } from "../../functions/popup"; +import { ObjectService } from "../../services/object.service"; +import { FormGroup, FormBuilder } from "@angular/forms"; + +const LIMIT = 10; + +@Component({ + selector: "monitoring-sitesgroups", + templateUrl: "./monitoring-sitesgroups.component.html", + styleUrls: ["./monitoring-sitesgroups.component.css"], +}) +export class MonitoringSitesGroupsComponent + extends MonitoringGeomComponent + implements OnInit +{ + @Input() page: IPage; + @Input() sitesGroups: ISitesGroup[]; + @Input() sitesChild: ISite[]; + @Input() columnNameSiteGroup: typeof columnNameSiteGroup = + columnNameSiteGroup; + @Input() columnNameSite: typeof columnNameSite = columnNameSite; + @Input() sitesGroupsSelected: ISitesGroup; + + // @Input() rows; + @Input() colsname; + @Input() obj; + objectType: string; + objForm: FormGroup; + objInitForm: Object = {}; + // siteGroupEmpty={ + // "comments" :'', + // sites_group_code: string; + // sites_group_description: string; + // sites_group_name: string; + // uuid_sites_group: string; //FIXME: see if OK + // } + + constructor( + private _sites_group_service: SitesGroupService, + public geojsonService: GeoJSONService, + private router: Router, + private _objService: ObjectService, + private _formBuilder: FormBuilder, + private _Activatedroute: ActivatedRoute // private _routingService: RoutingService + ) { + super(); + this.getAllItemsCallback = this.getSitesGroups; + } + + ngOnInit() { + this.initSiteGroup(); + } + + initSiteGroup() { + this._objService.changeObjectTypeParent( + this._sites_group_service.editObjectType() + ); + this._objService.changeObjectType( + this._sites_group_service.addObjectType() + ); + this.getSitesGroups(1); + this.geojsonService.getSitesGroupsGeometries( + this.onEachFeatureSiteGroups() + ); + } + + ngOnDestroy() { + this.geojsonService.removeFeatureGroup( + this.geojsonService.sitesGroupFeatureGroup + ); + } + + onEachFeatureSiteGroups(): Function { + const baseUrl = this.router.url; + return (feature, layer) => { + const popup = setPopup( + baseUrl, + feature.properties.id_sites_group, + "Groupe de site :" + feature.properties.sites_group_name + ); + layer.bindPopup(popup); + }; + } + + getSitesGroups(page = 1, params = {}) { + this._sites_group_service + .get(page, LIMIT, params) + .subscribe((data: IPaginated) => { + this.page = { + count: data.count, + limit: data.limit, + page: data.page - 1, + }; + this.sitesGroups = data.items; + this.colsname = this.columnNameSiteGroup; + // IF prefered observable compare to ngOnChanges uncomment this: + // this._dataTableService.changeColsTable(this.colsname,this.sitesGroups[0]) + }); + } + + seeDetails($event) { + // TODO: routerLink + this._objService.changeObjectTypeParent( + this._sites_group_service.editObjectType() + ); + this.router.navigate([$event.id_sites_group], { + relativeTo: this._Activatedroute, + }); + } + + addSiteGp($event) { + this.router.navigate(["/create"], { + relativeTo: this._Activatedroute, + }); + } + onSelect($event) { + this.geojsonService.selectSitesGroupLayer($event); + } + onObjChanged($event) {} +} diff --git a/frontend/app/functions/popup.ts b/frontend/app/functions/popup.ts new file mode 100644 index 000000000..e6e22b90c --- /dev/null +++ b/frontend/app/functions/popup.ts @@ -0,0 +1,14 @@ +export function setPopup(baseUrl: string, id: number, name: string) { + const url = `#/${baseUrl}/${id}/`; + + const popup = ` +
+

${name}

+ + + +
+ `; + + return popup; +} diff --git a/frontend/app/gnModule.module.ts b/frontend/app/gnModule.module.ts index bb54d438d..5e46a78c2 100644 --- a/frontend/app/gnModule.module.ts +++ b/frontend/app/gnModule.module.ts @@ -15,22 +15,39 @@ import { MonitoringObjectService } from './services/monitoring-object.service'; import { ConfigService } from './services/config.service'; // Component -import { BreadcrumbsComponent } from './components/breadcrumbs/breadcrumbs.component'; -import { ModulesComponent } from './components/modules/modules.component'; -import { MonitoringObjectComponent } from './components/monitoring-object/monitoring-object.component'; -import { DrawFormComponent } from './components/draw-form/draw-form.component'; -import { ModalMsgComponent } from './components/modal-msg/modal-msg.component'; -import { MonitoringMapComponent } from './components/monitoring-map/monitoring-map.component'; -import { MonitoringFormComponent } from './components/monitoring-form/monitoring-form.component'; -import { MonitoringListComponent } from './components/monitoring-lists/monitoring-lists.component'; -import { MonitoringPropertiesComponent } from './components/monitoring-properties/monitoring-properties.component'; -import { MonitoringDatatableComponent } from './components/monitoring-datatable/monitoring-datatable.component'; +import { BreadcrumbsComponent } from "./components/breadcrumbs/breadcrumbs.component"; +import { ModulesComponent } from "./components/modules/modules.component"; +import { MonitoringObjectComponent } from "./components/monitoring-object/monitoring-object.component"; +import { DrawFormComponent } from "./components/draw-form/draw-form.component"; +import { ModalMsgComponent } from "./components/modal-msg/modal-msg.component"; +import { MonitoringMapComponent } from "./components/monitoring-map/monitoring-map.component"; +import { MonitoringFormComponent } from "./components/monitoring-form/monitoring-form.component"; +import { MonitoringListComponent } from "./components/monitoring-lists/monitoring-lists.component"; +import { MonitoringPropertiesComponent } from "./components/monitoring-properties/monitoring-properties.component"; +import { MonitoringDatatableComponent } from "./components/monitoring-datatable/monitoring-datatable.component"; +import { MonitoringDatatableGComponent } from "./components/monitoring-datatable-g/monitoring-datatable-g.component"; -import { MatSlideToggleModule } from '@angular/material/slide-toggle'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { MatSelectModule } from '@angular/material/select'; -import { MatInputModule } from '@angular/material/input'; +import { MatSlideToggleModule } from "@angular/material/slide-toggle"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatAutocompleteModule } from "@angular/material/autocomplete"; +import { MatSelectModule } from "@angular/material/select"; +import { MatInputModule } from "@angular/material/input"; +import { MonitoringSitesGroupsComponent } from "./components/monitoring-sitesgroups/monitoring-sitesgroups.component"; +import { DataTableService } from "./services/data-table.service"; +import { MonitoringPropertiesGComponent } from "./components/monitoring-properties-g/monitoring-properties-g.component"; +import { GeoJSONService } from "./services/geojson.service"; +import { MonitoringSitesComponent } from "./components/monitoring-sites/monitoring-sites.component"; +import { MonitoringMapListComponent } from "./components/monitoring-map-list/monitoring-map-list.component"; +import { MonitoringFormComponentG } from "./components/monitoring-form-g/monitoring-form.component-g"; +import { EditObjectService } from "./services/edit-object.service"; +import { ObjectService } from "./services/object.service"; +import { + SitesGroupService, + SitesService, + ApiGeomService, +} from "./services/api-geom.service"; +import { MonitoringVisitsComponent } from "./components/monitoring-visits/monitoring-visits.component"; +import { MonitoringSitesGroupsCreateComponent } from "./components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component"; // my module routing const routes: Routes = [ @@ -38,9 +55,9 @@ const routes: Routes = [ { path: '', component: ModulesComponent }, /** module */ - { path: 'module/:moduleCode', component: MonitoringObjectComponent }, + { path: "module/:moduleCode", component: MonitoringObjectComponent }, /** create module */ - { path: 'module', component: MonitoringObjectComponent }, + { path: "module", component: MonitoringObjectComponent }, /** object */ { @@ -52,6 +69,27 @@ const routes: Routes = [ path: 'create_object/:moduleCode/:objectType', component: MonitoringObjectComponent, }, + { + path: "sites_group", + component: MonitoringMapListComponent, + children: [ + { + path: "", + component: MonitoringSitesGroupsComponent, + }, + { path: "create", component: MonitoringSitesGroupsCreateComponent }, + { + path: ":id", + // Add new component here + children: [ + { + path: "", + component: MonitoringSitesComponent, + }, + ], + }, + ], + }, ]; @NgModule({ @@ -66,6 +104,13 @@ const routes: Routes = [ MonitoringListComponent, MonitoringPropertiesComponent, MonitoringDatatableComponent, + MonitoringMapListComponent, + MonitoringSitesGroupsComponent, + MonitoringSitesComponent, + MonitoringDatatableGComponent, + MonitoringPropertiesGComponent, + MonitoringFormComponentG, + MonitoringSitesGroupsCreateComponent, ], imports: [ GN2CommonModule, @@ -89,6 +134,13 @@ const routes: Routes = [ DataUtilsService, ConfigService, MonitoringObjectService, + DataTableService, + SitesGroupService, + SitesService, + GeoJSONService, + EditObjectService, + ObjectService, + ApiGeomService, ], bootstrap: [ModulesComponent], schemas: [ diff --git a/frontend/app/interfaces/column.ts b/frontend/app/interfaces/column.ts new file mode 100644 index 000000000..82f803037 --- /dev/null +++ b/frontend/app/interfaces/column.ts @@ -0,0 +1,5 @@ +export interface IColumn { + name: string; + prop: string; + description?: string; +} diff --git a/frontend/app/interfaces/form.ts b/frontend/app/interfaces/form.ts new file mode 100644 index 000000000..e57edf803 --- /dev/null +++ b/frontend/app/interfaces/form.ts @@ -0,0 +1,7 @@ +import { JsonData } from "../types/jsondata"; +import { ISite, ISitesGroup } from "./geom"; + +export interface IDataForm extends JsonData, ISitesGroup, ISite { + patch_update:number + } + \ No newline at end of file diff --git a/frontend/app/interfaces/geom.ts b/frontend/app/interfaces/geom.ts new file mode 100644 index 000000000..9d43b5cdf --- /dev/null +++ b/frontend/app/interfaces/geom.ts @@ -0,0 +1,50 @@ +import { GeoJSON } from "geojson"; +import { Observable } from "rxjs"; +import { JsonData } from "../types/jsondata"; +import { Resp } from "../types/response"; +import { IPaginated } from "./page"; + +export interface IGeomObject { + data: JsonData; + geometry: GeoJSON.Geometry; +} + +export interface ISitesGroup extends IGeomObject { + pk: number; + comments?: string; + id_sites_group: number; + nb_sites: number; + nb_visits: number; + sites_group_code: string; + sites_group_description: string; + sites_group_name: string; + uuid_sites_group: string; //FIXME: see if OK +} + +export interface ISite extends IGeomObject { + altitude_max: number; + altitude_min: number; + base_site_code: string; + base_site_description?: string; + base_site_name: string; + first_use_date: string; + id_base_site: number; + id_nomenclature_type_site?: number; + last_visit?: Date; + meta_create_date: Date; + meta_update_date: Date; + nb_visits: number; + uuid_base_site: string; +} + +export interface IGeomService { + get( + limit: number, + page: number, + params: JsonData + ): Observable>; + get_geometries(params: JsonData): Observable; + create(postdata: IGeomObject): Observable; + patch(id: number, updatedData: IGeomObject): Observable; + // delete(obj: IGeomObject) +} diff --git a/frontend/app/interfaces/page.ts b/frontend/app/interfaces/page.ts new file mode 100644 index 000000000..cca644c5f --- /dev/null +++ b/frontend/app/interfaces/page.ts @@ -0,0 +1,17 @@ +export interface IPage { + count: number; + limit: number; + page: number; +} + +export interface IPaginated extends IPage { + items: T[]; +} + +// PageInfo = object given by ngx-datatable +export interface PageInfo { + offset: number; + pageSize: number; + limit: number; + count: number; +} diff --git a/frontend/app/interfaces/response.ts b/frontend/app/interfaces/response.ts new file mode 100644 index 000000000..317f5c94d --- /dev/null +++ b/frontend/app/interfaces/response.ts @@ -0,0 +1,6 @@ +import { JsonData } from "../types/jsondata"; +export interface ResponseUpdated { + message: string; + payload: JsonData; + status_code: number; +} diff --git a/frontend/app/services/api-geom.service.ts b/frontend/app/services/api-geom.service.ts new file mode 100644 index 000000000..296913ebf --- /dev/null +++ b/frontend/app/services/api-geom.service.ts @@ -0,0 +1,121 @@ +import { Injectable } from "@angular/core"; +import { Observable } from "rxjs"; +import { GeoJSON } from "geojson"; + +import { CacheService } from "./cache.service"; +import { IGeomService, ISitesGroup, ISite } from "../interfaces/geom"; +import { IPaginated } from "../interfaces/page"; +import { JsonData } from "../types/jsondata"; +import { Resp } from "../types/response"; + +export enum endPoints { + sites_groups = "sites_groups", + sites = "sites", +} + +@Injectable() +export class ApiGeomService implements IGeomService { + public objectType: endPoints = endPoints.sites_groups; + + constructor(protected _cacheService: CacheService) { + this.init(); + } + + init() { + this.objectType = endPoints.sites_groups; + } + get( + page: number = 1, + limit: number = 10, + params: JsonData = {} + ): Observable> { + return this._cacheService.request< + Observable> + >("get", this.objectType, { + queryParams: { page, limit, ...params }, + }); + } + + getById(id: number): Observable { + return this._cacheService.request>( + "get", + `${this.objectType}/${id}` + ); + } + + get_geometries(params: JsonData = {}): Observable { + return this._cacheService.request>( + "get", + `${this.objectType}/geometries`, + { + queryParams: { ...params }, + } + ); + } + + patch(id: number, updatedData: ISitesGroup | ISite): Observable { + return this._cacheService.request("patch", `${this.objectType}/${id}`, { + postData: updatedData, + }); + } + + create( postData: ISitesGroup | ISite): Observable { + return this._cacheService.request("post", `${this.objectType}`, { + postData: postData, + }); + } + + delete(id: number): Observable { + return this._cacheService.request("delete", `${this.objectType}/${id}`); + } + +} + +@Injectable() +export class SitesGroupService extends ApiGeomService { + constructor(_cacheService: CacheService) { + super(_cacheService); + } + init(): void { + this.objectType = endPoints.sites_groups; + } + + getSitesChild( + page: number = 1, + limit: number = 10, + params: JsonData = {} + ): Observable> { + return this._cacheService.request>>( + "get", + `sites`, + { + queryParams: { page, limit, ...params }, + } + ); + } + + addObjectType(): string { + return "un nouveau groupe de site"; + } + + editObjectType(): string { + return "le groupe de site"; + } +} + +@Injectable() +export class SitesService extends ApiGeomService { + constructor(_cacheService: CacheService) { + super(_cacheService); + } + init(): void { + this.objectType = endPoints.sites; + } + addObjectType(): string { + return " un nouveau site"; + } + + editObjectType(): string { + return "le site"; + } +} diff --git a/frontend/app/services/cache.service.ts b/frontend/app/services/cache.service.ts index f8d0ed5e7..952d62d47 100644 --- a/frontend/app/services/cache.service.ts +++ b/frontend/app/services/cache.service.ts @@ -2,8 +2,8 @@ import { ObserversComponent } from '@geonature_common/form/observers/observers.c import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { Observable, of, Subject } from 'rxjs'; -import { mergeMap, concatMap } from 'rxjs/operators'; +import { Observable, of, Subject } from "rxjs"; +import { mergeMap} from "rxjs/operators"; import { ConfigService } from './config.service'; @@ -30,10 +30,14 @@ export class CacheService { * @param urlRelative url relative de la route * @param data post data (optionnel) */ - request(requestType: string, urlRelative: string, { postData = {}, queryParams = {} } = {}) { + request>( + requestType: string, + urlRelative: string, + { postData = {}, queryParams = {} } = {} + ): Return { // verification de requestType - if (!['get', 'post', 'patch', 'delete'].includes(requestType)) { - return of(null); + if (!["get", "post", "patch", "delete"].includes(requestType)) { + throw console.error("Request must be get, post, patch or delete"); } const url_params = Object.keys(queryParams).length @@ -50,7 +54,7 @@ export class CacheService { const url = this._config.backendModuleUrl() + '/' + urlRelative + url_params; // requete - return this._http[requestType](url, postData); + return this._http[requestType](url, postData); } /** Cache diff --git a/frontend/app/services/config.service.ts b/frontend/app/services/config.service.ts index c85ef94fd..15f5c4ec9 100644 --- a/frontend/app/services/config.service.ts +++ b/frontend/app/services/config.service.ts @@ -141,7 +141,6 @@ export class ConfigService { moduleCode = moduleCode || 'generic'; const configObject = this._config[moduleCode][objectType]; - // gerer quand les paramètres ont un fonction comme valeur for (const typeSchema of ['generic', 'specific']) { diff --git a/frontend/app/services/data-table.service.ts b/frontend/app/services/data-table.service.ts new file mode 100644 index 000000000..8a76099ee --- /dev/null +++ b/frontend/app/services/data-table.service.ts @@ -0,0 +1,90 @@ +import { Injectable } from "@angular/core"; +import { IColumn } from "../interfaces/column"; +import { BehaviorSubject} from "rxjs"; +interface ItemObjectTable { + id: number | null; + selected: boolean; + visible: boolean; + current: boolean; +} + +type ItemsObjectTable = { [key: string]: ItemObjectTable }; + +@Injectable() +export class DataTableService { + obj: ItemsObjectTable; + objectsStatus: ItemsObjectTable; + rowStatus: ItemObjectTable; + idObj: number; + + + // IF prefered observable compare to ngOnChanges uncomment this: + // dataCol:IColumn[] =[{prop:"",name:"",description:""}] + // private dataCols = new BehaviorSubject(this.dataCol); + // currentCols = this.dataCols.asObservable(); + + + constructor() {} + + // IF prefered observable compare to ngOnChanges uncomment this: + // changeColsTable(newCols:IColumn[],newRows){ + // const arr = Object.keys(newCols); + // const allColumn: IColumn[] = arr + // .filter((item) => Object.keys(newRows).includes(item)) + // .map((elm) => ({ + // name: newCols[elm], + // prop: elm, + // description: elm, + // })); + // this.dataCols.next(allColumn) + // } + + colsTable(colName:IColumn[], dataTable): IColumn[] { + const arr = Object.keys(colName); + const allColumn: IColumn[] = arr + .filter((item) => Object.keys(dataTable).includes(item)) + .map((elm) => ({ + name: colName[elm], + prop: elm, + description: elm, + })); + return allColumn; + } + + initObjectsStatus(obj, key) { + const objectsStatus = {}; + // for (const childrenType of Object.keys(this.obj.children)) { + objectsStatus[key] = obj.map((groupSite) => { + return { + id: groupSite.id_sites_group, + selected: false, + visible: true, + current: false, + }; + }); + // } + + // init site status + if (this.idObj) { + objectsStatus[key] = []; + obj.features.forEach((f) => { + // determination du site courrant + let cur = false; + if (f.properties.id_sites_group == this.idObj) { + cur = true; + } + + objectsStatus[key].push({ + id: f.properties.id_sites_group, + selected: false, + visible: true, + current: cur, + }); + }); + } + + this.objectsStatus = objectsStatus; + this.rowStatus = this.objectsStatus[key]; + return [this.objectsStatus, this.rowStatus]; + } +} diff --git a/frontend/app/services/edit-object.service.ts b/frontend/app/services/edit-object.service.ts new file mode 100644 index 000000000..fc106678c --- /dev/null +++ b/frontend/app/services/edit-object.service.ts @@ -0,0 +1,58 @@ +import { Injectable } from "@angular/core"; +import { BehaviorSubject, Observable, of, forkJoin } from "rxjs"; +import { concatMap } from "rxjs/operators"; + +import { JsonData } from "../types/jsondata"; + +import { Utils } from "../utils/utils"; +import { MonitoringObjectService } from "./monitoring-object.service"; + +@Injectable() +export class EditObjectService { + data: JsonData = {}; + private dataSub = new BehaviorSubject(this.data); + currentData = this.dataSub.asObservable(); + properties: JsonData; + moduleCode:string; + objecType:string; + + constructor( + private _objService:MonitoringObjectService + ) {} + + changeDataSub(newDat: JsonData) { + this.properties = newDat; + newDat.moduleCode = "generic"; + newDat.objectType = "sites_group"; + this.moduleCode= "generic"; + this.objecType= "sites_group" + this.dataSub.next(newDat) + + } + + + + formValues(obj): Observable { + const properties = Utils.copy(this.properties); + const observables = {}; + const schema = obj[this.moduleCode]; + for (const attribut_name of Object.keys(schema)) { + const elem = schema[attribut_name]; + if (!elem.type_widget) { + continue; + } + observables[attribut_name] = this._objService.toForm(elem, properties[attribut_name]); + } + + return forkJoin(observables).pipe( + concatMap((formValues_in) => { + const formValues = Utils.copy(formValues_in); + // geometry + // if (this.config["geometry_type"]) { + // formValues["geometry"] = this.geometry; // copy??? + // } + return of(formValues); + }) + ); + } +} diff --git a/frontend/app/services/geojson.service.ts b/frontend/app/services/geojson.service.ts new file mode 100644 index 000000000..22a26e980 --- /dev/null +++ b/frontend/app/services/geojson.service.ts @@ -0,0 +1,112 @@ +import { Injectable } from "@angular/core"; +import * as L from "leaflet"; +import { Observable } from "rxjs"; +import { map } from "rxjs/operators"; +import { GeoJSON } from "geojson"; +import { MapService } from "@geonature_common/map/map.service"; + +import { SitesService,SitesGroupService } from "./api-geom.service"; + + +// This service will be used for sites and sites groups + +const siteGroupStyle = { + fillColor: "#800080", + fillOpacity: 0.5, + color: "#800080", + opacity: 0.8, + weight: 2, + fill: true, +}; + +@Injectable() +export class GeoJSONService { + geojsonSitesGroups: GeoJSON.FeatureCollection; + geojsonSites: GeoJSON.FeatureCollection; + sitesGroupFeatureGroup: L.FeatureGroup; + sitesFeatureGroup: L.FeatureGroup; + + constructor( + private _sites_group_service: SitesGroupService, + private _sites_service: SitesService, + private _mapService: MapService + ) {} + + getSitesGroupsGeometries(onEachFeature: Function, params = {}) { + this._sites_group_service + .get_geometries(params) + .subscribe((data: GeoJSON.FeatureCollection) => { + this.geojsonSitesGroups = data; + this.sitesGroupFeatureGroup = this.setMapData( + data, + onEachFeature, + siteGroupStyle + ); + }); + } + + getSitesGroupsChildGeometries(onEachFeature: Function, params = {}) { + this._sites_service + .get_geometries(params) + .subscribe((data: GeoJSON.FeatureCollection) => { + //this.removeFeatureGroup(this.sitesFeatureGroup); + this.sitesFeatureGroup = this.setMapData(data, onEachFeature); + }); + } + + setMapData( + geojson: GeoJSON.Geometry | GeoJSON.FeatureCollection, + onEachFeature: Function, + style? + ) { + const map = this._mapService.getMap(); + const layer: L.Layer = this._mapService.createGeojson( + geojson, + false, + onEachFeature, + style + ); + const featureGroup = new L.FeatureGroup(); + this._mapService.map.addLayer(featureGroup); + featureGroup.addLayer(layer); + map.fitBounds(featureGroup.getBounds()); + return featureGroup; + } + + removeFeatureGroup(feature: L.FeatureGroup) { + if (feature) { + this._mapService.map.removeLayer(feature); + } + } + + onEachFeature() {} + + filterSitesGroups(siteGroupId: number) { + if (this.geojsonSitesGroups !== undefined) { + const features = this.geojsonSitesGroups.features.filter( + (feature) => feature.properties.id_sites_group == siteGroupId + ); + this.geojsonSitesGroups.features = features; + this.removeFeatureGroup(this.sitesGroupFeatureGroup); + this.setMapData( + this.geojsonSitesGroups, + this.onEachFeature, + siteGroupStyle + ); + } + } + + selectSitesGroupLayer(id: number) { + this.sitesGroupFeatureGroup.eachLayer((layer) => { + if (layer instanceof L.GeoJSON) { + layer.eachLayer((sublayer: L.GeoJSON) => { + const feature = sublayer.feature as GeoJSON.Feature; + if (feature.properties["id_sites_group"] == id) { + sublayer.openPopup(); + return; + } + }); + } + }); + } +} diff --git a/frontend/app/services/object.service.ts b/frontend/app/services/object.service.ts new file mode 100644 index 000000000..c570f506c --- /dev/null +++ b/frontend/app/services/object.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from "@angular/core"; +import { BehaviorSubject } from "rxjs"; + +@Injectable() +export class ObjectService { + objectType: string = ""; + private dataObjType = new BehaviorSubject(this.objectType); + currentObjectType = this.dataObjType.asObservable(); + + objectTypeParent: string = ""; + private dataObjTypeParent = new BehaviorSubject(this.objectTypeParent); + currentObjectTypeParent = this.dataObjTypeParent.asObservable(); + + constructor() {} + + changeObjectType(newObjType: string) { + this.dataObjType.next(newObjType); + } + + changeObjectTypeParent(newObjType: string) { + this.dataObjTypeParent.next(newObjType); + } +} diff --git a/frontend/app/types/jsondata.ts b/frontend/app/types/jsondata.ts new file mode 100644 index 000000000..fa13d6798 --- /dev/null +++ b/frontend/app/types/jsondata.ts @@ -0,0 +1 @@ +export type JsonData = { [key: string]: any }; diff --git a/frontend/app/types/response.ts b/frontend/app/types/response.ts new file mode 100644 index 000000000..dd26b3360 --- /dev/null +++ b/frontend/app/types/response.ts @@ -0,0 +1,4 @@ +import { ISite, ISitesGroup } from "../interfaces/geom"; +import { ResponseUpdated } from "../interfaces/response"; + +export type Resp = ResponseUpdated | ISite | ISitesGroup;