diff --git a/angular.json b/angular.json index 1bd01a104..e43d5a112 100644 --- a/angular.json +++ b/angular.json @@ -26,7 +26,9 @@ "styles": [ "app/app.css", "node_modules/bootstrap/dist/css/bootstrap.min.css", - "node_modules/nouislider/dist/nouislider.css" + "node_modules/nouislider/dist/nouislider.css", + "node_modules/ag-grid-community/styles/ag-grid.css", + "node_modules/ag-grid-community/styles/ag-theme-alpine.css" ], "scripts": ["node_modules/mathjax/es5/tex-mml-chtml.js"] }, diff --git a/app/app-upgraded-providers.ts b/app/app-upgraded-providers.ts index a5abb51e3..6f84aca08 100644 --- a/app/app-upgraded-providers.ts +++ b/app/app-upgraded-providers.ts @@ -8,7 +8,7 @@ import {kommonitorDataGridHelperService} from 'util/genericServices/kommonitorDa import {kommonitorDiagramHelperService} from 'util/genericServices/kommonitorDiagramHelperService/kommonitor-diagram-helper-service.module'; import {kommonitorFilterHelperService} from 'util/genericServices/kommonitorFilterHelperService/kommonitor-filter-helper-service.module'; import {kommonitorImporterHelperService} from 'util/genericServices/kommonitorImporterHelperService/kommonitor-importer-helper-service.module'; -import {kommonitorKeycloackHelperService} from 'util/genericServices/kommonitorKeycloakHelperService/kommonitor-keycloak-helper-service.module' +import {kommonitorKeycloakHelperService} from 'util/genericServices/kommonitorKeycloakHelperService/kommonitor-keycloak-helper-service.module' import {kommonitorMultistepFormHelperService} from 'util/genericServices/kommonitorMultiStepFormHelperService/kommonitor-multi-step-form-helper-service.module' import {kommonitorScriptHelperService} from'util/genericServices/kommonitorScriptHelperService/kommonitor-script-helper-service.module'; import {kommonitorShareHelperService} from 'util/genericServices/kommonitorShareHelperService/kommonitor-share-helper-service.module' @@ -91,15 +91,26 @@ export const ajskommonitorFilterHelperServiceProvider: any = { useFactory:kommonitorFilterHelperServiceFactory, }; -//keycloack helper - export function kommonitorKeycloackHelperServiceFactory (injector:any){ - return injector.get('kommonitorKeycloackHelperService') +//importer helper +export function kommonitorImporterHelperServiceFactory (injector:any){ + return injector.get('kommonitorImporterHelperService') } -export const ajskommonitorKeycloackHelperServiceProvider: any = { +export const ajskommonitorImporterHelperServiceProvider: any = { deps: ['$injector'], - provide: 'kommonitorKeycloackHelperService', - useFactory:kommonitorKeycloackHelperServiceFactory, + provide: 'kommonitorImporterHelperService', + useFactory:kommonitorImporterHelperServiceFactory, + }; + +//keycloak helper + export function kommonitorKeycloakHelperServiceFactory (injector:any){ + return injector.get('kommonitorKeycloakHelperService') +} + +export const ajskommonitorKeycloakHelperServiceProvider: any = { + deps: ['$injector'], + provide: 'kommonitorKeycloakHelperService', + useFactory:kommonitorKeycloakHelperServiceFactory, }; //multistep form @@ -291,7 +302,7 @@ export const ajskommonitorFavServiceProvider: any = { ajskommonitorDataGridHelperServiceProvider, ajskommonitorDiagramHelperServiceProvider, ajskommonitorFilterHelperServiceProvider, - ajskommonitorKeycloackHelperServiceProvider, + ajskommonitorKeycloakHelperServiceProvider, ajskommonitorMultiStepFormHelperServiceProvider, ajskommonitorScriptHelperServiceProvider, ajskommonitorShareHelperServiceProvider, diff --git a/app/app.css b/app/app.css index 15cced841..68f682af2 100644 --- a/app/app.css +++ b/app/app.css @@ -1649,6 +1649,87 @@ table, th, td { } } +/* Custom width for Spatial Unit Add modal (ng-bootstrap modalDialogClass) */ +@media (min-width: 768px){ + .modal-dialog.spatial-unit-add-modal { + /* Bootstrap 5: use CSS var plus hard width to be robust */ + --bs-modal-width: 95vw; + max-width: 95vw !important; + width: 95vw !important; + } +} + +/* Fallback for older ng-bootstrap/Bootstrap versions: target modal dialog within windowClass */ +@media (min-width: 768px){ + /* High-specificity, robust override */ + body .modal-holder.spatial-unit-add-modal-window .modal-dialog, + body .spatial-unit-add-modal-window .modal-dialog, + body .modal.spatial-unit-add-modal-window .modal-dialog { + --bs-modal-width: 95vw; + max-width: 95vw !important; + width: 95vw !important; + } +} + +/* Custom width for Role Add and related role modals (use same sizing as spatial unit add) */ +@media (min-width: 768px){ + .modal-dialog.role-add-modal { + --bs-modal-width: 95vw; + max-width: 95vw !important; + width: 95vw !important; + } +} + +@media (min-width: 768px){ + body .modal-holder.role-add-modal-window .modal-dialog, + body .role-add-modal-window .modal-dialog, + body .modal.role-add-modal-window .modal-dialog { + --bs-modal-width: 95vw; + max-width: 95vw !important; + width: 95vw !important; + } +} + +/* Custom width for Script Add modal (align with spatial unit modal) */ +@media (min-width: 768px){ + .modal-dialog.script-add-modal { + --bs-modal-width: 95vw; + max-width: 95vw !important; + width: 95vw !important; + } +} + +/* Fallback targeting windowClass container for Script Add modal */ +@media (min-width: 768px){ + body .modal-holder.script-add-modal-window .modal-dialog, + body .script-add-modal-window .modal-dialog, + body .modal.script-add-modal-window .modal-dialog { + --bs-modal-width: 95vw; + max-width: 95vw !important; + width: 95vw !important; + } +} + +/* Custom width for Script Delete modal */ +@media (min-width: 768px){ + .modal-dialog.script-delete-modal { + --bs-modal-width: 95vw; + max-width: 95vw !important; + width: 95vw !important; + } +} + +/* Fallback targeting windowClass container for Script Delete modal */ +@media (min-width: 768px){ + body .modal-holder.script-delete-modal-window .modal-dialog, + body .script-delete-modal-window .modal-dialog, + body .modal.script-delete-modal-window .modal-dialog { + --bs-modal-width: 95vw; + max-width: 95vw !important; + width: 95vw !important; + } +} + select { overflow: auto; } diff --git a/app/app.module.ts b/app/app.module.ts index c4952c82d..11ae75b2d 100644 --- a/app/app.module.ts +++ b/app/app.module.ts @@ -4,13 +4,14 @@ import { DoBootstrap, NgModule, Version, inject, Input, Inject, CUSTOM_ELEMENTS_ import { BrowserModule } from '@angular/platform-browser'; import { UpgradeModule } from '@angular/upgrade/static'; import { downgradeComponent } from '@angular/upgrade/static'; +import { AgGridAngular } from 'ag-grid-angular'; import $ from 'jquery'; import Keycloak from 'keycloak-js'; import angular from "angular"; import { Router, RouterModule, Routes } from '@angular/router'; -import { HashLocationStrategy, LocationStrategy } from '@angular/common'; +import { HashLocationStrategy, LocationStrategy, CommonModule } from '@angular/common'; import { ajskommonitorCacheHelperServiceProvider, @@ -20,7 +21,8 @@ import { ajskommonitorDataGridHelperServiceProvider, ajskommonitorDiagramHelperServiceProvider, ajskommonitorFilterHelperServiceProvider, - ajskommonitorKeycloackHelperServiceProvider, + ajskommonitorImporterHelperServiceProvider, + ajskommonitorKeycloakHelperServiceProvider, ajskommonitorMultiStepFormHelperServiceProvider, ajskommonitorSingleFeatureMapServiceProvider, ajskommonitorElementVisibilityHelperServiceProvider, @@ -36,9 +38,10 @@ import { ajskommonitorGlobalFilterHelperServiceProvider, ajskommonitorFavServiceProvider} from 'app-upgraded-providers'; import { KommonitorLegendComponent } from 'components/ngComponents/userInterface/kommonitorLegend/kommonitor-legend.component'; -import { NgbCalendar, NgbDatepickerModule, NgbDateStruct, NgbAccordionModule, NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbCalendar, NgbDatepickerModule, NgbDateStruct, NgbAccordionModule, NgbModule, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { JsonPipe } from '@angular/common'; +import { DragDropModule } from '@angular/cdk/drag-drop'; import { KommonitorClassificationComponent } from './components/ngComponents/userInterface/kommonitorClassification/kommonitor-classification.component'; import { KommonitorDataSetupComponent } from './components/ngComponents/userInterface/sidebar/kommonitorDataSetup/kommonitor-data-setup.component'; import { SidebarComponent } from './components/ngComponents/userInterface/sidebar/sidebar.component'; @@ -47,6 +50,8 @@ import { KommonitorFilterComponent } from './components/ngComponents/userInterfa import { KommonitorMapComponent } from './components/ngComponents/userInterface/kommonitorMap/kommonitor-map.component'; import { DualListBoxComponent } from './components/ngComponents/customElements/dual-list-box/dual-list-box.component'; import { KommonitorBalanceComponent } from './components/ngComponents/userInterface/sidebar/kommonitorBalance/kommonitor-balance.component'; +import { KmDatePickerComponent } from './components/ngComponents/customElements/date-picker/km-date-picker.component'; +import { KmColorPickerComponent } from './components/ngComponents/customElements/color-picker/km-color-picker.component'; import { NouisliderModule } from 'ng2-nouislider'; import { KommonitorDiagramsComponent } from './components/ngComponents/userInterface/sidebar/kommonitorDiagrams/kommonitor-diagrams.component'; import { UserInterfaceComponent } from './components/ngComponents/userInterface/user-interface.component'; @@ -68,12 +73,44 @@ import { AuthInterceptor } from 'util/interceptors/auth.interceptor'; import { IndicatorFavFilter } from 'pipes/indicator-fav-filter.pipe'; import { GeoFavFilter } from 'pipes/georesources-fav-filter.pipe'; import { GeoFavItemFilter } from 'pipes/georesources-fav-item-filter.pipe'; +import { FilterPipe } from 'pipes/filter.pipe'; +import { OrderByPipe } from 'pipes/order-by.pipe'; import { AdminAppConfigComponent } from './components/ngComponents/admin/adminConfig/adminAppConfig/admin-app-config.component'; import { AdminControlsConfigComponent } from './components/ngComponents/admin/adminConfig/adminControlsConfig/admin-controls-config.component'; import { AdminRoleExplanationComponent } from './components/ngComponents/admin/adminRoleExplanation/admin-role-explanation.component'; import { AdminDashboardManagementComponent } from './components/ngComponents/admin/adminDashboardManagement/admin-dashboard-management.component'; - +import { AdminRoleManagementComponent } from './components/ngComponents/admin/adminRoleManagement/admin-role-management.component'; +import { RoleAddModalComponent } from './components/ngComponents/admin/adminRoleManagement/roleAddModal/role-add-modal.component'; +import { AdminSpatialUnitsManagementComponent } from './components/ngComponents/admin/adminSpatialUnitsManagement/admin-spatial-units-management.component'; +import { SpatialUnitAddModalComponent } from './components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitAddModal/spatial-unit-add-modal.component'; +import { SpatialUnitEditMetadataModalComponent } from './components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditMetadataModal/spatial-unit-edit-metadata-modal.component'; +import { SpatialUnitEditFeaturesModalComponent } from './components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditFeaturesModal/spatial-unit-edit-features-modal.component'; +import { SpatialUnitEditUserRolesModalComponent } from './components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditUserRolesModal/spatial-unit-edit-user-roles-modal.component'; +import { SpatialUnitDeleteModalComponent } from './components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitDeleteModal/spatial-unit-delete-modal.component'; +import { AdminIndicatorsManagementComponent } from './components/ngComponents/admin/adminIndicatorsManagement/admin-indicators-management.component'; +import { IndicatorAddModalComponent } from './components/ngComponents/admin/adminIndicatorsManagement/indicatorAddModal/indicator-add-modal.component'; +import { IndicatorEditMetadataModalComponent } from './components/ngComponents/admin/adminIndicatorsManagement/indicatorEditMetadataModal/indicator-edit-metadata-modal.component'; +import { IndicatorEditFeaturesModalComponent } from './components/ngComponents/admin/adminIndicatorsManagement/indicatorEditFeaturesModal/indicator-edit-features-modal.component'; +import { IndicatorEditIndicatorSpatialUnitRolesModalComponent } from './components/ngComponents/admin/adminIndicatorsManagement/indicatorEditIndicatorSpatialUnitRolesModal/indicator-edit-indicator-spatial-unit-roles-modal.component'; +import { IndicatorDeleteModalComponent } from './components/ngComponents/admin/adminIndicatorsManagement/indicatorDeleteModal/indicator-delete-modal.component'; +import { IndicatorBatchUpdateModalComponent } from './components/ngComponents/admin/adminIndicatorsManagement/indicatorBatchUpdateModal/indicator-batch-update-modal.component'; +import { AdminGeoresourcesManagementComponent } from './components/ngComponents/admin/adminGeoresourcesManagement/admin-georesources-management.component'; +import { AdminScriptExecutionComponent } from './components/ngComponents/admin/adminScriptExecution/admin-script-execution.component'; +import { AdminScriptManagementComponent } from './components/ngComponents/admin/adminScriptManagement/admin-script-management.component'; +import { ScriptAddModalComponent } from './components/ngComponents/admin/adminScriptManagement/scriptAddModal/script-add-modal.component'; +import { ScriptDeleteModalComponent } from './components/ngComponents/admin/adminScriptManagement/scriptDeleteModal/script-delete-modal.component'; +import { GeoresourceAddModalComponent } from './components/ngComponents/admin/adminGeoresourcesManagement/georesourceAddModal/georesource-add-modal.component'; +import { GeoresourceBatchUpdateModalComponent } from './components/ngComponents/admin/adminGeoresourcesManagement/georesourceBatchUpdateModal/georesource-batch-update-modal.component'; +import { GeoresourceEditMetadataModalComponent } from './components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditMetadataModal/georesource-edit-metadata-modal.component'; +import { GeoresourceEditFeaturesModalComponent } from './components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditFeaturesModal/georesource-edit-features-modal.component'; +import { GeoresourceEditUserRolesModalComponent } from './components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditUserRolesModal/georesource-edit-user-roles-modal.component'; +import { GeoresourceDeleteModalComponent } from './components/ngComponents/admin/adminGeoresourcesManagement/georesourceDeleteModal/georesource-delete-modal.component'; +import { RoleDeleteModalComponent } from './components/ngComponents/admin/adminRoleManagement/roleDeleteModal/role-delete-modal.component'; +import { RoleEditMetadataModalComponent } from './components/ngComponents/admin/adminRoleManagement/roleEditMetadataModal/role-edit-metadata-modal.component'; +import { RoleEditGroupRightsModalComponent } from './components/ngComponents/admin/adminRoleManagement/roleEditGroupRightsModal/role-edit-group-rights-modal.component'; + +import { ColorSketchModule } from 'ngx-color/sketch'; // currently the AngularJS routing is still used as part of kommonitorClient module const routes: Routes = []; @@ -83,6 +120,7 @@ declare var MathJax; @NgModule({ imports: [ BrowserModule, + CommonModule, UpgradeModule, RouterModule.forRoot(routes , { useHash: true }), NgbDatepickerModule, @@ -93,20 +131,27 @@ declare var MathJax; JsonPipe, NouisliderModule, NgbCollapseModule, - DualListBoxComponent + NgbModalModule, + DragDropModule, + DualListBoxComponent, + AgGridAngular, + ColorSketchModule, + KmDatePickerComponent, + KmColorPickerComponent ], providers:[ {provide: LocationStrategy, useClass: HashLocationStrategy}, ajskommonitorCacheHelperServiceProvider, ajskommonitorBatchUpdateHelperServiceProvider, ajskommonitorConfigStorageServiceProvider, - ajskommonitorKeycloackHelperServiceProvider, + ajskommonitorKeycloakHelperServiceProvider, ajskommonitorMultiStepFormHelperServiceProvider, ajskommonitorDataExchangeServiceeProvider, ajskommonitorDataGridHelperServiceProvider, ajskommonitorSingleFeatureMapServiceProvider, ajskommonitorDiagramHelperServiceProvider, ajskommonitorFilterHelperServiceProvider, + ajskommonitorImporterHelperServiceProvider, ajskommonitorElementVisibilityHelperServiceProvider, ajskommonitorShareHelperServiceProvider, ajskommonitorVisualStyleHelperServiceProvider, @@ -119,7 +164,6 @@ declare var MathJax; ajskommonitorScriptHelperServiceProvider, ajskommonitorGlobalFilterHelperServiceProvider, ajskommonitorFavServiceProvider, - NgbModule, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, @@ -145,6 +189,8 @@ declare var MathJax; IndicatorFavFilter, GeoFavFilter, GeoFavItemFilter, + FilterPipe, + OrderByPipe, BaseIndicatorOfComputedIndicatorFilter, BaseIndicatorOfHeadlineIndicatorFilter, RegressionDiagramComponent, @@ -155,7 +201,36 @@ declare var MathJax; AdminAppConfigComponent, AdminControlsConfigComponent, AdminRoleExplanationComponent, - AdminDashboardManagementComponent + AdminDashboardManagementComponent, + AdminRoleManagementComponent, + RoleAddModalComponent, + AdminSpatialUnitsManagementComponent, + SpatialUnitAddModalComponent, + SpatialUnitEditMetadataModalComponent, + SpatialUnitEditFeaturesModalComponent, + SpatialUnitEditUserRolesModalComponent, + SpatialUnitDeleteModalComponent, + AdminIndicatorsManagementComponent, + IndicatorAddModalComponent, + IndicatorEditMetadataModalComponent, + IndicatorEditFeaturesModalComponent, + IndicatorEditIndicatorSpatialUnitRolesModalComponent, + IndicatorDeleteModalComponent, + IndicatorBatchUpdateModalComponent, + AdminGeoresourcesManagementComponent, + GeoresourceAddModalComponent, + GeoresourceBatchUpdateModalComponent, + GeoresourceEditMetadataModalComponent, + GeoresourceEditFeaturesModalComponent, + GeoresourceEditUserRolesModalComponent, + GeoresourceDeleteModalComponent, + AdminScriptManagementComponent, + RoleDeleteModalComponent, + RoleEditMetadataModalComponent, + RoleEditGroupRightsModalComponent, + AdminScriptExecutionComponent, + ScriptAddModalComponent, + ScriptDeleteModalComponent ], schemas: [ CUSTOM_ELEMENTS_SCHEMA @@ -251,6 +326,101 @@ export class AppModule implements DoBootstrap { component: AdminDashboardManagementComponent }) as angular.IDirectiveFactory); + angular.module('kommonitorAdmin') + .directive('adminRoleManagementNew', downgradeComponent({ + component: AdminRoleManagementComponent + }) as angular.IDirectiveFactory); + + angular.module('kommonitorAdmin') + .directive('adminSpatialUnitsManagementNew', downgradeComponent({ + component: AdminSpatialUnitsManagementComponent + }) as angular.IDirectiveFactory); + + angular.module('kommonitorAdmin') + .directive('spatialUnitEditMetadataModalNew', downgradeComponent({ + component: SpatialUnitEditMetadataModalComponent + }) as angular.IDirectiveFactory); + + angular.module('kommonitorAdmin') + .directive('spatialUnitEditFeaturesModalNew', downgradeComponent({ + component: SpatialUnitEditFeaturesModalComponent + }) as angular.IDirectiveFactory); + + angular.module('kommonitorAdmin') + .directive('spatialUnitEditUserRolesModalNew', downgradeComponent({ + component: SpatialUnitEditUserRolesModalComponent + }) as angular.IDirectiveFactory); + + angular.module('kommonitorAdmin') + .directive('spatialUnitDeleteModalNew', downgradeComponent({ + component: SpatialUnitDeleteModalComponent + }) as angular.IDirectiveFactory); + + angular.module('kommonitorAdmin') + .directive('adminIndicatorsManagementNew', downgradeComponent({ + component: AdminIndicatorsManagementComponent + }) as angular.IDirectiveFactory); + + angular.module('kommonitorAdmin') + .directive('adminGeoresourcesManagementNew', downgradeComponent({ + component: AdminGeoresourcesManagementComponent + }) as angular.IDirectiveFactory); + + angular.module('kommonitorAdmin') + .directive('adminScriptExecutionNew', downgradeComponent({ + component: AdminScriptExecutionComponent + }) as angular.IDirectiveFactory); + + angular.module('kommonitorAdmin') + .directive('adminScriptManagementNew', downgradeComponent({ + component: AdminScriptManagementComponent + }) as angular.IDirectiveFactory); + + angular.module('kommonitorAdmin') + .directive('georesourceAddModalNew', downgradeComponent({ + component: GeoresourceAddModalComponent + }) as angular.IDirectiveFactory); + + angular.module('kommonitorAdmin') + .directive('georesourceBatchUpdateModalNew', downgradeComponent({ + component: GeoresourceBatchUpdateModalComponent + }) as angular.IDirectiveFactory); + + angular.module('kommonitorAdmin') + .directive('georesourceEditMetadataModalNew', downgradeComponent({ + component: GeoresourceEditMetadataModalComponent + }) as angular.IDirectiveFactory); + + angular.module('kommonitorAdmin') + .directive('georesourceEditFeaturesModalNew', downgradeComponent({ + component: GeoresourceEditFeaturesModalComponent + }) as angular.IDirectiveFactory); + + angular.module('kommonitorAdmin') + .directive('georesourceEditUserRolesModalNew', downgradeComponent({ + component: GeoresourceEditUserRolesModalComponent + }) as angular.IDirectiveFactory); + + angular.module('kommonitorAdmin') + .directive('georesourceDeleteModalNew', downgradeComponent({ + component: GeoresourceDeleteModalComponent + }) as angular.IDirectiveFactory); + + angular.module('kommonitorAdmin') + .directive('roleDeleteModalNew', downgradeComponent({ + component: RoleDeleteModalComponent + }) as angular.IDirectiveFactory); + + angular.module('kommonitorAdmin') + .directive('roleEditMetadataModalNew', downgradeComponent({ + component: RoleEditMetadataModalComponent + }) as angular.IDirectiveFactory); + + angular.module('kommonitorAdmin') + .directive('roleEditGroupRightsModalNew', downgradeComponent({ + component: RoleEditGroupRightsModalComponent + }) as angular.IDirectiveFactory); + console.log("registered downgraded Angular components for AngularJS usage"); } @@ -289,7 +459,7 @@ export class AppModule implements DoBootstrap { }; /* - LOAD CONFIG FILES FROM CONFIG STORAGE SERVER + LOAD CONFIG FILES FROM CONFIG STORAGE SERVER */ private ajaxCall_keycloakConfig(configStorageServerConfig: any): JQuery.jqXHR { console.log("try to fetch keycloak config file"); @@ -388,7 +558,7 @@ export class AppModule implements DoBootstrap { success: function(result){ console.log("local filter-config file with default values fetched"); window.__env.filterConfig = result; - return; + return; }, error: function(XMLHttpRequest, textStatus, errorThrown) { console.log("Error parsing local filterConfig.json backup file"); @@ -665,7 +835,7 @@ export class AppModule implements DoBootstrap { // encryptedWordArray.words.slice(this.env.encryption.ivLength_byte / 4) // ) // }, - // hashedKey, + // hashedKey, // { iv: iv } // ); diff --git a/app/components/kommonitorAdmin/kommonitor-admin.template.html b/app/components/kommonitorAdmin/kommonitor-admin.template.html index b4d5352db..d3ce08ece 100644 --- a/app/components/kommonitorAdmin/kommonitor-admin.template.html +++ b/app/components/kommonitorAdmin/kommonitor-admin.template.html @@ -185,7 +185,7 @@

Gruppen (Hierarchie)

- +
@@ -197,31 +197,31 @@

Gruppen (Hierarchie)

- +
- +
- +
- +
- +
@@ -296,6 +296,9 @@

Gruppen (Hierarchie)

+ + + diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/admin-georesources-management.component.css b/app/components/ngComponents/admin/adminGeoresourcesManagement/admin-georesources-management.component.css new file mode 100644 index 000000000..440839189 --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/admin-georesources-management.component.css @@ -0,0 +1,240 @@ +/* Admin Georesources Management Component Styles */ + +/* AG Grid CSS imports */ +@import '~ag-grid-community/styles/ag-grid.css'; +@import '~ag-grid-community/styles/ag-theme-alpine.css'; + +/* Loading overlay */ +.loading-overlay-admin-panel { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.8); + z-index: 9999; + display: flex; + justify-content: center; + align-items: center; +} + +.loading-overlay-admin-panel.ng-hide { + display: none !important; +} + +/* Header controls styling */ +.content-header { + padding: 15px 15px 0 15px; + margin-bottom: 20px; /* Add margin to separate from content */ +} + +.adminTableButtonWrapper { + display: flex; + align-items: center; + gap: 10px; + margin-top: 15px; + margin-bottom: 15px; + flex-wrap: wrap; +} + +.verticalAlign { + display: flex; + align-items: center; + gap: 10px; + margin-right: 20px; +} + +.verticalAlign span { + font-weight: 500; + color: #333; + white-space: nowrap; +} + +/* Switch styling */ +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.switchslider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: .4s; +} + +.switchslider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + transition: .4s; +} + +input:checked + .switchslider { + background-color: #2196F3; +} + +input:focus + .switchslider { + box-shadow: 0 0 1px #2196F3; +} + +input:checked + .switchslider:before { + transform: translateX(26px); +} + +.switchslider.round { + border-radius: 34px; +} + +.switchslider.round:before { + border-radius: 50%; +} + +/* Content section spacing */ +.content.container-fluid { + padding-top: 20px; /* Add top padding to ensure separation */ +} + +/* Box styling improvements */ +.box { + margin-bottom: 30px; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); +} + +.box-header { + padding: 15px; + border-bottom: 1px solid #f0f0f0; +} + +.box-title { + font-size: 18px; + font-weight: 600; + color: #333; + margin: 0; +} + +.box-body { + padding: 20px; +} + +/* Table wrapper styling */ +.admin-table-wrapper { + width: 100%; + min-height: 400px; + background: #fff; + border-radius: 4px; + overflow: hidden; +} + +/* Button styling */ +.btn { + margin-right: 10px; + margin-bottom: 5px; + border-radius: 4px; + font-weight: 500; + transition: all 0.3s ease; +} + +.btn:hover { + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0,0,0,0.15); +} + +.btn-success { + background-color: #28a745; + border-color: #28a745; +} + +.btn-success:hover { + background-color: #218838; + border-color: #1e7e34; +} + +.btn-danger { + background-color: #dc3545; + border-color: #dc3545; +} + +.btn-danger:hover { + background-color: #c82333; + border-color: #bd2130; +} + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +/* Responsive design */ +@media (max-width: 768px) { + .adminTableButtonWrapper { + flex-direction: column; + align-items: flex-start; + } + + .verticalAlign { + margin-right: 0; + margin-bottom: 10px; + } + + .content-header { + padding: 10px; + } + + .box-body { + padding: 15px; + } +} + +/* AG Grid theme customization */ +.ag-theme-alpine { + --ag-header-height: 50px; + --ag-header-foreground-color: #444; + --ag-header-background-color: #f8f9fa; + --ag-row-hover-color: #f5f5f5; + --ag-selected-row-background-color: #e3f2fd; + --ag-font-size: 13px; + --ag-font-family: 'Source Sans Pro', sans-serif; + font-family: var(--ag-font-family); +} + +.ag-theme-alpine .ag-header-cell-label { + font-weight: 600; +} + +.ag-theme-alpine .ag-row { + border-bottom: 1px solid #e9ecef; +} + +.ag-theme-alpine .ag-cell { + padding: 8px 12px; + line-height: 1.4; +} + +/* Ensure ag-grid containers have proper styling */ +ag-grid-angular { + width: 100%; + height: 70vh; + border: 1px solid #dee2e6; + border-radius: 4px; + overflow: hidden; +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/admin-georesources-management.component.html b/app/components/ngComponents/admin/adminGeoresourcesManagement/admin-georesources-management.component.html new file mode 100644 index 000000000..41fc46e93 --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/admin-georesources-management.component.html @@ -0,0 +1,198 @@ +
+ +
+
+ +
+
+ + +
+

+ Pranjal Verwalten der Geodaten + Info +

+ +
+
+ Nur editierbare Datensätze anzeigen +
+ +
+
+ + + + + + + + + +
+
+ + +
+ + + + +
+
+

Points of Interest

+ +
+ +
+
+ + +
+ +
+ + + + +
+
+ +
+ + + + +
+
+

Lines of Interest

+ +
+ +
+
+ +
+ +
+ + + + +
+
+ +
+ + + + +
+
+

Areas of Interest

+ +
+ +
+
+ +
+ +
+ + + + +
+
+ +
+ + +
+ + +
\ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/admin-georesources-management.component.ts b/app/components/ngComponents/admin/adminGeoresourcesManagement/admin-georesources-management.component.ts new file mode 100644 index 000000000..934a51c78 --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/admin-georesources-management.component.ts @@ -0,0 +1,401 @@ +import { Component, OnInit, OnDestroy, Inject, ViewChild, AfterViewInit } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Subscription } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { BroadcastService } from '../../../../services/broadcast-service/broadcast.service'; +import { KommonitorGeoresourceDataExchangeService } from '../../../../services/adminGeoresourceUnit/kommonitor-data-exchange.service'; +import { KommonitorGeoresourceCacheHelperService } from '../../../../services/adminGeoresourceUnit/kommonitor-cache-helper.service'; +import { KommonitorGeoresourceDataGridHelperService } from '../../../../services/adminGeoresourceUnit/kommonitor-data-grid-helper.service'; +import { AgGridAngular } from 'ag-grid-angular'; +import { GeoresourceAddModalComponent } from './georesourceAddModal/georesource-add-modal.component'; +import { GeoresourceBatchUpdateModalComponent } from './georesourceBatchUpdateModal/georesource-batch-update-modal.component'; +import { GeoresourceEditMetadataModalComponent } from './georesourceEditMetadataModal/georesource-edit-metadata-modal.component'; +import { GeoresourceEditFeaturesModalComponent } from './georesourceEditFeaturesModal/georesource-edit-features-modal.component'; +import { GeoresourceEditUserRolesModalComponent } from './georesourceEditUserRolesModal/georesource-edit-user-roles-modal.component'; +import { GeoresourceDeleteModalComponent } from './georesourceDeleteModal/georesource-delete-modal.component'; + +// Declare jQuery for AdminLTE +declare const $: any; + +@Component({ + selector: 'admin-georesources-management-new', + templateUrl: './admin-georesources-management.component.html', + styleUrls: ['./admin-georesources-management.component.css'] +}) +export class AdminGeoresourcesManagementComponent implements OnInit, OnDestroy, AfterViewInit { + + @ViewChild('poiGrid', { static: false }) poiGrid!: AgGridAngular; + @ViewChild('loiGrid', { static: false }) loiGrid!: AgGridAngular; + @ViewChild('aoiGrid', { static: false }) aoiGrid!: AgGridAngular; + + public loadingData: boolean = true; + public tableViewSwitcher: boolean = false; + + // Grid options for each table + public poiGridOptions: any = {}; + public loiGridOptions: any = {}; + public aoiGridOptions: any = {}; + + private subscriptions: Subscription[] = []; + + constructor( + @Inject(DOCUMENT) private document: Document, + private modalService: NgbModal, + private broadcastService: BroadcastService, + public kommonitorDataExchangeService: KommonitorGeoresourceDataExchangeService, + private kommonitorCacheHelperService: KommonitorGeoresourceCacheHelperService, + private kommonitorDataGridHelperService: KommonitorGeoresourceDataGridHelperService + ) {} + + ngOnInit(): void { + this.setupEventListeners(); + this.initialize(); + + // Initialize grid options with the service + this.poiGridOptions = this.kommonitorDataGridHelperService.getPoiGridOptions(); + this.loiGridOptions = this.kommonitorDataGridHelperService.getLoiGridOptions(); + this.aoiGridOptions = this.kommonitorDataGridHelperService.getAoiGridOptions(); + } + + ngAfterViewInit(): void { + // Initialize grids after view is ready + this.kommonitorDataGridHelperService.initializeGrids( + this.poiGrid, + this.loiGrid, + this.aoiGrid + ); + + // Set component reference for callbacks + this.kommonitorDataGridHelperService.setComponentRef(this); + + // Load data if not already loaded + if (this.kommonitorDataExchangeService.availableGeoresources.length === 0) { + this.loadDataFallback(); + } + } + + private loadDataFallback(): void { + // If we still don't have data after 1 second, try to manually trigger data loading + if (!this.kommonitorDataExchangeService.availableGeoresources || + this.kommonitorDataExchangeService.availableGeoresources.length === 0) { + + // Try to fetch metadata manually + this.kommonitorDataExchangeService.fetchGeoresourcesMetadata( + this.kommonitorDataExchangeService.currentKeycloakLoginRoles + ).then((response: any) => { + this.initializeOrRefreshOverviewTable(); + }).catch((error: any) => { + // As a last resort, try with test data to verify grids are working + this.testGridsWithSampleData(); + + this.loadingData = false; + }); + } + } + + private testGridsWithSampleData(): void { + const testData = [ + { + georesourceId: 'test-poi-1', + datasetName: 'Test POI 1', + isPOI: true, + isLOI: false, + isAOI: false, + poiSymbolColor: '#ff0000', + poiSymbolBootstrap3Name: 'home', + poiMarkerColor: '#0000ff', + metadata: { + description: 'Test POI description' + }, + ownerId: 'test-owner', + userPermissions: ['creator'] + }, + { + georesourceId: 'test-loi-1', + datasetName: 'Test LOI 1', + isPOI: false, + isLOI: true, + isAOI: false, + loiColor: '#00ff00', + loiWidth: 2, + loiDashArrayString: '5 5', + metadata: { + description: 'Test LOI description' + }, + ownerId: 'test-owner', + userPermissions: ['creator'] + }, + { + georesourceId: 'test-aoi-1', + datasetName: 'Test AOI 1', + isPOI: false, + isLOI: false, + isAOI: true, + aoiColor: '#ffff00', + metadata: { + description: 'Test AOI description' + }, + ownerId: 'test-owner', + userPermissions: ['creator'] + } + ]; + + this.kommonitorDataGridHelperService.buildDataGrid_georesources(testData); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(sub => sub.unsubscribe()); + } + + private setupEventListeners(): void { + // Listen for broadcast messages + const broadcastSub = this.broadcastService.currentBroadcastMsg.subscribe((data: any) => { + if (data.msg === 'initialMetadataLoadingCompleted') { + setTimeout(() => { + this.initializeOrRefreshOverviewTable(); + }, 250); + } else if (data.msg === 'initialMetadataLoadingFailed') { + this.loadingData = false; + } else if (data.msg === 'refreshGeoresourceOverviewTable') { + this.loadingData = true; + this.refreshGeoresourceOverviewTable(data.values.crudType, data.values.targetGeoresourceId); + } + }); + + this.subscriptions.push(broadcastSub); + } + + private initialize(): void { + // Initialize any adminLTE box widgets + if (typeof $ !== 'undefined' && $ && $.fn && $.fn.boxWidget) { + $('.box').boxWidget(); + } + } + + public onTableViewSwitch(): void { + this.initializeOrRefreshOverviewTable(); + } + + public initializeOrRefreshOverviewTable(): void { + this.loadingData = true; + + const georesources = this.initGeoresources(); + + this.kommonitorDataGridHelperService.buildDataGrid_georesources(georesources); + + setTimeout(() => { + this.loadingData = false; + }, 100); + } + + private initGeoresources(): any[] { + if (this.tableViewSwitcher) { + return this.kommonitorDataExchangeService.availableGeoresources.filter( + (e: any) => !(e.userPermissions.length === 1 && e.userPermissions.includes('viewer')) + ); + } else { + return this.kommonitorDataExchangeService.availableGeoresources; + } + } + + public refreshGeoresourceOverviewTable(crudType?: string, targetGeoresourceId?: string): void { + if (!crudType || !targetGeoresourceId) { + // refetch all metadata from georesources to update table + this.kommonitorDataExchangeService.fetchGeoresourcesMetadata( + this.kommonitorDataExchangeService.currentKeycloakLoginRoles + ).then((response: any) => { + this.initializeOrRefreshOverviewTable(); + this.broadcastService.broadcast('refreshGeoresourceOverviewTableCompleted'); + this.loadingData = false; + }).catch((response: any) => { + this.loadingData = false; + this.broadcastService.broadcast('refreshGeoresourceOverviewTableCompleted'); + }); + } else if (crudType && targetGeoresourceId) { + if (crudType === 'add') { + this.kommonitorCacheHelperService.fetchSingleGeoresourceMetadata( + targetGeoresourceId, + this.kommonitorDataExchangeService.currentKeycloakLoginRoles + ).then((data: any) => { + this.kommonitorDataExchangeService.addSingleGeoresourceMetadata(data); + this.initializeOrRefreshOverviewTable(); + this.broadcastService.broadcast('refreshGeoresourceOverviewTableCompleted'); + this.loadingData = false; + }).catch((response: any) => { + this.loadingData = false; + this.broadcastService.broadcast('refreshGeoresourceOverviewTableCompleted'); + }); + } else if (crudType === 'edit') { + this.kommonitorCacheHelperService.fetchSingleGeoresourceMetadata( + targetGeoresourceId, + this.kommonitorDataExchangeService.currentKeycloakLoginRoles + ).then((data: any) => { + this.kommonitorDataExchangeService.replaceSingleGeoresourceMetadata(data); + this.initializeOrRefreshOverviewTable(); + this.broadcastService.broadcast('refreshGeoresourceOverviewTableCompleted'); + this.loadingData = false; + }).catch((response: any) => { + this.loadingData = false; + this.broadcastService.broadcast('refreshGeoresourceOverviewTableCompleted'); + }); + } else if (crudType === 'delete') { + // targetGeoresourceId might be array in this case + if (targetGeoresourceId && typeof targetGeoresourceId === 'string') { + this.kommonitorDataExchangeService.deleteSingleGeoresourceMetadata(targetGeoresourceId); + this.initializeOrRefreshOverviewTable(); + this.broadcastService.broadcast('refreshGeoresourceOverviewTableCompleted'); + this.loadingData = false; + } else if (targetGeoresourceId && Array.isArray(targetGeoresourceId)) { + for (const id of targetGeoresourceId) { + this.kommonitorDataExchangeService.deleteSingleGeoresourceMetadata(id); + } + this.initializeOrRefreshOverviewTable(); + this.broadcastService.broadcast('refreshGeoresourceOverviewTableCompleted'); + this.loadingData = false; + } + } + } + } + + // Modal event handlers + onClickAddGeoresource(): void { + const modalRef = this.modalService.open(GeoresourceAddModalComponent, { + size: 'lg', + backdrop: 'static', + keyboard: false, + container: 'body', + animation: false + }); + + } + + onClickBatchUpdateGeoresource(): void { + const modalRef = this.modalService.open(GeoresourceBatchUpdateModalComponent, { + size: 'lg', + backdrop: 'static', + keyboard: false, + container: 'body', + animation: false + }); + + modalRef.result.then((result) => { + if (result) { + this.initializeOrRefreshOverviewTable(); + } + }).catch(() => { + // Modal dismissed + }); + } + + public onClickEditMetadata(georesourceDataset: any): void { + const modalRef = this.modalService.open(GeoresourceEditMetadataModalComponent, { + size: 'lg', + backdrop: 'static', + keyboard: false, + container: 'body', + animation: false + }); + + // Pass the georesource dataset to the modal + modalRef.componentInstance.currentGeoresourceDataset = georesourceDataset; + + modalRef.result.then((result) => { + if (result) { + // Handle successful edit + this.refreshGeoresourceOverviewTable('edit', georesourceDataset.georesourceId); + } + }, (reason) => { + // Modal dismissed + }); + } + + public onClickEditFeatures(georesourceDataset: any): void { + const modalRef = this.modalService.open(GeoresourceEditFeaturesModalComponent, { + size: 'xl', + backdrop: 'static', + keyboard: false, + container: 'body', + animation: false + }); + + // Pass the georesource dataset to the modal + modalRef.componentInstance.currentGeoresourceDataset = georesourceDataset; + + modalRef.result.then((result) => { + if (result) { + // Handle successful edit + this.refreshGeoresourceOverviewTable('edit', georesourceDataset.georesourceId); + } + }, (reason) => { + // Modal dismissed + }); + } + + public onClickEditUserRoles(georesourceDataset: any): void { + const modalRef = this.modalService.open(GeoresourceEditUserRolesModalComponent, { + size: 'xl', + backdrop: 'static', + keyboard: false, + container: 'body', + animation: false + }); + modalRef.componentInstance.currentGeoresourceDataset = georesourceDataset; + + modalRef.result.then((result) => { + if (result) { + // Handle successful edit + this.refreshGeoresourceOverviewTable('edit', georesourceDataset.georesourceId); + } + }, (reason) => { + // Modal dismissed + }); + } + + public onClickDeleteGeoresource(georesourceDataset: any): void { + const modalRef = this.modalService.open(GeoresourceDeleteModalComponent, { + size: 'xl', + backdrop: 'static', + keyboard: false, + container: 'body', + animation: false + }); + + // Pass the georesource dataset to the modal (as array like original) + this.broadcastService.broadcast('onDeleteGeoresources', [georesourceDataset]); + + modalRef.result.then( + (result) => { + console.log('Georesource delete modal closed with result:', result); + }, + (reason) => { + console.log('Georesource delete modal dismissed with reason:', reason); + } + ); + } + + // Utility methods + checkCreatePermission(): boolean { + return this.kommonitorDataExchangeService.checkCreatePermission(); + } + + checkEditorPermission(): boolean { + return this.kommonitorDataExchangeService.checkEditorPermission(); + } + + checkDeletePermission(): boolean { + return this.kommonitorDataExchangeService.checkDeletePermission(); + } + + // Callback methods for cell renderer + onEditMetadata(georesourceDataset: any): void { + this.broadcastService.broadcast('onEditGeoresourceMetadata', georesourceDataset); + } + + onEditFeatures(georesourceDataset: any): void { + this.onClickEditFeatures(georesourceDataset); + } + + onEditUserRoles(georesourceDataset: any): void { + this.onClickEditUserRoles(georesourceDataset); + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceAddModal/georesource-add-modal.component.css b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceAddModal/georesource-add-modal.component.css new file mode 100644 index 000000000..d6087d203 --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceAddModal/georesource-add-modal.component.css @@ -0,0 +1,415 @@ +/* Georesource Add Modal Component Styles */ + +/* Loading overlay */ +.loading-overlay-admin-panel { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.8); + z-index: 9999; + display: flex; + justify-content: center; + align-items: center; +} + +.loading-overlay-admin-panel.ng-hide { + display: none !important; +} + +/* Multi-step form styles */ +.multiStepForm { + margin-bottom: 0px; +} + +#progressbar { + margin-bottom: 30px; + overflow: hidden; + color: lightgrey; + padding-left: 0; +} + +#progressbar li { + list-style-type: none; + font-size: 15px; + width: 25%; + float: left; + position: relative; + font-weight: 400; +} + +#progressbar li:before { + width: 50px; + height: 50px; + line-height: 45px; + display: block; + font-size: 20px; + color: #ffffff; + background: lightgray; + border-radius: 50%; + margin: 0 auto 10px auto; + padding: 2px; +} + +#progressbar li.active:before, +#progressbar li.active:after { + background: #27AE60; +} + +#progressbar li.clickable { + cursor: pointer; +} + +/* Form fieldset styles */ +.fs-title { + font-size: 15px; + text-transform: uppercase; + color: #2C3E50; + margin-bottom: 10px; +} + +.fs-subtitle { + font-weight: normal; + font-size: 13px; + color: #666; + margin-bottom: 20px; +} + +/* Form group spacing */ +.form-group { + margin-bottom: 15px; +} + +/* Vertical alignment helper */ +.vertical-align { + display: flex; + align-items: center; +} + +/* Modal body padding */ +.modal-body { + padding: 20px; +} + +/* Modal footer styling */ +.modal-footer { + padding: 15px 20px; + border-top: 1px solid #e5e5e5; +} + +/* Switch toggle styles */ +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.switchslider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; +} + +.switchslider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .switchslider { + background-color: #2196F3; +} + +input:focus + .switchslider { + box-shadow: 0 0 1px #2196F3; +} + +input:checked + .switchslider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); +} + +.switchslider.round { + border-radius: 34px; +} + +.switchslider.round:before { + border-radius: 50%; +} + +/* Custom color picker dropdown styles */ +.customColorPicker .dropdown-menu { + min-width: 200px; +} + +.customColorPicker .dropdown-menu li a { + padding: 5px 10px; +} + +.customColorPicker .dropdown-menu li a i { + display: inline-block; + width: 20px; + height: 20px; + margin-right: 10px; + border: 1px solid #ccc; + vertical-align: middle; +} + +/* Table styles for attribute mappings */ +.table-condensed > thead > tr > th, +.table-condensed > tbody > tr > th, +.table-condensed > tfoot > tr > th, +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > td { + padding: 5px; +} + +.modal-footer .btn { + margin-left: 5px; +} + +/* Form validation styles */ +.help-block { + color: #737373; + font-size: 12px; + margin-top: 5px; +} + +.help-block.with-errors { + color: #a94442; +} + +/* Error message styling */ +.error-message { + color: #a94442; + background-color: #f2dede; + border: 1px solid #ebccd1; + border-radius: 4px; + padding: 10px; + margin-bottom: 15px; +} + +/* Success message styling */ +.success-message { + color: #3c763d; + background-color: #dff0d8; + border: 1px solid #d6e9c6; + border-radius: 4px; + padding: 10px; + margin-bottom: 15px; +} + +/* Checkbox styling */ +.checkbox { + margin-top: 10px; +} + +.checkbox label { + font-weight: normal; + cursor: pointer; +} + +/* File input styling */ +input[type="file"] { + padding: 6px 12px; + border: 1px solid #ccc; + border-radius: 4px; + background-color: #fff; +} + +/* Color input styling */ +input[type="color"] { + width: 100%; + height: 34px; + padding: 6px 12px; + border: 1px solid #ccc; + border-radius: 4px; +} + +/* Select styling */ +select.form-control { + height: 34px; + padding: 6px 12px; +} + +/* Textarea styling */ +textarea.form-control { + resize: vertical; + min-height: 60px; +} + +/* Button styling */ +.btn { + border-radius: 4px; + font-weight: 500; +} + +.btn-success { + background-color: #5cb85c; + border-color: #4cae4c; +} + +.btn-success:hover { + background-color: #449d44; + border-color: #398439; +} + +.btn-danger { + background-color: #d9534f; + border-color: #d43f3a; +} + +.btn-danger:hover { + background-color: #c9302c; + border-color: #ac2925; +} + +.btn-info { + background-color: #5bc0de; + border-color: #46b8da; +} + +.btn-info:hover { + background-color: #31b0d5; + border-color: #269abc; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .vertical-align { + flex-direction: column; + align-items: stretch; + } + + .col-xs-12 { + margin-bottom: 15px; + } + + #progressbar li { + font-size: 12px; + } + + #progressbar li:before { + width: 40px; + height: 40px; + line-height: 35px; + font-size: 16px; + } +} + +/* Modal size adjustments */ +.modal-xl { + width: 90%; + max-width: 1200px; +} + +/* Form validation states */ +.form-control.ng-invalid.ng-touched { + border-color: #a94442; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; +} + +.form-control.ng-valid.ng-touched { + border-color: #3c763d; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; +} + +/* Navigation buttons */ +.navigation-buttons { + margin-top: 20px; + text-align: center; +} + +.navigation-buttons .btn { + margin: 0 5px; +} + +/* Step content transitions */ +.step-content { + transition: opacity 0.3s ease-in-out; +} + +/* Icon picker styling (if used) */ +.icon-picker { + border: 1px solid #ccc; + border-radius: 4px; + padding: 10px; + background-color: #f9f9f9; +} + +/* Role management table styling */ +.role-management-table { + margin-top: 15px; + border: 1px solid #ddd; + border-radius: 4px; +} + +/* Topic hierarchy styling */ +.topic-hierarchy { + background-color: #f5f5f5; + padding: 15px; + border-radius: 4px; + margin-bottom: 15px; +} + +.topic-hierarchy select { + margin-bottom: 10px; +} + +/* Visual styling section */ +.visual-styling { + background-color: #f9f9f9; + padding: 15px; + border-radius: 4px; + margin-bottom: 15px; +} + +.visual-styling .form-group { + margin-bottom: 10px; +} + +/* Period of validity styling */ +.period-of-validity { + background-color: #e8f4f8; + padding: 15px; + border-radius: 4px; + margin-bottom: 15px; +} + +.period-of-validity .form-group { + margin-bottom: 10px; +} + +/* Importer section styling */ +.importer-section { + background-color: #f0f8ff; + padding: 15px; + border-radius: 4px; + margin-bottom: 15px; +} + +.importer-section .form-group { + margin-bottom: 10px; +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceAddModal/georesource-add-modal.component.html b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceAddModal/georesource-add-modal.component.html new file mode 100644 index 000000000..c647fb413 --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceAddModal/georesource-add-modal.component.html @@ -0,0 +1,871 @@ + + + + + + + +
+ +

Georessource registriert

+

Eine neue Georessource mit Namen {{successMessagePart}} wurde in KomMonitor registriert und in die Übersichtstabelle eingetragen. + + {{importedFeatures.length}} Features wurden dabei importiert. + +

+
+ + +
+ +

Registrierung gescheitert

+ Bei der Registrierung der Georessource ist ein Fehler aufgetreten. Fehlermeldung: +
+

+  
+
+ +
+

Bei den {{importerErrors.length}} Features mit folgenden IDs scheitert der Import:

+
+      
    +
  • {{error}}
  • +
+
+

Bitte beheben Sie die angezeigten Fehler im Datensatz und wiederholen den Prozess.

+
+
+ + +
+ +

Metadata Import gescheitert

+ Beim Import der Metadaten aus einer Datei ist ein Fehler aufgetreten. Fehlermeldung: +
+
{{georesourceMetadataImportError}}
+
+
+

Bitte stellen Sie sicher, dass folgendes JSON-Format eingehalten wird:

+

+
+ + +
+ +

Mapping-Konfiguration Import gescheitert

+ Beim Import der Mapping-Konfiguration aus einer Datei ist ein Fehler aufgetreten. Fehlermeldung: +
+

+  
+
+

Bitte stellen Sie sicher, dass folgendes JSON-Format eingehalten wird:

+

+
\ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceAddModal/georesource-add-modal.component.ts b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceAddModal/georesource-add-modal.component.ts new file mode 100644 index 000000000..443248372 --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceAddModal/georesource-add-modal.component.ts @@ -0,0 +1,1129 @@ +import { Component, OnInit, Inject, ViewChild, ElementRef } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; +import { HttpClient } from '@angular/common/http'; + +@Component({ + selector: 'georesource-add-modal-new', + templateUrl: './georesource-add-modal.component.html', + styleUrls: ['./georesource-add-modal.component.css'] +}) +export class GeoresourceAddModalComponent implements OnInit { + @ViewChild('metadataImportFile', { static: false }) metadataImportFile!: ElementRef; + @ViewChild('mappingConfigImportFile', { static: false }) mappingConfigImportFile!: ElementRef; + @ViewChild('georesourceDataSourceInput', { static: false }) georesourceDataSourceInput!: ElementRef; + + // Multi-step form + currentStep = 1; + totalSteps = 4; // Will be adjusted based on security settings + + // Form data + isSubmitting = false; + errorMessage = ''; + successMessage = ''; + loadingData = false; + + // Basic form data + datasetName = ''; + datasetNameInvalid = false; + georesourceType = 'poi'; + isPOI = true; + isLOI = false; + isAOI = false; + + // Metadata + metadata: any = { + description: '', + databasis: '', + datasource: '', + contact: '', + updateInterval: null, + lastUpdate: '', + literature: '', + note: '', + sridEPSG: 4326 + }; + + // Topic hierarchy + georesourceTopic_mainTopic: any = null; + georesourceTopic_subTopic: any = null; + georesourceTopic_subsubTopic: any = null; + georesourceTopic_subsubsubTopic: any = null; + + // Visual styling + selectedPoiMarkerColor: any = null; + selectedPoiSymbolColor: any = null; + selectedLoiDashArrayObject: any = null; + loiColor = '#bf3d2c'; + loiWidth = 3; + aoiColor = '#bf3d2c'; + selectedPoiIconName = 'home'; + selectedPoiMarkerStyle = 'symbol'; + poiMarkerText = ''; + poiMarkerTextInvalid = false; + + // Period of validity + periodOfValidity: { startDate: string; endDate: string } = { + startDate: '', + endDate: '' + }; + periodOfValidityInvalid = false; + + // Available options + availableTopics: any[] = []; + updateIntervalOptions: any[] = []; + availablePoiMarkerColors: any[] = []; + availableLoiDashArrayObjects: any[] = []; + availableDatasourceTypes: any[] = []; + + // Importer functionality + converter: any = null; + schema: string = ''; + mimeType: string = ''; + datasourceType: any = null; + georesourceDataSourceIdProperty = ''; + georesourceDataSourceIdPropertyInvalid = false; + georesourceDataSourceNameProperty = ''; + georesourceDataSourceNamePropertyInvalid = false; + + // Bbox parameters for OGCAPI_FEATURES + bboxType: string = ''; + bboxRefSpatialUnit: any = null; + + // Attribute mapping + attributeMapping_sourceAttributeName = ''; + attributeMapping_destinationAttributeName = ''; + attributeMapping_data: any = null; + attributeMapping_attributeType: any = null; + attributeMappings_adminView: any[] = []; + keepAttributes = true; + keepMissingValues = true; + + // Validity dates per feature + validityStartDate_perFeature = ''; + validityEndDate_perFeature = ''; + + // Role management + roleManagementTableOptions: any = null; + ownerOrganization = ''; + ownerOrgFilter = ''; + isPublic = false; + resourcesCreatorRights: any[] = []; + + // GeoJSON data + geoJsonString: any = null; + georesource_asGeoJson: any = null; + + // Import/Export functionality + metadataImportSettings: any = null; + mappingConfigImportSettings: any = null; + georesourceMetadataImportError = ''; + georesourceMappingConfigImportError = ''; + + // Success/Error data + successMessagePart = ''; + errorMessagePart = ''; + importerErrors: any[] = []; + importedFeatures: any[] = []; + + // Metadata structure for import/export + georesourceMetadataStructure: any = { + "metadata": { + "note": "an optional note", + "literature": "optional text about literature", + "updateInterval": "YEARLY|HALF_YEARLY|QUARTERLY|MONTHLY|ARBITRARY", + "sridEPSG": 4326, + "datasource": "text about data source", + "contact": "text about contact details", + "lastUpdate": "YYYY-MM-DD", + "description": "description about spatial unit dataset", + "databasis": "text about data basis", + }, + "allowedRoles": ['roleId'], + "datasetName": "Name of georesource dataset", + "isPOI": "boolean parameter for point of interest dataset - only one of isPOI, isLOI, isAOI can be true", + "isLOI": "boolean parameter for lines of interest dataset - only one of isPOI, isLOI, isAOI can be true", + "isAOI": "boolean parameter for area of interest dataset - only one of isPOI, isLOI, isAOI can be true", + "poiSymbolBootstrap3Name": "glyphicon name of bootstrap 3 symbol to use for a POI resource", + "poiSymbolColor": "'white'|'red'|'orange'|'beige'|'green'|'blue'|'purple'|'pink'|'gray'|'black'", + "loiDashArrayString": "dash array string value - e.g. 20 20", + "poiMarkerColor": "'white'|'red'|'orange'|'beige'|'green'|'blue'|'purple'|'pink'|'gray'|'black'", + "loiColor": "color for lines of interest dataset", + "loiWidth": "width for lines of interest dataset", + "aoiColor": "color for area of interest dataset" + }; + + georesourceMetadataStructure_pretty = ''; + georesourceMappingConfigStructure_pretty = ''; + + // Importer objects + converterDefinition: any = null; + datasourceTypeDefinition: any = null; + propertyMappingDefinition: any = null; + postBody_georesources: any = null; + + // Validation flags + idPropertyNotFound = false; + namePropertyNotFound = false; + georesourceDataSourceInputInvalid = false; + georesourceDataSourceInputInvalidReason = ''; + + // Icon picker options + iconPickerOptions: any = { + align: 'center', + arrowClass: 'btn-default', + arrowPrevIconClass: 'fas fa-angle-left', + arrowNextIconClass: 'fas fa-angle-right', + cols: 10, + footer: true, + header: true, + icon: 'glyphicon-home', + iconset: 'glyphicon', + labelHeader: '{0} von {1} Seiten', + labelFooter: '{0} - {1} von {2} Icons', + placement: 'bottom', + rows: 6, + search: true, + searchText: 'Stichwortsuche (Bootstrap Glyphicons)', + selectedClass: 'btn-success', + unselectedClass: '' + }; + + constructor( + public activeModal: NgbActiveModal, + @Inject('kommonitorDataExchangeService') public kommonitorDataExchangeService: any, + @Inject('kommonitorImporterHelperService') public kommonitorImporterHelperService: any, + @Inject('kommonitorMultiStepFormHelperService') public kommonitorMultiStepFormHelperService: any, + @Inject('kommonitorDataGridHelperService') public kommonitorDataGridHelperService: any, + private broadcastService: BroadcastService, + private http: HttpClient + ) {} + + ngOnInit(): void { + this.initializeForm(); + this.setupEventListeners(); + } + + private initializeForm(): void { + // Initialize form with default values + this.resetGeoresourceAddForm(); + + // Load available options + this.loadAvailableOptions(); + + // Adjust total steps based on security settings + this.totalSteps = this.kommonitorDataExchangeService.enableKeycloakSecurity ? 5 : 4; + } + + private setupEventListeners(): void { + // Listen for broadcast messages + const broadcastSub = this.broadcastService.currentBroadcastMsg.subscribe((data: any) => { + if (data.msg === 'availableRolesUpdate') { + this.refreshRoles(); + } else if (data.msg === 'initialMetadataLoadingCompleted') { + this.refreshRoles(); + } + }); + } + + private loadAvailableOptions(): void { + // Load available options from services + this.updateIntervalOptions = this.kommonitorDataExchangeService.updateIntervalOptions || []; + this.availablePoiMarkerColors = this.kommonitorDataExchangeService.availablePoiMarkerColors || []; + this.availableLoiDashArrayObjects = this.kommonitorDataExchangeService.availableLoiDashArrayObjects || []; + this.availableTopics = this.kommonitorDataExchangeService.availableTopics || []; + this.availableDatasourceTypes = this.kommonitorImporterHelperService.availableDatasourceTypes || []; + + // Initialize metadata structure pretty print + this.georesourceMetadataStructure_pretty = this.kommonitorDataExchangeService.syntaxHighlightJSON(this.georesourceMetadataStructure); + this.georesourceMappingConfigStructure_pretty = this.kommonitorDataExchangeService.syntaxHighlightJSON(this.kommonitorImporterHelperService.mappingConfigStructure); + } + + private refreshRoles(): void { + this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid( + 'georesourceAddRoleManagementTable', + this.roleManagementTableOptions, + this.kommonitorDataExchangeService.accessControl, + this.kommonitorDataExchangeService.getCurrentKomMonitorLoginRoleIds() + ); + } + + // Multi-step form navigation + goToStep(step: number): void { + if (step >= 1 && step <= this.totalSteps) { + this.currentStep = step; + } + } + + nextStep(): void { + if (this.currentStep < this.totalSteps) { + this.currentStep++; + } + } + + previousStep(): void { + if (this.currentStep > 1) { + this.currentStep--; + } + } + + // Form validation methods + checkDatasetName(): void { + this.datasetNameInvalid = false; + this.kommonitorDataExchangeService.availableGeoresources.forEach((georesource: any) => { + if (georesource.datasetName === this.datasetName) { + this.datasetNameInvalid = true; + return; + } + }); + } + + checkPeriodOfValidity(): void { + this.periodOfValidityInvalid = false; + if (this.periodOfValidity.startDate && this.periodOfValidity.endDate) { + const startDate = new Date(this.periodOfValidity.startDate); + const endDate = new Date(this.periodOfValidity.endDate); + + if ((startDate === endDate) || startDate > endDate) { + this.periodOfValidityInvalid = true; + } + } + } + + onChangeGeoresourceType(): void { + switch (this.georesourceType) { + case "poi": + this.isPOI = true; + this.isLOI = false; + this.isAOI = false; + break; + case "loi": + this.isPOI = false; + this.isLOI = true; + this.isAOI = false; + break; + case "aoi": + this.isPOI = false; + this.isLOI = false; + this.isAOI = true; + break; + default: + this.isPOI = true; + this.isLOI = false; + this.isAOI = false; + break; + } + } + + onChangeOwner(orgUnitId: string): void { + this.ownerOrganization = orgUnitId; + this.refreshRoles(); + } + + onChangeIsPublic(isPublic: boolean): void { + this.isPublic = isPublic; + } + + // Importer methods + onChangeConverter(): void { + this.schema = this.converter?.schemas ? this.converter.schemas[0] : undefined; + this.mimeType = this.converter?.mimeTypes ? this.converter.mimeTypes[0] : undefined; + } + + onChangeMimeType(mimeType: string): void { + this.mimeType = mimeType; + } + + onChangeDatasourceType(datasourceType: any): void { + this.datasourceType = datasourceType; + } + + // Color and styling methods + onChangeMarkerColor(markerColor: any): void { + this.selectedPoiMarkerColor = markerColor; + } + + onChangeSymbolColor(symbolColor: any): void { + this.selectedPoiSymbolColor = symbolColor; + } + + onChangeLoiDashArray(loiDashArrayObject: any): void { + this.selectedLoiDashArrayObject = loiDashArrayObject; + } + + onChangeMarkerStyle(markerStyle: string): void { + this.selectedPoiMarkerStyle = markerStyle; + } + + checkPoiMarkerText(): void { + this.poiMarkerTextInvalid = false; + if (this.poiMarkerText && this.poiMarkerText.length > 3) { + this.poiMarkerTextInvalid = true; + } + } + + // Attribute mapping methods + onAddOrUpdateAttributeMapping(): void { + const tmpAttributeMapping_adminView = { + "sourceName": this.attributeMapping_sourceAttributeName, + "destinationName": this.attributeMapping_destinationAttributeName, + "dataType": this.attributeMapping_attributeType + }; + + let processed = false; + + for (let index = 0; index < this.attributeMappings_adminView.length; index++) { + const attributeMappingEntry_adminView = this.attributeMappings_adminView[index]; + + if (attributeMappingEntry_adminView.sourceName === tmpAttributeMapping_adminView.sourceName) { + // replace object + this.attributeMappings_adminView[index] = tmpAttributeMapping_adminView; + processed = true; + break; + } + } + + if (!processed) { + // new entry + this.attributeMappings_adminView.push(tmpAttributeMapping_adminView); + } + + this.attributeMapping_sourceAttributeName = ''; + this.attributeMapping_destinationAttributeName = ''; + this.attributeMapping_attributeType = this.kommonitorImporterHelperService.attributeMapping_attributeTypes[0]; + } + + onClickEditAttributeMapping(attributeMappingEntry: any): void { + this.attributeMapping_sourceAttributeName = attributeMappingEntry.sourceName; + this.attributeMapping_destinationAttributeName = attributeMappingEntry.destinationName; + this.attributeMapping_attributeType = attributeMappingEntry.dataType; + } + + onClickDeleteAttributeMapping(attributeMappingEntry: any): void { + for (let index = 0; index < this.attributeMappings_adminView.length; index++) { + if (this.attributeMappings_adminView[index].sourceName === attributeMappingEntry.sourceName) { + // remove object + this.attributeMappings_adminView.splice(index, 1); + break; + } + } + } + + // Import/Export methods + onImportGeoresourceAddMetadata(): void { + this.georesourceMetadataImportError = ''; + this.metadataImportFile.nativeElement.click(); + } + + onExportGeoresourceAddMetadataTemplate(): void { + const metadataJSON = JSON.stringify(this.georesourceMetadataStructure); + const fileName = "Georessource_Metadaten_Vorlage_Export.json"; + this.downloadFile(metadataJSON, fileName); + } + + onExportGeoresourceAddMetadata(): void { + const metadataExport = JSON.parse(JSON.stringify(this.georesourceMetadataStructure)); + + metadataExport.metadata.note = this.metadata.note || ""; + metadataExport.metadata.literature = this.metadata.literature || ""; + metadataExport.metadata.sridEPSG = this.metadata.sridEPSG || ""; + metadataExport.metadata.datasource = this.metadata.datasource || ""; + metadataExport.metadata.contact = this.metadata.contact || ""; + metadataExport.metadata.lastUpdate = this.metadata.lastUpdate || ""; + metadataExport.metadata.description = this.metadata.description || ""; + metadataExport.metadata.databasis = this.metadata.databasis || ""; + metadataExport.datasetName = this.datasetName || ""; + + metadataExport.allowedRoles = []; + + if (this.roleManagementTableOptions) { + const roleIds = this.kommonitorDataGridHelperService.getSelectedRoleIds_roleManagementGrid(this.roleManagementTableOptions); + if (roleIds && Array.isArray(roleIds)) { + for (const roleId of roleIds) { + metadataExport.allowedRoles.push(roleId); + } + } + } + + if (this.metadata.updateInterval) { + metadataExport.metadata.updateInterval = this.metadata.updateInterval.apiName; + } + + const name = this.datasetName; + + // georesource specific properties + metadataExport.isPOI = this.isPOI; + metadataExport.isLOI = this.isLOI; + metadataExport.isAOI = this.isAOI; + + if (this.isPOI) { + metadataExport["poiSymbolBootstrap3Name"] = this.selectedPoiIconName; + metadataExport["poiSymbolColor"] = (this.selectedPoiSymbolColor as any)?.colorName || ''; + metadataExport["poiMarkerColor"] = (this.selectedPoiMarkerColor as any)?.colorName || ''; + + metadataExport["loiDashArrayString"] = ""; + metadataExport["loiColor"] = ""; + metadataExport["loiWidth"] = ""; + + metadataExport["aoiColor"] = ""; + } else if (this.isLOI) { + metadataExport["poiSymbolBootstrap3Name"] = ""; + metadataExport["poiSymbolColor"] = ""; + metadataExport["poiMarkerColor"] = ""; + + metadataExport["loiDashArrayString"] = this.selectedLoiDashArrayObject.dashArrayValue; + metadataExport["loiColor"] = this.loiColor; + metadataExport["loiWidth"] = this.loiWidth; + + metadataExport["aoiColor"] = ""; + } else if (this.isAOI) { + metadataExport["poiSymbolBootstrap3Name"] = ""; + metadataExport["poiSymbolColor"] = ""; + metadataExport["poiMarkerColor"] = ""; + + metadataExport["loiDashArrayString"] = ""; + metadataExport["loiColor"] = ""; + metadataExport["loiWidth"] = ""; + + metadataExport["aoiColor"] = this.aoiColor; + } + + // Topic reference + if (this.georesourceTopic_subsubsubTopic) { + metadataExport.topicReference = this.georesourceTopic_subsubsubTopic.topicId; + } else if (this.georesourceTopic_subsubTopic) { + metadataExport.topicReference = this.georesourceTopic_subsubTopic.topicId; + } else if (this.georesourceTopic_subTopic) { + metadataExport.topicReference = this.georesourceTopic_subTopic.topicId; + } else if (this.georesourceTopic_mainTopic) { + metadataExport.topicReference = this.georesourceTopic_mainTopic.topicId; + } else { + metadataExport.topicReference = ""; + } + + const metadataJSON = JSON.stringify(metadataExport); + let fileName = "Georessource_Metadaten_Export"; + + if (name) { + fileName += "-" + name; + } + + fileName += ".json"; + this.downloadFile(metadataJSON, fileName); + } + + onImportGeoresourceAddMappingConfig(): void { + this.georesourceMappingConfigImportError = ''; + this.mappingConfigImportFile.nativeElement.click(); + } + + onExportGeoresourceAddMappingConfig(): void { + this.buildImporterObjects().then(() => { + const mappingConfigExport: any = { + "converter": this.converterDefinition, + "dataSource": this.datasourceTypeDefinition, + "propertyMapping": this.propertyMappingDefinition, + }; + + mappingConfigExport.periodOfValidity = this.periodOfValidity; + + const name = this.datasetName; + const metadataJSON = JSON.stringify(mappingConfigExport); + let fileName = "KomMonitor-Import-Mapping-Konfiguration_Export"; + + if (name) { + fileName += "-" + name; + } + + fileName += ".json"; + this.downloadFile(metadataJSON, fileName); + }); + } + + // File handling methods + onMetadataFileSelected(event: any): void { + const file = event.target.files[0]; + if (file) { + this.parseMetadataFromFile(file); + } + } + + onMappingConfigFileSelected(event: any): void { + const file = event.target.files[0]; + if (file) { + this.parseMappingConfigFromFile(file); + } + } + + private parseMetadataFromFile(file: File): void { + const fileReader = new FileReader(); + + fileReader.onload = (event: any) => { + try { + this.parseFromMetadataFile(event); + } catch (error) { + console.error(error); + console.error("Uploaded Metadata File cannot be parsed."); + this.georesourceMetadataImportError = "Uploaded Metadata File cannot be parsed correctly"; + this.showMetadataErrorAlert(); + } + }; + + fileReader.readAsText(file); + } + + private parseMappingConfigFromFile(file: File): void { + const fileReader = new FileReader(); + + fileReader.onload = (event: any) => { + try { + this.parseFromMappingConfigFile(event); + } catch (error) { + console.error(error); + console.error("Uploaded MappingConfig File cannot be parsed."); + this.georesourceMappingConfigImportError = "Uploaded MappingConfig File cannot be parsed correctly"; + this.showMappingConfigErrorAlert(); + } + }; + + fileReader.readAsText(file); + } + + private parseFromMetadataFile(event: any): void { + this.metadataImportSettings = JSON.parse(event.target.result); + + if (!this.metadataImportSettings.metadata) { + console.error("uploaded Metadata File cannot be parsed - wrong structure."); + this.georesourceMetadataImportError = "Struktur der Datei stimmt nicht mit erwartetem Muster überein."; + this.showMetadataErrorAlert(); + return; + } + + this.metadata = {}; + this.metadata.note = this.metadataImportSettings.metadata.note; + this.metadata.literature = this.metadataImportSettings.metadata.literature; + + this.updateIntervalOptions.forEach((option: any) => { + if (option.apiName === this.metadataImportSettings.metadata.updateInterval) { + this.metadata.updateInterval = option; + } + }); + + this.metadata.sridEPSG = this.metadataImportSettings.metadata.sridEPSG; + this.metadata.datasource = this.metadataImportSettings.metadata.datasource; + this.metadata.contact = this.metadataImportSettings.metadata.contact; + this.metadata.lastUpdate = this.metadataImportSettings.metadata.lastUpdate; + this.metadata.description = this.metadataImportSettings.metadata.description; + this.metadata.databasis = this.metadataImportSettings.metadata.databasis; + + this.datasetName = this.metadataImportSettings.datasetName; + + this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid( + 'georesourceAddRoleManagementTable', + this.roleManagementTableOptions, + this.kommonitorDataExchangeService.accessControl, + this.metadataImportSettings.allowedRoles + ); + + // georesource specific properties + this.isPOI = this.metadataImportSettings.isPOI; + this.isLOI = this.metadataImportSettings.isLOI; + this.isAOI = this.metadataImportSettings.isAOI; + + if (this.metadataImportSettings.isPOI) { + this.georesourceType = "poi"; + } else if (this.metadataImportSettings.isLOI) { + this.georesourceType = "loi"; + } else { + this.georesourceType = "aoi"; + } + + this.availablePoiMarkerColors.forEach((option: any) => { + if (option.colorName === this.metadataImportSettings.poiMarkerColor) { + this.selectedPoiMarkerColor = option; + } + if (option.colorName === this.metadataImportSettings.poiSymbolColor) { + this.selectedPoiSymbolColor = option; + } + }); + + this.availableLoiDashArrayObjects.forEach((option: any) => { + if (option.dashArrayValue === this.metadataImportSettings.loiDashArrayString) { + this.selectedLoiDashArrayObject = option; + this.onChangeLoiDashArray(this.selectedLoiDashArrayObject); + } + }); + + this.loiColor = this.metadataImportSettings.loiColor; + this.loiWidth = this.metadataImportSettings.loiWidth; + this.aoiColor = this.metadataImportSettings.aoiColor; + this.selectedPoiIconName = this.metadataImportSettings.poiSymbolBootstrap3Name; + + const topicHierarchy = this.kommonitorDataExchangeService.getTopicHierarchyForTopicId(this.metadataImportSettings.topicReference); + + if (topicHierarchy && topicHierarchy[0]) { + this.georesourceTopic_mainTopic = topicHierarchy[0]; + } + if (topicHierarchy && topicHierarchy[1]) { + this.georesourceTopic_subTopic = topicHierarchy[1]; + } + if (topicHierarchy && topicHierarchy[2]) { + this.georesourceTopic_subsubTopic = topicHierarchy[2]; + } + if (topicHierarchy && topicHierarchy[3]) { + this.georesourceTopic_subsubsubTopic = topicHierarchy[3]; + } + } + + private parseFromMappingConfigFile(event: any): void { + this.mappingConfigImportSettings = JSON.parse(event.target.result); + + if (!this.mappingConfigImportSettings.converter || !this.mappingConfigImportSettings.dataSource || !this.mappingConfigImportSettings.propertyMapping) { + console.error("uploaded MappingConfig File cannot be parsed - wrong structure."); + this.georesourceMappingConfigImportError = "Struktur der Datei stimmt nicht mit erwartetem Muster überein."; + this.showMappingConfigErrorAlert(); + return; + } + + this.converter = undefined; + for (const converter of this.kommonitorImporterHelperService.availableConverters) { + if (converter.name === this.mappingConfigImportSettings.converter.name) { + this.converter = converter; + break; + } + } + + this.schema = ''; + if (this.converter && this.converter.schemas && this.mappingConfigImportSettings.converter.schema) { + for (const schema of this.converter.schemas) { + if (schema === this.mappingConfigImportSettings.converter.schema) { + this.schema = schema; + } + } + } + + this.mimeType = ''; + if (this.converter && this.converter.mimeTypes && this.mappingConfigImportSettings.converter.mimeType) { + for (const mimeType of this.converter.mimeTypes) { + if (mimeType === this.mappingConfigImportSettings.converter.mimeType) { + this.mimeType = mimeType; + } + } + } + + this.datasourceType = undefined; + for (const datasourceType of this.kommonitorImporterHelperService.availableDatasourceTypes) { + if (datasourceType.type === this.mappingConfigImportSettings.dataSource.type) { + this.datasourceType = datasourceType; + break; + } + } + + // converter parameters + if (this.converter) { + for (const convParameter of this.mappingConfigImportSettings.converter.parameters) { + const element = document.getElementById("converterParameter_georesourceAdd_" + convParameter.name) as HTMLInputElement; + if (element) { + element.value = convParameter.value; + } + } + } + + // datasourceTypes parameters + if (this.datasourceType) { + for (const dsParameter of this.mappingConfigImportSettings.dataSource.parameters) { + const element = document.getElementById("datasourceTypeParameter_georesourceAdd_" + dsParameter.name) as HTMLInputElement; + if (element) { + element.value = dsParameter.value; + } + } + } + + // property Mapping + this.georesourceDataSourceNameProperty = this.mappingConfigImportSettings.propertyMapping.nameProperty; + this.georesourceDataSourceIdProperty = this.mappingConfigImportSettings.propertyMapping.identifierProperty; + this.validityStartDate_perFeature = this.mappingConfigImportSettings.propertyMapping.validStartDateProperty; + this.validityEndDate_perFeature = this.mappingConfigImportSettings.propertyMapping.validEndDateProperty; + this.keepAttributes = this.mappingConfigImportSettings.propertyMapping.keepAttributes; + this.keepMissingValues = this.mappingConfigImportSettings.propertyMapping.keepMissingOrNullValueAttributes; + this.attributeMappings_adminView = []; + + for (const attributeMapping of this.mappingConfigImportSettings.propertyMapping.attributes) { + const tmpEntry: any = { + "sourceName": attributeMapping.name, + "destinationName": attributeMapping.mappingName + }; + + for (const dataType of this.kommonitorImporterHelperService.attributeMapping_attributeTypes) { + if (dataType.apiName === attributeMapping.type) { + tmpEntry.dataType = dataType; + } + } + + this.attributeMappings_adminView.push(tmpEntry); + } + + if (this.mappingConfigImportSettings.periodOfValidity) { + this.periodOfValidity = { + startDate: this.mappingConfigImportSettings.periodOfValidity.startDate, + endDate: this.mappingConfigImportSettings.periodOfValidity.endDate + }; + this.periodOfValidityInvalid = false; + } + } + + private downloadFile(content: string, fileName: string): void { + const blob = new Blob([content], { type: "application/json" }); + const data = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.download = fileName; + a.href = data; + a.textContent = "JSON"; + a.target = "_blank"; + a.rel = "noopener noreferrer"; + a.click(); + + a.remove(); + } + + // Alert methods + hideSuccessAlert(): void { + this.successMessage = ''; + } + + hideErrorAlert(): void { + this.errorMessage = ''; + } + + hideMetadataErrorAlert(): void { + this.georesourceMetadataImportError = ''; + } + + hideMappingConfigErrorAlert(): void { + this.georesourceMappingConfigImportError = ''; + } + + private showMetadataErrorAlert(): void { + // Implementation for showing metadata error alert + } + + private showMappingConfigErrorAlert(): void { + // Implementation for showing mapping config error alert + } + + // Form reset + resetGeoresourceAddForm(): void { + this.importerErrors = []; + this.successMessagePart = ''; + this.errorMessagePart = ''; + + this.datasetName = ''; + this.datasetNameInvalid = false; + + this.metadata = { + note: '', + literature: '', + updateInterval: null, + sridEPSG: 4326, + datasource: '', + databasis: '', + contact: '', + lastUpdate: '', + description: '' + }; + + this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid( + 'georesourceAddRoleManagementTable', + null, + this.kommonitorDataExchangeService.accessControl, + null + ); + + this.georesourceTopic_mainTopic = null; + this.georesourceTopic_subTopic = null; + this.georesourceTopic_subsubTopic = null; + this.georesourceTopic_subsubsubTopic = null; + + this.georesourceType = 'poi'; + this.isPOI = true; + this.isLOI = false; + this.isAOI = false; + this.selectedPoiMarkerColor = this.availablePoiMarkerColors[0] || null; + this.selectedPoiSymbolColor = this.availablePoiMarkerColors[1] || null; + this.selectedLoiDashArrayObject = this.availableLoiDashArrayObjects[0] || null; + this.loiColor = '#bf3d2c'; + this.loiWidth = 3; + this.aoiColor = '#bf3d2c'; + this.selectedPoiIconName = 'home'; + this.selectedPoiMarkerStyle = 'symbol'; + this.poiMarkerText = ''; + this.poiMarkerTextInvalid = false; + + this.periodOfValidity = { + startDate: '', + endDate: '' + }; + this.periodOfValidityInvalid = false; + + this.geoJsonString = null; + this.georesource_asGeoJson = null; + + this.georesourceDataSourceInputInvalidReason = ''; + this.georesourceDataSourceInputInvalid = false; + + this.georesourceDataSourceIdProperty = ''; + this.georesourceDataSourceNameProperty = ''; + + this.converter = null; + this.schema = ''; + this.mimeType = ''; + this.datasourceType = null; + + this.converterDefinition = null; + this.datasourceTypeDefinition = null; + this.propertyMappingDefinition = null; + this.postBody_georesources = null; + + this.validityEndDate_perFeature = ''; + this.validityStartDate_perFeature = ''; + + this.attributeMapping_sourceAttributeName = ''; + this.attributeMapping_destinationAttributeName = ''; + this.attributeMapping_data = null; + this.attributeMapping_attributeType = this.kommonitorImporterHelperService.attributeMapping_attributeTypes[0]; + this.attributeMappings_adminView = []; + this.keepAttributes = true; + this.keepMissingValues = true; + + this.ownerOrganization = ''; + this.ownerOrgFilter = ''; + this.isPublic = false; + + this.metadataImportSettings = null; + this.mappingConfigImportSettings = null; + this.georesourceMetadataImportError = ''; + this.georesourceMappingConfigImportError = ''; + } + + // Build post body for API request + buildPostBody_georesources(): any { + const postBody: any = { + "geoJsonString": "", // will be set by importer + "allowedRoles": [], + "metadata": { + "note": this.metadata.note, + "literature": this.metadata.literature, + "updateInterval": this.metadata.updateInterval?.apiName, + "sridEPSG": this.metadata.sridEPSG || 4326, + "datasource": this.metadata.datasource, + "contact": this.metadata.contact, + "lastUpdate": this.metadata.lastUpdate, + "description": this.metadata.description, + "databasis": this.metadata.databasis + }, + "jsonSchema": null, + "datasetName": this.datasetName, + "periodOfValidity": { + "endDate": this.periodOfValidity.endDate, + "startDate": this.periodOfValidity.startDate + }, + "isAOI": this.isAOI, + "isLOI": this.isLOI, + "isPOI": this.isPOI, + "topicReference": null, + "ownerId": this.ownerOrganization, + "isPublic": this.isPublic + }; + + if (this.roleManagementTableOptions) { + const roleIds = this.kommonitorDataGridHelperService.getSelectedRoleIds_roleManagementGrid(this.roleManagementTableOptions); + if (roleIds && Array.isArray(roleIds)) { + for (const roleId of roleIds) { + postBody.allowedRoles.push(roleId); + } + } + } + + if (this.isPOI) { + postBody["poiSymbolBootstrap3Name"] = this.selectedPoiIconName; + postBody["poiSymbolColor"] = (this.selectedPoiSymbolColor as any)?.colorName || ''; + postBody["poiMarkerColor"] = (this.selectedPoiMarkerColor as any)?.colorName || ''; + postBody["poiMarkerStyle"] = this.selectedPoiMarkerStyle; + postBody["poiMarkerText"] = this.poiMarkerText; + + postBody["loiDashArrayString"] = null; + postBody["loiColor"] = null; + postBody["loiWidth"] = 3; + + postBody["aoiColor"] = null; + } else if (this.isLOI) { + postBody["poiSymbolBootstrap3Name"] = null; + postBody["poiSymbolColor"] = null; + postBody["poiMarkerColor"] = null; + postBody["poiMarkerStyle"] = null; + postBody["poiMarkerText"] = null; + + postBody["loiDashArrayString"] = (this.selectedLoiDashArrayObject as any)?.dashArrayValue || ''; + postBody["loiColor"] = this.loiColor; + postBody["loiWidth"] = this.loiWidth; + + postBody["aoiColor"] = null; + } else if (this.isAOI) { + postBody["poiSymbolBootstrap3Name"] = null; + postBody["poiSymbolColor"] = null; + postBody["poiMarkerColor"] = null; + postBody["poiMarkerStyle"] = null; + postBody["poiMarkerText"] = null; + + postBody["loiDashArrayString"] = null; + postBody["loiColor"] = null; + postBody["loiWidth"] = 3; + + postBody["aoiColor"] = this.aoiColor; + } + + // TOPIC REFERENCE + if (this.georesourceTopic_subsubsubTopic) { + postBody.topicReference = this.georesourceTopic_subsubsubTopic.topicId; + } else if (this.georesourceTopic_subsubTopic) { + postBody.topicReference = this.georesourceTopic_subsubTopic.topicId; + } else if (this.georesourceTopic_subTopic) { + postBody.topicReference = this.georesourceTopic_subTopic.topicId; + } else if (this.georesourceTopic_mainTopic) { + postBody.topicReference = this.georesourceTopic_mainTopic.topicId; + } else { + postBody.topicReference = ""; + } + + return postBody; + } + + // Main add method + async addGeoresource(): Promise { + this.loadingData = true; + this.importerErrors = []; + this.successMessagePart = ''; + this.errorMessagePart = ''; + + try { + // Build importer objects + const allDataSpecified = await this.buildImporterObjects(); + + if (!allDataSpecified) { + // Validation failed + this.loadingData = false; + return; + } + + // Perform dry run + const newGeoresourceResponse_dryRun = await this.kommonitorImporterHelperService.registerNewGeoresource( + this.converterDefinition, + this.datasourceTypeDefinition, + this.propertyMappingDefinition, + this.postBody_georesources, + true + ); + + if (!this.kommonitorImporterHelperService.importerResponseContainsErrors(newGeoresourceResponse_dryRun)) { + // all good, really execute the request to import data against data management API + const newGeoresourceResponse = await this.kommonitorImporterHelperService.registerNewGeoresource( + this.converterDefinition, + this.datasourceTypeDefinition, + this.propertyMappingDefinition, + this.postBody_georesources, + false + ); + + // Broadcast refresh events + this.broadcastService.broadcast('refreshGeoresourceOverviewTable', { action: 'add', id: this.kommonitorImporterHelperService.getIdFromImporterResponse(newGeoresourceResponse) }); + + // refresh all admin dashboard diagrams due to modified metadata + setTimeout(() => { + this.broadcastService.broadcast('refreshAdminDashboardDiagrams'); + }, 500); + + this.successMessagePart = this.postBody_georesources.datasetName; + this.importedFeatures = this.kommonitorImporterHelperService.getImportedFeaturesFromImporterResponse(newGeoresourceResponse); + + this.successMessage = 'Georessource erfolgreich registriert'; + this.activeModal.close(true); + } else { + // errors occurred + this.errorMessagePart = "Einige der zu importierenden Features des Datensatzes weisen kritische Fehler auf"; + this.importerErrors = this.kommonitorImporterHelperService.getErrorsFromImporterResponse(newGeoresourceResponse_dryRun); + this.errorMessage = 'Validierung fehlgeschlagen'; + } + } catch (error: any) { + if (error.data) { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error.data); + } else { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error); + } + + this.errorMessage = 'Fehler beim Registrieren der Georessource'; + console.error('Error adding georesource:', error); + } finally { + this.loadingData = false; + } + } + + private async buildImporterObjects(): Promise { + this.converterDefinition = this.buildConverterDefinition(); + this.datasourceTypeDefinition = await this.buildDatasourceTypeDefinition(); + this.propertyMappingDefinition = this.buildPropertyMappingDefinition(); + this.postBody_georesources = this.buildPostBody_georesources(); + + if (!this.converterDefinition || !this.datasourceTypeDefinition || !this.propertyMappingDefinition || !this.postBody_georesources) { + return false; + } + + return true; + } + + private buildConverterDefinition(): any { + return this.kommonitorImporterHelperService.buildConverterDefinition( + this.converter, + "converterParameter_georesourceAdd_", + this.schema, + this.mimeType + ); + } + + private async buildDatasourceTypeDefinition(): Promise { + try { + return await this.kommonitorImporterHelperService.buildDatasourceTypeDefinition( + this.datasourceType, + 'datasourceTypeParameter_georesourceAdd_', + 'georesourceDataSourceInput_add' + ); + } catch (error: any) { + if (error.data) { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error.data); + } else { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error); + } + + this.loadingData = false; + return null; + } + } + + private buildPropertyMappingDefinition(): any { + return this.kommonitorImporterHelperService.buildPropertyMapping_spatialResource( + this.georesourceDataSourceNameProperty, + this.georesourceDataSourceIdProperty, + this.validityStartDate_perFeature, + this.validityEndDate_perFeature, + undefined, + this.keepAttributes, + this.keepMissingValues, + this.attributeMappings_adminView + ); + } + + // Modal control methods + cancel(): void { + this.activeModal.dismiss(); + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceBatchUpdateModal/georesource-batch-update-modal.component.css b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceBatchUpdateModal/georesource-batch-update-modal.component.css new file mode 100644 index 000000000..d16e59c82 --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceBatchUpdateModal/georesource-batch-update-modal.component.css @@ -0,0 +1,155 @@ +/* Batch Update Modal Styles */ +.batch-list-table-wrapper { + max-height: 60vh; + overflow: auto; +} + +.batch-list-table { + font-size: 11px; + min-width: 100%; +} + +.batch-list-table-sticky-column { + position: sticky; + background: white; + z-index: 10; +} + +.batch-list-table-sticky-column-1 { + left: 0; + min-width: 40px; + width: 40px; +} + +.batch-list-table-sticky-column-2 { + left: 40px; + min-width: 200px; + width: 200px; +} + +.batch-list-table-sticky-column-header { + top: 0; + z-index: 11; +} + +.batch-list-table-sticky-column-footer { + bottom: 0; + z-index: 11; +} + +.batch-list-table-name-field { + min-width: 180px; +} + +.batch-list-odd-rows { + background-color: #f9f9f9; +} + +.batch-list-even-rows { + background-color: #ffffff; +} + +/* Switch styles for toggle buttons */ +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.switchslider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; +} + +.switchslider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .switchslider { + background-color: #2196F3; +} + +input:focus + .switchslider { + box-shadow: 0 0 1px #2196F3; +} + +input:checked + .switchslider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); +} + +.switchslider.round { + border-radius: 34px; +} + +.switchslider.round:before { + border-radius: 50%; +} + +/* Loading overlay */ +.loading-overlay-admin-panel { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1000; + font-size: 2em; + color: #337ab7; +} + +.loading-overlay-admin-panel.ng-hide { + display: none; +} + +.icon-spin { + animation: spin 2s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Form adjustments */ +.form-control { + font-size: 11px; +} + +.btn-sm { + font-size: 11px; +} + +/* Table input fields */ +.georesourceMappingTableInputField, +.georesourceDataSourceFileInputField { + font-size: 11px; +} + +/* Input group date picker styling */ +.input-group-addon { + padding: 6px 8px; + font-size: 11px; +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceBatchUpdateModal/georesource-batch-update-modal.component.html b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceBatchUpdateModal/georesource-batch-update-modal.component.html new file mode 100644 index 000000000..d155ff0b9 --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceBatchUpdateModal/georesource-batch-update-modal.component.html @@ -0,0 +1,607 @@ + + + + + \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceBatchUpdateModal/georesource-batch-update-modal.component.ts b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceBatchUpdateModal/georesource-batch-update-modal.component.ts new file mode 100644 index 000000000..357572aea --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceBatchUpdateModal/georesource-batch-update-modal.component.ts @@ -0,0 +1,288 @@ +import { Component, OnInit, Inject, ViewChild, ElementRef, OnDestroy } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; +import { HttpClient } from '@angular/common/http'; +import { Subscription } from 'rxjs'; +import { FormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'georesource-batch-update-modal-new', + templateUrl: './georesource-batch-update-modal.component.html', + styleUrls: ['./georesource-batch-update-modal.component.css'] +}) +export class GeoresourceBatchUpdateModalComponent implements OnInit, OnDestroy { + @ViewChild('batchListFile', { static: false }) batchListFile!: ElementRef; + + // Component state + loadingData = false; + isFirstStart = true; + lastUpdateResponseObj: any = undefined; + keepMissingValues = true; + + // Batch list + batchList: any[] = []; + allRowsSelected = false; + + // Default value function + colDefaultFunctionSelectedColumn: string = ''; + colDefaultFunctionNewValue: any = undefined; + colDefaultFunctionAllRowsChb = false; + + // Subscriptions + private subscriptions: Subscription[] = []; + + constructor( + public activeModal: NgbActiveModal, + @Inject('kommonitorDataExchangeService') public kommonitorDataExchangeService: any, + @Inject('kommonitorImporterHelperService') public kommonitorImporterHelperService: any, + @Inject('kommonitorBatchUpdateHelperService') public kommonitorBatchUpdateHelperService: any, + private broadcastService: BroadcastService, + private http: HttpClient + ) {} + + ngOnInit(): void { + this.initialize(); + this.setupEventListeners(); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(sub => sub.unsubscribe()); + } + + private initialize(): void { + if (this.isFirstStart) { + this.kommonitorBatchUpdateHelperService.addNewRowToBatchList('georesource', this.batchList); + this.isFirstStart = false; + } + + // Initialize date pickers + setTimeout(() => { + this.initializeDatePickers(); + }); + } + + private initializeDatePickers(): void { + try { + // Initialize default column date pickers + const startDatePicker = document.getElementById('georesourceDefaultColumnDatePickerStart'); + const endDatePicker = document.getElementById('georesourceDefaultColumnDatePickerEnd'); + + if (startDatePicker && (window as any).$) { + (window as any).$('#georesourceDefaultColumnDatePickerStart').datepicker(this.kommonitorDataExchangeService.datePickerOptions); + } + if (endDatePicker && (window as any).$) { + (window as any).$('#georesourceDefaultColumnDatePickerEnd').datepicker(this.kommonitorDataExchangeService.datePickerOptions); + } + + // Initialize row date pickers + this.kommonitorBatchUpdateHelperService.initializeGeoresourceDatepickerFields(this.batchList); + } catch (error) { + console.warn('Date picker initialization failed:', error); + } + } + + private setupEventListeners(): void { + // Listen for georesource overview table refresh + const refreshSub = this.broadcastService.currentBroadcastMsg.subscribe((data: any) => { + if (data.msg === 'refreshGeoresourceOverviewTableCompleted') { + this.kommonitorBatchUpdateHelperService.refreshNameColumn('georesource', this.batchList); + } + }); + this.subscriptions.push(refreshSub); + + // Listen for batch update completion + const batchUpdateSub = this.broadcastService.currentBroadcastMsg.subscribe((data: any) => { + if (data.msg === 'batchUpdateCompleted' && data.resourceType === 'georesource') { + this.lastUpdateResponseObj = data; + } + }); + this.subscriptions.push(batchUpdateSub); + + // Listen for batch list parsing + const batchListSub = this.broadcastService.currentBroadcastMsg.subscribe((data: any) => { + if (data.msg === 'georesourceBatchListParsed') { + this.onBatchListParsed(data.newValue); + } + }); + this.subscriptions.push(batchListSub); + } + + // File handling methods + onMappingTableFileSelected(event: any, index: number): void { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.addEventListener('load', (event: any) => { + this.kommonitorBatchUpdateHelperService.onMappingTableSelected('georesource', event, index, file, this.batchList); + }); + reader.readAsText(file); + } + } + + onDataSourceFileSelected(event: any, index: number): void { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.addEventListener('load', () => { + this.kommonitorBatchUpdateHelperService.onDataSourceFileSelected(file, index, this.batchList); + }); + reader.readAsText(file); + } + } + + onBatchListFileSelected(event: any): void { + const file = event.target.files[0]; + if (file) { + this.kommonitorBatchUpdateHelperService.parseBatchListFromFile('georesource', file, this.batchList); + } + } + + private onBatchListParsed(newBatchList: any[]): void { + setTimeout(() => { + // Remove all rows + for (let i = 0; i < this.batchList.length; i++) { + this.batchList[i].isSelected = true; + } + this.kommonitorBatchUpdateHelperService.deleteSelectedRowsFromBatchList(this.batchList, this.allRowsSelected); + + // Add new rows + for (let i = 0; i < newBatchList.length; i++) { + this.kommonitorBatchUpdateHelperService.addNewRowToBatchList('georesource', this.batchList); + const row = this.batchList[i]; + + // isSelected + row.isSelected = newBatchList[i].isSelected; + + // name - convert georesourceId to georesource object + const georesourceId = newBatchList[i].name; + const georesourceObj = this.kommonitorDataExchangeService.getGeoresourceMetadataById(georesourceId); + row.name = georesourceObj; + + // mappingTableName + row.mappingTableName = newBatchList[i].mappingTableName; + // mappingObj + row.mappingObj = newBatchList[i].mappingObj; + + // converter parameters to properties + if (row.mappingObj.converter) { + row.mappingObj.converter = this.kommonitorBatchUpdateHelperService.converterParametersArrayToProperties(row.mappingObj.converter); + } + + // dataSource parameters to properties + if (row.mappingObj.dataSource) { + row.mappingObj.dataSource = this.kommonitorBatchUpdateHelperService.dataSourceParametersArrayToProperty(row.mappingObj.dataSource); + } + + // set selectedConverter + if (newBatchList[i].mappingObj.converter && newBatchList[i].mappingObj.converter.hasOwnProperty('name')) { + row.selectedConverter = this.kommonitorBatchUpdateHelperService.getConverterObjectByName(newBatchList[i].mappingObj.converter.name); + } + + // set selectedDatasourceType + if (newBatchList[i].mappingObj.dataSource && newBatchList[i].mappingObj.dataSource.hasOwnProperty('type')) { + row.selectedDatasourceType = this.kommonitorBatchUpdateHelperService.getDatasourceTypeObjectByType(newBatchList[i].mappingObj.dataSource.type); + } + } + + this.kommonitorBatchUpdateHelperService.initializeGeoresourceDatepickerFields(this.batchList); + this.kommonitorBatchUpdateHelperService.resizeNameColumnDropdowns(null); + }); + } + + // Batch list operations + addNewRow(): void { + this.kommonitorBatchUpdateHelperService.addNewRowToBatchList('georesource', this.batchList); + } + + deleteSelectedRows(): void { + this.kommonitorBatchUpdateHelperService.deleteSelectedRowsFromBatchList(this.batchList, this.allRowsSelected); + } + + onSelectAllRows(): void { + this.kommonitorBatchUpdateHelperService.onChangeSelectAllRows(this.allRowsSelected, this.batchList); + } + + onGeoresourceSelected(georesource: any, index: number): void { + this.kommonitorBatchUpdateHelperService.resizeNameColumnDropdowns(georesource); + } + + // Default value function + onChangeDefaultColumn(): void { + this.colDefaultFunctionNewValue = undefined; + } + + saveDefaultValue(): void { + this.kommonitorBatchUpdateHelperService.onClickSaveColDefaultValue( + 'georesource', + this.colDefaultFunctionSelectedColumn, + this.colDefaultFunctionNewValue, + this.colDefaultFunctionAllRowsChb, + this.batchList + ); + } + + // Import/Export methods + loadBatchList(): void { + this.batchListFile.nativeElement.click(); + } + + exportBatchList(): void { + this.kommonitorBatchUpdateHelperService.saveBatchListToFile('georesource', this.batchList, true, this.keepMissingValues); + } + + saveMappingObjectToFile(event: any): void { + this.kommonitorBatchUpdateHelperService.saveMappingObjectToFile('georesource', event, this.batchList); + } + + // Batch update execution + executeBatchUpdate(): void { + this.kommonitorBatchUpdateHelperService.batchUpdate('georesource', this.batchList); + } + + canExecuteBatchUpdate(): boolean { + return this.kommonitorBatchUpdateHelperService.checkIfNameAndFilesChosenInEachRow('georesource', this.batchList); + } + + reopenResultModal(): void { + if (this.lastUpdateResponseObj !== undefined) { + this.broadcastService.broadcast('reopenBatchUpdateResultModal', this.lastUpdateResponseObj); + } + } + + resetForm(): void { + this.kommonitorBatchUpdateHelperService.resetBatchUpdateForm('georesource', this.batchList); + } + + // Helper methods for template + checkColumnsToShow_selectedConverter(): string[] { + return this.kommonitorBatchUpdateHelperService.checkColumnsToShow_selectedConverter(this.batchList); + } + + checkIfSelectedDatasourceTypeIsFile(): boolean { + return this.kommonitorBatchUpdateHelperService.checkIfSelectedDatasourceTypeIsFile(this.batchList); + } + + checkIfSelectedDatasourceTypeIsHttp(): boolean { + return this.kommonitorBatchUpdateHelperService.checkIfSelectedDatasourceTypeIsHttp(this.batchList); + } + + checkIfSelectedDatasourceTypeIsInline(): boolean { + return this.kommonitorBatchUpdateHelperService.checkIfSelectedDatasourceTypeIsInline(this.batchList); + } + + getConverterObjectByName(name: string): any { + return this.kommonitorBatchUpdateHelperService.getConverterObjectByName(name); + } + + // Filter for georesources + filterGeoresources = (georesource: any, searchTerm: string): boolean => { + if (!searchTerm) return true; + return georesource.datasetName.toLowerCase().includes(searchTerm.toLowerCase()); + }; + + // Modal control + cancel(): void { + this.activeModal.dismiss(); + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceDeleteModal/georesource-delete-modal.component.css b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceDeleteModal/georesource-delete-modal.component.css new file mode 100644 index 000000000..458b35608 --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceDeleteModal/georesource-delete-modal.component.css @@ -0,0 +1,183 @@ +/* Loading overlay */ +.loading-overlay-admin-panel { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 9999; + color: #007bff; + font-size: 2rem; +} + +.icon-spin { + animation: spin 1s infinite linear; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Modal styling */ +.modal-header { + padding: 15px; + border-bottom: 1px solid #e5e5e5; +} + +.modal-title { + margin: 0; + line-height: 1.42857143; +} + +.btn-close { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; +} + +.modal-body { + position: relative; + padding: 15px; + max-height: 70vh; + overflow-y: auto; +} + +.modal-footer { + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} + +/* Alert styling */ +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} + +.alert-success { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.alert-danger { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} + +.alert-dismissible .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; + background: none; + border: none; + font-size: 1.3rem; + cursor: pointer; +} + +/* Table styling */ +.table { + width: 100%; + max-width: 100%; + margin-bottom: 20px; +} + +.table-bordered { + border: 1px solid #ddd; +} + +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #ddd; +} + +.table-condensed > thead > tr > th, +.table-condensed > tbody > tr > th, +.table-condensed > tfoot > tr > th, +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > td { + padding: 5px; +} + +/* Button styling */ +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + border: 1px solid transparent; + border-radius: 4px; +} + +.btn-default { + color: #333; + background-color: #fff; + border-color: #ccc; +} + +.btn-danger { + color: #fff; + background-color: #d9534f; + border-color: #d43f3a; +} + +.btn-danger:disabled { + opacity: 0.65; + cursor: not-allowed; +} + +.pull-left { + float: left; +} + +/* Typography */ +h3, h4 { + margin-top: 20px; + margin-bottom: 10px; +} + +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + color: #333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; +} + +ul { + margin-top: 0; + margin-bottom: 10px; +} + +/* Responsive table */ +@media (max-width: 768px) { + .table-responsive { + width: 100%; + margin-bottom: 15px; + overflow-y: hidden; + overflow-x: auto; + border: 1px solid #ddd; + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceDeleteModal/georesource-delete-modal.component.html b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceDeleteModal/georesource-delete-modal.component.html new file mode 100644 index 000000000..1894980f7 --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceDeleteModal/georesource-delete-modal.component.html @@ -0,0 +1,176 @@ + + + + + \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceDeleteModal/georesource-delete-modal.component.ts b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceDeleteModal/georesource-delete-modal.component.ts new file mode 100644 index 000000000..bd21330a0 --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceDeleteModal/georesource-delete-modal.component.ts @@ -0,0 +1,278 @@ +import { Component, OnInit, OnDestroy, Inject } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; +import { HttpClient } from '@angular/common/http'; +import { Subscription, forkJoin } from 'rxjs'; +import { tap, catchError } from 'rxjs/operators'; +import { of } from 'rxjs'; + +declare const $: any; + +interface AffectedScript { + scriptId: string; + name: string; + description: string; + indicatorId: string; +} + +interface AffectedIndicatorReference { + indicatorMetadata: { + indicatorId: string; + indicatorName: string; + characteristicValue: string; + indicatorType: string; + description: string; + }; + georesourceReference: { + referencedGeoresourceId: string; + referencedGeoresourceName: string; + referencedGeoresourceDescription: string; + }; +} + +@Component({ + selector: 'georesource-delete-modal-new', + templateUrl: './georesource-delete-modal.component.html', + styleUrls: ['./georesource-delete-modal.component.css'] +}) +export class GeoresourceDeleteModalComponent implements OnInit, OnDestroy { + + datasetsToDelete: any[] = []; + loadingData: boolean = false; + + successfullyDeletedDatasets: any[] = []; + failedDatasetsAndErrors: [any, string][] = []; + + affectedScripts: AffectedScript[] = []; + affectedIndicatorReferences: AffectedIndicatorReference[] = []; + + // Alert states + showSuccessAlert: boolean = false; + showErrorAlert: boolean = false; + successMessage: string = ''; + errorMessage: string = ''; + + // Subscriptions + private subscriptions: Subscription[] = []; + + constructor( + public activeModal: NgbActiveModal, + @Inject('kommonitorDataExchangeService') public kommonitorDataExchangeService: any, + private broadcastService: BroadcastService, + private http: HttpClient + ) { + console.log('GeoresourceDeleteModalComponent constructor initialized'); + } + + ngOnInit(): void { + this.setupEventListeners(); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(subscription => subscription.unsubscribe()); + } + + private setupEventListeners(): void { + // Listen for broadcast events + const deleteSubscription = this.broadcastService.currentBroadcastMsg.subscribe(broadcastMsg => { + if (broadcastMsg.msg === 'onDeleteGeoresources') { + this.onDeleteGeoresources(Array.isArray(broadcastMsg.values) ? broadcastMsg.values : [broadcastMsg.values]); + } + }); + this.subscriptions.push(deleteSubscription); + } + + onDeleteGeoresources(datasets: any[]): void { + console.log('onDeleteGeoresources called with datasets:', datasets); + this.loadingData = true; + this.datasetsToDelete = datasets; + this.resetGeoresourcesDeleteForm(); + + setTimeout(() => { + this.loadingData = false; + }, 250); + } + + resetGeoresourcesDeleteForm(): void { + console.log('Resetting delete form'); + this.successfullyDeletedDatasets = []; + this.failedDatasetsAndErrors = []; + this.affectedScripts = this.gatherAffectedScripts(); + this.affectedIndicatorReferences = this.gatherAffectedIndicatorReferences(); + this.hideSuccessAlert(); + this.hideErrorAlert(); + } + + gatherAffectedScripts(): AffectedScript[] { + const affectedScripts: AffectedScript[] = []; + + if (this.kommonitorDataExchangeService && this.kommonitorDataExchangeService.availableScripts) { + this.datasetsToDelete.forEach(dataset => { + this.kommonitorDataExchangeService.availableScripts.forEach((script: any) => { + if (script.requiredGeoresources) { + script.requiredGeoresources.forEach((requiredGeoresource: any) => { + if (requiredGeoresource.referencedGeoresourceId === dataset.georesourceId) { + affectedScripts.push({ + scriptId: script.scriptId, + name: script.name, + description: script.description, + indicatorId: script.indicatorId + }); + } + }); + } + }); + }); + } + + return affectedScripts; + } + + gatherAffectedIndicatorReferences(): AffectedIndicatorReference[] { + const affectedIndicatorReferences: AffectedIndicatorReference[] = []; + + if (this.kommonitorDataExchangeService && this.kommonitorDataExchangeService.availableIndicators) { + this.datasetsToDelete.forEach(dataset => { + this.kommonitorDataExchangeService.availableIndicators.forEach((indicator: any) => { + if (indicator.referencedGeoresources) { + indicator.referencedGeoresources.forEach((georesourceReference: any) => { + if (georesourceReference.referencedGeoresourceId === dataset.georesourceId) { + affectedIndicatorReferences.push({ + indicatorMetadata: { + indicatorId: indicator.indicatorId, + indicatorName: indicator.indicatorName, + characteristicValue: indicator.characteristicValue, + indicatorType: indicator.indicatorType, + description: indicator.description + }, + georesourceReference: georesourceReference + }); + } + }); + } + }); + }); + } + + return affectedIndicatorReferences; + } + + deleteGeoresources(): void { + console.log('Starting deletion of georesources'); + this.loadingData = true; + + const deletePromises = this.datasetsToDelete.map(dataset => this.getDeleteDatasetPromise(dataset)); + + forkJoin(deletePromises).subscribe({ + next: (results) => { + console.log('All delete operations completed'); + this.handleDeleteResults(); + }, + error: (error) => { + console.error('Error in delete operations:', error); + this.handleDeleteResults(); + } + }); + } + + private getDeleteDatasetPromise(dataset: any) { + const url = `${this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI}/georesources/${dataset.georesourceId}`; + + return this.http.delete(url).pipe( + tap((response) => { + console.log(`Successfully deleted georesource ${dataset.georesourceId}`); + this.successfullyDeletedDatasets.push(dataset); + + // Remove entry from array + const index = this.kommonitorDataExchangeService.availableGeoresources.findIndex( + (geo: any) => geo.georesourceId === dataset.georesourceId + ); + + if (index > -1) { + this.kommonitorDataExchangeService.availableGeoresources.splice(index, 1); + } + }), + catchError((error) => { + console.error(`Failed to delete georesource ${dataset.georesourceId}:`, error); + const errorMessage = error.error ? + this.kommonitorDataExchangeService.syntaxHighlightJSON(error.error) : + this.kommonitorDataExchangeService.syntaxHighlightJSON(error); + this.failedDatasetsAndErrors.push([dataset, errorMessage]); + + // Return a resolved observable so forkJoin continues + return of(null); + }) + ); + } + + private handleDeleteResults(): void { + if (this.failedDatasetsAndErrors.length > 0) { + this.showErrorAlert = true; + this.errorMessage = 'Löschen gescheitert'; + } + + if (this.successfullyDeletedDatasets.length > 0) { + this.showSuccessAlert = true; + this.successMessage = 'Folgende Georessourcen sowie assoziierte Indikatorenreferenzen und Skripte wurden erfolgreich gelöscht'; + + // Refresh overview table + this.broadcastService.broadcast('refreshGeoresourceOverviewTable', { + crudType: 'delete', + targetIds: this.successfullyDeletedDatasets.map(dataset => dataset.georesourceId) + }); + + // Refresh admin dashboard diagrams + setTimeout(() => { + this.broadcastService.broadcast('refreshAdminDashboardDiagrams', null); + }, 500); + } + + setTimeout(() => { + this.loadingData = false; + }, 500); + } + + // Filter methods for template + getPoiDatasets(): any[] { + return this.datasetsToDelete.filter(dataset => dataset.isPOI); + } + + getLoiDatasets(): any[] { + return this.datasetsToDelete.filter(dataset => dataset.isLOI); + } + + getAoiDatasets(): any[] { + return this.datasetsToDelete.filter(dataset => dataset.isAOI); + } + + getSuccessfulPoiDatasets(): any[] { + return this.successfullyDeletedDatasets.filter(dataset => dataset.isPOI); + } + + getSuccessfulLoiDatasets(): any[] { + return this.successfullyDeletedDatasets.filter(dataset => dataset.isLOI); + } + + getSuccessfulAoiDatasets(): any[] { + return this.successfullyDeletedDatasets.filter(dataset => dataset.isAOI); + } + + // Alert methods + hideSuccessAlert(): void { + this.showSuccessAlert = false; + } + + hideErrorAlert(): void { + this.showErrorAlert = false; + } + + // TrackBy function for *ngFor + trackByIndex(index: number, item: any): number { + return index; + } + + // Modal control + cancel(): void { + this.activeModal.dismiss('cancel'); + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditFeaturesModal/georesource-edit-features-modal.component.css b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditFeaturesModal/georesource-edit-features-modal.component.css new file mode 100644 index 000000000..eabdf6f17 --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditFeaturesModal/georesource-edit-features-modal.component.css @@ -0,0 +1,289 @@ +/* Georesource Edit Features Modal Styles */ + +/* Multi-step form styles */ +.multiStepForm { + position: relative; + margin: 0 auto; +} + +.multiStepForm fieldset { + background: white; + border: 0 none; + border-radius: 0.5rem; + box-sizing: border-box; + width: 100%; + margin: 0; + padding-bottom: 20px; + position: relative; +} + +.multiStepForm fieldset:not(:first-of-type) { + display: none; +} + +.multiStepForm .fs-title { + font-size: 15px; + text-transform: uppercase; + color: #2C3E50; + margin-bottom: 10px; +} + +.multiStepForm .fs-subtitle { + font-weight: normal; + font-size: 13px; + color: #666; + margin-bottom: 20px; +} + +/* Progress bar */ +#progressbar { + margin-bottom: 30px; + overflow: hidden; + color: lightgrey; + padding-left: 0; +} + +#progressbar li { + list-style-type: none; + font-size: 11px; + width: 33.33%; + float: left; + position: relative; + font-weight: 400; + cursor: pointer; + transition: all 0.3s ease; +} + +#progressbar li:hover { + color: #27AE60; +} + +#progressbar li:hover:before { + background: #27AE60; + transform: scale(1.1); +} + +#progressbar li:before { + width: 24px; + height: 24px; + line-height: 20px; + display: block; + font-size: 10px; + color: #ffffff; + background: lightgray; + border-radius: 50%; + margin: 0 auto 3px auto; + padding: 2px; + transition: all 0.3s ease; +} + +#progressbar li.active:before, +#progressbar li.active:after { + background: #27AE60; +} + +/* Action buttons */ +.action-button { + width: 100px; + background: #27AE60; + font-weight: bold; + color: white; + border: 0 none; + border-radius: 5px; + cursor: pointer; + padding: 10px 5px; + margin: 10px 5px; +} + +.action-button-previous { + width: 100px; + background: #616161; + font-weight: bold; + color: white; + border: 0 none; + border-radius: 5px; + cursor: pointer; + padding: 10px 5px; + margin: 10px 5px; +} + +.action-button:hover, +.action-button:focus { + box-shadow: 0 0 0 2px white, 0 0 0 3px #27AE60; +} + +.action-button-previous:hover, +.action-button-previous:focus { + box-shadow: 0 0 0 2px white, 0 0 0 3px #616161; +} + +/* Switch toggle styles */ +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.switchslider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; +} + +.switchslider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .switchslider { + background-color: #2196F3; +} + +input:focus + .switchslider { + box-shadow: 0 0 1px #2196F3; +} + +input:checked + .switchslider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); +} + +/* Rounded switch */ +.switchslider.round { + border-radius: 34px; +} + +.switchslider.round:before { + border-radius: 50%; +} + +/* Feature table wrapper */ +.featureTableWrapper { + margin: 20px 0; +} + +.admin-table-wrapper { + position: relative; + border: 1px solid #ddd; + border-radius: 4px; +} + +/* Loading overlay */ +.loading-overlay-admin-panel { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1000; + font-size: 2em; + color: #337ab7; +} + +.loading-overlay-admin-panel.ng-hide { + display: none; +} + +.icon-spin { + animation: spin 2s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Form adjustments */ +.form-control { + font-size: 12px; +} + +.help-block { + font-size: 11px; + color: #737373; +} + +/* Alert styles */ +.alert { + margin-bottom: 0; + border-radius: 0; +} + +.alert pre { + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 3px; + padding: 10px; + margin-top: 10px; +} + +/* Table styles */ +.table-condensed { + font-size: 12px; +} + +.table-condensed th, +.table-condensed td { + padding: 5px; + border-top: 1px solid #ddd; +} + +.btn-group-sm > .btn, +.btn-sm { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +/* Vertical alignment helper */ +.vertical-align { + display: flex; + align-items: flex-start; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .vertical-align { + display: block; + } + + .col-md-3, + .col-md-6 { + margin-bottom: 15px; + } + + .switch { + width: 50px; + height: 28px; + } + + .switchslider:before { + height: 20px; + width: 20px; + } + + input:checked + .switchslider:before { + transform: translateX(22px); + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditFeaturesModal/georesource-edit-features-modal.component.html b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditFeaturesModal/georesource-edit-features-modal.component.html new file mode 100644 index 000000000..3d7c37908 --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditFeaturesModal/georesource-edit-features-modal.component.html @@ -0,0 +1,522 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditFeaturesModal/georesource-edit-features-modal.component.ts b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditFeaturesModal/georesource-edit-features-modal.component.ts new file mode 100644 index 000000000..84953ff47 --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditFeaturesModal/georesource-edit-features-modal.component.ts @@ -0,0 +1,818 @@ +import { Component, OnInit, Inject, ViewChild, ElementRef, OnDestroy } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; +import { HttpClient } from '@angular/common/http'; +import { Subscription } from 'rxjs'; +import { AgGridAngular } from 'ag-grid-angular'; +import { ColDef, GridOptions, GridApi, ColumnApi, GridReadyEvent, FirstDataRenderedEvent, ColumnResizedEvent } from 'ag-grid-community'; + +declare const $: any; +declare const __env: any; + +@Component({ + selector: 'georesource-edit-features-modal-new', + templateUrl: './georesource-edit-features-modal.component.html', + styleUrls: ['./georesource-edit-features-modal.component.css'] +}) +export class GeoresourceEditFeaturesModalComponent implements OnInit, OnDestroy { + @ViewChild('mappingConfigImportFile', { static: false }) mappingConfigImportFile!: ElementRef; + @ViewChild('dataSourceInput', { static: false }) dataSourceInput!: ElementRef; + @ViewChild('georesourceFeatureTable', { static: true }) georesourceFeatureTable!: AgGridAngular; + + // Component state + loadingData = false; + private _currentGeoresourceDataset: any; + currentStep = 1; + + get currentGeoresourceDataset(): any { + return this._currentGeoresourceDataset; + } + + set currentGeoresourceDataset(value: any) { + this._currentGeoresourceDataset = value; + } + + // Feature management + enableDeleteFeatures = false; + georesourceFeaturesGeoJSON: any; + remainingFeatureHeaders: any[] = []; + + // AG-Grid configuration + featureTableGridOptions: GridOptions = {}; + private gridApi!: GridApi; + private columnApi!: ColumnApi; + + // Single feature variables + featureIdValue = 0; + featureIdExampleString: string = ''; + featureIdIsValid = false; + featureNameValue: string = ''; + featureGeometryValue: any; + featureStartDateValue: string = ''; + featureEndDateValue: string = ''; + featureSchemaProperties: any[] = []; + schemaObject: any; + + // Multiple feature import variables + periodOfValidity: any = { + startDate: '', + endDate: '' + }; + periodOfValidityInvalid = false; + + // Data source variables + georesourceDataSourceInputInvalid = false; + georesourceDataSourceInputInvalidReason: string = ''; + georesourceDataSourceIdProperty: string = ''; + georesourceDataSourceNameProperty: string = ''; + idPropertyNotFound = false; + namePropertyNotFound = false; + + // Import configuration + converter: any; + schema: string = ''; + mimeType: string = ''; + datasourceType: any; + + // Available options + availableDatasourceTypes: any[] = []; + availableSpatialUnits: any[] = []; + + // Converter parameters + converterDefinition: any; + datasourceTypeDefinition: any; + propertyMappingDefinition: any; + putBody_georesources: any; + + // Validity date attributes + validityEndDate_perFeature: string = ''; + validityStartDate_perFeature: string = ''; + + // Attribute mapping + attributeMapping_sourceAttributeName: string = ''; + attributeMapping_destinationAttributeName: string = ''; + attributeMapping_data: any; + attributeMapping_attributeType: any; + attributeMappings_adminView: any[] = []; + keepAttributes = true; + keepMissingValues = true; + + // BBOX configuration + bboxType: string = ''; + bboxRefSpatialUnit: any; + + // Partial update + isPartialUpdate = false; + + // Success/Error messages + successMessagePart: string = ''; + errorMessagePart: string = ''; + importerErrors: any; + importedFeatures: any[] = []; + + // Mapping config import/export + georesourceMappingConfigImportError: string = ''; + georesourceMappingConfigStructure_pretty: string = ''; + + // Subscriptions + private subscriptions: Subscription[] = []; + + constructor( + public activeModal: NgbActiveModal, + @Inject('kommonitorDataExchangeService') public kommonitorDataExchangeService: any, + @Inject('kommonitorMultiStepFormHelperService') public kommonitorMultiStepFormHelperService: any, + @Inject('kommonitorDataGridHelperService') public kommonitorDataGridHelperService: any, + @Inject('kommonitorImporterHelperService') public kommonitorImporterHelperService: any, + @Inject('kommonitorSingleFeatureMapHelperService') public kommonitorSingleFeatureMapHelperService: any, + private broadcastService: BroadcastService, + private http: HttpClient + ) { + console.log('GeoresourceEditFeaturesModalComponent constructor initialized'); + this.initializeDefaultValues(); + } + + ngOnInit(): void { + this.initializeDatePickers(); + this.setupEventListeners(); + this.initializeMappingConfigStructure(); + this.buildFeatureTable(); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(sub => sub.unsubscribe()); + } + + private initializeDefaultValues(): void { + this.attributeMapping_attributeType = this.kommonitorImporterHelperService.attributeMapping_attributeTypes[0]; + this.availableDatasourceTypes = this.kommonitorImporterHelperService.availableDatasourceTypes; + this.availableSpatialUnits = this.kommonitorDataExchangeService.availableSpatialUnits; + } + + private initializeMappingConfigStructure(): void { + this.georesourceMappingConfigStructure_pretty = this.kommonitorDataExchangeService.syntaxHighlightJSON( + this.kommonitorImporterHelperService.mappingConfigStructure + ); + } + + private initializeDatePickers(): void { + setTimeout(() => { + try { + if ((window as any).$) { + (window as any).$('#georesourceEditFeaturesDatepickerStart').datepicker( + this.kommonitorDataExchangeService.datePickerOptions + ); + (window as any).$('#georesourceEditFeaturesDatepickerEnd').datepicker( + this.kommonitorDataExchangeService.datePickerOptions + ); + (window as any).$('#georesourceSingleFeatureDatepickerStart').datepicker( + this.kommonitorDataExchangeService.datePickerOptions + ); + (window as any).$('#georesourceSingleFeatureDatepickerEnd').datepicker( + this.kommonitorDataExchangeService.datePickerOptions + ); + } + } catch (error) { + console.warn('Date picker initialization failed:', error); + } + }, 250); + } + + private setupEventListeners(): void { + // Setup broadcast listeners + const broadcastSubscription = this.broadcastService.currentBroadcastMsg.subscribe(broadcastMsg => { + if (broadcastMsg) { + if (broadcastMsg.msg === 'onEditGeoresourceFeatures') { + this.onEditGeoresourceFeatures(broadcastMsg.values); + } else if (broadcastMsg.msg === 'showLoadingIcon_' + this.kommonitorDataGridHelperService?.resourceType_georesource) { + this.loadingData = true; + } else if (broadcastMsg.msg === 'hideLoadingIcon_' + this.kommonitorDataGridHelperService?.resourceType_georesource) { + this.loadingData = false; + } else if (broadcastMsg.msg === 'onDeleteFeatureEntry_' + this.kommonitorDataGridHelperService?.resourceType_georesource) { + this.broadcastService.broadcast('refreshGeoresourceOverviewTable', { + crudType: 'edit', + targetGeoresourceId: this.currentGeoresourceDataset?.georesourceId + }); + this.refreshGeoresourceEditFeaturesOverviewTable(); + } + } + }); + + this.subscriptions.push(broadcastSubscription); + } + + onEditGeoresourceFeatures(georesourceDataset: any): void { + this.kommonitorMultiStepFormHelperService?.registerClickHandler(); + + if (this.currentGeoresourceDataset && + this.currentGeoresourceDataset.datasetName === georesourceDataset.datasetName) { + return; + } + + this.currentGeoresourceDataset = georesourceDataset; + this.resetGeoresourceEditFeaturesForm(); + this.buildFeatureTable(); + + // Load the georesource features + setTimeout(() => { + this.refreshGeoresourceEditFeaturesOverviewTable(); + }, 100); + } + + // Step navigation + nextStep(): void { + if (this.currentStep < 3) { + this.currentStep++; + } + } + + previousStep(): void { + if (this.currentStep > 1) { + this.currentStep--; + } + } + + goToStep(step: number): void { + if (step >= 1 && step <= 3) { + this.currentStep = step; + } + } + + // Feature table management + private buildFeatureTable(): void { + this.featureTableGridOptions = this.kommonitorDataGridHelperService.buildDataGrid_featureTable_spatialResource( + "georesourceFeatureTable", + [], + [], + undefined, + this.kommonitorDataGridHelperService.resourceType_georesource, + this.enableDeleteFeatures + ); + } + + refreshGeoresourceEditFeaturesOverviewTable(): void { + if (!this.currentGeoresourceDataset) { + console.warn('No current georesource dataset selected'); + return; + } + + console.log('Starting refresh of georesource features table...'); + this.loadingData = true; + + const url = `${this.kommonitorDataExchangeService.getBaseUrlToKomMonitorDataAPI_spatialResource()}/georesources/${this.currentGeoresourceDataset.georesourceId}/allFeatures`; + console.log('Fetching from URL:', url); + + this.http.get(url).subscribe({ + next: (response: any) => { + console.log('Successfully received georesource features data:', response); + this.georesourceFeaturesGeoJSON = response; + const tmpRemainingHeaders: string[] = []; + + // Extract headers from the first feature's properties + if (this.georesourceFeaturesGeoJSON?.features?.[0]?.properties) { + console.log('First feature properties:', this.georesourceFeaturesGeoJSON.features[0].properties); + for (const property in this.georesourceFeaturesGeoJSON.features[0].properties) { + if (property !== __env.FEATURE_ID_PROPERTY_NAME && + property !== __env.FEATURE_NAME_PROPERTY_NAME && + property !== __env.VALID_START_DATE_PROPERTY_NAME && + property !== __env.VALID_END_DATE_PROPERTY_NAME) { + tmpRemainingHeaders.push(property); + } + } + } + + this.remainingFeatureHeaders = tmpRemainingHeaders; + console.log('Remaining headers:', tmpRemainingHeaders); + console.log('Features count:', this.georesourceFeaturesGeoJSON.features?.length || 0); + + // Rebuild the grid options with new data + this.featureTableGridOptions = this.kommonitorDataGridHelperService.buildDataGrid_featureTable_spatialResource( + "georesourceFeatureTable", + tmpRemainingHeaders, + this.georesourceFeaturesGeoJSON.features, + this.currentGeoresourceDataset.georesourceId, + this.kommonitorDataGridHelperService.resourceType_georesource, + this.enableDeleteFeatures + ); + + // If grid API is available, update the data directly + if (this.gridApi) { + console.log('Updating grid data via API...'); + // Transform the data to match the expected format + const transformedData = (this.georesourceFeaturesGeoJSON.features || []).map((feature: any) => { + if (feature.properties) { + // Add geometry and record ID to properties + feature.properties.kommonitorGeometry = feature.geometry; + feature.properties.kommonitorRecordId = feature.id; + return feature.properties; + } + return feature; + }); + console.log('Transformed data for grid:', transformedData); + this.gridApi.setRowData(transformedData); + // Force refresh of the grid + this.gridApi.refreshCells(); + } + + // Use setTimeout to ensure proper change detection and DOM updates + setTimeout(() => { + this.loadingData = false; + console.log('Loading completed'); + }, 500); // Increased timeout to show loading state longer + }, + error: (error) => { + console.error('Error fetching georesource features:', error); + this.handleError(error); + setTimeout(() => { + this.loadingData = false; + }, 500); // Increased timeout to show loading state longer + } + }); + } + + onChangeEnableDeleteFeatures(): void { + // Rebuild the table with updated delete functionality + if (this.currentGeoresourceDataset && this.remainingFeatureHeaders && this.georesourceFeaturesGeoJSON) { + this.featureTableGridOptions = this.kommonitorDataGridHelperService.buildDataGrid_featureTable_spatialResource( + "georesourceFeatureTable", + this.remainingFeatureHeaders, + this.georesourceFeaturesGeoJSON.features || [], + this.currentGeoresourceDataset.georesourceId, + this.kommonitorDataGridHelperService.resourceType_georesource, + this.enableDeleteFeatures + ); + + // Update grid if API is available + if (this.gridApi && this.featureTableGridOptions && this.featureTableGridOptions.columnDefs) { + // Update column definitions to include/exclude delete buttons + this.gridApi.setColumnDefs(this.featureTableGridOptions.columnDefs); + } + } + } + + clearAllGeoresourceFeatures(): void { + if (!this.enableDeleteFeatures || !this.currentGeoresourceDataset) return; + + if (confirm('Sind Sie sicher, dass Sie alle Features dieser Georessource löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.')) { + this.loadingData = true; + + this.http.delete( + `${this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI}/georesources/${this.currentGeoresourceDataset.georesourceId}/allFeatures` + ).subscribe({ + next: (response: any) => { + this.loadingData = false; + this.refreshGeoresourceEditFeaturesOverviewTable(); + alert('Alle Features wurden erfolgreich gelöscht.'); + }, + error: (error: any) => { + this.loadingData = false; + console.error('Error deleting features:', error); + alert('Fehler beim Löschen der Features.'); + } + }); + } + } + + // Converter and data source methods + onChangeConverter(): void { + this.schema = ''; + this.mimeType = ''; + this.datasourceType = undefined; + } + + onChangeMimeType(mimeType: string): void { + this.mimeType = mimeType; + } + + onChangeDatasourceType(datasourceType: any): void { + this.datasourceType = datasourceType; + } + + // Validation methods + checkPeriodOfValidity(): void { + this.periodOfValidityInvalid = false; + + if (this.periodOfValidity.startDate && this.periodOfValidity.endDate) { + const startDate = new Date(this.periodOfValidity.startDate); + const endDate = new Date(this.periodOfValidity.endDate); + + if (startDate >= endDate) { + this.periodOfValidityInvalid = true; + } + } + } + + // Attribute mapping methods + onAddOrUpdateAttributeMapping(): void { + if (!this.attributeMapping_sourceAttributeName || + !this.attributeMapping_destinationAttributeName || + !this.attributeMapping_attributeType) { + return; + } + + const existingIndex = this.attributeMappings_adminView.findIndex( + mapping => mapping.sourceName === this.attributeMapping_sourceAttributeName + ); + + const newMapping = { + sourceName: this.attributeMapping_sourceAttributeName, + destinationName: this.attributeMapping_destinationAttributeName, + dataType: this.attributeMapping_attributeType + }; + + if (existingIndex >= 0) { + // Update existing mapping + this.attributeMappings_adminView[existingIndex] = newMapping; + } else { + // Add new mapping + this.attributeMappings_adminView.push(newMapping); + } + + // Clear form + this.attributeMapping_sourceAttributeName = ''; + this.attributeMapping_destinationAttributeName = ''; + this.attributeMapping_attributeType = this.kommonitorImporterHelperService.attributeMapping_attributeTypes[0]; + } + + onClickEditAttributeMapping(attributeMappingEntry: any): void { + this.attributeMapping_sourceAttributeName = attributeMappingEntry.sourceName; + this.attributeMapping_destinationAttributeName = attributeMappingEntry.destinationName; + this.attributeMapping_attributeType = attributeMappingEntry.dataType; + } + + onClickDeleteAttributeMapping(attributeMappingEntry: any): void { + const index = this.attributeMappings_adminView.indexOf(attributeMappingEntry); + if (index >= 0) { + this.attributeMappings_adminView.splice(index, 1); + } + } + + // Import/Export methods + onImportGeoresourceEditFeaturesMappingConfig(): void { + this.georesourceMappingConfigImportError = ''; + this.mappingConfigImportFile.nativeElement.click(); + } + + onMappingConfigFileSelected(event: any): void { + const file = event.target.files[0]; + if (file) { + this.parseMappingConfigFromFile(file); + } + } + + private parseMappingConfigFromFile(file: File): void { + const fileReader = new FileReader(); + + fileReader.onload = (event: any) => { + try { + this.parseFromMappingConfigFile(event); + } catch (error) { + console.error('Uploaded Mapping Config File cannot be parsed.'); + this.georesourceMappingConfigImportError = 'Uploaded Mapping Config File cannot be parsed correctly'; + const preElement = document.getElementById('georesourcesEditFeaturesMappingConfigPre'); + if (preElement) { + preElement.innerHTML = this.georesourceMappingConfigStructure_pretty; + } + this.showMappingConfigImportErrorAlert(); + } + }; + + fileReader.readAsText(file); + } + + private parseFromMappingConfigFile(event: any): void { + const mappingConfig = JSON.parse(event.target.result); + + // Apply mapping configuration + if (mappingConfig.converter) { + this.converter = mappingConfig.converter; + } + if (mappingConfig.datasourceType) { + this.datasourceType = mappingConfig.datasourceType; + } + if (mappingConfig.propertyMapping) { + this.attributeMappings_adminView = mappingConfig.propertyMapping || []; + } + // Add more mapping config properties as needed + } + + onExportGeoresourceEditFeaturesMappingConfig(): void { + const mappingConfig = { + converter: this.converter, + datasourceType: this.datasourceType, + propertyMapping: this.attributeMappings_adminView, + idProperty: this.georesourceDataSourceIdProperty, + nameProperty: this.georesourceDataSourceNameProperty, + validityStartDate: this.validityStartDate_perFeature, + validityEndDate: this.validityEndDate_perFeature, + keepAttributes: this.keepAttributes, + keepMissingValues: this.keepMissingValues, + isPartialUpdate: this.isPartialUpdate + }; + + const mappingJSON = JSON.stringify(mappingConfig, null, 2); + let fileName = 'Georessource_Features_Mapping_Export'; + + if (this.currentGeoresourceDataset?.datasetName) { + fileName += '-' + this.currentGeoresourceDataset.datasetName; + } + + fileName += '.json'; + + const blob = new Blob([mappingJSON], { type: 'application/json' }); + const data = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.download = fileName; + a.href = data; + a.textContent = 'JSON'; + a.target = '_blank'; + a.rel = 'noopener noreferrer'; + a.click(); + + a.remove(); + } + + // Main edit method + editGeoresourceFeatures(): void { + if (!this.currentGeoresourceDataset || !this.converter || !this.datasourceType) { + return; + } + + this.loadingData = true; + + // Build the request body + const putBody = this.buildPutBody(); + + this.http.put( + `${this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI}/georesources/${this.currentGeoresourceDataset.georesourceId}/features`, + putBody + ).subscribe({ + next: (response: any) => { + this.successMessagePart = this.currentGeoresourceDataset.datasetName; + this.importedFeatures = response.importedFeatures || []; + this.broadcastService.broadcast('refreshGeoresourceOverviewTable', { + crudType: 'edit', + targetGeoresourceId: this.currentGeoresourceDataset.georesourceId + }); + this.showSuccessAlert(); + this.loadingData = false; + }, + error: (error: any) => { + if (error.error) { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error.error); + } else { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error); + } + this.importerErrors = error.error?.importerErrors || []; + this.showErrorAlert(); + this.loadingData = false; + } + }); + } + + private buildPutBody(): any { + const putBody: any = { + geoJsonString: '', + periodOfValidity: { + startDate: this.periodOfValidity.startDate, + endDate: this.periodOfValidity.endDate + } + }; + + // Add converter definition + putBody.converterDefinition = { + name: this.converter.name, + parameters: this.getConverterParameters() + }; + + // Add datasource definition + putBody.datasourceTypeDefinition = { + type: this.datasourceType.type, + parameters: this.getDatasourceParameters() + }; + + // Add property mapping + putBody.propertyMappingDefinition = { + idProperty: this.georesourceDataSourceIdProperty, + nameProperty: this.georesourceDataSourceNameProperty, + validityStartDateProperty: this.validityStartDate_perFeature, + validityEndDateProperty: this.validityEndDate_perFeature, + keepAttributes: this.keepAttributes, + keepMissingValues: this.keepMissingValues, + attributeMappings: this.attributeMappings_adminView + }; + + // Add partial update flag + putBody.isPartialUpdate = this.isPartialUpdate; + + return putBody; + } + + private getConverterParameters(): any { + const parameters: any = {}; + + if (this.converter.parameters) { + this.converter.parameters.forEach((param: any) => { + const element = document.getElementById(`converterParameter_georesourceEditFeatures_${param.name}`); + if (element) { + parameters[param.name] = (element as HTMLInputElement).value; + } + }); + } + + if (this.schema) { + parameters.schema = this.schema; + } + if (this.mimeType) { + parameters.mimeType = this.mimeType; + } + + return parameters; + } + + private getDatasourceParameters(): any { + const parameters: any = {}; + + if (this.datasourceType.type === 'FILE') { + // Handle file upload + const fileInput = document.getElementById('georesourceDataSourceInput_editFeatures') as HTMLInputElement; + if (fileInput && fileInput.files && fileInput.files[0]) { + // File will be handled separately in actual implementation + parameters.file = fileInput.files[0]; + } + } else if (this.datasourceType.type === 'OGCAPI_FEATURES') { + // Handle BBOX parameters + if (this.bboxType === 'ref' && this.bboxRefSpatialUnit) { + parameters.spatialUnitId = this.bboxRefSpatialUnit.spatialUnitId; + } else if (this.bboxType === 'literal') { + const minx = (document.getElementById('datasourceTypeParameter_georesourceEditFeatures_bbox_minx') as HTMLInputElement)?.value; + const miny = (document.getElementById('datasourceTypeParameter_georesourceEditFeatures_bbox_miny') as HTMLInputElement)?.value; + const maxx = (document.getElementById('datasourceTypeParameter_georesourceEditFeatures_bbox_maxx') as HTMLInputElement)?.value; + const maxy = (document.getElementById('datasourceTypeParameter_georesourceEditFeatures_bbox_maxy') as HTMLInputElement)?.value; + + parameters.bbox = `${minx},${miny},${maxx},${maxy}`; + } + } + + // Add other datasource parameters + if (this.datasourceType.parameters) { + this.datasourceType.parameters.forEach((param: any) => { + if (param.name !== 'bbox') { + const element = document.getElementById(`datasourceTypeParameter_georesourceEditFeatures_${param.name}`); + if (element) { + parameters[param.name] = (element as HTMLTextAreaElement).value; + } + } + }); + } + + return parameters; + } + + // Filter methods + getFilteredConverters(): any[] { + return this.kommonitorImporterHelperService.availableConverters.filter( + this.kommonitorImporterHelperService.filterConverters('georesource') + ); + } + + getFilteredDatasourceParameters(): any[] { + if (!this.datasourceType?.parameters) return []; + return this.datasourceType.parameters.filter((param: any) => param.name !== 'bbox'); + } + + // Form reset + resetGeoresourceEditFeaturesForm(): void { + this.currentStep = 1; + this.enableDeleteFeatures = false; + + // Reset all form fields + this.converter = undefined; + this.schema = ''; + this.mimeType = ''; + this.datasourceType = undefined; + this.georesourceDataSourceIdProperty = ''; + this.georesourceDataSourceNameProperty = ''; + this.validityStartDate_perFeature = ''; + this.validityEndDate_perFeature = ''; + + this.periodOfValidity = { + startDate: '', + endDate: '' + }; + this.periodOfValidityInvalid = false; + + this.isPartialUpdate = false; + this.keepAttributes = true; + this.keepMissingValues = true; + + this.attributeMappings_adminView = []; + this.attributeMapping_sourceAttributeName = ''; + this.attributeMapping_destinationAttributeName = ''; + this.attributeMapping_attributeType = this.kommonitorImporterHelperService.attributeMapping_attributeTypes[0]; + + this.bboxType = ''; + this.bboxRefSpatialUnit = undefined; + + // Reset messages + this.successMessagePart = ''; + this.errorMessagePart = ''; + this.importerErrors = undefined; + this.importedFeatures = []; + } + + // Alert methods + showSuccessAlert(): void { + const alertElement = document.getElementById('georesourceEditFeaturesSuccessAlert'); + if (alertElement) { + alertElement.hidden = false; + } + } + + showErrorAlert(): void { + const alertElement = document.getElementById('georesourceEditFeaturesErrorAlert'); + if (alertElement) { + alertElement.hidden = false; + } + } + + showMappingConfigImportErrorAlert(): void { + const alertElement = document.getElementById('georesourceEditFeaturesMappingConfigImportErrorAlert'); + if (alertElement) { + alertElement.hidden = false; + } + } + + hideSuccessAlert(): void { + const alertElement = document.getElementById('georesourceEditFeaturesSuccessAlert'); + if (alertElement) { + alertElement.hidden = true; + } + } + + hideErrorAlert(): void { + const alertElement = document.getElementById('georesourceEditFeaturesErrorAlert'); + if (alertElement) { + alertElement.hidden = true; + } + } + + hideMappingConfigErrorAlert(): void { + const alertElement = document.getElementById('georesourceEditFeaturesMappingConfigImportErrorAlert'); + if (alertElement) { + alertElement.hidden = true; + } + } + + // Validation for form submission + canSubmitForm(): boolean { + return !!this.currentGeoresourceDataset?.datasetName && + !!this.georesourceDataSourceIdProperty && + !!this.georesourceDataSourceNameProperty && + !!this.periodOfValidity.startDate && + !this.periodOfValidityInvalid && + !!this.converter && + !!this.datasourceType; + } + + // AG-Grid event handlers + onGridReady(event: GridReadyEvent): void { + this.gridApi = event.api; + this.columnApi = event.columnApi; + console.log('Grid is ready, API initialized'); + + // Auto-size columns to fit content + this.gridApi.sizeColumnsToFit(); + } + + onFirstDataRendered(event: FirstDataRenderedEvent): void { + // Handle first data rendered event + } + + onColumnResized(event: ColumnResizedEvent): void { + // Handle column resize event + } + + onCellValueChanged(params: any): void { + // Handle cell value changes here + console.log('Cell value changed:', params); + + // TODO: Implement API call to update the feature in the backend + // Similar to the AngularJS version's cell update functionality + } + + private handleError(error: any): void { + console.error('Error occurred:', error); + if (error.data) { + this.errorMessagePart = this.kommonitorDataExchangeService?.syntaxHighlightJSON(error.data) || 'An error occurred'; + } else { + this.errorMessagePart = this.kommonitorDataExchangeService?.syntaxHighlightJSON(error) || 'An error occurred'; + } + this.showErrorAlert(); + } + + // Modal control + cancel(): void { + this.activeModal.dismiss(); + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditMetadataModal/georesource-edit-metadata-modal.component.css b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditMetadataModal/georesource-edit-metadata-modal.component.css new file mode 100644 index 000000000..fc9916935 --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditMetadataModal/georesource-edit-metadata-modal.component.css @@ -0,0 +1,255 @@ +/* Georesource Edit Metadata Modal Styles */ + +/* Multi-step form styles */ +.multiStepForm { + position: relative; + margin: 0 auto; +} + +.multiStepForm fieldset { + background: white; + border: 0 none; + border-radius: 0.5rem; + box-sizing: border-box; + width: 100%; + margin: 0; + padding-bottom: 20px; + position: relative; +} + +.multiStepForm fieldset:not(:first-of-type) { + display: none; +} + +.multiStepForm .fs-title { + font-size: 15px; + text-transform: uppercase; + color: #2C3E50; + margin-bottom: 10px; +} + +.multiStepForm .fs-subtitle { + font-weight: normal; + font-size: 13px; + color: #666; + margin-bottom: 20px; +} + +/* Progress bar */ +#progressbar { + margin-bottom: 30px; + overflow: hidden; + color: lightgrey; + padding-left: 0; +} + +#progressbar li { + list-style-type: none; + font-size: 12px; + width: 33.33%; + float: left; + position: relative; + font-weight: 400; + cursor: pointer; + transition: all 0.3s ease; +} + +#progressbar li:hover { + color: #27AE60; +} + +#progressbar li:hover:before { + background: #27AE60; + transform: scale(1.1); +} + +#progressbar li:before { + width: 30px; + height: 30px; + line-height: 25px; + display: block; + font-size: 12px; + color: #ffffff; + background: lightgray; + border-radius: 50%; + margin: 0 auto 5px auto; + padding: 2px; + transition: all 0.3s ease; +} + +#progressbar li.active:before, +#progressbar li.active:after { + background: #27AE60; +} + +/* Action buttons */ +.action-button { + width: 100px; + background: #27AE60; + font-weight: bold; + color: white; + border: 0 none; + border-radius: 5px; + cursor: pointer; + padding: 10px 5px; + margin: 10px 5px; +} + +.action-button-previous { + width: 100px; + background: #616161; + font-weight: bold; + color: white; + border: 0 none; + border-radius: 5px; + cursor: pointer; + padding: 10px 5px; + margin: 10px 5px; +} + +.action-button:hover, +.action-button:focus { + box-shadow: 0 0 0 2px white, 0 0 0 3px #27AE60; +} + +.action-button-previous:hover, +.action-button-previous:focus { + box-shadow: 0 0 0 2px white, 0 0 0 3px #616161; +} + +/* Color picker styles */ +.customColorPicker .dropdown-menu { + min-width: 200px; +} + +.customColorPicker .dropdown-menu li { + padding: 5px 10px; + cursor: pointer; +} + +.customColorPicker .dropdown-menu li:hover { + background-color: #f5f5f5; +} + +.customColorPicker .dropdown-menu li i { + display: inline-block; + width: 20px; + height: 20px; + border-radius: 3px; + margin-right: 10px; + border: 1px solid #ccc; +} + +.customColorPicker .btn i { + display: inline-block; + width: 20px; + height: 20px; + border-radius: 3px; + margin-right: 5px; + border: 1px solid #ccc; +} + +/* Loading overlay */ +.loading-overlay-admin-panel { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1000; + font-size: 2em; + color: #337ab7; +} + +.loading-overlay-admin-panel.ng-hide { + display: none; +} + +.icon-spin { + animation: spin 2s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Form adjustments */ +.form-control { + font-size: 12px; +} + +.help-block { + font-size: 11px; + color: #737373; +} + +/* Alert styles */ +.alert { + margin-bottom: 0; + border-radius: 0; +} + +.alert pre { + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 3px; + padding: 10px; + margin-top: 10px; +} + +/* Box styles for topics management */ +.box.box-primary { + border-top-color: #3c8dbc; +} + +.box.box-primary.collapsed-box .box-body, +.box.box-primary.collapsed-box .box-footer { + display: none; +} + +.box-header { + padding: 10px; + border-bottom: 1px solid #f4f4f4; + color: #444; + display: block; + font-size: 18px; + line-height: 1.42857143; + background-color: #ffffff; +} + +.box-tools { + position: absolute; + right: 10px; + top: 5px; +} + +.btn-box-tool { + padding: 5px; + font-size: 12px; + background: transparent; + color: #97a0b3; + border: none; +} + +.btn-box-tool:hover { + color: #606c84; +} + +/* Vertical alignment helper */ +.vertical-align { + display: flex; + align-items: center; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .vertical-align { + display: block; + } + + .col-md-3, + .col-md-4, + .col-md-6 { + margin-bottom: 15px; + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditMetadataModal/georesource-edit-metadata-modal.component.html b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditMetadataModal/georesource-edit-metadata-modal.component.html new file mode 100644 index 000000000..c178b9e0f --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditMetadataModal/georesource-edit-metadata-modal.component.html @@ -0,0 +1,450 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditMetadataModal/georesource-edit-metadata-modal.component.ts b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditMetadataModal/georesource-edit-metadata-modal.component.ts new file mode 100644 index 000000000..fdcc0b56d --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditMetadataModal/georesource-edit-metadata-modal.component.ts @@ -0,0 +1,790 @@ +import { Component, OnInit, Inject, ViewChild, ElementRef, OnDestroy } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; +import { HttpClient } from '@angular/common/http'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'georesource-edit-metadata-modal-new', + templateUrl: './georesource-edit-metadata-modal.component.html', + styleUrls: ['./georesource-edit-metadata-modal.component.css'] +}) +export class GeoresourceEditMetadataModalComponent implements OnInit, OnDestroy { + @ViewChild('metadataImportFile', { static: false }) metadataImportFile!: ElementRef; + + // Component state + loadingData = false; + currentGeoresourceDataset: any; + currentStep = 1; + + // Form data + datasetName: string = ''; + datasetNameInvalid = false; + poiMarkerText: string = ''; + poiMarkerTextInvalid = false; + + // Metadata + metadata: any = { + note: '', + literature: '', + updateInterval: undefined, + sridEPSG: 4326, + datasource: '', + contact: '', + lastUpdate: '', + description: '', + databasis: '' + }; + + // Georesource type + georesourceType: string = 'poi'; + isPOI = true; + isLOI = false; + isAOI = false; + + // POI specific + selectedPoiMarkerColor: any; + selectedPoiSymbolColor: any; + selectedPoiMarkerStyle: string = 'symbol'; + selectedPoiIconName: string = 'home'; + + // LOI specific + selectedLoiDashArrayObject: any; + loiColor: string = '#bf3d2c'; + loiWidth: number = 3; + + // AOI specific + aoiColor: string = '#bf3d2c'; + + // Topic hierarchy + georesourceTopic_mainTopic: any; + georesourceTopic_subTopic: any; + georesourceTopic_subsubTopic: any; + georesourceTopic_subsubsubTopic: any; + + // Role management + roleManagementTableOptions: any; + + // Import/Export + metadataImportSettings: any; + georesourceMetadataImportError: string = ''; + georesourceMetadataStructure: any; + georesourceMetadataStructure_pretty: string = ''; + + // Success/Error messages + successMessagePart: string = ''; + errorMessagePart: string = ''; + + // Subscriptions + private subscriptions: Subscription[] = []; + + constructor( + public activeModal: NgbActiveModal, + @Inject('kommonitorDataExchangeService') public kommonitorDataExchangeService: any, + @Inject('kommonitorMultiStepFormHelperService') public kommonitorMultiStepFormHelperService: any, + @Inject('kommonitorDataGridHelperService') public kommonitorDataGridHelperService: any, + private broadcastService: BroadcastService, + private http: HttpClient + ) { + this.initializeDefaultValues(); + } + + ngOnInit(): void { + this.setupEventListeners(); + this.initializeMetadataStructure(); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(sub => sub.unsubscribe()); + } + + private initializeDefaultValues(): void { + this.selectedPoiMarkerColor = this.kommonitorDataExchangeService.availablePoiMarkerColors[0]; + this.selectedPoiSymbolColor = this.kommonitorDataExchangeService.availablePoiMarkerColors[1]; + this.selectedLoiDashArrayObject = this.kommonitorDataExchangeService.availableLoiDashArrayObjects[0]; + } + + private initializeMetadataStructure(): void { + this.georesourceMetadataStructure = { + "metadata": { + "note": "an optional note", + "literature": "optional text about literature", + "updateInterval": "YEARLY|HALF_YEARLY|QUARTERLY|MONTHLY|ARBITRARY", + "sridEPSG": 4326, + "datasource": "text about data source", + "contact": "text about contact details", + "lastUpdate": "YYYY-MM-DD", + "description": "description about spatial unit dataset", + "databasis": "text about data basis", + }, + "allowedRoles": ['roleId'], + "datasetName": "Name of georesource dataset", + "isPOI": "boolean parameter for point of interest dataset - only one of isPOI, isLOI, isAOI can be true", + "isLOI": "boolean parameter for lines of interest dataset - only one of isPOI, isLOI, isAOI can be true", + "isAOI": "boolean parameter for area of interest dataset - only one of isPOI, isLOI, isAOI can be true", + "poiSymbolBootstrap3Name": "glyphicon name of bootstrap 3 symbol to use for a POI resource", + "poiSymbolColor": "'white'|'red'|'orange'|'beige'|'green'|'blue'|'purple'|'pink'|'gray'|'black'", + "loiDashArrayString": "dash array string value - e.g. 20 20", + "poiMarkerColor": "'white'|'red'|'orange'|'beige'|'green'|'blue'|'purple'|'pink'|'gray'|'black'", + "loiColor": "color for lines of interest dataset", + "loiWidth": "width for lines of interest dataset", + "aoiColor": "color for area of interest dataset" + }; + + this.georesourceMetadataStructure_pretty = this.kommonitorDataExchangeService.syntaxHighlightJSON(this.georesourceMetadataStructure); + } + + private setupEventListeners(): void { + // Listen for edit georesource metadata event + const editSub = this.broadcastService.currentBroadcastMsg.subscribe((data: any) => { + if (data.msg === 'onEditGeoresourceMetadata') { + this.currentGeoresourceDataset = data.georesourceDataset; + this.resetGeoresourceEditMetadataForm(); + this.kommonitorMultiStepFormHelperService.registerClickHandler(); + } + }); + this.subscriptions.push(editSub); + + // Listen for available roles update + const rolesSub = this.broadcastService.currentBroadcastMsg.subscribe((data: any) => { + if (data.msg === 'availableRolesUpdate') { + this.refreshRoles(); + } + }); + this.subscriptions.push(rolesSub); + } + + private refreshRoles(): void { + const allowedRoles = this.currentGeoresourceDataset ? this.currentGeoresourceDataset.allowedRoles : []; + this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid( + 'georesourceEditRoleManagementTable', + this.roleManagementTableOptions, + this.kommonitorDataExchangeService.accessControl, + allowedRoles + ); + } + + // Form methods + resetGeoresourceEditMetadataForm(): void { + if (!this.currentGeoresourceDataset) return; + + this.currentStep = 1; + this.datasetName = this.currentGeoresourceDataset.datasetName; + this.datasetNameInvalid = false; + + // Reset metadata + this.metadata = { + note: this.currentGeoresourceDataset.metadata.note, + literature: this.currentGeoresourceDataset.metadata.literature, + sridEPSG: 4326, + datasource: this.currentGeoresourceDataset.metadata.datasource, + databasis: this.currentGeoresourceDataset.metadata.databasis, + contact: this.currentGeoresourceDataset.metadata.contact, + description: this.currentGeoresourceDataset.metadata.description, + lastUpdate: this.currentGeoresourceDataset.metadata.lastUpdate + }; + + // Set update interval + this.kommonitorDataExchangeService.updateIntervalOptions.forEach((option: any) => { + if (option.apiName === this.currentGeoresourceDataset.metadata.updateInterval) { + this.metadata.updateInterval = option; + } + }); + + // Set role management + this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid( + 'georesourceEditRoleManagementTable', + this.roleManagementTableOptions, + this.kommonitorDataExchangeService.accessControl, + this.currentGeoresourceDataset.allowedRoles + ); + + // Set georesource type + this.isPOI = this.currentGeoresourceDataset.isPOI; + this.isLOI = this.currentGeoresourceDataset.isLOI; + this.isAOI = this.currentGeoresourceDataset.isAOI; + + if (this.isPOI) { + this.georesourceType = 'poi'; + } else if (this.isLOI) { + this.georesourceType = 'loi'; + } else { + this.georesourceType = 'aoi'; + } + + // Set POI colors + this.kommonitorDataExchangeService.availablePoiMarkerColors.forEach((option: any) => { + if (option.colorName === this.currentGeoresourceDataset.poiMarkerColor) { + this.selectedPoiMarkerColor = option; + } + if (option.colorName === this.currentGeoresourceDataset.poiSymbolColor) { + this.selectedPoiSymbolColor = option; + } + }); + + // Set LOI properties + this.kommonitorDataExchangeService.availableLoiDashArrayObjects.forEach((option: any) => { + if (option.dashArrayValue === this.currentGeoresourceDataset.loiDashArrayString) { + this.selectedLoiDashArrayObject = option; + this.onChangeLoiDashArray(this.selectedLoiDashArrayObject); + } + }); + + this.loiColor = this.currentGeoresourceDataset.loiColor; + this.loiWidth = this.currentGeoresourceDataset.loiWidth || 3; + this.aoiColor = this.currentGeoresourceDataset.aoiColor; + this.selectedPoiIconName = this.currentGeoresourceDataset.poiSymbolBootstrap3Name; + + // Set topic hierarchy + const topicHierarchy = this.kommonitorDataExchangeService.getTopicHierarchyForTopicId( + this.currentGeoresourceDataset.topicReference + ); + + if (topicHierarchy && topicHierarchy[0]) { + this.georesourceTopic_mainTopic = topicHierarchy[0]; + } + if (topicHierarchy && topicHierarchy[1]) { + this.georesourceTopic_subTopic = topicHierarchy[1]; + } + if (topicHierarchy && topicHierarchy[2]) { + this.georesourceTopic_subsubTopic = topicHierarchy[2]; + } + if (topicHierarchy && topicHierarchy[3]) { + this.georesourceTopic_subsubsubTopic = topicHierarchy[3]; + } + + // Reset messages + this.successMessagePart = ''; + this.errorMessagePart = ''; + + // Initialize date picker + setTimeout(() => { + this.initializeDatePickers(); + }, 250); + } + + private initializeDatePickers(): void { + try { + const datePicker = document.getElementById('georesourceEditLastUpdateDatepicker'); + if (datePicker && (window as any).$) { + (window as any).$('#georesourceEditLastUpdateDatepicker').datepicker( + this.kommonitorDataExchangeService.datePickerOptions + ); + (window as any).$('#georesourceEditLastUpdateDatepicker').datepicker('setDate', this.metadata.lastUpdate); + } + + // Initialize color pickers + const loiColorPicker = document.getElementById('loiColorEditPicker'); + const aoiColorPicker = document.getElementById('aoiColorEditPicker'); + + if (loiColorPicker && (window as any).$) { + (window as any).$('#loiColorEditPicker').colorpicker(); + (window as any).$('#loiColorEditPicker').colorpicker('setValue', this.loiColor); + } + + if (aoiColorPicker && (window as any).$) { + (window as any).$('#aoiColorEditPicker').colorpicker(); + (window as any).$('#aoiColorEditPicker').colorpicker('setValue', this.aoiColor); + } + + // Initialize icon picker + const iconPicker = document.getElementById('poiSymbolEditPicker'); + if (iconPicker && (window as any).$) { + const iconPickerOptions = { + align: 'center', + arrowClass: 'btn-default', + arrowPrevIconClass: 'fas fa-angle-left', + arrowNextIconClass: 'fas fa-angle-right', + cols: 10, + footer: true, + header: true, + icon: 'glyphicon-' + this.selectedPoiIconName, + iconset: 'glyphicon', + labelHeader: '{0} von {1} Seiten', + labelFooter: '{0} - {1} von {2} Icons', + placement: 'bottom', + rows: 6, + search: true, + searchText: 'Stichwortsuche (Bootstrap Glyphicons)', + selectedClass: 'btn-success', + unselectedClass: '' + }; + + (window as any).$('#poiSymbolEditPicker').iconpicker(iconPickerOptions); + (window as any).$('#poiSymbolEditPicker').on('change', (e: any) => { + this.selectedPoiIconName = e.icon.substring(e.icon.indexOf('-') + 1); + }); + (window as any).$('#poiSymbolEditPicker').iconpicker('setIcon', 'glyphicon-' + this.selectedPoiIconName); + } + + // Initialize LOI dash array dropdown + setTimeout(() => { + for (let i = 0; i < this.kommonitorDataExchangeService.availableLoiDashArrayObjects.length; i++) { + const element = document.getElementById('loiDashArrayEditDropdownItem-' + i); + if (element) { + element.innerHTML = this.kommonitorDataExchangeService.availableLoiDashArrayObjects[i].svgString; + } + } + + const buttonElement = document.getElementById('loiDashArrayEditDropdownButton'); + if (buttonElement) { + buttonElement.innerHTML = this.selectedLoiDashArrayObject.svgString; + } + }, 1000); + + } catch (error) { + console.warn('Date picker/color picker initialization failed:', error); + } + } + + // Validation methods + checkDatasetName(): void { + this.datasetNameInvalid = false; + this.kommonitorDataExchangeService.availableGeoresources.forEach((georesource: any) => { + if (georesource.datasetName === this.datasetName && + georesource.georesourceId !== this.currentGeoresourceDataset?.georesourceId) { + this.datasetNameInvalid = true; + return; + } + }); + } + + checkPoiMarkerText(): void { + this.poiMarkerTextInvalid = this.poiMarkerText.length > 3; + } + + // Georesource type change + onChangeGeoresourceType(): void { + this.isPOI = this.georesourceType === 'poi'; + this.isLOI = this.georesourceType === 'loi'; + this.isAOI = this.georesourceType === 'aoi'; + } + + // POI methods + onChangeMarkerColor(markerColor: any): void { + this.selectedPoiMarkerColor = markerColor; + } + + onChangeSymbolColor(symbolColor: any): void { + this.selectedPoiSymbolColor = symbolColor; + } + + onChangeMarkerStyle(style: string): void { + this.selectedPoiMarkerStyle = style; + } + + // LOI methods + onChangeLoiDashArray(loiDashArrayObject: any): void { + this.selectedLoiDashArrayObject = loiDashArrayObject; + const buttonElement = document.getElementById('loiDashArrayEditDropdownButton'); + if (buttonElement) { + buttonElement.innerHTML = loiDashArrayObject.svgString; + } + } + + // Import/Export methods + onImportGeoresourceEditMetadata(): void { + this.georesourceMetadataImportError = ''; + this.metadataImportFile.nativeElement.click(); + } + + onMetadataFileSelected(event: any): void { + const file = event.target.files[0]; + if (file) { + this.parseMetadataFromFile(file); + } + } + + private parseMetadataFromFile(file: File): void { + const fileReader = new FileReader(); + + fileReader.onload = (event: any) => { + try { + this.parseFromMetadataFile(event); + } catch (error) { + console.error('Uploaded Metadata File cannot be parsed.'); + this.georesourceMetadataImportError = 'Uploaded Metadata File cannot be parsed correctly'; + const preElement = document.getElementById('georesourcesEditMetadataPre'); + if (preElement) { + preElement.innerHTML = this.georesourceMetadataStructure_pretty; + } + this.showMetadataImportErrorAlert(); + } + }; + + fileReader.readAsText(file); + } + + private parseFromMetadataFile(event: any): void { + this.metadataImportSettings = JSON.parse(event.target.result); + + if (!this.metadataImportSettings.metadata) { + console.error('uploaded Metadata File cannot be parsed - wrong structure.'); + this.georesourceMetadataImportError = 'Struktur der Datei stimmt nicht mit erwartetem Muster überein.'; + const preElement = document.getElementById('georesourcesEditMetadataPre'); + if (preElement) { + preElement.innerHTML = this.georesourceMetadataStructure_pretty; + } + this.showMetadataImportErrorAlert(); + return; + } + + // Parse metadata + this.metadata = { + note: this.metadataImportSettings.metadata.note, + literature: this.metadataImportSettings.metadata.literature, + sridEPSG: this.metadataImportSettings.metadata.sridEPSG, + datasource: this.metadataImportSettings.metadata.datasource, + contact: this.metadataImportSettings.metadata.contact, + lastUpdate: this.metadataImportSettings.metadata.lastUpdate, + description: this.metadataImportSettings.metadata.description, + databasis: this.metadataImportSettings.metadata.databasis + }; + + // Set update interval + this.kommonitorDataExchangeService.updateIntervalOptions.forEach((option: any) => { + if (option.apiName === this.metadataImportSettings.metadata.updateInterval) { + this.metadata.updateInterval = option; + } + }); + + this.datasetName = this.metadataImportSettings.datasetName; + + // Set role management + this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid( + 'georesourceEditRoleManagementTable', + this.roleManagementTableOptions, + this.kommonitorDataExchangeService.accessControl, + this.metadataImportSettings.allowedRoles + ); + + // Set georesource specific properties + this.isPOI = this.metadataImportSettings.isPOI; + this.isLOI = this.metadataImportSettings.isLOI; + this.isAOI = this.metadataImportSettings.isAOI; + + if (this.metadataImportSettings.isPOI) { + this.georesourceType = 'poi'; + } else if (this.metadataImportSettings.isLOI) { + this.georesourceType = 'loi'; + } else { + this.georesourceType = 'aoi'; + } + + // Set POI colors + this.kommonitorDataExchangeService.availablePoiMarkerColors.forEach((option: any) => { + if (option.colorName === this.metadataImportSettings.poiMarkerColor) { + this.selectedPoiMarkerColor = option; + } + if (option.colorName === this.metadataImportSettings.poiSymbolColor) { + this.selectedPoiSymbolColor = option; + } + }); + + // Set LOI properties + this.kommonitorDataExchangeService.availableLoiDashArrayObjects.forEach((option: any) => { + if (option.dashArrayValue === this.metadataImportSettings.loiDashArrayString) { + this.selectedLoiDashArrayObject = option; + this.onChangeLoiDashArray(this.selectedLoiDashArrayObject); + } + }); + + this.loiColor = this.metadataImportSettings.loiColor; + this.loiWidth = this.metadataImportSettings.loiWidth; + this.aoiColor = this.metadataImportSettings.aoiColor; + this.selectedPoiIconName = this.metadataImportSettings.poiSymbolBootstrap3Name; + + // Set color pickers + setTimeout(() => { + if ((window as any).$) { + (window as any).$('#loiColorEditPicker').colorpicker('setValue', this.loiColor); + (window as any).$('#aoiColorEditPicker').colorpicker('setValue', this.aoiColor); + (window as any).$('#poiSymbolEditPicker').iconpicker('setIcon', 'glyphicon-' + this.metadataImportSettings.poiSymbolBootstrap3Name); + } + }, 200); + + // Set topic hierarchy + const topicHierarchy = this.kommonitorDataExchangeService.getTopicHierarchyForTopicId( + this.metadataImportSettings.topicReference + ); + + if (topicHierarchy && topicHierarchy[0]) { + this.georesourceTopic_mainTopic = topicHierarchy[0]; + } + if (topicHierarchy && topicHierarchy[1]) { + this.georesourceTopic_subTopic = topicHierarchy[1]; + } + if (topicHierarchy && topicHierarchy[2]) { + this.georesourceTopic_subsubTopic = topicHierarchy[2]; + } + if (topicHierarchy && topicHierarchy[3]) { + this.georesourceTopic_subsubsubTopic = topicHierarchy[3]; + } + } + + onExportGeoresourceEditMetadata(): void { + const metadataExport = JSON.parse(JSON.stringify(this.georesourceMetadataStructure)); + + metadataExport.metadata.note = this.metadata.note || ''; + metadataExport.metadata.literature = this.metadata.literature || ''; + metadataExport.metadata.sridEPSG = this.metadata.sridEPSG || ''; + metadataExport.metadata.datasource = this.metadata.datasource || ''; + metadataExport.metadata.contact = this.metadata.contact || ''; + metadataExport.metadata.lastUpdate = this.metadata.lastUpdate || ''; + metadataExport.metadata.description = this.metadata.description || ''; + metadataExport.metadata.databasis = this.metadata.databasis || ''; + metadataExport.datasetName = this.datasetName || ''; + + metadataExport.allowedRoles = []; + + const roleIds = this.kommonitorDataGridHelperService.getSelectedRoleIds_roleManagementGrid(this.roleManagementTableOptions); + for (const roleId of roleIds) { + metadataExport.allowedRoles.push(roleId); + } + + if (this.metadata.updateInterval) { + metadataExport.metadata.updateInterval = this.metadata.updateInterval.apiName; + } + + // Georesource specific properties + metadataExport.isPOI = this.isPOI; + metadataExport.isLOI = this.isLOI; + metadataExport.isAOI = this.isAOI; + + if (this.isPOI) { + metadataExport.poiSymbolBootstrap3Name = this.selectedPoiIconName; + metadataExport.poiSymbolColor = this.selectedPoiSymbolColor.colorName; + metadataExport.poiMarkerColor = this.selectedPoiMarkerColor.colorName; + metadataExport.loiDashArrayString = ''; + metadataExport.loiColor = ''; + metadataExport.loiWidth = ''; + metadataExport.aoiColor = ''; + } else if (this.isLOI) { + metadataExport.poiSymbolBootstrap3Name = ''; + metadataExport.poiSymbolColor = ''; + metadataExport.poiMarkerColor = ''; + metadataExport.loiDashArrayString = this.selectedLoiDashArrayObject.dashArrayValue; + metadataExport.loiColor = this.loiColor; + metadataExport.loiWidth = this.loiWidth; + metadataExport.aoiColor = ''; + } else if (this.isAOI) { + metadataExport.poiSymbolBootstrap3Name = ''; + metadataExport.poiSymbolColor = ''; + metadataExport.poiMarkerColor = ''; + metadataExport.loiDashArrayString = ''; + metadataExport.loiColor = ''; + metadataExport.loiWidth = ''; + metadataExport.aoiColor = this.aoiColor; + } + + // Set topic reference + if (this.georesourceTopic_subsubsubTopic) { + metadataExport.topicReference = this.georesourceTopic_subsubsubTopic.topicId; + } else if (this.georesourceTopic_subsubTopic) { + metadataExport.topicReference = this.georesourceTopic_subsubTopic.topicId; + } else if (this.georesourceTopic_subTopic) { + metadataExport.topicReference = this.georesourceTopic_subTopic.topicId; + } else if (this.georesourceTopic_mainTopic) { + metadataExport.topicReference = this.georesourceTopic_mainTopic.topicId; + } else { + metadataExport.topicReference = ''; + } + + const metadataJSON = JSON.stringify(metadataExport); + let fileName = 'Georessource_Metadaten_Export'; + + if (this.datasetName) { + fileName += '-' + this.datasetName; + } + + fileName += '.json'; + + const blob = new Blob([metadataJSON], { type: 'application/json' }); + const data = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.download = fileName; + a.href = data; + a.textContent = 'JSON'; + a.target = '_blank'; + a.rel = 'noopener noreferrer'; + a.click(); + + a.remove(); + } + + // Main edit method + editGeoresourceMetadata(): void { + const patchBody: any = { + metadata: { + note: this.metadata.note, + literature: this.metadata.literature, + updateInterval: this.metadata.updateInterval.apiName, + sridEPSG: this.metadata.sridEPSG, + datasource: this.metadata.datasource, + contact: this.metadata.contact, + lastUpdate: this.metadata.lastUpdate, + description: this.metadata.description, + databasis: this.metadata.databasis + }, + allowedRoles: [], + datasetName: this.datasetName, + isAOI: this.isAOI, + isLOI: this.isLOI, + isPOI: this.isPOI, + topicReference: null + }; + + const roleIds = this.kommonitorDataGridHelperService.getSelectedRoleIds_roleManagementGrid(this.roleManagementTableOptions); + for (const roleId of roleIds) { + patchBody.allowedRoles.push(roleId); + } + + if (this.isPOI) { + patchBody.poiSymbolBootstrap3Name = this.selectedPoiIconName; + patchBody.poiSymbolColor = this.selectedPoiSymbolColor.colorName; + patchBody.poiMarkerColor = this.selectedPoiMarkerColor.colorName; + patchBody.loiDashArrayString = null; + patchBody.loiColor = null; + patchBody.loiWidth = null; + patchBody.aoiColor = null; + } else if (this.isLOI) { + patchBody.poiSymbolBootstrap3Name = null; + patchBody.poiSymbolColor = null; + patchBody.poiMarkerColor = null; + patchBody.loiDashArrayString = this.selectedLoiDashArrayObject.dashArrayValue; + patchBody.loiColor = this.loiColor; + patchBody.loiWidth = this.loiWidth; + patchBody.aoiColor = null; + } else if (this.isAOI) { + patchBody.poiSymbolBootstrap3Name = null; + patchBody.poiSymbolColor = null; + patchBody.poiMarkerColor = null; + patchBody.loiDashArrayString = null; + patchBody.loiColor = null; + patchBody.loiWidth = null; + patchBody.aoiColor = this.aoiColor; + } + + // Set topic reference + if (this.georesourceTopic_subsubsubTopic) { + patchBody.topicReference = this.georesourceTopic_subsubsubTopic.topicId; + } else if (this.georesourceTopic_subsubTopic) { + patchBody.topicReference = this.georesourceTopic_subsubTopic.topicId; + } else if (this.georesourceTopic_subTopic) { + patchBody.topicReference = this.georesourceTopic_subTopic.topicId; + } else if (this.georesourceTopic_mainTopic) { + patchBody.topicReference = this.georesourceTopic_mainTopic.topicId; + } else { + patchBody.topicReference = ''; + } + + this.loadingData = true; + + this.http.patch( + this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI + '/georesources/' + this.currentGeoresourceDataset.georesourceId, + patchBody + ).subscribe({ + next: (response: any) => { + this.successMessagePart = this.datasetName; + this.broadcastService.broadcast('refreshGeoresourceOverviewTable', { crudType: 'edit', targetGeoresourceId: this.currentGeoresourceDataset.georesourceId }); + this.showSuccessAlert(); + this.loadingData = false; + }, + error: (error: any) => { + if (error.data) { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error.data); + } else { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error); + } + this.showErrorAlert(); + this.loadingData = false; + } + }); + } + + // Alert methods + showSuccessAlert(): void { + const alertElement = document.getElementById('georesourceEditMetadataSuccessAlert'); + if (alertElement) { + alertElement.hidden = false; + } + } + + showErrorAlert(): void { + const alertElement = document.getElementById('georesourceEditMetadataErrorAlert'); + if (alertElement) { + alertElement.hidden = false; + } + } + + showMetadataImportErrorAlert(): void { + const alertElement = document.getElementById('georesourceEditMetadataImportErrorAlert'); + if (alertElement) { + alertElement.hidden = false; + } + } + + hideSuccessAlert(): void { + const alertElement = document.getElementById('georesourceEditMetadataSuccessAlert'); + if (alertElement) { + alertElement.hidden = true; + } + } + + hideErrorAlert(): void { + const alertElement = document.getElementById('georesourceEditMetadataErrorAlert'); + if (alertElement) { + alertElement.hidden = true; + } + } + + hideMetadataErrorAlert(): void { + const alertElement = document.getElementById('georesourceEditMetadataImportErrorAlert'); + if (alertElement) { + alertElement.hidden = true; + } + } + + // Get filtered topics for georesource + getMainTopicsForGeoresource(): any[] { + return this.kommonitorDataExchangeService.availableTopics.filter((topic: any) => + topic.topicType === 'main' && topic.topicResource === 'georesource' + ); + } + + // Validation for form submission + canSubmitForm(): boolean { + return !this.datasetNameInvalid && + !!this.metadata.description && + !!this.metadata.datasource && + !!this.metadata.contact && + !!this.metadata.updateInterval && + !!this.metadata.lastUpdate && + !this.poiMarkerTextInvalid; + } + + // Step navigation + nextStep(): void { + if (this.currentStep < 3) { + this.currentStep++; + } + } + + previousStep(): void { + if (this.currentStep > 1) { + this.currentStep--; + } + } + + goToStep(step: number): void { + if (step >= 1 && step <= 3) { + this.currentStep = step; + } + } + + // Modal control + cancel(): void { + this.activeModal.dismiss(); + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditUserRolesModal/georesource-edit-user-roles-modal.component.css b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditUserRolesModal/georesource-edit-user-roles-modal.component.css new file mode 100644 index 000000000..4ef9b936d --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditUserRolesModal/georesource-edit-user-roles-modal.component.css @@ -0,0 +1,241 @@ +/* Progress bar styling */ +#progressbar { + margin-bottom: 30px; + overflow: hidden; + color: lightgrey; + padding-left: 0px; + margin-top: 20px; +} + +#progressbar li { + list-style-type: none; + font-size: 14px; + width: 25%; + float: left; + position: relative; + text-align: center; + cursor: pointer; +} + +#progressbar li:hover { + color: #337ab7; +} + +#progressbar li.active { + color: #337ab7; +} + +#progressbar li:before { + width: 30px; + height: 30px; + line-height: 28px; + display: block; + font-size: 12px; + color: #ffffff; + background: lightgray; + border-radius: 50%; + margin: 0 auto 10px auto; + content: counter(step); + counter-increment: step; +} + +#progressbar li.active:before { + background: #337ab7; +} + +/* Form styling */ +.fs-title { + font-size: 24px; + text-transform: uppercase; + color: #2C3E50; + margin-bottom: 10px; + text-align: center; +} + +.fs-subtitle { + font-weight: normal; + font-size: 16px; + color: #666; + margin-bottom: 20px; + text-align: center; +} + +/* Action buttons */ +.action-button-previous { + width: 100px; + background: #C5C5F1; + font-weight: bold; + color: white; + border: 0 none; + border-radius: 0px; + cursor: pointer; + padding: 10px 5px; + margin: 10px 5px; +} + +.action-button-previous:hover, +.action-button-previous:focus { + box-shadow: 0 0 0 2px white, 0 0 0 3px #C5C5F1; +} + +/* Switch toggle styles */ +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; + margin: 0 10px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.switchslider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; +} + +.switchslider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .switchslider { + background-color: #2196F3; +} + +input:focus + .switchslider { + box-shadow: 0 0 1px #2196F3; +} + +input:checked + .switchslider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); +} + +.switchslider.round { + border-radius: 34px; +} + +.switchslider.round:before { + border-radius: 50%; +} + +/* Loading overlay */ +.loading-overlay-admin-panel { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.8); + z-index: 1000; + display: flex; + justify-content: center; + align-items: center; +} + +.loading-overlay-admin-panel.ng-hide { + display: none !important; +} + +.icon-spin { + animation: spin 1s infinite linear; + font-size: 24px; + color: #337ab7; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Vertical alignment helper */ +.vertical-align { + display: flex; + align-items: center; +} + +.margin-right { + margin-right: 10px; +} + +/* Alert customization */ +.alert { + margin-bottom: 20px; +} + +.alert-dismissable .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} + +/* Form group spacing */ +.form-group { + margin-bottom: 15px; +} + +/* Input group styling */ +.input-group-addon { + background-color: #eee; + border: 1px solid #ccc; + border-radius: 4px 0 0 4px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + text-align: center; + min-width: 40px; +} + +/* Help block styling */ +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #737373; + font-size: 13px; +} + +/* Select styling */ +select.form-control { + height: auto; + min-height: 34px; +} + +/* Modal specific adjustments */ +.modal-header { + padding: 15px; + border-bottom: 1px solid #e5e5e5; +} + +.modal-footer { + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} + +.modal-body { + position: relative; + padding: 15px; +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditUserRolesModal/georesource-edit-user-roles-modal.component.html b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditUserRolesModal/georesource-edit-user-roles-modal.component.html new file mode 100644 index 000000000..b82ebe2fb --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditUserRolesModal/georesource-edit-user-roles-modal.component.html @@ -0,0 +1,151 @@ + + + + + + + +
+ +

Zugriffsschutz und Eigentuümerschaft aktualisiert

+ Erfolgreiche Aktualisierung des Zugriffsschutzes und der Eigentuümerschaft für die Georessource '{{successMessagePart}}' +
+ + +
+ +

Aktualisierung gescheitert

+ Bei der Aktualisierung des Zugriffsschutzes und der Eigentuümerschaft ist ein Fehler aufgetreten. Fehlermeldung: +
+

+
\ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditUserRolesModal/georesource-edit-user-roles-modal.component.ts b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditUserRolesModal/georesource-edit-user-roles-modal.component.ts new file mode 100644 index 000000000..2756d328c --- /dev/null +++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditUserRolesModal/georesource-edit-user-roles-modal.component.ts @@ -0,0 +1,462 @@ +import { Component, OnInit, OnDestroy, Inject, ViewChild, ElementRef } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; +import { HttpClient } from '@angular/common/http'; +import { Subscription } from 'rxjs'; +import { AgGridAngular } from 'ag-grid-angular'; +import { ColDef, GridOptions, GridApi, ColumnApi, GridReadyEvent, FirstDataRenderedEvent, ColumnResizedEvent } from 'ag-grid-community'; +import { KommonitorDataGridHelperService } from 'services/adminSpatialUnit/kommonitor-data-grid-helper.service'; + +declare const $: any; +declare const __env: any; + +@Component({ + selector: 'georesource-edit-user-roles-modal-new', + templateUrl: './georesource-edit-user-roles-modal.component.html', + styleUrls: ['./georesource-edit-user-roles-modal.component.css'] +}) +export class GeoresourceEditUserRolesModalComponent implements OnInit, OnDestroy { + @ViewChild('roleManagementTable', { static: true }) roleManagementTable!: AgGridAngular; + + // Multi-step form + currentStep = 1; + totalSteps = 2; + + // Form data + loadingData = false; + errorMessage = ''; + successMessage = ''; + + // Current dataset being edited + private _currentGeoresourceDataset: any; + + get currentGeoresourceDataset(): any { + return this._currentGeoresourceDataset; + } + + set currentGeoresourceDataset(value: any) { + console.log('Setting currentGeoresourceDataset:', value); + this._currentGeoresourceDataset = value; + if (value) { + setTimeout(() => { + this.resetGeoresourceEditUserRolesForm(); + this.kommonitorMultiStepFormHelperService.registerClickHandler(); + }, 100); + } + } + + // Role management + roleManagementTableOptions: GridOptions = {}; + private gridApi!: GridApi; + private columnApi!: ColumnApi; + + // Form fields + activeRolesOnly = true; + permissions: any[] = []; + resourcesCreatorRights: any[] = []; + ownerOrgFilter = ''; + ownerOrganization: any; + + // Messages + successMessagePart = ''; + errorMessagePart = ''; + + // Subscriptions + private subscriptions: Subscription[] = []; + + constructor( + public activeModal: NgbActiveModal, + @Inject('kommonitorDataExchangeService') public kommonitorDataExchangeService: any, + @Inject('kommonitorMultiStepFormHelperService') public kommonitorMultiStepFormHelperService: any, + private kommonitorDataGridHelperService: KommonitorDataGridHelperService, + private broadcastService: BroadcastService, + private http: HttpClient + ) { + console.log('GeoresourceEditUserRolesModalComponent constructor initialized'); + } + + ngOnInit(): void { + console.log('GeoresourceEditUserRolesModalComponent ngOnInit'); + console.log('kommonitorDataExchangeService:', this.kommonitorDataExchangeService); + console.log('accessControl:', this.kommonitorDataExchangeService?.accessControl); + + this.setupEventListeners(); + this.prepareCreatorList(); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(sub => sub.unsubscribe()); + } + + private setupEventListeners(): void { + // Setup broadcast listeners + const broadcastSubscription = this.broadcastService.currentBroadcastMsg.subscribe(broadcastMsg => { + if (broadcastMsg) { + if (broadcastMsg.msg === 'onEditGeoresourcesUserRoles') { + this.onEditGeoresourcesUserRoles(broadcastMsg.values); + } else if (broadcastMsg.msg === 'availableRolesUpdate') { + this.refreshRoleManagementTable(); + } + } + }); + + this.subscriptions.push(broadcastSubscription); + } + + onEditGeoresourcesUserRoles(georesourceDataset: any): void { + this.currentGeoresourceDataset = georesourceDataset; + this.prepareCreatorList(); + this.resetGeoresourceEditUserRolesForm(); + this.kommonitorMultiStepFormHelperService?.registerClickHandler('georesourceEditUserRolesForm'); + } + + prepareCreatorList(): void { + if (this.kommonitorDataExchangeService.currentKomMonitorLoginRoleNames.length > 0) { + const creatorRights: string[] = []; + const creatorRightsChildren: string[] = []; + + this.kommonitorDataExchangeService.currentKomMonitorLoginRoleNames.forEach((roles: string) => { + const key = roles.split('.')[0]; + const role = roles.split('.')[1]; + + // case unit-resources-creator + if (role === 'unit-resources-creator' && !this.resourcesCreatorRights.includes(key)) { + creatorRights.push(key); + } + + // case client-resources-creator, gather unit-ids first, then fetch all unit-data + if (role === 'client-resources-creator' && !creatorRightsChildren.includes(key)) { + creatorRightsChildren.push(key); + } + }); + + // gather all children + this.gatherCreatorRightsChildren(creatorRights, creatorRightsChildren); + + this.resourcesCreatorRights = this.kommonitorDataExchangeService.accessControl.filter( + (elem: any) => creatorRights.includes(elem.name) + ); + } + } + + private gatherCreatorRightsChildren(creatorRights: string[], creatorRightsChildren: string[]): void { + if (creatorRightsChildren.length > 0) { + this.kommonitorDataExchangeService.accessControl + .filter((elem: any) => creatorRightsChildren.includes(elem.name)) + .flatMap((res: any) => res.children) + .forEach((child: any) => { + this.kommonitorDataExchangeService.accessControl + .filter((elem: any) => elem.organizationalUnitId === child) + .forEach((childData: any) => { + creatorRights.push(childData.name); + this.gatherCreatorRightsChildren(creatorRights, [childData.name]); + }); + }); + } + } + + refreshRoleManagementTable(): void { + console.log('refreshRoleManagementTable called'); + console.log('currentGeoresourceDataset:', this.currentGeoresourceDataset); + + this.permissions = this.currentGeoresourceDataset ? this.currentGeoresourceDataset.permissions : []; + console.log('permissions:', this.permissions); + + // set datasetOwner to disable checkboxes for owned datasets in permissions-table + if (this.kommonitorDataExchangeService.accessControl) { + this.kommonitorDataExchangeService.accessControl.forEach((item: any) => { + if (this.currentGeoresourceDataset) { + if (item.organizationalUnitId === this.currentGeoresourceDataset.ownerId) { + item.datasetOwner = true; + } else { + item.datasetOwner = false; + } + } + }); + } + + if (this.permissions.length === 0) { + this.activeRolesOnly = false; + } + + let access = this.kommonitorDataExchangeService.accessControl || []; + console.log('accessControl before filter:', access); + + if (this.permissions.length > 0 && this.activeRolesOnly) { + access = this.kommonitorDataExchangeService.accessControl.filter((unit: any) => { + return unit.permissions.filter((unitPermission: any) => + this.permissions.includes(unitPermission.permissionId) + ).length > 0; + }); + } + + console.log('accessControl after filter:', access); + + this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid( + 'georesourceEditRoleManagementTable', + this.roleManagementTableOptions, + access, + this.permissions, + true + ); + + console.log('roleManagementTableOptions created:', this.roleManagementTableOptions); + } + + onActiveRolesOnlyChange(): void { + this.activeRolesOnly = !this.activeRolesOnly; + this.refreshRoleManagementTable(); + } + + onChangeOwner(ownerOrganization: any): void { + this.ownerOrganization = ownerOrganization; + console.log('Target creator role selected to be', this.ownerOrganization); + this.refreshRoles(ownerOrganization); + } + + private refreshRoles(orgUnitId: any): void { + const permissionIds_ownerUnit = orgUnitId ? + this.kommonitorDataExchangeService.getAccessControlById(orgUnitId).permissions + .filter((permission: any) => permission.permissionLevel === 'viewer' || permission.permissionLevel === 'editor') + .map((permission: any) => permission.permissionId) : []; + + // set datasetOwner to disable checkboxes for owned datasets in permissions-table + this.kommonitorDataExchangeService.accessControl.forEach((item: any) => { + if (item.organizationalUnitId === orgUnitId) { + item.datasetOwner = true; + } else { + item.datasetOwner = false; + } + }); + + this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid( + 'georesourceEditRoleManagementTable', + this.roleManagementTableOptions, + this.kommonitorDataExchangeService.accessControl, + permissionIds_ownerUnit, + true + ); + } + + // Step navigation + nextStep(): void { + if (this.currentStep < this.totalSteps) { + this.currentStep++; + } + } + + previousStep(): void { + if (this.currentStep > 1) { + this.currentStep--; + } + } + + goToStep(step: number): void { + if (step >= 1 && step <= this.totalSteps) { + this.currentStep = step; + } + } + + // AG Grid event handlers + onGridReady(event: GridReadyEvent): void { + this.gridApi = event.api; + this.columnApi = event.columnApi; + console.log('Role management grid is ready, API initialized'); + } + + onFirstDataRendered(event: FirstDataRenderedEvent): void { + // Handle first data rendered event + } + + onColumnResized(event: ColumnResizedEvent): void { + // Handle column resize event + } + + // Form actions + editGeoresourceEditUserRolesForm(): void { + if (this.ownerOrganization !== undefined && this.ownerOrganization !== this.currentGeoresourceDataset.ownerId) { + if (!confirm('Sind Sie sicher, dass Sie den Eigentümerschaft an dieser Resource endgültig und unwiderruflich übertragen und damit abgeben wollen?')) { + return; + } + } + + this.putUserRoles(); + this.putOwnership(); + } + + putUserRoles(): void { + this.loadingData = true; + + const putBody = { + permissions: this.getSelectedRoleIds(), + isPublic: this.currentGeoresourceDataset.isPublic + }; + + this.http.put( + `${this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI}/georesources/${this.currentGeoresourceDataset.georesourceId}/permissions`, + putBody, + { + headers: { + 'Content-Type': 'application/json' + } + } + ).subscribe({ + next: (response: any) => { + this.successMessagePart = this.currentGeoresourceDataset.datasetName; + this.broadcastService.broadcast('refreshGeoresourceOverviewTable', { + crudType: 'edit', + targetGeoresourceId: this.currentGeoresourceDataset.georesourceId + }); + this.showSuccessAlert(); + setTimeout(() => { + this.loadingData = false; + }, 250); + }, + error: (error: any) => { + this.errorMessagePart = 'Fehler beim Aktualisieren der Zugriffsrechte. Fehler lautet: \n\n'; + if (error.data) { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error.data); + } else { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error); + } + this.showErrorAlert(); + setTimeout(() => { + this.loadingData = false; + }, 250); + } + }); + } + + putOwnership(): void { + this.loadingData = true; + + const putBody = { + ownerId: this.ownerOrganization === undefined ? this.currentGeoresourceDataset.ownerId : this.ownerOrganization + }; + + this.http.put( + `${this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI}/georesources/${this.currentGeoresourceDataset.georesourceId}/ownership`, + putBody, + { + headers: { + 'Content-Type': 'application/json' + } + } + ).subscribe({ + next: (response: any) => { + this.successMessagePart = this.currentGeoresourceDataset.datasetName; + this.broadcastService.broadcast('refreshGeoresourceOverviewTable', { + crudType: 'edit', + targetGeoresourceId: this.currentGeoresourceDataset.georesourceId + }); + this.showSuccessAlert(); + setTimeout(() => { + this.loadingData = false; + }, 250); + }, + error: (error: any) => { + this.errorMessagePart = 'Fehler beim Aktualisieren der Eigentümerschaft. Fehler lautet: \n\n'; + if (error.data) { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error.data); + } else { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error); + } + this.showErrorAlert(); + setTimeout(() => { + this.loadingData = false; + }, 250); + } + }); + } + + resetGeoresourceEditUserRolesForm(): void { + this.ownerOrganization = this.currentGeoresourceDataset?.ownerId; + this.refreshRoleManagementTable(); + this.ownerOrgFilter = ''; + this.successMessagePart = ''; + this.errorMessagePart = ''; + this.hideSuccessAlert(); + this.hideErrorAlert(); + + setTimeout(() => { + // Trigger change detection if needed + }, 250); + } + + // Helper methods + getFilteredOrganizations(): any[] { + if (!this.ownerOrgFilter) { + return this.kommonitorDataExchangeService.accessControl || []; + } + return this.kommonitorDataExchangeService.accessControl?.filter((org: any) => + org.name.toLowerCase().includes(this.ownerOrgFilter.toLowerCase()) + ) || []; + } + + getFilteredCreatorRights(): any[] { + if (!this.ownerOrgFilter) { + return this.resourcesCreatorRights; + } + return this.resourcesCreatorRights.filter((org: any) => + org.name.toLowerCase().includes(this.ownerOrgFilter.toLowerCase()) + ); + } + + getCurrentOwnerName(): string { + if (this.currentGeoresourceDataset?.ownerId) { + const owner = this.kommonitorDataExchangeService.getAccessControlById(this.currentGeoresourceDataset.ownerId); + return owner ? owner.name : ''; + } + return ''; + } + + // Helper method to get selected role IDs from the grid + private getSelectedRoleIds(): string[] { + const ids: string[] = []; + const deselectedIds: string[] = []; + + if (this.gridApi) { + this.gridApi.forEachNode((node: any, index: number) => { + if (node.data) { + for (const permission of node.data.permissions) { + if (permission) { + if (permission.isChecked) { + if (!deselectedIds.includes(permission.permissionId)) { + ids.push(permission.permissionId); + } + } else { + deselectedIds.push(permission.permissionId); + } + } + } + } + }); + } + + return ids; + } + + // Alert methods + showSuccessAlert(): void { + this.successMessage = 'Zugriffsschutz und Eigentümerschaft erfolgreich aktualisiert'; + setTimeout(() => this.hideSuccessAlert(), 5000); + } + + hideSuccessAlert(): void { + this.successMessage = ''; + } + + showErrorAlert(): void { + setTimeout(() => this.hideErrorAlert(), 10000); + } + + hideErrorAlert(): void { + this.errorMessage = ''; + this.errorMessagePart = ''; + } + + // Modal control methods + cancel(): void { + this.activeModal.dismiss(); + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/admin-indicators-management.component.css b/app/components/ngComponents/admin/adminIndicatorsManagement/admin-indicators-management.component.css new file mode 100644 index 000000000..eb74df43e --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/admin-indicators-management.component.css @@ -0,0 +1,305 @@ +/* Admin Indicators Management Component Styles */ + +.loading-overlay-admin-panel { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.8); + z-index: 9999; + display: flex; + justify-content: center; + align-items: center; +} + +.loading-overlay-admin-panel .glyphicon { + font-size: 2em; + color: #3c8dbc; +} + +.adminTableButtonWrapper { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + flex-wrap: wrap; + gap: 10px; +} + +.verticalAlign { + display: flex; + align-items: center; + gap: 10px; +} + +/* Switch styles */ +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.switchslider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: .4s; + border-radius: 34px; +} + +.switchslider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + transition: .4s; + border-radius: 50%; +} + +input:checked + .switchslider { + background-color: #2196F3; +} + +input:checked + .switchslider:before { + transform: translateX(26px); +} + +/* AG Grid customizations */ +.ag-theme-alpine { + --ag-header-height: 50px; + --ag-row-height: 60px; + --ag-header-background-color: #f4f4f4; + --ag-header-foreground-color: #333; + --ag-border-color: #ddd; + --ag-row-hover-color: #f8f9fa; + --ag-selected-row-background-color: #e3f2fd; +} + +.ag-header-cell { + font-weight: bold; + border-bottom: 2px solid #ddd; +} + +.ag-cell { + padding: 8px; + border-right: 1px solid #eee; +} + +/* Button group styles */ +.btn-group-sm > .btn { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; + margin-right: 2px; +} + +.btn-group-sm > .btn:last-child { + margin-right: 0; +} + +/* Hierarchy view styles */ +.hierarchy-placeholder { + padding: 20px; + text-align: center; + color: #666; + background-color: #f9f9f9; + border: 1px dashed #ccc; + border-radius: 4px; +} + +/* Topic hierarchy styles */ +.list-group-root { + padding: 0; +} + +.list-group-item { + border-radius: 0; + border-left: none; + border-right: none; +} + +.list-group-item:first-child { + border-top: none; +} + +.list-group-item:last-child { + border-bottom: none; +} + +.kommonitor-theme { + background: #2171b5 !important; + color: white !important; +} + +.kommonitor-theme-light { + background: #6baed6 !important; + color: white !important; +} + +.collapseTrigger { + display: block; + text-decoration: none; + cursor: pointer; +} + +.collapseTrigger:hover { + text-decoration: none; +} + +/* Indicator styles */ +.list-group-item-default.style-simple-indicator { + background-color: #f5f5f5; + border-color: #ddd; + color: black; +} + +.list-group-item-default.style-headline-indicator { + background-color: #337ab7; + border-color: #2e6da4; + color: white; +} + +.list-group-item-default.style-headline-indicator p { + color: white !important; +} + +.list-group-item-default.style-simple-indicator p { + color: black !important; +} + +/* Drag & Drop styles */ +.cdk-drag-preview { + box-sizing: border-box; + border-radius: 4px; + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), + 0 8px 10px 1px rgba(0, 0, 0, 0.14), + 0 3px 14px 2px rgba(0, 0, 0, 0.12); +} + +.cdk-drag-placeholder { + opacity: 0.3; +} + +.cdk-drag-animating { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.indicatorInputForm.cdk-drop-list-dragging .list-group-item:not(.cdk-drag-placeholder) { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +/* Drag handle styles */ +.cdk-drag-handle { + cursor: move; +} + +/* Hover effects for draggable items */ +.list-group-item[cdkDrag]:hover { + background-color: rgba(0, 0, 0, 0.05); + transform: translateY(-1px); + transition: all 0.2s ease; +} + +/* Collapse animation */ +.collapse { + transition: height 0.35s ease; +} + +.collapse.in { + display: block; +} + +/* Topic level indentation */ +.list-group .list-group { + margin-left: 20px; +} + +.list-group .list-group .list-group { + margin-left: 20px; +} + +.list-group .list-group .list-group .list-group { + margin-left: 20px; +} + +/* Box styles */ +.box { + margin-bottom: 20px; +} + +.box-header { + padding: 15px; + border-bottom: 1px solid #f4f4f4; + background-color: #fff; +} + +.box-title { + margin: 0; + font-size: 18px; + font-weight: 600; +} + +.box-body { + padding: 15px; +} + +.box-tools { + float: right; +} + +.btn-box-tool { + padding: 5px 10px; + background: transparent; + border: 0; + color: #97a0b3; +} + +.btn-box-tool:hover { + color: #606c84; +} + +/* Responsive design */ +@media (max-width: 768px) { + .adminTableButtonWrapper { + flex-direction: column; + align-items: stretch; + } + + .verticalAlign { + justify-content: center; + } + + .ag-theme-alpine { + font-size: 12px; + } +} + +/* Animation for loading */ +.icon-spin { + animation: spin 1s infinite linear; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/admin-indicators-management.component.html b/app/components/ngComponents/admin/adminIndicatorsManagement/admin-indicators-management.component.html new file mode 100644 index 000000000..ffe91f928 --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/admin-indicators-management.component.html @@ -0,0 +1,345 @@ +
+ +
+
+ +
+
+ + +
+

+ Verwalten der Indikatoren + Info +

+ +
+
+ Nur editierbare Datensätze anzeigen +
+ +
+
+ + + + + + +
+
+ + +
+ + + + +
+
+

Indikatoren-Metadaten

+ +
+ +
+
+ + +
+ +
+ + + + +
+
+ +
+ + + +
+
+

Indikatoren-Reihenfolge in Themenkatalog

+ +
+ +
+
+
+ +
+ +
+ + +
+

+    + Standardindikator (numerische Wertverteilung) [Einheitsbezeichnung] +

+
+ +
+

+    + Leitindikator (bewertende Aussage) [Einheitsbezeichnung] +

+
+ +
+ + + +
+ Laden der Themenhierarchie... +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + +
+
+
+

+ +   {{indicatorMetadata.indicatorName}} + [{{indicatorMetadata.unit}}] +

+
+
+

+ +   {{indicatorMetadata.indicatorName}} + [{{indicatorMetadata.unit}}] +

+
+
+
+
+ + +
+
+
+

+ +   {{indicatorMetadata.indicatorName}} + [{{indicatorMetadata.unit}}] +

+
+
+

+ +   {{indicatorMetadata.indicatorName}} + [{{indicatorMetadata.unit}}] +

+
+
+
+
+
+ + +
+
+
+

+ +   {{indicatorMetadata.indicatorName}} + [{{indicatorMetadata.unit}}] +

+
+
+

+ +   {{indicatorMetadata.indicatorName}} + [{{indicatorMetadata.unit}}] +

+
+
+
+
+
+ + +
+
+
+

+ +   {{indicatorMetadata.indicatorName}} + [{{indicatorMetadata.unit}}] +

+
+
+

+ +   {{indicatorMetadata.indicatorName}} + [{{indicatorMetadata.unit}}] +

+
+
+
+
+
+ +
+ +
+ +
+ +
+ + +
+ + +
\ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/admin-indicators-management.component.ts b/app/components/ngComponents/admin/adminIndicatorsManagement/admin-indicators-management.component.ts new file mode 100644 index 000000000..2788b6591 --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/admin-indicators-management.component.ts @@ -0,0 +1,784 @@ +import { Component, Inject, OnInit, NgZone, OnDestroy, ViewChild } from '@angular/core'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; +import { DOCUMENT } from '@angular/common'; +import { Subscription } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { AgGridAngular } from 'ag-grid-angular'; +import { ColDef, GridOptions, GridReadyEvent, RowNode, SelectionChangedEvent } from 'ag-grid-community'; +import { KommonitorIndicatorDataExchangeService } from 'services/adminIndicatorUnit/kommonitor-data-exchange.service'; +import { KommonitorIndicatorCacheHelperService } from 'services/adminIndicatorUnit/kommonitor-cache-helper.service'; +import { KommonitorIndicatorDataGridHelperService } from 'services/adminIndicatorUnit/kommonitor-data-grid-helper.service'; +import { IndicatorAddModalComponent } from './indicatorAddModal/indicator-add-modal.component'; +import { IndicatorEditMetadataModalComponent } from './indicatorEditMetadataModal/indicator-edit-metadata-modal.component'; +import { IndicatorEditFeaturesModalComponent } from './indicatorEditFeaturesModal/indicator-edit-features-modal.component'; +import { IndicatorDeleteModalComponent } from './indicatorDeleteModal/indicator-delete-modal.component'; +import { IndicatorBatchUpdateModalComponent } from './indicatorBatchUpdateModal/indicator-batch-update-modal.component'; + +declare const $: any; +declare const __env: any; + +@Component({ + selector: 'admin-indicators-management-new', + templateUrl: './admin-indicators-management.component.html', + styleUrls: ['./admin-indicators-management.component.css'] +}) +export class AdminIndicatorsManagementComponent implements OnInit, OnDestroy { + + @ViewChild(AgGridAngular) agGrid!: AgGridAngular; + + public loadingData: boolean = true; + public initializationCompleted: boolean = false; + public tableViewSwitcher: boolean = false; + public selectIndicatorEntriesInput: boolean = false; + + // AG Grid properties + public columnDefs: ColDef[] = []; + public rowData: any[] = []; + public gridOptions: GridOptions = {}; + public selectedRows: any[] = []; + + // Drag & Drop properties + public collapsedTopics: Set = new Set(); + public sortableConfig: any = { + onEnd: (evt: any) => { + const updatedIndicatorMetadataEntries = evt.models; + + // for those models send API request to persist new sort order + const patchBody: Array<{indicatorId: string, displayOrder: number}> = []; + for (let index = 0; index < updatedIndicatorMetadataEntries.length; index++) { + const indicatorMetadata = updatedIndicatorMetadataEntries[index]; + + patchBody.push({ + "indicatorId": indicatorMetadata.indicatorId, + "displayOrder": index + }); + } + + this.http.patch( + this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI + "/indicators/display-order", + patchBody + ).subscribe({ + next: (response: any) => { + // Success - no action needed + }, + error: (error: any) => { + this.kommonitorDataExchangeService.displayMapApplicationError(error); + } + }); + } + }; + private subscriptions: Subscription[] = []; + + constructor( + @Inject(DOCUMENT) private document: Document, + private zone: NgZone, + private modalService: NgbModal, + private broadcastService: BroadcastService, + private http: HttpClient, + public kommonitorDataExchangeService: KommonitorIndicatorDataExchangeService, + private kommonitorCacheHelperService: KommonitorIndicatorCacheHelperService, + private kommonitorDataGridHelperService: KommonitorIndicatorDataGridHelperService + ) {} + + ngOnInit(): void { + // Initialize any adminLTE box widgets + (window as any).$('.box').boxWidget(); + + // Make component available globally for debugging + (window as any).adminIndicatorsComponent = this; + + // Try to load data if not already available + this.ensureDataLoaded(); + + this.initializeOrRefreshOverviewTable(); + this.setupEventListeners(); + + // Add polling mechanism to check for data availability + this.startDataPolling(); + + // Add a fallback timeout to prevent infinite loading + setTimeout(() => { + if (this.loadingData) { + this.ensureDataLoaded(); + this.initializeOrRefreshOverviewTable(); + + // If still no data after fallback, stop loading anyway + const filteredIndicators = this.getFilteredIndicators(); + if (!filteredIndicators || filteredIndicators.length === 0) { + this.loadingData = false; + this.initializationCompleted = true; + } + } + }, 3000); // 3 second timeout + } + + private async ensureDataLoaded(): Promise { + // If no indicators are available, try to fetch them + if (!this.kommonitorDataExchangeService.availableIndicators || + this.kommonitorDataExchangeService.availableIndicators.length === 0) { + try { + await this.kommonitorDataExchangeService.fetchIndicatorsMetadata( + this.kommonitorDataExchangeService.currentKeycloakLoginRoles + ); + // Force refresh the table after data is loaded + setTimeout(() => { + this.forceRefreshGrid(); + }, 100); + } catch (error) { + console.error('Error fetching indicators:', error); + } + } + } + + private forceRefreshGrid(): void { + const indicators = this.getFilteredIndicators(); + + if (indicators && indicators.length > 0) { + this.columnDefs = this.kommonitorDataGridHelperService.buildDataGridColumnConfig_indicators(indicators); + this.rowData = this.kommonitorDataGridHelperService.buildDataGridRowData_indicators(indicators); + + // Update the grid if it's ready + if (this.agGrid && this.agGrid.api) { + this.agGrid.api.setRowData(this.rowData); + this.agGrid.api.setColumnDefs(this.columnDefs); + this.agGrid.api.refreshCells(); + this.loadingData = false; + this.initializationCompleted = true; + } + } + } + + ngOnDestroy(): void { + this.subscriptions.forEach(sub => sub.unsubscribe()); + + // Clean up global reference + if ((window as any).adminIndicatorsComponent === this) { + delete (window as any).adminIndicatorsComponent; + } + } + + private setupEventListeners(): void { + // Listen for the global metadata loading completion event + const sub = this.broadcastService.currentBroadcastMsg.subscribe(data => { + if (data.msg === 'initialMetadataLoadingCompleted') { + this.zone.run(() => { + setTimeout(() => { + this.initializeOrRefreshOverviewTable(); + // Also ensure topics are collapsed when metadata is loaded + this.initializeCollapsedTopics(); + }, 250); + }); + } + else if (data.msg === 'initialMetadataLoadingFailed') { + this.zone.run(() => { + this.loadingData = false; + }); + } + else if (data.msg === 'refreshIndicatorOverviewTable') { + this.zone.run(() => { + this.loadingData = true; + // Extract crudType and targetIndicatorId from the broadcast data + const crudType = (data as any).crudType; + const targetIndicatorId = (data as any).targetIndicatorId; + this.refreshIndicatorOverviewTable(crudType, targetIndicatorId); + }); + } + // Handle grid button click events + else if (data.msg === 'onEditIndicatorMetadata') { + this.zone.run(() => { + this.onClickEditMetadata(data.values); + }); + } + else if (data.msg === 'onEditIndicatorFeatures') { + this.zone.run(() => { + this.onClickEditFeatures(data.values); + }); + } + else if (data.msg === 'onEditIndicatorSpatialUnitRoles') { + this.zone.run(() => { + this.onClickEditIndicatorSpatialUnitRoles(data.values); + }); + } + else if (data.msg === 'onDeleteIndicators') { + this.zone.run(() => { + // Ensure data.values is an array for delete operation + const datasetsToDelete = Array.isArray(data.values) ? data.values : [data.values]; + this.onClickDeleteIndicators(datasetsToDelete); + }); + } + }); + this.subscriptions.push(sub); + + // Listen for custom events from the data grid helper service + const handleEditMetadata = (event: CustomEvent) => { + this.zone.run(() => { + this.onClickEditMetadata(event.detail.values); + }); + }; + + const handleEditFeatures = (event: CustomEvent) => { + this.zone.run(() => { + this.onClickEditFeatures(event.detail.values); + }); + }; + + const handleEditUserRoles = (event: CustomEvent) => { + this.zone.run(() => { + this.onClickEditIndicatorSpatialUnitRoles(event.detail.values); + }); + }; + + // Add event listeners + document.addEventListener('onEditIndicatorMetadata', handleEditMetadata as EventListener); + document.addEventListener('onEditIndicatorFeatures', handleEditFeatures as EventListener); + document.addEventListener('onEditIndicatorSpatialUnitRoles', handleEditUserRoles as EventListener); + + // Store references for cleanup + const customEventSubscription = { + unsubscribe: () => { + document.removeEventListener('onEditIndicatorMetadata', handleEditMetadata as EventListener); + document.removeEventListener('onEditIndicatorFeatures', handleEditFeatures as EventListener); + document.removeEventListener('onEditIndicatorSpatialUnitRoles', handleEditUserRoles as EventListener); + } + } as any; + this.subscriptions.push(customEventSubscription); + } + + public initializeOrRefreshOverviewTable(): void { + const indicators = this.getFilteredIndicators(); + + if (indicators && indicators.length > 0) { + this.loadingData = false; + this.initializationCompleted = true; + + // Initialize all topics as collapsed + this.initializeCollapsedTopics(); + + // Set up grid options first + this.setupGridOptions(indicators); + + // Use the data grid helper service to build column definitions and row data + this.columnDefs = this.kommonitorDataGridHelperService.buildDataGridColumnConfig_indicators(indicators); + this.rowData = this.kommonitorDataGridHelperService.buildDataGridRowData_indicators(indicators); + + // Force change detection + setTimeout(() => { + if (this.agGrid && this.agGrid.api) { + this.agGrid.api.setRowData(this.rowData); + this.agGrid.api.setColumnDefs(this.columnDefs); + this.agGrid.api.refreshCells(); + } + }, 100); + } else { + // Data not ready yet, keep loading + this.loadingData = true; + this.initializationCompleted = false; + } + } + + private setupGridOptions(indicatorMetadataArray: any[]): void { + this.gridOptions = { + defaultColDef: { + editable: false, + sortable: true, + flex: 1, + minWidth: 200, + filter: true, + floatingFilter: true, + resizable: true, + wrapText: true, + autoHeight: true, + cellStyle: { + 'font-size': '12px;', + 'white-space': 'normal !important', + "line-height": "20px !important", + "word-break": "break-word !important", + "padding-top": "17px", + "padding-bottom": "17px" + }, + headerComponentParams: { + template: + '', + }, + }, + components: { + displayEditButtons_indicators: this.kommonitorDataGridHelperService.displayEditButtons_indicators + }, + enableCellTextSelection: true, + ensureDomOrder: true, + pagination: true, + paginationPageSize: 10, + suppressColumnVirtualisation: true, + rowSelection: 'multiple', + suppressRowClickSelection: true, + onGridReady: (params: GridReadyEvent) => { + this.onGridReady(params); + }, + onFirstDataRendered: () => { + this.onFirstDataRendered(); + }, + onColumnResized: () => { + this.onColumnResized(); + }, + onModelUpdated: () => { + this.onModelUpdated(indicatorMetadataArray); + }, + onViewportChanged: () => { + this.onViewportChanged(indicatorMetadataArray); + }, + onSelectionChanged: (event: SelectionChangedEvent) => { + this.onSelectionChanged(event); + } + }; + } + + // Grid event handlers + onGridReady(params: GridReadyEvent): void { + // If we have data, set it now + if (this.rowData && this.rowData.length > 0) { + params.api.setRowData(this.rowData); + params.api.setColumnDefs(this.columnDefs); + } else { + // If no data is available, try to load it + if (!this.kommonitorDataExchangeService.availableIndicators || + this.kommonitorDataExchangeService.availableIndicators.length === 0) { + this.ensureDataLoaded(); + } else { + this.forceRefreshGrid(); + } + } + } + + onFirstDataRendered(): void { + } + + onColumnResized(): void { + // Column resized + } + + onModelUpdated(indicatorMetadataArray: any[]): void { + this.kommonitorDataGridHelperService.registerClickHandler_indicators(indicatorMetadataArray); + } + + + + onViewportChanged(indicatorMetadataArray: any[]): void { + this.kommonitorDataGridHelperService.registerClickHandler_indicators(indicatorMetadataArray); + setTimeout(() => { + // MathJax rendering if available + if ((window as any).MathJax && (window as any).MathJax.typesetPromise) { + (window as any).MathJax.typesetPromise().then(() => { + // MathJax rendering completed + }); + } + }, 250); + } + + onSelectionChanged(event: SelectionChangedEvent): void { + this.selectedRows = event.api.getSelectedRows(); + } + + private getFilteredIndicators(): any[] { + const allIndicators = this.kommonitorDataExchangeService.availableIndicators; + + if (this.tableViewSwitcher) { + // Filter out indicators where user only has viewer permission + const filtered = allIndicators.filter(e => !(e.userPermissions && e.userPermissions.length === 1 && e.userPermissions.includes('viewer'))); + return filtered; + } else { + return allIndicators; + } + } + + // Debug method to force stop loading + stopLoading(): void { + this.loadingData = false; + this.initializationCompleted = true; + } + + // Debug method to manually refresh the grid + debugRefreshGrid(): void { + // Force refresh + this.forceRefreshGrid(); + } + + // Table view switcher method + onTableViewSwitch(): void { + // Filter the data based on the tableViewSwitcher state + this.initializeOrRefreshOverviewTable(); + } + + // Alias for the add indicator modal (matching HTML template) + openAddIndicatorModal(): void { + this.onClickAddIndicator(); + } + + // Modal event handlers + onClickAddIndicator(): void { + try { + const modalRef = this.modalService.open(IndicatorAddModalComponent, { + size: 'lg', + backdrop: 'static', + keyboard: false, + container: 'body', + animation: false + }); + + modalRef.result.then((result) => { + if (result) { + // Modal was closed successfully, refresh the table + this.initializeOrRefreshOverviewTable(); + } + }).catch((error) => { + // Modal dismissed + }); + } catch (error) { + console.error('Error opening modal:', error); + } + } + + onClickEditMetadata(indicatorMetadata: any): void { + try { + const modalRef = this.modalService.open(IndicatorEditMetadataModalComponent, { + size: 'lg', + backdrop: 'static', + keyboard: false, + container: 'body', + animation: false + }); + + // Set the current indicator dataset in the modal component + const modalComponent = modalRef.componentInstance as IndicatorEditMetadataModalComponent; + modalComponent.currentIndicatorDataset = indicatorMetadata; + modalComponent.resetIndicatorEditMetadataForm(); + + modalRef.result.then((result) => { + if (result) { + // Modal was closed successfully, refresh the table + this.initializeOrRefreshOverviewTable(); + } + }).catch((error) => { + // Modal dismissed + }); + } catch (error) { + console.error('Error opening edit metadata modal:', error); + } + } + + onClickEditFeatures(indicatorMetadata: any): void { + try { + const modalRef = this.modalService.open(IndicatorEditFeaturesModalComponent, { + size: 'lg', + backdrop: 'static', + keyboard: false, + container: 'body', + animation: false + }); + + const modalComponent = modalRef.componentInstance as IndicatorEditFeaturesModalComponent; + modalComponent.openModal(indicatorMetadata); + + modalRef.result.then((result) => { + if (result) { + // Modal was closed successfully, refresh the table + this.initializeOrRefreshOverviewTable(); + } + }).catch((error) => { + // Modal dismissed + }); + } catch (error) { + console.error('Error opening edit features modal:', error); + } + } + + onClickEditIndicatorSpatialUnitRoles(indicatorMetadata: any): void { + try { + // Broadcast the event to open the modal + this.broadcastService.broadcast('onEditIndicatorSpatialUnitRoles', indicatorMetadata); + } catch (error) { + console.error('Error opening edit indicator spatial unit roles modal:', error); + } + } + + onClickDeleteIndicators(indicatorsMetadata: any[]): void { + if (indicatorsMetadata.length === 1) { + // Open the Angular delete modal for single indicator + this.openDeleteIndicatorModal(indicatorsMetadata[0]); + } else { + // For multiple indicators, we might need to handle differently + // For now, just open the modal with the first indicator + console.log('Multiple indicators delete not yet supported'); + } + } + + openDeleteIndicatorModal(indicatorDataset: any): void { + const modalRef = this.modalService.open(IndicatorDeleteModalComponent, { + size: 'lg', + backdrop: 'static', + keyboard: false, + container: 'body', + animation: false + }); + + // Set the selected indicator in the modal + modalRef.componentInstance.selectedIndicatorDataset = indicatorDataset; + modalRef.componentInstance.onChangeSelectedIndicator(); + + modalRef.result.then((result) => { + // Delete modal closed with result + }).catch((error) => { + // Delete modal dismissed + }); + } + + onClickBatchUpdate(): void { + try { + const modalRef = this.modalService.open(IndicatorBatchUpdateModalComponent, { + size: 'lg', + backdrop: 'static', + keyboard: false, + container: 'body', + animation: false + }); + + // Pass the modal reference to the component + const modalComponent = modalRef.componentInstance as IndicatorBatchUpdateModalComponent; + modalComponent.modalRef = modalRef; + + modalRef.result.then((result) => { + if (result) { + // Modal was closed successfully, refresh the table + this.initializeOrRefreshOverviewTable(); + } + }).catch((error) => { + // Modal dismissed + }); + } catch (error) { + console.error('Error opening batch update modal:', error); + } + } + + onClickDeleteSelected(): void { + const selectedIndicators = this.getSelectedIndicatorsMetadata(); + if (selectedIndicators.length > 0) { + this.onClickDeleteIndicators(selectedIndicators); + } else { + // Show message that no indicators are selected + } + } + + onChangeSelectIndicatorEntries(): void { + if (this.selectIndicatorEntriesInput) { + // TODO: Implement when availableIndicatorDatasets is available + // this.availableIndicatorDatasets.forEach(function(dataset) { + // dataset.isSelected = true; + // }); + } else { + // TODO: Implement when availableIndicatorDatasets is available + // this.availableIndicatorDatasets.forEach(function(dataset) { + // dataset.isSelected = false; + // }); + } + } + + refreshIndicatorOverviewTable(crudType?: string, targetIndicatorId?: string): void { + if (!crudType || !targetIndicatorId) { + // refetch all metadata from indicators to update table + this.kommonitorDataExchangeService.fetchIndicatorsMetadata(this.kommonitorDataExchangeService.currentKeycloakLoginRoles) + .then((response: any) => { + this.initializeOrRefreshOverviewTable(); + this.broadcastService.broadcast('refreshIndicatorOverviewTableCompleted'); + this.loadingData = false; + }) + .catch((response: any) => { + this.loadingData = false; + this.broadcastService.broadcast('refreshIndicatorOverviewTableCompleted'); + }); + } + else if (crudType && targetIndicatorId) { + if (crudType === 'add') { + this.kommonitorCacheHelperService.fetchSingleIndicatorMetadata(targetIndicatorId, this.kommonitorDataExchangeService.currentKeycloakLoginRoles) + .then((data: any) => { + this.kommonitorDataExchangeService.addSingleIndicatorMetadata(data); + this.initializeOrRefreshOverviewTable(); + this.broadcastService.broadcast('refreshIndicatorOverviewTableCompleted'); + this.loadingData = false; + }) + .catch((response: any) => { + this.loadingData = false; + this.broadcastService.broadcast('refreshIndicatorOverviewTableCompleted'); + }); + } + else if (crudType === 'edit') { + this.kommonitorCacheHelperService.fetchSingleIndicatorMetadata(targetIndicatorId, this.kommonitorDataExchangeService.currentKeycloakLoginRoles) + .then((data: any) => { + this.kommonitorDataExchangeService.replaceSingleIndicatorMetadata(data); + this.initializeOrRefreshOverviewTable(); + this.broadcastService.broadcast('refreshIndicatorOverviewTableCompleted'); + this.loadingData = false; + }) + .catch((response: any) => { + this.loadingData = false; + this.broadcastService.broadcast('refreshIndicatorOverviewTableCompleted'); + }); + } + else if (crudType === 'delete') { + this.kommonitorDataExchangeService.deleteSingleIndicatorMetadata(targetIndicatorId); + this.initializeOrRefreshOverviewTable(); + this.broadcastService.broadcast('refreshIndicatorOverviewTableCompleted'); + this.loadingData = false; + } + } + } + + // Utility methods + checkCreatePermission(): boolean { + return this.kommonitorDataExchangeService.checkCreatePermission(); + } + + checkEditorPermission(): boolean { + return this.kommonitorDataExchangeService.checkEditorPermission(); + } + + checkDeletePermission(): boolean { + return this.kommonitorDataExchangeService.checkDeletePermission(); + } + + private startDataPolling(): void { + // Poll every 500ms for data availability + const pollInterval = setInterval(() => { + if (this.loadingData) { + this.initializeOrRefreshOverviewTable(); + + // If data is found, stop polling + if (!this.loadingData) { + clearInterval(pollInterval); + } + } else { + // Data loaded, stop polling + clearInterval(pollInterval); + } + }, 500); + + // Stop polling after 10 seconds regardless + setTimeout(() => { + clearInterval(pollInterval); + }, 10000); + } + + getSelectedIndicatorsMetadata(): any[] { + return this.selectedRows; + } + + // Getter to check if we have topic hierarchy data + get hasTopicData(): boolean { + return this.kommonitorDataExchangeService.topicIndicatorHierarchy_forOrderView && + this.kommonitorDataExchangeService.topicIndicatorHierarchy_forOrderView.length > 0; + } + + // Drag & Drop methods + initializeCollapsedTopics(): void { + // Clear existing collapsed topics + this.collapsedTopics.clear(); + + // Initialize all topics as collapsed by default + if (this.kommonitorDataExchangeService.topicIndicatorHierarchy_forOrderView && + this.kommonitorDataExchangeService.topicIndicatorHierarchy_forOrderView.length > 0) { + + this.kommonitorDataExchangeService.topicIndicatorHierarchy_forOrderView.forEach((mainTopic: any) => { + this.collapsedTopics.add(mainTopic.topicId); + + if (mainTopic.subTopics && mainTopic.subTopics.length > 0) { + mainTopic.subTopics.forEach((subTopic: any) => { + this.collapsedTopics.add(subTopic.topicId); + + if (subTopic.subTopics && subTopic.subTopics.length > 0) { + subTopic.subTopics.forEach((subsubTopic: any) => { + this.collapsedTopics.add(subsubTopic.topicId); + + if (subsubTopic.subTopics && subsubTopic.subTopics.length > 0) { + subsubTopic.subTopics.forEach((subsubsubTopic: any) => { + this.collapsedTopics.add(subsubsubTopic.topicId); + }); + } + }); + } + }); + } + }); + + } else { + // No topic hierarchy data available yet + } + } + + toggleTopicCollapse(topicId: string): void { + if (this.collapsedTopics.has(topicId)) { + this.collapsedTopics.delete(topicId); + } else { + this.collapsedTopics.add(topicId); + } + } + + isTopicCollapsed(topicId: string): boolean { + // If topic hierarchy is not loaded yet, assume collapsed + if (!this.kommonitorDataExchangeService.topicIndicatorHierarchy_forOrderView || + this.kommonitorDataExchangeService.topicIndicatorHierarchy_forOrderView.length === 0) { + return true; + } + + // If the collapsedTopics set is empty, initialize it and return true (collapsed by default) + if (this.collapsedTopics.size === 0) { + this.initializeCollapsedTopics(); + return true; + } + + return this.collapsedTopics.has(topicId); + } + + onDragEnd(event: any, indicators: any[]): void { + const { previousIndex, currentIndex } = event; + + if (previousIndex === currentIndex) { + return; + } + + // Reorder the indicators array + const movedItem = indicators.splice(previousIndex, 1)[0]; + indicators.splice(currentIndex, 0, movedItem); + + // Update display order for all indicators in this group + const patchBody: Array<{indicatorId: string, displayOrder: number}> = []; + for (let index = 0; index < indicators.length; index++) { + const indicatorMetadata = indicators[index]; + patchBody.push({ + "indicatorId": indicatorMetadata.indicatorId, + "displayOrder": index + }); + } + + // Send API request to persist new sort order + this.http.patch( + this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI + "/indicators/display-order", + patchBody + ).subscribe({ + next: (response: any) => { + // Display order updated successfully + }, + error: (error: any) => { + this.kommonitorDataExchangeService.displayMapApplicationError(error); + } + }); + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorAddModal/indicator-add-modal.component.css b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorAddModal/indicator-add-modal.component.css new file mode 100644 index 000000000..667731f8c --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorAddModal/indicator-add-modal.component.css @@ -0,0 +1,894 @@ +/* Modal Styles */ +.modal-xl { + max-width: 90%; +} + +.modal-body { + max-height: 80vh; + overflow-y: auto; +} + +/* Loading Overlay */ +.loading-overlay-admin-panel { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.8); + z-index: 1000; + display: flex; + justify-content: center; + align-items: center; +} + +.icon-spin { + animation: spin 1s infinite linear; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* Multi-step Form Styles */ +.multiStepForm { + position: relative; + margin-top: 30px; +} + +#progressbar { + margin-bottom: 30px; + overflow: hidden; + color: lightgrey; + padding-left: 0; +} + +#progressbar li { + list-style-type: none; + font-size: 15px; + width: 14.28%; + float: left; + position: relative; + font-weight: 400; +} + +#progressbar li:before { + width: 50px; + height: 50px; + line-height: 45px; + display: block; + font-size: 20px; + color: #ffffff; + background: lightgray; + border-radius: 50%; + margin: 0 auto 10px auto; + padding: 2px; +} + +#progressbar li.active:before, +#progressbar li.active:after { + background: #27AE60; +} + +#progressbar li:after { + content: ''; + width: 100%; + height: 2px; + background: lightgray; + position: absolute; + left: -50%; + top: 25px; + z-index: -1; +} + +#progressbar li:first-child:after { + content: none; +} + +/* Fieldset Styles */ +fieldset { + border: 0 none; + border-radius: 0.5rem; + box-sizing: border-box; + margin: 0; + padding-bottom: 20px; + position: relative; +} + +.fs-title { + font-size: 25px; + color: #2C3E50; + margin-bottom: 10px; + font-weight: bold; + text-align: left; +} + +.fs-subtitle { + font-weight: normal; + font-size: 15px; + color: #666; + margin-bottom: 20px; +} + +/* Form Styles */ +.form-group { + margin-bottom: 15px; +} + +.form-control { + border-radius: 0; + border: 1px solid #ddd; +} + +.form-control:focus { + border-color: #27AE60; + box-shadow: 0 0 0 0.2rem rgba(39, 174, 96, 0.25); +} + +.help-block { + color: #737373; + font-size: 12px; + margin-top: 5px; +} + +/* Switch Styles */ +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.switchslider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: .4s; + border-radius: 34px; +} + +.switchslider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + transition: .4s; + border-radius: 50%; +} + +input:checked + .switchslider { + background-color: #27AE60; +} + +input:checked + .switchslider:before { + transform: translateX(26px); +} + +/* Tab Styles */ +.nav-tabs { + border-bottom: 2px solid #ddd; +} + +.nav-tabs > li > a { + border: none; + color: #666; + border-radius: 0; +} + +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:focus, +.nav-tabs > li.active > a:hover { + border: none; + color: #27AE60; + background-color: transparent; + border-bottom: 2px solid #27AE60; +} + +.nav-tabs > li.tab-completed > a { + color: #27AE60; +} + +.nav-tabs > li.tab-error > a { + color: #e74c3c; +} + +.tab-content { + padding: 20px 0; +} + +.tab-pane { + display: none; +} + +.tab-pane.active { + display: block; +} + +/* Table Styles */ +.table { + margin-top: 15px; +} + +.table th { + background-color: #f8f9fa; + border-top: none; +} + +/* Button Styles */ +.btn-group { + margin-top: 20px; +} + +.btn { + border-radius: 0; + margin-right: 5px; +} + +.btn-primary { + background-color: #27AE60; + border-color: #27AE60; +} + +.btn-primary:hover { + background-color: #229954; + border-color: #229954; +} + +.btn-secondary { + background-color: #95a5a6; + border-color: #95a5a6; +} + +.btn-secondary:hover { + background-color: #7f8c8d; + border-color: #7f8c8d; +} + +.btn-success { + background-color: #27AE60; + border-color: #27AE60; +} + +.btn-success:hover { + background-color: #229954; + border-color: #229954; +} + +.btn-warning { + background-color: #f39c12; + border-color: #f39c12; +} + +.btn-warning:hover { + background-color: #e67e22; + border-color: #e67e22; +} + +.btn-danger { + background-color: #e74c3c; + border-color: #e74c3c; +} + +.btn-danger:hover { + background-color: #c0392b; + border-color: #c0392b; +} + +.btn-info { + background-color: #3498db; + border-color: #3498db; +} + +.btn-info:hover { + background-color: #2980b9; + border-color: #2980b9; +} + +/* Alert Styles */ +.alert { + border-radius: 0; + margin-top: 15px; +} + +.alert-success { + background-color: #d4edda; + border-color: #c3e6cb; + color: #155724; +} + +.alert-danger { + background-color: #f8d7da; + border-color: #f5c6cb; + color: #721c24; +} + +.alert-warning { + background-color: #fff3cd; + border-color: #ffeaa7; + color: #856404; +} + +.alert-info { + background-color: #d1ecf1; + border-color: #bee5eb; + color: #0c5460; +} + +/* Utility Classes */ +.vertical-align { + display: flex; + align-items: center; +} + +.margin-right { + margin-right: 10px; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .modal-xl { + max-width: 95%; + } + + .col-xs-12 { + margin-bottom: 15px; + } + + #progressbar li { + font-size: 12px; + } + + #progressbar li:before { + width: 40px; + height: 40px; + line-height: 35px; + font-size: 16px; + } +} + +/* Color Brewer Preview */ +.color-preview { + width: 20px; + height: 20px; + display: inline-block; + margin-right: 5px; + border: 1px solid #ddd; +} + +/* Classification Breaks */ +.classification-break { + border: 1px solid #ddd; + padding: 5px; + margin: 2px; + border-radius: 3px; +} + +.classification-break.invalid { + border-color: #e74c3c; + background-color: #fdf2f2; +} + +/* Reference Lists */ +.reference-item { + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 3px; + padding: 10px; + margin-bottom: 10px; +} + +.reference-item .reference-name { + font-weight: bold; + color: #495057; +} + +.reference-item .reference-description { + color: #6c757d; + font-style: italic; +} + +/* Topic Hierarchy */ +.topic-hierarchy { + border-left: 3px solid #27AE60; + padding-left: 15px; + margin-left: 10px; +} + +.topic-level { + margin-bottom: 10px; +} + +.topic-level-main { + font-weight: bold; + color: #2C3E50; +} + +.topic-level-sub { + color: #34495e; + margin-left: 20px; +} + +.topic-level-subsub { + color: #7f8c8d; + margin-left: 40px; +} + +.topic-level-subsubsub { + color: #95a5a6; + margin-left: 60px; +} + +/* Step 5: Classification Options Styles */ +.color-palette-container { + margin-top: 10px; +} + +.color-palette-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 10px; + max-height: 200px; + overflow-y: auto; +} + +.color-palette-item { + border: 2px solid #ddd; + border-radius: 4px; + padding: 8px; + cursor: pointer; + transition: all 0.2s ease; + text-align: center; +} + +.color-palette-item:hover { + border-color: #27AE60; + transform: translateY(-2px); +} + +.color-palette-item.selected { + border-color: #27AE60; + background-color: #f8fff9; +} + +.color-strip { + height: 20px; + border-radius: 2px; + margin-bottom: 5px; +} + +.palette-name { + font-size: 11px; + color: #666; + font-weight: 500; +} + +.classification-tab-content { + padding: 20px; + background-color: #f8f9fa; + border-radius: 4px; +} + +.class-breaks-container { + display: flex; + flex-direction: column; + gap: 10px; +} + +.class-break-input { + display: flex; + align-items: center; + gap: 10px; +} + +.class-break-input label { + min-width: 100px; + font-weight: 500; + margin: 0; +} + +.class-break-input input { + flex: 1; +} + +.classification-preview { + border: 1px solid #ddd; + border-radius: 4px; + padding: 15px; + background-color: white; + max-height: 300px; + overflow-y: auto; +} + +.class-preview-item { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; + padding: 5px; + border-radius: 3px; +} + +.class-color { + width: 20px; + height: 20px; + border-radius: 3px; + border: 1px solid #ddd; +} + +.class-range { + font-size: 12px; + font-weight: 500; + color: #333; +} + +.nav-tabs-custom { + border: 1px solid #ddd; + border-radius: 4px; + overflow: hidden; +} + +.nav-tabs-custom .nav-tabs { + margin-bottom: 0; + background-color: #f8f9fa; +} + +.nav-tabs-custom .tab-content { + padding: 0; +} + +.nav-tabs-custom .tab-pane { + padding: 0; +} + +/* Dynamic color assignment styles */ +.dynamic-color-section { + background-color: #f8f9fa; + border: 1px solid #ddd; + border-radius: 4px; + padding: 15px; + margin-top: 15px; +} + +/* Responsive adjustments for classification */ +@media (max-width: 768px) { + .color-palette-grid { + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + } + + .class-break-input { + flex-direction: column; + align-items: flex-start; + } + + .class-break-input label { + min-width: auto; + } +} + +/* Step 6: Regional Comparison Values Styles */ +.threshold-config { + display: flex; + flex-direction: column; + gap: 10px; +} + +.threshold-item { + display: flex; + align-items: center; + gap: 10px; +} + +.threshold-item label { + min-width: 120px; + font-weight: 500; + margin: 0; +} + +.threshold-item input { + flex: 1; +} + +.benchmarking-preview { + border: 1px solid #ddd; + border-radius: 4px; + padding: 15px; + background-color: white; +} + +.preview-item { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; + padding: 5px; + border-radius: 3px; +} + +.preview-item:last-child { + margin-bottom: 0; +} + +.preview-color { + width: 20px; + height: 20px; + border-radius: 3px; + border: 1px solid #ddd; +} + +.preview-color.green { + background-color: #27AE60; +} + +.preview-color.yellow { + background-color: #f39c12; +} + +.preview-color.red { + background-color: #e74c3c; +} + +.preview-text { + font-size: 12px; + font-weight: 500; + color: #333; +} + +.comparison-values-table { + margin-top: 15px; +} + +.comparison-values-table .table th { + background-color: #f8f9fa; + font-weight: 600; +} + +.comparison-values-table .table td { + vertical-align: middle; +} + +/* Responsive adjustments for comparison values */ +@media (max-width: 768px) { + .threshold-item { + flex-direction: column; + align-items: flex-start; + } + + .threshold-item label { + min-width: auto; + } + + .preview-item { + flex-direction: column; + align-items: flex-start; + gap: 5px; + } +} + +/* Clickable Progress Bar Styles */ +#progressbar { + margin-bottom: 30px; + overflow: hidden; + color: lightgrey; + padding-left: 0px; +} + +#progressbar li { + list-style-type: none; + font-size: 12px; + width: 14.28%; + float: left; + position: relative; + font-weight: 400; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; +} + +#progressbar li:hover { + background-color: #f8f9fa; + transform: translateY(-2px); + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +#progressbar li.active { + color: #007bff; +} + +#progressbar li.current { + color: #28a745; + font-weight: bold; +} + +#progressbar li:before { + width: 50px; + height: 50px; + line-height: 45px; + display: block; + font-size: 18px; + color: #ffffff; + background: lightgray; + border-radius: 50%; + margin: 0 auto 10px auto; + padding: 2px; + transition: all 0.3s ease; +} + +#progressbar li.active:before, +#progressbar li.current:before { + background: #007bff; +} + +#progressbar li.current:before { + background: #28a745; +} + +#progressbar li:after { + content: ''; + width: 100%; + height: 2px; + background: lightgray; + position: absolute; + left: -50%; + top: 25px; + z-index: -1; + transition: all 0.3s ease; +} + +#progressbar li.active:after, +#progressbar li.current:after { + background: #007bff; +} + +#progressbar li:first-child:after { + content: none; +} + +.step-number { + display: block; + font-weight: bold; + font-size: 14px; +} + +.step-title { + display: block; + font-size: 10px; + line-height: 1.2; + margin-top: 5px; +} + +/* Step 7: Access Control and Ownership Styles */ +.role-selection-container { + border: 1px solid #ddd; + border-radius: 4px; + max-height: 300px; + overflow: hidden; +} + +.role-filter { + padding: 10px; + border-bottom: 1px solid #ddd; + background-color: #f8f9fa; +} + +.role-list { + max-height: 250px; + overflow-y: auto; +} + +.role-item { + display: flex; + align-items: center; + padding: 10px; + border-bottom: 1px solid #eee; + cursor: pointer; + transition: background-color 0.2s; +} + +.role-item:hover { + background-color: #f8f9fa; +} + +.role-item.selected { + background-color: #e3f2fd; + border-left: 3px solid #2196f3; +} + +.role-checkbox { + margin-right: 10px; +} + +.role-info { + flex: 1; +} + +.role-name { + font-weight: 500; + color: #333; + margin-bottom: 2px; +} + +.role-description { + font-size: 12px; + color: #666; +} + +.selected-roles-container { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 10px; + border: 1px solid #ddd; + border-radius: 4px; + background-color: #f8f9fa; +} + +.selected-role-item { + display: flex; + align-items: center; + gap: 5px; +} + +.role-badge { + background-color: #2196f3; + color: white; + padding: 4px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.access-summary { + background-color: #f8f9fa; + border: 1px solid #ddd; + border-radius: 4px; + padding: 15px; +} + +.summary-item { + margin-bottom: 8px; + padding: 5px 0; + border-bottom: 1px solid #eee; +} + +.summary-item:last-child { + border-bottom: none; + margin-bottom: 0; +} + +.summary-item strong { + color: #333; + margin-right: 8px; +} + +@media (max-width: 768px) { + .role-item { + flex-direction: column; + align-items: flex-start; + } + + .role-checkbox { + margin-bottom: 5px; + } + + .selected-roles-container { + flex-direction: column; + } + + .selected-role-item { + justify-content: space-between; + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorAddModal/indicator-add-modal.component.html b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorAddModal/indicator-add-modal.component.html new file mode 100644 index 000000000..a792b20d6 --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorAddModal/indicator-add-modal.component.html @@ -0,0 +1,1525 @@ + + + + + + + +
+ +

Indikator-Metadaten registriert

+

Eine neuer Indikator mit Namen {{successMessagePart}} wurde in KomMonitor registriert und in die Übersichtstabelle eingetragen.

+
+ + +
+ +

Registrierung gescheitert

+ Bei der Registrierung des Indikators ist ein Fehler aufgetreten. Fehlermeldung: +
+

+  
+
+
+ + +
+ +

Metadata Import gescheitert

+ Beim Import der Metadaten aus einer Datei ist ein Fehler aufgetreten. Fehlermeldung: +
+
{{indicatorMetadataImportError}}
+
+
+

Bitte stellen Sie sicher, dass folgendes JSON-Format eingehalten wird:

+
{{indicatorMetadataStructure_pretty}}
+
\ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorAddModal/indicator-add-modal.component.ts b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorAddModal/indicator-add-modal.component.ts new file mode 100644 index 000000000..4ea60fcc7 --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorAddModal/indicator-add-modal.component.ts @@ -0,0 +1,1529 @@ +import { Component, OnInit, Inject, ViewChild, ElementRef } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; +import { HttpClient } from '@angular/common/http'; + +@Component({ + selector: 'indicator-add-modal-new', + templateUrl: './indicator-add-modal.component.html', + styleUrls: ['./indicator-add-modal.component.css'] +}) +export class IndicatorAddModalComponent implements OnInit { + @ViewChild('metadataImportFile', { static: false }) metadataImportFile!: ElementRef; + @ViewChild('mappingConfigImportFile', { static: false }) mappingConfigImportFile!: ElementRef; + + // Multi-step form + currentStep = 1; + totalSteps = 7; // Will be adjusted based on security settings + + // Form data + isSubmitting = false; + errorMessage = ''; + successMessage = ''; + loadingData = false; + + // Basic form data + datasetName = ''; + datasetNameInvalid = false; + indicatorAbbreviation = ''; + indicatorType: any = null; + isHeadlineIndicator = false; + indicatorUnit = ''; + enableFreeTextUnit = false; + indicatorProcessDescription = ''; + indicatorTagsString_withCommas = ''; + indicatorInterpretation = ''; + indicatorCreationType: any = null; + indicatorLowestSpatialUnitMetadataObjectForComputation: any = null; + enableLowestSpatialUnitSelect = false; + indicatorPrecision: any = null; + showCustomCommaValue = false; + + // Metadata + metadata: any = { + description: '', + databasis: '', + datasource: '', + contact: '', + updateInterval: null, + lastUpdate: '', + literature: '', + note: '', + sridEPSG: 4326 + }; + + // References + indicatorReferences_adminView: any[] = []; + indicatorReferences_apiRequest: any[] = []; + georesourceReferences_adminView: any[] = []; + georesourceReferences_apiRequest: any[] = []; + + // Topic hierarchy + indicatorTopic_mainTopic: any = null; + indicatorTopic_subTopic: any = null; + indicatorTopic_subsubTopic: any = null; + indicatorTopic_subsubsubTopic: any = null; + + // Step 3: Topic Hierarchy + selectedTopic: any = null; + selectedSubTopic: any = null; + availableSubTopics: any[] = []; + additionalTopic: any = null; + additionalSubTopic: any = null; + additionalSubTopics: any[] = []; + additionalTopicAssignments: Array<{topic: any, subTopic: any}> = []; + + // Classification + numClassesArray = [3, 4, 5, 6, 7, 8]; + numClassesPerSpatialUnit = 5; + classificationMethod = 'jenks'; + selectedColorBrewerPaletteEntry: any = null; + spatialUnitClassification: any[] = []; + classBreaksInvalid = false; + tabClasses: string[] = []; + + // Role management + roleManagementTableOptions: any = null; + ownerOrganization: any = null; + ownerOrgFilter = ''; + isPublic = false; + resourcesCreatorRights: any[] = []; + + // Import/Export functionality + metadataImportSettings: any = null; + mappingConfigImportSettings: any = null; + indicatorMetadataImportError = ''; + indicatorMappingConfigImportError = ''; + + // Success/Error data + successMessagePart = ''; + errorMessagePart = ''; + importerErrors: any[] = []; + importedFeatures: any[] = []; + + // Available options + availableSpatialUnits: any[] = []; + updateIntervalOptions: any[] = []; + indicatorTypeOptions: any[] = []; + colorbrewerPalettes: any[] = []; + colorbrewerSchemes: any = {}; + availableIndicators: any[] = []; + availableGeoresources: any[] = []; + availableTopics: any[] = []; + accessControl: any[] = []; + colorbreweSchemeName_dynamicIncrease = 'Blues'; + colorbreweSchemeName_dynamicDecrease = 'Reds'; + + // Step 5: Classification Options + enableDynamicColorAssignment = false; + currentClassificationTab = 0; + + // Step 6: Regional Comparison Values + comparisonValueType: string | null = null; + comparisonValue: number | null = null; + comparisonRegion: string | null = null; + comparisonTimeframe: string | null = null; + comparisonDescription = ''; + evaluationDirection: string | null = null; + toleranceRange: number | null = null; + + // Additional comparison values + additionalComparisonType: string | null = null; + additionalComparisonValue: number | null = null; + additionalComparisonDescription = ''; + additionalComparisonValues: Array<{type: string, value: number, description: string}> = []; + + // Benchmarking configuration + enableBenchmarking = false; + benchmarkingVisualizationType: string | null = null; + greenThreshold: number | null = null; + yellowThreshold: number | null = null; + redThreshold: number | null = null; + + // Step 7: Access Control and Ownership + filteredOrganizations: any[] = []; + roleFilter = ''; + filteredRoles: any[] = []; + selectedRoles: any[] = []; + + + + // Advanced access control + enableTimeRestrictedAccess = false; + enableGeographicRestriction = false; + accessStartDate = ''; + accessEndDate = ''; + allowedRegions: any[] = []; + availableRegions: any[] = []; + enableAccessLogging = false; + + // Temporary variables for references + indicatorNameFilter = ''; + tmpIndicatorReference_selectedIndicatorMetadata: any = null; + tmpIndicatorReference_referenceDescription = ''; + georesourceNameFilter = ''; + tmpGeoresourceReference_selectedGeoresourceMetadata: any = null; + tmpGeoresourceReference_referenceDescription = ''; + + // Step 4: Filtered lists for references + filteredIndicators: any[] = []; + filteredGeoresources: any[] = []; + + // Post body + postBody_indicators: any = null; + + // Reference date + indicatorReferenceDateNote = ''; + displayOrder = 0; + + constructor( + public activeModal: NgbActiveModal, + @Inject('kommonitorDataExchangeService') public kommonitorDataExchangeService: any, + @Inject('kommonitorImporterHelperService') public kommonitorImporterHelperService: any, + @Inject('kommonitorDataGridHelperService') private kommonitorDataGridHelperService: any, + @Inject('kommonitorMultiStepFormHelperService') private kommonitorMultiStepFormHelperService: any, + private http: HttpClient, + private broadcastService: BroadcastService, + @Inject('kommonitorConfigStorageService') private kommonitorConfigStorageService: any + ) { + console.log('IndicatorAddModalComponent constructor initialized - Modal is being created'); + } + + ngOnInit() { + console.log('IndicatorAddModalComponent ngOnInit - Modal is being initialized'); + this.loadInitialData(); + this.initializeMultiStepForm(); + console.log('IndicatorAddModalComponent ngOnInit - Modal initialization complete'); + console.log('Current step:', this.currentStep); + console.log('Total steps:', this.totalSteps); + } + + private loadInitialData() { + this.loadingData = true; + + // Load available spatial units + if (this.kommonitorDataExchangeService && this.kommonitorDataExchangeService.availableSpatialUnits) { + this.availableSpatialUnits = this.kommonitorDataExchangeService.availableSpatialUnits; + this.indicatorLowestSpatialUnitMetadataObjectForComputation = this.availableSpatialUnits.length > 0 ? this.availableSpatialUnits[0] : null; + } + + // Load update interval options + if (this.kommonitorDataExchangeService && this.kommonitorDataExchangeService.updateIntervalOptions) { + this.updateIntervalOptions = this.kommonitorDataExchangeService.updateIntervalOptions; + } + + // Load indicator type options + if (this.kommonitorDataExchangeService && this.kommonitorDataExchangeService.indicatorTypeOptions) { + this.indicatorTypeOptions = this.kommonitorDataExchangeService.indicatorTypeOptions; + this.indicatorType = this.indicatorTypeOptions.length > 0 ? this.indicatorTypeOptions[0] : null; + } + + // Load available indicators + if (this.kommonitorDataExchangeService && this.kommonitorDataExchangeService.availableIndicators) { + this.availableIndicators = this.kommonitorDataExchangeService.availableIndicators; + } + + // Load available georesources + if (this.kommonitorDataExchangeService && this.kommonitorDataExchangeService.availableGeoresources) { + this.availableGeoresources = this.kommonitorDataExchangeService.availableGeoresources; + } + + // Load available topics + if (this.kommonitorDataExchangeService && this.kommonitorDataExchangeService.availableTopics) { + this.availableTopics = this.kommonitorDataExchangeService.availableTopics; + } + + // Load access control + if (this.kommonitorDataExchangeService && this.kommonitorDataExchangeService.accessControl) { + this.accessControl = this.kommonitorDataExchangeService.accessControl; + } + + // Load color brewer schemes + this.loadColorBrewerSchemes(); + + // Initialize filtered lists for Step 4 + this.filteredIndicators = this.availableIndicators || []; + this.filteredGeoresources = this.availableGeoresources || []; + + // Initialize data for Step 7 + this.filteredOrganizations = this.accessControl || []; + this.filteredRoles = this.accessControl || []; + this.availableRegions = this.availableSpatialUnits || []; + + this.loadingData = false; + } + + private initializeMultiStepForm() { + // Initialize multi-step form based on security settings + if (this.kommonitorDataExchangeService && this.kommonitorDataExchangeService.enableKeycloakSecurity) { + this.totalSteps = 7; // Include role management step + } else { + this.totalSteps = 6; + } + + // Initialize role management if available + if (this.kommonitorDataExchangeService && this.kommonitorDataExchangeService.accessControl && this.kommonitorDataGridHelperService) { + this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid( + 'indicatorAddRoleManagementTable', + this.roleManagementTableOptions, + this.kommonitorDataExchangeService.accessControl, + [] + ); + } + + // Initialize classification + this.onNumClassesChanged(this.numClassesPerSpatialUnit); + } + + private loadColorBrewerSchemes() { + // Load color brewer schemes from environment or default + const customColorSchemes = (window as any).__env?.customColorSchemes; + this.colorbrewerSchemes = (window as any).colorbrewer || {}; + + if (customColorSchemes) { + this.colorbrewerSchemes = Object.assign(customColorSchemes, this.colorbrewerSchemes); + } + + this.instantiateColorBrewerPalettes(); + } + + private instantiateColorBrewerPalettes() { + this.colorbrewerPalettes = []; + + for (const key in this.colorbrewerSchemes) { + if (this.colorbrewerSchemes.hasOwnProperty(key)) { + const colorPalettes = this.colorbrewerSchemes[key]; + + const paletteEntry = { + "paletteName": key, + "paletteArrayObject": colorPalettes + }; + + this.colorbrewerPalettes.push(paletteEntry); + } + } + + // Instantiate with palette 'Blues' + this.selectedColorBrewerPaletteEntry = this.colorbrewerPalettes[13] || this.colorbrewerPalettes[0]; + } + + checkDatasetName() { + this.datasetNameInvalid = false; + + if (this.datasetName && this.indicatorType && this.kommonitorDataExchangeService && this.kommonitorDataExchangeService.availableIndicators) { + this.kommonitorDataExchangeService.availableIndicators.forEach((indicator: any) => { + if (indicator.datasetName === this.datasetName && + indicator.indicatorType === this.indicatorType.apiName) { + this.datasetNameInvalid = true; + return; + } + }); + } + } + + + + // Reference management methods + onAddOrUpdateIndicatorReference() { + if (this.tmpIndicatorReference_selectedIndicatorMetadata && this.tmpIndicatorReference_referenceDescription) { + const tmpReference = { + "indicatorMetadata": this.tmpIndicatorReference_selectedIndicatorMetadata, + "referenceDescription": this.tmpIndicatorReference_referenceDescription + }; + + let processed = false; + for (let index = 0; index < this.indicatorReferences_adminView.length; index++) { + const indicatorReference = this.indicatorReferences_adminView[index]; + if (indicatorReference.indicatorMetadata.indicatorId === tmpReference.indicatorMetadata.indicatorId) { + // replace object + this.indicatorReferences_adminView[index] = tmpReference; + processed = true; + break; + } + } + + if (!processed) { + // new entry + this.indicatorReferences_adminView.push(tmpReference); + } + + this.tmpIndicatorReference_selectedIndicatorMetadata = null; + this.tmpIndicatorReference_referenceDescription = ''; + } + } + + onClickEditIndicatorReference(indicatorReference: any) { + this.tmpIndicatorReference_selectedIndicatorMetadata = indicatorReference.indicatorMetadata; + this.tmpIndicatorReference_referenceDescription = indicatorReference.referenceDescription; + } + + onClickDeleteIndicatorReference(indicatorReference: any) { + for (let index = 0; index < this.indicatorReferences_adminView.length; index++) { + if (this.indicatorReferences_adminView[index].indicatorMetadata.indicatorId === indicatorReference.indicatorMetadata.indicatorId) { + // remove object + this.indicatorReferences_adminView.splice(index, 1); + break; + } + } + } + + onAddOrUpdateGeoresourceReference() { + if (this.tmpGeoresourceReference_selectedGeoresourceMetadata && this.tmpGeoresourceReference_referenceDescription) { + const tmpReference = { + "georesourceMetadata": this.tmpGeoresourceReference_selectedGeoresourceMetadata, + "referenceDescription": this.tmpGeoresourceReference_referenceDescription + }; + + let processed = false; + for (let index = 0; index < this.georesourceReferences_adminView.length; index++) { + const georesourceReference = this.georesourceReferences_adminView[index]; + if (georesourceReference.georesourceMetadata.georesourceId === tmpReference.georesourceMetadata.georesourceId) { + // replace object + this.georesourceReferences_adminView[index] = tmpReference; + processed = true; + break; + } + } + + if (!processed) { + // new entry + this.georesourceReferences_adminView.push(tmpReference); + } + + this.tmpGeoresourceReference_selectedGeoresourceMetadata = null; + this.tmpGeoresourceReference_referenceDescription = ''; + } + } + + onClickEditGeoresourceReference(georesourceReference: any) { + this.tmpGeoresourceReference_selectedGeoresourceMetadata = georesourceReference.georesourceMetadata; + this.tmpGeoresourceReference_referenceDescription = georesourceReference.referenceDescription; + } + + onClickDeleteGeoresourceReference(georesourceReference: any) { + for (let index = 0; index < this.georesourceReferences_adminView.length; index++) { + if (this.georesourceReferences_adminView[index].georesourceMetadata.georesourceId === georesourceReference.georesourceMetadata.georesourceId) { + // remove object + this.georesourceReferences_adminView.splice(index, 1); + break; + } + } + } + + // Build post body for API request + buildPostBody_indicators() { + // Convert references to API format + this.convertReferencesToApiFormat(); + + const postBody: any = { + "datasetName": this.datasetName, + "abbreviation": this.indicatorAbbreviation, + "indicatorType": this.indicatorType?.apiName, + "isHeadlineIndicator": this.isHeadlineIndicator, + "unit": this.indicatorUnit, + "processDescription": this.indicatorProcessDescription, + "interpretation": this.indicatorInterpretation, + "creationType": this.indicatorCreationType?.apiName, + "lowestSpatialUnitForComputation": this.indicatorLowestSpatialUnitMetadataObjectForComputation?.spatialUnitLevel, + "referenceDateNote": this.indicatorReferenceDateNote, + "displayOrder": this.displayOrder, + "metadata": { + "note": this.metadata.note, + "literature": this.metadata.literature, + "updateInterval": this.metadata.updateInterval?.apiName, + "sridEPSG": this.metadata.sridEPSG, + "datasource": this.metadata.datasource, + "contact": this.metadata.contact, + "lastUpdate": this.metadata.lastUpdate, + "description": this.metadata.description, + "databasis": this.metadata.databasis + }, + "allowedRoles": [] as string[], + "refrencesToOtherIndicators": this.indicatorReferences_apiRequest, + "refrencesToGeoresources": this.georesourceReferences_apiRequest, + "defaultClassificationMapping": { + "colorBrewerSchemeName": this.selectedColorBrewerPaletteEntry?.paletteName, + "numClasses": this.numClassesPerSpatialUnit, + "classificationMethod": this.classificationMethod, + "items": this.spatialUnitClassification.map(classification => ({ + "spatialUnit": classification.spatialUnitId, + "breaks": classification.breaks.filter(breakVal => breakVal !== null) + })) + } + }; + + // Add topic reference if selected + if (this.indicatorTopic_subsubsubTopic) { + postBody.topicReference = this.indicatorTopic_subsubsubTopic.topicId; + } else if (this.indicatorTopic_subsubTopic) { + postBody.topicReference = this.indicatorTopic_subsubTopic.topicId; + } else if (this.indicatorTopic_subTopic) { + postBody.topicReference = this.indicatorTopic_subTopic.topicId; + } else if (this.indicatorTopic_mainTopic) { + postBody.topicReference = this.indicatorTopic_mainTopic.topicId; + } + + // Add tags if provided + if (this.indicatorTagsString_withCommas) { + postBody.tags = this.indicatorTagsString_withCommas.split(',').map((tag: string) => tag.trim()); + } + + // Add precision if custom value is enabled + if (this.showCustomCommaValue && this.indicatorPrecision !== null) { + postBody.precision = this.indicatorPrecision; + } + + // Add role permissions + if (this.roleManagementTableOptions && this.kommonitorDataGridHelperService) { + const roleIds = this.kommonitorDataGridHelperService.getSelectedRoleIds_roleManagementGrid(this.roleManagementTableOptions); + if (roleIds && Array.isArray(roleIds)) { + for (const roleId of roleIds) { + postBody.allowedRoles.push(roleId); + } + } + } + + return postBody; + } + + async addIndicator() { + this.loadingData = true; + this.importerErrors = []; + this.successMessagePart = ''; + this.errorMessagePart = ''; + + try { + this.postBody_indicators = this.buildPostBody_indicators(); + + // Check if service is available + if (!this.kommonitorDataExchangeService || !this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI) { + throw new Error('Data exchange service not available'); + } + + const response = await this.http.post( + this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI + "/indicators", + this.postBody_indicators + ).toPromise(); + + this.broadcastService.broadcast("refreshIndicatorOverviewTable", ["add", (response as any).indicatorId]); + + // Refresh all admin dashboard diagrams due to modified metadata + setTimeout(() => { + this.broadcastService.broadcast("refreshAdminDashboardDiagrams"); + }, 500); + + this.successMessagePart = this.postBody_indicators.datasetName; + this.loadingData = false; + + // Close modal with success result + setTimeout(() => { + this.activeModal.close('success'); + }, 2000); + + } catch (error: any) { + if (this.kommonitorDataExchangeService && this.kommonitorDataExchangeService.syntaxHighlightJSON) { + if (error.data) { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error.data); + } else { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error); + } + } else { + this.errorMessagePart = error.message || 'An error occurred'; + } + + this.loadingData = false; + } + } + + onSubmit() { + if (!this.datasetNameInvalid && !this.classBreaksInvalid) { + this.addIndicator(); + } + } + + // Multi-step navigation + nextStep() { + const maxSteps = this.kommonitorDataExchangeService && this.kommonitorDataExchangeService.enableKeycloakSecurity ? 7 : 6; + if (this.currentStep < maxSteps) { + this.currentStep++; + } + } + + previousStep() { + if (this.currentStep > 1) { + this.currentStep--; + } + } + + goToStep(step: number) { + const maxSteps = this.kommonitorDataExchangeService && this.kommonitorDataExchangeService.enableKeycloakSecurity ? 7 : 6; + + // Allow navigation to any step without validation (like old AngularJS counterpart) + if (step >= 1 && step <= maxSteps) { + console.log(`Navigating to step: ${step}`); + this.currentStep = step; + } + } + + // Import/Export functionality + onImportIndicatorAddMetadata() { + this.indicatorMetadataImportError = ''; + if (this.metadataImportFile) { + this.metadataImportFile.nativeElement.click(); + } + } + + onImportIndicatorAddMappingConfig() { + this.indicatorMappingConfigImportError = ''; + if (this.mappingConfigImportFile) { + this.mappingConfigImportFile.nativeElement.click(); + } + } + + onMetadataFileSelected(event: any) { + const file = event.target.files[0]; + if (file) { + this.parseMetadataFromFile(file); + } + } + + onMappingConfigFileSelected(event: any) { + const file = event.target.files[0]; + if (file) { + this.parseMappingConfigFromFile(file); + } + } + + parseMetadataFromFile(file: File) { + const fileReader = new FileReader(); + + fileReader.onload = (event: any) => { + try { + this.parseFromMetadataFile(event); + } catch (error) { + console.error(error); + console.error("Uploaded Metadata File cannot be parsed."); + this.indicatorMetadataImportError = "Uploaded Metadata File cannot be parsed correctly"; + } + }; + + fileReader.readAsText(file); + } + + parseMappingConfigFromFile(file: File) { + const fileReader = new FileReader(); + + fileReader.onload = (event: any) => { + try { + this.parseFromMappingConfigFile(event); + } catch (error) { + console.error(error); + console.error("Uploaded MappingConfig File cannot be parsed."); + this.indicatorMappingConfigImportError = "Uploaded MappingConfig File cannot be parsed correctly"; + } + }; + + fileReader.readAsText(file); + } + + parseFromMetadataFile(event: any) { + this.metadataImportSettings = JSON.parse(event.target.result); + + if (!this.metadataImportSettings.metadata) { + console.error("uploaded Metadata File cannot be parsed - wrong structure."); + this.indicatorMetadataImportError = "Struktur der Datei stimmt nicht mit erwartetem Muster überein."; + return; + } + + // Parse metadata + this.metadata = {}; + this.metadata.note = this.metadataImportSettings.metadata.note; + this.metadata.literature = this.metadataImportSettings.metadata.literature; + + if (this.kommonitorDataExchangeService && this.kommonitorDataExchangeService.updateIntervalOptions) { + this.kommonitorDataExchangeService.updateIntervalOptions.forEach((option: any) => { + if (option.apiName === this.metadataImportSettings.metadata.updateInterval) { + this.metadata.updateInterval = option; + } + }); + } + + this.metadata.sridEPSG = this.metadataImportSettings.metadata.sridEPSG; + this.metadata.datasource = this.metadataImportSettings.metadata.datasource; + this.metadata.contact = this.metadataImportSettings.metadata.contact; + this.metadata.lastUpdate = this.metadataImportSettings.metadata.lastUpdate; + this.metadata.description = this.metadataImportSettings.metadata.description; + this.metadata.databasis = this.metadataImportSettings.metadata.databasis; + + // Parse basic fields + this.datasetName = this.metadataImportSettings.datasetName || ''; + this.indicatorAbbreviation = this.metadataImportSettings.abbreviation || ''; + this.indicatorUnit = this.metadataImportSettings.unit || ''; + this.indicatorProcessDescription = this.metadataImportSettings.processDescription || ''; + this.indicatorInterpretation = this.metadataImportSettings.interpretation || ''; + this.indicatorReferenceDateNote = this.metadataImportSettings.referenceDateNote || ''; + this.displayOrder = this.metadataImportSettings.displayOrder || 0; + this.isHeadlineIndicator = this.metadataImportSettings.isHeadlineIndicator || false; + + // Parse indicator type + if (this.metadataImportSettings.indicatorType && this.kommonitorDataExchangeService && this.kommonitorDataExchangeService.indicatorTypeOptions) { + this.kommonitorDataExchangeService.indicatorTypeOptions.forEach((option: any) => { + if (option.apiName === this.metadataImportSettings.indicatorType) { + this.indicatorType = option; + } + }); + } + + // Parse creation type + if (this.metadataImportSettings.creationType) { + // Add creation type options if available + // this.indicatorCreationType = ... + } + + // Parse tags + if (this.metadataImportSettings.tags && Array.isArray(this.metadataImportSettings.tags)) { + this.indicatorTagsString_withCommas = this.metadataImportSettings.tags.join(', '); + } + + // Parse references + if (this.metadataImportSettings.refrencesToOtherIndicators && this.kommonitorDataExchangeService && this.kommonitorDataExchangeService.availableIndicators) { + this.indicatorReferences_apiRequest = this.metadataImportSettings.refrencesToOtherIndicators; + // Populate admin view + this.indicatorReferences_adminView = []; + this.indicatorReferences_apiRequest.forEach((ref: any) => { + const indicator = this.kommonitorDataExchangeService.availableIndicators.find((ind: any) => ind.indicatorId === ref.indicatorId); + if (indicator) { + this.indicatorReferences_adminView.push({ + indicatorId: ref.indicatorId, + referenceDescription: ref.referenceDescription, + indicatorName: indicator.indicatorName + }); + } + }); + } + + if (this.metadataImportSettings.refrencesToGeoresources && this.kommonitorDataExchangeService && this.kommonitorDataExchangeService.availableGeoresources) { + this.georesourceReferences_apiRequest = this.metadataImportSettings.refrencesToGeoresources; + // Populate admin view + this.georesourceReferences_adminView = []; + this.georesourceReferences_apiRequest.forEach((ref: any) => { + const georesource = this.kommonitorDataExchangeService.availableGeoresources.find((geo: any) => geo.georesourceId === ref.georesourceId); + if (georesource) { + this.georesourceReferences_adminView.push({ + georesourceId: ref.georesourceId, + referenceDescription: ref.referenceDescription, + georesourceName: georesource.georesourceName + }); + } + }); + } + + // Parse classification mapping + if (this.metadataImportSettings.defaultClassificationMapping) { + const mapping = this.metadataImportSettings.defaultClassificationMapping; + this.numClassesPerSpatialUnit = mapping.numClasses || 5; + this.classificationMethod = mapping.classificationMethod || 'jenks'; + + // Set color brewer palette + if (mapping.colorBrewerSchemeName) { + this.selectedColorBrewerPaletteEntry = this.colorbrewerPalettes.find(palette => + palette.paletteName === mapping.colorBrewerSchemeName + ); + } + + // Parse spatial unit classification + if (mapping.items) { + this.onNumClassesChanged(this.numClassesPerSpatialUnit); + mapping.items.forEach((item: any) => { + const index = this.spatialUnitClassification.findIndex(classification => + classification.spatialUnitId === item.spatialUnit + ); + if (index > -1) { + this.spatialUnitClassification[index].breaks = item.breaks; + } + }); + } + } + + // Parse role permissions + if (this.kommonitorDataExchangeService && this.kommonitorDataExchangeService.accessControl && this.metadataImportSettings.allowedRoles && this.kommonitorDataGridHelperService) { + this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid( + 'indicatorAddRoleManagementTable', + this.roleManagementTableOptions, + this.kommonitorDataExchangeService.accessControl, + this.metadataImportSettings.allowedRoles + ); + } + } + + parseFromMappingConfigFile(event: any) { + this.mappingConfigImportSettings = JSON.parse(event.target.result); + + if (!this.mappingConfigImportSettings.converter || !this.mappingConfigImportSettings.dataSource || !this.mappingConfigImportSettings.propertyMapping) { + console.error("uploaded MappingConfig File cannot be parsed - wrong structure."); + this.indicatorMappingConfigImportError = "Struktur der Datei stimmt nicht mit erwartetem Muster überein."; + return; + } + + // Parse converter settings + // This would be similar to spatial unit mapping config parsing + // but adapted for indicators + } + + onExportIndicatorAddMetadataTemplate() { + const metadataJSON = JSON.stringify(this.indicatorMetadataStructure); + const fileName = "Indikator_Metadaten_Vorlage_Export.json"; + this.downloadFile(metadataJSON, fileName); + } + + onExportIndicatorAddMetadata() { + const metadataExport: any = { ...this.indicatorMetadataStructure }; + + // Populate with current form data + metadataExport.datasetName = this.datasetName || ""; + metadataExport.abbreviation = this.indicatorAbbreviation || ""; + metadataExport.unit = this.indicatorUnit || ""; + metadataExport.processDescription = this.indicatorProcessDescription || ""; + metadataExport.interpretation = this.indicatorInterpretation || ""; + metadataExport.referenceDateNote = this.indicatorReferenceDateNote || ""; + metadataExport.displayOrder = this.displayOrder || 0; + metadataExport.isHeadlineIndicator = this.isHeadlineIndicator || false; + + if (this.indicatorType) { + metadataExport.indicatorType = this.indicatorType.apiName; + } + + if (this.indicatorCreationType) { + metadataExport.creationType = this.indicatorCreationType.apiName; + } + + if (this.indicatorTagsString_withCommas) { + metadataExport.tags = this.indicatorTagsString_withCommas.split(',').map((tag: string) => tag.trim()); + } + + if (this.showCustomCommaValue && this.indicatorPrecision !== null) { + metadataExport.precision = this.indicatorPrecision; + } + + // Add metadata + metadataExport.metadata.note = this.metadata.note || ""; + metadataExport.metadata.literature = this.metadata.literature || ""; + metadataExport.metadata.sridEPSG = this.metadata.sridEPSG || ""; + metadataExport.metadata.datasource = this.metadata.datasource || ""; + metadataExport.metadata.contact = this.metadata.contact || ""; + metadataExport.metadata.lastUpdate = this.metadata.lastUpdate || ""; + metadataExport.metadata.description = this.metadata.description || ""; + metadataExport.metadata.databasis = this.metadata.databasis || ""; + + if (this.metadata.updateInterval) { + metadataExport.metadata.updateInterval = this.metadata.updateInterval.apiName; + } + + // Add references + metadataExport.refrencesToOtherIndicators = this.indicatorReferences_apiRequest; + metadataExport.refrencesToGeoresources = this.georesourceReferences_apiRequest; + + // Add classification mapping + metadataExport.defaultClassificationMapping = { + colorBrewerSchemeName: this.selectedColorBrewerPaletteEntry?.paletteName, + numClasses: this.numClassesPerSpatialUnit, + classificationMethod: this.classificationMethod, + items: this.spatialUnitClassification.map(classification => ({ + spatialUnit: classification.spatialUnitId, + breaks: classification.breaks.filter(breakVal => breakVal !== null) + })) + }; + + // Add role permissions + metadataExport.allowedRoles = []; + if (this.roleManagementTableOptions && this.kommonitorDataGridHelperService) { + const roleIds = this.kommonitorDataGridHelperService.getSelectedRoleIds_roleManagementGrid(this.roleManagementTableOptions); + if (roleIds && Array.isArray(roleIds)) { + for (const roleId of roleIds) { + metadataExport.allowedRoles.push(roleId); + } + } + } + + const name = this.datasetName; + const metadataJSON = JSON.stringify(metadataExport); + let fileName = "Indikator_Metadaten_Export"; + + if (name) { + fileName += "-" + name; + } + + fileName += ".json"; + this.downloadFile(metadataJSON, fileName); + } + + async onExportIndicatorAddMappingConfig() { + const mappingConfigExport = { + "converter": {}, // Would be populated if converter is used + "dataSource": {}, // Would be populated if data source is used + "propertyMapping": {}, // Would be populated if property mapping is used + }; + + const name = this.datasetName; + const metadataJSON = JSON.stringify(mappingConfigExport); + let fileName = "KomMonitor-Import-Mapping-Konfiguration_Export"; + + if (name) { + fileName += "-" + name; + } + + fileName += ".json"; + this.downloadFile(metadataJSON, fileName); + } + + private downloadFile(content: string, fileName: string) { + const blob = new Blob([content], { type: "application/json" }); + const data = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.download = fileName; + a.href = data; + a.textContent = "JSON"; + a.target = "_blank"; + a.rel = "noopener noreferrer"; + a.click(); + + a.remove(); + } + + // Metadata structure for export + get indicatorMetadataStructure() { + return { + "metadata": { + "note": "", + "literature": "", + "updateInterval": "", + "sridEPSG": "", + "datasource": "", + "contact": "", + "lastUpdate": "", + "description": "", + "databasis": "" + }, + "allowedRoles": [], + "datasetName": "", + "abbreviation": "", + "indicatorType": "", + "isHeadlineIndicator": false, + "unit": "", + "processDescription": "", + "interpretation": "", + "creationType": "", + "lowestSpatialUnitForComputation": "", + "referenceDateNote": "", + "displayOrder": 0, + "refrencesToOtherIndicators": [], + "refrencesToGeoresources": [], + "tags": [], + "precision": null, + "defaultClassificationMapping": { + "colorBrewerSchemeName": "", + "numClasses": 5, + "classificationMethod": "jenks", + "items": [] + } + }; + } + + get indicatorMetadataStructure_pretty() { + return JSON.stringify(this.indicatorMetadataStructure, null, 2); + } + + get indicatorMappingConfigStructure_pretty() { + if (this.kommonitorImporterHelperService && this.kommonitorImporterHelperService.mappingConfigStructure_indicator) { + return JSON.stringify(this.kommonitorImporterHelperService.mappingConfigStructure_indicator, null, 2); + } + return JSON.stringify({}, null, 2); + } + + resetForm() { + this.currentStep = 1; + this.datasetName = ''; + this.datasetNameInvalid = false; + this.indicatorAbbreviation = ''; + this.indicatorType = this.indicatorTypeOptions && this.indicatorTypeOptions.length > 0 ? this.indicatorTypeOptions[0] : null; + this.isHeadlineIndicator = false; + this.indicatorUnit = ''; + this.enableFreeTextUnit = false; + this.indicatorProcessDescription = ''; + this.indicatorTagsString_withCommas = ''; + this.indicatorInterpretation = ''; + this.indicatorCreationType = null; + this.indicatorLowestSpatialUnitMetadataObjectForComputation = this.availableSpatialUnits && this.availableSpatialUnits.length > 0 ? this.availableSpatialUnits[0] : null; + this.enableLowestSpatialUnitSelect = false; + this.indicatorPrecision = null; + this.showCustomCommaValue = false; + this.indicatorReferenceDateNote = ''; + this.displayOrder = 0; + this.indicatorTopic_mainTopic = null; + this.indicatorTopic_subTopic = null; + this.indicatorTopic_subsubTopic = null; + this.indicatorTopic_subsubsubTopic = null; + + // Reset Step 3: Topic Hierarchy + this.selectedTopic = null; + this.selectedSubTopic = null; + this.availableSubTopics = []; + this.additionalTopic = null; + this.additionalSubTopic = null; + this.additionalSubTopics = []; + this.additionalTopicAssignments = []; + this.indicatorReferences_adminView = []; + this.indicatorReferences_apiRequest = []; + this.georesourceReferences_adminView = []; + this.georesourceReferences_apiRequest = []; + this.numClassesPerSpatialUnit = 5; + this.classificationMethod = 'jenks'; + this.selectedColorBrewerPaletteEntry = this.colorbrewerPalettes && this.colorbrewerPalettes.length > 13 ? this.colorbrewerPalettes[13] : (this.colorbrewerPalettes && this.colorbrewerPalettes.length > 0 ? this.colorbrewerPalettes[0] : null); + this.spatialUnitClassification = []; + this.classBreaksInvalid = false; + this.tabClasses = []; + this.ownerOrganization = ''; + this.ownerOrgFilter = ''; + this.isPublic = false; + this.roleManagementTableOptions = null; + this.metadataImportSettings = null; + this.mappingConfigImportSettings = null; + this.indicatorMetadataImportError = ''; + this.indicatorMappingConfigImportError = ''; + this.resourcesCreatorRights = []; + this.successMessagePart = ''; + this.errorMessagePart = ''; + this.importerErrors = []; + this.importedFeatures = []; + this.postBody_indicators = null; + this.errorMessage = ''; + this.successMessage = ''; + + // Reset metadata + this.metadata = { + description: '', + databasis: '', + datasource: '', + contact: '', + updateInterval: null, + lastUpdate: '', + literature: '', + note: '', + sridEPSG: 4326 + }; + + // Reset temporary variables + this.indicatorNameFilter = ''; + this.tmpIndicatorReference_selectedIndicatorMetadata = null; + this.tmpIndicatorReference_referenceDescription = ''; + this.georesourceNameFilter = ''; + this.tmpGeoresourceReference_selectedGeoresourceMetadata = null; + this.tmpGeoresourceReference_referenceDescription = ''; + + // Reset Step 4: Filtered lists + this.filteredIndicators = this.availableIndicators || []; + this.filteredGeoresources = this.availableGeoresources || []; + + // Reset Step 5: Classification Options + this.enableDynamicColorAssignment = false; + this.currentClassificationTab = 0; + + // Reset Step 6: Regional Comparison Values + this.comparisonValueType = null; + this.comparisonValue = null; + this.comparisonRegion = null; + this.comparisonTimeframe = null; + this.comparisonDescription = ''; + this.evaluationDirection = null; + this.toleranceRange = null; + this.additionalComparisonType = null; + this.additionalComparisonValue = null; + this.additionalComparisonDescription = ''; + this.additionalComparisonValues = []; + this.enableBenchmarking = false; + this.benchmarkingVisualizationType = null; + this.greenThreshold = null; + this.yellowThreshold = null; + this.redThreshold = null; + + // Reset Step 7: Access Control and Ownership + this.roleFilter = ''; + this.selectedRoles = []; + this.enableTimeRestrictedAccess = false; + this.enableGeographicRestriction = false; + this.accessStartDate = ''; + this.accessEndDate = ''; + this.allowedRegions = []; + this.enableAccessLogging = false; + this.filteredOrganizations = this.accessControl || []; + this.filteredRoles = this.accessControl || []; + + // Reinitialize classification + this.onNumClassesChanged(this.numClassesPerSpatialUnit); + } + + hideSuccessAlert() { + this.successMessage = ''; + } + + hideErrorAlert() { + this.errorMessage = ''; + } + + hideMetadataErrorAlert() { + this.indicatorMetadataImportError = ''; + } + + hideMappingConfigErrorAlert() { + this.indicatorMappingConfigImportError = ''; + } + + onChangeIndicatorUnit() { + if (this.indicatorUnit && this.indicatorUnit.includes("Freitext")) { + this.enableFreeTextUnit = true; + } else { + this.enableFreeTextUnit = false; + } + } + + onChangeCreationType() { + if (this.indicatorCreationType && this.indicatorCreationType.apiName === "COMPUTATION") { + this.enableLowestSpatialUnitSelect = true; + } else { + this.enableLowestSpatialUnitSelect = false; + } + } + + onChangeOwner(ownerOrganization: any) { + this.ownerOrganization = ownerOrganization; + } + + onChangeIsPublic(isPublic: boolean) { + this.isPublic = isPublic; + } + + // Step 3: Topic Hierarchy Methods + onTopicChange() { + if (this.selectedTopic) { + // Load sub-topics for the selected topic + this.availableSubTopics = this.selectedTopic.subTopics || []; + this.selectedSubTopic = null; + + // Update main topic reference + this.indicatorTopic_mainTopic = this.selectedTopic; + this.indicatorTopic_subTopic = null; + this.indicatorTopic_subsubTopic = null; + this.indicatorTopic_subsubsubTopic = null; + } else { + this.availableSubTopics = []; + this.selectedSubTopic = null; + } + } + + onSubTopicChange() { + if (this.selectedSubTopic) { + // Update sub topic reference + this.indicatorTopic_subTopic = this.selectedSubTopic; + this.indicatorTopic_subsubTopic = null; + this.indicatorTopic_subsubsubTopic = null; + } + } + + onAdditionalTopicChange() { + if (this.additionalTopic) { + // Load sub-topics for the additional topic + this.additionalSubTopics = this.additionalTopic.subTopics || []; + this.additionalSubTopic = null; + } else { + this.additionalSubTopics = []; + this.additionalSubTopic = null; + } + } + + addAdditionalTopicAssignment() { + if (this.additionalTopic && this.additionalSubTopic) { + // Check if this assignment already exists + const existingAssignment = this.additionalTopicAssignments.find( + assignment => assignment.topic.topicId === this.additionalTopic.topicId && + assignment.subTopic.subTopicId === this.additionalSubTopic.subTopicId + ); + + if (!existingAssignment) { + // Check if it's the same as the main assignment + const isMainAssignment = this.selectedTopic && this.selectedSubTopic && + this.selectedTopic.topicId === this.additionalTopic.topicId && + this.selectedSubTopic.subTopicId === this.additionalSubTopic.subTopicId; + + if (!isMainAssignment) { + this.additionalTopicAssignments.push({ + topic: this.additionalTopic, + subTopic: this.additionalSubTopic + }); + + // Reset additional topic selection + this.additionalTopic = null; + this.additionalSubTopic = null; + this.additionalSubTopics = []; + } + } + } + } + + removeAdditionalTopicAssignment(index: number) { + if (index >= 0 && index < this.additionalTopicAssignments.length) { + this.additionalTopicAssignments.splice(index, 1); + } + } + + // Helper method to get all topic assignments (main + additional) + getAllTopicAssignments(): Array<{topic: any, subTopic: any, isMain: boolean}> { + const assignments: Array<{topic: any, subTopic: any, isMain: boolean}> = []; + + // Add main assignment if exists + if (this.selectedTopic && this.selectedSubTopic) { + assignments.push({ + topic: this.selectedTopic, + subTopic: this.selectedSubTopic, + isMain: true + }); + } + + // Add additional assignments + this.additionalTopicAssignments.forEach(assignment => { + assignments.push({ + ...assignment, + isMain: false + }); + }); + + return assignments; + } + + // Step 4: Reference Filtering Methods + filterIndicators() { + if (!this.indicatorNameFilter || this.indicatorNameFilter.trim() === '') { + this.filteredIndicators = this.availableIndicators || []; + } else { + const filter = this.indicatorNameFilter.toLowerCase().trim(); + this.filteredIndicators = (this.availableIndicators || []).filter(indicator => + indicator.datasetName && indicator.datasetName.toLowerCase().includes(filter) + ); + } + } + + filterGeoresources() { + if (!this.georesourceNameFilter || this.georesourceNameFilter.trim() === '') { + this.filteredGeoresources = this.availableGeoresources || []; + } else { + const filter = this.georesourceNameFilter.toLowerCase().trim(); + this.filteredGeoresources = (this.availableGeoresources || []).filter(georesource => + georesource.datasetName && georesource.datasetName.toLowerCase().includes(filter) + ); + } + } + + // Convert admin view references to API format + private convertReferencesToApiFormat() { + // Convert indicator references + this.indicatorReferences_apiRequest = this.indicatorReferences_adminView.map(ref => ({ + "referencedIndicatorName": ref.indicatorMetadata.datasetName, + "referencedIndicatorId": ref.indicatorMetadata.indicatorId, + "referencedIndicatorAbbreviation": ref.indicatorMetadata.abbreviation, + "referencedIndicatorDescription": ref.referenceDescription + })); + + // Convert georesource references + this.georesourceReferences_apiRequest = this.georesourceReferences_adminView.map(ref => ({ + "referencedGeoresourceName": ref.georesourceMetadata.datasetName, + "referencedGeoresourceId": ref.georesourceMetadata.georesourceId, + "referencedGeoresourceDescription": ref.referenceDescription + })); + } + + // Step 5: Classification Methods + goToClassificationTab(tabIndex: number) { + this.currentClassificationTab = tabIndex; + + // Update active tab classes + this.tabClasses.forEach((_, index) => { + if (index === tabIndex) { + this.tabClasses[index] = 'active'; + } else { + this.tabClasses[index] = ''; + } + }); + } + + getClassColor(classIndex: number, palette: any): string { + if (!palette || !palette.colors) { + return '#cccccc'; + } + + const colors = palette.colors; + if (classIndex >= 0 && classIndex < colors.length) { + return colors[classIndex]; + } + + return '#cccccc'; + } + + // Override existing classification methods to work with Step 5 + onClassificationMethodSelected(method: any) { + this.classificationMethod = method; + // Reinitialize classification when method changes + this.onNumClassesChanged(this.numClassesPerSpatialUnit); + } + + onClickColorBrewerEntry(colorPaletteEntry: any) { + this.selectedColorBrewerPaletteEntry = colorPaletteEntry; + } + + onNumClassesChanged(numClasses: number) { + this.numClassesPerSpatialUnit = numClasses; + + // Initialize classification for each spatial unit + this.spatialUnitClassification = []; + this.tabClasses = []; + + if (this.availableSpatialUnits && this.availableSpatialUnits.length > 0) { + this.availableSpatialUnits.forEach((spatialUnit, index) => { + // Initialize breaks array + const breaks: Array = []; + for (let i = 0; i < numClasses - 1; i++) { + breaks.push(null); + } + + this.spatialUnitClassification.push({ + spatialUnitId: spatialUnit.spatialUnitId, + spatialUnitLevel: spatialUnit.spatialUnitLevel, + breaks: breaks + }); + + // Initialize tab class + this.tabClasses[index] = index === 0 ? 'active' : ''; + }); + } + + // Reset validation + this.classBreaksInvalid = false; + } + + onBreaksChanged(tabIndex: number) { + if (!this.spatialUnitClassification[tabIndex]) { + return; + } + + const breaks = this.spatialUnitClassification[tabIndex].breaks; + let cssClass = 'active'; + this.classBreaksInvalid = false; + + // Validate breaks for manual classification + if (this.classificationMethod === 'manual') { + let lastValidBreak = null; + for (let i = 0; i < breaks.length; i++) { + if (breaks[i] !== null && breaks[i] !== undefined) { + if (lastValidBreak !== null && breaks[i] <= lastValidBreak) { + cssClass = 'tab-error'; + this.classBreaksInvalid = true; + break; + } + lastValidBreak = breaks[i]; + } + } + } else { + for (const classBreak of this.spatialUnitClassification[tabIndex].breaks) { + if (classBreak !== null) { + cssClass = 'tab-error'; + this.classBreaksInvalid = true; + } + } + } + + this.tabClasses[tabIndex] = cssClass; + } + + // Step 6: Regional Comparison Methods + onComparisonValueTypeChange() { + // Reset comparison value when type changes + if (this.comparisonValueType === null) { + this.comparisonValue = null; + } + } + + addAdditionalComparisonValue() { + if (this.additionalComparisonType && this.additionalComparisonValue !== null) { + // Check if this comparison already exists + const existingComparison = this.additionalComparisonValues.find( + comparison => comparison.type === this.additionalComparisonType && + comparison.value === this.additionalComparisonValue + ); + + if (!existingComparison) { + this.additionalComparisonValues.push({ + type: this.additionalComparisonType, + value: this.additionalComparisonValue, + description: this.additionalComparisonDescription || '' + }); + + // Reset additional comparison inputs + this.additionalComparisonType = null; + this.additionalComparisonValue = null; + this.additionalComparisonDescription = ''; + } + } + } + + removeAdditionalComparisonValue(index: number) { + if (index >= 0 && index < this.additionalComparisonValues.length) { + this.additionalComparisonValues.splice(index, 1); + } + } + + getComparisonTypeDisplayName(type: string): string { + const typeMap: { [key: string]: string } = { + 'target': 'Zielwert', + 'average': 'Durchschnittswert', + 'median': 'Medianwert', + 'best_practice': 'Best Practice', + 'threshold': 'Schwellenwert', + 'custom': 'Benutzerdefiniert' + }; + return typeMap[type] || type; + } + + // Helper method to get all comparison values (main + additional) + getAllComparisonValues(): Array<{type: string, value: number, description: string, isMain: boolean}> { + const comparisons: Array<{type: string, value: number, description: string, isMain: boolean}> = []; + + // Add main comparison if exists + if (this.comparisonValueType && this.comparisonValue !== null) { + comparisons.push({ + type: this.comparisonValueType, + value: this.comparisonValue, + description: this.comparisonDescription, + isMain: true + }); + } + + // Add additional comparisons + this.additionalComparisonValues.forEach(comparison => { + comparisons.push({ + ...comparison, + isMain: false + }); + }); + + return comparisons; + } + + // Validate benchmarking thresholds + validateBenchmarkingThresholds(): boolean { + if (!this.enableBenchmarking) { + return true; + } + + if (this.greenThreshold === null || this.yellowThreshold === null || this.redThreshold === null) { + return false; + } + + // Ensure thresholds are in logical order + return this.greenThreshold <= this.yellowThreshold && this.yellowThreshold <= this.redThreshold; + } + + // Step 7: Access Control Methods + filterOrganizations() { + if (!this.ownerOrgFilter || this.ownerOrgFilter.trim() === '') { + this.filteredOrganizations = this.accessControl || []; + } else { + const filter = this.ownerOrgFilter.toLowerCase().trim(); + this.filteredOrganizations = (this.accessControl || []).filter(org => + org.organizationName && org.organizationName.toLowerCase().includes(filter) + ); + } + } + + clearOwnerFilter() { + this.ownerOrgFilter = ''; + this.filterOrganizations(); + } + + filterRoles() { + if (!this.roleFilter || this.roleFilter.trim() === '') { + this.filteredRoles = this.accessControl || []; + } else { + const filter = this.roleFilter.toLowerCase().trim(); + this.filteredRoles = (this.accessControl || []).filter(role => + role.roleName && role.roleName.toLowerCase().includes(filter) + ); + } + } + + isRoleSelected(role: any): boolean { + return this.selectedRoles.some(selectedRole => selectedRole.roleId === role.roleId); + } + + toggleRoleSelection(role: any) { + if (this.isRoleSelected(role)) { + this.removeRole(role); + } else { + this.addRole(role); + } + } + + addRole(role: any) { + if (!this.isRoleSelected(role)) { + this.selectedRoles.push(role); + } + } + + removeRole(role: any) { + const index = this.selectedRoles.findIndex(selectedRole => selectedRole.roleId === role.roleId); + if (index >= 0) { + this.selectedRoles.splice(index, 1); + } + } + + // Validate access control configuration + validateAccessControl(): boolean { + // Owner organization is required + if (!this.ownerOrganization) { + return false; + } + + // If not public, at least one role must be selected + if (!this.isPublic && this.selectedRoles.length === 0) { + return false; + } + + // Validate time restrictions if enabled + if (this.enableTimeRestrictedAccess) { + if (!this.accessStartDate || !this.accessEndDate) { + return false; + } + // Check if end date is after start date + const startDate = new Date(this.accessStartDate); + const endDate = new Date(this.accessEndDate); + if (endDate <= startDate) { + return false; + } + } + + // Validate geographic restrictions if enabled + if (this.enableGeographicRestriction && (!this.allowedRegions || this.allowedRegions.length === 0)) { + return false; + } + + return true; + } + + // Get selected role IDs for API + getSelectedRoleIds(): string[] { + return this.selectedRoles.map(role => role.roleId); + } + + cancel() { + console.log('Modal cancelled'); + this.activeModal.dismiss('cancel'); + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorBatchUpdateModal/indicator-batch-update-modal.component.css b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorBatchUpdateModal/indicator-batch-update-modal.component.css new file mode 100644 index 000000000..23b96c933 --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorBatchUpdateModal/indicator-batch-update-modal.component.css @@ -0,0 +1,339 @@ +/* Batch update modal styles */ +.batch-list-table-wrapper { + max-height: 500px; + overflow-y: auto; +} + +.batch-list-table { + font-size: 11px; + width: 100%; + overflow: auto; +} + +.batch-list-table th, +.batch-list-table td { + padding: 8px; + vertical-align: middle; + white-space: nowrap; +} + +.batch-list-table-sticky-column { + position: sticky; + background-color: #fff; + z-index: 10; +} + +.batch-list-table-sticky-column-1 { + left: 0; + width: 50px; +} + +.batch-list-table-sticky-column-2 { + left: 50px; + width: 200px; +} + +.batch-list-table-sticky-column-header { + background-color: #f8f9fa; + border-bottom: 2px solid #dee2e6; +} + +.batch-list-odd-rows { + background-color: #f8f9fa; +} + +.batch-list-even-rows { + background-color: #ffffff; +} + +.batch-list-table-name-field { + min-width: 180px; +} + +.indicatorTimeseriesMappingBtn { + background-color: #5cb85c !important; + border-color: #4cae4c !important; +} + +.indicatorTimeseriesMappingBtn:hover { + background-color: #449d44 !important; + border-color: #398439 !important; +} + +.loading-overlay-admin-panel { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(255, 255, 255, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.icon-spin { + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Form control styles */ +.form-control { + font-size: 11px; + padding: 4px 8px; + height: auto; +} + +.form-control:focus { + border-color: #80bdff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +/* Button styles */ +.btn { + font-size: 11px; + padding: 4px 8px; +} + +.btn-sm { + font-size: 10px; + padding: 3px 6px; +} + +/* Modal styles */ +.modal-xl { + max-width: 95%; +} + +.modal-body { + max-height: 80vh; + overflow-y: auto; +} + +/* Table responsive styles */ +.table-responsive { + border: 1px solid #dee2e6; +} + +/* Checkbox styles */ +input[type="checkbox"] { + margin: 0; + vertical-align: middle; +} + +/* Select styles */ +select.form-control { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m1 6 7 7 7-7'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 0.75rem center; + background-size: 16px 12px; + padding-right: 2rem; +} + +/* File input styles */ +input[type="file"] { + padding: 2px; +} + +/* Form check styles */ +.form-check { + margin-bottom: 0; +} + +.form-check-input { + margin-right: 8px; +} + +/* Utility classes */ +.text-end { + text-align: right; +} + +.mb-3 { + margin-bottom: 1rem; +} + +.mt-3 { + margin-top: 1rem; +} + +/* Switch styles */ +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.switchslider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; +} + +.switchslider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .switchslider { + background-color: #2196F3; +} + +input:focus + .switchslider { + box-shadow: 0 0 1px #2196F3; +} + +input:checked + .switchslider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); +} + +.switchslider.round { + border-radius: 34px; +} + +.switchslider.round:before { + border-radius: 50%; +} + +/* AdminLTE box styles */ +.box { + position: relative; + border-radius: 3px; + background: #ffffff; + border-top: 3px solid #d2d6de; + margin-bottom: 20px; + width: 100%; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); +} + +.box.box-primary { + border-top-color: #3c8dbc; +} + +.box.collapsed-box .box-body, +.box.collapsed-box .box-footer { + display: none; +} + +.box-header { + color: #444; + display: block; + padding: 10px; + position: relative; +} + +.box-header.with-border { + border-bottom: 1px solid #f4f4f4; +} + +.box-title { + font-size: 18px; + margin: 0; + line-height: 1; +} + +.box-tools { + position: absolute; + right: 10px; + top: 5px; +} + +.btn-box-tool { + padding: 5px; + font-size: 12px; + background: transparent; + color: #97a0b3; + border: none; +} + +.pull-right { + float: right !important; +} + +.pull-left { + float: left !important; +} + +.box-body { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; + padding: 10px; +} + +/* Vertical align */ +.vertical-align { + display: flex; + align-items: flex-start; +} + +/* Help block */ +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #737373; +} + +/* Alert styles */ +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} + +.alert-warning { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .batch-list-table { + font-size: 10px; + } + + .batch-list-table th, + .batch-list-table td { + padding: 4px; + } + + .modal-xl { + max-width: 100%; + margin: 0; + } + + .vertical-align { + flex-direction: column; + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorBatchUpdateModal/indicator-batch-update-modal.component.html b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorBatchUpdateModal/indicator-batch-update-modal.component.html new file mode 100644 index 000000000..c994a466b --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorBatchUpdateModal/indicator-batch-update-modal.component.html @@ -0,0 +1,419 @@ + +
+
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorBatchUpdateModal/indicator-batch-update-modal.component.ts b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorBatchUpdateModal/indicator-batch-update-modal.component.ts new file mode 100644 index 000000000..efd858e03 --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorBatchUpdateModal/indicator-batch-update-modal.component.ts @@ -0,0 +1,509 @@ +import { Component, OnInit, OnDestroy, ViewChild, ElementRef, Input } from '@angular/core'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { HttpClient } from '@angular/common/http'; +import { Subscription } from 'rxjs'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; +import { KommonitorIndicatorDataExchangeService } from 'services/adminIndicatorUnit/kommonitor-data-exchange.service'; +import { KommonitorIndicatorCacheHelperService } from 'services/adminIndicatorUnit/kommonitor-cache-helper.service'; + +declare const $: any; +declare const __env: any; + +interface BatchListItem { + isSelected: boolean; + name: any; + mappingTableName: string; + mappingObj: { + converter: any; + dataSource: any; + propertyMapping: { + timeseriesMappings: any[]; + spatialReferenceKeyProperty: string; + keepMissingOrNullValueIndicator: boolean; + }; + targetSpatialUnitName: string; + }; + selectedConverter: any; + selectedDatasourceType: any; + selectedTargetSpatialUnit: any; +} + +@Component({ + selector: 'indicator-batch-update-modal', + templateUrl: './indicator-batch-update-modal.component.html', + styleUrls: ['./indicator-batch-update-modal.component.css'] +}) +export class IndicatorBatchUpdateModalComponent implements OnInit, OnDestroy { + + @ViewChild('batchListFileInput') batchListFileInput!: ElementRef; + @Input() modalRef?: NgbModalRef; + + public isFirstStart: boolean = true; + public lastUpdateResponseObj: any; + public timeseriesMappingReference: any; + public selected: any = { value: null }; + public keepMissingValues: boolean = true; + public batchList: BatchListItem[] = []; + public timeseriesMappingModalOpenForIndex: number | undefined; + public defaultTimeseriesMappingSave: any[] = []; + public allRowsSelected: boolean = false; + public loadingData: boolean = false; + + private subscriptions: Subscription[] = []; + private keyDownHandler: (event: KeyboardEvent) => void; + + constructor( + private modalService: NgbModal, + private http: HttpClient, + private broadcastService: BroadcastService, + public kommonitorDataExchangeService: KommonitorIndicatorDataExchangeService, + private kommonitorCacheHelperService: KommonitorIndicatorCacheHelperService + ) { + this.keyDownHandler = this.handleKeyDown.bind(this); + } + + ngOnInit(): void { + this.setupEventListeners(); + this.initialize(); + + // Add keyboard event listener for Escape key + document.addEventListener('keydown', this.keyDownHandler); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(sub => sub.unsubscribe()); + + // Remove keyboard event listener + document.removeEventListener('keydown', this.keyDownHandler); + } + + private setupEventListeners(): void { + // Listen for batch update completion + const sub1 = this.broadcastService.currentBroadcastMsg.subscribe(data => { + if (data.msg === 'batchUpdateCompleted' && (data as any).resourceType === 'indicator') { + this.lastUpdateResponseObj = data; + } + else if (data.msg === 'refreshIndicatorOverviewTableCompleted') { + this.refreshNameColumn(); + } + else if (data.msg === 'timeseriesMappingChanged') { + this.timeseriesMappingReference = (data as any).mapping; + } + }); + this.subscriptions.push(sub1); + } + + public openModal(): void { + // This method will be called from the parent component + this.initialize(); + } + + private initialize(): void { + if (this.isFirstStart) { + this.addNewRowToBatchList(); + this.isFirstStart = false; + } + + // Set initial selected value if available + if (this.kommonitorDataExchangeService.availableIndicators && + this.kommonitorDataExchangeService.availableIndicators.length > 0) { + this.selected.value = this.kommonitorDataExchangeService.availableIndicators[0]; + } + } + + public addNewRowToBatchList(): void { + const newRow: BatchListItem = { + isSelected: true, + name: null, + mappingTableName: '', + mappingObj: { + converter: { + encoding: '', + mimeType: '', + name: '', + parameters: [], + schema: '', + crs: 'EPSG:4326', + separator: ',', + schemaNamespace: '', + schemaLocation: '' + }, + dataSource: { + parameters: [], + type: 'FILE', + url: '', + payload: '' + }, + propertyMapping: { + timeseriesMappings: [], + spatialReferenceKeyProperty: '', + keepMissingOrNullValueIndicator: true + }, + targetSpatialUnitName: '' + }, + selectedConverter: null, + selectedDatasourceType: null, + selectedTargetSpatialUnit: null + }; + + this.batchList.push(newRow); + } + + public deleteSelectedRowsFromBatchList(): void { + this.batchList = this.batchList.filter(row => !row.isSelected); + } + + public onChangeSelectAllRows(): void { + this.batchList.forEach(row => { + row.isSelected = this.allRowsSelected; + }); + } + + public loadIndicatorsBatchList(): void { + if (this.batchListFileInput) { + this.batchListFileInput.nativeElement.click(); + } + } + + public onBatchListFileSelected(event: Event): void { + const target = event.target as HTMLInputElement; + const file = target.files?.[0]; + if (file) { + this.parseBatchListFromFile(file); + } + } + + private parseBatchListFromFile(file: File): void { + const reader = new FileReader(); + reader.onload = (e: any) => { + try { + const newBatchList = JSON.parse(e.target.result); + this.processParsedBatchList(newBatchList); + } catch (error) { + console.error('Error parsing batch list file:', error); + } + }; + reader.readAsText(file); + } + + private processParsedBatchList(newBatchList: any[]): void { + // Remove all existing rows + this.batchList.forEach(row => row.isSelected = true); + this.deleteSelectedRowsFromBatchList(); + + // Add new rows from file + newBatchList.forEach((item: any) => { + this.addNewRowToBatchList(); + const row = this.batchList[this.batchList.length - 1]; + + row.isSelected = item.isSelected; + + // Set indicator by ID + const indicatorId = item.name; + const indicatorObj = this.kommonitorDataExchangeService.getIndicatorMetadataById(indicatorId); + row.name = indicatorObj; + + row.mappingTableName = item.mappingTableName; + row.mappingObj = item.mappingObj; + + // Convert parameters to properties + if (row.mappingObj.converter) { + row.mappingObj.converter = this.converterParametersArrayToProperties(row.mappingObj.converter); + } + if (row.mappingObj.dataSource) { + row.mappingObj.dataSource = this.dataSourceParametersArrayToProperty(row.mappingObj.dataSource); + } + + // Set selected objects + if (item.mappingObj.converter?.name) { + row.selectedConverter = this.getConverterObjectByName(item.mappingObj.converter.name); + } + if (item.mappingObj.dataSource?.type) { + row.selectedDatasourceType = this.getDatasourceTypeObjectByType(item.mappingObj.dataSource.type); + } + if (item.mappingObj.targetSpatialUnitName) { + row.selectedTargetSpatialUnit = this.getSpatialUnitObjectByName(item.mappingObj.targetSpatialUnitName); + } + }); + } + + public onMappingTableSelected(event: Event, index: number): void { + const target = event.target as HTMLInputElement; + const file = target.files?.[0]; + if (file) { + // Implementation for mapping table selection + console.log('Mapping table selected for index:', index, file); + + const reader = new FileReader(); + reader.onload = (e: any) => { + try { + // Handle mapping table file content + console.log('Mapping table file content loaded for index:', index); + } catch (error) { + console.error('Error reading mapping table file:', error); + } + }; + reader.readAsText(file); + } + } + + public onDataSourceFileSelected(event: Event, index: number): void { + const target = event.target as HTMLInputElement; + const file = target.files?.[0]; + if (file) { + // Implementation for data source file selection + console.log('Data source file selected for index:', index, file); + + const reader = new FileReader(); + reader.onload = () => { + try { + // Handle data source file content + console.log('Data source file content loaded for index:', index); + } catch (error) { + console.error('Error reading data source file:', error); + } + }; + reader.readAsText(file); + } + } + + public onTimeseriesMappingBtnClicked(event: any, index: number): void { + this.timeseriesMappingModalOpenForIndex = index; + // Open timeseries mapping modal + console.log('Opening timeseries mapping modal for index:', index); + } + + public onDefaultTimeseriesMappingBtnClicked(event: any): void { + // Open default timeseries mapping modal + console.log('Opening default timeseries mapping modal'); + } + + public saveMappingObjectToFile(event: any, index: number): void { + const row = this.batchList[index]; + const mappingData = { + name: row.name?.indicatorId || '', + mappingTableName: row.mappingTableName, + mappingObj: row.mappingObj, + isSelected: row.isSelected + }; + + const blob = new Blob([JSON.stringify(mappingData, null, 2)], { type: 'application/json' }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `indicator-mapping-${row.name?.indicatorName || 'unknown'}.json`; + a.click(); + window.URL.revokeObjectURL(url); + } + + public startBatchUpdate(): void { + this.loadingData = true; + + // Implementation for batch update + console.log('Starting batch update for indicators:', this.batchList); + + // Simulate batch update process + setTimeout(() => { + this.loadingData = false; + this.broadcastService.broadcast('batchUpdateCompleted', { + resourceType: 'indicator', + status: 'success', + message: 'Batch update completed successfully' + }); + }, 2000); + } + + public reopenResultModal(): void { + if (this.lastUpdateResponseObj) { + this.broadcastService.broadcast('reopenBatchUpdateResultModal', this.lastUpdateResponseObj); + } + } + + private refreshNameColumn(): void { + // Refresh name column dropdowns + console.log('Refreshing name column'); + } + + // Helper methods for parameter conversion + private converterParametersArrayToProperties(converter: any): any { + const properties: any = {}; + if (converter.parameters) { + converter.parameters.forEach((param: any) => { + const propertyName = this.getConverterParameterPropertyName(param.name); + if (propertyName) { + properties[propertyName] = param.value; + } + }); + } + return { ...converter, ...properties }; + } + + private dataSourceParametersArrayToProperty(dataSource: any): any { + const properties: any = {}; + if (dataSource.parameters) { + dataSource.parameters.forEach((param: any) => { + const propertyName = this.getDataSourceParameterPropertyName(param.name); + if (propertyName) { + properties[propertyName] = param.value; + } + }); + } + return { ...dataSource, ...properties }; + } + + private getConverterParameterPropertyName(paramName: string): string | null { + const mapping: { [key: string]: string } = { + 'CRS': 'crs', + 'Hausnummer_Spaltenname': 'hnrColumnName', + 'Strasse_Spaltenname': 'streetColumnName', + 'Adresse_Spaltenname': 'addressColumnName', + 'Strasse_Hausnummer_Spaltenname': 'streetHnrColumnName', + 'X_Koordinatenspalte_Rechtswert': 'xCoordColumnName', + 'Y_Koordinatenspalte_Hochwert': 'yCoordColumnName', + 'Postleitzahl_Spaltenname': 'plzColumnName', + 'Stadt_Spaltenname': 'cityColumnName', + 'NAMESPACE': 'schemaNamespace', + 'SCHEMA_LOCATION': 'schemaLocation', + 'Trennzeichen': 'separator' + }; + return mapping[paramName] || null; + } + + private getDataSourceParameterPropertyName(paramName: string): string | null { + const mapping: { [key: string]: string } = { + 'NAME': 'name', + 'URL': 'url', + 'payload': 'payload' + }; + return mapping[paramName] || null; + } + + private getConverterObjectByName(name: string): any { + // Implementation to get converter object by name + // Access through AngularJS service for now + const angularJsService = (this.kommonitorDataExchangeService as any).angularJsDataExchangeService; + if (angularJsService && angularJsService.availableConverters) { + return angularJsService.availableConverters.find((c: any) => c.name === name); + } + return null; + } + + private getDatasourceTypeObjectByType(type: string): any { + // Implementation to get datasource type object by type + // Access through AngularJS service for now + const angularJsService = (this.kommonitorDataExchangeService as any).angularJsDataExchangeService; + if (angularJsService && angularJsService.availableDatasourceTypes) { + return angularJsService.availableDatasourceTypes.find((d: any) => d.type === type); + } + return null; + } + + private getSpatialUnitObjectByName(name: string): any { + // Implementation to get spatial unit object by name + if (this.kommonitorDataExchangeService.availableSpatialUnits) { + return this.kommonitorDataExchangeService.availableSpatialUnits.find(s => s.spatialUnitLevel === name); + } + return null; + } + + // Column visibility methods + public checkColumnsToShowSelectedConverter(): string[] { + const converters = this.batchList + .map(row => row.selectedConverter?.name) + .filter(name => name); + + const columns: string[] = []; + if (converters.some(name => name && name.includes('Tabelle_Zeitreihe_zu_Indikator'))) { + columns.push('Tabelle_Zeitreihe_zu_Indikator'); + } + if (converters.some(name => name && name.includes('WFS_v1'))) { + columns.push('WFS_v1'); + } + return columns; + } + + public checkIfSelectedDatasourceTypeIsFile(): boolean { + return this.batchList.some(row => row.selectedDatasourceType?.type === 'FILE'); + } + + public checkIfSelectedDatasourceTypeIsHttp(): boolean { + return this.batchList.some(row => row.selectedDatasourceType?.type === 'HTTP'); + } + + public checkIfSelectedDatasourceTypeIsInline(): boolean { + return this.batchList.some(row => row.selectedDatasourceType?.type === 'INLINE'); + } + + public checkIfSelectedConverterIsCsvOnlyIndicator(): boolean { + return this.batchList.some(row => row.selectedConverter?.name?.includes('csv_onlyIndicator')); + } + + // Helper methods to get available options + public getAvailableConverters(): any[] { + const angularJsService = (this.kommonitorDataExchangeService as any).angularJsDataExchangeService; + return angularJsService?.availableConverters || []; + } + + public getAvailableDatasourceTypes(): any[] { + const angularJsService = (this.kommonitorDataExchangeService as any).angularJsDataExchangeService; + return angularJsService?.availableDatasourceTypes || []; + } + + // Default value function properties + public colDefaultFunctionSelectedColumn: string | null = null; + public colDefaultFunctionNewValue: any = undefined; + public colDefaultFunctionAllRowsChb: boolean = false; + + public onClickSaveColDefaultValue(): void { + // Implementation for saving default column value + console.log('Saving default column value:', this.colDefaultFunctionSelectedColumn, this.colDefaultFunctionNewValue); + } + + public saveBatchListToFile(): void { + const batchData = this.batchList.map(item => ({ + name: item.name?.indicatorId || '', + mappingTableName: item.mappingTableName, + mappingObj: item.mappingObj, + isSelected: item.isSelected + })); + + const blob = new Blob([JSON.stringify(batchData, null, 2)], { type: 'application/json' }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'indicator-batch-list.json'; + a.click(); + window.URL.revokeObjectURL(url); + } + + public checkIfNameAndFilesChosenInEachRow(): boolean { + // Check if each row has required fields filled + return this.batchList.length > 0 && this.batchList.every(row => + row.name && + row.selectedConverter && + row.selectedDatasourceType && + row.mappingObj.propertyMapping.spatialReferenceKeyProperty && + row.selectedTargetSpatialUnit + ); + } + + public resetBatchUpdateForm(): void { + this.batchList = []; + this.addNewRowToBatchList(); + this.colDefaultFunctionSelectedColumn = null; + this.colDefaultFunctionNewValue = undefined; + this.colDefaultFunctionAllRowsChb = false; + } + + public closeModal(): void { + if (this.modalRef) { + this.modalRef.close(); + } + } + + private handleKeyDown(event: KeyboardEvent): void { + if (event.key === 'Escape') { + this.closeModal(); + } + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorDeleteModal/indicator-delete-modal.component.css b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorDeleteModal/indicator-delete-modal.component.css new file mode 100644 index 000000000..586f1e6d2 --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorDeleteModal/indicator-delete-modal.component.css @@ -0,0 +1,459 @@ +.modal-header { + background-color: #f8f9fa; + border-bottom: 1px solid #dee2e6; + padding: 1rem 1.5rem; +} + +.modal-title { + margin-bottom: 0; + line-height: 1.5; + color: #495057; +} + +.btn-close { + background: none; + border: none; + font-size: 1.25rem; + color: #000; + opacity: 0.5; + cursor: pointer; +} + +.btn-close:hover { + opacity: 0.75; +} + +.modal-body { + padding: 1.5rem; + position: relative; + min-height: 400px; +} + +.loading-overlay-admin-panel { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(255, 255, 255, 0.8); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; +} + +.fs-title { + font-size: 1.5rem; + color: #495057; + margin-bottom: 0.5rem; +} + +.fs-subtitle { + font-size: 1rem; + color: #6c757d; + margin-bottom: 1.5rem; +} + +.input-group-text { + background-color: #e9ecef; + border: 1px solid #ced4da; + color: #495057; +} + +.form-control { + border: 1px solid #ced4da; + border-radius: 0.25rem; + padding: 0.375rem 0.75rem; + font-size: 0.875rem; +} + +.form-control:focus { + border-color: #80bdff; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.form-control[size] { + min-height: 200px; +} + +.help-block { + font-size: 0.75rem; + color: #6c757d; + margin-top: 0.25rem; +} + +.card { + border: 1px solid #dee2e6; + border-radius: 0.25rem; + margin-bottom: 1rem; +} + +.card-info { + border-color: #b3d4fc; +} + +.card-header { + background-color: #ebf0fd; + border-bottom: 1px solid #dee2e6; + padding: 0.75rem 1rem; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; +} + +.card-title { + margin-bottom: 0; + font-size: 1rem; + color: #495057; +} + +.card-tools .btn-tool { + background: none; + border: none; + color: #495057; + cursor: pointer; + padding: 0.25rem 0.5rem; +} + +.card-tools .btn-tool:hover { + color: #007bff; +} + +.card-body { + padding: 1rem; +} + +.admin-table-wrapper { + overflow-x: auto; + max-height: 400px; +} + +.table { + margin-bottom: 0; + font-size: 0.75rem; +} + +.table th, +.table td { + padding: 0.5rem; + vertical-align: top; + border-top: 1px solid #dee2e6; + word-wrap: break-word; + max-width: 200px; +} + +.table thead th { + vertical-align: bottom; + border-bottom: 2px solid #dee2e6; + background-color: #f8f9fa; + font-weight: 600; +} + +.table-bordered { + border: 1px solid #dee2e6; +} + +.table-bordered th, +.table-bordered td { + border: 1px solid #dee2e6; +} + +.table-condensed th, +.table-condensed td { + padding: 0.25rem; + font-size: 0.75rem; +} + +.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(0, 0, 0, 0.05); +} + +.multiStepForm { + margin-top: 1.5rem; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + font-weight: 600; + color: #495057; + margin-bottom: 0.5rem; + display: block; +} + +.form-check { + margin-bottom: 1rem; +} + +.form-check-input { + margin-right: 0.5rem; +} + +.form-check-label { + color: #495057; + cursor: pointer; +} + +.alert { + border: 1px solid transparent; + border-radius: 0.25rem; + padding: 0.75rem 1.25rem; + margin-bottom: 1rem; +} + +.alert-success { + color: #155724; + background-color: #d4edda; + border-color: #c3e6cb; +} + +.alert-danger { + color: #721c24; + background-color: #f8d7da; + border-color: #f5c6cb; +} + +.alert-dismissible { + padding-right: 4rem; +} + +.alert-dismissible .btn-close { + position: absolute; + top: 0; + right: 0; + padding: 0.75rem 1.25rem; + color: inherit; +} + +.position-absolute { + position: absolute !important; +} + +.bottom-0 { + bottom: 0 !important; +} + +.w-100 { + width: 100% !important; +} + +.modal-footer { + background-color: #f8f9fa; + border-top: 1px solid #dee2e6; + padding: 1rem 1.5rem; + display: flex; + justify-content: flex-end; + gap: 0.5rem; +} + +.btn { + display: inline-block; + font-weight: 400; + text-align: center; + white-space: nowrap; + vertical-align: middle; + user-select: none; + border: 1px solid transparent; + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.25rem; + cursor: pointer; + text-decoration: none; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.btn:hover { + text-decoration: none; +} + +.btn:focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.btn:disabled { + opacity: 0.65; + cursor: not-allowed; +} + +.btn-secondary { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-secondary:hover { + background-color: #5a6268; + border-color: #545b62; +} + +.btn-danger { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} + +.btn-danger:hover { + background-color: #c82333; + border-color: #bd2130; +} + +.btn-danger:disabled { + background-color: #dc3545; + border-color: #dc3545; +} + +.vertical-align { + align-items: center; +} + +.row { + display: flex; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; +} + +.col-md-6, +.col-md-10, +.col-md-12, +.col-sm-6, +.col-sm-12, +.col-xs-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} + +@media (min-width: 768px) { + .col-md-6 { + flex: 0 0 50%; + max-width: 50%; + } + + .col-md-10 { + flex: 0 0 83.33333%; + max-width: 83.33333%; + } + + .col-md-12 { + flex: 0 0 100%; + max-width: 100%; + } +} + +@media (min-width: 576px) { + .col-sm-6 { + flex: 0 0 50%; + max-width: 50%; + } + + .col-sm-12 { + flex: 0 0 100%; + max-width: 100%; + } +} + +.col-xs-12 { + flex: 0 0 100%; + max-width: 100%; +} + +/* Responsive table */ +@media (max-width: 768px) { + .admin-table-wrapper { + font-size: 0.625rem; + } + + .table th, + .table td { + padding: 0.25rem; + max-width: 150px; + } +} + +/* Scrollbar styling */ +.admin-table-wrapper::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.admin-table-wrapper::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +.admin-table-wrapper::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; +} + +.admin-table-wrapper::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} + +/* Typography adjustments */ +h3 { + color: #dc3545; + font-size: 1.25rem; + margin-bottom: 1rem; +} + +h4 { + color: #495057; + font-size: 1.1rem; + margin-bottom: 0.75rem; + margin-top: 1.5rem; +} + +p { + margin-bottom: 1rem; + line-height: 1.5; +} + +ul { + margin-bottom: 1rem; + padding-left: 1.5rem; +} + +li { + margin-bottom: 0.25rem; +} + +/* Icon styling */ +.fas { + margin-right: 0.25rem; +} + +.fa-spin { + animation: fa-spin 2s infinite linear; +} + +@keyframes fa-spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(359deg); + } +} + +/* Text utilities */ +.text-center { + text-align: center; +} + +/* Spacing utilities */ +.mb-3 { + margin-bottom: 1rem; +} + +.mt-2 { + margin-top: 0.5rem; +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorDeleteModal/indicator-delete-modal.component.html b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorDeleteModal/indicator-delete-modal.component.html new file mode 100644 index 000000000..98174086c --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorDeleteModal/indicator-delete-modal.component.html @@ -0,0 +1,418 @@ + + + + + \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorDeleteModal/indicator-delete-modal.component.ts b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorDeleteModal/indicator-delete-modal.component.ts new file mode 100644 index 000000000..2f8dcb211 --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorDeleteModal/indicator-delete-modal.component.ts @@ -0,0 +1,447 @@ +import { Component, OnInit, OnDestroy, Inject } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { HttpClient } from '@angular/common/http'; +import { Subscription } from 'rxjs'; + +import { DataExchangeService } from '../../../../../services/data-exchange-service/data-exchange.service'; +import { BroadcastService } from '../../../../../services/broadcast-service/broadcast.service'; + +declare var __env: any; + +interface IndicatorDeleteType { + displayName: string; + apiName: string; +} + +interface ApplicableDate { + timestamp: string; + isSelected: boolean; +} + +interface ApplicableSpatialUnit { + spatialUnitMetadata: any; + isSelected: boolean; +} + +interface AffectedScript { + scriptId: string; + name: string; + description: string; + requiredIndicatorIds: string[]; +} + +interface AffectedIndicatorReference { + indicatorMetadata: any; + indicatorReference: any; +} + +interface AffectedGeoresourceReference { + indicatorMetadata: any; + georesourceReference: any; +} + +@Component({ + selector: 'app-indicator-delete-modal', + templateUrl: './indicator-delete-modal.component.html', + styleUrls: ['./indicator-delete-modal.component.css'] +}) +export class IndicatorDeleteModalComponent implements OnInit, OnDestroy { + + indicatorDeleteTypes: IndicatorDeleteType[] = [ + { + displayName: "Gesamter Datensatz", + apiName: "indicatorDataset" + }, + { + displayName: "Einzelne Zeitschnitte", + apiName: "indicatorTimestamp" + }, + { + displayName: "Einzelne Raumebenen", + apiName: "indicatorSpatialUnit" + } + ]; + + indicatorDeleteType: IndicatorDeleteType = this.indicatorDeleteTypes[0]; + selectedIndicatorDataset: any = undefined; + currentIndicatorId: string = ''; + currentApplicableDates: ApplicableDate[] = []; + selectIndicatorTimestampsInput: boolean = false; + currentApplicableSpatialUnits: ApplicableSpatialUnit[] = []; + selectIndicatorSpatialUnitsInput: boolean = false; + indicatorNameFilter: string = ''; + + loadingData: boolean = false; + + successfullyDeletedDatasets: any[] = []; + successfullyDeletedTimestamps: ApplicableDate[] = []; + successfullyDeletedSpatialUnits: ApplicableSpatialUnit[] = []; + failedDatasetsAndErrors: [any, string][] = []; + failedTimestampsAndErrors: [ApplicableDate, string][] = []; + failedSpatialUnitsAndErrors: [ApplicableSpatialUnit, string][] = []; + + affectedScripts: AffectedScript[] = []; + affectedIndicatorReferences: AffectedIndicatorReference[] = []; + affectedGeoresourceReferences: AffectedGeoresourceReference[] = []; + + showSuccessAlert: boolean = false; + showErrorAlert: boolean = false; + + private subscriptions: Subscription[] = []; + + constructor( + public activeModal: NgbActiveModal, + private http: HttpClient, + private dataExchangeService: DataExchangeService, + private broadcastService: BroadcastService, + @Inject('kommonitorDataExchangeService') public angularJsDataExchangeService: any + ) { } + + ngOnInit(): void { + this.resetIndicatorsDeleteForm(); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(sub => sub.unsubscribe()); + } + + onChangeSelectIndicatorTimestampEntries(): void { + this.selectIndicatorTimestampsInput = !this.selectIndicatorTimestampsInput; + + this.currentApplicableDates.forEach(applicableDate => { + applicableDate.isSelected = this.selectIndicatorTimestampsInput; + }); + } + + onChangeSelectIndicatorSpatialUnitsEntries(): void { + this.selectIndicatorSpatialUnitsInput = !this.selectIndicatorSpatialUnitsInput; + + this.currentApplicableSpatialUnits.forEach(applicableSpatialUnit => { + applicableSpatialUnit.isSelected = this.selectIndicatorSpatialUnitsInput; + }); + } + + onChangeSelectedIndicator(): void { + if (this.selectedIndicatorDataset) { + this.currentIndicatorId = this.selectedIndicatorDataset.indicatorId; + + this.successfullyDeletedDatasets = []; + this.successfullyDeletedTimestamps = []; + this.successfullyDeletedSpatialUnits = []; + this.failedDatasetsAndErrors = []; + this.failedTimestampsAndErrors = []; + this.failedSpatialUnitsAndErrors = []; + + this.currentApplicableDates = []; + for (const timestamp of this.selectedIndicatorDataset.applicableDates) { + this.currentApplicableDates.push({ + timestamp: timestamp, + isSelected: false + }); + } + + this.currentApplicableSpatialUnits = []; + for (const spatialUnitMetadata of this.angularJsDataExchangeService.availableSpatialUnits) { + if (this.selectedIndicatorDataset.applicableSpatialUnits && + this.selectedIndicatorDataset.applicableSpatialUnits.some((o: any) => o.spatialUnitName === spatialUnitMetadata.spatialUnitLevel)) { + + this.currentApplicableSpatialUnits.push({ + spatialUnitMetadata: spatialUnitMetadata, + isSelected: false + }); + } + } + + this.affectedScripts = this.gatherAffectedScripts(); + this.affectedIndicatorReferences = this.gatherAffectedIndicatorReferences(); + this.affectedGeoresourceReferences = this.gatherAffectedGeoresourceReferences(); + } + } + + resetIndicatorsDeleteForm(): void { + this.selectedIndicatorDataset = undefined; + this.currentApplicableDates = []; + this.selectIndicatorTimestampsInput = false; + this.currentApplicableSpatialUnits = []; + this.selectIndicatorSpatialUnitsInput = false; + this.indicatorDeleteType = this.indicatorDeleteTypes[0]; + + this.successfullyDeletedDatasets = []; + this.successfullyDeletedTimestamps = []; + this.successfullyDeletedSpatialUnits = []; + this.failedDatasetsAndErrors = []; + this.failedTimestampsAndErrors = []; + this.failedSpatialUnitsAndErrors = []; + this.affectedScripts = []; + this.affectedIndicatorReferences = []; + this.affectedGeoresourceReferences = []; + + this.hideSuccessAlert(); + this.hideErrorAlert(); + } + + gatherAffectedScripts(): AffectedScript[] { + const affectedScripts: AffectedScript[] = []; + + this.angularJsDataExchangeService.availableProcessScripts.forEach(script => { + const requiredIndicatorIds = script.requiredIndicatorIds; + + for (let i = 0; i < requiredIndicatorIds.length; i++) { + const indicatorId = requiredIndicatorIds[i]; + if (indicatorId === this.selectedIndicatorDataset.indicatorId) { + affectedScripts.push(script); + break; + } + } + }); + + return affectedScripts; + } + + gatherAffectedGeoresourceReferences(): AffectedGeoresourceReference[] { + const affectedGeoresourceReferences: AffectedGeoresourceReference[] = []; + + const georesourceReferences = this.selectedIndicatorDataset.referencedGeoresources; + + for (let i = 0; i < georesourceReferences.length; i++) { + const georesourceReference = georesourceReferences[i]; + + affectedGeoresourceReferences.push({ + indicatorMetadata: this.selectedIndicatorDataset, + georesourceReference: georesourceReference + }); + } + + return affectedGeoresourceReferences; + } + + gatherAffectedIndicatorReferences(): AffectedIndicatorReference[] { + const affectedIndicatorReferences: AffectedIndicatorReference[] = []; + + // First add all direct references from selected indicator + const indicatorReferences_selectedIndicator = this.selectedIndicatorDataset.referencedIndicators; + + for (let i = 0; i < indicatorReferences_selectedIndicator.length; i++) { + const indicatorReference_selectedIndicator = indicatorReferences_selectedIndicator[i]; + + affectedIndicatorReferences.push({ + indicatorMetadata: this.selectedIndicatorDataset, + indicatorReference: indicatorReference_selectedIndicator + }); + } + + // Then add all references, where selected indicator is the referencedIndicator + this.angularJsDataExchangeService.availableIndicators.forEach(indicator => { + const indicatorReferences = indicator.referencedIndicators; + + for (let i = 0; i < indicatorReferences.length; i++) { + const indicatorReference = indicatorReferences[i]; + if (indicatorReference.referencedIndicatorId === this.selectedIndicatorDataset.indicatorId) { + affectedIndicatorReferences.push({ + indicatorMetadata: this.selectedIndicatorDataset, + indicatorReference: indicatorReference + }); + } + } + }); + + return affectedIndicatorReferences; + } + + deleteIndicatorData(): void { + this.loadingData = true; + + this.successfullyDeletedDatasets = []; + this.successfullyDeletedTimestamps = []; + this.successfullyDeletedSpatialUnits = []; + this.failedDatasetsAndErrors = []; + this.failedTimestampsAndErrors = []; + this.failedSpatialUnitsAndErrors = []; + + // Depending on deleteType we must execute different DELETE requests + if (this.indicatorDeleteType.apiName === "indicatorDataset") { + // Delete complete dataset + this.deleteWholeIndicatorDataset(); + } else if (this.indicatorDeleteType.apiName === "indicatorTimestamp") { + // Delete all selected timestamps from indicator + this.deleteSelectedIndicatorTimestamps(); + } else if (this.indicatorDeleteType.apiName === "indicatorSpatialUnit") { + // Delete all selected spatial units from indicator + this.deleteSelectedIndicatorSpatialUnits(); + } + } + + deleteWholeIndicatorDataset(): void { + this.loadingData = true; + + const url = `${this.angularJsDataExchangeService.baseUrlToKomMonitorDataAPI}/indicators/${this.selectedIndicatorDataset.indicatorId}`; + + this.http.delete(url).subscribe({ + next: (response) => { + this.successfullyDeletedDatasets.push(this.selectedIndicatorDataset); + + // Fetch indicator metadata again as an indicator was deleted + this.broadcastService.broadcast("refreshIndicatorOverviewTable", { action: "delete", indicatorId: this.currentIndicatorId }); + + setTimeout(() => { + this.broadcastService.broadcast("refreshAdminDashboardDiagrams"); + }, 500); + + this.showSuccessAlert = true; + + setTimeout(() => { + this.loadingData = false; + }); + }, + error: (error) => { + if (error.error) { + this.failedDatasetsAndErrors.push([this.selectedIndicatorDataset, this.angularJsDataExchangeService.syntaxHighlightJSON(error.error)]); + } else { + this.failedDatasetsAndErrors.push([this.selectedIndicatorDataset, this.angularJsDataExchangeService.syntaxHighlightJSON(error)]); + } + + this.showErrorAlert = true; + this.loadingData = false; + } + }); + } + + async deleteSelectedIndicatorTimestamps(): Promise { + // Iterate over all applicable spatial units and selected applicable dates + for (const applicableDate of this.currentApplicableDates) { + if (applicableDate.isSelected) { + for (const applicableSpatialUnit of this.currentApplicableSpatialUnits) { + await this.getDeleteTimestampPromise(applicableDate, applicableSpatialUnit.spatialUnitMetadata.spatialUnitId); + } + } + } + + if (this.failedTimestampsAndErrors.length > 0) { + // Error handling + this.showErrorAlert = true; + this.loadingData = false; + } + + if (this.successfullyDeletedTimestamps.length > 0) { + this.showSuccessAlert = true; + + // Refresh overview table + this.broadcastService.broadcast("refreshIndicatorOverviewTable", { action: "edit", indicatorId: this.currentIndicatorId }); + + // Refresh all admin dashboard diagrams due to modified metadata + setTimeout(() => { + this.broadcastService.broadcast("refreshAdminDashboardDiagrams"); + }, 500); + + this.loadingData = false; + } + } + + async deleteSelectedIndicatorSpatialUnits(): Promise { + // Iterate over all applicable spatial units + for (const applicableSpatialUnit of this.currentApplicableSpatialUnits) { + if (applicableSpatialUnit.isSelected) { + await this.getDeleteSpatialUnitPromise(applicableSpatialUnit); + } + } + + if (this.failedSpatialUnitsAndErrors.length > 0) { + // Error handling + this.showErrorAlert = true; + this.loadingData = false; + } + + if (this.successfullyDeletedSpatialUnits.length > 0) { + this.showSuccessAlert = true; + + // Fetch indicator metadata again as an indicator was modified + await this.angularJsDataExchangeService.fetchIndicatorsMetadata(this.angularJsDataExchangeService.currentKeycloakLoginRoles); + + // Refresh overview table + this.broadcastService.broadcast("refreshIndicatorOverviewTable", { action: "edit", indicatorId: this.currentIndicatorId }); + + // Refresh all admin dashboard diagrams due to modified metadata + setTimeout(() => { + this.broadcastService.broadcast("refreshAdminDashboardDiagrams"); + }, 500); + + this.loadingData = false; + } + } + + getDeleteTimestampPromise(applicableDate: ApplicableDate, spatialUnitId: string): Promise { + // Timestamp looks like 2020-12-31 + const timestamp = applicableDate.timestamp; + + // [yyyy, mm, dd] + const timestampComps = timestamp.split("-"); + + const url = `${this.angularJsDataExchangeService.baseUrlToKomMonitorDataAPI}/indicators/${this.selectedIndicatorDataset.indicatorId}/${spatialUnitId}/${timestampComps[0]}/${timestampComps[1]}/${timestampComps[2]}`; + + return this.http.delete(url).toPromise().then( + (response) => { + if (!this.successfullyDeletedTimestamps.includes(applicableDate)) { + this.successfullyDeletedTimestamps.push(applicableDate); + } + }, + (error) => { + if (error.error) { + this.failedTimestampsAndErrors.push([applicableDate, this.angularJsDataExchangeService.syntaxHighlightJSON(error.error)]); + } else { + this.failedTimestampsAndErrors.push([applicableDate, this.angularJsDataExchangeService.syntaxHighlightJSON(error)]); + } + } + ); + } + + getDeleteSpatialUnitPromise(applicableSpatialUnit: ApplicableSpatialUnit): Promise { + const url = `${this.angularJsDataExchangeService.baseUrlToKomMonitorDataAPI}/indicators/${this.selectedIndicatorDataset.indicatorId}/${applicableSpatialUnit.spatialUnitMetadata.spatialUnitId}`; + + return this.http.delete(url).toPromise().then( + (response) => { + if (!this.successfullyDeletedSpatialUnits.includes(applicableSpatialUnit)) { + this.successfullyDeletedSpatialUnits.push(applicableSpatialUnit); + } + }, + (error) => { + if (error.error) { + this.failedSpatialUnitsAndErrors.push([applicableSpatialUnit, this.angularJsDataExchangeService.syntaxHighlightJSON(error.error)]); + } else { + this.failedSpatialUnitsAndErrors.push([applicableSpatialUnit, this.angularJsDataExchangeService.syntaxHighlightJSON(error)]); + } + } + ); + } + + hideSuccessAlert(): void { + this.showSuccessAlert = false; + } + + hideErrorAlert(): void { + this.showErrorAlert = false; + } + + getIndicatorsWithPermission(): any[] { + return this.angularJsDataExchangeService.availableIndicators.filter(indicator => + indicator.userPermissions.includes("creator") + ); + } + + getFilteredIndicators(): any[] { + const indicators = this.getIndicatorsWithPermission(); + if (!this.indicatorNameFilter) { + return indicators; + } + return indicators.filter(indicator => + indicator.indicatorName.toLowerCase().includes(this.indicatorNameFilter.toLowerCase()) + ); + } + + trackByIndex(index: number, item: any): number { + return index; + } + + close(): void { + this.activeModal.dismiss(); + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditFeaturesModal/indicator-edit-features-modal.component.css b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditFeaturesModal/indicator-edit-features-modal.component.css new file mode 100644 index 000000000..c0b4ae099 --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditFeaturesModal/indicator-edit-features-modal.component.css @@ -0,0 +1,391 @@ +/* Modal Styles */ +.modal-xl { + max-width: 90%; +} + +.loading-overlay-admin-panel { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.8); + z-index: 1000; + display: flex; + justify-content: center; + align-items: center; +} + +.icon-spin { + animation: spin 1s infinite linear; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* Multi-step Form Styles */ +.multiStepForm { + position: relative; + margin-top: 30px; +} + +.multiStepForm fieldset { + background: white; + border: 0 none; + border-radius: 0.5rem; + box-sizing: border-box; + width: 100%; + margin: 0; + padding-bottom: 20px; + position: relative; +} + +.multiStepForm fieldset:not(:first-of-type) { + display: none; +} + +.multiStepForm .fs-title { + font-size: 15px; + text-transform: uppercase; + color: #2C3E50; + margin-bottom: 10px; + letter-spacing: 2px; + font-weight: bold; +} + +.multiStepForm .fs-subtitle { + font-weight: normal; + font-size: 13px; + color: #666; + margin-bottom: 20px; +} + +/* Progress Bar */ +#progressbar { + margin-bottom: 30px; + overflow: hidden; + color: lightgrey; + padding-left: 0; +} + +#progressbar li { + list-style-type: none; + font-size: 15px; + width: 50%; + float: left; + position: relative; + font-weight: 400; + cursor: pointer; +} + +#progressbar li:before { + width: 50px; + height: 50px; + line-height: 45px; + display: block; + font-size: 20px; + color: #ffffff; + background: lightgray; + border-radius: 50%; + margin: 0 auto 10px auto; + padding: 2px; +} + +#progressbar li.active:before, +#progressbar li.active:after { + background: #27AE60; +} + +#progressbar li.active { + color: #27AE60; +} + +/* Action Buttons */ +.action-button { + width: 100px; + background: #27AE60; + font-weight: bold; + color: white; + border: 0 none; + border-radius: 1px; + cursor: pointer; + padding: 10px 5px; + margin: 10px 5px; +} + +.action-button-previous { + width: 100px; + background: #616161; + font-weight: bold; + color: white; + border: 0 none; + border-radius: 1px; + cursor: pointer; + padding: 10px 5px; + margin: 10px 5px; +} + +.action-button:hover, +.action-button:focus { + box-shadow: 0 0 0 2px white, 0 0 0 3px #27AE60; +} + +.action-button-previous:hover, +.action-button-previous:focus { + box-shadow: 0 0 0 2px white, 0 0 0 3px #616161; +} + +/* Switch Styles */ +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.switchslider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: .4s; +} + +.switchslider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + transition: .4s; +} + +input:checked + .switchslider { + background-color: #2196F3; +} + +input:focus + .switchslider { + box-shadow: 0 0 1px #2196F3; +} + +input:checked + .switchslider:before { + transform: translateX(26px); +} + +.switchslider.round { + border-radius: 34px; +} + +.switchslider.round:before { + border-radius: 50%; +} + +/* Table Styles */ +.admin-table-wrapper { + margin-top: 20px; + margin-bottom: 20px; +} + +.featureTableWrapper { + border: 1px solid #ddd; + border-radius: 4px; + overflow: hidden; +} + +/* Alert Styles */ +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} + +.alert-success { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.alert-danger { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} + +.alert-info { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.alert-warning { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} + +.alert-dismissable .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} + +/* Form Styles */ +.form-group { + margin-bottom: 15px; +} + +.form-control { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: inset 0 1px 1px rgba(0,0,0,.075); + transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; +} + +.form-control:focus { + border-color: #66afe9; + outline: 0; + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6); +} + +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #737373; +} + +.with-errors { + color: #a94442; +} + +/* Button Styles */ +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: 400; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + border: 1px solid transparent; + border-radius: 4px; +} + +.btn-info { + color: #fff; + background-color: #5bc0de; + border-color: #46b8da; +} + +.btn-danger { + color: #fff; + background-color: #d9534f; + border-color: #d43f3a; +} + +.btn-success { + color: #fff; + background-color: #5cb85c; + border-color: #4cae4c; +} + +.btn-default { + color: #333; + background-color: #fff; + border-color: #ccc; +} + +.btn:disabled { + opacity: 0.65; + cursor: not-allowed; +} + +/* Modal Footer */ +.modal-footer { + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} + +.pull-left { + float: left !important; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .modal-xl { + max-width: 95%; + } + + .col-md-3, + .col-md-4, + .col-md-6 { + margin-bottom: 15px; + } + + #progressbar li { + font-size: 12px; + } + + .action-button, + .action-button-previous { + width: 80px; + font-size: 12px; + } +} + +/* Vertical Align Helper */ +.vertical-align { + display: flex; + align-items: center; +} + +/* Pre and Code Styles */ +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + color: #333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; +} + +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 4px; +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditFeaturesModal/indicator-edit-features-modal.component.html b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditFeaturesModal/indicator-edit-features-modal.component.html new file mode 100644 index 000000000..c93a1900a --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditFeaturesModal/indicator-edit-features-modal.component.html @@ -0,0 +1,402 @@ + \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditFeaturesModal/indicator-edit-features-modal.component.ts b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditFeaturesModal/indicator-edit-features-modal.component.ts new file mode 100644 index 000000000..a2ce8b419 --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditFeaturesModal/indicator-edit-features-modal.component.ts @@ -0,0 +1,577 @@ +import { Component, OnInit, Inject, ViewChild, ElementRef } from '@angular/core'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; +import { DataExchangeService } from 'services/data-exchange-service/data-exchange.service'; +import { KommonitorIndicatorDataGridHelperService } from 'services/adminIndicatorUnit/kommonitor-data-grid-helper.service'; +import { MultiStepHelperServiceService } from 'services/multi-step-helper-service/multi-step-helper-service.service'; + +declare const $: any; + +@Component({ + selector: 'app-indicator-edit-features-modal', + templateUrl: './indicator-edit-features-modal.component.html', + styleUrls: ['./indicator-edit-features-modal.component.css'] +}) +export class IndicatorEditFeaturesModalComponent implements OnInit { + @ViewChild('modal') modal!: ElementRef; + + private modalRef?: NgbModalRef; + + // Form data + currentIndicatorDataset: any; + targetApplicableSpatialUnit: any; + overviewTableTargetSpatialUnitMetadata: any; + indicatorFeaturesJSON: any; + remainingFeatureHeaders: any[] = []; + + // Converter settings + converter: any; + schema: any; + mimeType: any; + datasourceType: any; + spatialUnitRefKeyProperty: string = ''; + targetSpatialUnitMetadata: any; + + // Importer objects + converterDefinition: any; + datasourceTypeDefinition: any; + propertyMappingDefinition: any; + putBody_indicators: any; + + // Settings + keepMissingValues: boolean = true; + isPublic: boolean = false; + enableDeleteFeatures: boolean = false; + + // Timeseries mapping + timeseriesMappingReference: any[] = []; + + // Role management + roleManagementTableOptions: any; + + // Messages + successMessagePart: string = ''; + errorMessagePart: string = ''; + importerErrors: any[] = []; + indicatorMappingConfigImportError: string = ''; + + // Loading states + loadingData: boolean = false; + + // Imported features + importedFeatures: any[] = []; + + // Multi-step form + currentStep: number = 1; + totalSteps: number = 2; + + constructor( + private modalService: NgbModal, + private broadcastService: BroadcastService, + @Inject('kommonitorDataExchangeService') public angularJsDataExchangeService: any, + @Inject('kommonitorDataGridHelperService') public angularJsDataGridHelperService: any, + @Inject('kommonitorImporterHelperService') public angularJsImporterHelperService: any, + @Inject('kommonitorMultiStepFormHelperService') private angularJsMultiStepFormHelperService: any, + private dataExchangeService: DataExchangeService, + private dataGridHelperService: KommonitorIndicatorDataGridHelperService, + private multiStepHelperService: MultiStepHelperServiceService + ) {} + + ngOnInit(): void { + this.setupEventListeners(); + this.initializeForm(); + } + + private setupEventListeners(): void { + // Listen for edit indicator features event + this.broadcastService.currentBroadcastMsg.subscribe((data: any) => { + if (data.msg === 'onEditIndicatorFeatures') { + this.openModal(data.values); + } else if (data.msg === 'timeseriesMappingChanged') { + this.timeseriesMappingReference = data.mapping; + } else if (data.msg === 'refreshIndicatorOverviewTableCompleted') { + if (this.currentIndicatorDataset) { + this.currentIndicatorDataset = this.angularJsDataExchangeService.getIndicatorMetadataById(this.currentIndicatorDataset.indicatorId); + } + } else if (data.msg === 'showLoadingIcon_indicator') { + this.loadingData = true; + } else if (data.msg === 'hideLoadingIcon_indicator') { + this.loadingData = false; + } else if (data.msg === 'onDeleteFeatureEntry_indicator') { + this.broadcastService.broadcast('refreshIndicatorOverviewTable', { action: 'edit', indicatorId: this.currentIndicatorDataset.indicatorId }); + this.refreshIndicatorEditFeaturesOverviewTable(); + } + }); + } + + private initializeForm(): void { + this.resetIndicatorEditFeaturesForm(); + } + + openModal(indicatorDataset: any): void { + if (this.currentIndicatorDataset && this.currentIndicatorDataset.indicatorId === indicatorDataset.indicatorId) { + return; + } + + this.currentIndicatorDataset = indicatorDataset; + this.resetIndicatorEditFeaturesForm(); + this.angularJsDataGridHelperService.buildDataGrid_featureTable_indicatorResource("indicatorFeatureTable", [], []); + + // Register multi-step form handlers + this.angularJsMultiStepFormHelperService.registerClickHandler("indicatorEditFeaturesForm"); + } + + closeModal(): void { + if (this.modalRef) { + this.modalRef.close(); + } + } + + resetIndicatorEditFeaturesForm(): void { + this.isPublic = false; + this.enableDeleteFeatures = false; + + // Reset edit banners + this.angularJsDataGridHelperService.featureTable_indicator_lastUpdate_timestamp_success = undefined; + this.angularJsDataGridHelperService.featureTable_indicator_lastUpdate_timestamp_failure = undefined; + + this.indicatorFeaturesJSON = undefined; + this.remainingFeatureHeaders = []; + this.overviewTableTargetSpatialUnitMetadata = undefined; + + // Set default spatial unit + for (const spatialUnitMetadataEntry of this.angularJsDataExchangeService.availableSpatialUnits) { + if (this.currentIndicatorDataset?.applicableSpatialUnits?.some((o: any) => o.spatialUnitName === spatialUnitMetadataEntry.spatialUnitLevel)) { + this.overviewTableTargetSpatialUnitMetadata = spatialUnitMetadataEntry; + break; + } + } + + this.roleManagementTableOptions = this.angularJsDataGridHelperService.buildRoleManagementGrid( + 'indicatorEditFeaturesRoleManagementTable', + this.roleManagementTableOptions, + this.angularJsDataExchangeService.accessControl, + [], + true + ); + + this.spatialUnitRefKeyProperty = ''; + this.targetSpatialUnitMetadata = undefined; + this.targetApplicableSpatialUnit = undefined; + + this.converter = undefined; + this.schema = undefined; + this.mimeType = undefined; + this.datasourceType = undefined; + + this.converterDefinition = undefined; + this.datasourceTypeDefinition = undefined; + this.propertyMappingDefinition = undefined; + this.putBody_indicators = undefined; + + this.keepMissingValues = true; + + this.successMessagePart = ''; + this.errorMessagePart = ''; + this.importerErrors = []; + this.indicatorMappingConfigImportError = ''; + + this.broadcastService.broadcast('resetTimeseriesMapping'); + + this.hideSuccessAlert(); + this.hideErrorAlert(); + this.hideMappingConfigErrorAlert(); + } + + refreshIndicatorEditFeaturesOverviewTable(): void { + if (!this.overviewTableTargetSpatialUnitMetadata) { + return; + } + + this.loadingData = true; + + const url = this.angularJsDataExchangeService.getBaseUrlToKomMonitorDataAPI_spatialResource() + + "/indicators/" + this.currentIndicatorDataset.indicatorId + "/" + + this.overviewTableTargetSpatialUnitMetadata.spatialUnitId + "/without-geometry"; + + this.angularJsDataExchangeService.$http({ + url: url, + method: "GET" + }).then((response: any) => { + this.indicatorFeaturesJSON = response.data; + + const tmpRemainingHeaders: string[] = []; + + for (const property in this.indicatorFeaturesJSON[0]) { + // Only show indicator date columns as editable fields + if (property.includes(window.__env.indicatorDatePrefix)) { + tmpRemainingHeaders.push(property); + } + } + + // Sort date headers + tmpRemainingHeaders.sort((a, b) => a.localeCompare(b)); + + this.remainingFeatureHeaders = tmpRemainingHeaders; + + this.angularJsDataGridHelperService.buildDataGrid_featureTable_indicatorResource( + "indicatorFeatureTable", + tmpRemainingHeaders, + this.indicatorFeaturesJSON, + this.currentIndicatorDataset.indicatorId, + this.angularJsDataGridHelperService.resourceType_indicator, + this.enableDeleteFeatures, + this.overviewTableTargetSpatialUnitMetadata.spatialUnitId + ); + + this.loadingData = false; + }).catch((error: any) => { + if (error.data) { + this.errorMessagePart = this.angularJsDataExchangeService.syntaxHighlightJSON(error.data); + } else { + this.errorMessagePart = this.angularJsDataExchangeService.syntaxHighlightJSON(error); + } + this.showErrorAlert(); + this.loadingData = false; + }); + } + + clearAllIndicatorFeatures(): void { + if (!this.overviewTableTargetSpatialUnitMetadata) { + return; + } + + this.loadingData = true; + + const url = this.angularJsDataExchangeService.baseUrlToKomMonitorDataAPI + + "/indicators/" + this.currentIndicatorDataset.indicatorId + "/" + + this.overviewTableTargetSpatialUnitMetadata.spatialUnitId; + + this.angularJsDataExchangeService.$http({ + url: url, + method: "DELETE" + }).then((response: any) => { + this.indicatorFeaturesJSON = undefined; + this.remainingFeatureHeaders = []; + + this.broadcastService.broadcast('refreshIndicatorOverviewTable', { action: 'edit', indicatorId: this.currentIndicatorDataset.indicatorId }); + + // Force empty feature overview table on successful deletion of entries + this.angularJsDataGridHelperService.buildDataGrid_featureTable_indicatorResource("indicatorFeatureTable", [], []); + + this.successMessagePart = this.currentIndicatorDataset.indicatorName; + this.showSuccessAlert(); + this.loadingData = false; + }).catch((error: any) => { + if (error.data) { + this.errorMessagePart = this.angularJsDataExchangeService.syntaxHighlightJSON(error.data); + } else { + this.errorMessagePart = this.angularJsDataExchangeService.syntaxHighlightJSON(error); + } + this.showErrorAlert(); + this.loadingData = false; + }); + } + + onChangeSelectedSpatialUnit(targetSpatialUnitMetadata: any): void { + const applicableSpatialUnits = this.currentIndicatorDataset.applicableSpatialUnits; + + for (const applicableSpatialUnit of applicableSpatialUnits) { + if (applicableSpatialUnit.spatialUnitId === targetSpatialUnitMetadata.spatialUnitId) { + this.targetApplicableSpatialUnit = applicableSpatialUnit; + break; + } + } + + this.refreshRoles(); + } + + refreshRoles(): void { + let permissions = this.targetApplicableSpatialUnit ? this.targetApplicableSpatialUnit.permissions : []; + + if (this.currentIndicatorDataset) { + const permissionIds_ownerUnit = this.angularJsDataExchangeService.getAccessControlById(this.currentIndicatorDataset.ownerId) + .permissions + .filter((permission: any) => permission.permissionLevel == "viewer" || permission.permissionLevel == "editor") + .map((permission: any) => permission.permissionId); + + permissions = permissions.concat(permissionIds_ownerUnit); + } + + // Set datasetOwner to disable checkboxes for owned datasets in permissions-table + this.angularJsDataExchangeService.accessControl.forEach((item: any) => { + if (this.currentIndicatorDataset) { + if (item.organizationalUnitId == this.currentIndicatorDataset.ownerId) { + item.datasetOwner = true; + } else { + item.datasetOwner = false; + } + } + }); + + this.roleManagementTableOptions = this.angularJsDataGridHelperService.buildRoleManagementGrid( + 'indicatorEditFeaturesRoleManagementTable', + this.roleManagementTableOptions, + this.angularJsDataExchangeService.accessControl, + permissions, + true + ); + } + + onChangeConverter(): void { + this.schema = this.converter.schemas ? this.converter.schemas[0] : undefined; + this.mimeType = this.converter.mimeTypes[0]; + } + + onChangeMimeType(mimeType: string): void { + this.mimeType = mimeType; + } + + onChangeIsPublic(isPublic: boolean): void { + this.isPublic = isPublic; + } + + onChangeEnableDeleteFeatures(): void { + if (this.enableDeleteFeatures) { + $(".indicatorDeleteFeatureRecordBtn").attr("disabled", false); + } else { + $(".indicatorDeleteFeatureRecordBtn").attr("disabled", true); + } + } + + filterOverviewTargetSpatialUnits(): any { + return (spatialUnitMetadata: any) => { + if (this.currentIndicatorDataset) { + const isIncluded = this.currentIndicatorDataset.applicableSpatialUnits.some((o: any) => o.spatialUnitName === spatialUnitMetadata.spatialUnitLevel); + return isIncluded; + } + return false; + }; + } + + filterByKomMonitorProperties(): any { + return (item: any) => { + try { + if (item === window.__env.FEATURE_ID_PROPERTY_NAME || + item === window.__env.FEATURE_NAME_PROPERTY_NAME || + item === "validStartDate" || + item === "validEndDate") { + return false; + } + return true; + } catch (error) { + return false; + } + }; + } + + async buildImporterObjects(): Promise { + this.converterDefinition = this.buildConverterDefinition(); + this.datasourceTypeDefinition = await this.buildDatasourceTypeDefinition(); + this.propertyMappingDefinition = this.buildPropertyMappingDefinition(); + + const roleIds = this.angularJsDataGridHelperService.getSelectedRoleIds_roleManagementGrid(this.roleManagementTableOptions); + + const scopeProperties = { + "targetSpatialUnitMetadata": { + "spatialUnitLevel": this.targetSpatialUnitMetadata.spatialUnitLevel, + }, + "currentIndicatorDataset": { + "defaultClassificationMapping": this.currentIndicatorDataset.defaultClassificationMapping + }, + "permissions": roleIds, + "ownerId": this.currentIndicatorDataset.ownerId, + "isPublic": this.isPublic + }; + + this.putBody_indicators = this.angularJsImporterHelperService.buildPutBody_indicators(scopeProperties); + + if (!this.converterDefinition || !this.datasourceTypeDefinition || !this.propertyMappingDefinition || !this.putBody_indicators) { + return false; + } + + return true; + } + + buildConverterDefinition(): any { + return this.angularJsImporterHelperService.buildConverterDefinition( + this.converter, + "converterParameter_indicatorEditFeatures_", + this.schema, + this.mimeType + ); + } + + async buildDatasourceTypeDefinition(): Promise { + try { + return await this.angularJsImporterHelperService.buildDatasourceTypeDefinition( + this.datasourceType, + 'datasourceTypeParameter_indicatorEditFeatures_', + 'indicatorDataSourceInput_editFeatures' + ); + } catch (error: any) { + if (error.data) { + this.errorMessagePart = this.angularJsDataExchangeService.syntaxHighlightJSON(error.data); + } else { + this.errorMessagePart = this.angularJsDataExchangeService.syntaxHighlightJSON(error); + } + this.showErrorAlert(); + this.loadingData = false; + return null; + } + } + + buildPropertyMappingDefinition(): any { + let timeseriesMappingForImporter = this.timeseriesMappingReference || []; + return this.angularJsImporterHelperService.buildPropertyMapping_indicatorResource( + this.spatialUnitRefKeyProperty, + timeseriesMappingForImporter, + this.keepMissingValues + ); + } + + async editIndicatorFeatures(): Promise { + this.loadingData = true; + this.importerErrors = []; + this.successMessagePart = ''; + this.errorMessagePart = ''; + + // Collect data and build request for importer + const allDataSpecified = await this.buildImporterObjects(); + + if (!allDataSpecified) { + $("#indicatorEditFeaturesForm").validator("update"); + $("#indicatorEditFeaturesForm").validator("validate"); + this.loadingData = false; + return; + } + + try { + // Dry run first + const updateIndicatorResponse_dryRun = await this.angularJsImporterHelperService.updateIndicator( + this.converterDefinition, + this.datasourceTypeDefinition, + this.propertyMappingDefinition, + this.currentIndicatorDataset.indicatorId, + this.putBody_indicators, + true + ); + + if (!this.angularJsImporterHelperService.importerResponseContainsErrors(updateIndicatorResponse_dryRun)) { + // All good, really execute the request to import data against data management API + const updateIndicatorResponse = await this.angularJsImporterHelperService.updateIndicator( + this.converterDefinition, + this.datasourceTypeDefinition, + this.propertyMappingDefinition, + this.currentIndicatorDataset.indicatorId, + this.putBody_indicators, + false + ); + + this.broadcastService.broadcast('refreshIndicatorOverviewTable', { action: 'edit', indicatorId: this.currentIndicatorDataset.indicatorId }); + + this.successMessagePart = this.currentIndicatorDataset.indicatorName; + this.importedFeatures = this.angularJsImporterHelperService.getImportedFeaturesFromImporterResponse(updateIndicatorResponse); + + this.showSuccessAlert(); + this.loadingData = false; + } else { + // Errors occurred + this.errorMessagePart = "Einige der zu importierenden Zeitreihen des Datensatzes weisen kritische Fehler auf"; + this.importerErrors = this.angularJsImporterHelperService.getErrorsFromImporterResponse(updateIndicatorResponse_dryRun); + + this.showErrorAlert(); + this.loadingData = false; + } + } catch (error: any) { + if (error.data) { + this.errorMessagePart = this.angularJsDataExchangeService.syntaxHighlightJSON(error.data); + } else { + this.errorMessagePart = this.angularJsDataExchangeService.syntaxHighlightJSON(error); + } + + this.showErrorAlert(); + this.loadingData = false; + } + } + + onImportIndicatorEditFeaturesMappingConfig(): void { + this.indicatorMappingConfigImportError = ""; + $("#indicatorMappingConfigEditFeaturesImportFile").files = []; + $("#indicatorMappingConfigEditFeaturesImportFile").click(); + } + + onExportIndicatorEditFeaturesMappingConfig(): void { + this.buildImporterObjects().then(() => { + const mappingConfigExport: any = { + "converter": this.converterDefinition, + "dataSource": this.datasourceTypeDefinition, + "propertyMapping": this.propertyMappingDefinition, + "targetSpatialUnitName": this.targetSpatialUnitMetadata.spatialUnitLevel, + "permissions": [] + }; + + const roleIds = this.angularJsDataGridHelperService.getSelectedRoleIds_roleManagementGrid(this.roleManagementTableOptions); + mappingConfigExport.permissions = roleIds; + + mappingConfigExport.isPublic = this.isPublic; + mappingConfigExport.ownerId = this.currentIndicatorDataset.ownerId; + + const metadataJSON = JSON.stringify(mappingConfigExport); + const fileName = "KomMonitor-Import-Mapping-Konfiguration_Export.json"; + + const blob = new Blob([metadataJSON], { type: "application/json" }); + const data = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.download = fileName; + a.href = data; + a.textContent = "JSON"; + a.target = "_blank"; + a.rel = "noopener noreferrer"; + a.click(); + + a.remove(); + }); + } + + // Multi-step form navigation + nextStep(): void { + if (this.currentStep < this.totalSteps) { + this.currentStep++; + } + } + + previousStep(): void { + if (this.currentStep > 1) { + this.currentStep--; + } + } + + goToStep(step: number): void { + if (step >= 1 && step <= this.totalSteps) { + this.currentStep = step; + } + } + + // Alert management + showSuccessAlert(): void { + $("#indicatorEditFeaturesSuccessAlert").show(); + } + + hideSuccessAlert(): void { + $("#indicatorEditFeaturesSuccessAlert").hide(); + } + + showErrorAlert(): void { + $("#indicatorEditFeaturesErrorAlert").show(); + } + + hideErrorAlert(): void { + $("#indicatorEditFeaturesErrorAlert").hide(); + } + + hideMappingConfigErrorAlert(): void { + $("#indicatorEditFeaturesMappingConfigImportErrorAlert").hide(); + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditIndicatorSpatialUnitRolesModal/indicator-edit-indicator-spatial-unit-roles-modal.component.css b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditIndicatorSpatialUnitRolesModal/indicator-edit-indicator-spatial-unit-roles-modal.component.css new file mode 100644 index 000000000..52d7aaa12 --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditIndicatorSpatialUnitRolesModal/indicator-edit-indicator-spatial-unit-roles-modal.component.css @@ -0,0 +1,425 @@ +/* Modal Styles */ +.modal-xl { + max-width: 90%; +} + +.loading-overlay-admin-panel { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.8); + z-index: 1000; + display: flex; + justify-content: center; + align-items: center; +} + +.icon-spin { + animation: spin 1s infinite linear; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* Multi-step Form Styles */ +.multiStepForm { + position: relative; + margin-top: 30px; +} + +.multiStepForm fieldset { + background: white; + border: 0 none; + border-radius: 0.5rem; + box-sizing: border-box; + width: 100%; + margin: 0; + padding-bottom: 20px; + position: relative; +} + +.multiStepForm fieldset:not(:first-of-type) { + display: none; +} + +.multiStepForm .fs-title { + font-size: 15px; + text-transform: uppercase; + color: #2C3E50; + margin-bottom: 10px; + letter-spacing: 2px; + font-weight: bold; +} + +.multiStepForm .fs-subtitle { + font-weight: normal; + font-size: 13px; + color: #666; + margin-bottom: 20px; +} + +/* Progress Bar */ +#progressbar { + margin-bottom: 30px; + overflow: hidden; + color: lightgrey; + padding-left: 0; +} + +#progressbar li { + list-style-type: none; + font-size: 15px; + width: 33.33%; + float: left; + position: relative; + font-weight: 400; + cursor: pointer; +} + +#progressbar li:before { + width: 50px; + height: 50px; + line-height: 45px; + display: block; + font-size: 20px; + color: #ffffff; + background: lightgray; + border-radius: 50%; + margin: 0 auto 10px auto; + padding: 2px; +} + +#progressbar li.active:before, +#progressbar li.active:after { + background: #27AE60; +} + +#progressbar li.active { + color: #27AE60; +} + +/* Action Buttons */ +.action-button { + width: 100px; + background: #27AE60; + font-weight: bold; + color: white; + border: 0 none; + border-radius: 1px; + cursor: pointer; + padding: 10px 5px; + margin: 10px 5px; +} + +.action-button-previous { + width: 100px; + background: #616161; + font-weight: bold; + color: white; + border: 0 none; + border-radius: 1px; + cursor: pointer; + padding: 10px 5px; + margin: 10px 5px; +} + +.action-button:hover, +.action-button:focus { + box-shadow: 0 0 0 2px white, 0 0 0 3px #27AE60; +} + +.action-button-previous:hover, +.action-button-previous:focus { + box-shadow: 0 0 0 2px white, 0 0 0 3px #616161; +} + +/* Switch Styles */ +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.switchslider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: .4s; +} + +.switchslider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + transition: .4s; +} + +input:checked + .switchslider { + background-color: #2196F3; +} + +input:focus + .switchslider { + box-shadow: 0 0 1px #2196F3; +} + +input:checked + .switchslider:before { + transform: translateX(26px); +} + +.switchslider.round { + border-radius: 34px; +} + +.switchslider.round:before { + border-radius: 50%; +} + +/* Alert Styles */ +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} + +.alert-success { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.alert-danger { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} + +.alert-info { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.alert-warning { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} + +.alert-dismissable .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} + +/* Form Styles */ +.form-group { + margin-bottom: 15px; +} + +.form-control { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: inset 0 1px 1px rgba(0,0,0,.075); + transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; +} + +.form-control:focus { + border-color: #66afe9; + outline: 0; + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6); +} + +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #737373; +} + +.with-errors { + color: #a94442; +} + +/* Button Styles */ +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: 400; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + border: 1px solid transparent; + border-radius: 4px; +} + +.btn-info { + color: #fff; + background-color: #5bc0de; + border-color: #46b8da; +} + +.btn-danger { + color: #fff; + background-color: #d9534f; + border-color: #d43f3a; +} + +.btn-success { + color: #fff; + background-color: #5cb85c; + border-color: #4cae4c; +} + +.btn-default { + color: #333; + background-color: #fff; + border-color: #ccc; +} + +.btn:disabled { + opacity: 0.65; + cursor: not-allowed; +} + +/* Modal Footer */ +.modal-footer { + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} + +.pull-left { + float: left !important; +} + +/* Input Group Styles */ +.input-group { + position: relative; + display: table; + border-collapse: separate; +} + +.input-group-addon { + padding: 6px 12px; + font-size: 14px; + font-weight: 400; + line-height: 1; + color: #555; + text-align: center; + background-color: #eee; + border: 1px solid #ccc; + border-radius: 4px; + display: table-cell; + width: 1%; + white-space: nowrap; + vertical-align: middle; +} + +.input-group .form-control { + position: relative; + z-index: 2; + float: left; + width: 100%; + margin-bottom: 0; + display: table-cell; +} + +.input-group .form-control:first-child { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group .form-control:last-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +/* Vertical Align Helper */ +.vertical-align { + display: flex; + align-items: center; +} + +.margin-right { + margin-right: 10px; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .modal-xl { + max-width: 95%; + } + + .col-md-3, + .col-md-6, + .col-md-9 { + margin-bottom: 15px; + } + + #progressbar li { + font-size: 12px; + } + + .action-button, + .action-button-previous { + width: 80px; + font-size: 12px; + } +} + +/* Pre and Code Styles */ +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + color: #333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; +} + +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 4px; +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditIndicatorSpatialUnitRolesModal/indicator-edit-indicator-spatial-unit-roles-modal.component.html b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditIndicatorSpatialUnitRolesModal/indicator-edit-indicator-spatial-unit-roles-modal.component.html new file mode 100644 index 000000000..b57afd8f8 --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditIndicatorSpatialUnitRolesModal/indicator-edit-indicator-spatial-unit-roles-modal.component.html @@ -0,0 +1,226 @@ + \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditIndicatorSpatialUnitRolesModal/indicator-edit-indicator-spatial-unit-roles-modal.component.ts b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditIndicatorSpatialUnitRolesModal/indicator-edit-indicator-spatial-unit-roles-modal.component.ts new file mode 100644 index 000000000..8431e1a53 --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditIndicatorSpatialUnitRolesModal/indicator-edit-indicator-spatial-unit-roles-modal.component.ts @@ -0,0 +1,444 @@ +import { Component, OnInit, Inject, ViewChild, ElementRef } from '@angular/core'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; +import { DataExchangeService } from 'services/data-exchange-service/data-exchange.service'; +import { KommonitorIndicatorDataGridHelperService } from 'services/adminIndicatorUnit/kommonitor-data-grid-helper.service'; +import { MultiStepHelperServiceService } from 'services/multi-step-helper-service/multi-step-helper-service.service'; +import { HttpClient } from '@angular/common/http'; + +declare const $: any; + +@Component({ + selector: 'app-indicator-edit-indicator-spatial-unit-roles-modal', + templateUrl: './indicator-edit-indicator-spatial-unit-roles-modal.component.html', + styleUrls: ['./indicator-edit-indicator-spatial-unit-roles-modal.component.css'] +}) +export class IndicatorEditIndicatorSpatialUnitRolesModalComponent implements OnInit { + @ViewChild('modal') modal!: ElementRef; + + private modalRef?: NgbModalRef; + + // Form data + currentIndicatorDataset: any; + targetApplicableSpatialUnit: any; + + // Role management tables + roleManagementTableOptions_indicatorMetadata: any; + roleManagementTableOptions_indicatorSpatialUnitTimeseries: any; + + // Messages + successMessagePart: string = ''; + errorMessagePart: string = ''; + + // Form controls + ownerOrgFilter: string = ''; + ownerOrganization: any; + activeRolesOnly: boolean = true; + activeConnectedRolesOnly: boolean = true; + permissions: any[] = []; + resourcesCreatorRights: any[] = []; + + // Loading states + loadingData: boolean = false; + + // Multi-step form + currentStep: number = 1; + totalSteps: number = 3; + + constructor( + private modalService: NgbModal, + private broadcastService: BroadcastService, + private http: HttpClient, + @Inject('kommonitorDataExchangeService') public angularJsDataExchangeService: any, + @Inject('kommonitorDataGridHelperService') private angularJsDataGridHelperService: any, + @Inject('kommonitorMultiStepFormHelperService') private angularJsMultiStepFormHelperService: any, + private dataExchangeService: DataExchangeService, + private dataGridHelperService: KommonitorIndicatorDataGridHelperService, + private multiStepHelperService: MultiStepHelperServiceService + ) {} + + ngOnInit(): void { + this.setupEventListeners(); + this.initializeForm(); + } + + private setupEventListeners(): void { + // Listen for edit indicator spatial unit roles event + this.broadcastService.currentBroadcastMsg.subscribe((data: any) => { + if (data.msg === 'onEditIndicatorSpatialUnitRoles') { + this.openModal(data.values); + } else if (data.msg === 'availableRolesUpdate') { + this.refreshRoleManagementTable_indicatorMetadata(); + this.refreshRoleManagementTable_indicatorSpatialUnitTimeseries(); + } + }); + } + + private initializeForm(): void { + this.resetIndicatorEditIndicatorSpatialUnitRolesForm(); + } + + openModal(indicatorDataset: any): void { + this.currentIndicatorDataset = indicatorDataset; + this.prepareCreatorList(); + this.resetIndicatorEditIndicatorSpatialUnitRolesForm(); + + // Register the multi-step form handler + this.angularJsMultiStepFormHelperService.registerClickHandler("indicatorEditIndicatorSpatialUnitRolesForm"); + + // Show the modal using jQuery (since this is a legacy modal) + $('#modal-edit-indicator-spatial-unit-roles').modal('show'); + } + + closeModal(): void { + // Hide the modal using jQuery (since this is a legacy modal) + $('#modal-edit-indicator-spatial-unit-roles').modal('hide'); + } + + prepareCreatorList(): void { + if (this.angularJsDataExchangeService.currentKomMonitorLoginRoleNames.length > 0) { + let creatorRights: string[] = []; + let creatorRightsChildren: string[] = []; + + this.angularJsDataExchangeService.currentKomMonitorLoginRoleNames.forEach((roles: string) => { + let key = roles.split('.')[0]; + let role = roles.split('.')[1]; + + // case unit-resources-creator + if (role == 'unit-resources-creator' && !this.resourcesCreatorRights.includes(key)) { + creatorRights.push(key); + } + + // case client-resources-creator, gather unit-ids first, then fetch all unit-data + if (role == 'client-resources-creator' && !creatorRightsChildren.includes(key)) { + creatorRightsChildren.push(key); + } + }); + + // gather all children + this.gatherCreatorRightsChildren(creatorRights, creatorRightsChildren); + + this.resourcesCreatorRights = this.angularJsDataExchangeService.accessControl.filter((elem: any) => creatorRights.includes(elem.name)); + } + } + + gatherCreatorRightsChildren(creatorRights: string[], creatorRightsChildren: string[]): void { + if (creatorRightsChildren.length > 0) { + this.angularJsDataExchangeService.accessControl + .filter((elem: any) => creatorRightsChildren.includes(elem.name)) + .flatMap((res: any) => res.children) + .forEach((child: any) => { + this.angularJsDataExchangeService.accessControl + .filter((elem: any) => elem.organizationalUnitId == child) + .forEach((childData: any) => { + creatorRights.push(childData.name); + this.gatherCreatorRightsChildren(creatorRights, [childData.name]); + }); + }); + } + } + + resetIndicatorEditIndicatorSpatialUnitRolesForm(): void { + this.ownerOrganization = this.currentIndicatorDataset?.ownerId; + this.ownerOrgFilter = ''; + this.targetApplicableSpatialUnit = this.currentIndicatorDataset?.applicableSpatialUnits?.[0]; + + this.refreshRoleManagementTable_indicatorMetadata(); + this.refreshRoleManagementTable_indicatorSpatialUnitTimeseries(); + + this.successMessagePart = ''; + this.errorMessagePart = ''; + this.hideSuccessAlert(); + this.hideErrorAlert(); + } + + refreshRoleManagementTable_indicatorMetadata(): void { + this.permissions = this.currentIndicatorDataset ? this.currentIndicatorDataset.permissions : []; + + // set datasetOwner to disable checkboxes for owned datasets in permissions-table + this.angularJsDataExchangeService.accessControl.forEach((item: any) => { + if (this.currentIndicatorDataset) { + if (item.organizationalUnitId == this.currentIndicatorDataset.ownerId) { + item.datasetOwner = true; + } else { + item.datasetOwner = false; + } + } + }); + + if (this.permissions.length == 0) { + this.activeRolesOnly = false; + } + + let access = this.angularJsDataExchangeService.accessControl; + if (this.permissions.length > 0 && this.activeRolesOnly) { + access = this.angularJsDataExchangeService.accessControl.filter((unit: any) => { + return (unit.permissions.filter((unitPermission: any) => this.permissions.includes(unitPermission.permissionId)).length > 0 ? true : false); + }); + } + + this.roleManagementTableOptions_indicatorMetadata = this.angularJsDataGridHelperService.buildRoleManagementGrid( + 'indicatorEditRoleManagementTable', + this.roleManagementTableOptions_indicatorMetadata, + access, + this.permissions, + true + ); + } + + refreshRoleManagementTable_indicatorSpatialUnitTimeseries(): void { + if (this.targetApplicableSpatialUnit && this.targetApplicableSpatialUnit.permissions) { + if (this.targetApplicableSpatialUnit.permissions.length == 0) { + this.activeConnectedRolesOnly = false; + } + + let connectedAccess = this.angularJsDataExchangeService.accessControl; + if (this.targetApplicableSpatialUnit.permissions.length > 0 && this.activeConnectedRolesOnly) { + connectedAccess = this.angularJsDataExchangeService.accessControl.filter((unit: any) => { + return (unit.permissions.filter((unitPermission: any) => this.targetApplicableSpatialUnit.permissions.includes(unitPermission.permissionId)).length > 0 ? true : false); + }); + } + + this.roleManagementTableOptions_indicatorSpatialUnitTimeseries = this.angularJsDataGridHelperService.buildRoleManagementGrid( + 'indicatorEditIndicatorSpatialUnitsRoleManagementTable', + this.roleManagementTableOptions_indicatorSpatialUnitTimeseries, + connectedAccess, + this.targetApplicableSpatialUnit.permissions, + true + ); + } else { + this.activeConnectedRolesOnly = false; + this.roleManagementTableOptions_indicatorSpatialUnitTimeseries = this.angularJsDataGridHelperService.buildRoleManagementGrid( + 'indicatorEditIndicatorSpatialUnitsRoleManagementTable', + this.roleManagementTableOptions_indicatorSpatialUnitTimeseries, + this.angularJsDataExchangeService.accessControl, + [], + true + ); + } + } + + onActiveConnectedRolesOnlyChange(): void { + this.refreshRoleManagementTable_indicatorSpatialUnitTimeseries(); + } + + onActiveRolesOnlyChange(): void { + this.refreshRoleManagementTable_indicatorMetadata(); + } + + onChangeOwner(ownerOrganization: any): void { + this.ownerOrganization = ownerOrganization; + console.log("Target creator role selected to be:", this.ownerOrganization); + this.refreshRoles(this.ownerOrganization); + } + + refreshRoles(orgUnitId: string): void { + let permissionIds_ownerUnit = orgUnitId ? + this.angularJsDataExchangeService.getAccessControlById(orgUnitId).permissions + .filter((permission: any) => permission.permissionLevel == "viewer" || permission.permissionLevel == "editor") + .map((permission: any) => permission.permissionId) : []; + + // set datasetOwner to disable checkboxes for owned datasets in permissions-table + this.angularJsDataExchangeService.accessControl.forEach((item: any) => { + if (item.organizationalUnitId == orgUnitId) { + item.datasetOwner = true; + } else { + item.datasetOwner = false; + } + }); + + this.roleManagementTableOptions_indicatorMetadata = this.angularJsDataGridHelperService.buildRoleManagementGrid( + 'indicatorEditRoleManagementTable', + this.roleManagementTableOptions_indicatorMetadata, + this.angularJsDataExchangeService.accessControl, + permissionIds_ownerUnit, + true + ); + + this.roleManagementTableOptions_indicatorSpatialUnitTimeseries = this.angularJsDataGridHelperService.buildRoleManagementGrid( + 'indicatorEditIndicatorSpatialUnitsRoleManagementTable', + this.roleManagementTableOptions_indicatorSpatialUnitTimeseries, + this.angularJsDataExchangeService.accessControl, + permissionIds_ownerUnit, + true + ); + } + + editIndicatorSpatialUnitRoles(): void { + if (this.ownerOrganization !== undefined && this.ownerOrganization != this.currentIndicatorDataset.ownerId) { + if (!confirm('Sind Sie sicher, dass Sie den Eigentümerschaft an dieser Resource endgültig und unwiderruflich übertragen und damit abgeben wollen?')) { + return; + } + } + + this.executeRequest_indicatorMetadataRoles(); + this.executeRequest_indicatorOwnership(); + this.executeRequest_indicatorSpatialUnitRoles(); + this.executeRequest_indicatorSpatialUnitOwnership(); + } + + executeRequest_indicatorMetadataRoles(): void { + this.loadingData = true; + + let putBody = { + "permissions": this.angularJsDataGridHelperService.getSelectedRoleIds_roleManagementGrid(this.roleManagementTableOptions_indicatorMetadata), + "isPublic": this.currentIndicatorDataset.isPublic + }; + + this.http.put( + this.angularJsDataExchangeService.baseUrlToKomMonitorDataAPI + "/indicators/" + this.currentIndicatorDataset.indicatorId + "/permissions", + putBody + ).subscribe({ + next: (response: any) => { + this.successMessagePart = this.currentIndicatorDataset.indicatorName; + this.broadcastService.broadcast('refreshIndicatorOverviewTable', { crudType: 'edit', targetIndicatorId: this.currentIndicatorDataset.indicatorId }); + this.showSuccessAlert(); + this.loadingData = false; + }, + error: (error: any) => { + this.errorMessagePart = "Fehler beim Aktualisieren der Metadaten-Zugriffsrechte. Fehler lautet: \n\n"; + if (error.data) { + this.errorMessagePart += this.angularJsDataExchangeService.syntaxHighlightJSON(error.data); + } else { + this.errorMessagePart += this.angularJsDataExchangeService.syntaxHighlightJSON(error); + } + this.showErrorAlert(); + this.loadingData = false; + } + }); + } + + executeRequest_indicatorOwnership(): void { + this.loadingData = true; + + let putBody = { + "ownerId": this.ownerOrganization === undefined ? this.currentIndicatorDataset.ownerId : this.ownerOrganization + }; + + this.http.put( + this.angularJsDataExchangeService.baseUrlToKomMonitorDataAPI + "/indicators/" + this.currentIndicatorDataset.indicatorId + "/ownership", + putBody + ).subscribe({ + next: (response: any) => { + this.successMessagePart = this.currentIndicatorDataset.indicatorName; + this.broadcastService.broadcast('refreshIndicatorOverviewTable', { crudType: 'edit', targetIndicatorId: this.currentIndicatorDataset.indicatorId }); + this.showSuccessAlert(); + this.loadingData = false; + }, + error: (error: any) => { + this.errorMessagePart = "Fehler beim Aktualisieren der Metadaten-Eigentümerschaft. Fehler lautet: \n\n"; + if (error.data) { + this.errorMessagePart += this.angularJsDataExchangeService.syntaxHighlightJSON(error.data); + } else { + this.errorMessagePart += this.angularJsDataExchangeService.syntaxHighlightJSON(error); + } + this.showErrorAlert(); + this.loadingData = false; + } + }); + } + + executeRequest_indicatorSpatialUnitOwnership(): void { + this.loadingData = true; + + if (this.currentIndicatorDataset.applicableSpatialUnits && this.currentIndicatorDataset.applicableSpatialUnits.length > 0) { + this.currentIndicatorDataset.applicableSpatialUnits.forEach((indicatorSpatialUnit: any) => { + let putBody = { + "ownerId": this.ownerOrganization === undefined ? this.currentIndicatorDataset.ownerId : this.ownerOrganization + }; + + this.http.put( + this.angularJsDataExchangeService.baseUrlToKomMonitorDataAPI + "/indicators/" + this.currentIndicatorDataset.indicatorId + "/" + indicatorSpatialUnit.spatialUnitId + "/ownership", + putBody + ).subscribe({ + next: (response: any) => { + this.successMessagePart = this.currentIndicatorDataset.indicatorName; + this.broadcastService.broadcast('refreshIndicatorOverviewTable', { crudType: 'edit', targetIndicatorId: this.currentIndicatorDataset.indicatorId }); + this.showSuccessAlert(); + this.loadingData = false; + }, + error: (error: any) => { + this.errorMessagePart = "Fehler beim Aktualisieren der Metadaten-Eigentümerschaft. Fehler lautet: \n\n"; + if (error.data) { + this.errorMessagePart += this.angularJsDataExchangeService.syntaxHighlightJSON(error.data); + } else { + this.errorMessagePart += this.angularJsDataExchangeService.syntaxHighlightJSON(error); + } + this.showErrorAlert(); + this.loadingData = false; + } + }); + }); + } + } + + executeRequest_indicatorSpatialUnitRoles(): void { + let putBody = { + "permissions": this.angularJsDataGridHelperService.getSelectedRoleIds_roleManagementGrid(this.roleManagementTableOptions_indicatorSpatialUnitTimeseries), + "isPublic": this.targetApplicableSpatialUnit.isPublic + }; + + this.loadingData = true; + + this.http.put( + this.angularJsDataExchangeService.baseUrlToKomMonitorDataAPI + "/indicators/" + this.currentIndicatorDataset.indicatorId + "/" + this.targetApplicableSpatialUnit.spatialUnitId + "/permissions", + putBody + ).subscribe({ + next: (response: any) => { + this.broadcastService.broadcast('refreshIndicatorOverviewTable', { crudType: 'edit', targetIndicatorId: this.currentIndicatorDataset.indicatorId }); + this.showSuccessAlert(); + this.loadingData = false; + }, + error: (error: any) => { + this.errorMessagePart = "Fehler beim Aktualisieren der Zugriffsrechte auf Zeitreihe der Raumeinheit " + this.targetApplicableSpatialUnit.spatialUnitName + ". Fehler lautet: \n\n"; + if (error.data) { + this.errorMessagePart += this.angularJsDataExchangeService.syntaxHighlightJSON(error.data); + } else { + this.errorMessagePart += this.angularJsDataExchangeService.syntaxHighlightJSON(error); + } + this.showErrorAlert(); + this.loadingData = false; + } + }); + } + + onChangeSelectedSpatialUnit(targetApplicableSpatialUnit: any): void { + this.refreshRoleManagementTable_indicatorSpatialUnitTimeseries(); + } + + // Multi-step form navigation + nextStep(): void { + if (this.currentStep < this.totalSteps) { + this.currentStep++; + } + } + + previousStep(): void { + if (this.currentStep > 1) { + this.currentStep--; + } + } + + goToStep(step: number): void { + if (step >= 1 && step <= this.totalSteps) { + this.currentStep = step; + } + } + + // Alert management + showSuccessAlert(): void { + $("#indicatorEditIndicatorSpatialUnitRolesSuccessAlert").show(); + } + + hideSuccessAlert(): void { + $("#indicatorEditIndicatorSpatialUnitRolesSuccessAlert").hide(); + } + + showErrorAlert(): void { + $("#indicatorEditIndicatorSpatialUnitRolesErrorAlert").show(); + } + + hideErrorAlert(): void { + $("#indicatorEditIndicatorSpatialUnitRolesErrorAlert").hide(); + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditMetadataModal/indicator-edit-metadata-modal.component.css b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditMetadataModal/indicator-edit-metadata-modal.component.css new file mode 100644 index 000000000..c75f274e7 --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditMetadataModal/indicator-edit-metadata-modal.component.css @@ -0,0 +1,371 @@ +/* Indicator Edit Metadata Modal Styles */ + +.modal-header { + background-color: #f8f9fa; + border-bottom: 1px solid #dee2e6; +} + +.modal-title { + color: #495057; + font-weight: 600; +} + +.modal-body { + max-height: 70vh; + overflow-y: auto; +} + +/* Loading overlay */ +.loading-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(255, 255, 255, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +/* Form styles */ +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + font-weight: 500; + color: #495057; + margin-bottom: 0.5rem; +} + +.form-control { + border-radius: 0.25rem; + border: 1px solid #ced4da; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.form-control:focus { + border-color: #80bdff; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.form-control.is-invalid { + border-color: #dc3545; +} + +.invalid-feedback { + display: block; + width: 100%; + margin-top: 0.25rem; + font-size: 0.875rem; + color: #dc3545; +} + +/* Checkbox styles */ +.form-check { + padding-left: 1.25rem; +} + +.form-check-input { + margin-left: -1.25rem; +} + +.form-check-label { + margin-bottom: 0; + cursor: pointer; +} + +/* Section headers */ +h5 { + color: #495057; + border-bottom: 2px solid #007bff; + padding-bottom: 0.5rem; + margin-top: 2rem; + margin-bottom: 1rem; +} + +h6 { + color: #6c757d; + margin-top: 1.5rem; + margin-bottom: 1rem; +} + +/* Color palette grid */ +.color-palette-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.color-palette-item { + border: 2px solid #dee2e6; + border-radius: 0.25rem; + padding: 0.5rem; + cursor: pointer; + transition: all 0.2s ease; + text-align: center; +} + +.color-palette-item:hover { + border-color: #007bff; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.color-palette-item.selected { + border-color: #007bff; + background-color: #f8f9fa; +} + +.color-samples { + display: flex; + justify-content: center; + margin-bottom: 0.5rem; +} + +.color-sample { + width: 20px; + height: 20px; + margin: 0 1px; + border-radius: 2px; +} + +.palette-name { + font-size: 0.875rem; + font-weight: 500; + color: #495057; +} + +/* Tab styles */ +.nav-tabs { + border-bottom: 1px solid #dee2e6; +} + +.nav-tabs .nav-link { + border: 1px solid transparent; + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; + color: #495057; + cursor: pointer; +} + +.nav-tabs .nav-link:hover { + border-color: #e9ecef #e9ecef #dee2e6; +} + +.nav-tabs .nav-link.active { + color: #495057; + background-color: #fff; + border-color: #dee2e6 #dee2e6 #fff; +} + +.nav-tabs .nav-link.tab-completed { + background-color: #d4edda; + border-color: #c3e6cb; + color: #155724; +} + +.nav-tabs .nav-link.tab-error { + background-color: #f8d7da; + border-color: #f5c6cb; + color: #721c24; +} + +.tab-content { + padding: 1rem; + border: 1px solid #dee2e6; + border-top: none; + background-color: #fff; +} + +.tab-pane { + display: none; +} + +.tab-pane.active { + display: block; +} + +/* Table styles */ +.table { + margin-bottom: 0; +} + +.table th { + border-top: none; + font-weight: 600; + color: #495057; +} + +.table td { + vertical-align: middle; +} + +/* Button styles */ +.btn { + border-radius: 0.25rem; + font-weight: 500; + transition: all 0.15s ease-in-out; +} + +.btn-primary { + background-color: #007bff; + border-color: #007bff; +} + +.btn-primary:hover { + background-color: #0069d9; + border-color: #0062cc; +} + +.btn-secondary { + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-secondary:hover { + background-color: #5a6268; + border-color: #545b62; +} + +.btn-info { + background-color: #17a2b8; + border-color: #17a2b8; +} + +.btn-info:hover { + background-color: #138496; + border-color: #117a8b; +} + +.btn-danger { + background-color: #dc3545; + border-color: #dc3545; +} + +.btn-danger:hover { + background-color: #c82333; + border-color: #bd2130; +} + +.btn-sm { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +/* Alert styles */ +.alert { + border-radius: 0.25rem; + margin-bottom: 1rem; +} + +.alert-success { + color: #155724; + background-color: #d4edda; + border-color: #c3e6cb; +} + +.alert-danger { + color: #721c24; + background-color: #f8d7da; + border-color: #f5c6cb; +} + +.alert-dismissible { + padding-right: 4rem; +} + +.alert .close { + position: absolute; + top: 0; + right: 0; + padding: 0.75rem 1.25rem; + color: inherit; + background: none; + border: 0; + font-size: 1.5rem; + font-weight: 700; + line-height: 1; + cursor: pointer; +} + +/* AG Grid styles */ +.ag-theme-alpine { + --ag-header-height: 40px; + --ag-row-height: 35px; + --ag-header-background-color: #f8f9fa; + --ag-header-foreground-color: #495057; + --ag-border-color: #dee2e6; + --ag-row-hover-color: #f8f9fa; + --ag-selected-row-background-color: #e3f2fd; +} + +/* Responsive design */ +@media (max-width: 768px) { + .modal-body { + max-height: 60vh; + } + + .color-palette-grid { + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 0.5rem; + } + + .color-palette-item { + padding: 0.25rem; + } + + .color-sample { + width: 15px; + height: 15px; + } + + .palette-name { + font-size: 0.75rem; + } +} + +/* Animation for loading spinner */ +.spinner-border { + animation: spinner-border 0.75s linear infinite; +} + +@keyframes spinner-border { + to { + transform: rotate(360deg); + } +} + +/* Form validation styles */ +.form-control.ng-invalid.ng-touched { + border-color: #dc3545; +} + +.form-control.ng-valid.ng-touched { + border-color: #28a745; +} + +/* Custom scrollbar for modal body */ +.modal-body::-webkit-scrollbar { + width: 8px; +} + +.modal-body::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +.modal-body::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; +} + +.modal-body::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditMetadataModal/indicator-edit-metadata-modal.component.html b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditMetadataModal/indicator-edit-metadata-modal.component.html new file mode 100644 index 000000000..d12b0c0a4 --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditMetadataModal/indicator-edit-metadata-modal.component.html @@ -0,0 +1,628 @@ + + + + + + + + \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditMetadataModal/indicator-edit-metadata-modal.component.ts b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditMetadataModal/indicator-edit-metadata-modal.component.ts new file mode 100644 index 000000000..e33107ce0 --- /dev/null +++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditMetadataModal/indicator-edit-metadata-modal.component.ts @@ -0,0 +1,748 @@ +import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { HttpClient } from '@angular/common/http'; +import { Subscription } from 'rxjs'; +import { KommonitorIndicatorDataExchangeService } from 'services/adminIndicatorUnit/kommonitor-data-exchange.service'; +import { KommonitorIndicatorCacheHelperService } from 'services/adminIndicatorUnit/kommonitor-cache-helper.service'; +import { KommonitorIndicatorDataGridHelperService } from 'services/adminIndicatorUnit/kommonitor-data-grid-helper.service'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; + +declare const $: any; +declare const __env: any; +declare const colorbrewer: any; + +@Component({ + selector: 'indicator-edit-metadata-modal', + templateUrl: './indicator-edit-metadata-modal.component.html', + styleUrls: ['./indicator-edit-metadata-modal.component.css'] +}) +export class IndicatorEditMetadataModalComponent implements OnInit, OnDestroy { + @ViewChild('modal') modal!: ElementRef; + + private subscriptions: Subscription[] = []; + + // Current indicator dataset + currentIndicatorDataset: any = null; + + // Form data + datasetName = ''; + datasetNameInvalid = false; + indicatorAbbreviation = ''; + indicatorType: any = null; + isHeadlineIndicator = false; + indicatorUnit = ''; + enableFreeTextUnit = false; + indicatorProcessDescription = ''; + indicatorTagsString_withCommas = ''; + indicatorInterpretation = ''; + indicatorCreationType: any = null; + indicatorLowestSpatialUnitMetadataObjectForComputation: any = null; + enableLowestSpatialUnitSelect = false; + indicatorPrecision: number | null = null; + showCustomCommaValue = false; + indicatorReferenceDateNote = ''; + displayOrder = 0; + + // Metadata + metadata: any = { + note: '', + literature: '', + updateInterval: null, + sridEPSG: 4326, + datasource: '', + contact: '', + lastUpdate: '', + description: '', + databasis: '' + }; + + // Topic hierarchy + indicatorTopic_mainTopic: any = null; + indicatorTopic_subTopic: any = null; + indicatorTopic_subsubTopic: any = null; + indicatorTopic_subsubsubTopic: any = null; + + // References + indicatorNameFilter = ''; + tmpIndicatorReference_selectedIndicatorMetadata: any = null; + tmpIndicatorReference_referenceDescription = ''; + indicatorReferences_adminView: any[] = []; + indicatorReferences_apiRequest: any[] = []; + + georesourceNameFilter = ''; + tmpGeoresourceReference_selectedGeoresourceMetadata: any = null; + tmpGeoresourceReference_referenceDescription = ''; + georesourceReferences_adminView: any[] = []; + georesourceReferences_apiRequest: any[] = []; + + // Classification + numClassesArray = [3, 4, 5, 6, 7, 8]; + selectedColorBrewerPaletteEntry: any = null; + numClassesPerSpatialUnit: number | null = null; + classificationMethod = __env?.defaultClassifyMethod || 'jenks'; + spatialUnitClassification: any[] = []; + classBreaksInvalid = false; + tabClasses: string[] = []; + + // Regional reference values + regionalReferenceValuesManagementTableOptions: any = null; + tmpIndicatorRegionalReferenceValuesObject: any = null; + noneColumnValue = '-- keine --'; + file_regionalReferenceValuesImport: any = null; + + // Messages + successMessagePart = ''; + errorMessagePart = ''; + indicatorMetadataImportError = ''; + indicatorAddMetadataImportErrorAlert = false; + + // Loading state + loadingData = false; + + // Color brewer + colorbrewerSchemes = colorbrewer; + colorbreweSchemeName_dynamicIncrease = __env?.defaultColorBrewerPaletteForBalanceIncreasingValues; + colorbreweSchemeName_dynamicDecrease = __env?.defaultColorBrewerPaletteForBalanceDecreasingValues; + colorbrewerPalettes: any[] = []; + + // Metadata structure + indicatorMetadataStructure: any = { + "metadata": { + "note": "an optional note", + "literature": "optional text about literature", + "updateInterval": "YEARLY|HALF_YEARLY|QUARTERLY|MONTHLY|ARBITRARY", + "sridEPSG": 4326, + "datasource": "text about data source", + "contact": "text about contact details", + "lastUpdate": "YYYY-MM-DD", + "description": "description about spatial unit dataset", + "databasis": "text about data basis", + }, + "precision": "Custom decimal place", + "refrencesToOtherIndicators": [ + { + "referenceDescription": "description about the reference", + "indicatorId": "ID of referenced indicator dataset" + } + ], + "refrencesToGeoresources": [ + { + "referenceDescription": "description about the reference", + "georesourceId": "ID of referenced georesource dataset" + } + ], + "datasetName": "Name of indicator dataset", + "abbreviation": "optional abbreviation of the indicator dataset", + "characteristicValue": "if the same datasetName is used for different indicators, the optional characteristicValue parameter may serve to distinguish between them (i.e. Habitants - male, Habitants - female, Habitants - diverse)", + "tags": [ + "optinal list of tags; each tag is a free text tag" + ], + "creationType": "INSERTION|COMPUTATION <-- enum parameter controls whether each timestamp must be updated manually (INSERTION) or if KomMonitor shall compute the indicator values for respective timestamps based on script file (COMPUTATION)", + "unit": "unit of the indicator", + "topicReference": "ID of the respective main/sub topic instance", + "indicatorType": "STATUS_ABSOLUTE|STATUS_RELATIVE|DYNAMIC_ABSOLUTE|DYNAMIC_RELATIVE|STATUS_STANDARDIZED|DYNAMIC_STANDARDIZED", + "interpretation": "interpretation hints for the user to better understand the indicator values", + "isHeadlineIndicator": "boolean parameter to indicate if indicator is a headline indicator", + "processDescription": "detailed description about the computation/creation of the indicator", + "lowestSpatialUnitForComputation": "the name of the lowest possible spatial unit for which an indicator of creationType=COMPUTATION may be computed. All other superior spatial units will be aggregated automatically", + "referenceDateNote": "optional note for indicator reference date", + "displayOrder": 0, + "defaultClassificationMapping": { + "colorBrewerSchemeName": "schema name of colorBrewer colorPalette to use for classification", + "numClasses": "number of Classes", + "classificationMethod": "Classification Method ID", + "items": [ + { + "spatialUnit": "spatial unit id for manual classification", + "breaks": ['break'] + } + ] + }, + "regionalReferenceValues": [ + { + "referenceDate": "2024-04-23", + "regionalSum": 0, + "regionalAverage": 0, + "spatiallyUnassignable": 0 + } + ], + }; + + indicatorMetadataStructure_pretty = ''; + + constructor( + public activeModal: NgbActiveModal, + private http: HttpClient, + public kommonitorDataExchangeService: KommonitorIndicatorDataExchangeService, + private kommonitorCacheHelperService: KommonitorIndicatorCacheHelperService, + private kommonitorDataGridHelperService: KommonitorIndicatorDataGridHelperService, + private broadcastService: BroadcastService + ) {} + + ngOnInit(): void { + this.setupEventListeners(); + this.instantiateColorBrewerPalettes(); + this.indicatorMetadataStructure_pretty = this.kommonitorDataExchangeService.syntaxHighlightJSON(this.indicatorMetadataStructure); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(sub => sub.unsubscribe()); + } + + private setupEventListeners(): void { + const sub = this.broadcastService.currentBroadcastMsg.subscribe(data => { + if (data.msg === 'onEditIndicatorMetadata') { + this.currentIndicatorDataset = data.values; + this.resetIndicatorEditMetadataForm(); + } + }); + this.subscriptions.push(sub); + } + + openModal(): void { + // Modal is already opened by the parent component + // This method is called after the modal is already open + this.resetIndicatorEditMetadataForm(); + this.instantiateColorBrewerPalettes(); + } + + closeModal(): void { + this.activeModal.dismiss(); + } + + instantiateColorBrewerPalettes(): void { + const customColorSchemes = __env?.customColorSchemes; + let colorbrewerExtended = colorbrewer; + + // Add custom color themes from configuration properties + if (customColorSchemes) { + colorbrewerExtended = Object.assign(customColorSchemes, colorbrewer); + } + + for (const key in colorbrewerExtended) { + if (colorbrewerExtended.hasOwnProperty(key)) { + const colorPalettes = colorbrewerExtended[key]; + + const paletteEntry = { + "paletteName": key, + "paletteArrayObject": colorPalettes + }; + + this.colorbrewerPalettes.push(paletteEntry); + } + } + + // instantiate with palette 'Blues' + this.selectedColorBrewerPaletteEntry = this.colorbrewerPalettes[13]; + } + + onClassificationMethodSelected(method: any): void { + this.classificationMethod = method.id; + } + + onNumClassesChanged(numClasses: number): void { + this.numClassesPerSpatialUnit = numClasses; + for (let i = 0; i < this.kommonitorDataExchangeService.availableSpatialUnits.length; i++) { + const spatialUnit = this.kommonitorDataExchangeService.availableSpatialUnits[i]; + this.spatialUnitClassification[i] = { + spatialUnitId: spatialUnit.spatialUnitId, + breaks: [] + }; + this.tabClasses[i] = ''; + for (let classNr = 0; classNr < numClasses - 1; classNr++) { + this.spatialUnitClassification[i].breaks.push(null); + } + } + } + + onBreaksChanged(tabIndex: number): void { + this.classBreaksInvalid = false; + let cssClass = 'tab-completed'; + + for (const classBreak of this.spatialUnitClassification[tabIndex].breaks) { + if (classBreak === null) { + cssClass = ''; + } + } + + if (cssClass === 'tab-completed') { + for (let i = 0; i < this.spatialUnitClassification[tabIndex].breaks.length - 1; i++) { + if (this.spatialUnitClassification[tabIndex].breaks[i] > this.spatialUnitClassification[tabIndex].breaks[i + 1]) { + cssClass = 'tab-error'; + this.classBreaksInvalid = true; + } + } + } else { + for (const classBreak of this.spatialUnitClassification[tabIndex].breaks) { + if (classBreak !== null) { + cssClass = 'tab-error'; + this.classBreaksInvalid = true; + } + } + } + this.tabClasses[tabIndex] = cssClass; + this.updateDecreaseAndIncreaseBreaks(tabIndex); + } + + updateDecreaseAndIncreaseBreaks(tabIndex: number): void { + const increaseBreaksLength = this.spatialUnitClassification[tabIndex].breaks.filter((val: number) => val > 0).length; + const decreaseBreaksLength = this.spatialUnitClassification[tabIndex].breaks.filter((val: number) => val < 0).length; + + if (increaseBreaksLength < 3) { + // Handle minimum increase breaks + } + if (decreaseBreaksLength < 3) { + // Handle minimum decrease breaks + } + } + + refreshReferenceValuesManagementTable(): void { + this.regionalReferenceValuesManagementTableOptions = this.kommonitorDataGridHelperService.buildReferenceValuesManagementGrid( + this.regionalReferenceValuesManagementTableOptions + ); + } + + resetIndicatorEditMetadataForm(): void { + this.successMessagePart = ''; + this.errorMessagePart = ''; + + this.datasetName = this.currentIndicatorDataset.indicatorName; + this.datasetNameInvalid = false; + + this.indicatorReferenceDateNote = this.currentIndicatorDataset.referenceDateNote; + this.displayOrder = this.currentIndicatorDataset.displayOrder; + + // Reset metadata + this.metadata = { + note: this.currentIndicatorDataset.metadata.note, + literature: this.currentIndicatorDataset.metadata.literature, + sridEPSG: 4326, + datasource: this.currentIndicatorDataset.metadata.datasource, + databasis: this.currentIndicatorDataset.metadata.databasis, + contact: this.currentIndicatorDataset.metadata.contact, + description: this.currentIndicatorDataset.metadata.description, + lastUpdate: this.currentIndicatorDataset.metadata.lastUpdate + }; + + // Set update interval + this.kommonitorDataExchangeService.updateIntervalOptions.forEach((option: any) => { + if (option.apiName === this.currentIndicatorDataset.metadata.updateInterval) { + this.metadata.updateInterval = option; + } + }); + + this.refreshReferenceValuesManagementTable(); + + this.indicatorAbbreviation = this.currentIndicatorDataset.abbreviation; + this.indicatorPrecision = this.currentIndicatorDataset.precision; + + if (this.currentIndicatorDataset.defaultPrecision === false) { + this.showCustomCommaValue = true; + } else { + this.showCustomCommaValue = false; + } + + // Set indicator type + this.kommonitorDataExchangeService.indicatorTypeOptions.forEach((option: any) => { + if (option.apiName === this.currentIndicatorDataset.indicatorType) { + this.indicatorType = option; + } + }); + + this.isHeadlineIndicator = this.currentIndicatorDataset.isHeadlineIndicator; + this.indicatorUnit = this.currentIndicatorDataset.unit; + + this.enableFreeTextUnit = true; + this.kommonitorDataExchangeService.indicatorUnitOptions.forEach((option: any) => { + if (option === this.currentIndicatorDataset.unit) { + this.enableFreeTextUnit = false; + } + }); + + this.indicatorProcessDescription = this.currentIndicatorDataset.processDescription; + this.indicatorTagsString_withCommas = ''; + + if (this.currentIndicatorDataset.tags && this.currentIndicatorDataset.tags.length > 0) { + for (let index = 0; index < this.currentIndicatorDataset.tags.length; index++) { + this.indicatorTagsString_withCommas += this.currentIndicatorDataset.tags[index]; + if (index < this.currentIndicatorDataset.tags.length - 1) { + this.indicatorTagsString_withCommas += ','; + } + } + } else { + this.indicatorTagsString_withCommas = ''; + } + + this.indicatorInterpretation = this.currentIndicatorDataset.interpretation; + + // Set creation type + this.kommonitorDataExchangeService.indicatorCreationTypeOptions.forEach((option: any) => { + if (option.apiName === this.currentIndicatorDataset.creationType) { + this.indicatorCreationType = option; + } + }); + + this.indicatorLowestSpatialUnitMetadataObjectForComputation = null; + + for (let i = 0; i < this.kommonitorDataExchangeService.availableSpatialUnits.length; i++) { + const spatialUnitMetadata = this.kommonitorDataExchangeService.availableSpatialUnits[i]; + if (spatialUnitMetadata.spatialUnitLevel === this.currentIndicatorDataset.lowestSpatialUnitForComputation) { + this.indicatorLowestSpatialUnitMetadataObjectForComputation = spatialUnitMetadata; + break; + } + } + + if (this.indicatorCreationType?.apiName === 'COMPUTATION') { + this.enableLowestSpatialUnitSelect = true; + } else { + this.enableLowestSpatialUnitSelect = false; + } + + // Set topic hierarchy + const topicHierarchy = this.kommonitorDataExchangeService.getTopicHierarchyForTopicId(this.currentIndicatorDataset.topicReference); + + if (topicHierarchy && topicHierarchy[0]) { + this.indicatorTopic_mainTopic = topicHierarchy[0]; + } + if (topicHierarchy && topicHierarchy[1]) { + this.indicatorTopic_subTopic = topicHierarchy[1]; + } + if (topicHierarchy && topicHierarchy[2]) { + this.indicatorTopic_subsubTopic = topicHierarchy[2]; + } + if (topicHierarchy && topicHierarchy[3]) { + this.indicatorTopic_subsubsubTopic = topicHierarchy[3]; + } + + // Reset references + this.indicatorNameFilter = ''; + this.tmpIndicatorReference_selectedIndicatorMetadata = null; + this.tmpIndicatorReference_referenceDescription = ''; + this.indicatorReferences_adminView = []; + this.indicatorReferences_apiRequest = []; + + if (this.currentIndicatorDataset.referencedIndicators && this.currentIndicatorDataset.referencedIndicators.length > 0) { + for (const indicatorReference of this.currentIndicatorDataset.referencedIndicators.filter((item: any) => item != null && item != undefined)) { + const indicatorMetadata = this.kommonitorDataExchangeService.getIndicatorMetadataById(indicatorReference.referencedIndicatorId); + const referenceEntry = { + "referencedIndicatorName": indicatorMetadata.indicatorName, + "referencedIndicatorId": indicatorMetadata.indicatorId, + "referencedIndicatorAbbreviation": indicatorMetadata.abbreviation, + "referencedIndicatorDescription": indicatorReference.referencedIndicatorDescription + }; + this.indicatorReferences_adminView.push(referenceEntry); + } + } + + this.georesourceNameFilter = ''; + this.tmpGeoresourceReference_selectedGeoresourceMetadata = null; + this.tmpGeoresourceReference_referenceDescription = ''; + this.georesourceReferences_adminView = []; + this.georesourceReferences_apiRequest = []; + + if (this.currentIndicatorDataset.referencedGeoresources && this.currentIndicatorDataset.referencedGeoresources.length > 0) { + for (const georesourceReference of this.currentIndicatorDataset.referencedGeoresources) { + const georesourceMetadata = this.kommonitorDataExchangeService.getGeoresourceMetadataById(georesourceReference.referencedGeoresourceId); + const geo_referenceEntry = { + "referencedGeoresourceName": georesourceMetadata.datasetName, + "referencedGeoresourceId": georesourceMetadata.georesourceId, + "referencedGeoresourceDescription": georesourceReference.referencedGeoresourceDescription + }; + this.georesourceReferences_adminView.push(geo_referenceEntry); + } + } + + // Reset classification + this.numClassesArray = [3, 4, 5, 6, 7, 8]; + this.numClassesPerSpatialUnit = null; + this.classificationMethod = __env?.defaultClassifyMethod || 'jenks'; + this.spatialUnitClassification = []; + this.classBreaksInvalid = false; + + if (this.currentIndicatorDataset.defaultClassificationMapping.classificationMethod) { + this.classificationMethod = this.currentIndicatorDataset.defaultClassificationMapping.classificationMethod.toLowerCase(); + } + if (this.currentIndicatorDataset.defaultClassificationMapping.numClasses) { + this.numClassesPerSpatialUnit = this.currentIndicatorDataset.defaultClassificationMapping.numClasses; + this.onNumClassesChanged(this.numClassesPerSpatialUnit || 5); + + // apply breaks for spatial units: + for (let i = 0; i < this.spatialUnitClassification.length; i++) { + for (let item of this.currentIndicatorDataset.defaultClassificationMapping.items) { + if (item.spatialUnitId == this.spatialUnitClassification[i].spatialUnitId) { + this.spatialUnitClassification[i] = item; + this.onBreaksChanged(i); + } + } + } + } + + // instantiate with palette 'Blues' + this.selectedColorBrewerPaletteEntry = this.colorbrewerPalettes[13]; + + for (const colorbrewerPalette of this.colorbrewerPalettes) { + if (colorbrewerPalette.paletteName === this.currentIndicatorDataset.defaultClassificationMapping.colorBrewerSchemeName) { + this.selectedColorBrewerPaletteEntry = colorbrewerPalette; + break; + } + } + + this.successMessagePart = ''; + this.errorMessagePart = ''; + } + + onClickColorBrewerEntry(colorPaletteEntry: any): void { + this.selectedColorBrewerPaletteEntry = colorPaletteEntry; + } + + onAddOrUpdateIndicatorReference(): void { + const tmpIndicatorReference_adminView = { + "referencedIndicatorName": this.tmpIndicatorReference_selectedIndicatorMetadata.indicatorName, + "referencedIndicatorId": this.tmpIndicatorReference_selectedIndicatorMetadata.indicatorId, + "referencedIndicatorAbbreviation": this.tmpIndicatorReference_selectedIndicatorMetadata.abbreviation, + "referencedIndicatorDescription": this.tmpIndicatorReference_referenceDescription + }; + + let processed = false; + + for (let index = 0; index < this.indicatorReferences_adminView.length; index++) { + const indicatorReference_adminView = this.indicatorReferences_adminView[index]; + + if (indicatorReference_adminView.referencedIndicatorId === tmpIndicatorReference_adminView.referencedIndicatorId) { + // replace object + this.indicatorReferences_adminView[index] = tmpIndicatorReference_adminView; + processed = true; + break; + } + } + + if (!processed) { + // new entry + this.indicatorReferences_adminView.push(tmpIndicatorReference_adminView); + } + + this.tmpIndicatorReference_selectedIndicatorMetadata = null; + this.tmpIndicatorReference_referenceDescription = ''; + } + + onClickEditIndicatorReference(indicatorReference_adminView: any): void { + this.tmpIndicatorReference_selectedIndicatorMetadata = this.kommonitorDataExchangeService.getIndicatorMetadataById(indicatorReference_adminView.referencedIndicatorId); + this.tmpIndicatorReference_referenceDescription = indicatorReference_adminView.referencedIndicatorDescription; + } + + onClickDeleteIndicatorReference(indicatorReference_adminView: any): void { + for (let index = 0; index < this.indicatorReferences_adminView.length; index++) { + if (this.indicatorReferences_adminView[index].referencedIndicatorId === indicatorReference_adminView.referencedIndicatorId) { + // remove object + this.indicatorReferences_adminView.splice(index, 1); + break; + } + } + } + + onAddOrUpdateGeoresourceReference(): void { + const tmpGeoresourceReference_adminView = { + "referencedGeoresourceName": this.tmpGeoresourceReference_selectedGeoresourceMetadata.datasetName, + "referencedGeoresourceId": this.tmpGeoresourceReference_selectedGeoresourceMetadata.georesourceId, + "referencedGeoresourceDescription": this.tmpGeoresourceReference_referenceDescription + }; + + let processed = false; + + for (let index = 0; index < this.georesourceReferences_adminView.length; index++) { + const georesourceReference_adminView = this.georesourceReferences_adminView[index]; + + if (georesourceReference_adminView.referencedGeoresourceId === tmpGeoresourceReference_adminView.referencedGeoresourceId) { + // replace object + this.georesourceReferences_adminView[index] = tmpGeoresourceReference_adminView; + processed = true; + break; + } + } + + if (!processed) { + // new entry + this.georesourceReferences_adminView.push(tmpGeoresourceReference_adminView); + } + + this.tmpGeoresourceReference_selectedGeoresourceMetadata = null; + this.tmpGeoresourceReference_referenceDescription = ''; + } + + onClickEditGeoresourceReference(georesourceReference_adminView: any): void { + this.tmpGeoresourceReference_selectedGeoresourceMetadata = this.kommonitorDataExchangeService.getGeoresourceMetadataById(georesourceReference_adminView.referencedGeoresourceId); + this.tmpGeoresourceReference_referenceDescription = georesourceReference_adminView.referencedGeoresourceDescription; + } + + onClickDeleteGeoresourceReference(georesourceReference_adminView: any): void { + for (let index = 0; index < this.georesourceReferences_adminView.length; index++) { + if (this.georesourceReferences_adminView[index].referencedGeoresourceId === georesourceReference_adminView.referencedGeoresourceId) { + // remove object + this.georesourceReferences_adminView.splice(index, 1); + break; + } + } + } + + onChangeCreationType(): void { + if (this.indicatorCreationType?.apiName === 'COMPUTATION') { + this.enableLowestSpatialUnitSelect = true; + } else { + this.enableLowestSpatialUnitSelect = false; + } + } + + onChangeIndicatorUnit(): void { + if (this.indicatorUnit.includes('Freitext')) { + this.enableFreeTextUnit = true; + } else { + this.enableFreeTextUnit = false; + } + } + + checkDatasetName(): void { + this.datasetNameInvalid = false; + this.kommonitorDataExchangeService.availableIndicators.forEach((indicator: any) => { + // show error only if indicator is renamed to another already existing indicator + if (indicator.indicatorName === this.datasetName && + indicator.indicatorType === this.indicatorType?.apiName && + indicator.indicatorId != this.currentIndicatorDataset.indicatorId) { + this.datasetNameInvalid = true; + return; + } + }); + } + + buildPatchBody_indicators(): any { + const patchBody: any = { + "metadata": { + "note": this.metadata.note || null, + "literature": this.metadata.literature || null, + "updateInterval": this.metadata.updateInterval?.apiName, + "sridEPSG": this.metadata.sridEPSG || 4326, + "datasource": this.metadata.datasource, + "contact": this.metadata.contact, + "lastUpdate": this.metadata.lastUpdate, + "description": this.metadata.description || null, + "databasis": this.metadata.databasis || null + }, + "refrencesToOtherIndicators": [] as any[], + "regionalReferenceValues": [] as any[], + "datasetName": this.datasetName, + "abbreviation": this.indicatorAbbreviation || null, + "precision": (this.showCustomCommaValue === true) ? this.indicatorPrecision : null, + "characteristicValue": null, + "tags": [] as string[], + "creationType": this.indicatorCreationType?.apiName, + "unit": this.indicatorUnit, + "topicReference": "", + "refrencesToGeoresources": [] as any[], + "indicatorType": this.indicatorType?.apiName, + "interpretation": this.indicatorInterpretation || "", + "isHeadlineIndicator": this.isHeadlineIndicator || false, + "processDescription": this.indicatorProcessDescription || "", + "referenceDateNote": this.indicatorReferenceDateNote || "", + "displayOrder": this.displayOrder, + "lowestSpatialUnitForComputation": this.indicatorLowestSpatialUnitMetadataObjectForComputation ? + this.indicatorLowestSpatialUnitMetadataObjectForComputation.spatialUnitLevel : null, + "defaultClassificationMapping": { + "colorBrewerSchemeName": this.selectedColorBrewerPaletteEntry.paletteName, + "classificationMethod": this.classificationMethod.toUpperCase(), + "numClasses": this.numClassesPerSpatialUnit ? Number(this.numClassesPerSpatialUnit) : 5, + "items": this.spatialUnitClassification.filter((entry: any) => !entry.breaks.includes(null)), + } + }; + + // regionalReferenceValues + const regionalReferenceValuesList = this.kommonitorDataGridHelperService.getReferenceValues_regionalReferenceValuesManagementGrid(this.regionalReferenceValuesManagementTableOptions); + for (const referenceValueEntry of regionalReferenceValuesList) { + patchBody.regionalReferenceValues.push(referenceValueEntry); + } + + // TAGS + if (this.indicatorTagsString_withCommas) { + const tags_splitted = this.indicatorTagsString_withCommas.split(","); + for (const tagString of tags_splitted) { + patchBody.tags.push(tagString.trim()); + } + } + + // TOPIC REFERENCE + if (this.indicatorTopic_subsubsubTopic) { + patchBody.topicReference = this.indicatorTopic_subsubsubTopic.topicId; + } else if (this.indicatorTopic_subsubTopic) { + patchBody.topicReference = this.indicatorTopic_subsubTopic.topicId; + } else if (this.indicatorTopic_subTopic) { + patchBody.topicReference = this.indicatorTopic_subTopic.topicId; + } else if (this.indicatorTopic_mainTopic) { + patchBody.topicReference = this.indicatorTopic_mainTopic.topicId; + } else { + patchBody.topicReference = ""; + } + + // REFERENCES + if (this.indicatorReferences_adminView && this.indicatorReferences_adminView.length > 0) { + patchBody.refrencesToOtherIndicators = []; + + for (const indicRef of this.indicatorReferences_adminView) { + patchBody.refrencesToOtherIndicators.push({ + "indicatorId": indicRef.referencedIndicatorId, + "referenceDescription": indicRef.referencedIndicatorDescription + }); + } + } + + if (this.georesourceReferences_adminView && this.georesourceReferences_adminView.length > 0) { + patchBody.refrencesToGeoresources = []; + + for (const geoRef of this.georesourceReferences_adminView) { + patchBody.refrencesToGeoresources.push({ + "georesourceId": geoRef.referencedGeoresourceId, + "referenceDescription": geoRef.referencedGeoresourceDescription + }); + } + } + + return patchBody; + } + + editIndicatorMetadata(): void { + const patchBody = this.buildPatchBody_indicators(); + + this.loadingData = true; + + this.http.patch( + this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI + "/indicators/" + this.currentIndicatorDataset.indicatorId, + patchBody + ).subscribe({ + next: (response: any) => { + this.successMessagePart = this.datasetName; + this.broadcastService.broadcast('refreshIndicatorOverviewTable', { crudType: 'edit', targetIndicatorId: this.currentIndicatorDataset.indicatorId }); + this.loadingData = false; + }, + error: (error: any) => { + console.error("Error while updating indicator metadata."); + if (error.data?.message) { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error.data.message); + } else if (error.data) { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error.data); + } else { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error); + } + this.loadingData = false; + } + }); + } + + hideSuccessAlert(): void { + this.successMessagePart = ''; + } + + hideErrorAlert(): void { + this.errorMessagePart = ''; + } + + hideMetadataErrorAlert(): void { + this.indicatorAddMetadataImportErrorAlert = false; + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminRoleManagement/admin-role-management.component.css b/app/components/ngComponents/admin/adminRoleManagement/admin-role-management.component.css new file mode 100644 index 000000000..206d736af --- /dev/null +++ b/app/components/ngComponents/admin/adminRoleManagement/admin-role-management.component.css @@ -0,0 +1,63 @@ +/* Admin Role Management Component Styles */ + +.loading-overlay-admin-panel { + width: 100%; + height: 100%; + position: absolute; + background-color: rgb(128,128,128); + top: 0; + left: 0; + border-radius: 5px; + opacity: 0.8; + z-index: 100000; +} + +.loading-overlay-admin-panel .icon-spin { + font-size: 25px; + width: 25px; + height: 25px; + position: absolute; + top: 50%; + left: 50%; + margin: -12.5px 0 0 -12.5px; + -webkit-animation: spin 1s infinite linear; + -moz-animation: spin 1s infinite linear; + -o-animation: spin 1s infinite linear; + animation: spin 1s infinite linear; + -webkit-transform-origin: 50% 50%; + transform-origin:50% 50%; + -ms-transform-origin:50% 50%; +} + +@-webkit-keyframes spin { + from { -webkit-transform: rotate(0deg); } + to { -webkit-transform: rotate(360deg); } +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.admin-table-wrapper { + margin-top: 20px; +} + +.ag-theme-alpine { + --ag-header-height: 50px; + --ag-row-height: 60px; + --ag-header-background-color: #f4f4f4; + --ag-header-foreground-color: #333; + --ag-border-color: #ddd; +} + +.box-primary { border-top-color: #3c8dbc; } +.box-header { padding: 15px; border-bottom: 1px solid #f4f4f4; } +.box-title { margin: 0; font-size: 18px; font-weight: 600; } +.box-body { padding: 15px; } + +.content-header { padding: 15px 0; background-color: #f9f9f9; border-bottom: 1px solid #ddd; } +.content-header h1 { margin: 0; font-size: 24px; font-weight: 300; } +.content-header h1 small { font-size: 14px; color: #777; } + + diff --git a/app/components/ngComponents/admin/adminRoleManagement/admin-role-management.component.html b/app/components/ngComponents/admin/adminRoleManagement/admin-role-management.component.html new file mode 100644 index 000000000..4db9f1bd9 --- /dev/null +++ b/app/components/ngComponents/admin/adminRoleManagement/admin-role-management.component.html @@ -0,0 +1,88 @@ +
+ +
+
+ +
+
+ +
+

+ Mandanten- und Gruppenverwaltung + Userverwaltung via Keycloak +

+ +
+
+ Nur editierbare Datensätze anzeigen +
+ +
+
+ + +
+
+ +
+
+
+
+
+

Zugriffskontrolle

+
+
+

KomMonitor nutzt die Open Source Software Keycloak zum Nutzer- und Rechtenmanagement.

+

In der Konfiguration ist der Zugriffsschutz jedoch deaktiviert. Es muss zuerst aktiviert und mit einer validen Keycloak-Instanz konfiguriert werden bevor die Zugriffskontrolle verwaltet werden können.

+
+
+
+ +
+
+
+

Mandantenspezifische Gruppenverwaltung

+
+
+ Hinweise und Erläuterungen sind unter Gruppen-/Rechtekonzept zu finden +
+
+ + +
+
+
+
+
+
+
+ + diff --git a/app/components/ngComponents/admin/adminRoleManagement/admin-role-management.component.ts b/app/components/ngComponents/admin/adminRoleManagement/admin-role-management.component.ts new file mode 100644 index 000000000..966250840 --- /dev/null +++ b/app/components/ngComponents/admin/adminRoleManagement/admin-role-management.component.ts @@ -0,0 +1,697 @@ +import { Component, Inject, OnDestroy, OnInit, NgZone, ViewChild } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Subscription } from 'rxjs'; +import { AgGridAngular } from 'ag-grid-angular'; +import { GridOptions, ColDef, GridApi, ColumnApi, FirstDataRenderedEvent, ColumnResizedEvent, RowClickedEvent, GridReadyEvent } from 'ag-grid-community'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; +import { KommonitorDataExchangeService } from 'services/adminSpatialUnit/kommonitor-data-exchange.service'; +import { KommonitorDataGridHelperService } from 'services/adminSpatialUnit/kommonitor-data-grid-helper.service'; +import { RoleDeleteModalComponent } from './roleDeleteModal/role-delete-modal.component'; +import { RoleEditMetadataModalComponent } from './roleEditMetadataModal/role-edit-metadata-modal.component'; +import { KommonitorRolePermissionService } from 'services/adminRoleUnit/kommonitor-role-permission.service'; +import { RoleAddModalComponent } from './roleAddModal/role-add-modal.component'; +import { RoleEditGroupRightsModalComponent } from './roleEditGroupRightsModal/role-edit-group-rights-modal.component'; + +@Component({ + selector: 'admin-role-management-new', + templateUrl: './admin-role-management.component.html', + styleUrls: ['./admin-role-management.component.css'] +}) +export class AdminRoleManagementComponent implements OnInit, OnDestroy { + @ViewChild('accessControlOverviewTable', { static: true }) accessControlOverviewTable!: AgGridAngular; + + public loadingData: boolean = true; + public initializationCompleted: boolean = false; + public tableViewSwitcher: boolean = false; + + // AG Grid properties + public columnDefs: ColDef[] = []; + public rowData: any[] = []; + public defaultColDef: ColDef = {}; + public gridOptions: GridOptions = {}; + private gridApi!: GridApi; + private columnApi!: ColumnApi; + private lastClickedRowData: any | null = null; + + // Pagination properties + public paginationPageSize: number = 10; + public paginationPageSizeSelector: number[] = [10, 25, 50, 100]; + + private subscriptions: Subscription[] = []; + + constructor( + @Inject(DOCUMENT) private document: Document, + private zone: NgZone, + private modalService: NgbModal, + private broadcastService: BroadcastService, + public kommonitorDataExchangeService: KommonitorDataExchangeService, + public kommonitorRolePermissionService: KommonitorRolePermissionService, + private kommonitorDataGridHelperService: KommonitorDataGridHelperService + ) {} + + ngOnInit(): void { + // Removed jQuery usage; AdminLTE boxWidget init skipped + // Subscribe to loading state + const loadingSub = this.kommonitorDataExchangeService.loading$.subscribe(loading => { + this.loadingData = loading; + }); + this.subscriptions.push(loadingSub); + + // Subscribe to access control data + const acSub = this.kommonitorDataExchangeService.accessControl$.subscribe(accessControl => { + if (accessControl && accessControl.length > 0) { + this.loadingData = false; + this.initializationCompleted = true; + this.buildDataGrid_accessControl(accessControl); + } + }); + this.subscriptions.push(acSub); + + // Listen for refresh requests + const bcSub = this.broadcastService.currentBroadcastMsg.subscribe((data: any) => { + if (data.msg === 'initialMetadataLoadingCompleted') { + this.zone.run(() => this.initializeOrRefreshOverviewTable()); + } else if (data.msg === 'refreshAccessControlTable') { + const values: any = data?.values || {}; + this.zone.run(() => this.refreshAccessControlTable(values.crudType, values.targetId)); + } + }); + this.subscriptions.push(bcSub); + + // Initial fetch + this.fetchAccessControlData(); + setTimeout(() => { + if (this.loadingData) { + this.fetchAccessControlData(); + if (!this.kommonitorDataExchangeService.accessControl || this.kommonitorDataExchangeService.accessControl.length === 0) { + this.loadingData = false; + this.initializationCompleted = true; + } + } + }, 2000); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(s => s.unsubscribe()); + } + + public initializeOrRefreshOverviewTable(): void { + this.loadingData = true; + this.fetchAccessControlData(); + } + + private fetchAccessControlData(): void { + this.kommonitorDataExchangeService.fetchAccessControlMetadata().subscribe({ + next: () => { + // handled by accessControl$ subscription + }, + error: () => { + this.loadingData = false; + this.initializationCompleted = true; + } + }); + } + + public refreshAccessControlTable(crudType?: string, targetId?: string | string[]): void { + this.loadingData = true; + if (!crudType || !targetId) { + this.kommonitorDataExchangeService.fetchAccessControlMetadata().subscribe({ + next: () => { + this.initializeOrRefreshOverviewTable(); + this.loadingData = false; + }, + error: () => { + this.loadingData = false; + } + }); + } else if (crudType && targetId) { + if (crudType === 'delete') { + this.initializeOrRefreshOverviewTable(); + this.loadingData = false; + } else { + this.kommonitorDataExchangeService.fetchAccessControlMetadata().subscribe({ + next: () => { + this.initializeOrRefreshOverviewTable(); + this.loadingData = false; + }, + error: () => { + this.loadingData = false; + } + }); + } + } + } + + private buildDataGrid_accessControl(accessControlMetadataArray: any[]): void { + // Build column defs and row data to mirror legacy AngularJS grid + this.columnDefs = this.buildAccessControlColumnDefs(); + const toDisplay = this.getFilteredAccessControl(accessControlMetadataArray); + this.rowData = this.buildAccessControlRowData(toDisplay); + this.defaultColDef = this.kommonitorDataGridHelperService.buildRoleManagementDefaultColDef(); + + this.gridOptions = { + ...this.kommonitorDataGridHelperService.buildRoleManagementGridOptionsPublic(), + columnDefs: this.columnDefs, + rowData: this.rowData, + suppressRowClickSelection: false, + rowSelection: 'multiple', + rowMultiSelectWithClick: true, + paginationPageSize: this.paginationPageSize, + onGridReady: (params) => { + this.gridApi = params.api; + this.columnApi = params.columnApi; + }, + onColumnResized: () => { + this.headerHeightSetter(); + }, + onCellClicked: (event: any) => { + try { + try { console.log('[RoleMgmt] onCellClicked fired', event); } catch {} + const nativeEvent = event && event.event ? event.event : null; + const targetEl = nativeEvent && nativeEvent.target ? (nativeEvent.target as HTMLElement) : null; + if (!targetEl) { try { console.log('[RoleMgmt] onCellClicked: no targetEl'); } catch {}; return; } + const buttonEl = (targetEl as any).closest ? (targetEl as any).closest('button') : null; + if (!buttonEl || !buttonEl.id) { try { console.log('[RoleMgmt] onCellClicked: no button id'); } catch {}; return; } + const id: string = buttonEl.id as string; + try { console.log('[RoleMgmt] onCellClicked: button id', id, 'disabled=', (buttonEl as any).disabled); } catch {} + if ((buttonEl as any).disabled) { return; } + if (id.startsWith('btn_role_editMetadata_')) { + const roleId = id.split('_')[3]; + try { console.log('[RoleMgmt] Opening RoleEditMetadataModalComponent for', roleId); } catch {} + const roleMetadata = this.kommonitorDataExchangeService.getAccessControlById(roleId); + this.zone.run(() => this.onClickEditMetadata(roleMetadata)); + return; + } + if (id.startsWith('btn_role_editGroupRight_')) { + const roleId = id.split('_')[3]; + try { console.log('[RoleMgmt] Opening RoleEditGroupRightsModalComponent for', roleId); } catch {} + const roleMetadata = this.kommonitorDataExchangeService.getAccessControlById(roleId); + this.zone.run(() => this.onClickEditGroupRights(roleMetadata)); + return; + } + } catch {} + } + } as GridOptions; + // Remove invalid/legacy gridOptions properties that cause warnings + try { + if ((this.gridOptions as any).floatingFilter !== undefined) { + delete (this.gridOptions as any).floatingFilter; + console.log('[RoleMgmt] Removed invalid gridOptions.floatingFilter'); + } + } catch {} + } + public onGridReady(event: GridReadyEvent): void { + try { console.log('[RoleMgmt] onGridReady (template)'); } catch {} + this.gridApi = event.api; + this.columnApi = event.columnApi; + } + + private buildAccessControlColumnDefs(): ColDef[] { + const columnDefs: ColDef[] = []; + // Dedicated selection checkbox column + columnDefs.push({ + headerName: '', + pinned: 'left', + maxWidth: 50, + width: 50, + checkboxSelection: true, + headerCheckboxSelection: true, + headerCheckboxSelectionFilteredOnly: true, + filter: false, + sortable: false, + suppressMenu: true, + resizable: false + }); + // Edit buttons column (only enabled if user has creator role for orga) + columnDefs.push({ + headerName: 'Editierfunktionen', + pinned: 'left', + maxWidth: 150, + filter: false, + sortable: false, + cellRenderer: this.displayEditButtons_accessControl.bind(this) + }); + + columnDefs.push( + { + headerName: 'Organisationseinheit', + field: 'name', + pinned: 'left', + minWidth: 250, + cellClass: 'user-roles-normal' + }, + { headerName: 'Hierarchie - übergeordnete Organisationseinheit', field: 'parentName', maxWidth: 250 }, + { + headerName: 'Hierarchie - direkt untergeordnete Organisationseinheiten', + maxWidth: 250, + filter: false, + cellRenderer: (param: any) => { + const count = param?.data?.ownChildGroupsCount || 0; + const names = (param?.data?.ownChildGroupNames || []).join(', '); + return `${count} direkte Untergruppe(n)

${names}`; + } + }, + { headerName: 'Beschreibung', field: 'description', maxWidth: 300 }, + { headerName: 'Kontakt', field: 'contact', maxWidth: 300 }, + { headerName: 'Mandant', field: 'mandant', maxWidth: 125 } + ); + + return columnDefs; + } + + // Toggle handler from template + public onTableViewSwitch(): void { + this.initializeOrRefreshOverviewTable(); + } + + // Filter to only show editable (creator) organizational units when toggled + private getFilteredAccessControl(accessControlMetadataArray: any[]): any[] { + if (!this.tableViewSwitcher) { + return accessControlMetadataArray || []; + } + try { + return (accessControlMetadataArray || []).filter((e: any) => { + const roles: string[] = e?.userAdminRoles || []; + return Array.isArray(roles) && (roles.includes('unit-users-creator') || roles.includes('client-users-creator')); + }); + } catch { + return accessControlMetadataArray || []; + } + } + + private buildAccessControlRowData(dataArray: any[]): any[] { + return (dataArray || []).map((dataItem: any) => { + const parentId = dataItem.parentId; + let parentName = ''; + const parentObject = (dataArray || []).find((item: any) => item.organizationalUnitId === parentId); + if (parentObject && parentObject.name) { + parentName = parentObject.name; + } + dataItem.parentName = parentName; + + dataItem.ownChildGroupsCount = (dataItem.children || []).length; + const organizationalUnitChildrenUnits = (dataItem.children || []) + .map((id: string) => this.kommonitorDataExchangeService.getAccessControlById(id)) + .filter((o: any) => !!o) + .map((o: any) => o.name); + dataItem.ownChildGroupNames = organizationalUnitChildrenUnits; + return dataItem; + }); + } + + private displayEditButtons_accessControl(params: any): string { + const data = params.data; + let html = '
'; + const hasCreator = (data?.userAdminRoles || []).includes('client-users-creator') || (data?.userAdminRoles || []).includes('unit-users-creator'); + html += ''; + html += ''; + html += '
'; + return html; + } + + private registerClickHandler_accessControl(): void { + const $: any = (window as any).$ || (window as any).jQuery || undefined; + + // Remove existing handlers first + if ($) { + $('#accessControlOverviewTable').off('click', '.roleEditMetadataBtn'); + $('#accessControlOverviewTable').off('click', '.roleEditGroupRightsBtn'); + $(document).off('click', '#accessControlOverviewTable .roleEditMetadataBtn'); + $(document).off('click', '#accessControlOverviewTable .roleEditGroupRightsBtn'); + try { console.log('[RoleMgmt] Binding delegated handlers on #accessControlOverviewTable (jQuery)'); } catch {} + } else { + try { console.log('[RoleMgmt] jQuery not found; binding native delegated handlers on #accessControlOverviewTable'); } catch {} + } + + if ($) $('#accessControlOverviewTable').on('click', '.roleEditMetadataBtn', (event: any) => { + try { console.log('[RoleMgmt] Click: .roleEditMetadataBtn (grid container)'); } catch {} + event.stopPropagation(); + event.preventDefault(); + const button = $(event.target).closest('.roleEditMetadataBtn')[0]; + if (!button || (button as any).disabled) { try { console.log('[RoleMgmt] Edit metadata button disabled or missing'); } catch {}; return; } + const id: string = button.id || ''; + const roleId = id.startsWith('btn_role_editMetadata_') ? id.slice('btn_role_editMetadata_'.length) : (id.split('_').pop() || ''); + try { console.log('[RoleMgmt] Resolved roleId from button:', roleId); } catch {} + const roleMetadata = this.kommonitorDataExchangeService.getAccessControlById(roleId); + try { console.log('[RoleMgmt] Resolved roleMetadata:', !!roleMetadata, roleMetadata?.name); } catch {} + this.zone.run(() => this.onClickEditMetadata(roleMetadata)); + }); + + if ($) $('#accessControlOverviewTable').on('click', '.roleEditGroupRightsBtn', (event: any) => { + try { console.log('[RoleMgmt] Click: .roleEditGroupRightsBtn (grid container)'); } catch {} + event.stopPropagation(); + event.preventDefault(); + const button = $(event.target).closest('.roleEditGroupRightsBtn')[0]; + if (!button || (button as any).disabled) { try { console.log('[RoleMgmt] Edit group rights button disabled or missing'); } catch {}; return; } + const id: string = button.id || ''; + const roleId = id.startsWith('btn_role_editGroupRight_') ? id.slice('btn_role_editGroupRight_'.length) : (id.split('_').pop() || ''); + try { console.log('[RoleMgmt] Resolved roleId from button:', roleId); } catch {} + const roleMetadata = this.kommonitorDataExchangeService.getAccessControlById(roleId); + try { console.log('[RoleMgmt] Resolved roleMetadata for group rights:', !!roleMetadata, roleMetadata?.name); } catch {} + this.zone.run(() => this.onClickEditGroupRights(roleMetadata)); + }); + + // Document-level fallback delegation in case events don't bubble to the ag-grid host element + if ($) $(document).on('click', '#accessControlOverviewTable .roleEditMetadataBtn', (event: any) => { + try { console.log('[RoleMgmt] Click: .roleEditMetadataBtn (document fallback)'); } catch {} + event.stopPropagation(); + event.preventDefault(); + const button = $(event.target).closest('.roleEditMetadataBtn')[0]; + if (!button || (button as any).disabled) { return; } + const id: string = button.id || ''; + const roleId = id.startsWith('btn_role_editMetadata_') ? id.slice('btn_role_editMetadata_'.length) : (id.split('_').pop() || ''); + const roleMetadata = this.kommonitorDataExchangeService.getAccessControlById(roleId); + this.zone.run(() => this.onClickEditMetadata(roleMetadata)); + }); + + if ($) $(document).on('click', '#accessControlOverviewTable .roleEditGroupRightsBtn', (event: any) => { + try { console.log('[RoleMgmt] Click: .roleEditGroupRightsBtn (document fallback)'); } catch {} + event.stopPropagation(); + event.preventDefault(); + const button = $(event.target).closest('.roleEditGroupRightsBtn')[0]; + if (!button || (button as any).disabled) { return; } + const id: string = button.id || ''; + const roleId = id.startsWith('btn_role_editGroupRight_') ? id.slice('btn_role_editGroupRight_'.length) : (id.split('_').pop() || ''); + const roleMetadata = this.kommonitorDataExchangeService.getAccessControlById(roleId); + this.zone.run(() => this.onClickEditGroupRights(roleMetadata)); + }); + + // Native delegated handler fallback (works without jQuery) + const hostEl = document.getElementById('accessControlOverviewTable'); + if (hostEl) { + hostEl.addEventListener('click', (evt: Event) => { + const target = evt.target as HTMLElement; + if (!target) { return; } + const metaBtn = target.closest('.roleEditMetadataBtn') as HTMLElement | null; + if (metaBtn) { + try { console.log('[RoleMgmt] (native) Click: .roleEditMetadataBtn'); } catch {} + evt.stopPropagation(); + evt.preventDefault(); + if ((metaBtn as any).disabled) { return; } + const id: string = metaBtn.id || ''; + const roleId = id.startsWith('btn_role_editMetadata_') ? id.slice('btn_role_editMetadata_'.length) : (id.split('_').pop() || ''); + try { console.log('[RoleMgmt] (native) Resolved roleId:', roleId); } catch {} + const roleMetadata = this.kommonitorDataExchangeService.getAccessControlById(roleId); + try { console.log('[RoleMgmt] (native) Resolved roleMetadata:', !!roleMetadata, roleMetadata?.name); } catch {} + this.zone.run(() => this.onClickEditMetadata(roleMetadata)); + return; + } + const rightsBtn = target.closest('.roleEditGroupRightsBtn') as HTMLElement | null; + if (rightsBtn) { + try { console.log('[RoleMgmt] (native) Click: .roleEditGroupRightsBtn'); } catch {} + evt.stopPropagation(); + evt.preventDefault(); + if ((rightsBtn as any).disabled) { return; } + const id: string = rightsBtn.id || ''; + const roleId = id.startsWith('btn_role_editGroupRight_') ? id.slice('btn_role_editGroupRight_'.length) : (id.split('_').pop() || ''); + try { console.log('[RoleMgmt] (native) Resolved roleId:', roleId); } catch {} + const roleMetadata = this.kommonitorDataExchangeService.getAccessControlById(roleId); + try { console.log('[RoleMgmt] (native) Resolved roleMetadata:', !!roleMetadata, roleMetadata?.name); } catch {} + this.zone.run(() => this.onClickEditGroupRights(roleMetadata)); + } + }); + } + + // Add a global native delegated handler as a final fallback (no jQuery required) + const globalFlag = '__roleMgmtDocClickHandlersBound'; + const w = window as any; + if (!w[globalFlag]) { + try { console.log('[RoleMgmt] Binding global document-level native delegated handlers'); } catch {} + document.addEventListener('click', (evt: Event) => { + const target = evt.target as HTMLElement | null; + if (!target) { return; } + const metaBtn = target.closest('.roleEditMetadataBtn') as HTMLElement | null; + if (metaBtn) { + evt.stopPropagation(); + evt.preventDefault(); + if ((metaBtn as any).disabled) { return; } + const id: string = metaBtn.id || ''; + const roleId = id.startsWith('btn_role_editMetadata_') ? id.slice('btn_role_editMetadata_'.length) : (id.split('_').pop() || ''); + const roleMetadata = this.kommonitorDataExchangeService.getAccessControlById(roleId); + this.zone.run(() => this.onClickEditMetadata(roleMetadata)); + return; + } + const rightsBtn = target.closest('.roleEditGroupRightsBtn') as HTMLElement | null; + if (rightsBtn) { + evt.stopPropagation(); + evt.preventDefault(); + if ((rightsBtn as any).disabled) { return; } + const id: string = rightsBtn.id || ''; + const roleId = id.startsWith('btn_role_editGroupRight_') ? id.slice('btn_role_editGroupRight_'.length) : (id.split('_').pop() || ''); + const roleMetadata = this.kommonitorDataExchangeService.getAccessControlById(roleId); + this.zone.run(() => this.onClickEditGroupRights(roleMetadata)); + } + }); + w[globalFlag] = true; + } + + // Capture-phase native delegated handler as a safeguard if bubbling is prevented by AG Grid internals + const captureFlag = '__roleMgmtDocClickHandlersCaptureBound'; + if (!w[captureFlag]) { + try { console.log('[RoleMgmt] Binding global document-level native delegated handlers (capture phase)'); } catch {} + document.addEventListener('click', (evt: Event) => { + const target = evt.target as HTMLElement | null; + if (!target) { return; } + const metaBtn = target.closest('.roleEditMetadataBtn') as HTMLElement | null; + if (metaBtn) { + evt.stopPropagation(); + evt.preventDefault(); + if ((metaBtn as any).disabled) { return; } + const id: string = metaBtn.id || ''; + const roleId = id.startsWith('btn_role_editMetadata_') ? id.slice('btn_role_editMetadata_'.length) : (id.split('_').pop() || ''); + const roleMetadata = this.kommonitorDataExchangeService.getAccessControlById(roleId); + this.zone.run(() => this.onClickEditMetadata(roleMetadata)); + return; + } + const rightsBtn = target.closest('.roleEditGroupRightsBtn') as HTMLElement | null; + if (rightsBtn) { + evt.stopPropagation(); + evt.preventDefault(); + if ((rightsBtn as any).disabled) { return; } + const id: string = rightsBtn.id || ''; + const roleId = id.startsWith('btn_role_editGroupRight_') ? id.slice('btn_role_editGroupRight_'.length) : (id.split('_').pop() || ''); + const roleMetadata = this.kommonitorDataExchangeService.getAccessControlById(roleId); + this.zone.run(() => this.onClickEditGroupRights(roleMetadata)); + } + }, { capture: true }); + w[captureFlag] = true; + } + } + + public onClickDeleteDatasets(): void { + try { console.log('[RoleMgmt] Delete clicked'); } catch {} + let selectedDatasets: any[] = []; + try { + if (this.gridApi) { + const byRows = (this.gridApi as any).getSelectedRows ? (this.gridApi as any).getSelectedRows() : []; + if (byRows && byRows.length > 0) { + selectedDatasets = byRows; + try { console.log('[RoleMgmt] Selected rows', byRows.length); } catch {} + } else { + const selectedNodes = this.gridApi.getSelectedNodes ? this.gridApi.getSelectedNodes() : []; + try { console.log('[RoleMgmt] Selected nodes', selectedNodes.length); } catch {} + selectedDatasets = (selectedNodes || []).map((node: any) => node.data).filter(Boolean); + } + // If nothing is selected, try to use the focused row + if ((!selectedDatasets || selectedDatasets.length === 0)) { + const focused = this.gridApi.getFocusedCell && this.gridApi.getFocusedCell(); + try { console.log('[RoleMgmt] Focused cell', focused); } catch {} + if (focused && typeof focused.rowIndex === 'number') { + const rowNode = this.gridApi.getDisplayedRowAtIndex && this.gridApi.getDisplayedRowAtIndex(focused.rowIndex); + if (rowNode && rowNode.data) { + selectedDatasets = [rowNode.data]; + } + } + } + // As another fallback, scan nodes for isSelected + if ((!selectedDatasets || selectedDatasets.length === 0) && (this.gridApi as any).forEachNode) { + const tmp: any[] = []; + (this.gridApi as any).forEachNode((node: any) => { if (node && node.isSelected && node.isSelected()) { tmp.push(node.data); } }); + if (tmp.length > 0) { selectedDatasets = tmp; } + } + // Final fallback: read from DOM selected rows and map back by displayed index + if ((!selectedDatasets || selectedDatasets.length === 0)) { + try { + const host = document.getElementById('accessControlOverviewTable'); + if (host) { + const selectedRowEls = host.querySelectorAll('.ag-row.ag-row-selected, .ag-row[aria-selected="true"]'); + const domSelected: any[] = []; + selectedRowEls.forEach((el: Element) => { + const asAny = el as any; + const rowIndexAttr = (asAny.getAttribute && (asAny.getAttribute('row-index') || asAny.getAttribute('row-id') || asAny.getAttribute('data-row-index'))) || null; + const idx = rowIndexAttr !== null ? parseInt(rowIndexAttr, 10) : NaN; + if (!Number.isNaN(idx) && (this.gridApi as any).getDisplayedRowAtIndex) { + const rowNode = (this.gridApi as any).getDisplayedRowAtIndex(idx); + if (rowNode && rowNode.data) { + domSelected.push(rowNode.data); + } + } + }); + if (domSelected.length > 0) { + selectedDatasets = domSelected; + try { console.log('[RoleMgmt] DOM-based selected rows', domSelected.length); } catch {} + } + } + } catch {} + } + } + } catch {} + // Fallback: if no selection was registered, but a row was clicked, use it + if ((!selectedDatasets || selectedDatasets.length === 0) && this.lastClickedRowData) { + try { console.log('[RoleMgmt] Using last clicked row fallback'); } catch {} + selectedDatasets = [this.lastClickedRowData]; + } + // Normalize to full access control records + if (selectedDatasets && selectedDatasets.length > 0) { + try { + selectedDatasets = (selectedDatasets || []) + .map((d: any) => d && d.organizationalUnitId ? (this.kommonitorDataExchangeService.getAccessControlById(d.organizationalUnitId) || d) : d) + .filter(Boolean); + console.log('[RoleMgmt] Normalized datasets to access control records', selectedDatasets.length); + } catch {} + } + + // Always open modal, even with zero selection (legacy behavior shows empty message in modal) + try { + console.log('[RoleMgmt] Datasets to delete', (selectedDatasets || []).map(d => `${d?.name} (${d?.organizationalUnitId})`)); + } catch {} + const modalRef = this.modalService.open(RoleDeleteModalComponent, { + backdrop: true, + keyboard: false, + container: 'body', + animation: false, + modalDialogClass: 'role-add-modal', + windowClass: 'role-add-modal-window' + }); + try { console.log('[RoleMgmt] Opened RoleDeleteModalComponent'); } catch {} + // Pass datasets via input and also call the initializer for compatibility + if ((modalRef as any).componentInstance) { + (modalRef as any).componentInstance.initialDatasets = selectedDatasets || []; + if (typeof (modalRef as any).componentInstance.onDeleteOrganizationalUnit === 'function') { + setTimeout(() => { + try { console.log('[RoleMgmt] Passing datasets to modal', selectedDatasets?.length || 0); } catch {} + (modalRef as any).componentInstance.onDeleteOrganizationalUnit(selectedDatasets || []); + }, 0); + } + } + modalRef.result.then(() => { + this.initializeOrRefreshOverviewTable(); + }).catch(() => {}); + try { console.log('[RoleMgmt] Modal opened and datasets passed, leaving loadingData unchanged'); } catch {} + } + + public onClickCreateRole(): void { + this.modalService.open(RoleAddModalComponent, { + // omit size to avoid Bootstrap max-width caps + backdrop: true, + keyboard: false, + container: 'body', + animation: false, + modalDialogClass: 'role-add-modal', + windowClass: 'role-add-modal-window' + }).result.then(() => { + this.initializeOrRefreshOverviewTable(); + }).catch(() => {}); + } + + // Aliases and shared modal openers to mirror spatial units flow + public openAddRoleModal(): void { + this.onClickCreateRole(); + } + + public onClickEditMetadata(organizationalUnit: any): void { + try { console.log('[RoleMgmt] onClickEditMetadata invoked for', organizationalUnit?.organizationalUnitId, organizationalUnit?.name); } catch {} + const modalRef = this.modalService.open(RoleEditMetadataModalComponent, { + backdrop: true, + keyboard: false, + container: 'body', + animation: false, + modalDialogClass: 'role-add-modal', + windowClass: 'role-add-modal-window' + }); + try { console.log('[RoleMgmt] RoleEditMetadataModalComponent opened'); } catch {} + const parentUnit = organizationalUnit && organizationalUnit.parentId ? this.kommonitorDataExchangeService.getAccessControlById(organizationalUnit.parentId) : null; + (modalRef as any).componentInstance.current = { ...(organizationalUnit || {}) }; + (modalRef as any).componentInstance.old = { name: organizationalUnit && organizationalUnit.name }; + (modalRef as any).componentInstance.parentOrganizationalUnit = parentUnit || null; + setTimeout(() => { + if (typeof (modalRef as any).componentInstance.resetRoleEditMetadataForm === 'function') { + try { console.log('[RoleMgmt] Calling resetRoleEditMetadataForm on modal'); } catch {} + (modalRef as any).componentInstance.resetRoleEditMetadataForm(); + } + }, 0); + modalRef.result.then(() => { + try { console.log('[RoleMgmt] RoleEditMetadataModal closed with success; refreshing table'); } catch {} + this.initializeOrRefreshOverviewTable(); + }).catch(() => {}); + } + + public onClickEditGroupRights(organizationalUnit: any): void { + try { console.log('[RoleMgmt] onClickEditGroupRights invoked for', organizationalUnit?.organizationalUnitId, organizationalUnit?.name); } catch {} + const modalRef = this.modalService.open(RoleEditGroupRightsModalComponent, { + backdrop: true, + keyboard: false, + container: 'body', + animation: false, + modalDialogClass: 'role-add-modal', + windowClass: 'role-add-modal-window' + }); + try { console.log('[RoleMgmt] RoleEditGroupRightsModal opened'); } catch {} + // Pass data via component instance (no broadcast) + (modalRef as any).componentInstance.current = { ...(organizationalUnit || {}) }; + modalRef.result.then(() => { + try { console.log('[RoleMgmt] RoleEditGroupRightsModal closed with success; refreshing table'); } catch {} + this.initializeOrRefreshOverviewTable(); + }).catch(() => {}); + } + + // Grid event handlers for template usage if needed + public onFirstDataRendered(event: FirstDataRenderedEvent): void { + this.headerHeightSetter(); + // Ensure delegated click handlers are bound after grid render (mirrors spatial units component) + this.registerClickHandler_accessControl(); + } + + public onColumnResized(event: ColumnResizedEvent): void { + this.headerHeightSetter(); + } + + public headerHeightSetter(): void { + if (this.gridApi) { + const headerHeight = this.headerHeightGetter(); + try { + if ((this.gridApi as any).setGridOption) { + (this.gridApi as any).setGridOption('headerHeight', headerHeight); + } else if ((this.gridApi as any).updateGridOptions) { + (this.gridApi as any).updateGridOptions({ headerHeight }); + } else if ((this.gridApi as any).setHeaderHeight) { + // Fallback for older versions + (this.gridApi as any).setHeaderHeight(headerHeight); + } + } catch {} + } + } + + public onRowClicked(event: RowClickedEvent): void { + this.lastClickedRowData = event?.data || null; + } + + private headerHeightGetter(): number { + const headerElement = document.querySelector('.ag-header'); + if (headerElement) { + const headerTextElements = headerElement.querySelectorAll('.ag-header-cell-text'); + let maxHeight = 0; + headerTextElements.forEach(element => { + const height = (element as HTMLElement).scrollHeight; + if (height > maxHeight) { + maxHeight = height; + } + }); + return Math.max(maxHeight + 20, 50); + } + return 50; + } +} + + diff --git a/app/components/ngComponents/admin/adminRoleManagement/roleAddModal/role-add-modal.component.css b/app/components/ngComponents/admin/adminRoleManagement/roleAddModal/role-add-modal.component.css new file mode 100644 index 000000000..128890155 --- /dev/null +++ b/app/components/ngComponents/admin/adminRoleManagement/roleAddModal/role-add-modal.component.css @@ -0,0 +1,40 @@ +.loading-overlay-admin-panel { + width: 100%; + height: 100%; + position: absolute; + background-color: rgb(128,128,128); + top: 0; + left: 0; + border-radius: 5px; + opacity: 0.8; + z-index: 100000; +} + +.loading-overlay-admin-panel .icon-spin { + font-size: 25px; + width: 25px; + height: 25px; + position: absolute; + top: 50%; + left: 50%; + margin: -12.5px 0 0 -12.5px; + -webkit-animation: spin 1s infinite linear; + -moz-animation: spin 1s infinite linear; + -o-animation: spin 1s infinite linear; + animation: spin 1s infinite linear; + -webkit-transform-origin: 50% 50%; + transform-origin:50% 50%; + -ms-transform-origin:50% 50%; +} + +@-webkit-keyframes spin { + from { -webkit-transform: rotate(0deg); } + to { -webkit-transform: rotate(360deg); } +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + + diff --git a/app/components/ngComponents/admin/adminRoleManagement/roleAddModal/role-add-modal.component.html b/app/components/ngComponents/admin/adminRoleManagement/roleAddModal/role-add-modal.component.html new file mode 100644 index 000000000..cdb192adf --- /dev/null +++ b/app/components/ngComponents/admin/adminRoleManagement/roleAddModal/role-add-modal.component.html @@ -0,0 +1,168 @@ + + + + + diff --git a/app/components/ngComponents/admin/adminRoleManagement/roleAddModal/role-add-modal.component.ts b/app/components/ngComponents/admin/adminRoleManagement/roleAddModal/role-add-modal.component.ts new file mode 100644 index 000000000..3d4b622a5 --- /dev/null +++ b/app/components/ngComponents/admin/adminRoleManagement/roleAddModal/role-add-modal.component.ts @@ -0,0 +1,404 @@ +import { Component } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { HttpClient } from '@angular/common/http'; +import { KommonitorDataExchangeService } from 'services/adminSpatialUnit/kommonitor-data-exchange.service'; +import { KommonitorRoleDataExchangeService } from 'services/adminRoleUnit/kommonitor-role-data-exchange.service'; +import { KommonitorRoleKeycloakHelperService } from 'services/adminRoleUnit/kommonitor-role-keycloak-helper.service'; +import { KommonitorRoleDataGridHelperService } from 'services/adminRoleUnit/kommonitor-role-data-grid-helper.service'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; + +@Component({ + selector: 'app-role-add-modal', + templateUrl: './role-add-modal.component.html', + styleUrls: ['./role-add-modal.component.css'] +}) +export class RoleAddModalComponent { + loadingData: boolean = false; + currentStep: number = 0; + + newOrganizationalUnit: any = { + name: '', + description: '', + contact: '', + mandant: false, + parentId: '' + }; + + nameInvalid: boolean = false; + errorMessagePart: string | undefined = undefined; + keycloakErrorMessagePart: string | undefined = undefined; + + infoExpanded: boolean = false; + + parentOrganizationalUnitFilter: string = ''; + parentOrganizationalUnit: any = null; + + // Alerts/state flags + unitAddSuccess: boolean = false; + roleDelegatesSuccess: boolean = false; + keycloakGroupAddSuccess: boolean = false; + unitAddError: string | undefined = undefined; + roleDelegatesError: string | undefined = undefined; + + constructor( + public activeModal: NgbActiveModal, + private http: HttpClient, + public kommonitorDataExchangeService: KommonitorDataExchangeService, + private roleDataExchange: KommonitorRoleDataExchangeService, + private roleKeycloakHelper: KommonitorRoleKeycloakHelperService, + public roleDataGridHelper: KommonitorRoleDataGridHelperService, + private broadcastService: BroadcastService + ) {} + + private async resolveCreatedOuIdByName(name: string, maxAttempts: number = 6, delayMs: number = 500): Promise { + for (let attempt = 0; attempt < maxAttempts; attempt++) { + try { + await this.kommonitorDataExchangeService.fetchAccessControlMetadata().toPromise(); + const created = (this.kommonitorDataExchangeService.accessControl || []).find(e => e.name === name); + if (created?.organizationalUnitId) { + return created.organizationalUnitId; + } + } catch (_) { /* noop */ } + await new Promise(res => setTimeout(res, delayMs)); + } + return null; + } + + get accessControlList(): any[] { + return this.kommonitorDataExchangeService.accessControl || []; + } + + goToStep(step: number): void { + if (step < 0) { + this.currentStep = 0; + return; + } + if (step > 1) { + this.currentStep = 1; + return; + } + this.currentStep = step; + + // Build grid when entering step 2 + if (this.currentStep === 1) { + this.buildDelegatedRolesTable(); + } + } + + nextStep(): void { + this.goToStep(this.currentStep + 1); + } + + prevStep(): void { + this.goToStep(this.currentStep - 1); + } + + toggleInfo(): void { + this.infoExpanded = !this.infoExpanded; + } + + // --- ag-Grid (delegated roles) --- + delegatedRoleManagementTableOptions: any = undefined; + delegatedColumnDefs: any[] = []; + delegatedRowData: any[] = []; + delegatedDefaultColDef: any = {}; + delegatedGridOptions: any = {}; + access: any[] = []; + + private extendAccessWithAdvancedRoles(access: any[]): any[] { + const permissionStrings = [ + 'unit-users-creator', + 'client-users-creator', + 'unit-resources-creator', + 'client-resources-creator', + 'unit-themes-creator', + 'client-themes-creator' + ]; + (access || []).forEach((elem: any) => { + elem.permissions = elem.permissions || []; + permissionStrings.forEach(role => { + elem.permissions.push({ + permissionLevel: role, + permissionId: `${elem.organizationalUnitId}-${role}` + }); + }); + }); + return access; + } + + private buildDelegatedRolesTable(): void { + // Ensure access control is available + this.access = this.kommonitorDataExchangeService.accessControl || []; + if (!this.access || this.access.length === 0) { + this.kommonitorDataExchangeService.fetchAccessControlMetadata().subscribe({ + next: () => { + this.access = this.kommonitorDataExchangeService.accessControl || []; + this.access = this.extendAccessWithAdvancedRoles(this.access); + this.initializeDelegatedGrid(); + }, + error: () => { + // ignore, grid will stay empty + } + }); + return; + } + + this.access = this.extendAccessWithAdvancedRoles(this.access); + this.initializeDelegatedGrid(); + } + + private initializeDelegatedGrid(): void { + this.delegatedRoleManagementTableOptions = this.roleDataGridHelper.buildAdvancedRoleManagementGrid( + 'addRoleEditGroupRoleManagementTable', + this.delegatedRoleManagementTableOptions, + this.access, + [] + ); + + if (this.delegatedRoleManagementTableOptions) { + // Ensure column headers match AngularJS advanced version + // Columns: Organisationseinheit | Verwalten von Nutzern (Dieser Gruppe | Untergruppen) + // | Verwalten von Resourcen (Dieser Gruppe | Untergruppen) + // | Verwalten von Themen (Dieser Gruppe | Untergruppen) + this.delegatedColumnDefs = [ + { + headerName: 'Organisationseinheit', + field: 'name', + minWidth: 200, + }, + { + headerName: 'Verwalten von Nutzern', + field: 'permissions', + filter: false, + sortable: false, + children: [ + { + headerName: 'Dieser Gruppe', + field: 'permissions', + filter: false, + sortable: false, + width: 120, + cellRenderer: 'CheckboxRenderer_UM_group' + }, + { + headerName: 'Untergruppen', + field: 'permissions', + filter: false, + sortable: false, + width: 120, + cellRenderer: 'CheckboxRenderer_UM_subGroup' + } + ] + }, + { + headerName: 'Verwalten von Resourcen', + field: 'permissions', + filter: false, + sortable: false, + children: [ + { + headerName: 'Dieser Gruppe', + field: 'permissions', + filter: false, + sortable: false, + width: 120, + cellRenderer: 'CheckboxRenderer_RM_group' + }, + { + headerName: 'Untergruppen', + field: 'permissions', + filter: false, + sortable: false, + width: 120, + cellRenderer: 'CheckboxRenderer_RM_subGroup' + } + ] + }, + { + headerName: 'Verwalten von Themen', + field: 'permissions', + filter: false, + sortable: false, + children: [ + { + headerName: 'Dieser Gruppe', + field: 'permissions', + filter: false, + sortable: false, + width: 120, + cellRenderer: 'CheckboxRenderer_TM_group' + }, + { + headerName: 'Untergruppen', + field: 'permissions', + filter: false, + sortable: false, + width: 120, + cellRenderer: 'CheckboxRenderer_TM_subGroup' + } + ] + } + ]; + this.delegatedRowData = this.delegatedRoleManagementTableOptions.rowData || []; + this.delegatedDefaultColDef = { + ...this.roleDataGridHelper.buildRoleManagementDefaultColDef(), + filter: true, + floatingFilter: true + }; + const base = this.roleDataGridHelper.buildRoleManagementGridOptionsPublic(this.roleDataGridHelper.getAdvancedRoleManagementGridComponents()); + this.delegatedGridOptions = { + ...base, + onGridReady: (params: any) => { + this.roleDataGridHelper.setGridApi(params.api); + } + }; + } + } + + onChangeParentOrganizationalUnit(ou: any): void { + this.parentOrganizationalUnit = ou; + this.newOrganizationalUnit.parentId = ou ? ou.organizationalUnitId : ''; + } + + checkRoleName(): void { + const accessControl = this.kommonitorDataExchangeService.accessControl || []; + this.nameInvalid = !!accessControl.some(ou => ou.name === this.newOrganizationalUnit.name); + } + + resetRoleAddForm(): void { + this.newOrganizationalUnit = { + name: '', + description: '', + contact: '', + mandant: false, + parentId: '' + }; + this.parentOrganizationalUnit = null; + this.errorMessagePart = undefined; + this.keycloakErrorMessagePart = undefined; + this.checkRoleName(); + } + + async addRole(): Promise { + this.errorMessagePart = undefined; + this.keycloakErrorMessagePart = undefined; + this.unitAddError = undefined; + this.roleDelegatesError = undefined; + this.unitAddSuccess = false; + this.roleDelegatesSuccess = false; + this.keycloakGroupAddSuccess = false; + + try { + const postBody: any = { + name: this.newOrganizationalUnit.name, + description: this.newOrganizationalUnit.description, + contact: this.newOrganizationalUnit.contact, + mandant: !!this.newOrganizationalUnit.mandant, + parentId: this.newOrganizationalUnit.parentId ? this.newOrganizationalUnit.parentId : null + }; + + this.loadingData = true; + + const response: any = await this.roleDataExchange.createOrganizationalUnit(postBody).toPromise(); + this.unitAddSuccess = true; + + try { + await this.kommonitorDataExchangeService.fetchAccessControlMetadata().toPromise(); + const created = (this.kommonitorDataExchangeService.accessControl || []).find(e => e.name === this.newOrganizationalUnit.name); + + // Attempt to create Keycloak group and associated roles (best-effort) + try { + const parent = this.parentOrganizationalUnit || null; + const organizationalUnitForKeycloak = { + ...created, + mandant: !!this.newOrganizationalUnit.mandant + }; + await this.roleKeycloakHelper.postNewGroup(organizationalUnitForKeycloak, parent); + await this.roleKeycloakHelper.fetchAndSetKeycloakRoles(); + this.keycloakGroupAddSuccess = true; + } catch (kcError: any) { + this.keycloakErrorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(kcError?.data || kcError); + } + + // After creating the OU, build delegated role PUT body from selected checkboxes (advanced grid) + try { + const createdOu = created; + let createdOuId = response?.organizationalUnitId || createdOu?.organizationalUnitId; + if (!createdOuId) { + createdOuId = await this.resolveCreatedOuIdByName(this.newOrganizationalUnit.name); + } + const permissionIdList: string[] = this.roleDataGridHelper.getSelectedRoleIds_roleManagementGrid(this.delegatedRoleManagementTableOptions); + + const unitToRoles: Record = {}; + for (const id of permissionIdList || []) { + const parts = (id || '').split('-'); + if (parts.length < 6) { continue; } + const unitId = parts.slice(0, 5).join('-'); + const role = parts.slice(5).join('-'); + if (!unitToRoles[unitId]) { unitToRoles[unitId] = []; } + if (!unitToRoles[unitId].includes(role)) { unitToRoles[unitId].push(role); } + } + + const putBody: any[] = []; + const access = this.kommonitorDataExchangeService.accessControl || []; + Object.keys(unitToRoles).forEach(unitId => { + const orgUnit = access.find((e: any) => e.organizationalUnitId === unitId); + if (orgUnit) { + putBody.push({ + organizationalUnitId: unitId, + organizationalUnitName: orgUnit.name, + keycloakId: orgUnit.keycloakId, + adminRoles: unitToRoles[unitId] + }); + } + }); + + // Always send PUT like AngularJS version, even when no roles are selected + if (createdOuId) { + this.http.put( + `${this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI}/organizationalUnits/${createdOuId}/role-delegates`, + putBody + ).subscribe({ + next: () => { + this.roleDelegatesSuccess = true; + this.broadcastService.broadcast('refreshAccessControlTable', { crudType: 'add', targetId: createdOuId }); + }, + error: (err) => { + this.roleDelegatesError = this.kommonitorDataExchangeService.syntaxHighlightJSON(err?.error || err); + } + }); + } + } catch (e) { + // non-fatal; continue + } + + // Broadcast refresh to overview table + this.broadcastService.broadcast('refreshAccessControlTable', { crudType: 'add', targetId: created?.organizationalUnitId }); + } catch (refreshError) { + // ignore + } + + this.loadingData = false; + // do not auto-close; show alerts instead + } catch (error: any) { + if (error && error.error) { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error.error); + } else { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error); + } + this.unitAddError = 'failed'; + this.loadingData = false; + } + finally { + this.loadingData = false; + } + } + + canSubmit(): boolean { + const hasBasics = !!this.newOrganizationalUnit.name && !!this.newOrganizationalUnit.description && !!this.newOrganizationalUnit.contact; + const parentOk = this.newOrganizationalUnit.mandant ? !this.newOrganizationalUnit.parentId : true; + return hasBasics && parentOk && !this.nameInvalid; + } +} + + diff --git a/app/components/ngComponents/admin/adminRoleManagement/roleDeleteModal/role-delete-modal.component.css b/app/components/ngComponents/admin/adminRoleManagement/roleDeleteModal/role-delete-modal.component.css new file mode 100644 index 000000000..5676c5e55 --- /dev/null +++ b/app/components/ngComponents/admin/adminRoleManagement/roleDeleteModal/role-delete-modal.component.css @@ -0,0 +1,49 @@ +.loading-overlay-admin-panel { + width: 100%; + height: 100%; + position: absolute; + background-color: rgb(128,128,128); + top: 0; + left: 0; + border-radius: 5px; + opacity: 0.8; + z-index: 100000; +} + +.loading-overlay-admin-panel .icon-spin { + font-size: 25px; + width: 25px; + height: 25px; + position: absolute; + top: 50%; + left: 50%; + margin: -12.5px 0 0 -12.5px; + -webkit-animation: spin 1s infinite linear; + -moz-animation: spin 1s infinite linear; + -o-animation: spin 1s infinite linear; + animation: spin 1s infinite linear; + -webkit-transform-origin: 50% 50%; + transform-origin:50% 50%; + -ms-transform-origin:50% 50%; +} + +@-webkit-keyframes spin { + from { -webkit-transform: rotate(0deg); } + to { -webkit-transform: rotate(360deg); } +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.organizationalUnit-subGroup-item { + color: #3c8dbc; +} + +.spatial-unit-list-item { + display: inline-block; + margin-right: 8px; +} + + diff --git a/app/components/ngComponents/admin/adminRoleManagement/roleDeleteModal/role-delete-modal.component.html b/app/components/ngComponents/admin/adminRoleManagement/roleDeleteModal/role-delete-modal.component.html new file mode 100644 index 000000000..a5e8e8b78 --- /dev/null +++ b/app/components/ngComponents/admin/adminRoleManagement/roleDeleteModal/role-delete-modal.component.html @@ -0,0 +1,172 @@ + + + + +
+ +

Folgende Organisationseinheiten wurde erfolgreich gelöscht

+
    +
  • {{dataset.name}}
  • +
+
+ +
+ +

Löschen fehlgeschlagen

+ Folgende Datensätze konnten nicht gelöscht werden. +
+ + + + + + + + + + + + + +
NameFehlermeldung
{{dataset[0]?.name}}
+
+ + diff --git a/app/components/ngComponents/admin/adminRoleManagement/roleDeleteModal/role-delete-modal.component.ts b/app/components/ngComponents/admin/adminRoleManagement/roleDeleteModal/role-delete-modal.component.ts new file mode 100644 index 000000000..5b3150d48 --- /dev/null +++ b/app/components/ngComponents/admin/adminRoleManagement/roleDeleteModal/role-delete-modal.component.ts @@ -0,0 +1,321 @@ +import { Component, OnDestroy, OnInit, Inject, Input } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { HttpClient } from '@angular/common/http'; +import { Subscription, forkJoin, of } from 'rxjs'; +import { catchError, tap } from 'rxjs/operators'; + +@Component({ + selector: 'role-delete-modal-new', + templateUrl: './role-delete-modal.component.html', + styleUrls: ['./role-delete-modal.component.css'] +}) +export class RoleDeleteModalComponent implements OnInit, OnDestroy { + + @Input() initialDatasets: any[] = []; + + elementsToDelete: any[] = []; + loadingData: boolean = false; + + successfullyDeletedDatasets: any[] = []; + failedDatasetsAndErrors: [any, string][] = []; + + affectedSpatialUnits: any[] = []; + affectedGeoresources: any[] = []; + affectedIndicators: any[] = []; + + organizationalChildrenEffected: boolean = false; + + showSuccessAlert: boolean = false; + showErrorAlert: boolean = false; + + private subscriptions: Subscription[] = []; + + constructor( + public activeModal: NgbActiveModal, + @Inject('kommonitorDataExchangeService') public kommonitorDataExchangeService: any, + private http: HttpClient + ) {} + + ngOnInit(): void { + if (this.initialDatasets && this.initialDatasets.length > 0) { + this.onDeleteOrganizationalUnit(this.initialDatasets); + } else if (this.elementsToDelete && this.elementsToDelete.length > 0) { + this.onDeleteOrganizationalUnit(this.elementsToDelete); + } + } + + ngOnDestroy(): void { + this.subscriptions.forEach(s => s.unsubscribe()); + } + + onDeleteOrganizationalUnit(datasets: any[]): void { + try { console.debug('[RoleDeleteModal] Init delete with datasets', (datasets || []).map(d => d?.organizationalUnitId)); } catch {} + this.resetRolesDeleteForm(); + // include children like legacy AngularJS behavior + const withChildren = this.fetchOrganizationalChildren(datasets || []); + // Filter out system orgs like legacy behavior + const originalSize = withChildren.length; + const filtered = (withChildren || []).filter((org: any) => org.name !== 'public' && org.name !== 'kommonitor'); + this.elementsToDelete = filtered; + try { console.debug('[RoleDeleteModal] elementsToDelete', this.elementsToDelete.map(d => d?.organizationalUnitId)); } catch {} + if (filtered.length < originalSize) { + this.failedDatasetsAndErrors.push([{ name: 'public / kommonitor' }, 'System Organisationseinheiten können nicht gelöscht werden! Die betroffene Einheit wurde aus der Liste entfernt.']); + this.showErrorAlert = true; + } + + this.affectedSpatialUnits = this.gatherAffectedSpatialUnits(); + this.affectedGeoresources = this.gatherAffectedGeoresources(); + this.affectedIndicators = this.gatherAffectedIndicators(); + + // keep flag consistent in case no children were added but parent has any + this.organizationalChildrenEffected = this.organizationalChildrenEffected || this.hasOrganizationalChildren(); + try { console.debug('[RoleDeleteModal] affected counts', { su: this.affectedSpatialUnits.length, gr: this.affectedGeoresources.length, ind: this.affectedIndicators.length }); } catch {} + } + + private fetchOrganizationalChildren(datasets: any[]): any[] { + try { + this.organizationalChildrenEffected = false; + const accessControl: any[] = this.kommonitorDataExchangeService.accessControl || []; + const result: any[] = [...(datasets || [])]; + const selectedIds = new Set(result.map(e => e?.organizationalUnitId).filter(Boolean)); + // iterate over a snapshot of current result to avoid infinite loop while pushing + const parentsSnapshot = [...result]; + for (const parent of parentsSnapshot) { + const children: string[] = (parent && parent.children) ? parent.children : []; + for (const childId of children) { + const child = accessControl.find((e: any) => e && e.organizationalUnitId === childId); + if (child && !selectedIds.has(child.organizationalUnitId)) { + const childWithFlag = { ...child, subGroup: true }; + result.push(childWithFlag); + selectedIds.add(child.organizationalUnitId); + this.organizationalChildrenEffected = true; + } + } + } + return result; + } catch { + return datasets || []; + } + } + + private hasOrganizationalChildren(): boolean { + try { + const map = new Map(); + (this.kommonitorDataExchangeService.accessControl || []).forEach((unit: any) => map.set(unit.organizationalUnitId, unit)); + return (this.elementsToDelete || []).some((unit: any) => { + const curr = map.get(unit.organizationalUnitId); + return !!(curr && curr.children && curr.children.length > 0); + }); + } catch { + return false; + } + } + + resetRolesDeleteForm(): void { + this.loadingData = false; + this.successfullyDeletedDatasets = []; + this.failedDatasetsAndErrors = []; + this.affectedSpatialUnits = []; + this.affectedGeoresources = []; + this.affectedIndicators = []; + this.hideSuccessAlert(); + this.hideErrorAlert(); + } + + gatherAffectedSpatialUnits(): any[] { + const affected: any[] = []; + try { + const spatialUnits = this.kommonitorDataExchangeService.availableSpatialUnits || []; + for (const spatialUnit of spatialUnits) { + const permissions: string[] = spatialUnit?.permissions || []; + for (const dataset of this.elementsToDelete) { + const datasetPermissions = (dataset?.permissions || []).map((p: any) => p.permissionId); + const overlaps = permissions && datasetPermissions && permissions.some((p: string) => datasetPermissions.includes(p)); + if (overlaps) { + const connectedItems: any[] = []; + (permissions || []).forEach((permissionId: string) => { + const match = (dataset?.permissions || []).find((p: any) => p.permissionId === permissionId); + if (match) { + connectedItems.push({ + name: dataset?.name, + permission: match.permissionLevel, + subGroup: !!dataset?.subGroup + }); + } + }); + const enriched = { ...spatialUnit, connectedItems }; + affected.push(enriched); + break; + } + } + } + } catch {} + return affected; + } + + gatherAffectedGeoresources(): any[] { + const affected: any[] = []; + try { + const georesources = this.kommonitorDataExchangeService.availableGeoresources || []; + for (const georesource of georesources) { + const permissions: string[] = georesource?.permissions || []; + for (const dataset of this.elementsToDelete) { + const datasetPermissions = (dataset?.permissions || []).map((p: any) => p.permissionId); + const overlaps = permissions && datasetPermissions && permissions.some((p: string) => datasetPermissions.includes(p)); + if (overlaps) { + const connectedItems: any[] = []; + (permissions || []).forEach((permissionId: string) => { + const match = (dataset?.permissions || []).find((p: any) => p.permissionId === permissionId); + if (match) { + connectedItems.push({ + name: dataset?.name, + permission: match.permissionLevel, + subGroup: !!dataset?.subGroup + }); + } + }); + const enriched = { ...georesource, connectedItems }; + affected.push(enriched); + break; + } + } + } + } catch {} + return affected; + } + + gatherAffectedIndicators(): any[] { + const affected: any[] = []; + try { + const indicators = this.kommonitorDataExchangeService.availableIndicators || []; + for (const indicator of indicators) { + const permissions_metadata: string[] = indicator?.permissions || []; + let found = false; + let temp_indicator: any = { ...indicator }; + for (const dataset of this.elementsToDelete) { + const datasetPermissions = (dataset?.permissions || []).map((p: any) => p.permissionId); + const applicableSpatialUnits = indicator?.applicableSpatialUnits || []; + + // Base indicator-level connections + let connectedItems: any[] = temp_indicator.connectedItems || []; + const overlapsBase = permissions_metadata && datasetPermissions && permissions_metadata.some((p: string) => datasetPermissions.includes(p)); + if (overlapsBase) { + (permissions_metadata || []).forEach((permissionId: string) => { + const match = (dataset?.permissions || []).find((p: any) => p.permissionId === permissionId); + if (match) { + connectedItems.push({ + name: dataset?.name, + permission: match.permissionLevel, + subGroup: !!dataset?.subGroup + }); + } + }); + temp_indicator.connectedItems = connectedItems; + found = true; + } + + // Spatial unit specific connections + const connectedSpatialUnits: any[] = []; + for (const applicableSpatialUnit of applicableSpatialUnits) { + const permissions_SU: string[] = applicableSpatialUnit?.permissions || []; + const overlapsSU = permissions_SU && datasetPermissions && permissions_SU.some((p: string) => datasetPermissions.includes(p)); + if (overlapsSU) { + const spatialItem = { name: applicableSpatialUnit?.spatialUnitName, ids: [] as any[] }; + (permissions_SU || []).forEach((permissionId: string) => { + const match = (dataset?.permissions || []).find((p: any) => p.permissionId === permissionId); + if (match) { + // Ensure base connectedItems have an entry too if not found earlier + if (!found) { + connectedItems.push({ + name: dataset?.name, + permission: match.permissionLevel, + subGroup: !!dataset?.subGroup + }); + } + spatialItem.ids.push({ + name: dataset?.name, + permission: match.permissionLevel, + subGroup: !!dataset?.subGroup + }); + } + }); + if (spatialItem.ids.length > 0) { + temp_indicator.connectedItems = connectedItems; // ensure present + connectedSpatialUnits.push(spatialItem); + found = true; + } + } + } + if (connectedSpatialUnits.length > 0) { + temp_indicator.connectedSpatialUnits = connectedSpatialUnits; + } + + if (found) { + affected.push(temp_indicator); + break; // move to next indicator + } + } + } + } catch {} + return affected; + } + + isDeleteDisabled(): boolean { + return this.elementsToDelete.length === 0 || this.affectedSpatialUnits.length > 0 || this.affectedGeoresources.length > 0 || this.affectedIndicators.length > 0 || this.organizationalChildrenEffected; + } + + deleteOrganizationalUnits(): void { + this.loadingData = true; + const deleteCalls = this.elementsToDelete.map(ds => this.getDeleteDatasetObservable(ds)); + if (deleteCalls.length === 0) { + this.loadingData = false; + return; + } + forkJoin(deleteCalls).subscribe({ + next: () => this.handleDeleteResults(), + error: () => this.handleDeleteResults() + }); + } + + private getDeleteDatasetObservable(dataset: any) { + const url = `${this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI}/organizationalUnits/${dataset.organizationalUnitId}`; + return this.http.delete(url).pipe( + tap(() => { + this.successfullyDeletedDatasets.push(dataset); + // remove from local array if present + const roles = this.kommonitorDataExchangeService.availableRoles || []; + const idx = roles.findIndex((r: any) => r.organizationalUnitId === dataset.organizationalUnitId); + if (idx > -1) { + roles.splice(idx, 1); + } + }), + catchError((error) => { + const message = error && error.error ? this.kommonitorDataExchangeService.syntaxHighlightJSON(error.error) : this.kommonitorDataExchangeService.syntaxHighlightJSON(error); + this.failedDatasetsAndErrors.push([dataset, message]); + return of(null); + }) + ); + } + + private handleDeleteResults(): void { + if (this.failedDatasetsAndErrors.length > 0) { + this.showErrorAlert = true; + } + if (this.successfullyDeletedDatasets.length > 0) { + this.showSuccessAlert = true; + // Parent should refresh table after modal closes + } + setTimeout(() => { + this.loadingData = false; + }, 300); + } + + hideSuccessAlert(): void { this.showSuccessAlert = false; } + hideErrorAlert(): void { this.showErrorAlert = false; } + + cancel(): void { this.activeModal.dismiss('cancel'); } + + trackByIndex(index: number): number { return index; } +} + + diff --git a/app/components/ngComponents/admin/adminRoleManagement/roleEditGroupRightsModal/role-edit-group-rights-modal.component.css b/app/components/ngComponents/admin/adminRoleManagement/roleEditGroupRightsModal/role-edit-group-rights-modal.component.css new file mode 100644 index 000000000..ecf7a6721 --- /dev/null +++ b/app/components/ngComponents/admin/adminRoleManagement/roleEditGroupRightsModal/role-edit-group-rights-modal.component.css @@ -0,0 +1,70 @@ +.vertical-align { + display: flex; + align-items: center; +} + +.margin-right { + margin-right: 10px; +} + +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.switchslider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; +} + +.switchslider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .switchslider { + background-color: #2196F3; +} + +input:focus + .switchslider { + box-shadow: 0 0 1px #2196F3; +} + +input:checked + .switchslider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); +} + +.switchslider.round { + border-radius: 34px; +} + +.switchslider.round:before { + border-radius: 50%; +} + + + diff --git a/app/components/ngComponents/admin/adminRoleManagement/roleEditGroupRightsModal/role-edit-group-rights-modal.component.html b/app/components/ngComponents/admin/adminRoleManagement/roleEditGroupRightsModal/role-edit-group-rights-modal.component.html new file mode 100644 index 000000000..f2f981259 --- /dev/null +++ b/app/components/ngComponents/admin/adminRoleManagement/roleEditGroupRightsModal/role-edit-group-rights-modal.component.html @@ -0,0 +1,134 @@ + + + + +
+ +

Rolle aktualisiert

+ Die Metadaten der Rolle mit Namen {{successMessagePart}} wurden in KomMonitor aktualisiert und in die Übersichtstabelle eingetragen. +
+ +
+ +

Aktualisierung gescheitert

+ Bei der Aktualisierung der Metadaten der Rolle ist ein Fehler aufgetreten. +
+

+
+ + diff --git a/app/components/ngComponents/admin/adminRoleManagement/roleEditGroupRightsModal/role-edit-group-rights-modal.component.ts b/app/components/ngComponents/admin/adminRoleManagement/roleEditGroupRightsModal/role-edit-group-rights-modal.component.ts new file mode 100644 index 000000000..6bafbf22a --- /dev/null +++ b/app/components/ngComponents/admin/adminRoleManagement/roleEditGroupRightsModal/role-edit-group-rights-modal.component.ts @@ -0,0 +1,760 @@ +import { Component, OnDestroy, OnInit, Inject } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { HttpClient } from '@angular/common/http'; +import { Subscription } from 'rxjs'; +import { KommonitorRoleDataExchangeService } from 'services/adminRoleUnit/kommonitor-role-data-exchange.service'; +import { KommonitorRoleDataGridHelperService } from 'services/adminRoleUnit/kommonitor-role-data-grid-helper.service'; + +declare const $: any; + +@Component({ + selector: 'role-edit-group-rights-modal-new', + templateUrl: './role-edit-group-rights-modal.component.html', + styleUrls: ['./role-edit-group-rights-modal.component.css'] +}) +export class RoleEditGroupRightsModalComponent implements OnInit, OnDestroy { + + loadingData: boolean = false; + private pendingLoads: number = 0; + + current: any = {}; + access: any[] = []; + + authorityRoleManagementTableOptions: any = undefined; + delegatedRoleManagementTableOptions: any = undefined; + + // ag-Grid explicit bindings (mirror working pattern) + authorityColumnDefs: any[] = []; + authorityRowData: any[] = []; + authorityDefaultColDef: any = {}; + authorityGridOptions: any = {}; + + delegatedColumnDefs: any[] = []; + delegatedRowData: any[] = []; + delegatedDefaultColDef: any = {}; + delegatedGridOptions: any = {}; + + authorityRoleIDs: string[] = []; + authorityAccess: any[] | undefined = undefined; + authorityPermissions: string[] = []; + + delegatedRoleIDs: string[] = []; + delegatedAccess: any[] | undefined = undefined; + delegatedPermissions: string[] = []; + activeDelegatedRolesOnly: boolean = true; + + successMessagePart: string | undefined = undefined; + errorMessagePart: string | undefined = undefined; + + authorityInfoExpanded: boolean = false; + delegatedInfoExpanded: boolean = false; + + currentStep: number = 1; + totalSteps: number = 2; + + private subscriptions: Subscription[] = []; + + // Advanced checkbox renderers (ported from AngularJS) + private CheckboxRenderer_UM_group = class { + private params: any; + private eGui: HTMLElement | null = null; + private boundCheckedHandler: any; + + init(params: any) { + this.params = params; + + let isChecked = false; + let exists = false; + let className: string | undefined = undefined; + if (params && params.data && Array.isArray(params.data.permissions)) { + for (const permission of params.data.permissions) { + if (permission.permissionLevel === 'unit-users-creator') { + exists = true; + isChecked = !!permission.isChecked; + className = permission.permissionId; + break; + } + } + } + + if (exists) { + const input = document.createElement('input') as HTMLInputElement; + this.eGui = input; + input.className = className || ''; + input.type = 'checkbox'; + input.checked = isChecked; + + // disabled when row disabled or higher-level (client) permission is checked + input.disabled = !!params.data?.disabled || this.anyHigherChecked('client-users-creator'); + + this.boundCheckedHandler = this.checkedHandler.bind(this); + input.addEventListener('click', this.boundCheckedHandler); + } else { + this.eGui = document.createElement('span'); + } + } + + private anyHigherChecked(higherLevel: string): boolean { + if (!this.params?.data?.permissions) return false; + for (const p of this.params.data.permissions) { + if (p.permissionLevel === higherLevel) { + return !!p.isChecked; + } + } + return false; + } + + checkedHandler(e: any) { + const checked = e.target.checked; + for (const permission of this.params.data.permissions) { + if (permission.permissionLevel === 'unit-users-creator') { + permission.isChecked = checked; + break; + } + } + } + + getGui() { return this.eGui; } + + destroy() { + if (this.eGui && this.boundCheckedHandler) { + this.eGui.removeEventListener('click', this.boundCheckedHandler); + } + } + }; + + private CheckboxRenderer_UM_subGroup = class { + private params: any; + private eGui: HTMLElement | null = null; + private boundCheckedHandler: any; + + init(params: any) { + this.params = params; + + let isChecked = false; + let exists = false; + let className: string | undefined = undefined; + if (params && params.data && Array.isArray(params.data.permissions)) { + for (const permission of params.data.permissions) { + if (permission.permissionLevel === 'client-users-creator') { + exists = true; + isChecked = !!permission.isChecked; + className = permission.permissionId; + break; + } + } + } + + if (exists) { + const input = document.createElement('input') as HTMLInputElement; + this.eGui = input; + input.className = className || ''; + input.type = 'checkbox'; + input.checked = isChecked; + input.disabled = !!params.data?.disabled; + + this.boundCheckedHandler = this.checkedHandler.bind(this); + input.addEventListener('click', this.boundCheckedHandler); + } else { + this.eGui = document.createElement('span'); + } + } + + checkedHandler(e: any) { + const checked = e.target.checked; + for (const permission of this.params.data.permissions) { + if (permission.permissionLevel === 'unit-users-creator') { + if (checked) { + permission.isChecked = true; + } + } else if (permission.permissionLevel === 'client-users-creator') { + permission.isChecked = checked; + } + } + if (this.params.api && this.params.node) { + this.params.api.refreshCells({ force: true, rowNodes: [this.params.node] }); + } + } + + getGui() { return this.eGui; } + + destroy() { + if (this.eGui && this.boundCheckedHandler) { + this.eGui.removeEventListener('click', this.boundCheckedHandler); + } + } + }; + + private CheckboxRenderer_RM_group = class { + private params: any; + private eGui: HTMLElement | null = null; + private boundCheckedHandler: any; + + init(params: any) { + this.params = params; + + let isChecked = false; + let exists = false; + let className: string | undefined = undefined; + if (params && params.data && Array.isArray(params.data.permissions)) { + for (const permission of params.data.permissions) { + if (permission.permissionLevel === 'unit-resources-creator') { + exists = true; + isChecked = !!permission.isChecked; + className = permission.permissionId; + break; + } + } + } + + if (exists) { + const input = document.createElement('input') as HTMLInputElement; + this.eGui = input; + input.className = className || ''; + input.type = 'checkbox'; + input.checked = isChecked; + input.disabled = !!params.data?.disabled || this.anyHigherChecked('client-resources-creator'); + + this.boundCheckedHandler = this.checkedHandler.bind(this); + input.addEventListener('click', this.boundCheckedHandler); + } else { + this.eGui = document.createElement('span'); + } + } + + private anyHigherChecked(higherLevel: string): boolean { + if (!this.params?.data?.permissions) return false; + for (const p of this.params.data.permissions) { + if (p.permissionLevel === higherLevel) { + return !!p.isChecked; + } + } + return false; + } + + checkedHandler(e: any) { + const checked = e.target.checked; + for (const permission of this.params.data.permissions) { + if (permission.permissionLevel === 'unit-resources-creator') { + permission.isChecked = checked; + break; + } + } + } + + getGui() { return this.eGui; } + + destroy() { + if (this.eGui && this.boundCheckedHandler) { + this.eGui.removeEventListener('click', this.boundCheckedHandler); + } + } + }; + + private CheckboxRenderer_RM_subGroup = class { + private params: any; + private eGui: HTMLElement | null = null; + private boundCheckedHandler: any; + + init(params: any) { + this.params = params; + + let isChecked = false; + let exists = false; + let className: string | undefined = undefined; + if (params && params.data && Array.isArray(params.data.permissions)) { + for (const permission of params.data.permissions) { + if (permission.permissionLevel === 'client-resources-creator') { + exists = true; + isChecked = !!permission.isChecked; + className = permission.permissionId; + break; + } + } + } + + if (exists) { + const input = document.createElement('input') as HTMLInputElement; + this.eGui = input; + input.className = className || ''; + input.type = 'checkbox'; + input.checked = isChecked; + input.disabled = !!params.data?.disabled; + + this.boundCheckedHandler = this.checkedHandler.bind(this); + input.addEventListener('click', this.boundCheckedHandler); + } else { + this.eGui = document.createElement('span'); + } + } + + checkedHandler(e: any) { + const checked = e.target.checked; + for (const permission of this.params.data.permissions) { + if (permission.permissionLevel === 'unit-resources-creator') { + if (checked) { + permission.isChecked = true; + } + } else if (permission.permissionLevel === 'client-resources-creator') { + permission.isChecked = checked; + } + } + if (this.params.api && this.params.node) { + this.params.api.refreshCells({ force: true, rowNodes: [this.params.node] }); + } + } + + getGui() { return this.eGui; } + + destroy() { + if (this.eGui && this.boundCheckedHandler) { + this.eGui.removeEventListener('click', this.boundCheckedHandler); + } + } + }; + + private CheckboxRenderer_TM_group = class { + private params: any; + private eGui: HTMLElement | null = null; + private boundCheckedHandler: any; + + init(params: any) { + this.params = params; + + let isChecked = false; + let exists = false; + let className: string | undefined = undefined; + if (params && params.data && Array.isArray(params.data.permissions)) { + for (const permission of params.data.permissions) { + if (permission.permissionLevel === 'unit-themes-creator') { + exists = true; + isChecked = !!permission.isChecked; + className = permission.permissionId; + break; + } + } + } + + if (exists) { + const input = document.createElement('input') as HTMLInputElement; + this.eGui = input; + input.className = className || ''; + input.type = 'checkbox'; + input.checked = isChecked; + input.disabled = !!params.data?.disabled || this.anyHigherChecked('client-themes-creator'); + + this.boundCheckedHandler = this.checkedHandler.bind(this); + input.addEventListener('click', this.boundCheckedHandler); + } else { + this.eGui = document.createElement('span'); + } + } + + private anyHigherChecked(higherLevel: string): boolean { + if (!this.params?.data?.permissions) return false; + for (const p of this.params.data.permissions) { + if (p.permissionLevel === higherLevel) { + return !!p.isChecked; + } + } + return false; + } + + checkedHandler(e: any) { + const checked = e.target.checked; + for (const permission of this.params.data.permissions) { + if (permission.permissionLevel === 'unit-themes-creator') { + permission.isChecked = checked; + break; + } + } + } + + getGui() { return this.eGui; } + + destroy() { + if (this.eGui && this.boundCheckedHandler) { + this.eGui.removeEventListener('click', this.boundCheckedHandler); + } + } + }; + + private CheckboxRenderer_TM_subGroup = class { + private params: any; + private eGui: HTMLElement | null = null; + private boundCheckedHandler: any; + + init(params: any) { + this.params = params; + + let isChecked = false; + let exists = false; + let className: string | undefined = undefined; + if (params && params.data && Array.isArray(params.data.permissions)) { + for (const permission of params.data.permissions) { + if (permission.permissionLevel === 'client-themes-creator') { + exists = true; + isChecked = !!permission.isChecked; + className = permission.permissionId; + break; + } + } + } + + if (exists) { + const input = document.createElement('input') as HTMLInputElement; + this.eGui = input; + input.className = className || ''; + input.type = 'checkbox'; + input.checked = isChecked; + input.disabled = !!params.data?.disabled; + + this.boundCheckedHandler = this.checkedHandler.bind(this); + input.addEventListener('click', this.boundCheckedHandler); + } else { + this.eGui = document.createElement('span'); + } + } + + checkedHandler(e: any) { + const checked = e.target.checked; + for (const permission of this.params.data.permissions) { + if (permission.permissionLevel === 'unit-themes-creator') { + if (checked) { + permission.isChecked = true; + } + } else if (permission.permissionLevel === 'client-themes-creator') { + permission.isChecked = checked; + } + } + if (this.params.api && this.params.node) { + this.params.api.refreshCells({ force: true, rowNodes: [this.params.node] }); + } + } + + getGui() { return this.eGui; } + + destroy() { + if (this.eGui && this.boundCheckedHandler) { + this.eGui.removeEventListener('click', this.boundCheckedHandler); + } + } + }; + + constructor( + public activeModal: NgbActiveModal, + private http: HttpClient, + public kommonitorDataExchangeService: KommonitorRoleDataExchangeService, + @Inject('kommonitorMultiStepFormHelperService') private multiStepFormHelper: any, + public roleDataGridHelper: KommonitorRoleDataGridHelperService + ) {} + + ngOnInit(): void { + if (this.current && this.current.organizationalUnitId) { + // Parent passed data via componentInstance + this.onEditOrganizationalUnitGroupRights(this.current); + } + } + + ngOnDestroy(): void { + this.subscriptions.forEach(s => s.unsubscribe()); + } + + cancel(): void { + this.activeModal.dismiss('cancel'); + } + + private onEditOrganizationalUnitGroupRights(organizationalUnit: any): void { + this.current = organizationalUnit || {}; + this.access = this.kommonitorDataExchangeService.accessControl || []; + + // extend permissions with advanced admin roles + const permissionStrings = [ + 'unit-users-creator', + 'client-users-creator', + 'unit-resources-creator', + 'client-resources-creator', + 'unit-themes-creator', + 'client-themes-creator' + ]; + + (this.access || []).forEach((elem: any) => { + elem.permissions = elem.permissions || []; + permissionStrings.forEach(role => { + elem.permissions.push({ + permissionLevel: role, + permissionId: `${elem.organizationalUnitId}-${role}` + }); + }); + }); + + this.buildAuthorityRolesTable(); + this.buildDelegatedRolesTable(); + + // Do not register legacy jQuery multi-step handlers in Angular component + // to avoid conflicts with Angular's currentStep state. + } + + onActiveDelegatedRolesOnlyChange(): void { + this.buildDelegatedRolesTable(); + } + + buildAuthorityRolesTable(): void { + this.beginLoading(); + this.authorityPermissions = []; + this.authorityRoleIDs = []; + + const url = `${this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI}/organizationalUnits/${this.current.organizationalUnitId}/role-authorities`; + this.http.get(url).subscribe((response) => { + const roleAuthorities = response?.authorityRoles || []; + roleAuthorities.forEach((elem: any) => { + this.authorityRoleIDs.push(elem.organizationalUnitId); + (elem.adminRoles || []).forEach((role: string) => { + this.authorityPermissions.push(`${elem.organizationalUnitId}-${role}`); + }); + }); + + this.authorityAccess = (this.access || []).filter(elem => this.authorityRoleIDs.includes(elem.organizationalUnitId)); + this.authorityRoleManagementTableOptions = this.roleDataGridHelper.buildAdvancedRoleManagementGrid( + 'editAuthorityGroupRoleManagementTable', + this.authorityRoleManagementTableOptions, + this.authorityAccess, + this.authorityPermissions, + true + ); + + if (this.authorityRoleManagementTableOptions) { + // Use shared advanced components to avoid registration mismatches + this.authorityRoleManagementTableOptions.components = this.roleDataGridHelper.getAdvancedRoleManagementGridComponents(); + this.authorityColumnDefs = this.buildAdvancedRoleManagementGridColumnConfig(); + // Mark rows disabled to make authority table read-only + this.authorityRowData = (this.authorityRoleManagementTableOptions.rowData || []).map((row: any) => ({ ...row, disabled: true })); + this.authorityDefaultColDef = this.roleDataGridHelper.buildRoleManagementDefaultColDef(); + const base = this.roleDataGridHelper.buildRoleManagementGridOptionsPublic(this.authorityRoleManagementTableOptions.components); + this.authorityGridOptions = { + ...base, + overlayNoRowsTemplate: 'No rows to show', + onGridReady: (params: any) => { + this.roleDataGridHelper.setGridApi(params.api); + } + }; + } + this.endLoading(); + }, (err) => { + this.endLoading(); + }); + } + + buildDelegatedRolesTable(): void { + this.beginLoading(); + const url = `${this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI}/organizationalUnits/${this.current.organizationalUnitId}/role-delegates`; + this.http.get(url).subscribe((response) => { + this.delegatedRoleIDs = []; + this.delegatedPermissions = []; + + const roleDelegates = response?.roleDelegates || []; + roleDelegates.forEach((elem: any) => { + this.delegatedRoleIDs.push(elem.organizationalUnitId); + (elem.adminRoles || []).forEach((role: string) => { + this.delegatedPermissions.push(`${elem.organizationalUnitId}-${role}`); + }); + }); + + if (this.delegatedRoleIDs.length === 0) { + this.activeDelegatedRolesOnly = false; + } + + this.delegatedAccess = this.access; + if (this.delegatedRoleIDs.length > 0 && this.activeDelegatedRolesOnly) { + this.delegatedAccess = (this.access || []).filter((elem: any) => this.delegatedRoleIDs.includes(elem.organizationalUnitId)); + } + + this.delegatedRoleManagementTableOptions = this.roleDataGridHelper.buildAdvancedRoleManagementGrid( + 'editDelegatedGroupRoleManagementTable', + this.delegatedRoleManagementTableOptions, + this.delegatedAccess, + this.delegatedPermissions + ); + + if (this.delegatedRoleManagementTableOptions) { + // Use shared advanced components to avoid registration mismatches + this.delegatedRoleManagementTableOptions.components = this.roleDataGridHelper.getAdvancedRoleManagementGridComponents(); + this.delegatedColumnDefs = this.buildAdvancedRoleManagementGridColumnConfig(); + this.delegatedRowData = this.delegatedRoleManagementTableOptions.rowData || []; + this.delegatedDefaultColDef = this.roleDataGridHelper.buildRoleManagementDefaultColDef(); + const base = this.roleDataGridHelper.buildRoleManagementGridOptionsPublic(this.delegatedRoleManagementTableOptions.components); + this.delegatedGridOptions = { + ...base, + overlayNoRowsTemplate: 'No rows to show', + onGridReady: (params: any) => { + this.roleDataGridHelper.setGridApi(params.api); + } + }; + } + this.endLoading(); + }, (err) => { + this.endLoading(); + }); + } + + private buildAdvancedRoleManagementGridColumnConfig(): any[] { + const columnDefs: any[] = []; + columnDefs.push({ + headerName: 'Organisationseinheit', + field: 'name', + minWidth: 200, + cellClassRules: { + 'user-roles-normal': (row: any) => row != undefined + } + }); + columnDefs.push({ + headerName: 'Verwalten von Nutzern', + children: [ + { field: 'Dieser Gruppe', cellRenderer: 'CheckboxRenderer_UM_group' }, + { field: 'Untergruppen', cellRenderer: 'CheckboxRenderer_UM_subGroup' } + ], + field: 'permissions', + filter: false, + sortable: false, + maxWidth: 100 + }); + columnDefs.push({ + headerName: 'Verwalten von Resourcen', + children: [ + { field: 'Dieser Gruppe', cellRenderer: 'CheckboxRenderer_RM_group' }, + { field: 'Untergruppen', cellRenderer: 'CheckboxRenderer_RM_subGroup' } + ], + field: 'permissions', + filter: false, + sortable: false, + maxWidth: 100 + }); + columnDefs.push({ + headerName: 'Verwalten von Themen', + children: [ + { field: 'Dieser Gruppe', cellRenderer: 'CheckboxRenderer_TM_group' }, + { field: 'Untergruppen', cellRenderer: 'CheckboxRenderer_TM_subGroup' } + ], + field: 'permissions', + filter: false, + sortable: false, + maxWidth: 100 + }); + return columnDefs; + } + + async editRoleDelegates(): Promise { + // Build permissions directly from delegated grid rowData to avoid cross-grid API conflicts + const permissions: Record = {}; + this.delegatedRoleIDs = []; + + const rows: any[] = (this.delegatedRoleManagementTableOptions?.rowData || []) as any[]; + for (const row of rows) { + if (!row?.permissions) continue; + for (const p of row.permissions) { + if (!p?.isChecked || !p?.permissionId) continue; + const parts = (p.permissionId as string).split('-'); + const unitId = parts.slice(0, 5).join('-'); + const role = parts.slice(5).join('-'); + if (!permissions[unitId]) permissions[unitId] = []; + if (!permissions[unitId].includes(role)) permissions[unitId].push(role); + } + } + this.delegatedRoleIDs = Object.keys(permissions); + + const putBody: any[] = []; + for (const key of Object.keys(permissions)) { + const orgUnit = (this.access || []).find(elem => elem.organizationalUnitId === key); + putBody.push({ + organizationalUnitId: key, + organizationalUnitName: orgUnit?.name, + keycloakId: orgUnit?.keycloakId, + adminRoles: permissions[key] + }); + } + + this.loadingData = true; + this.errorMessagePart = undefined; + this.successMessagePart = undefined; + + const url = `${this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI}/organizationalUnits/${this.current.organizationalUnitId}/role-delegates`; + this.http.put(url, putBody).subscribe({ + next: async () => { + this.successMessagePart = this.current?.name; + this.errorMessagePart = undefined; + this.buildAuthorityRolesTable(); + this.buildDelegatedRolesTable(); + setTimeout(() => { this.loadingData = false; }, 0); + }, + error: (error: any) => { + if (error && error.error) { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error.error); + } else { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error); + } + this.successMessagePart = undefined; + this.loadingData = false; + } + }); + } + + resetRoleDelegatesForm(): void { + this.successMessagePart = undefined; + this.errorMessagePart = undefined; + this.buildDelegatedRolesTable(); + } + + hideSuccessAlert(): void { + this.successMessagePart = undefined; + } + + hideErrorAlert(): void { + this.errorMessagePart = undefined; + } + + toggleAuthorityInfo(): void { + this.authorityInfoExpanded = !this.authorityInfoExpanded; + } + + toggleDelegatedInfo(): void { + this.delegatedInfoExpanded = !this.delegatedInfoExpanded; + } + + nextStep(): void { + this.goToStep(this.currentStep + 1); + } + + prevStep(): void { + this.goToStep(this.currentStep - 1); + } + + goToStep(step: number): void { + if (step < 1) { + this.currentStep = 1; + } else if (step > this.totalSteps) { + this.currentStep = this.totalSteps; + } else { + this.currentStep = step; + } + + // Build tables on entering steps, mirroring role-add modal behavior + if (this.currentStep === 1) { + // ensure authority grid is ready + this.buildAuthorityRolesTable(); + } else if (this.currentStep === 2) { + this.buildDelegatedRolesTable(); + } + } + + private beginLoading(): void { + this.pendingLoads++; + this.loadingData = true; + } + + private endLoading(): void { + this.pendingLoads = Math.max(0, this.pendingLoads - 1); + if (this.pendingLoads === 0) { + this.loadingData = false; + } + } +} diff --git a/app/components/ngComponents/admin/adminRoleManagement/roleEditMetadataModal/role-edit-metadata-modal.component.css b/app/components/ngComponents/admin/adminRoleManagement/roleEditMetadataModal/role-edit-metadata-modal.component.css new file mode 100644 index 000000000..65f9eb775 --- /dev/null +++ b/app/components/ngComponents/admin/adminRoleManagement/roleEditMetadataModal/role-edit-metadata-modal.component.css @@ -0,0 +1,124 @@ +.loading-overlay-admin-panel { + width: 100%; + height: 100%; + position: absolute; + background-color: rgb(128,128,128); + top: 0; + left: 0; + border-radius: 5px; + opacity: 0.8; + z-index: 100000; +} + +.loading-overlay-admin-panel .icon-spin { + font-size: 25px; + width: 25px; + height: 25px; + position: absolute; + top: 50%; + left: 50%; + margin: -12.5px 0 0 -12.5px; + -webkit-animation: spin 1s infinite linear; + -moz-animation: spin 1s infinite linear; + -o-animation: spin 1s infinite linear; + animation: spin 1s infinite linear; + -webkit-transform-origin: 50% 50%; + transform-origin:50% 50%; + -ms-transform-origin:50% 50%; +} + +@-webkit-keyframes spin { + from { -webkit-transform: rotate(0deg); } + to { -webkit-transform: rotate(360deg); } +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.fs-title { + margin-top: 0.5em; +} + +.vertical-align { + display: flex; + align-items: flex-start; +} + +#progressbar { + margin-bottom: 10px; + overflow: hidden; + counter-reset: step; +} + +#progressbar li { + list-style-type: none; + color: black; + text-transform: uppercase; + font-size: 9px; + float: left; + position: relative; + letter-spacing: 1px; + cursor: pointer; +} + +#progressbar li:before { + content: counter(step); + counter-increment: step; + width: 24px; + height: 24px; + line-height: 26px; + display: block; + font-size: 12px; + color: #333; + background: #cccc; + border-radius: 25px; + margin: 0 auto 10px auto; + transform: translateZ(-1px); +} + +#progressbar li:after { + content: ''; + width: 100%; + height: 2px; + background: #cccc; + position: absolute; + left: -50%; + top: 9px; + z-index: -1; +} + +#progressbar li:first-child:after { + content: none; +} + +#progressbar li.active:before, #progressbar li.active:after { + background: var(--kommonitor-primary); + color: white; +} + +#progressbar li.clickable { + cursor: pointer; + transition: all 0.3s ease; +} + +#progressbar li.clickable:hover { + color: var(--kommonitor-primary); +} + +#progressbar li.clickable:hover:before { + background: var(--kommonitor-primary); + transform: scale(1.1); +} + +/* Center multi-step form like the original */ +.multiStepForm { + text-align: center; + position: relative; + margin-top: 30px; + z-index: 11000; + font-size: 12px; +} + + diff --git a/app/components/ngComponents/admin/adminRoleManagement/roleEditMetadataModal/role-edit-metadata-modal.component.html b/app/components/ngComponents/admin/adminRoleManagement/roleEditMetadataModal/role-edit-metadata-modal.component.html new file mode 100644 index 000000000..43c4f392c --- /dev/null +++ b/app/components/ngComponents/admin/adminRoleManagement/roleEditMetadataModal/role-edit-metadata-modal.component.html @@ -0,0 +1,104 @@ + + + + + + +
+ +

Rolle aktualisiert

+ Die Metadaten der Rolle mit Namen {{ successMessagePart }} wurden in KomMonitor aktualisiert und in die Übersichtstabelle eingetragen. +
+ +
+ +

Aktualisierung gescheitert

+ Bei der Aktualisierung der Metadaten der Rolle ist ein Fehler aufgetreten. Fehlermeldung: +
+

+
+ +
+ +

Rolle in Keycloak editiert

+

Die Rollen für die Einheit {{ successMessagePart }} wurden in Keycloak editiert. Bestehende Verknüpfungen mit Usern bleiben erhalten.

+
+ +
+ +

Editierung in Keycloak gescheitert

+ Bei der Editierung in Keycloak ist ein Fehler aufgetreten. Fehlermeldung: +
+

+  
+
+ Die Rolle kann auch direkt in der Keycloak Administrationsseite editiert werden. + {{ keycloakHelper.targetUrlToKeycloakInstance }} +
+ + + diff --git a/app/components/ngComponents/admin/adminRoleManagement/roleEditMetadataModal/role-edit-metadata-modal.component.ts b/app/components/ngComponents/admin/adminRoleManagement/roleEditMetadataModal/role-edit-metadata-modal.component.ts new file mode 100644 index 000000000..40a03652c --- /dev/null +++ b/app/components/ngComponents/admin/adminRoleManagement/roleEditMetadataModal/role-edit-metadata-modal.component.ts @@ -0,0 +1,142 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { HttpClient } from '@angular/common/http'; +import { Subscription } from 'rxjs'; +import { KommonitorDataExchangeService } from 'services/adminSpatialUnit/kommonitor-data-exchange.service'; +import { KommonitorRoleKeycloakHelperService } from 'services/adminRoleUnit/kommonitor-role-keycloak-helper.service'; + +@Component({ + selector: 'role-edit-metadata-modal-new', + templateUrl: './role-edit-metadata-modal.component.html', + styleUrls: ['./role-edit-metadata-modal.component.css'] +}) +export class RoleEditMetadataModalComponent implements OnInit, OnDestroy { + + loadingData: boolean = false; + + current: any = {}; + old: any = { name: undefined }; + + parentOrganizationalUnit: any = null; + + nameInvalid: boolean = false; + + // Alerts/messages + successMessagePart: string | undefined = undefined; + errorMessagePart: string | undefined = undefined; + keycloakErrorMessagePart: string | undefined = undefined; + + showSuccessAlert: boolean = false; + showErrorAlert: boolean = false; + showKeycloakSuccessAlert: boolean = false; + showKeycloakErrorAlert: boolean = false; + + private subscriptions: Subscription[] = []; + + constructor( + public activeModal: NgbActiveModal, + private http: HttpClient, + public kommonitorDataExchangeService: KommonitorDataExchangeService, + public keycloakHelper: KommonitorRoleKeycloakHelperService + ) {} + + ngOnInit(): void { + // If parent passed data via componentInstance, ensure validation state is set + if (this.current && this.current.organizationalUnitId) { + this.resetRoleEditMetadataForm(); + } + } + + ngOnDestroy(): void { + this.subscriptions.forEach(s => s.unsubscribe()); + } + + // Initialization now happens via parent setting componentInstance fields + + checkRoleName(): void { + this.nameInvalid = false; + const accessControl = this.kommonitorDataExchangeService.accessControl || []; + accessControl.forEach((ou: any) => { + if (ou.name === this.current.name && ou.organizationalUnitId !== this.current.organizationalUnitId) { + this.nameInvalid = true; + } + }); + } + + resetRoleEditMetadataForm(): void { + this.successMessagePart = undefined; + this.errorMessagePart = undefined; + this.keycloakErrorMessagePart = undefined; + + this.showSuccessAlert = false; + this.showErrorAlert = false; + this.showKeycloakSuccessAlert = false; + this.showKeycloakErrorAlert = false; + + // Trigger name validation state + setTimeout(() => this.checkRoleName(), 0); + } + + async editRoleMetadata(): Promise { + const putBody = { + name: this.current.name, + description: this.current.description, + contact: this.current.contact, + mandant: (this.current && typeof this.current.mandant === 'boolean') ? this.current.mandant : false + }; + + this.loadingData = true; + this.showSuccessAlert = false; + this.showErrorAlert = false; + this.showKeycloakSuccessAlert = false; + this.showKeycloakErrorAlert = false; + this.errorMessagePart = undefined; + this.keycloakErrorMessagePart = undefined; + + try { + const url = `${this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI}/organizationalUnits/${this.current.organizationalUnitId}`; + await this.http.put(url, putBody).toPromise(); + + this.successMessagePart = this.current.name; + this.showSuccessAlert = true; + + // Attempt to rename in Keycloak (best-effort) + try { + await this.keycloakHelper.renameExistingRoles(this.old.name, this.current.name); + this.old.name = this.current.name; // keep in sync + await this.keycloakHelper.fetchAndSetKeycloakRoles(); + this.showKeycloakSuccessAlert = true; + } catch (error: any) { + this.keycloakErrorMessagePart = error && error.data + ? this.kommonitorDataExchangeService.syntaxHighlightJSON(error.data) + : this.kommonitorDataExchangeService.syntaxHighlightJSON(error); + this.showKeycloakErrorAlert = true; + } + + this.loadingData = false; + + // Close after a short delay to let user see success + setTimeout(() => this.activeModal.close('success'), 1500); + } catch (error: any) { + if (error && error.error) { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error.error); + } else { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error); + } + this.showErrorAlert = true; + this.loadingData = false; + } + } + + hideSuccessAlert(): void { this.showSuccessAlert = false; } + hideErrorAlert(): void { this.showErrorAlert = false; } + hideKeycloakSuccessAlert(): void { this.showKeycloakSuccessAlert = false; } + hideKeycloakErrorAlert(): void { this.showKeycloakErrorAlert = false; } + + canSubmit(): boolean { + return !!this.current?.name && !!this.current?.description && !!this.current?.contact && !this.nameInvalid; + } +} + + + diff --git a/app/components/ngComponents/admin/adminScriptExecution/admin-script-execution.component.css b/app/components/ngComponents/admin/adminScriptExecution/admin-script-execution.component.css new file mode 100644 index 000000000..81e7975c8 --- /dev/null +++ b/app/components/ngComponents/admin/adminScriptExecution/admin-script-execution.component.css @@ -0,0 +1,5 @@ +/* Reuse existing admin styles if present */ +.adminTableButtonWrapper { margin-top: 10px; } +.small-box .inner h3 { font-weight: 600; } + + diff --git a/app/components/ngComponents/admin/adminScriptExecution/admin-script-execution.component.html b/app/components/ngComponents/admin/adminScriptExecution/admin-script-execution.component.html new file mode 100644 index 000000000..6eade2500 --- /dev/null +++ b/app/components/ngComponents/admin/adminScriptExecution/admin-script-execution.component.html @@ -0,0 +1,206 @@ +
+
+

+ Indikatoren-Berechnung + Anzeige und Ausführung von Prozessierungsjobs +

+ +
+ +
+
+ +
+
+
+

Übersicht Berechnungs-Jobs zur Indikatoren-Zeitreihen-fortführung

+
+ +
+
+
+
+
+
+
+

{{defaultComputationJobHealth.queueStatus}}

+

Status der Job-Ausführung

+
+
+ +
+
+
+
+
+

{{defaultComputationJobHealth.newestJobId}}

+

neueste Job-Id

+
+
+ +
+
+
+
+
+

{{defaultComputationJobHealth.succeededJobs}}

+

abgeschlossene Jobs

+
+
+ +
+
+
+
+
+

{{defaultComputationJobHealth.failedJobs}}

+

gescheiterte Jobs

+
+
+ +
+
+
+ +
+
+
+
+

{{defaultComputationJobHealth.activeJobs}}

+

laufende Jobs

+
+
+ +
+
+
+
+
+

{{defaultComputationJobHealth.waitingJobs}}

+

wartende Jobs

+
+
+ +
+
+
+
+
+

{{defaultComputationJobHealth.delayedJobs}}

+

wartende Jobs

+
+
+ +
+
+
+ +
+ +
+ + +
+
+
+ +
+
+

Übersicht Berechnungs-Jobs zur individuell-parametrisierten Indikatoren-Berechnung

+
+ +
+
+
+
+
+
+
+

{{customizedComputationJobHealth.queueStatus}}

+

Status der Job-Ausführung

+
+
+ +
+
+
+
+
+

{{customizedComputationJobHealth.newestJobId}}

+

neueste Job-Id

+
+
+ +
+
+
+
+
+

{{customizedComputationJobHealth.succeededJobs}}

+

abgeschlossene Jobs

+
+
+ +
+
+
+
+
+

{{customizedComputationJobHealth.failedJobs}}

+

gescheiterte Jobs

+
+
+ +
+
+
+ +
+
+
+
+

{{customizedComputationJobHealth.activeJobs}}

+

laufende Jobs

+
+
+ +
+
+
+
+
+

{{customizedComputationJobHealth.waitingJobs}}

+

wartende Jobs

+
+
+ +
+
+
+
+
+

{{customizedComputationJobHealth.delayedJobs}}

+

wartende Jobs

+
+
+ +
+
+
+ +
+ + +
+
+
+
+
+ + diff --git a/app/components/ngComponents/admin/adminScriptExecution/admin-script-execution.component.ts b/app/components/ngComponents/admin/adminScriptExecution/admin-script-execution.component.ts new file mode 100644 index 000000000..51f76faff --- /dev/null +++ b/app/components/ngComponents/admin/adminScriptExecution/admin-script-execution.component.ts @@ -0,0 +1,96 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { GridOptions, ColDef } from 'ag-grid-community'; +import { Subscription, forkJoin, of } from 'rxjs'; +import { catchError } from 'rxjs/operators'; +import { KommonitorDataExchangeService } from 'services/adminSpatialUnit/kommonitor-data-exchange.service'; +import { KommonitorScriptExecutionDataExchangeService } from 'services/adminScriptUnit/kommonitor-script-execution-data-exchange.service'; +import { KommonitorScriptExecutionDataGridHelperService } from 'services/adminScriptUnit/kommonitor-script-execution-data-grid-helper.service'; + +interface JobHealth { + queueStatus?: string; + newestJobId?: number; + succeededJobs?: number; + failedJobs?: number; + activeJobs?: number; + waitingJobs?: number; + delayedJobs?: number; +} + +@Component({ + selector: 'admin-script-execution-new', + templateUrl: './admin-script-execution.component.html', + styleUrls: ['./admin-script-execution.component.css'] +}) +export class AdminScriptExecutionComponent implements OnInit, OnDestroy { + loadingData: boolean = true; + + defaultComputationJobHealth: JobHealth = {}; + customizedComputationJobHealth: JobHealth = {}; + + defaultJobs: any[] = []; + customizedJobs: any[] = []; + + defaultJobsGridOptions: GridOptions = {}; + customizedJobsGridOptions: GridOptions = {}; + + defaultJobsColumnDefs: ColDef[] = []; + customizedJobsColumnDefs: ColDef[] = []; + + private subs: Subscription[] = []; + + constructor( + private dataExchange: KommonitorDataExchangeService, + private scriptExchange: KommonitorScriptExecutionDataExchangeService, + private gridHelper: KommonitorScriptExecutionDataGridHelperService + ) {} + + ngOnInit(): void { + this.setupColumns(); + // initialize base grid options; rowData/columnDefs bound in template + this.defaultJobsGridOptions = this.gridHelper.buildGridOptions(this.defaultJobsColumnDefs, []); + this.customizedJobsGridOptions = this.gridHelper.buildGridOptions(this.customizedJobsColumnDefs, []); + this.loadAll(); + } + + ngOnDestroy(): void { + this.subs.forEach(s => s.unsubscribe()); + } + + refresh(): void { + this.loadAll(); + } + + private loadAll(): void { + this.loadingData = true; + const s = forkJoin({ + defaultHealth: this.scriptExchange.fetchDefaultIndicatorJobHealth(), + customizedHealth: this.scriptExchange.fetchCustomizedIndicatorJobHealth(), + defaultJobs: this.scriptExchange.fetchDefaultIndicatorJobs(), + customizedJobs: this.scriptExchange.fetchCustomizedIndicatorJobs() + }).subscribe({ + next: res => { + this.defaultComputationJobHealth = res.defaultHealth || {}; + this.customizedComputationJobHealth = res.customizedHealth || {}; + this.defaultJobs = this.sortJobs(res.defaultJobs || []); + this.customizedJobs = this.sortJobs(res.customizedJobs || []); + // rowData is bound in template; assigning to arrays triggers grid update + this.loadingData = false; + }, + error: _ => { + this.loadingData = false; + } + }); + this.subs.push(s); + } + + private setupColumns(): void { + this.defaultJobsColumnDefs = this.gridHelper.buildDefaultJobsColumnDefs(); + this.customizedJobsColumnDefs = this.gridHelper.buildCustomizedJobsColumnDefs(); + } + + + private sortJobs(arr: any[]): any[] { return (arr || []).sort((a: any, b: any) => (b?.jobId || 0) - (a?.jobId || 0)); } + +} + + diff --git a/app/components/ngComponents/admin/adminScriptManagement/admin-script-management.component.css b/app/components/ngComponents/admin/adminScriptManagement/admin-script-management.component.css new file mode 100644 index 000000000..dce74ed89 --- /dev/null +++ b/app/components/ngComponents/admin/adminScriptManagement/admin-script-management.component.css @@ -0,0 +1,13 @@ +.adminTableButtonWrapper { + margin-top: 10px; +} + +.admin-table-wrapper { + width: 100%; +} + + + + + + diff --git a/app/components/ngComponents/admin/adminScriptManagement/admin-script-management.component.html b/app/components/ngComponents/admin/adminScriptManagement/admin-script-management.component.html new file mode 100644 index 000000000..a2f348046 --- /dev/null +++ b/app/components/ngComponents/admin/adminScriptManagement/admin-script-management.component.html @@ -0,0 +1,55 @@ +
+ +
+

+ Skriptverwaltung + Steuerung der semi-automatisierten Indikatorenberechnung +

+ +
+ + + +
+
+ + +
+ + +
+
+

Indikatoren-Skripte

+
+ +
+ +
+ + + + +
+
+ +
+ + +
+ +
+ + + + + + diff --git a/app/components/ngComponents/admin/adminScriptManagement/admin-script-management.component.ts b/app/components/ngComponents/admin/adminScriptManagement/admin-script-management.component.ts new file mode 100644 index 000000000..d3824af72 --- /dev/null +++ b/app/components/ngComponents/admin/adminScriptManagement/admin-script-management.component.ts @@ -0,0 +1,221 @@ +import { Component, Inject, NgZone, OnDestroy, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; +import { GridApi, GridOptions, ColDef } from 'ag-grid-community'; +import { KommonitorScriptManagementDataGridHelperService } from 'services/script-management/kommonitor-script-management-data-grid-helper.service'; +import { KommonitorScriptManagementDataExchangeService } from 'services/script-management/kommonitor-script-management-data-exchange.service'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { ScriptAddModalComponent } from './scriptAddModal/script-add-modal.component'; +import { ScriptDeleteModalComponent } from './scriptDeleteModal/script-delete-modal.component'; + +declare const $: any; + +@Component({ + selector: 'admin-script-management-new', + templateUrl: './admin-script-management.component.html', + styleUrls: ['./admin-script-management.component.css'] +}) +export class AdminScriptManagementComponent implements OnInit, OnDestroy { + + loadingData: boolean = true; + availableScriptDatasets: any[] = []; + private subscriptions: Subscription[] = []; + scriptsGridOptions: GridOptions = {}; + scriptsColumnDefs: ColDef[] = []; + scriptsDefaultColDef: ColDef = {}; + gridApi: GridApi | null = null; + paginationPageSize: number = 10; + paginationPageSizeSelector: number[] = [10, 25, 50, 100]; + + constructor( + private zone: NgZone, + private broadcastService: BroadcastService, + private scriptExchange: KommonitorScriptManagementDataExchangeService, + private gridHelper: KommonitorScriptManagementDataGridHelperService, + private modalService: NgbModal + ) {} + + ngOnInit(): void { + // initialize any adminLTE box widgets + try { (window as any).$('.box').boxWidget(); } catch {} + + // Make component available for debugging if needed + (window as any).adminScriptManagementComponent = this; + + // Build column defs + this.scriptsColumnDefs = this.gridHelper.buildScriptsColumnDefs(); + + // Listen for metadata loading events + const sub = this.broadcastService.currentBroadcastMsg.subscribe(data => { + if (data.msg === 'initialMetadataLoadingCompleted') { + this.zone.run(() => { + setTimeout(() => this.initializeOrRefreshOverviewTable(), 250); + }); + } else if (data.msg === 'initialMetadataLoadingFailed') { + this.zone.run(() => { this.loadingData = false; }); + } else if (data.msg === 'refreshScriptOverviewTable') { + this.zone.run(() => { + const crudType = (data as any).values?.crudType; + const scriptId = (data as any).values?.scriptId; + this.refreshScriptOverviewTable(crudType, scriptId); + }); + } + }); + this.subscriptions.push(sub); + + // Proactively fetch and build in case data is already present but broadcast not fired + console.debug('[AdminScriptManagement] ngOnInit -> initial fetch/build'); + setTimeout(() => this.refreshScriptOverviewTable(), 0); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(s => s.unsubscribe()); + if ((window as any).adminScriptManagementComponent === this) { + delete (window as any).adminScriptManagementComponent; + } + } + + initializeOrRefreshOverviewTable(): void { + console.debug('[AdminScriptManagement] initializeOrRefreshOverviewTable'); + this.loadingData = true; + const scripts = this.scriptExchange?.availableProcessScripts || []; + console.debug('[AdminScriptManagement] availableProcessScripts length:', Array.isArray(scripts) ? scripts.length : 0); + this.availableScriptDatasets = JSON.parse(JSON.stringify(scripts)); + + try { + this.scriptsGridOptions = this.gridHelper.buildGridOptions(this.scriptsColumnDefs, this.availableScriptDatasets); + this.scriptsDefaultColDef = (this.scriptsGridOptions as any).defaultColDef || {}; + // Align pagination behavior with spatial units grid: set page size and selector on GridOptions + this.scriptsGridOptions = { + ...this.scriptsGridOptions, + paginationPageSize: this.paginationPageSize, + paginationPageSizeSelector: this.paginationPageSizeSelector + } as GridOptions; + } catch (e) {} + this.loadingData = false; + } + + refreshScriptOverviewTable(crudType?: string, targetScriptId?: string): void { + console.debug('[AdminScriptManagement] refreshScriptOverviewTable', { crudType, targetScriptId }); + this.loadingData = true; + if (!crudType || !targetScriptId) { + this.scriptExchange.fetchIndicatorScriptsMetadata( + this.scriptExchange.currentKeycloakLoginRoles + ).then(() => { + console.debug('[AdminScriptManagement] fetchIndicatorScriptsMetadata success'); + this.initializeOrRefreshOverviewTable(); + this.loadingData = false; + }).catch(() => { + console.error('[AdminScriptManagement] fetchIndicatorScriptsMetadata failed'); + this.loadingData = false; + }); + return; + } + + if (crudType === 'add') { + this.scriptExchange + .addSingleProcessScriptMetadata( + this.scriptExchange.getProcessScriptMetadataById(targetScriptId) + ); + this.initializeOrRefreshOverviewTable(); + this.loadingData = false; + } else if (crudType === 'edit') { + // Replace with latest fetched metadata + this.scriptExchange.replaceSingleProcessScriptMetadata( + this.scriptExchange.getProcessScriptMetadataById(targetScriptId) + ); + this.initializeOrRefreshOverviewTable(); + this.loadingData = false; + } else if (crudType === 'delete') { + if (Array.isArray(targetScriptId)) { + for (const id of targetScriptId) { + this.scriptExchange.deleteSingleProcessScriptMetadata(id); + } + } else { + this.scriptExchange.deleteSingleProcessScriptMetadata(targetScriptId); + } + this.initializeOrRefreshOverviewTable(); + this.loadingData = false; + } else { + this.loadingData = false; + } + } + + onClickDeleteDatasets(): void { + this.loadingData = true; + let markedEntriesForDeletion: any[] = []; + try { markedEntriesForDeletion = this.gridHelper.getSelectedScriptsFromApi(this.gridApi); } catch {} + + const modalRef = this.modalService.open(ScriptDeleteModalComponent, { + backdrop: true, + keyboard: false, + container: 'body', + animation: false, + modalDialogClass: 'script-delete-modal', + windowClass: 'script-delete-modal-window' + }); + if ((modalRef as any).componentInstance && typeof (modalRef as any).componentInstance.onDeleteScripts === 'function') { + setTimeout(() => { + (modalRef as any).componentInstance.onDeleteScripts(markedEntriesForDeletion); + }, 0); + } + modalRef.result.then(() => { + this.initializeOrRefreshOverviewTable(); + }).catch(() => {}); + this.loadingData = false; + } + + onGridReady(event: any): void { + try { this.gridApi = event?.api || null; } catch { this.gridApi = null; } + console.debug('[AdminScriptManagement] onGridReady set gridApi:', !!this.gridApi); + if (this.gridApi) { + try { this.gridApi.paginationSetPageSize(this.paginationPageSize); } catch {} + } + } + + onPaginationPageSizeChanged(newPageSize: number): void { + this.paginationPageSize = Number(newPageSize); + // Keep GridOptions in sync for built-in pagination panel controls + if (this.scriptsGridOptions) { + (this.scriptsGridOptions as any).paginationPageSize = this.paginationPageSize; + (this.scriptsGridOptions as any).paginationPageSizeSelector = this.paginationPageSizeSelector; + } + if (this.gridApi) { + this.gridApi.paginationSetPageSize(this.paginationPageSize); + } + } + + goToPrevPage(): void { if (this.gridApi) { this.gridApi.paginationGoToPreviousPage(); } } + goToNextPage(): void { if (this.gridApi) { this.gridApi.paginationGoToNextPage(); } } + + get currentPage(): number { try { return (this.gridApi?.paginationGetCurrentPage?.() || 0) + 1; } catch { return 0; } } + get totalPages(): number { try { return this.gridApi?.paginationGetTotalPages?.() || 0; } catch { return 0; } } + + checkCreatePermission(): boolean { + try { return !!this.scriptExchange.checkCreatePermission(); } catch { return false; } + } + + checkDeletePermission(): boolean { + try { return !!this.scriptExchange.checkDeletePermission(); } catch { return false; } + } + + onClickCreateScript(): void { + const modalRef = this.modalService.open(ScriptAddModalComponent, { + backdrop: true, + keyboard: false, + container: 'body', + animation: false, + modalDialogClass: 'script-add-modal modal-xl', + windowClass: 'script-add-modal-window' + }); + modalRef.result.then(() => { + this.initializeOrRefreshOverviewTable(); + }).catch(() => {}); + } +} + + + + + + diff --git a/app/components/ngComponents/admin/adminScriptManagement/scriptAddModal/script-add-modal.component.css b/app/components/ngComponents/admin/adminScriptManagement/scriptAddModal/script-add-modal.component.css new file mode 100644 index 000000000..7151b5d0a --- /dev/null +++ b/app/components/ngComponents/admin/adminScriptManagement/scriptAddModal/script-add-modal.component.css @@ -0,0 +1,156 @@ +.loading-overlay-admin-panel { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: rgba(255,255,255,0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.icon-spin { + -webkit-animation: icon-spin 2s infinite linear; + animation: icon-spin 2s infinite linear; +} + +@-webkit-keyframes icon-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} + +/* Multistep form styles to mirror legacy */ +.multiStepForm #progressbar { + margin-bottom: 30px; + overflow: hidden; + counter-reset: step; + display: flex; + padding-left: 0; +} +.multiStepForm #progressbar li { + list-style-type: none; + color: #666; + text-transform: none; + position: relative; + text-align: center; +} +.multiStepForm #progressbar li.active { + color: #000; + font-weight: bold; +} +.multiStepForm fieldset { + border: 0; +} +.fs-title { font-size: 18px; margin-bottom: 15px; } +.fs-subtitle { font-size: 14px; margin-bottom: 10px; color: #666; } +.action-button { padding: 6px 12px; } +.action-button-previous { padding: 6px 12px; } + +/* Match progress bar styling from spatial unit modal */ +#progressbar { + margin-bottom: 10px; + overflow: hidden; + counter-reset: step; +} + +#progressbar li { + list-style-type: none; + color: black; + text-transform: uppercase; + font-size: 9px; + float: left; + position: relative; + letter-spacing: 1px; + cursor: pointer; +} + +#progressbar li:before { + content: counter(step); + counter-increment: step; + width: 24px; + height: 24px; + line-height: 26px; + display: block; + font-size: 12px; + color: #333; + background: #cccc; + border-radius: 25px; + margin: 0 auto 10px auto; + transform: translateZ(-1px); +} + +#progressbar li:after { + content: ''; + width: 100%; + height: 2px; + background: #cccc; + position: absolute; + left: -50%; + top: 9px; + z-index: -1; +} + +#progressbar li:first-child:after { + content: none; +} + +#progressbar li.active:before, #progressbar li.active:after { + background: var(--kommonitor-primary); + color: white; +} + +#progressbar li.clickable { cursor: pointer; transition: all 0.3s ease; } +#progressbar li.clickable:hover { color: var(--kommonitor-primary); } +#progressbar li.clickable:hover:before { background: var(--kommonitor-primary); transform: scale(1.1); } + +/* Modal sizing via window/dialog classes with CSS custom properties */ +/* Defaults can be overridden by setting these variables on body/.script-add-modal-window */ +:root { + --script-add-modal-max-width: 85%; + --script-add-modal-width: 85%; +} + +/* Apply sizing when dialog gets our custom class */ +::ng-deep .modal-dialog.script-add-modal { + max-width: var(--script-add-modal-max-width) !important; + width: var(--script-add-modal-width) !important; + margin: 1.75rem auto; +} + +/* Allow overriding via windowClass (adds class on the modal window container) */ +::ng-deep .script-add-modal-window .modal-dialog { + max-width: var(--script-add-modal-max-width) !important; + width: var(--script-add-modal-width) !important; +} + +/* Ensure modal content scrolls properly */ +::ng-deep .modal-content { + max-height: 90vh; + overflow-y: auto; +} + +::ng-deep .modal-body { + max-height: calc(90vh - 120px); + overflow-y: auto; + padding: 2rem; +} +@keyframes icon-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} + + diff --git a/app/components/ngComponents/admin/adminScriptManagement/scriptAddModal/script-add-modal.component.html b/app/components/ngComponents/admin/adminScriptManagement/scriptAddModal/script-add-modal.component.html new file mode 100644 index 000000000..3a130b71e --- /dev/null +++ b/app/components/ngComponents/admin/adminScriptManagement/scriptAddModal/script-add-modal.component.html @@ -0,0 +1,364 @@ + + + + + + + diff --git a/app/components/ngComponents/admin/adminScriptManagement/scriptAddModal/script-add-modal.component.ts b/app/components/ngComponents/admin/adminScriptManagement/scriptAddModal/script-add-modal.component.ts new file mode 100644 index 000000000..349735549 --- /dev/null +++ b/app/components/ngComponents/admin/adminScriptManagement/scriptAddModal/script-add-modal.component.ts @@ -0,0 +1,208 @@ +import { Component } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { KommonitorIndicatorDataExchangeService } from 'services/adminIndicatorUnit/kommonitor-data-exchange.service'; +import { KommonitorScriptHelperService } from 'services/script-management/kommonitor-script-helper.service'; + +@Component({ + selector: 'app-script-add-modal', + templateUrl: './script-add-modal.component.html', + styleUrls: ['./script-add-modal.component.css'] +}) +export class ScriptAddModalComponent { + loadingData: boolean = false; + + // multistep + currentStep: number = 0; + + datasetName: string = ''; + description: string = ''; + + indicatorNameFilter: string = ''; + georesourceNameFilter: string = ''; + + selectedTargetIndicator: any = null; + tmpIndicatorSelection: any = null; + tmpGeoresourceSelection: any = null; + + requiredIndicators: any[] = []; + requiredGeoresources: any[] = []; + + parameterNameTmp: string = ''; + parameterDescriptionTmp: string = ''; + parameterDataTypeTmp: any = null; + parameterDefaultValueTmp: any = ''; + parameterNumericMinValueTmp: number | null = null; + parameterNumericMaxValueTmp: number | null = null; + + scriptCodePreview: string | undefined = undefined; + + errorMessagePart: string | undefined = undefined; + errorMessagePartIndicatorMetadata: string | undefined = undefined; + + constructor( + public activeModal: NgbActiveModal, + public indicatorExchange: KommonitorIndicatorDataExchangeService, + public scriptHelper: KommonitorScriptHelperService + ) { + try { this.scriptHelper.reset(); } catch {} + } + + get availableIndicators(): any[] { + try { return this.indicatorExchange.availableIndicators || []; } catch { return []; } + } + + get availableGeoresources(): any[] { + try { return this.indicatorExchange.availableGeoresources || []; } catch { return []; } + } + + get availableScriptDataTypes(): any[] { + try { return this.scriptHelper.availableScriptDataTypes || []; } catch { return []; } + } + + get availableScriptTypeOptions(): any[] { + try { return this.scriptHelper.availableScriptTypeOptions || []; } catch { return []; } + } + + get filteredIndicators(): any[] { + const all = this.availableIndicators || []; + const f = (this.indicatorNameFilter || '').toLowerCase(); + if (!f) { return all; } + return all.filter((ind: any) => { + const name = (ind?.indicatorName || '').toLowerCase(); + const unit = (ind?.unit || '').toLowerCase(); + return name.includes(f) || unit.includes(f); + }); + } + + get filteredGeoresources(): any[] { + const all = this.availableGeoresources || []; + const f = (this.georesourceNameFilter || '').toLowerCase(); + if (!f) { return all; } + return all.filter((g: any) => { + const name = (g?.datasetName || '').toLowerCase(); + return name.includes(f); + }); + } + + onChangeTargetIndicator(indicator: any): void { + this.selectedTargetIndicator = indicator || null; + try { this.scriptHelper.targetIndicator = this.selectedTargetIndicator; } catch {} + } + + addBaseIndicator(): void { + if (!this.tmpIndicatorSelection) { return; } + try { this.scriptHelper.addBaseIndicator(this.tmpIndicatorSelection); } catch {} + this.requiredIndicators = [...(this.requiredIndicators || []), this.tmpIndicatorSelection]; + this.tmpIndicatorSelection = null; + } + + removeBaseIndicator(ind: any): void { + try { this.scriptHelper.removeBaseIndicator(ind); } catch {} + this.requiredIndicators = (this.requiredIndicators || []).filter((x: any) => x?.indicatorId !== ind?.indicatorId); + } + + addBaseGeoresource(): void { + if (!this.tmpGeoresourceSelection) { return; } + try { this.scriptHelper.addBaseGeoresource(this.tmpGeoresourceSelection); } catch {} + this.requiredGeoresources = [...(this.requiredGeoresources || []), this.tmpGeoresourceSelection]; + this.tmpGeoresourceSelection = null; + } + + removeBaseGeoresource(geo: any): void { + try { this.scriptHelper.removeBaseGeoresource(geo); } catch {} + this.requiredGeoresources = (this.requiredGeoresources || []).filter((x: any) => x?.georesourceId !== geo?.georesourceId); + } + + onParamTypeChange(): void { + // reset numeric constraints when type changes + if (!this.parameterDataTypeTmp || (this.parameterDataTypeTmp.apiName !== 'integer' && this.parameterDataTypeTmp.apiName !== 'double')) { + this.parameterNumericMinValueTmp = null; + this.parameterNumericMaxValueTmp = null; + } + } + + addScriptParameter(): void { + if (!this.parameterNameTmp || !this.parameterDescriptionTmp || this.parameterDefaultValueTmp === undefined || this.parameterDefaultValueTmp === null || !this.parameterDataTypeTmp) { + return; + } + try { + this.scriptHelper.addScriptParameter( + this.parameterNameTmp, + this.parameterDescriptionTmp, + this.parameterDataTypeTmp, + this.parameterDefaultValueTmp, + this.parameterNumericMinValueTmp, + this.parameterNumericMaxValueTmp + ); + } catch {} + this.parameterNameTmp = ''; + this.parameterDescriptionTmp = ''; + this.parameterDefaultValueTmp = ''; + this.parameterDataTypeTmp = null; + this.parameterNumericMinValueTmp = null; + this.parameterNumericMaxValueTmp = null; + } + + removeScriptParameter(p: any): void { + try { this.scriptHelper.removeScriptParameter(p); } catch {} + } + + onScriptFileSelected(event: any): void { + const file: File | undefined = event?.target?.files?.[0]; + if (!file) { return; } + const reader = new FileReader(); + reader.onload = () => { + const content = String(reader.result || ''); + this.scriptCodePreview = content; + try { this.scriptHelper.scriptCode_readableString = content; } catch {} + }; + reader.onerror = () => { + this.errorMessagePart = 'Fehler beim Lesen der Skriptdatei.'; + }; + reader.readAsText(file); + } + + canSubmit(): boolean { + const hasBasics = !!this.datasetName && !!this.description && !!this.selectedTargetIndicator; + const hasCode = !!this.scriptHelper.scriptCode_readableString; + const hasInputs = (this.requiredIndicators?.length || 0) > 0 || (this.requiredGeoresources?.length || 0) > 0; + return hasBasics && hasCode && hasInputs && !this.loadingData; + } + + async onRegisterScript(): Promise { + if (!this.canSubmit()) { return; } + this.loadingData = true; + this.errorMessagePart = undefined; + this.errorMessagePartIndicatorMetadata = undefined; + try { + const resp = await this.scriptHelper.postNewScript(this.datasetName, this.description, this.selectedTargetIndicator); + // optionally update indicator method if requested in helper + try { + if (this.scriptHelper.scriptFormulaHTML_overwriteTargetIndicatorMethod) { + await this.scriptHelper.replaceMethodMetadataForTargetIndicator(this.selectedTargetIndicator); + } + } catch (metaErr: any) { + try { this.errorMessagePartIndicatorMetadata = this.indicatorExchange.syntaxHighlightJSON(metaErr?.data || metaErr); } catch {} + } + this.loadingData = false; + this.activeModal.close('success'); + } catch (err: any) { + try { this.errorMessagePart = this.indicatorExchange.syntaxHighlightJSON(err?.data || err); } catch {} + this.loadingData = false; + } + } + + // step controls + goToStep(step: any): void { + const idx = typeof step === 'number' ? step : parseInt(step, 10); + if (isNaN(idx)) { return; } + if (idx < 0) { this.currentStep = 0; return; } + if (idx > 2) { this.currentStep = 2; return; } + this.currentStep = idx; + } + + nextStep(): void { this.goToStep(this.currentStep + 1); } + prevStep(): void { this.goToStep(this.currentStep - 1); } +} + + diff --git a/app/components/ngComponents/admin/adminScriptManagement/scriptDeleteModal/script-delete-modal.component.css b/app/components/ngComponents/admin/adminScriptManagement/scriptDeleteModal/script-delete-modal.component.css new file mode 100644 index 000000000..a13a0e0b5 --- /dev/null +++ b/app/components/ngComponents/admin/adminScriptManagement/scriptDeleteModal/script-delete-modal.component.css @@ -0,0 +1,14 @@ +.loading-overlay-admin-panel { + position: relative; +} + +.icon-spin { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + + diff --git a/app/components/ngComponents/admin/adminScriptManagement/scriptDeleteModal/script-delete-modal.component.html b/app/components/ngComponents/admin/adminScriptManagement/scriptDeleteModal/script-delete-modal.component.html new file mode 100644 index 000000000..4f885557c --- /dev/null +++ b/app/components/ngComponents/admin/adminScriptManagement/scriptDeleteModal/script-delete-modal.component.html @@ -0,0 +1,60 @@ + + + + + diff --git a/app/components/ngComponents/admin/adminScriptManagement/scriptDeleteModal/script-delete-modal.component.ts b/app/components/ngComponents/admin/adminScriptManagement/scriptDeleteModal/script-delete-modal.component.ts new file mode 100644 index 000000000..b7a5803a4 --- /dev/null +++ b/app/components/ngComponents/admin/adminScriptManagement/scriptDeleteModal/script-delete-modal.component.ts @@ -0,0 +1,111 @@ +import { Component, Input, OnDestroy, OnInit, Inject } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { HttpClient } from '@angular/common/http'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; +import { firstValueFrom, Subscription } from 'rxjs'; + +@Component({ + selector: 'script-delete-modal-new', + templateUrl: './script-delete-modal.component.html', + styleUrls: ['./script-delete-modal.component.css'] +}) +export class ScriptDeleteModalComponent implements OnInit, OnDestroy { + @Input() datasetsToDelete: any[] = []; + + loadingData: boolean = false; + successfullyDeletedDatasets: any[] = []; + failedDatasetsAndErrors: Array<[any, any]> = []; + successMessage: string = ''; + errorMessage: string = ''; + + private subscriptions: Subscription[] = []; + + constructor( + public activeModal: NgbActiveModal, + private http: HttpClient, + private broadcastService: BroadcastService, + @Inject('kommonitorDataExchangeService') private angularJsDataExchangeService: any + ) {} + + ngOnInit(): void { + this.resetScriptsDeleteForm(); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(s => s.unsubscribe()); + } + + onDeleteScripts(datasets: any[]): void { + this.datasetsToDelete = Array.isArray(datasets) ? datasets : []; + this.resetScriptsDeleteForm(); + } + + resetScriptsDeleteForm(): void { + this.successfullyDeletedDatasets = []; + this.failedDatasetsAndErrors = []; + this.successMessage = ''; + this.errorMessage = ''; + } + + async deleteScripts(): Promise { + this.loadingData = true; + this.successMessage = ''; + this.errorMessage = ''; + this.successfullyDeletedDatasets = []; + this.failedDatasetsAndErrors = []; + + const baseUrl: string = this.angularJsDataExchangeService?.baseUrlToKomMonitorDataAPI || ''; + const deletePromises = (this.datasetsToDelete || []).map(async (dataset: any) => { + const id = dataset?.scriptId; + if (!id) { return; } + try { + await firstValueFrom(this.http.delete(`${baseUrl}/process-scripts/${id}`)); + this.successfullyDeletedDatasets.push(dataset); + } catch (error: any) { + const errorPayload = error?.error ? error.error : error; + this.failedDatasetsAndErrors.push([dataset, errorPayload]); + } + }); + + await Promise.all(deletePromises); + + if (this.failedDatasetsAndErrors.length > 0) { + this.errorMessage = 'Einige Skripte konnten nicht gelöscht werden.'; + } + if (this.successfullyDeletedDatasets.length > 0) { + this.successMessage = `${this.successfullyDeletedDatasets.length} Skript(e) erfolgreich gelöscht.`; + + // Refresh script overview table + const deletedIds = this.successfullyDeletedDatasets.map(d => d.scriptId); + this.broadcastService.broadcast('refreshScriptOverviewTable', { crudType: 'delete', scriptId: deletedIds }); + + // Refresh all admin dashboard diagrams due to modified metadata + setTimeout(() => { + this.broadcastService.broadcast('refreshAdminDashboardDiagrams'); + }, 300); + } + + this.loadingData = false; + + // Auto-close modal if all successful and none failed + if (this.successfullyDeletedDatasets.length > 0 && this.failedDatasetsAndErrors.length === 0) { + setTimeout(() => { + this.activeModal.close({ action: 'deleted', deleted: this.successfullyDeletedDatasets }); + }, 1200); + } + } + + hideSuccessAlert(): void { + this.successMessage = ''; + } + + hideErrorAlert(): void { + this.errorMessage = ''; + } + + closeModal(): void { + this.activeModal.dismiss('cancel'); + } +} + + diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/admin-spatial-units-management.component.css b/app/components/ngComponents/admin/adminSpatialUnitsManagement/admin-spatial-units-management.component.css new file mode 100644 index 000000000..78a40e496 --- /dev/null +++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/admin-spatial-units-management.component.css @@ -0,0 +1,239 @@ +/* Admin Spatial Units Management Component Styles */ + +.loading-overlay-admin-panel { + width: 100%; + height: 100%; + position: absolute; + background-color: rgb(128,128,128); + top: 0; + left: 0; + border-radius: 5px; + opacity: 0.8; + z-index: 100000; +} + +.loading-overlay-admin-panel .icon-spin { + font-size: 25px; + width: 25px; + height: 25px; + position: absolute; + top: 50%; + left: 50%; + margin: -12.5px 0 0 -12.5px; + + -webkit-animation: spin 1s infinite linear; + -moz-animation: spin 1s infinite linear; + -o-animation: spin 1s infinite linear; + animation: spin 1s infinite linear; + -webkit-transform-origin: 50% 50%; + transform-origin:50% 50%; + -ms-transform-origin:50% 50%; /* IE 9 */ +} + +.adminTableButtonWrapper { + display: flex; + float: right; + background: transparent; + font-size: 12px; + position: absolute; + top: 15px; + right: 40px; +} + +.verticalAlign { + display: table; +} + +.verticalAlign * { + display: table-cell; + vertical-align: middle; + padding-right: 1em; +} + +.verticalAlign div { + position: relative; + top: .2em; +} + +/* Switch styles */ +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 28px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +/* The slider */ +.switchslider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; +} + +.switchslider:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .switchslider { + background-color: var(--kommonitor-primary); +} + +input:focus + .switchslider { + box-shadow: 0 0 1px var(--kommonitor-primary); +} + +input:checked + .switchslider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); +} + +/* Rounded sliders */ +.switchslider.round { + border-radius: 20px; +} + +.switchslider.round:before { + border-radius: 50%; +} + +/* AG Grid customizations */ +.ag-theme-alpine { + --ag-header-height: 50px; + --ag-row-height: 60px; + --ag-header-background-color: #f4f4f4; + --ag-header-foreground-color: #333; + --ag-border-color: #ddd; +} + +.admin-table-wrapper { + margin-top: 20px; +} + +/* Box styles */ +.box-primary { + border-top-color: #3c8dbc; +} + +.box-header { + padding: 15px; + border-bottom: 1px solid #f4f4f4; +} + +.box-title { + margin: 0; + font-size: 18px; + font-weight: 600; +} + +.box-body { + padding: 15px; +} + +/* Button styles */ +.btn-success { + background-color: #00a65a; + border-color: #008d4c; +} + +.btn-success:hover { + background-color: #008d4c; + border-color: #006633; +} + +.btn-success:disabled { + background-color: #ccc; + border-color: #ccc; + cursor: not-allowed; +} + +/* Content header styles */ +.content-header { + padding: 15px 0; + background-color: #f9f9f9; + border-bottom: 1px solid #ddd; +} + +.content-header h1 { + margin: 0; + font-size: 24px; + font-weight: 300; +} + +.content-header h1 small { + font-size: 14px; + color: #777; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .adminTableButtonWrapper { + flex-direction: column; + gap: 10px; + } + + .verticalAlign { + justify-content: center; + } +} + +/* AG Grid pagination styles */ +.ag-theme-alpine .ag-paging-panel { + background-color: #f8f9fa; + border-top: 1px solid #dee2e6; + padding: 8px 12px; + font-size: 12px; +} + +.ag-theme-alpine .ag-paging-button { + background-color: #fff; + border: 1px solid #dee2e6; + color: #495057; + padding: 4px 8px; + margin: 0 2px; + border-radius: 3px; + cursor: pointer; +} + +.ag-theme-alpine .ag-paging-button:hover { + background-color: #e9ecef; + border-color: #adb5bd; +} + +.ag-theme-alpine .ag-paging-button:disabled { + background-color: #f8f9fa; + color: #6c757d; + cursor: not-allowed; +} + +.ag-theme-alpine .ag-paging-page-summary-panel { + color: #6c757d; +} + +.ag-theme-alpine .ag-paging-page-size-select { + background-color: #fff; + border: 1px solid #dee2e6; + border-radius: 3px; + padding: 2px 4px; + font-size: 12px; +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/admin-spatial-units-management.component.html b/app/components/ngComponents/admin/adminSpatialUnitsManagement/admin-spatial-units-management.component.html new file mode 100644 index 000000000..7af2dff31 --- /dev/null +++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/admin-spatial-units-management.component.html @@ -0,0 +1,94 @@ +
+ +
+
+ +
+
+ + + +
+

+ Verwalten der Raumebenen + Info +

+ +
+
+ Nur editierbare Datensätze anzeigen +
+ +
+
+ + + + + + +
+
+ + +
+ + +
+
+ + +
+
+

Raumebenen

+
+ +
+ +
+ + + + +
+
+ +
+ + +
+
+ +
+ +
\ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/admin-spatial-units-management.component.ts b/app/components/ngComponents/admin/adminSpatialUnitsManagement/admin-spatial-units-management.component.ts new file mode 100644 index 000000000..fc7f6d729 --- /dev/null +++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/admin-spatial-units-management.component.ts @@ -0,0 +1,763 @@ +import { Component, Inject, OnInit, NgZone, OnDestroy, ViewChild, ElementRef } from '@angular/core'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; +import { DOCUMENT } from '@angular/common'; +import { Subscription } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { SpatialUnitAddModalComponent } from './spatialUnitAddModal/spatial-unit-add-modal.component'; +import { SpatialUnitEditMetadataModalComponent } from './spatialUnitEditMetadataModal/spatial-unit-edit-metadata-modal.component'; +import { SpatialUnitEditFeaturesModalComponent } from './spatialUnitEditFeaturesModal/spatial-unit-edit-features-modal.component'; +import { SpatialUnitEditUserRolesModalComponent } from './spatialUnitEditUserRolesModal/spatial-unit-edit-user-roles-modal.component'; +import { SpatialUnitDeleteModalComponent } from './spatialUnitDeleteModal/spatial-unit-delete-modal.component'; +import { KommonitorDataExchangeService } from 'services/adminSpatialUnit/kommonitor-data-exchange.service'; +import { KommonitorCacheHelperService } from 'services/adminSpatialUnit/kommonitor-cache-helper.service'; +import { KommonitorDataGridHelperService } from 'services/adminSpatialUnit/kommonitor-data-grid-helper.service'; +import { AgGridAngular } from 'ag-grid-angular'; +import { ColDef, GridOptions, GridApi, ColumnApi, FirstDataRenderedEvent, ColumnResizedEvent } from 'ag-grid-community'; +declare const $: any; +declare const __env: any; + +@Component({ + selector: 'admin-spatial-units-management-new', + templateUrl: './admin-spatial-units-management.component.html', + styleUrls: ['./admin-spatial-units-management.component.css'] +}) +export class AdminSpatialUnitsManagementComponent implements OnInit, OnDestroy { + @ViewChild('spatialUnitOverviewTable', { static: true }) spatialUnitOverviewTable!: AgGridAngular; + + public loadingData: boolean = true; + public initializationCompleted: boolean = false; + public tableViewSwitcher: boolean = false; + private subscriptions: Subscription[] = []; + + // AG Grid properties + public columnDefs: ColDef[] = []; + public rowData: any[] = []; + public defaultColDef: ColDef = {}; + public gridOptions: GridOptions = {}; + private gridApi!: GridApi; + private columnApi!: ColumnApi; + + // Pagination properties + public paginationPageSize: number = 10; + public paginationPageSizeSelector: number[] = [10, 25, 50, 100]; + + constructor( + @Inject(DOCUMENT) private document: Document, + private zone: NgZone, + private modalService: NgbModal, + private broadcastService: BroadcastService, + private http: HttpClient, + public kommonitorDataExchangeService: KommonitorDataExchangeService, + private kommonitorCacheHelperService: KommonitorCacheHelperService, + private kommonitorDataGridHelperService: KommonitorDataGridHelperService + ) {} + + ngOnInit(): void { + console.log('AdminSpatialUnitsManagementComponent ngOnInit started'); + + // Subscribe to spatial units data + const spatialUnitsSub = this.kommonitorDataExchangeService.spatialUnits$.subscribe(spatialUnits => { + console.log('Spatial units subscription received:', spatialUnits); + if (spatialUnits && spatialUnits.length > 0) { + console.log('Building data grid with', spatialUnits.length, 'spatial units'); + this.loadingData = false; + this.initializationCompleted = true; + this.buildDataGrid_spatialUnits(spatialUnits); + } else { + console.log('No spatial units data received yet'); + } + }); + this.subscriptions.push(spatialUnitsSub); + + // Subscribe to loading state + const loadingSub = this.kommonitorDataExchangeService.loading$.subscribe(loading => { + console.log('Loading state changed:', loading); + this.loadingData = loading; + }); + this.subscriptions.push(loadingSub); + + // Subscribe to error state + const errorSub = this.kommonitorDataExchangeService.error$.subscribe(error => { + if (error) { + console.error('Data exchange error:', error); + // You can add error handling UI here + } + }); + this.subscriptions.push(errorSub); + + this.setupEventListeners(); + + // Fetch spatial units data + this.fetchSpatialUnitsData(); + + // Add a fallback timeout to prevent infinite loading + setTimeout(() => { + if (this.loadingData) { + console.log('Fallback timeout reached, checking data again...'); + this.fetchSpatialUnitsData(); + + // If still no data after fallback, stop loading anyway + if (!this.kommonitorDataExchangeService.availableSpatialUnits || this.kommonitorDataExchangeService.availableSpatialUnits.length === 0) { + console.log('No data after fallback timeout, stopping loading'); + this.loadingData = false; + this.initializationCompleted = true; + } + } + }, 3000); // 3 second timeout + } + + ngOnDestroy(): void { + this.subscriptions.forEach(sub => sub.unsubscribe()); + } + + private setupEventListeners(): void { + // Listen for the global metadata loading completion event + const sub = this.broadcastService.currentBroadcastMsg.subscribe(data => { + if (data.msg === 'initialMetadataLoadingCompleted') { + this.zone.run(() => { + this.fetchSpatialUnitsData(); + }); + } + else if (data.msg === 'refreshSpatialUnitOverviewTable') { + this.zone.run(() => { + this.loadingData = true; + // Extract crudType and targetSpatialUnitId from the broadcast data values + const crudType = (data.values as any)?.crudType; + const targetSpatialUnitId = (data.values as any)?.targetSpatialUnitId; + this.refreshSpatialUnitOverviewTable(crudType, targetSpatialUnitId); + }); + } + // Handle grid button click events + else if (data.msg === 'onEditSpatialUnitMetadata') { + this.zone.run(() => { + this.onClickEditMetadata(data.values); + }); + } + else if (data.msg === 'onEditSpatialUnitFeatures') { + this.zone.run(() => { + this.onClickEditFeatures(data.values); + }); + } + else if (data.msg === 'onEditSpatialUnitUserRoles') { + this.zone.run(() => { + this.onClickEditUserRoles(data.values); + }); + } + else if (data.msg === 'onDeleteSpatialUnits') { + this.zone.run(() => { + // Ensure data.values is an array for delete operation + const datasetsToDelete = Array.isArray(data.values) ? data.values : [data.values]; + this.onClickDeleteSpatialUnits(datasetsToDelete); + }); + } + }); + this.subscriptions.push(sub); + } + + /** + * Fetch spatial units data from the service + */ + private fetchSpatialUnitsData(): void { + console.log('Fetching spatial units data...'); + + // Get current roles or use empty array as fallback + const currentRoles = this.kommonitorDataExchangeService.currentKeycloakLoginRoles || []; + console.log('Current roles:', currentRoles); + + this.kommonitorDataExchangeService.fetchSpatialUnitsMetadata(currentRoles).subscribe({ + next: (spatialUnits) => { + console.log('Spatial units data received:', spatialUnits); + // The data will be handled by the subscription in ngOnInit + }, + error: (error) => { + console.error('Error fetching spatial units:', error); + this.loadingData = false; + this.initializationCompleted = true; + } + }); + } + + public initializeOrRefreshOverviewTable(): void { + console.log('Initializing/refreshing overview table...'); + this.fetchSpatialUnitsData(); + } + + // Debug method to force stop loading + stopLoading(): void { + this.loadingData = false; + this.initializationCompleted = true; + } + + // Table view switcher method + onTableViewSwitch(): void { + // Filter the data based on the tableViewSwitcher state + // For now, just refresh the table + this.initializeOrRefreshOverviewTable(); + } + + // Alias for the add spatial unit modal (matching HTML template) + openAddSpatialUnitModal(): void { + this.onClickAddSpatialUnit(); + } + + // Modal event handlers + onClickAddSpatialUnit(): void { + const modalRef = this.modalService.open(SpatialUnitAddModalComponent, { + // omit size to avoid Bootstrap max-width caps like modal-lg + backdrop: true, + keyboard: false, + container: 'body', + animation: false, + modalDialogClass: 'spatial-unit-add-modal', + windowClass: 'spatial-unit-add-modal-window' + }); + + modalRef.result.then((result) => { + if (result) { + this.initializeOrRefreshOverviewTable(); + } + }).catch(() => { + // Modal dismissed + }); + } + + onClickEditMetadata(spatialUnitMetadata: any): void { + const modalRef = this.modalService.open(SpatialUnitEditMetadataModalComponent, { + backdrop: true, + keyboard: false, + container: 'body', + animation: false, + modalDialogClass: 'spatial-unit-add-modal', + windowClass: 'spatial-unit-add-modal-window' + }); + + modalRef.componentInstance.currentSpatialUnitDataset = spatialUnitMetadata; + + modalRef.result.then((result) => { + if (result) { + this.initializeOrRefreshOverviewTable(); + } + }).catch(() => { + // Modal dismissed + }); + } + + onClickEditFeatures(spatialUnitMetadata: any): void { + const modalRef = this.modalService.open(SpatialUnitEditFeaturesModalComponent, { + backdrop: true, + keyboard: false, + container: 'body', + animation: false, + modalDialogClass: 'spatial-unit-add-modal', + windowClass: 'spatial-unit-add-modal-window' + }); + + modalRef.componentInstance.currentSpatialUnitDataset = spatialUnitMetadata; + + modalRef.result.then((result) => { + if (result) { + this.initializeOrRefreshOverviewTable(); + } + }).catch(() => { + // Modal dismissed + }); + } + + onClickEditUserRoles(spatialUnitMetadata: any): void { + const modalRef = this.modalService.open(SpatialUnitEditUserRolesModalComponent, { + backdrop: true, + keyboard: false, + container: 'body', + animation: false, + modalDialogClass: 'spatial-unit-add-modal', + windowClass: 'spatial-unit-add-modal-window' + }); + + modalRef.componentInstance.currentSpatialUnitDataset = spatialUnitMetadata; + + modalRef.result.then((result) => { + if (result) { + this.initializeOrRefreshOverviewTable(); + } + }).catch(() => { + // Modal dismissed + }); + } + + onClickDeleteSpatialUnits(spatialUnitsMetadata: any[]): void { + const modalRef = this.modalService.open(SpatialUnitDeleteModalComponent, { + backdrop: true, + keyboard: false, + container: 'body', + animation: false, + modalDialogClass: 'spatial-unit-add-modal', + windowClass: 'spatial-unit-add-modal-window' + }); + + modalRef.componentInstance.datasetsToDelete = spatialUnitsMetadata; + + modalRef.result.then((result) => { + if (result) { + this.initializeOrRefreshOverviewTable(); + } + }).catch(() => { + // Modal dismissed + }); + } + + // Utility methods + checkCreatePermission(): boolean { + return this.kommonitorDataExchangeService.checkCreatePermission(); + } + + refreshSpatialUnitOverviewTable(crudType?: string, targetSpatialUnitId?: string | string[]): void { + if (!crudType || !targetSpatialUnitId) { + // Refetch all metadata from spatial units to update table + this.kommonitorDataExchangeService.fetchSpatialUnitsMetadata( + this.kommonitorDataExchangeService.currentKeycloakLoginRoles + ).subscribe({ + next: (response) => { + this.initializeOrRefreshOverviewTable(); + this.loadingData = false; + }, + error: (response) => { + console.error('Error fetching spatial units metadata:', response); + this.loadingData = false; + } + }); + } + else if (crudType && targetSpatialUnitId) { + if (crudType === 'edit') { + // Fetch single spatial unit metadata and update the table + this.kommonitorCacheHelperService.fetchSingleSpatialUnitMetadata( + targetSpatialUnitId as string, + this.kommonitorDataExchangeService.currentKeycloakLoginRoles + ).subscribe({ + next: (data) => { + this.kommonitorDataExchangeService.replaceSingleSpatialUnitMetadata(data); + this.initializeOrRefreshOverviewTable(); + this.loadingData = false; + }, + error: (response) => { + console.error('Error fetching single spatial unit metadata:', response); + this.loadingData = false; + } + }); + } + else if (crudType === 'add') { + // Fetch single spatial unit metadata and add to table + this.kommonitorCacheHelperService.fetchSingleSpatialUnitMetadata( + targetSpatialUnitId as string, + this.kommonitorDataExchangeService.currentKeycloakLoginRoles + ).subscribe({ + next: (data) => { + this.kommonitorDataExchangeService.addSingleSpatialUnitMetadata(data); + this.initializeOrRefreshOverviewTable(); + this.loadingData = false; + }, + error: (response) => { + console.error('Error fetching single spatial unit metadata:', response); + this.loadingData = false; + } + }); + } + else if (crudType === 'delete') { + // Handle delete operation + if (typeof targetSpatialUnitId === 'string') { + this.kommonitorDataExchangeService.deleteSingleSpatialUnitMetadata(targetSpatialUnitId); + } else if (Array.isArray(targetSpatialUnitId)) { + for (const id of targetSpatialUnitId) { + this.kommonitorDataExchangeService.deleteSingleSpatialUnitMetadata(id); + } + } + this.initializeOrRefreshOverviewTable(); + this.loadingData = false; + } + } + } + + // AG Grid methods - using hybrid approach + private buildDataGrid_spatialUnits(spatialUnitMetadataArray: any[]): void { + // Get base configuration from service + const baseGridOptions = this.kommonitorDataGridHelperService.buildDataGridOptions_spatialUnits(spatialUnitMetadataArray); + + // Extract service configuration + this.columnDefs = baseGridOptions.columnDefs || []; + this.rowData = baseGridOptions.rowData || []; + this.defaultColDef = baseGridOptions.defaultColDef || {}; + + // Add component-specific columns that are not in the service + this.addComponentSpecificColumns(); + + // Override with component-specific settings + this.gridOptions = { + ...baseGridOptions, + columnDefs: this.columnDefs, // Use updated columnDefs + paginationPageSize: this.paginationPageSize, + paginationPageSizeSelector: this.paginationPageSizeSelector, + onGridReady: (params) => { + this.gridApi = params.api; + this.columnApi = params.columnApi; + }, + onFirstDataRendered: (event) => { + this.headerHeightSetter(); + // Click handler registration is now handled by the service + }, + onColumnResized: (event) => { + this.headerHeightSetter(); + } + }; + } + + // Add component-specific columns that are not in the service + private addComponentSpecificColumns(): void { + // Add the missing Umringslayer columns after the existing columns + this.columnDefs.push( + { + headerName: 'Linienfarbe (Umringslayer)', + minWidth: 200, + cellRenderer: (params: any) => params.data.outlineColor || '-', + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + (params.data.outlineColor || '-') + }, + { + headerName: 'Linienbreite (Umringslayer)', + minWidth: 200, + cellRenderer: (params: any) => params.data.outlineWidth || '-', + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + (params.data.outlineWidth || '-') + }, + { + headerName: 'Linienmuster (Umringslayer)', + minWidth: 200, + cellRenderer: (params: any) => params.data.outlineDashArrayString || '-', + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + (params.data.outlineDashArrayString || '-') + } + ); + } + + private buildDefaultColDef(): ColDef { + return { + editable: false, + sortable: true, + flex: 1, + minWidth: 200, + filter: true, + floatingFilter: true, + resizable: true, + wrapText: true, + autoHeight: true, + cellStyle: { + 'font-size': '12px', + 'white-space': 'normal !important', + 'line-height': '20px !important', + 'word-break': 'break-word !important', + 'padding-top': '17px', + 'padding-bottom': '17px' + } + }; + } + + private buildGridOptions(spatialUnitMetadataArray: any[]): GridOptions { + return { + suppressRowClickSelection: true, + rowSelection: 'multiple', + enableCellTextSelection: true, + ensureDomOrder: true, + pagination: true, + paginationPageSize: this.paginationPageSize, + paginationPageSizeSelector: this.paginationPageSizeSelector, + suppressColumnVirtualisation: true, + onGridReady: (params) => { + this.gridApi = params.api; + this.columnApi = params.columnApi; + }, + onFirstDataRendered: (event) => { + this.headerHeightSetter(); + // Click handler registration is now handled by the service + }, + onColumnResized: (event) => { + this.headerHeightSetter(); + } + }; + } + + /** + * Handle pagination page size change + */ + onPaginationPageSizeChanged(newPageSize: number): void { + this.paginationPageSize = newPageSize; + if (this.gridApi) { + this.gridApi.paginationSetPageSize(newPageSize); + } + } + + /** + * Get current pagination info + */ + getPaginationInfo(): any { + if (this.gridApi) { + return { + currentPage: this.gridApi.paginationGetCurrentPage(), + totalPages: this.gridApi.paginationGetTotalPages(), + totalRows: this.gridApi.paginationGetRowCount(), + pageSize: this.gridApi.paginationGetPageSize() + }; + } + return null; + } + + private buildDataGridColumnConfig_spatialUnits(spatialUnitMetadataArray: any[]): ColDef[] { + return [ + { + headerName: 'Editierfunktionen', + pinned: 'left', + maxWidth: 170, + checkboxSelection: false, + headerCheckboxSelection: false, + headerCheckboxSelectionFilteredOnly: true, + filter: false, + sortable: false, + cellRenderer: this.displayEditButtons_spatialUnits.bind(this) + }, + { headerName: 'Id', field: 'spatialUnitId', pinned: 'left', maxWidth: 125 }, + { headerName: 'Name', field: 'spatialUnitLevel', pinned: 'left', minWidth: 300 }, + { + headerName: 'Beschreibung', + minWidth: 400, + cellRenderer: (params: any) => params.data.metadata.description, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + params.data.metadata.description + }, + { headerName: 'Nächst niedrigere Raumebene', field: 'nextLowerHierarchyLevel', minWidth: 250 }, + { headerName: 'Nächst höhere Raumebene', field: 'nextUpperHierarchyLevel', minWidth: 250 }, + { + headerName: 'Gültigkeitszeitraum', + minWidth: 400, + cellRenderer: (params: any) => { + let html = '
    '; + for (const periodOfValidity of params.data.availablePeriodsOfValidity) { + html += '
  • '; + if (periodOfValidity.endDate) { + html += '

    ' + periodOfValidity.startDate + ' ‐ ' + periodOfValidity.endDate + '

    '; + } else { + html += '

    ' + periodOfValidity.startDate + ' ‐ heute

    '; + } + html += '
  • '; + } + html += '
'; + return html; + }, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => { + if (params.data.availablePeriodsOfValidity && params.data.availablePeriodsOfValidity.length > 1) { + return '' + JSON.stringify(params.data.availablePeriodsOfValidity); + } + return params.data.availablePeriodsOfValidity; + } + }, + { + headerName: 'Datenquelle', + minWidth: 400, + cellRenderer: (params: any) => params.data.metadata.datasource, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + params.data.metadata.datasource + }, + { + headerName: 'Datenhalter und Kontakt', + minWidth: 400, + cellRenderer: (params: any) => params.data.metadata.contact, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + params.data.metadata.contact + }, + { + headerName: 'Rollen', + minWidth: 400, + cellRenderer: (params: any) => this.kommonitorDataExchangeService.getAllowedRolesString(params.data.permissions), + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + this.kommonitorDataExchangeService.getAllowedRolesString(params.data.permissions) + }, + { + headerName: 'Öffentlich sichtbar', + minWidth: 400, + cellRenderer: (params: any) => params.data.isPublic ? 'ja' : 'nein', + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + (params.data.isPublic ? 'ja' : 'nein') + }, + { + headerName: 'Eigentümer', + minWidth: 400, + cellRenderer: (params: any) => this.kommonitorDataExchangeService.getRoleTitle(params.data.ownerId), + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + this.kommonitorDataExchangeService.getRoleTitle(params.data.ownerId) + } + ]; + } + + private buildDataGridRowData_spatialUnits(spatialUnitMetadataArray: any[]): any[] { + return spatialUnitMetadataArray.map(metadata => ({ + ...metadata, + spatialUnitId: metadata.spatialUnitId, + spatialUnitLevel: metadata.spatialUnitLevel + })); + } + + private displayEditButtons_spatialUnits(params: any): string { + const data = params.data; + let html = '
'; + + // Edit Metadata Button + html += ''; + + // Edit Features Button + html += ''; + + // Edit User Roles Button + html += ''; + + // Delete Button + html += ''; + + html += '
'; + return html; + } + + // Grid event handlers + onFirstDataRendered(event: FirstDataRenderedEvent): void { + this.headerHeightSetter(); + this.registerClickHandler_spatialUnits(); + } + + onColumnResized(event: ColumnResizedEvent): void { + this.headerHeightSetter(); + } + + onRowDataChanged(): void { + // Click handler registration is now handled by the service + } + + onModelUpdated(): void { + // Click handler registration is now handled by the service + } + + onViewportChanged(): void { + // Click handler registration is now handled by the service + } + + private registerClickHandler_spatialUnits(): void { + // Use event delegation on the grid container instead of individual buttons + // This ensures handlers work even for dynamically rendered buttons + const $ = (window as any).$; + + // Remove any existing handlers first to avoid duplicates + $('#spatialUnitOverviewTable').off('click', '.spatialUnitEditMetadataBtn'); + $('#spatialUnitOverviewTable').off('click', '.spatialUnitEditFeaturesBtn'); + $('#spatialUnitOverviewTable').off('click', '.spatialUnitEditUserRolesBtn'); + $('#spatialUnitOverviewTable').off('click', '.spatialUnitDeleteBtn'); + + // Edit Metadata Button - use event delegation + $('#spatialUnitOverviewTable').on('click', '.spatialUnitEditMetadataBtn', (event: any) => { + event.stopPropagation(); + event.preventDefault(); + + // Get the button element (could be the icon inside) + const button = $(event.target).closest('.spatialUnitEditMetadataBtn')[0]; + const spatialUnitId = button.id.split('_')[3]; + const spatialUnitMetadata = this.kommonitorDataExchangeService.getSpatialUnitMetadataById(spatialUnitId); + + if (spatialUnitMetadata) { + this.zone.run(() => { + this.onClickEditMetadata(spatialUnitMetadata); + }); + } + }); + + // Edit Features Button - use event delegation + $('#spatialUnitOverviewTable').on('click', '.spatialUnitEditFeaturesBtn', (event: any) => { + event.stopPropagation(); + event.preventDefault(); + + // Get the button element (could be the icon inside) + const button = $(event.target).closest('.spatialUnitEditFeaturesBtn')[0]; + const spatialUnitId = button.id.split('_')[3]; + const spatialUnitMetadata = this.kommonitorDataExchangeService.getSpatialUnitMetadataById(spatialUnitId); + + if (spatialUnitMetadata) { + this.zone.run(() => { + this.onClickEditFeatures(spatialUnitMetadata); + }); + } + }); + + // Edit User Roles Button - use event delegation + $('#spatialUnitOverviewTable').on('click', '.spatialUnitEditUserRolesBtn', (event: any) => { + event.stopPropagation(); + event.preventDefault(); + + // Get the button element (could be the icon inside) + const button = $(event.target).closest('.spatialUnitEditUserRolesBtn')[0]; + const spatialUnitId = button.id.split('_')[3]; + const spatialUnitMetadata = this.kommonitorDataExchangeService.getSpatialUnitMetadataById(spatialUnitId); + + if (spatialUnitMetadata) { + this.zone.run(() => { + this.onClickEditUserRoles(spatialUnitMetadata); + }); + } + }); + + // Delete Button - use event delegation + $('#spatialUnitOverviewTable').on('click', '.spatialUnitDeleteBtn', (event: any) => { + event.stopPropagation(); + event.preventDefault(); + + // Get the button element (could be the icon inside) + const button = $(event.target).closest('.spatialUnitDeleteBtn')[0]; + const spatialUnitId = button.id.split('_')[3]; + const spatialUnitMetadata = this.kommonitorDataExchangeService.getSpatialUnitMetadataById(spatialUnitId); + + if (spatialUnitMetadata) { + this.zone.run(() => { + this.onClickDeleteSpatialUnits([spatialUnitMetadata]); + }); + } + }); + } + + private headerHeightSetter(): void { + if (this.gridApi) { + const headerHeight = this.headerHeightGetter(); + this.gridApi.setHeaderHeight(headerHeight); + } + } + + private headerHeightGetter(): number { + const headerElement = document.querySelector('.ag-header'); + if (headerElement) { + const headerTextElements = headerElement.querySelectorAll('.ag-header-cell-text'); + let maxHeight = 0; + headerTextElements.forEach(element => { + const height = element.scrollHeight; + if (height > maxHeight) { + maxHeight = height; + } + }); + return Math.max(maxHeight + 20, 50); // Add padding and minimum height + } + return 50; + } + + getSelectedSpatialUnitsMetadata(): any[] { + if (this.gridApi) { + const selectedNodes = this.gridApi.getSelectedNodes(); + return selectedNodes.map(node => node.data); + } + return []; + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitAddModal/spatial-unit-add-modal.component.css b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitAddModal/spatial-unit-add-modal.component.css new file mode 100644 index 000000000..96f9ecc6a --- /dev/null +++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitAddModal/spatial-unit-add-modal.component.css @@ -0,0 +1,937 @@ +/* Modal Header */ +.modal-header .close { + margin-left: auto; + padding: 1rem; + margin: -1rem -1rem -1rem auto; +} + +.modal-title { + margin: 0; + color: #495057; + flex: 1; +} + +/* Modal Body */ +:host ::ng-deep .modal-body { + position: relative; + flex: 1 1 auto; + padding: 1rem; +} + +/* Form Elements */ +.form-group { + margin-bottom: 1rem; +} + +.form-control { + display: block; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + padding: 0.375rem 0.75rem; + color: #495057; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #ced4da; + border-radius: 0.25rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.form-control:focus { + color: #495057; + background-color: #fff; + border-color: #80bdff; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +textarea.form-control { + height: auto; +} + +.invalid-feedback { + display: block; + margin-top: 0.25rem; + font-size: 80%; + color: #dc3545; +} + +/* Modal Footer */ +.modal-footer, +:host ::ng-deep .modal-footer { + background-color: #f8f9fa; + border-top: 1px solid #dee2e6; + padding: 0.75rem; + border-bottom-right-radius: 0.3rem; + border-bottom-left-radius: 0.3rem; +} + +.modal-footer .btn { + margin-left: 0.25rem; +} + +.modal-footer .btn:first-child { + margin-left: 0; +} + +/* Spinner */ +.spinner-border-sm { + width: 1rem; + height: 1rem; + border-width: 0.2em; +} + +.icon-spin { + animation: spin 1s infinite linear; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* Alerts */ +.alert { + position: relative; + padding: 0.75rem 3.25rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +/* ng-bootstrap Datepicker Styles */ +.datepicker-dropdown { + position: absolute !important; + z-index: 9999 !important; + background: white !important; + border: 1px solid #ccc !important; + border-radius: 4px !important; + box-shadow: 0 2px 10px rgba(0,0,0,0.1) !important; + margin-top: 2px !important; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important; + width: 320px !important; + left: 0 !important; + top: 100% !important; +} + +/* Position the datepicker relative to the input group */ +.input-group { + position: relative !important; +} + +.input-group .datepicker-dropdown { + position: absolute !important; + top: 100% !important; + left: 0 !important; + right: auto !important; + margin-top: 2px !important; + z-index: 9999 !important; +} + +/* Ensure datepicker renders above all other elements */ +.ngb-datepicker { + position: absolute !important; + z-index: 9999 !important; + background: white !important; + border: 1px solid #ccc !important; + border-radius: 4px !important; + box-shadow: 0 2px 10px rgba(0,0,0,0.1) !important; + margin-top: 2px !important; + width: 320px !important; + left: 0 !important; + top: 100% !important; +} + +/* Override any ng-bootstrap default positioning */ +.ngb-datepicker-picker { + position: absolute !important; + z-index: 9999 !important; + background: white !important; + border: 1px solid #ccc !important; + border-radius: 4px !important; + box-shadow: 0 2px 10px rgba(0,0,0,0.1) !important; + margin-top: 2px !important; + width: 320px !important; + left: 0 !important; + top: 100% !important; +} + +/* Ensure the datepicker container doesn't clip content */ +.date-input-group { + overflow: visible !important; + position: relative !important; +} + +/* Force datepicker to render outside button group */ +.input-group-btn { + position: relative !important; + overflow: visible !important; +} + +.input-group-btn .ngb-datepicker { + position: absolute !important; + z-index: 9999 !important; + left: 0 !important; + top: 100% !important; + margin-top: 2px !important; +} + +/* Date input group specific styling - matching original AngularJS version */ +.date-input-group { + border-radius: 4px !important; + overflow: visible !important; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) !important; + position: relative !important; +} + +/* Left button styling for calendar icon */ +.date-input-group .input-group-btn { + position: relative !important; +} + +.date-input-group .date-toggle-btn { + border-right: none !important; + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + background-color: #f8f9fa !important; + border-color: #ced4da !important; + color: #495057 !important; + padding: 8px 12px !important; + min-width: 40px !important; + transition: all 0.15s ease-in-out !important; + border-top-left-radius: 4px !important; + border-bottom-left-radius: 4px !important; +} + +.date-input-group .date-toggle-btn:hover { + background-color: #e9ecef !important; + border-color: #adb5bd !important; + color: #007bff !important; +} + +.date-input-group .date-toggle-btn:focus { + outline: none !important; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25) !important; +} + +.date-input-group .form-control { + border-left: none !important; + border-right: 1px solid #ced4da !important; + border-radius: 0 !important; + padding: 8px 42px !important; + font-size: 14px !important; + border-top-right-radius: 4px !important; + border-bottom-right-radius: 4px !important; + cursor: pointer !important; + transition: all 0.15s ease-in-out !important; +} + +.date-input-group .form-control:hover { + background-color: #f8f9fa !important; + border-color: #adb5bd !important; +} + +.date-input-group .form-control:focus { + border-color: #80bdff !important; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25) !important; + outline: none !important; + background-color: #fff !important; +} + +/* Enhanced visual feedback for clickable elements */ +.date-input-group .date-toggle-btn { + cursor: pointer !important; +} + +.date-input-group .date-toggle-btn:active { + background-color: #dee2e6 !important; + transform: translateY(1px) !important; +} + +.date-input-group .form-control:active { + background-color: #f8f9fa !important; +} + +/* Ensure proper spacing and alignment */ +.date-input-group .input-group-btn { + margin-right: 0 !important; +} + +.date-input-group .form-control { + margin-left: 0 !important; +} + +/* Fix unintended gap between calendar button and input */ +.date-input-group { + display: flex !important; + align-items: stretch !important; +} + +.date-input-group .input-group-btn { + flex: 0 0 auto !important; + margin: 0 !important; +} + +.date-input-group .date-toggle-btn { + height: 100% !important; + border-right: 0 !important; + z-index: 10; +} + +.date-input-group > div { + flex: 1 1 auto !important; + margin: 0 !important; + padding: 0 !important; +} + +.date-input-group > div > .form-control, +.date-input-group > .form-control { + width: 100% !important; + height: 100% !important; + border-left: 0 !important; +} + +/* Hover effect for the entire input group */ +.date-input-group:hover { + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15) !important; +} + +/* Enhanced datepicker styling with larger fonts - matching original design */ +.datepicker-dropdown .ngb-dp-header { + background-color: #f8f9fa !important; + border-bottom: 1px solid #dee2e6 !important; + padding: 18px 15px !important; + border-radius: 4px 4px 0 0 !important; +} + +.datepicker-dropdown .ngb-dp-month { + background: white !important; + padding: 15px !important; +} + +.datepicker-dropdown .ngb-dp-weekday { + color: #6c757d !important; + font-weight: 600 !important; + font-size: 18px !important; + padding: 12px 8px !important; + text-align: center !important; + text-transform: uppercase !important; + letter-spacing: 0.5px !important; +} + +.datepicker-dropdown .ngb-dp-day { + padding: 10px !important; + text-align: center !important; + cursor: pointer !important; + border-radius: 4px !important; + transition: all 0.15s ease-in-out !important; + font-weight: 500 !important; + font-size: 18px !important; + min-width: 45px !important; + height: 45px !important; + line-height: 25px !important; +} + +.datepicker-dropdown .ngb-dp-day:hover { + background-color: #e9ecef !important; + transform: scale(1.05) !important; +} + +.datepicker-dropdown .ngb-dp-day.selected { + background-color: #007bff !important; + color: white !important; + font-weight: bold !important; + box-shadow: 0 2px 4px rgba(0, 123, 255, 0.3) !important; +} + +.datepicker-dropdown .ngb-dp-day.focused { + background-color: #007bff !important; + color: white !important; + font-weight: bold !important; +} + +.datepicker-dropdown .ngb-dp-day.today { + background-color: #fff3cd !important; + color: #856404 !important; + font-weight: bold !important; + border: 2px solid #ffc107 !important; +} + +.datepicker-dropdown .ngb-dp-day.disabled { + color: #6c757d !important; + cursor: not-allowed !important; + opacity: 0.4 !important; +} + +.datepicker-dropdown .ngb-dp-day.outside { + color: #6c757d !important; + opacity: 0.5 !important; +} + +.datepicker-dropdown .ngb-dp-navigation-chevron { + border-style: solid !important; + border-width: 0.35em 0.35em 0 0 !important; + content: "" !important; + display: inline-block !important; + height: 0.7em !important; + transform: rotate(-45deg) !important; + vertical-align: top !important; + width: 0.7em !important; + color: #495057 !important; +} + +.datepicker-dropdown .ngb-dp-navigation-chevron.right { + transform: rotate(45deg) !important; +} + +.datepicker-dropdown .ngb-dp-month-name { + font-size: 20px !important; + font-weight: 600 !important; + color: #495057 !important; + text-transform: capitalize !important; +} + +.datepicker-dropdown .ngb-dp-arrow { + background: transparent !important; + border: none !important; + padding: 12px 18px !important; + cursor: pointer !important; + border-radius: 4px !important; + transition: all 0.15s ease-in-out !important; + min-width: 50px !important; +} + +.datepicker-dropdown .ngb-dp-arrow:hover { + background-color: #e9ecef !important; + transform: scale(1.1) !important; +} + +.datepicker-dropdown .ngb-dp-arrow:focus { + outline: 2px solid #007bff !important; + outline-offset: 2px !important; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .datepicker-dropdown { + width: 300px !important; + left: 50% !important; + transform: translateX(-50%) !important; + } + + .input-group .datepicker-dropdown { + left: 50% !important; + transform: translateX(-50%) !important; + } + + .datepicker-dropdown .ngb-dp-day { + font-size: 16px !important; + min-width: 40px !important; + height: 40px !important; + } + + .datepicker-dropdown .ngb-dp-weekday { + font-size: 16px !important; + } + + .datepicker-dropdown .ngb-dp-month-name { + font-size: 18px !important; + } +} + +/* Progress Bar Styles - Matching Original */ +#progressbar { + margin-bottom: 10px; + overflow: hidden; + /*CSS counters to number the steps*/ + counter-reset: step; +} + +#progressbar li { + list-style-type: none; + color: black; + text-transform: uppercase; + font-size: 9px; + float: left; + position: relative; + letter-spacing: 1px; + cursor: pointer; +} + +#progressbar li:before { + content: counter(step); + counter-increment: step; + width: 24px; + height: 24px; + line-height: 26px; + display: block; + font-size: 12px; + color: #333; + background: #cccc; + border-radius: 25px; + margin: 0 auto 10px auto; + transform: translateZ(-1px); +} + +/*progressbar connectors*/ +#progressbar li:after { + content: ''; + width: 100%; + height: 2px; + background: #cccc; + position: absolute; + left: -50%; + top: 9px; + /*put it behind the numbers */ + z-index: -1; +} + +#progressbar li:first-child:after { + /*connector not needed before the first step*/ + content: none; +} + +/*marking active/completed steps green*/ +/*The number of the step and the connector before it = green*/ +#progressbar li.active:before, #progressbar li.active:after { + background: var(--kommonitor-primary); + color: white; +} + +#progressbar li.clickable { + cursor: pointer; + transition: all 0.3s ease; +} + +#progressbar li.clickable:hover { + color: var(--kommonitor-primary); +} + +#progressbar li.clickable:hover:before { + background: var(--kommonitor-primary); + transform: scale(1.1); +} + +/* Switch styles */ +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 28px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.switchslider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; +} + +.switchslider:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .switchslider { + background-color: var(--kommonitor-primary); +} + +input:focus + .switchslider { + box-shadow: 0 0 1px var(--kommonitor-primary); +} + +input:checked + .switchslider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); +} + +.switchslider.round { + border-radius: 20px; +} + +.switchslider.round:before { + border-radius: 50%; +} + +/* Loading overlay */ +.loading-overlay-admin-panel { + width: 100%; + height: 100%; + position: absolute; + background-color: rgb(128,128,128); + top: 0; + left: 0; + border-radius: 5px; + opacity: 0.8; + z-index: 100000; +} + +.loading-overlay-admin-panel .icon-spin { + font-size: 25px; + width: 25px; + height: 25px; + position: absolute; + top: 50%; + left: 50%; + margin: -12.5px 0 0 -12.5px; + -webkit-animation: spin 1s infinite linear; + -moz-animation: spin 1s infinite linear; + -o-animation: spin 1s infinite linear; + animation: spin 1s infinite linear; + -webkit-transform-origin: 50% 50%; + transform-origin:50% 50%; + -ms-transform-origin:50% 50%; +} + +/* Form validation */ +.help-block.with-errors { + color: #dc3545; + font-size: 80%; + margin-top: 0.25rem; +} + +/* Color picker */ +.color-picker-container { + position: relative; + display: inline-block; + z-index: 1; +} + +.color-picker-button { + background-color: #fff; + border: 2px solid #ccc; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; + font-family: monospace; + font-size: 12px; + color: #333; + display: flex; + align-items: center; + justify-content: center; + position: relative; + z-index: 1; +} + +.color-picker-button:hover { + border-color: #007bff; + box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2); +} + +.color-picker-button:focus { + outline: none; + border-color: #007bff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.color-display-text { + color: #333; + font-weight: 500; + text-shadow: 0 0 2px rgba(255, 255, 255, 0.8); +} + +.color-picker-overlay { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 99999999; + background: white; + border: 1px solid #ccc; + border-radius: 8px; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); + padding: 10px; + animation: fadeIn 0.2s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translate(-50%, -50%) scale(0.95); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} + +.color-picker-backdrop { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + z-index: 99999998; + background: transparent; + cursor: default; +} + +/* Legacy color input styles for fallback */ +input[type="color"] { + -webkit-appearance: none; + appearance: none; + border: none; + width: 50px; + height: 34px; + border-radius: 4px; + cursor: pointer; +} + +input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; +} + +input[type="color"]::-webkit-color-swatch { + border: none; + border-radius: 4px; +} + +/* Ensure modal is visible */ +:host { + display: block; + z-index: 99999999 !important; +} + +/* Override any conflicting styles */ +.spatial-unit-add-modal .modal-dialog { + max-width: 85% !important; + width: 85% !important; + margin: 1.75rem auto; +} + +/* Ensure modal content is properly sized */ +.spatial-unit-add-modal .modal-content { + background-color: white; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + max-height: 90vh; + overflow-y: auto; +} + +.spatial-unit-add-modal .modal-content { + background-color: white; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); +} + +/* Ensure modal backdrop is visible */ +.modal-backdrop { + opacity: 0.5; + background-color: #000; + z-index: 99999998 !important; +} + +/* Force modal to be visible */ +.modal { + display: block !important; + z-index: 99999999 !important; +} + +.modal-dialog { + z-index: 99999999 !important; +} + +.modal-content { + z-index: 99999999 !important; +} + +/* Debug styles - add a bright border to see if modal is rendered */ +.spatial-unit-add-modal { + border: 3px solid red !important; +} + +/* Global modal size overrides for NgbModal */ +:host ::ng-deep .modal-dialog { + margin: 1.75rem auto; +} + +/* Explicit override when dialog gets our custom class from open() options */ +:host ::ng-deep .modal-dialog.spatial-unit-add-modal { + max-width: 1200px !important; + width: 90% !important; +} + +:host ::ng-deep .modal-content { + max-height: 90vh; + overflow-y: auto; +} + +:host ::ng-deep .modal-body { + max-height: calc(90vh - 120px); + overflow-y: auto; + padding: 2rem; +} + +/* Multi-step form styles - Matching Original */ +.multiStepForm { + text-align: center; + position: relative; + margin-top: 30px; + z-index: 11000; + font-size: 12px; +} + +.multiStepForm fieldset { + background: white; + border: 0 none; + border-radius: 0px; + box-shadow: 0 0 15px 1px rgba(0, 0, 0, 0.4); + padding: 0px 30px; + box-sizing: border-box; + /*stacking fieldsets above each other*/ + position: relative; + width: 100%; +} + +/*inputs*/ +.multiStepForm input, .multiStepForm textarea, .multiStepForm select { + border: 1px solid #ccc; + border-radius: 0px; + margin-bottom: 10px; + width: 100%; + box-sizing: border-box; + color: #2C3E50; + font-size: 13px; +} + +.multiStepForm input:focus, .multiStepForm textarea:focus { + -moz-box-shadow: none !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; + border: 1px solid var(--kommonitor-primary); + outline-width: 0; + transition: All 0.5s ease-in; + -webkit-transition: All 0.5s ease-in; + -moz-transition: All 0.5s ease-in; + -o-transition: All 0.5s ease-in; +} + +/*buttons*/ +.multiStepForm .action-button { + width: auto; + background: var(--kommonitor-primary); + font-weight: bold; + color: white; + border: 0 none; + border-radius: 25px; + cursor: pointer; + padding: 10px 5px; + margin: 10px 5px; +} + +.multiStepForm .action-button:hover, .multiStepForm .action-button:focus { + box-shadow: 0 0 0 2px white, 0 0 0 3px var(--kommonitor-primary); +} + +.multiStepForm .action-button-previous { + width: 100px; + background: rgb(236, 138, 138); + font-weight: bold; + color: white; + border: 0 none; + border-radius: 25px; + cursor: pointer; + padding: 10px 5px; + margin: 10px 5px; +} + +.multiStepForm .action-button-previous:hover, .multiStepForm .action-button-previous:focus { + box-shadow: 0 0 0 2px white, 0 0 0 3px #C5C5F1; +} + +/*headings*/ +.fs-title { + font-size: 18px; + text-transform: uppercase; + color: #2C3E50; + margin-bottom: 10px; + letter-spacing: 2px; + font-weight: bold; +} + +.fs-subtitle { + font-weight: normal; + font-size: 13px; + color: #666; + margin-bottom: 20px; +} + +/* Make sure form fields have proper spacing */ +.row.vertical-align { + margin-bottom: 1.5rem; +} + +/* Align columns like in edit-features modal */ +.vertical-align { + display: flex; + align-items: flex-start; +} + +.vertical-align .col-md-3, +.vertical-align .col-md-6 { + margin-bottom: 1rem; +} + +/* Ensure first metadata row fields align horizontally */ +.meta-row { + display: flex; + align-items: flex-start; +} + +.meta-row .form-group { + display: flex; + flex-direction: column; +} + +.meta-row label { + min-height: 20px; +} + +/* Ensure proper spacing between form groups */ +.form-group { + margin-bottom: 1.5rem; +} + +/* Align filter icon and input horizontally in step 3 */ +.owner-filter-group { + display: flex; + align-items: stretch; +} + +.owner-filter-group .input-group-addon { + display: flex; + align-items: center; + padding: 0 10px; + background-color: #f8f9fa; + border: 1px solid #ced4da; + border-right: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + height: calc(1.5em + 0.75rem + 2px); + width: 30px; +} + +.owner-filter-group .form-control { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitAddModal/spatial-unit-add-modal.component.html b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitAddModal/spatial-unit-add-modal.component.html new file mode 100644 index 000000000..3642b071a --- /dev/null +++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitAddModal/spatial-unit-add-modal.component.html @@ -0,0 +1,889 @@ + + + + + + + +
+ +

Raumebene registriert

+

Eine neue Raumebene mit Namen {{successMessagePart}} wurde in KomMonitor registriert und in die Übersichtstabelle eingetragen.

+
+ {{importedFeatures.length}} Raumeinheiten wurden dabei importiert. +
+
+ + +
+ +

Registrierung gescheitert

+ Bei der Registrierung der Raumebene ist ein Fehler aufgetreten. Fehlermeldung: +
+

+  
+
+
+

Bei den {{importerErrors.length}} Raumeinheiten mit folgenden IDs scheitert der Import:

+
+      
    +
  • {{error}}
  • +
+
+

Bitte beheben Sie die angezeigten Fehler im Datensatz und wiederholen den Prozess.

+
+
+ + +
+ +

Metadata Import gescheitert

+ Beim Import der Metadaten aus einer Datei ist ein Fehler aufgetreten. Fehlermeldung: +
+
{{spatialUnitMetadataImportError}}
+
+
+

Bitte stellen Sie sicher, dass folgendes JSON-Format eingehalten wird:

+

+
+ + +
+ +

Mapping-Konfiguration Import gescheitert

+ Beim Import der Mapping-Konfiguration aus einer Datei ist ein Fehler aufgetreten. Fehlermeldung: +
+
								
+  
+
+

Bitte stellen Sie sicher, dass folgendes JSON-Format eingehalten wird:

+

+
\ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitAddModal/spatial-unit-add-modal.component.ts b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitAddModal/spatial-unit-add-modal.component.ts new file mode 100644 index 000000000..f46ed0e96 --- /dev/null +++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitAddModal/spatial-unit-add-modal.component.ts @@ -0,0 +1,1462 @@ +import { Component, OnInit, Inject, ViewChild, ElementRef, HostListener, Injectable } from '@angular/core'; +import { NgbActiveModal, NgbDatepicker, NgbDateParserFormatter, NgbDateStruct, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; +import { HttpClient } from '@angular/common/http'; +import { KommonitorImporterHelperService } from '../../../../../services/adminSpatialUnit/kommonitor-importer-helper.service'; +import { KommonitorDataGridHelperService } from '../../../../../services/adminSpatialUnit/kommonitor-data-grid-helper.service'; +import { KommonitorDataExchangeService } from '../../../../../services/adminSpatialUnit/kommonitor-data-exchange.service'; +import { AgGridAngular } from 'ag-grid-angular'; +import { ColDef, GridOptions, GridApi, ColumnApi } from 'ag-grid-community'; +import { ColorEvent } from 'ngx-color'; +import { KmColorPickerComponent } from '../../../customElements/color-picker/km-color-picker.component'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; + +// Removed in favor of standalone km-date-picker component providers + +@Component({ + selector: 'spatial-unit-add-modal-new', + templateUrl: './spatial-unit-add-modal.component.html', + styleUrls: ['./spatial-unit-add-modal.component.css'] +}) +export class SpatialUnitAddModalComponent implements OnInit { + @ViewChild('metadataImportFile', { static: false }) metadataImportFile!: ElementRef; + @ViewChild('mappingConfigImportFile', { static: false }) mappingConfigImportFile!: ElementRef; + @ViewChild('spatialUnitDataSourceInput', { static: false }) spatialUnitDataSourceInput!: ElementRef; + @ViewChild('roleManagementGrid', { static: false }) roleManagementGrid!: AgGridAngular; + // datepickers handled by km-date-picker + @ViewChild('lastUpdateDatepicker', { static: false }) lastUpdateDatepicker!: NgbDatepicker; + @ViewChild('outlineDashArrayDropdown', { static: false }) outlineDashArrayDropdown!: ElementRef; + + // Multi-step form + currentStep = 1; + totalSteps = 3; // Will be adjusted based on security settings + + // Form data + isSubmitting = false; + errorMessage = ''; + successMessage = ''; + loadingData = false; + + // Basic form data + spatialUnitLevel = ''; + spatialUnitLevelInvalid = false; + metadata: any = { + description: '', + databasis: '', + datasource: '', + contact: '', + updateInterval: null, + lastUpdate: '', + literature: '', + note: '', + sridEPSG: 4326 + }; + + // Hierarchy + nextLowerHierarchySpatialUnit: any = null; + nextUpperHierarchySpatialUnit: any = null; + hierarchyInvalid = false; + + // Outline layer settings + isOutlineLayer = false; + loiColor = '#bf3d2c'; + outlineWidth = 3; + outlineDashArray: any = null; + + // Period of validity + periodOfValidity: { startDate: any; endDate: any } = { + startDate: '', + endDate: '' + }; + periodOfValidityInvalid = false; + + // Available options + availableSpatialUnits: any[] = []; + updateIntervalOptions: any[] = []; + availableDatasourceTypes: any[] = []; + availableLoiDashArrayObjects: any[] = []; + + // Importer functionality + converter: any = null; + schema: string = ''; + mimeType: string = ''; + datasourceType: any = null; + spatialUnitDataSourceIdProperty = ''; + spatialUnitDataSourceIdPropertyInvalid = false; + spatialUnitDataSourceNameProperty = ''; + spatialUnitDataSourceNamePropertyInvalid = false; + + // Bbox parameters for OGCAPI_FEATURES + bboxType: string = ''; + bboxRefSpatialUnit: any = null; + bbox_minx: any = null; + bbox_miny: any = null; + bbox_maxx: any = null; + bbox_maxy: any = null; + + // Attribute mapping + attributeMapping_sourceAttributeName = ''; + attributeMapping_destinationAttributeName = ''; + attributeMapping_attributeType: any = null; + attributeMappings_adminView: any[] = []; + keepAttributes = true; + keepMissingValues = true; + + // Persisted parameter values for converter and datasource type + converterParameterValues: { [key: string]: string } = {}; + datasourceTypeParameterValues: { [key: string]: string } = {}; + + // Validity dates per feature + validityStartDate_perFeature = ''; + validityEndDate_perFeature = ''; + + // Role management + roleManagementTableOptions: any = null; + roleManagementColumnDefs: ColDef[] = []; + roleManagementRowData: any[] = []; + roleManagementDefaultColDef: ColDef = {}; + roleManagementGridOptions: GridOptions = {}; + roleManagementGridApi: GridApi | null = null; + roleManagementColumnApi: ColumnApi | null = null; + ownerOrganization = ''; + ownerOrgFilter = ''; + isPublic = false; + resourcesCreatorRights: any[] = []; + + // Import/Export functionality + metadataImportSettings: any = null; + mappingConfigImportSettings: any = null; + spatialUnitMetadataImportError = ''; + spatialUnitMappingConfigImportError = ''; + + // Success/Error data + successMessagePart = ''; + errorMessagePart = ''; + importerErrors: any[] = []; + importedFeatures: any[] = []; + + // Importer objects + converterDefinition: any = null; + datasourceTypeDefinition: any = null; + propertyMappingDefinition: any = null; + postBody_spatialUnits: any = null; + + // Validation flags + idPropertyNotFound = false; + namePropertyNotFound = false; + spatialUnitDataSourceInputInvalid = false; + spatialUnitDataSourceInputInvalidReason = ''; + + // Missing properties from original component + outlineColor = "#000000"; + selectedOutlineDashArrayObject: any = null; + spatialUnitMetadataStructure_pretty: string = ''; + spatialUnitMappingConfigStructure: any = {}; + + // Role form visibility + showRoleForm = false; + + // Color picker handled by km-color-picker + + // Dropdown state for outline dash array (Angular-native toggle) + showOutlineDashArrayDropdown = false; + + // Grid ready event handler + onRoleManagementGridReady(params: any) { + this.roleManagementGridApi = params.api; + this.roleManagementColumnApi = params.columnApi; + + // Update the service with the grid API so it can be used for getSelectedRoleIds + this.kommonitorDataGridHelperService.setGridApi(params.api); + } + + // Additional grid event handlers to match parent component + onRoleManagementFirstDataRendered(event: any): void { + this.roleManagementHeaderHeightSetter(); + } + + onRoleManagementColumnResized(event: any): void { + this.roleManagementHeaderHeightSetter(); + } + + onRoleManagementModelUpdated(): void { + // Grid model updated + } + + onRoleManagementViewportChanged(): void { + // Viewport changed + } + + private roleManagementHeaderHeightSetter(): void { + if (this.roleManagementGridApi) { + const headerHeight = this.roleManagementHeaderHeightGetter(); + this.roleManagementGridApi.setHeaderHeight(headerHeight); + } + } + + private roleManagementHeaderHeightGetter(): number { + const headerElement = document.querySelector('#roleManagementGrid .ag-header'); + if (headerElement) { + const headerTextElements = headerElement.querySelectorAll('.ag-header-cell-text'); + let maxHeight = 0; + headerTextElements.forEach(element => { + const height = element.scrollHeight; + if (height > maxHeight) { + maxHeight = height; + } + }); + return Math.max(maxHeight + 20, 40); // Add padding and minimum height + } + return 40; + } + + // Filter organizations based on ownerOrgFilter + get filteredAccessControl() { + const accessControl = this.kommonitorDataExchangeService.accessControl || []; + + if (!this.ownerOrgFilter) { + return accessControl; + } + const filtered = accessControl.filter(org => + org.name.toLowerCase().includes(this.ownerOrgFilter.toLowerCase()) + ); + return filtered; + } + + get filteredResourcesCreatorRights() { + if (!this.ownerOrgFilter) { + return this.resourcesCreatorRights; + } + const filtered = this.resourcesCreatorRights.filter(org => + org.name.toLowerCase().includes(this.ownerOrgFilter.toLowerCase()) + ); + return filtered; + } + + constructor( + public activeModal: NgbActiveModal, + public kommonitorDataExchangeService: KommonitorDataExchangeService, + public kommonitorImporterHelperService: KommonitorImporterHelperService, + private kommonitorDataGridHelperService: KommonitorDataGridHelperService, + private http: HttpClient, + private broadcastService: BroadcastService, + private sanitizer: DomSanitizer + ) { + } + + ngOnInit() { + this.loadInitialData(); + this.initializeMultiStepForm(); + this.initializeOutlineLayerSettings(); + this.initializeMetadataStructures(); + this.setupEventListeners(); + } + + private async loadInitialData() { + this.loadingData = true; + + // Load available spatial units + if (this.kommonitorDataExchangeService.availableSpatialUnits) { + this.availableSpatialUnits = this.kommonitorDataExchangeService.availableSpatialUnits; + } + + // Load update interval options + if (this.kommonitorDataExchangeService.updateIntervalOptions) { + this.updateIntervalOptions = this.kommonitorDataExchangeService.updateIntervalOptions; + } else { + console.warn('No update interval options available from service'); + } + + // Initialize attribute mapping types + const attributeMappingTypes = this.kommonitorImporterHelperService.getAttributeMappingTypes(); + if (attributeMappingTypes && attributeMappingTypes.length > 0) { + this.attributeMapping_attributeType = attributeMappingTypes[0]; + } + + // Ensure importer resources are fetched before reading converters/datasource types + try { + await this.kommonitorImporterHelperService.fetchResourcesFromImporter(); + } catch (error) { + console.error('Failed to fetch importer resources:', error); + } + + // Load datasource types from importer helper after fetch + this.loadDatasourceTypes(); + + // Load access control data and prepare creator list + this.loadAccessControlData(); + + // Initialize metadata structures + this.spatialUnitMappingConfigStructure = this.kommonitorImporterHelperService.mappingConfigStructure; + } + + private loadAccessControlData() { + // Check if access control data is already available + if (this.kommonitorDataExchangeService.accessControl && this.kommonitorDataExchangeService.accessControl.length > 0) { + this.prepareCreatorList(); + this.loadingData = false; + } else { + // Fetch access control data from server + this.kommonitorDataExchangeService.fetchAccessControlMetadata().subscribe({ + next: (data) => { + this.prepareCreatorList(); + this.loadingData = false; + }, + error: (error) => { + console.error('Error fetching access control data:', error); + // Set empty arrays to avoid errors + this.resourcesCreatorRights = []; + this.loadingData = false; + } + }); + } + } + + private initializeMultiStepForm() { + // Initialize multi-step form based on security settings + if (this.kommonitorDataExchangeService.accessControl && + this.kommonitorDataExchangeService.accessControl.length > 0) { + this.totalSteps = 5; // Include role management step + } else { + this.totalSteps = 4; + } + + // Initialize role management if available + if (this.kommonitorDataExchangeService.accessControl && + this.kommonitorDataExchangeService.accessControl.length > 0) { + this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid( + 'spatialUnitAddRoleManagementTable', + this.roleManagementTableOptions, + this.kommonitorDataExchangeService.accessControl, + [] + ); + + // Extract initial column definitions and row data and build grid config + if (this.roleManagementTableOptions) { + this.roleManagementColumnDefs = this.roleManagementTableOptions.columnDefs || []; + this.roleManagementRowData = this.roleManagementTableOptions.rowData || []; + + // Build grid configuration + this.buildRoleManagementGridConfig(); + } + } + } + + private loadConverters(): void { + // Converters are fetched and exposed by the importer helper service. + // The template reads them directly from the service; no component state needed here. + return; + } + + private loadDatasourceTypes(): void { + const datasourceTypes = this.kommonitorImporterHelperService.getAvailableDatasourceTypes(); + this.availableDatasourceTypes = datasourceTypes || []; + } + + private initializeOutlineLayerSettings() { + this.selectedOutlineDashArrayObject = this.kommonitorDataExchangeService.availableLoiDashArrayObjects?.[0] || null; + this.availableLoiDashArrayObjects = this.kommonitorDataExchangeService.availableLoiDashArrayObjects || []; + + // Remove jQuery-specific initialization - Angular will handle this automatically + + // Update dropdown button display with selected SVG + setTimeout(() => { + this.updateDropdownButtonDisplay(); + }, 100); + } + + private initializeMetadataStructures() { + this.spatialUnitMetadataStructure_pretty = this.kommonitorDataExchangeService.syntaxHighlightJSON(this.kommonitorDataExchangeService.spatialUnitMetadataStructure); + this.spatialUnitMappingConfigStructure = this.kommonitorImporterHelperService.mappingConfigStructure; + } + + prepareCreatorList() { + if (this.kommonitorDataExchangeService.currentKomMonitorLoginRoleNames?.length > 0) { + let creatorRights: string[] = []; + + this.kommonitorDataExchangeService.currentKomMonitorLoginRoleNames.forEach((roles: string) => { + let key = roles.split('.')[0]; + let role = roles.split('.')[1]; + + if (role === 'unit-resources-creator' && !creatorRights.includes(key)) { + creatorRights.push(key); + } + }); + + // Simplified approach - just filter based on creator rights + this.resourcesCreatorRights = this.kommonitorDataExchangeService.accessControl?.filter(elem => creatorRights.includes(elem.name)) || []; + } else { + this.resourcesCreatorRights = []; + } + } + + private refreshRoles(orgUnitId?: string) { + let permissionIds_ownerUnit: string[] = []; + + if (orgUnitId) { + const accessControl = this.kommonitorDataExchangeService.getAccessControlById(orgUnitId); + permissionIds_ownerUnit = accessControl?.permissions + ?.filter(permission => permission.permissionLevel === "viewer" || permission.permissionLevel === "editor") + .map(permission => permission.permissionId) || []; + } + + // Set datasetOwner flags + this.kommonitorDataExchangeService.accessControl?.forEach(item => { + item.datasetOwner = item.organizationalUnitId === orgUnitId; + }); + + // Build the role management grid options + this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid( + 'spatialUnitAddRoleManagementTable', + this.roleManagementTableOptions, + this.kommonitorDataExchangeService.accessControl || [], + permissionIds_ownerUnit, + true + ); + + // Extract column definitions and row data for ag-grid-angular and rebuild grid config + if (this.roleManagementTableOptions) { + this.roleManagementColumnDefs = this.roleManagementTableOptions.columnDefs || []; + this.roleManagementRowData = this.roleManagementTableOptions.rowData || []; + + // Build grid configuration (this will use the components from roleManagementTableOptions) + this.buildRoleManagementGridConfig(); + + // If grid is already initialized, update the data and grid options + if (this.roleManagementGridApi) { + // Update data + this.roleManagementGridApi.setRowData(this.roleManagementRowData); + this.roleManagementGridApi.setColumnDefs(this.roleManagementColumnDefs); + + // Refresh the grid to ensure it updates + setTimeout(() => { + if (this.roleManagementGridApi) { + this.roleManagementGridApi.refreshCells(); + this.roleManagementGridApi.redrawRows(); + } + }, 100); + } + } + } + + private setupEventListeners() { + // Note: In Angular, we typically use subscription to broadcast events + // For now, we'll handle these events in the appropriate service calls + // The original AngularJS component used $scope.$on which is not available in Angular + } + + checkSpatialUnitName() { + this.spatialUnitLevelInvalid = false; + const level = this.spatialUnitLevel; + + if (level) { + this.availableSpatialUnits.forEach(spatialUnit => { + if (spatialUnit.spatialUnitLevel === level) { + this.spatialUnitLevelInvalid = true; + return; + } + }); + } + } + + checkSpatialUnitHierarchy() { + this.hierarchyInvalid = false; + + // smaller indices represent higher spatial units + // i.e. city districts will have a smaller index than building blocks + if (this.nextLowerHierarchySpatialUnit && this.nextUpperHierarchySpatialUnit) { + let indexOfLowerHierarchyUnit: number; + let indexOfUpperHierarchyUnit: number; + + for (let i = 0; i < this.kommonitorDataExchangeService.availableSpatialUnits.length; i++) { + const spatialUnit = this.kommonitorDataExchangeService.availableSpatialUnits[i]; + if (spatialUnit.spatialUnitLevel === this.nextLowerHierarchySpatialUnit.spatialUnitLevel) { + indexOfLowerHierarchyUnit = i; + } + if (spatialUnit.spatialUnitLevel === this.nextUpperHierarchySpatialUnit.spatialUnitLevel) { + indexOfUpperHierarchyUnit = i; + } + } + + if ((indexOfLowerHierarchyUnit! <= indexOfUpperHierarchyUnit!)) { + // failure + this.hierarchyInvalid = true; + } + } + } + + checkPeriodOfValidity() { + // Normalize to ISO strings first (handles NgbDateStruct or string) + const startIso = this.toIsoDateString(this.periodOfValidity.startDate); + const endIso = this.toIsoDateString(this.periodOfValidity.endDate); + + // Use service validation (guards optional end) + const validation = this.kommonitorDataExchangeService.validatePeriodOfValidity( + startIso as any, + endIso as any + ); + + this.periodOfValidityInvalid = !validation.isValid; + + if (!validation.isValid && validation.error) { + console.warn('Period of validity validation error:', validation.error); + } + } + + // Attribute mapping methods + onAddOrUpdateAttributeMapping() { + const tmpAttributeMapping_adminView = { + "sourceName": this.attributeMapping_sourceAttributeName, + "destinationName": this.attributeMapping_destinationAttributeName, + "dataType": this.attributeMapping_attributeType + }; + + let processed = false; + + for (let index = 0; index < this.attributeMappings_adminView.length; index++) { + const attributeMappingEntry_adminView = this.attributeMappings_adminView[index]; + + if (attributeMappingEntry_adminView.sourceName === tmpAttributeMapping_adminView.sourceName) { + // replace object + this.attributeMappings_adminView[index] = tmpAttributeMapping_adminView; + processed = true; + break; + } + } + + if (!processed) { + // new entry + this.attributeMappings_adminView.push(tmpAttributeMapping_adminView); + } + + this.attributeMapping_sourceAttributeName = ''; + this.attributeMapping_destinationAttributeName = ''; + const attributeMappingTypes = this.kommonitorImporterHelperService.getAttributeMappingTypes(); + this.attributeMapping_attributeType = attributeMappingTypes[0]; + } + + onClickEditAttributeMapping(attributeMappingEntry: any) { + this.attributeMapping_sourceAttributeName = attributeMappingEntry.sourceName; + this.attributeMapping_destinationAttributeName = attributeMappingEntry.destinationName; + this.attributeMapping_attributeType = attributeMappingEntry.dataType; + } + + onClickDeleteAttributeMapping(attributeMappingEntry: any) { + for (let index = 0; index < this.attributeMappings_adminView.length; index++) { + if (this.attributeMappings_adminView[index].sourceName === attributeMappingEntry.sourceName) { + // remove object + this.attributeMappings_adminView.splice(index, 1); + break; + } + } + } + + onChangeConverter(schema?: any) { + this.schema = this.converter.schemas ? this.converter.schemas[0] : undefined; + this.mimeType = this.converter.mimeTypes ? this.converter.mimeTypes[0] : undefined; + this.converterParameterValues = {}; + } + + onChangeMimeType(mimeType: any) { + this.mimeType = mimeType; + } + + onChangeDatasourceType(datasourceType: any) { + // Handle datasource type change + this.datasourceType = datasourceType; + // Reset related fields when datasource type changes + this.spatialUnitDataSourceIdProperty = ''; + this.spatialUnitDataSourceNameProperty = ''; + this.bboxType = ''; + this.bboxRefSpatialUnit = null; + this.bbox_minx = null; + this.bbox_miny = null; + this.bbox_maxx = null; + this.bbox_maxy = null; + this.datasourceTypeParameterValues = {}; + } + + onChangeOutlineDashArray(outlineDashArrayObject: any) { + console.log('=== onChangeOutlineDashArray called ==='); + console.log('Selected object:', outlineDashArrayObject); + console.log('Object label:', outlineDashArrayObject?.label); + console.log('Object SVG string:', outlineDashArrayObject?.svgString?.substring(0, 50) + '...'); + + // Handle outline dash array change + this.selectedOutlineDashArrayObject = outlineDashArrayObject; + this.outlineDashArray = outlineDashArrayObject; + + // Update dropdown button display using helper method + this.updateDropdownButtonDisplay(); + + // Close the dropdown via Angular state + this.closeOutlineDashArrayDropdown(); + + console.log('=== onChangeOutlineDashArray completed ==='); + } + + // Toggle/close handlers for outline dash array dropdown + toggleOutlineDashArrayDropdown(event?: Event) { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + this.showOutlineDashArrayDropdown = !this.showOutlineDashArrayDropdown; + } + + closeOutlineDashArrayDropdown() { + this.showOutlineDashArrayDropdown = false; + } + + @HostListener('document:click', ['$event']) + onDocumentClick(event: MouseEvent) { + const clickTarget = event.target as Node; + // Close dropdown only if click is outside the dropdown container + if (this.showOutlineDashArrayDropdown && this.outlineDashArrayDropdown && this.outlineDashArrayDropdown.nativeElement && this.outlineDashArrayDropdown.nativeElement.contains) { + if (this.outlineDashArrayDropdown.nativeElement.contains(clickTarget)) { + return; + } + } + this.closeOutlineDashArrayDropdown(); + } + + // Color picker logic removed; handled by km-color-picker + + // Date picker methods + // Datepicker toggling handled by km-date-picker + + // Ensure valid date or set to today's date on blur + // Date normalization handled by km-date-picker + + // Method to update dropdown button display + private updateDropdownButtonDisplay() { + // No-op: rendering handled via Angular bindings with cached SafeHtml + return; + } + + // Method to safely sanitize SVG content + private svgSanitizeCache: Map = new Map(); + + getSafeSvgCached(svgString: string): SafeHtml { + if (!svgString) { + return '' as unknown as SafeHtml; + } + const cached = this.svgSanitizeCache.get(svgString); + if (cached) { + return cached; + } + const trusted = this.sanitizer.bypassSecurityTrustHtml(svgString); + this.svgSanitizeCache.set(svgString, trusted); + return trusted; + } + + trackLoiOption(index: number, item: any) { + return item?.dashArrayValue ?? index; + } + + // Importer object building methods + async buildImporterObjects() { + + this.converterDefinition = this.buildConverterDefinition(); + + this.datasourceTypeDefinition = await this.buildDatasourceTypeDefinition(); + + this.propertyMappingDefinition = this.buildPropertyMappingDefinition(); + + this.postBody_spatialUnits = this.buildPostBody_spatialUnits(); + + const allValid = this.converterDefinition && + this.datasourceTypeDefinition && + this.propertyMappingDefinition && + this.postBody_spatialUnits; + + if (!allValid) { + console.error('=== BUILDING IMPORTER OBJECTS - FAILED ==='); + console.error('One or more required objects could not be built'); + } + + return allValid; + } + + buildConverterDefinition() { + + const result = this.kommonitorImporterHelperService.buildConverterDefinition( + this.converter, + "converterParameter_spatialUnitAdd_", + this.schema, + this.mimeType, + this.converterParameterValues + ); + + return result; + } + + async buildDatasourceTypeDefinition() { + try { + // Prefer robust Angular-native handling for FILE uploads + if (this.datasourceType?.type === 'FILE') { + const inputEl = this.spatialUnitDataSourceInput?.nativeElement as HTMLInputElement | undefined; + const file = inputEl?.files?.[0]; + if (!file) { + return null; + } + const uploadedName = await this.kommonitorImporterHelperService.uploadNewFile(file, file.name); + return { + type: 'FILE', + parameters: [ + { name: 'NAME', value: uploadedName } + ] + }; + } + + const formValues: { [key: string]: string } = { + ...this.datasourceTypeParameterValues, + bboxType: this.bboxType as any, + bboxRef: this.bboxRefSpatialUnit as any, + bbox_minx: this.bbox_minx as any, + bbox_miny: this.bbox_miny as any, + bbox_maxx: this.bbox_maxx as any, + bbox_maxy: this.bbox_maxy as any + } as any; + + const result = await this.kommonitorImporterHelperService.buildDatasourceTypeDefinition( + this.datasourceType, + 'datasourceTypeParameter_spatialUnitAdd_', + 'spatialUnitDataSourceInput', + formValues + ); + + return result; + } catch (error: any) { + console.error('=== BUILDING DATASOURCE TYPE DEFINITION - ERROR ==='); + console.error('- Error:', error); + console.error('- Error data:', error.data); + + if (error.data) { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error.data); + } else { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error); + } + + this.loadingData = false; + return null; + } + } + + buildPropertyMappingDefinition() { + + const result = this.kommonitorImporterHelperService.buildPropertyMapping_spatialResource( + this.spatialUnitDataSourceNameProperty, + this.spatialUnitDataSourceIdProperty, + this.validityStartDate_perFeature, + this.validityEndDate_perFeature, + '', + this.keepAttributes, + this.keepMissingValues, + this.attributeMappings_adminView + ); + + return result; + } + + private toIsoDateString(value: any): string | null { + if (!value) { + return null; + } + if (typeof value === 'string') { + return value; + } + const maybeStruct = value as { year?: number; month?: number; day?: number }; + if (maybeStruct && typeof maybeStruct.year === 'number' && typeof maybeStruct.month === 'number' && typeof maybeStruct.day === 'number') { + const y = maybeStruct.year; + const m = String(maybeStruct.month).padStart(2, '0'); + const d = String(maybeStruct.day).padStart(2, '0'); + return `${y}-${m}-${d}`; + } + return null; + } + + buildPostBody_spatialUnits() { + + const postBody: any = { + "geoJsonString": "", // will be set by importer + "metadata": { + "note": this.metadata.note, + "literature": this.metadata.literature, + "updateInterval": this.metadata.updateInterval?.apiName, + "sridEPSG": this.metadata.sridEPSG, + "datasource": this.metadata.datasource, + "contact": this.metadata.contact, + "lastUpdate": this.toIsoDateString(this.metadata.lastUpdate), + "description": this.metadata.description, + "databasis": this.metadata.databasis + }, + "jsonSchema": undefined, + "permissions": [] as string[], // Changed from allowedRoles to match original + "nextLowerHierarchyLevel": this.nextLowerHierarchySpatialUnit ? this.nextLowerHierarchySpatialUnit.spatialUnitLevel : null, + "spatialUnitLevel": this.spatialUnitLevel, + "periodOfValidity": { + "endDate": this.toIsoDateString(this.periodOfValidity && this.periodOfValidity.endDate ? this.periodOfValidity.endDate : null), + "startDate": this.toIsoDateString(this.periodOfValidity && this.periodOfValidity.startDate ? this.periodOfValidity.startDate : null) + }, + "nextUpperHierarchyLevel": this.nextUpperHierarchySpatialUnit ? this.nextUpperHierarchySpatialUnit.spatialUnitLevel : null, + // Add missing outline layer properties + "isOutlineLayer": this.isOutlineLayer, + "outlineColor": this.outlineColor, + "outlineWidth": this.outlineWidth, + "outlineDashArrayString": this.selectedOutlineDashArrayObject?.dashArrayValue, + "ownerId": this.ownerOrganization, + "isPublic": this.isPublic + }; + + if (this.roleManagementTableOptions) { + const roleIds = this.kommonitorDataGridHelperService.getSelectedRoleIds_roleManagementGrid(this.roleManagementTableOptions); + if (roleIds && Array.isArray(roleIds)) { + for (const roleId of roleIds) { + postBody.permissions.push(roleId); + } + } + } + + return postBody; + } + + async addSpatialUnit() { + + this.loadingData = true; + this.importerErrors = []; + this.successMessagePart = ''; + this.errorMessagePart = ''; + + const allDataSpecified = await this.buildImporterObjects(); + + if (!allDataSpecified) { + console.error('=== VALIDATION FAILED ==='); + console.error('- Not all data was specified correctly'); + console.error('- converterDefinition exists:', !!this.converterDefinition); + console.error('- datasourceTypeDefinition exists:', !!this.datasourceTypeDefinition); + console.error('- propertyMappingDefinition exists:', !!this.propertyMappingDefinition); + console.error('- postBody_spatialUnits exists:', !!this.postBody_spatialUnits); + + // TODO: Add form validation here + this.loadingData = false; + return; + } else { + // TODO verify input + // TODO Create and perform POST Request with loading screen + + let newSpatialUnitResponse_dryRun: any = undefined; + try { + + newSpatialUnitResponse_dryRun = await this.kommonitorImporterHelperService.registerNewSpatialUnit( + this.converterDefinition, + this.datasourceTypeDefinition, + this.propertyMappingDefinition, + this.postBody_spatialUnits, + true // isDryRun + ); + + if (!this.kommonitorImporterHelperService.importerResponseContainsErrors(newSpatialUnitResponse_dryRun)) { + // all good, really execute the request to import data against data management API + const newSpatialUnitResponse = await this.kommonitorImporterHelperService.registerNewSpatialUnit( + this.converterDefinition, + this.datasourceTypeDefinition, + this.propertyMappingDefinition, + this.postBody_spatialUnits, + false // isDryRun + ); + + this.broadcastService.broadcast("refreshSpatialUnitOverviewTable", ["add", this.kommonitorImporterHelperService.getIdFromImporterResponse(newSpatialUnitResponse)]); + + // refresh all admin dashboard diagrams due to modified metadata + setTimeout(() => { + this.broadcastService.broadcast("refreshAdminDashboardDiagrams"); + }, 500); + + this.successMessagePart = this.postBody_spatialUnits.spatialUnitLevel; + const importedFeatures = this.kommonitorImporterHelperService.getImportedFeaturesFromImporterResponse(newSpatialUnitResponse); + this.importedFeatures = importedFeatures || []; + + this.loadingData = false; + } else { + // errors occurred + // show them + this.errorMessagePart = "Einige der zu importierenden Features des Datensatzes weisen kritische Fehler auf"; + const errors = this.kommonitorImporterHelperService.getErrorsFromImporterResponse(newSpatialUnitResponse_dryRun); + this.importerErrors = errors || []; + + this.loadingData = false; + } + } catch (error: any) { + console.error('=== EXCEPTION DURING IMPORT ==='); + console.error('- Error type:', typeof error); + console.error('- Error:', error); + console.error('- Error message:', error?.message); + console.error('- Error data:', error?.data); + console.error('- Error status:', error?.status); + + if (error.data) { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error.data); + } else { + this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error); + } + + if (newSpatialUnitResponse_dryRun) { + const errors = this.kommonitorImporterHelperService.getErrorsFromImporterResponse(newSpatialUnitResponse_dryRun); + this.importerErrors = errors || []; + } + + this.loadingData = false; + } + } + } + + onSubmit() { + + if (!this.spatialUnitLevelInvalid && !this.hierarchyInvalid) { + this.addSpatialUnit(); + } else { + this.loadingData = false; + } + } + + // Multi-step navigation + nextStep() { + const maxSteps = this.kommonitorDataExchangeService.enableKeycloakSecurity ? 4 : 3; + if (this.currentStep < maxSteps) { + this.currentStep++; + } + } + + previousStep() { + if (this.currentStep > 1) { + this.currentStep--; + } + } + + goToStep(step: number) { + const maxSteps = this.kommonitorDataExchangeService.enableKeycloakSecurity ? 4 : 3; + + // Validate step range + if (step < 1 || step > maxSteps) { + console.log(`Invalid step: ${step}. Valid range: 1-${maxSteps}`); + return; + } + + // For now, allow navigation to any step for testing + // TODO: Add validation back once basic navigation works + this.currentStep = step; + } + + // Import/Export functionality + onImportSpatialUnitAddMetadata() { + this.spatialUnitMetadataImportError = ''; + if (this.metadataImportFile) { + this.metadataImportFile.nativeElement.click(); + } + } + + onImportSpatialUnitAddMappingConfig() { + this.spatialUnitMappingConfigImportError = ''; + if (this.mappingConfigImportFile) { + this.mappingConfigImportFile.nativeElement.click(); + } + } + + onMetadataFileSelected(event: any) { + const file = event.target.files[0]; + if (file) { + this.parseMetadataFromFile(file); + } + } + + onMappingConfigFileSelected(event: any) { + const file = event.target.files[0]; + if (file) { + this.parseMappingConfigFromFile(file); + } + } + + parseMetadataFromFile(file: File) { + const fileReader = new FileReader(); + + fileReader.onload = (event: any) => { + try { + this.parseFromMetadataFile(event); + } catch (error) { + console.error(error); + console.error("Uploaded Metadata File cannot be parsed."); + this.spatialUnitMetadataImportError = "Uploaded Metadata File cannot be parsed correctly"; + } + }; + + fileReader.readAsText(file); + } + + parseMappingConfigFromFile(file: File) { + const fileReader = new FileReader(); + + fileReader.onload = (event: any) => { + try { + this.parseFromMappingConfigFile(event); + } catch (error) { + console.error(error); + console.error("Uploaded MappingConfig File cannot be parsed."); + this.spatialUnitMappingConfigImportError = "Uploaded MappingConfig File cannot be parsed correctly"; + } + }; + + fileReader.readAsText(file); + } + + parseFromMetadataFile(event: any) { + this.metadataImportSettings = JSON.parse(event.target.result); + + if (!this.metadataImportSettings.metadata) { + console.error("uploaded Metadata File cannot be parsed - wrong structure."); + this.spatialUnitMetadataImportError = "Struktur der Datei stimmt nicht mit erwartetem Muster überein."; + return; + } + + // Parse metadata + this.metadata = {}; + this.metadata.note = this.metadataImportSettings.metadata.note; + this.metadata.literature = this.metadataImportSettings.metadata.literature; + + // Use the same array instance as the select options to ensure object identity matches + const intervalOptions = this.updateIntervalOptions && this.updateIntervalOptions.length + ? this.updateIntervalOptions + : this.kommonitorDataExchangeService.updateIntervalOptions; + + for (const option of intervalOptions) { + if (option.apiName === this.metadataImportSettings.metadata.updateInterval) { + this.metadata.updateInterval = option; + break; + } + } + + this.metadata.sridEPSG = this.metadataImportSettings.metadata.sridEPSG; + this.metadata.datasource = this.metadataImportSettings.metadata.datasource; + this.metadata.contact = this.metadataImportSettings.metadata.contact; + this.metadata.lastUpdate = this.metadataImportSettings.metadata.lastUpdate; + this.metadata.description = this.metadataImportSettings.metadata.description; + this.metadata.databasis = this.metadataImportSettings.metadata.databasis; + + // Parse role management (changed from allowedRoles to permissions) + if (this.kommonitorDataExchangeService.accessControl) { + this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid( + 'spatialUnitAddRoleManagementTable', + this.roleManagementTableOptions, + this.kommonitorDataExchangeService.accessControl, + this.metadataImportSettings.permissions || [], // Changed from allowedRoles + true + ); + } + + // Parse hierarchy + this.kommonitorDataExchangeService.availableSpatialUnits.forEach((spatialUnit: any) => { + if (spatialUnit.spatialUnitLevel === this.metadataImportSettings.nextLowerHierarchyLevel) { + this.nextLowerHierarchySpatialUnit = spatialUnit; + } + if (spatialUnit.spatialUnitLevel === this.metadataImportSettings.nextUpperHierarchyLevel) { + this.nextUpperHierarchySpatialUnit = spatialUnit; + } + }); + + // Parse outline layer settings + this.isOutlineLayer = this.metadataImportSettings.isOutlineLayer || false; + this.outlineColor = this.metadataImportSettings.outlineColor || "#000000"; + this.outlineWidth = this.metadataImportSettings.outlineWidth || 3; + + this.kommonitorDataExchangeService.availableLoiDashArrayObjects?.forEach((option: any) => { + if (option.dashArrayValue === this.metadataImportSettings.outlineDashArrayString) { + this.selectedOutlineDashArrayObject = option; + this.onChangeOutlineDashArray(this.selectedOutlineDashArrayObject); + } + }); + + // Update dropdown button display with selected SVG + setTimeout(() => { + this.updateDropdownButtonDisplay(); + }, 100); + + this.spatialUnitLevel = this.metadataImportSettings.spatialUnitLevel; + this.ownerOrganization = this.metadataImportSettings.ownerId; + this.isPublic = this.metadataImportSettings.isPublic; + + // Initialize metadata structures + this.spatialUnitMappingConfigStructure = this.kommonitorImporterHelperService.mappingConfigStructure; + } + + parseFromMappingConfigFile(event: any) { + this.mappingConfigImportSettings = JSON.parse(event.target.result); + + if (!this.mappingConfigImportSettings.converter || !this.mappingConfigImportSettings.dataSource || !this.mappingConfigImportSettings.propertyMapping) { + console.error("uploaded MappingConfig File cannot be parsed - wrong structure."); + this.spatialUnitMappingConfigImportError = "Struktur der Datei stimmt nicht mit erwartetem Muster überein."; + return; + } + + this.converter = undefined; + const converters = this.kommonitorImporterHelperService.getAvailableConverters(); + for (const converter of converters) { + if (converter.name === this.mappingConfigImportSettings.converter.name) { + this.converter = converter; + break; + } + } + + this.schema = ''; + if (this.converter && this.converter.schemas && this.mappingConfigImportSettings.converter.schema) { + for (const schema of this.converter.schemas) { + if (schema === this.mappingConfigImportSettings.converter.schema) { + this.schema = schema; + } + } + } + + this.mimeType = ''; + if (this.converter && this.converter.mimeTypes && this.mappingConfigImportSettings.converter.mimeType) { + for (const mimeType of this.converter.mimeTypes) { + if (mimeType === this.mappingConfigImportSettings.converter.mimeType) { + this.mimeType = mimeType; + } + } + } + + // Populate converter parameters (e.g., CRS) from imported mapping config + // Defer to ensure inputs exist in the DOM after bindings render + setTimeout(() => { + const params = this.mappingConfigImportSettings?.converter?.parameters || []; + if (this.converter && Array.isArray(params)) { + for (const convParameter of params) { + const el = document.getElementById(`converterParameter_spatialUnitAdd_${convParameter.name}`) as HTMLInputElement | null; + if (el) { + el.value = convParameter.value ?? ''; + } + this.converterParameterValues[convParameter.name] = convParameter.value ?? ''; + } + } + }, 0); + + this.datasourceType = null; + const datasourceTypes = this.kommonitorImporterHelperService.getAvailableDatasourceTypes(); + for (const datasourceType of datasourceTypes) { + if (datasourceType.type === this.mappingConfigImportSettings.dataSource.type) { + this.datasourceType = datasourceType; + break; + } + } + + // Populate datasource type params and bbox + this.datasourceTypeParameterValues = {}; + const dsParams = this.mappingConfigImportSettings?.dataSource?.parameters || []; + const bboxTypeParam = dsParams.find((p: any) => p.name === 'bboxType'); + if (bboxTypeParam) { + this.bboxType = bboxTypeParam.value || ''; + } + const bboxParam = dsParams.find((p: any) => p.name === 'bbox'); + if (bboxParam && typeof bboxParam.value === 'string') { + if (this.bboxType === 'ref') { + this.bboxRefSpatialUnit = bboxParam.value; + } else { + const parts = bboxParam.value.split(','); + if (parts.length === 4) { + this.bbox_minx = parts[0]; + this.bbox_miny = parts[1]; + this.bbox_maxx = parts[2]; + this.bbox_maxy = parts[3]; + } + } + } + for (const p of dsParams) { + if (p.name !== 'bbox' && p.name !== 'bboxType') { + this.datasourceTypeParameterValues[p.name] = p.value ?? ''; + } + } + + // Property Mapping + this.spatialUnitDataSourceNameProperty = this.mappingConfigImportSettings.propertyMapping.nameProperty; + this.spatialUnitDataSourceIdProperty = this.mappingConfigImportSettings.propertyMapping.identifierProperty; + this.validityStartDate_perFeature = this.mappingConfigImportSettings.propertyMapping.validStartDateProperty; + this.validityEndDate_perFeature = this.mappingConfigImportSettings.propertyMapping.validEndDateProperty; + this.keepAttributes = this.mappingConfigImportSettings.propertyMapping.keepAttributes; + this.keepMissingValues = this.mappingConfigImportSettings.propertyMapping.keepMissingOrNullValueAttributes; + this.attributeMappings_adminView = []; + + for (const attributeMapping of this.mappingConfigImportSettings.propertyMapping.attributes) { + const tmpEntry: any = { + "sourceName": attributeMapping.name, + "destinationName": attributeMapping.mappingName + }; + + const attributeMappingTypes = this.kommonitorImporterHelperService.getAttributeMappingTypes(); + for (const dataType of attributeMappingTypes) { + if (dataType.apiName === attributeMapping.type) { + tmpEntry.dataType = dataType; + } + } + + this.attributeMappings_adminView.push(tmpEntry); + } + + if (this.mappingConfigImportSettings.periodOfValidity) { + this.periodOfValidity = { + startDate: this.mappingConfigImportSettings.periodOfValidity.startDate || '', + endDate: this.mappingConfigImportSettings.periodOfValidity.endDate || '' + }; + this.periodOfValidityInvalid = false; + } + + // Initialize metadata structures + this.spatialUnitMappingConfigStructure = this.kommonitorImporterHelperService.mappingConfigStructure; + + // Update dropdown button display with selected SVG + setTimeout(() => { + this.updateDropdownButtonDisplay(); + }, 100); + } + + onExportSpatialUnitAddMetadataTemplate() { + const metadataJSON = JSON.stringify(this.kommonitorDataExchangeService.spatialUnitMetadataStructure); + const fileName = "Raumebene_Metadaten_Vorlage_Export.json"; + this.downloadFile(metadataJSON, fileName); + } + + onExportSpatialUnitAddMetadata() { + // Use service method to build export structure + const metadataExport = this.kommonitorDataExchangeService.buildSpatialUnitMetadataExport( + this.metadata, + this.spatialUnitLevel, + this.nextLowerHierarchySpatialUnit?.spatialUnitLevel || null, + this.nextUpperHierarchySpatialUnit?.spatialUnitLevel || null, + this.isOutlineLayer, + this.outlineColor, + this.outlineWidth, + this.selectedOutlineDashArrayObject?.dashArrayValue || null + ); + + // Add component-specific properties + metadataExport.permissions = []; + if (this.roleManagementTableOptions) { + const roleIds = this.kommonitorDataGridHelperService.getSelectedRoleIds_roleManagementGrid(this.roleManagementTableOptions); + metadataExport.permissions.push(...roleIds); + } + + // Add owner properties + metadataExport.ownerId = this.ownerOrganization; + metadataExport.isPublic = this.isPublic; + + const name = this.spatialUnitLevel; + const fileName = `Raumebene_Metadaten_Export${name ? '-' + name : ''}.json`; + this.downloadFile(JSON.stringify(metadataExport), fileName); + } + + async onExportSpatialUnitAddMappingConfig() { + const converterDefinition = this.buildConverterDefinition(); + const datasourceTypeDefinition = await this.buildDatasourceTypeDefinition(); + const propertyMappingDefinition = this.buildPropertyMappingDefinition(); + + // Use service method to build export structure + const mappingConfigExport = this.kommonitorDataExchangeService.buildMappingConfigExport( + converterDefinition, + datasourceTypeDefinition, + propertyMappingDefinition, + this.periodOfValidity + ); + + const name = this.spatialUnitLevel; + const metadataJSON = JSON.stringify(mappingConfigExport); + let fileName = "KomMonitor-Import-Mapping-Konfiguration_Export"; + + if (name) { + fileName += "-" + name; + } + + fileName += ".json"; + this.downloadFile(metadataJSON, fileName); + } + + private downloadFile(content: string, fileName: string) { + const blob = new Blob([content], { type: "application/json" }); + const data = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.download = fileName; + a.href = data; + a.textContent = "JSON"; + a.target = "_blank"; + a.rel = "noopener noreferrer"; + a.click(); + + a.remove(); + } + + // Metadata structure for export + get spatialUnitMetadataStructure() { + return this.kommonitorDataExchangeService.spatialUnitMetadataStructure; + } + + get spatialUnitMappingConfigStructure_pretty() { + return this.kommonitorDataExchangeService.syntaxHighlightJSON(this.kommonitorImporterHelperService.mappingConfigStructure); + } + + resetForm() { + this.currentStep = 1; + this.spatialUnitLevel = ''; + this.spatialUnitLevelInvalid = false; + this.metadata = { + description: '', + databasis: '', + datasource: '', + contact: '', + updateInterval: null, + lastUpdate: '', + literature: '', + note: '', + sridEPSG: 4326 + }; + this.nextLowerHierarchySpatialUnit = null; + this.nextUpperHierarchySpatialUnit = null; + this.hierarchyInvalid = false; + this.periodOfValidity = { startDate: '', endDate: '' }; + this.periodOfValidityInvalid = false; + + // Reset outline layer settings + this.isOutlineLayer = false; + this.outlineColor = "#000000"; + this.outlineWidth = 3; + this.outlineDashArray = null; + this.selectedOutlineDashArrayObject = this.kommonitorDataExchangeService.availableLoiDashArrayObjects?.[0] || null; + this.spatialUnitMappingConfigStructure = {}; + + // Update dropdown button display with selected SVG + setTimeout(() => { + this.updateDropdownButtonDisplay(); + }, 100); + + this.converter = null; + this.schema = ''; + this.mimeType = ''; + this.datasourceType = null; + this.spatialUnitDataSourceIdProperty = ''; + this.spatialUnitDataSourceNameProperty = ''; + this.validityStartDate_perFeature = ''; + this.validityEndDate_perFeature = ''; + this.converterParameterValues = {}; + this.datasourceTypeParameterValues = {}; + this.bboxType = ''; + this.bboxRefSpatialUnit = null; + this.bbox_minx = null; + this.bbox_miny = null; + this.bbox_maxx = null; + this.bbox_maxy = null; + this.attributeMapping_sourceAttributeName = ''; + this.attributeMapping_destinationAttributeName = ''; + this.attributeMappings_adminView = []; + this.keepAttributes = true; + this.keepMissingValues = true; + this.successMessagePart = ''; + this.errorMessagePart = ''; + this.importerErrors = []; + this.importedFeatures = []; + this.converterDefinition = null; + this.datasourceTypeDefinition = null; + this.propertyMappingDefinition = null; + this.postBody_spatialUnits = null; + this.idPropertyNotFound = false; + this.namePropertyNotFound = false; + this.spatialUnitDataSourceInputInvalid = false; + this.spatialUnitDataSourceInputInvalidReason = ''; + + // Reset role management + this.ownerOrganization = ''; + this.ownerOrgFilter = ''; + this.isPublic = false; + this.resourcesCreatorRights = []; + this.showRoleForm = false; + + // Reset role management table + if (this.kommonitorDataExchangeService.accessControl) { + this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid( + 'spatialUnitAddRoleManagementTable', + this.roleManagementTableOptions, + this.kommonitorDataExchangeService.accessControl, + [], + true + ); + } + + this.metadataImportSettings = null; + this.mappingConfigImportSettings = null; + this.spatialUnitMetadataImportError = ''; + this.spatialUnitMappingConfigImportError = ''; + this.spatialUnitDataSourceIdPropertyInvalid = false; + this.spatialUnitDataSourceNamePropertyInvalid = false; + this.spatialUnitMappingConfigStructure = {}; + this.spatialUnitMetadataStructure_pretty = ''; + const attributeMappingTypes = this.kommonitorImporterHelperService.getAttributeMappingTypes(); + this.attributeMapping_attributeType = attributeMappingTypes[0]; + this.errorMessage = ''; + this.successMessage = ''; + } + + hideSuccessAlert() { + this.successMessage = ''; + } + + hideErrorAlert() { + this.errorMessage = ''; + } + + hideMetadataErrorAlert() { + this.spatialUnitMetadataImportError = ''; + } + + hideMappingConfigErrorAlert() { + this.spatialUnitMappingConfigImportError = ''; + } + + onChangeOwner(ownerOrganization: any) { + // Handle owner organization change + this.ownerOrganization = ownerOrganization; + + // Refresh roles for the selected organization + this.refreshRoles(ownerOrganization); + + // Show/hide the role form based on whether an organization is selected + this.showRoleForm = !!ownerOrganization; + } + + onChangeIsPublic(isPublic: boolean) { + // Handle public access change + this.isPublic = isPublic; + } + + cancel() { + console.log('Modal cancelled'); + this.activeModal.dismiss('cancel'); + } + + private buildRoleManagementGridConfig() { + // Use service methods for base grid configuration + this.roleManagementDefaultColDef = this.kommonitorDataGridHelperService.buildRoleManagementDefaultColDef(); + const baseGridOptions = this.kommonitorDataGridHelperService.buildRoleManagementGridOptionsPublic( + this.roleManagementTableOptions?.components + ); + + // Apply component-specific overrides + this.roleManagementGridOptions = { + ...baseGridOptions, + onGridReady: (params) => { + this.onRoleManagementGridReady(params); + }, + onFirstDataRendered: (event) => { + this.onRoleManagementFirstDataRendered(event); + }, + onColumnResized: (event) => { + this.onRoleManagementColumnResized(event); + } + }; + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitDeleteModal/spatial-unit-delete-modal.component.css b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitDeleteModal/spatial-unit-delete-modal.component.css new file mode 100644 index 000000000..835052931 --- /dev/null +++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitDeleteModal/spatial-unit-delete-modal.component.css @@ -0,0 +1,303 @@ +/* Loading overlay */ +.loading-overlay-admin-panel { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 1050; +} + +.loading-overlay-admin-panel .glyphicon { + font-size: 2rem; +} + +.ng-hide { + display: none !important; +} + +/* Loading spinner animation */ +.icon-spin { + animation: spin 2s infinite linear; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +/* Modal header */ +.modal-header { + padding: 15px; + border-bottom: 1px solid #e5e5e5; +} + +.modal-header .close { + margin-top: -2px; + font-size: 21px; + font-weight: bold; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + opacity: 0.2; + border: none; + background: none; + cursor: pointer; +} + +.modal-header .close:hover, +.modal-header .close:focus { + color: #000; + text-decoration: none; + opacity: 0.5; +} + +.modal-title { + margin: 0; + line-height: 1.42857143; +} + +/* Modal body */ +.modal-body { + position: relative; + padding: 15px; +} + +.modal-body h4 { + margin-top: 0; + margin-bottom: 10px; +} + +.modal-body p { + margin: 0 0 10px; +} + +.modal-body ul { + margin-bottom: 10px; + padding-left: 20px; +} + +.modal-body li { + margin-bottom: 5px; +} + +.modal-body pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + color: #333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; +} + +/* Modal footer */ +.modal-footer { + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} + +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} + +.pull-left { + float: left !important; +} + +/* Button styles */ +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + border: 1px solid transparent; + border-radius: 4px; + text-decoration: none; +} + +.btn:focus, +.btn:active:focus, +.btn.active:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn:hover, +.btn:focus { + color: #333; + text-decoration: none; +} + +.btn-default { + color: #333; + background-color: #fff; + border-color: #ccc; +} + +.btn-default:hover, +.btn-default:focus { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} + +.btn-danger { + color: #fff; + background-color: #d9534f; + border-color: #d43f3a; +} + +.btn-danger:hover, +.btn-danger:focus { + color: #fff; + background-color: #c9302c; + border-color: #ac2925; +} + +.btn:disabled, +.btn[disabled] { + cursor: not-allowed; + opacity: 0.65; + box-shadow: none; +} + +/* Alert styles */ +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} + +.alert-success { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.alert-danger { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} + +.alert-dismissable .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} + +.alert h4 { + margin-top: 0; + color: inherit; +} + +.alert .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} + +.alert ul { + margin-bottom: 0; +} + +.alert li { + margin-bottom: 5px; +} + +/* Table styles */ +.table { + width: 100%; + max-width: 100%; + margin-bottom: 20px; + background-color: transparent; + border-collapse: collapse; + border-spacing: 0; +} + +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > tbody > tr > td, +.table > tfoot > tr > td { + padding: 8px; + line-height: 1.42857143; + vertical-align: top; + border-top: 1px solid #ddd; +} + +.table > thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid #ddd; +} + +.table-bordered { + border: 1px solid #ddd; +} + +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #ddd; +} + +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + border-bottom-width: 2px; +} + +.table-condensed > thead > tr > th, +.table-condensed > tbody > tr > th, +.table-condensed > tfoot > tr > th, +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > td { + padding: 5px; +} + +/* Font Awesome icons */ +.icon { + margin-right: 5px; +} + +.fa-check:before { + content: "\f00c"; +} + +.fa-ban:before { + content: "\f05e"; +} + +/* Hidden attribute support */ +[hidden] { + display: none !important; +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitDeleteModal/spatial-unit-delete-modal.component.html b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitDeleteModal/spatial-unit-delete-modal.component.html new file mode 100644 index 000000000..b5e3ead49 --- /dev/null +++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitDeleteModal/spatial-unit-delete-modal.component.html @@ -0,0 +1,62 @@ + + + + + + +
+ +

Folgende Raumebenen wurde erfolgreich gelöscht

+
    +
  • {{dataset.spatialUnitLevel}}
  • +
+
+ +
+ +

Löschen gescheitert

+ Folgende Datensätze konnten nicht gelöscht werden. +
+ + + + + + + + + + + + + +
NameFehlermeldung
{{dataset[0].spatialUnitLevel}}
+
\ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitDeleteModal/spatial-unit-delete-modal.component.ts b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitDeleteModal/spatial-unit-delete-modal.component.ts new file mode 100644 index 000000000..1e06bfb29 --- /dev/null +++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitDeleteModal/spatial-unit-delete-modal.component.ts @@ -0,0 +1,159 @@ +import { Component, OnInit, OnDestroy, Input } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; +import { HttpClient } from '@angular/common/http'; +import { Subscription } from 'rxjs'; +import { KommonitorDataExchangeService } from 'services/adminSpatialUnit/kommonitor-data-exchange.service'; + +declare const $: any; +declare const __env: any; + +@Component({ + selector: 'spatial-unit-delete-modal-new', + templateUrl: './spatial-unit-delete-modal.component.html', + styleUrls: ['./spatial-unit-delete-modal.component.css'] +}) +export class SpatialUnitDeleteModalComponent implements OnInit, OnDestroy { + @Input() datasetsToDelete: any[] = []; + + loadingData = false; + errorMessage = ''; + successMessage = ''; + + successfullyDeletedDatasets: any[] = []; + failedDatasetsAndErrors: any[] = []; + + // Subscriptions + private subscriptions: Subscription[] = []; + + constructor( + public activeModal: NgbActiveModal, + public kommonitorDataExchangeService: KommonitorDataExchangeService, + private http: HttpClient, + private broadcastService: BroadcastService + ) { + console.log('SpatialUnitDeleteModalComponent constructor initialized'); + } + + ngOnInit(): void { + this.setupEventListeners(); + this.resetForm(); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(sub => sub.unsubscribe()); + } + + private setupEventListeners(): void { + // Setup broadcast listeners + const broadcastSubscription = this.broadcastService.currentBroadcastMsg.subscribe(broadcastMsg => { + if (broadcastMsg && broadcastMsg.msg === 'onDeleteSpatialUnits') { + const datasets = Array.isArray(broadcastMsg.values) ? broadcastMsg.values : [broadcastMsg.values]; + this.onDeleteSpatialUnits(datasets); + } + }); + + this.subscriptions.push(broadcastSubscription); + } + + onDeleteSpatialUnits(datasets: any[]): void { + this.loadingData = true; + this.datasetsToDelete = datasets; + this.resetForm(); + + setTimeout(() => { + this.loadingData = false; + }, 100); + } + + resetForm(): void { + this.successfullyDeletedDatasets = []; + this.failedDatasetsAndErrors = []; + this.errorMessage = ''; + this.successMessage = ''; + } + + async deleteSpatialUnits(): Promise { + this.loadingData = true; + this.resetForm(); + + try { + // Use service method for bulk deletion + const spatialUnitIds = this.datasetsToDelete.map(dataset => dataset.spatialUnitId); + const result = await this.kommonitorDataExchangeService.bulkDeleteSpatialUnits(spatialUnitIds); + + // Process results + this.successfullyDeletedDatasets = this.datasetsToDelete.filter(dataset => + result.successful.includes(dataset.spatialUnitId) + ); + + this.failedDatasetsAndErrors = result.failed.map(failure => { + const dataset = this.datasetsToDelete.find(d => d.spatialUnitId === failure.id); + return [dataset, failure.error]; + }); + + if (this.failedDatasetsAndErrors.length > 0) { + this.errorMessage = 'Einige Raumebenen konnten nicht gelöscht werden.'; + } + + if (this.successfullyDeletedDatasets.length > 0) { + this.successMessage = `${this.successfullyDeletedDatasets.length} Raumebene(n) erfolgreich gelöscht.`; + + // Fetch indicator metadata again as spatial units were deleted + await this.kommonitorDataExchangeService.fetchIndicatorsMetadata( + this.kommonitorDataExchangeService.currentKeycloakLoginRoles + ); + + // Refresh spatial unit overview table + const deletedIds = this.successfullyDeletedDatasets.map(dataset => dataset.spatialUnitId); + this.broadcastService.broadcast('refreshSpatialUnitOverviewTable', ['delete', deletedIds]); + + // Refresh all admin dashboard diagrams due to modified metadata + setTimeout(() => { + this.broadcastService.broadcast('refreshAdminDashboardDiagrams'); + }, 500); + } + + this.loadingData = false; + + // Auto-close modal after successful deletion + if (this.successfullyDeletedDatasets.length > 0 && this.failedDatasetsAndErrors.length === 0) { + setTimeout(() => { + this.activeModal.close({ + action: 'deleted', + deletedDatasets: this.successfullyDeletedDatasets + }); + }, 2000); + } + + } catch (error) { + console.error('Error during bulk deletion:', error); + this.errorMessage = 'Ein unerwarteter Fehler ist aufgetreten.'; + this.loadingData = false; + } + } + + + + hideSuccessAlert(): void { + this.successMessage = ''; + } + + hideErrorAlert(): void { + this.errorMessage = ''; + } + + // Modal control methods + closeModal(): void { + this.activeModal.dismiss('cancel'); + } + + // Helper methods + get hasValidDatasets(): boolean { + return this.datasetsToDelete && this.datasetsToDelete.length > 0; + } + + get canDelete(): boolean { + return this.hasValidDatasets && !this.loadingData; + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditFeaturesModal/spatial-unit-edit-features-modal.component.css b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditFeaturesModal/spatial-unit-edit-features-modal.component.css new file mode 100644 index 000000000..4c342853f --- /dev/null +++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditFeaturesModal/spatial-unit-edit-features-modal.component.css @@ -0,0 +1,1316 @@ +/* AG Grid CSS imports */ +@import '~ag-grid-community/styles/ag-grid.css'; +@import '~ag-grid-community/styles/ag-theme-alpine.css'; + +/* AG Grid specific styles for this component */ +.ag-theme-alpine { + --ag-header-height: 50px; + --ag-row-height: 48px; + --ag-header-foreground-color: #333; + --ag-header-background-color: #f8f9fa; + --ag-odd-row-background-color: #f8f9fa; + --ag-row-hover-color: #e9ecef; + --ag-selected-row-background-color: #007bff; + --ag-font-size: 14px; + --ag-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; +} + +/* Grid container styling */ +.featureTableWrapper { + margin: 20px 0; + border: 1px solid #dee2e6; + border-radius: 8px; + overflow: hidden; + background-color: #fff; +} + +.featureTableWrapper ag-grid-angular { + display: block; + width: 100%; + height: 50vh; + min-height: 400px; +} + +/* Ensure grid has proper dimensions */ +#spatialUnitFeatureTable { + width: 100% !important; + height: 100% !important; +} + +.ag-theme-alpine .ag-header-cell { + border-bottom: 2px solid #dee2e6; + font-weight: 600; +} + +.ag-theme-alpine .ag-header-cell-filtered { + background-color: #e3f2fd; +} + +.ag-theme-alpine .ag-floating-filter-body input { + border: 1px solid #ced4da; + border-radius: 4px; + padding: 4px 8px; + font-size: 14px; +} + +.ag-theme-alpine .ag-paging-panel { + background-color: #f8f9fa; + border-top: 1px solid #dee2e6; + padding: 10px; +} + +.ag-theme-alpine .ag-paging-button { + background-color: #fff; + border: 1px solid #ced4da; + border-radius: 4px; + padding: 6px 12px; + margin: 0 2px; + cursor: pointer; +} + +.ag-theme-alpine .ag-paging-button:hover { + background-color: #e9ecef; + border-color: #adb5bd; +} + +.ag-theme-alpine .ag-paging-button:disabled { + background-color: #e9ecef; + color: #6c757d; + cursor: not-allowed; +} + +.ag-theme-alpine .ag-paging-page-summary-panel { + margin: 0 15px; +} + +.ag-theme-alpine .ag-paging-page-size-select { + border: 1px solid #ced4da; + border-radius: 4px; + padding: 4px 8px; +} + +/* Loading overlay */ +.loading-overlay-admin-panel { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 1050; +} + +.loading-overlay-admin-panel .spinner-border { + width: 3rem; + height: 3rem; +} + +/* Modal header */ +.modal-header { + background-color: #f8f9fa; + border-bottom: 1px solid #dee2e6; + padding: 1rem 1.5rem; +} + +.modal-header .modal-title { + font-weight: 500; + color: #343a40; +} + +.modal-header .btn-close { + background: none; + border: none; + font-size: 1.5rem; + color: #6c757d; + cursor: pointer; +} + +.modal-header .btn-close:hover { + color: #343a40; +} + +/* Modal body */ +.modal-body { + padding: 1.5rem; + max-height: 70vh; + overflow-y: auto; +} + +/* Form styles */ +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: #343a40; +} + +.form-control { + display: block; + width: 100%; + padding: 0.5rem 0.75rem; + line-height: 1.5; + color: #495057; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #ced4da; + border-radius: 0.25rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.form-control:focus { + color: #495057; + background-color: #fff; + border-color: #80bdff; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.form-control:invalid { + border-color: #dc3545; +} + +.form-control:invalid:focus { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +/* Progress bar */ +.progress-container { + margin-bottom: 2rem; +} + +.progressbar { + counter-reset: step; + list-style: none; + padding: 0; + margin: 0; + display: flex; + justify-content: space-between; + position: relative; +} + +.progressbar::before { + content: ''; + position: absolute; + top: 15px; + left: 0; + width: 100%; + height: 2px; + background-color: #dee2e6; + z-index: 1; +} + +.progressbar li { + counter-increment: step; + position: relative; + text-align: center; + color: #6c757d; + font-weight: 500; + z-index: 2; +} + +.progressbar li::before { + content: counter(step); + width: 30px; + height: 30px; + border-radius: 50%; + background-color: #dee2e6; + color: #6c757d; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 0.5rem; + font-weight: bold; + font-size: 0.875rem; +} + +.progressbar li.active { + color: #007bff; +} + +.progressbar li.active::before { + background-color: #007bff; + color: #fff; +} + +.progressbar li.active ~ li::before { + background-color: #dee2e6; + color: #6c757d; +} + +/* Fieldset styles */ +fieldset { + border: none; + margin: 0; + padding: 1rem 3.5rem; + min-width: 0; +} + +.fs-title { + font-size: 18px; + font-weight: 500; + color: #343a40; + margin-bottom: 0.5rem; +} + +.fs-subtitle { + font-size: 1rem; + color: #6c757d; + margin-bottom: 1.5rem; +} + +/* Multi-step form styles */ +.multiStepForm { + width: 100%; +} + +.vertical-align { + display: flex; + align-items: flex-start; +} + +.vertical-align .col-md-3, +.vertical-align .col-md-6 { + margin-bottom: 1rem; +} + +/* Button styles */ +.btn { + display: inline-block; + font-weight: 400; + text-align: center; + white-space: nowrap; + vertical-align: middle; + user-select: none; + border: 1px solid transparent; + padding: 0.375rem 0.75rem; + line-height: 1.5; + border-radius: 0.25rem; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + cursor: pointer; + text-decoration: none; +} + +.btn:hover { + text-decoration: none; +} + +.btn:focus, +.btn.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.btn-primary { + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.btn-primary:hover { + color: #fff; + background-color: #0056b3; + border-color: #0056b3; +} + +.btn-primary:focus, +.btn-primary.focus { + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); +} + +.btn-secondary { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-secondary:hover { + color: #fff; + background-color: #545b62; + border-color: #545b62; +} + +.btn-success { + color: #fff; + background-color: #28a745; + border-color: #28a745; +} + +.btn-success:hover { + color: #fff; + background-color: #218838; + border-color: #218838; +} + +.btn-info { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} + +.btn-info:hover { + color: #fff; + background-color: #138496; + border-color: #138496; +} + +.btn-warning { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} + +.btn-warning:hover { + color: #212529; + background-color: #e0a800; + border-color: #e0a800; +} + +.btn-danger { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} + +.btn-danger:hover { + color: #fff; + background-color: #c82333; + border-color: #c82333; +} + +.btn:disabled, +.btn.disabled { + opacity: 0.65; + cursor: not-allowed; +} + +.btn-sm { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.btn-group { + position: relative; + display: inline-flex; + vertical-align: middle; +} + +.btn-group > .btn { + position: relative; + flex: 1 1 auto; +} + +.btn-group > .btn:not(:first-child) { + margin-left: -1px; +} + +.btn-group > .btn:not(:last-child):not(.dropdown-toggle) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.btn-group > .btn:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +/* Form check styles */ +.form-check { + position: relative; + display: block; + padding-left: 1.25rem; + margin-bottom: 0.5rem; +} + +.form-check-input { + position: absolute; + margin-top: 0.3rem; + margin-left: -1.25rem; +} + +.form-check-label { + margin-bottom: 0; +} + +.form-check-input[type="checkbox"] { + border-radius: 0.25rem; +} + +.form-check-input:checked { + background-color: #007bff; + border-color: #007bff; +} + +.form-check-input:focus { + border-color: #80bdff; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.form-switch { + padding-left: 2.5rem; +} + +.form-switch .form-check-input { + width: 2rem; + margin-left: -2.5rem; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e"); + background-position: left center; + background-repeat: no-repeat; + background-size: contain; + border-radius: 2rem; +} + +.form-switch .form-check-input:checked { + background-position: right center; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 1.0%29'/%3e%3c/svg%3e"); +} + +/* Input group styles */ +.input-group { + position: relative; + display: flex; + flex-wrap: wrap; + align-items: stretch; + width: 100%; +} + +.input-group > .form-control { + position: relative; + flex: 1 1 auto; + width: 1%; + min-width: 0; +} + +.input-group-text { + display: flex; + align-items: center; + padding: 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + text-align: center; + white-space: nowrap; + background-color: #e9ecef; + border: 1px solid #ced4da; + border-radius: 0.25rem; +} + +.input-group > .input-group-text:not(:last-child) { + border-right: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group > .form-control:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group > .form-control:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +/* Help block styles */ +.help-block { + display: block; + margin-top: 0.25rem; + margin-bottom: 0; + font-size: 0.875rem; + color: #6c757d; +} + +.help-block p { + margin-bottom: 0; +} + +.text-danger { + color: #dc3545; +} + +/* Table styles */ +.table { + width: 100%; + margin-bottom: 1rem; + color: #212529; + vertical-align: top; + border-color: #dee2e6; +} + +.table th, +.table td { + padding: 0.5rem; + vertical-align: top; + border-top: 1px solid #dee2e6; +} + +.table thead th { + vertical-align: bottom; + border-bottom: 2px solid #dee2e6; + background-color: #f8f9fa; + font-weight: 500; +} + +.table-condensed th, +.table-condensed td { + padding: 0.25rem; +} + +.table tbody tr:nth-of-type(odd) { + background-color: rgba(0, 0, 0, 0.05); +} + +/* Feature table wrapper */ +.admin-table-wrapper { + margin: 1rem 0; + border: 1px solid #dee2e6; + border-radius: 0.25rem; + overflow: hidden; +} + +.featureTableWrapper { + height: 50vh; + width: 100%; +} + +.ag-theme-alpine { + --ag-background-color: #fff; + --ag-border-color: #dee2e6; + --ag-header-background-color: #f8f9fa; + --ag-header-foreground-color: #343a40; + --ag-row-hover-color: #f8f9fa; + --ag-selected-row-background-color: #007bff; + --ag-selected-row-foreground-color: #fff; +} + +/* Alert styles */ +.alert { + position: relative; + padding: 0.75rem 3.25rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +.alert-success { + color: #155724; + background-color: #d4edda; + border-color: #c3e6cb; +} + +.alert-danger { + color: #721c24; + background-color: #f8d7da; + border-color: #f5c6cb; +} + +.alert-dismissible { + padding-right: 4rem; +} + +.alert-dismissible .btn-close { + position: absolute; + top: 0; + right: 0; + padding: 0.75rem 1.25rem; + color: inherit; + background: none; + border: none; + font-size: 1.25rem; + line-height: 1; + cursor: pointer; +} + +.alert-dismissible .btn-close:hover { + opacity: 0.75; +} + +/* Import/Export buttons */ +.import-export-buttons { + position: absolute; + top: 0; + right: 0; + z-index: 10; +} + +.import-export-buttons .btn { + margin-left: 0.5rem; +} + +/* Form navigation */ +.form-navigation { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 2rem; + padding-top: 1rem; + border-top: 1px solid #dee2e6; +} + +.form-navigation .btn { + margin: 0 0.25rem; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .col-md-3, + .col-md-6 { + margin-bottom: 1rem; + } + + .vertical-align { + flex-direction: column; + } + + .form-navigation { + flex-direction: column; + gap: 0.5rem; + } + + .form-navigation .btn { + width: 100%; + margin: 0.25rem 0; + } + + .import-export-buttons { + position: static; + margin-bottom: 1rem; + } + + .import-export-buttons .btn { + margin: 0.25rem; + width: 100%; + } +} + +/* Utility classes */ +.d-none { + display: none; +} + +.d-block { + display: block; +} + +.d-flex { + display: flex; +} + +.justify-content-center { + justify-content: center; +} + +.align-items-center { + align-items: center; +} + +.text-center { + text-align: center; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Fade animation */ +.fade { + transition: opacity 0.15s linear; +} + +.fade:not(.show) { + opacity: 0; +} + +.show { + opacity: 1; +} + +/* Custom scrollbar */ +.modal-body::-webkit-scrollbar { + width: 8px; +} + +.modal-body::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +.modal-body::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; +} + +.modal-body::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} + +/* AG Grid cell editing visual feedback */ +.ag-theme-alpine .ag-cell[style*="background-color: #9DC89F"] { + background-color: #9DC89F !important; + transition: background-color 0.3s ease; +} + +.ag-theme-alpine .ag-cell[style*="background-color: #E79595"] { + background-color: #E79595 !important; + transition: background-color 0.3s ease; +} + +/* Cell editing success state */ +.cell-edit-success { + background-color: #9DC89F !important; + transition: background-color 0.3s ease; +} + +/* Cell editing error state */ +.cell-edit-error { + background-color: #E79595 !important; + transition: background-color 0.3s ease; +} + +/* Timestamp display styles */ +.timestamp-success { + background-color: #9DC89F; + color: #155724; + padding: 0.5rem; + border-radius: 0.25rem; + margin-bottom: 0.5rem; + font-weight: 500; +} + +.timestamp-error { + background-color: #E79595; + color: #721c24; + padding: 0.5rem; + border-radius: 0.25rem; + margin-bottom: 0.5rem; + font-weight: 500; +} + +/* ng-bootstrap Datepicker Styles */ +.datepicker-dropdown { + position: absolute !important; + z-index: 9999 !important; + background: white !important; + border: 1px solid #ccc !important; + border-radius: 4px !important; + box-shadow: 0 2px 10px rgba(0,0,0,0.1) !important; + margin-top: 2px !important; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important; + width: 320px !important; + left: 0 !important; + top: 100% !important; +} + +/* Position the datepicker relative to the input group */ +.input-group { + position: relative !important; +} + +.input-group .datepicker-dropdown { + position: absolute !important; + top: 100% !important; + left: 0 !important; + right: auto !important; + margin-top: 2px !important; + z-index: 9999 !important; +} + +/* Ensure datepicker renders above all other elements */ +.ngb-datepicker { + position: absolute !important; + z-index: 9999 !important; + background: white !important; + border: 1px solid #ccc !important; + border-radius: 4px !important; + box-shadow: 0 2px 10px rgba(0,0,0,0.1) !important; + margin-top: 2px !important; + width: 320px !important; + left: 0 !important; + top: 100% !important; +} + +/* Override any ng-bootstrap default positioning */ +.ngb-datepicker-picker { + position: absolute !important; + z-index: 9999 !important; + background: white !important; + border: 1px solid #ccc !important; + border-radius: 4px !important; + box-shadow: 0 2px 10px rgba(0,0,0,0.1) !important; + margin-top: 2px !important; + width: 320px !important; + left: 0 !important; + top: 100% !important; +} + +/* Ensure the datepicker container doesn't clip content */ +.date-input-group { + overflow: visible !important; + position: relative !important; +} + +/* Force datepicker to render outside button group */ +.input-group-btn { + position: relative !important; + overflow: visible !important; +} + +.input-group-btn .ngb-datepicker { + position: absolute !important; + z-index: 9999 !important; + left: 0 !important; + top: 100% !important; + margin-top: 2px !important; +} + +/* Date input group specific styling - matching original AngularJS version */ +.date-input-group { + border-radius: 4px !important; + overflow: visible !important; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) !important; + position: relative !important; +} + +/* Left button styling for calendar icon */ +.date-input-group .input-group-btn { + position: relative !important; +} + +.date-input-group .date-toggle-btn { + border-right: none !important; + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + background-color: #f8f9fa !important; + border-color: #ced4da !important; + color: #495057 !important; + padding: 8px 12px !important; + min-width: 40px !important; + transition: all 0.15s ease-in-out !important; + border-top-left-radius: 4px !important; + border-bottom-left-radius: 4px !important; +} + +.date-input-group .date-toggle-btn:hover { + background-color: #e9ecef !important; + border-color: #adb5bd !important; + color: #007bff !important; +} + +.date-input-group .date-toggle-btn:focus { + outline: none !important; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25) !important; +} + +.date-input-group .form-control { + border-left: none !important; + border-right: 1px solid #ced4da !important; + border-radius: 0 !important; + padding: 8px 42px !important; + font-size: 14px !important; + border-top-right-radius: 4px !important; + border-bottom-right-radius: 4px !important; + cursor: pointer !important; + transition: all 0.15s ease-in-out !important; +} + +.date-input-group .form-control:hover { + background-color: #f8f9fa !important; + border-color: #adb5bd !important; +} + +.date-input-group .form-control:focus { + border-color: #80bdff !important; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25) !important; + outline: none !important; + background-color: #fff !important; +} + +/* Enhanced visual feedback for clickable elements */ +.date-input-group .date-toggle-btn { + cursor: pointer !important; +} + +.date-input-group .date-toggle-btn:active { + background-color: #dee2e6 !important; + transform: translateY(1px) !important; +} + +.date-input-group .form-control:active { + background-color: #f8f9fa !important; +} + +/* Ensure proper spacing and alignment */ +.date-input-group .input-group-btn { + margin-right: 0 !important; +} + +.date-input-group .form-control { + margin-left: 0 !important; +} + +/* Align calendar button and input flush; remove unintended gaps */ +.date-input-group { + display: flex !important; + align-items: stretch !important; +} + +.date-input-group .input-group-btn { + flex: 0 0 auto !important; + margin: 0 !important; +} + +.date-input-group .date-toggle-btn { + height: 100% !important; + border-right: 0 !important; + z-index: 10; +} + +.date-input-group > div { + flex: 1 1 auto !important; + margin: 0 !important; + padding: 0 !important; +} + +.date-input-group > div > .form-control, +.date-input-group > .form-control { + width: 100% !important; + height: 100% !important; + border-left: 0 !important; +} + +/* Hover effect for the entire input group */ +.date-input-group:hover { + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15) !important; +} + +/* Enhanced datepicker styling with larger fonts - matching original design */ +.datepicker-dropdown .ngb-dp-header { + background-color: #f8f9fa !important; + border-bottom: 1px solid #dee2e6 !important; + padding: 18px 15px !important; + border-radius: 4px 4px 0 0 !important; +} + +.datepicker-dropdown .ngb-dp-month { + background: white !important; + padding: 15px !important; +} + +.datepicker-dropdown .ngb-dp-weekday { + color: #6c757d !important; + font-weight: 600 !important; + font-size: 18px !important; + padding: 12px 8px !important; + text-align: center !important; + text-transform: uppercase !important; + letter-spacing: 0.5px !important; +} + +.datepicker-dropdown .ngb-dp-day { + padding: 10px !important; + text-align: center !important; + cursor: pointer !important; + border-radius: 4px !important; + transition: all 0.15s ease-in-out !important; + font-weight: 500 !important; + font-size: 18px !important; + min-width: 45px !important; + height: 45px !important; + line-height: 25px !important; +} + +.datepicker-dropdown .ngb-dp-day:hover { + background-color: #e9ecef !important; + transform: scale(1.05) !important; +} + +.datepicker-dropdown .ngb-dp-day.selected { + background-color: #007bff !important; + color: white !important; + font-weight: bold !important; + box-shadow: 0 2px 4px rgba(0, 123, 255, 0.3) !important; +} + +.datepicker-dropdown .ngb-dp-day.focused { + background-color: #007bff !important; + color: white !important; + font-weight: bold !important; +} + +.datepicker-dropdown .ngb-dp-day.today { + background-color: #fff3cd !important; + color: #856404 !important; + font-weight: bold !important; + border: 2px solid #ffc107 !important; +} + +.datepicker-dropdown .ngb-dp-day.disabled { + color: #6c757d !important; + cursor: not-allowed !important; + opacity: 0.4 !important; +} + +.datepicker-dropdown .ngb-dp-day.outside { + color: #6c757d !important; + opacity: 0.5 !important; +} + +.datepicker-dropdown .ngb-dp-navigation-chevron { + border-style: solid !important; + border-width: 0.35em 0.35em 0 0 !important; + content: "" !important; + display: inline-block !important; + height: 0.7em !important; + transform: rotate(-45deg) !important; + vertical-align: top !important; + width: 0.7em !important; + color: #495057 !important; +} + +.datepicker-dropdown .ngb-dp-navigation-chevron.right { + transform: rotate(45deg) !important; +} + +.datepicker-dropdown .ngb-dp-month-name { + font-size: 20px !important; + font-weight: 600 !important; + color: #495057 !important; + text-transform: capitalize !important; +} + +.datepicker-dropdown .ngb-dp-arrow { + background: transparent !important; + border: none !important; + padding: 12px 18px !important; + cursor: pointer !important; + border-radius: 4px !important; + transition: all 0.15s ease-in-out !important; + min-width: 50px !important; +} + +.datepicker-dropdown .ngb-dp-arrow:hover { + background-color: #e9ecef !important; + transform: scale(1.1) !important; +} + +.datepicker-dropdown .ngb-dp-arrow:focus { + outline: 2px solid #007bff !important; + outline-offset: 2px !important; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .datepicker-dropdown { + width: 300px !important; + left: 50% !important; + transform: translateX(-50%) !important; + } + + .input-group .datepicker-dropdown { + left: 50% !important; + transform: translateX(-50%) !important; + } + + .datepicker-dropdown .ngb-dp-day { + font-size: 16px !important; + min-width: 40px !important; + height: 40px !important; + } + + .datepicker-dropdown .ngb-dp-weekday { + font-size: 16px !important; + } + + .datepicker-dropdown .ngb-dp-month-name { + font-size: 18px !important; + } +} + +/* Calendar Icon Fallback - Ensure icons are always visible */ +.calendar-icon-fallback { + display: inline-block !important; + font-size: 16px !important; + line-height: 1 !important; + margin-left: 4px !important; +} + +/* Font Awesome Calendar Icon Styling */ +.date-toggle-btn .fas.fa-calendar-alt { + display: inline-block !important; + font-size: 18px !important; + line-height: 1 !important; + color: #495057 !important; + margin-right: 4px !important; + transition: all 0.15s ease-in-out !important; +} + +/* Fallback: If Font Awesome is not available, show text prominently */ +.date-toggle-btn .fas.fa-calendar-alt:not([class*="fa-calendar-alt"]) { + display: none !important; +} + +.date-toggle-btn .fas.fa-calendar-alt:not([class*="fa-calendar-alt"]) + .calendar-text { + font-size: 16px !important; + font-weight: bold !important; + margin-left: 0 !important; +} + +/* New Calendar Icon Styling */ +.calendar-icon { + display: inline-block !important; + font-size: 18px !important; + line-height: 1 !important; + color: #495057 !important; + transition: all 0.15s ease-in-out !important; +} + +.date-toggle-btn:hover .calendar-icon { + color: #007bff !important; + transform: scale(1.1) !important; +} + +.date-toggle-btn:active .calendar-icon { + transform: scale(0.95) !important; +} + +/* Ensure Font Awesome calendar icons are always visible */ +.date-toggle-btn .fas.fa-calendar-alt { + display: inline-block !important; + font-size: 18px !important; + line-height: 1 !important; + color: #495057 !important; + margin-right: 4px !important; + transition: all 0.15s ease-in-out !important; +} + +.date-toggle-btn:hover .fas.fa-calendar-alt { + color: #007bff !important; + transform: scale(1.1) !important; +} + +.date-toggle-btn:active .fas.fa-calendar-alt { + transform: scale(0.95) !important; +} + +/* Calendar Text Styling - Always visible fallback */ +.calendar-text { + display: inline-block !important; + font-size: 12px !important; + font-weight: bold !important; + color: #495057 !important; + margin-left: 4px !important; + text-transform: uppercase !important; + letter-spacing: 0.5px !important; + transition: all 0.15s ease-in-out !important; +} + +.date-toggle-btn:hover .calendar-text { + color: #007bff !important; +} + +/* Ensure button has proper sizing for both icon and text */ +.date-toggle-btn { + min-width: 60px !important; + padding: 8px 12px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; +} + +/* SVG Calendar Icon Styling */ +.calendar-svg { + width: 16px !important; + height: 16px !important; + color: #495057 !important; + transition: all 0.15s ease-in-out !important; + margin-right: 4px !important; +} + +.date-toggle-btn:hover .calendar-svg { + color: #007bff !important; + transform: scale(1.1) !important; +} + +.date-toggle-btn:active .calendar-svg { + transform: scale(0.95) !important; +} + +/* Text sizing to MATCH add modal */ +.multiStepForm { + font-size: 12px !important; +} + +/* Progress bar text sizes like add modal */ +#progressbar li { + font-size: 9px !important; +} +#progressbar li:before { + font-size: 12px !important; +} + +/* Remove inflated modal-body font, keep default like add modal */ +:host ::ng-deep .modal-body { + font-size: inherit !important; +} + +/* Override small admin wrapper defaults within this modal */ +/* Use app defaults for admin table wrappers (matches add modal behavior) */ +:host ::ng-deep .admin-table-wrapper, +:host ::ng-deep .featureTableWrapper { + font-size: inherit !important; +} + +/* Strengthen progress bar overrides against global app styles (match add modal) */ +:host ::ng-deep #progressbar li { + font-size: 9px !important; +} +:host ::ng-deep #progressbar li:before { + font-size: 12px !important; +} + +/* Normalize key text elements */ +/* Keep form element sizes consistent with add modal (inputs 13px base) */ +:host ::ng-deep .form-group label { + font-size: inherit !important; +} +:host ::ng-deep .form-control { + font-size: 13px !important; +} +:host ::ng-deep .help-block { + font-size: 13px !important; +} +:host ::ng-deep table, +:host ::ng-deep th, +:host ::ng-deep td { + font-size: inherit !important; +} + +/* Center alignment for specific toggle switches */ +.toggle-center { + display: flex; + justify-content: center; + width: 100%; +} + +.toggle-center .switch { + display: inline-flex; +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditFeaturesModal/spatial-unit-edit-features-modal.component.html b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditFeaturesModal/spatial-unit-edit-features-modal.component.html new file mode 100644 index 000000000..a872dabcc --- /dev/null +++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditFeaturesModal/spatial-unit-edit-features-modal.component.html @@ -0,0 +1,534 @@ + + + + + + + +
+ +

Erfolg!

+ {{ successMessage }} +
+

Raumebene: {{successMessagePart}}

+
+
+ + +
+ +

Fehler!

+ {{ errorMessage }} +
+

+  
+
+

Bei den {{importerErrors.length}} Raumeinheiten mit folgenden IDs scheitert der Import:

+
+      
    +
  • {{error}}
  • +
+
+

Bitte beheben Sie die angezeigten Fehler im Datensatz und wiederholen den Prozess.

+
+
+ + +
+ +

Mapping-Konfiguration Fehler!

+

+
\ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditFeaturesModal/spatial-unit-edit-features-modal.component.ts b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditFeaturesModal/spatial-unit-edit-features-modal.component.ts new file mode 100644 index 000000000..c33b8264b --- /dev/null +++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditFeaturesModal/spatial-unit-edit-features-modal.component.ts @@ -0,0 +1,1295 @@ +import { Component, OnInit, OnDestroy, Inject, ViewChild, ElementRef } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; +import { HttpClient } from '@angular/common/http'; +import { Subscription } from 'rxjs'; +import { KommonitorDataGridHelperService } from 'services/adminSpatialUnit/kommonitor-data-grid-helper.service'; +import { KommonitorDataExchangeService } from 'services/adminSpatialUnit/kommonitor-data-exchange.service'; +import { KommonitorImporterHelperService } from 'services/adminSpatialUnit/kommonitor-importer-helper.service'; +import { AgGridAngular } from 'ag-grid-angular'; +import { ColDef, GridOptions, GridApi, ColumnApi, GridReadyEvent, FirstDataRenderedEvent, ColumnResizedEvent } from 'ag-grid-community'; + +declare const $: any; +declare const __env: any; + +@Component({ + selector: 'spatial-unit-edit-features-modal-new', + templateUrl: './spatial-unit-edit-features-modal.component.html', + styleUrls: ['./spatial-unit-edit-features-modal.component.css'] +}) +export class SpatialUnitEditFeaturesModalComponent implements OnInit, OnDestroy { + @ViewChild('mappingConfigImportFile', { static: false }) mappingConfigImportFile!: ElementRef; + @ViewChild('spatialUnitDataSourceInput', { static: false }) spatialUnitDataSourceInput!: ElementRef; + @ViewChild('spatialUnitFeatureTable', { static: true }) spatialUnitFeatureTable!: AgGridAngular; + // km-date-picker handles its own datepicker internally; no ngb refs needed + + // Multi-step form + currentStep = 1; + totalSteps = 2; + + // Form data + isSubmitting = false; + errorMessage = ''; + successMessage = ''; + loadingData = false; + + // Current dataset being edited + currentSpatialUnitDataset: any = null; + + // Basic form data + spatialUnitFeaturesGeoJSON: any = null; + remainingFeatureHeaders: string[] = []; + spatialUnitMappingConfigStructure_pretty = ''; + spatialUnitMappingConfigImportError = ''; + + // Period of validity + periodOfValidity: { startDate: string; endDate: string } = { + startDate: '', + endDate: '' + }; + periodOfValidityInvalid = false; + + // Data source input + geoJsonString: string = ''; + spatialUnit_asGeoJson: any = null; + spatialUnitEditFeaturesDataSourceInputInvalidReason = ''; + spatialUnitEditFeaturesDataSourceInputInvalid = false; + fileSelected: boolean = false; + spatialUnitDataSourceIdProperty = ''; + spatialUnitDataSourceNameProperty = ''; + + // Converter settings + converter: any = null; + schema: string = ''; + mimeType: string = ''; + datasourceType: any = null; + + // Importer objects + converterDefinition: any = null; + datasourceTypeDefinition: any = null; + propertyMappingDefinition: any = null; + putBody_spatialUnits: any = null; + + // Validity dates per feature + validityEndDate_perFeature = ''; + validityStartDate_perFeature = ''; + + // Attribute mapping + attributeMapping_sourceAttributeName = ''; + attributeMapping_destinationAttributeName = ''; + attributeMapping_data: any = null; + attributeMapping_attributeType: any = null; + attributeMappings_adminView: any[] = []; + keepAttributes = true; + keepMissingValues = true; + + // Partial update + isPartialUpdate = false; + + // Error handling + importerErrors: any[] = []; + successMessagePart = ''; + errorMessagePart = ''; + + // Available options + availableDatasourceTypes: any[] = []; + availableConverters: any[] = []; + availableSpatialUnits: any[] = []; + + // Bbox parameters for OGCAPI_FEATURES + bboxType: string = ''; + bboxRefSpatialUnit: any = null; + bboxRefSpatialUnitLevel: string = ''; + bbox_minx: any = null; + bbox_miny: any = null; + bbox_maxx: any = null; + bbox_maxy: any = null; + + // Feature table settings + enableDeleteFeatures = false; + + // Import/Export functionality + mappingConfigImportSettings: any = null; + + // Grid options for feature table + featureTableGridOptions: GridOptions = {}; + private gridApi!: GridApi; + private columnApi!: ColumnApi; + + // AG Grid inputs (align with parent component pattern) + public columnDefs: ColDef[] = []; + public rowData: any[] = []; + public defaultColDef: ColDef = {}; + public paginationPageSize: number = 20; + public paginationPageSizeSelector: number[] = [10, 20, 50, 100]; + + // Persisted converter parameter values (e.g., CRS) + public converterParameters: { [key: string]: any } = {}; + public datasourceTypeParameters: { [key: string]: any } = {}; + + // compare functions for selects to keep selection across renders + public compareConverter = (a: any, b: any) => a && b ? a.name === b.name : a === b; + public compareDatasourceType = (a: any, b: any) => a && b ? a.type === b.type : a === b; + + // Subscriptions + private subscriptions: Subscription[] = []; + + constructor( + public activeModal: NgbActiveModal, + public kommonitorDataExchangeService: KommonitorDataExchangeService, + public kommonitorImporterHelperService: KommonitorImporterHelperService, + public kommonitorDataGridHelperService: KommonitorDataGridHelperService, + private http: HttpClient, + private broadcastService: BroadcastService + ) { + } + + async ngOnInit(): Promise { + this.initializeDatePickers(); + this.initializeForm(); + this.setupEventListeners(); + await this.loadAvailableOptions(); + this.buildFeatureTable(); + this.ensureGridConfiguration(); + + // Add a small delay to ensure everything is initialized + setTimeout(() => { + this.checkGridConfiguration(); + }, 500); + } + + private checkGridConfiguration(): void { + // Grid configuration check completed + } + + private ensureGridConfiguration(): void { + // Ensure grid options are properly configured + if (this.featureTableGridOptions) { + // Force enable pagination and filtering + this.featureTableGridOptions.pagination = true; + this.featureTableGridOptions.paginationPageSize = 20; + this.featureTableGridOptions.paginationPageSizeSelector = [10, 20, 50, 100]; + + // Ensure defaultColDef has proper filtering + if (this.featureTableGridOptions.defaultColDef) { + this.featureTableGridOptions.defaultColDef.filter = true; + this.featureTableGridOptions.defaultColDef.floatingFilter = true; + this.featureTableGridOptions.defaultColDef.sortable = true; + } + } + } + + ngOnDestroy(): void { + this.subscriptions.forEach(sub => sub.unsubscribe()); + } + + private initializeDatePickers(): void { + // ng-bootstrap date pickers are automatically initialized via template + // No additional initialization needed + } + + private initializeForm(): void { + // Initialize form with defaults + this.spatialUnitMappingConfigStructure_pretty = this.kommonitorDataExchangeService?.syntaxHighlightJSON( + this.kommonitorImporterHelperService?.mappingConfigStructure + ) || ''; + + if (this.kommonitorImporterHelperService?.attributeMapping_attributeTypes?.length > 0) { + this.attributeMapping_attributeType = this.kommonitorImporterHelperService.attributeMapping_attributeTypes[0]; + } + } + + private setupEventListeners(): void { + // Setup broadcast listeners + const broadcastSubscription = this.broadcastService.currentBroadcastMsg.subscribe(broadcastMsg => { + if (broadcastMsg) { + if (broadcastMsg.msg === 'onEditSpatialUnitFeatures') { + this.onEditSpatialUnitFeatures(broadcastMsg.values); + } else if (broadcastMsg.msg === 'showLoadingIcon_' + this.kommonitorDataGridHelperService?.resourceType_spatialUnit) { + this.loadingData = true; + } else if (broadcastMsg.msg === 'hideLoadingIcon_' + this.kommonitorDataGridHelperService?.resourceType_spatialUnit) { + this.loadingData = false; + } else if (broadcastMsg.msg === 'onDeleteFeatureEntry_' + this.kommonitorDataGridHelperService?.resourceType_spatialUnit) { + // Handle individual feature deletion + this.broadcastService.broadcast('refreshSpatialUnitOverviewTable', { + crudType: 'edit', + targetSpatialUnitId: this.currentSpatialUnitDataset?.spatialUnitId + }); + this.refreshSpatialUnitEditFeaturesOverviewTable(); + } + } + }); + + this.subscriptions.push(broadcastSubscription); + } + + private async loadAvailableOptions(): Promise { + // Wait for the importer helper service to load data if it hasn't already + if (!this.kommonitorImporterHelperService?.getAvailableDatasourceTypes()?.length) { + try { + await this.kommonitorImporterHelperService.fetchResourcesFromImporter(); + } catch (error) { + console.error('Error loading importer resources:', error); + } + } + + // Load available datasource types from the importer helper service + this.availableDatasourceTypes = this.kommonitorImporterHelperService?.getAvailableDatasourceTypes() || []; + // Cache available converters to preserve object identity across renders + this.availableConverters = this.kommonitorImporterHelperService?.getAvailableConverters() || []; + } + + private buildFeatureTable(): void { + + // Get base configuration from service + const baseGridOptions = this.kommonitorDataGridHelperService.buildDataGrid_featureTable_spatialResource( + "spatialUnitFeatureTable", + this.remainingFeatureHeaders || [], + this.spatialUnitFeaturesGeoJSON?.features || [], + this.currentSpatialUnitDataset?.spatialUnitId, + this.kommonitorDataGridHelperService.resourceType_spatialUnit, + this.enableDeleteFeatures + ); + + // Extract service configuration + const columnDefs = baseGridOptions.columnDefs || []; + const rowData = baseGridOptions.rowData || []; + const defaultColDef = baseGridOptions.defaultColDef || {}; + + // Bind to template inputs + this.columnDefs = columnDefs; + this.rowData = rowData; + this.defaultColDef = { + ...defaultColDef, + editable: true, + sortable: true, + flex: 1, + minWidth: 150, + filter: true, + floatingFilter: true, + resizable: true, + wrapText: true, + autoHeight: true, + cellEditor: 'agLargeTextCellEditor', + cellStyle: { + 'font-size': '12px', + 'white-space': 'normal !important', + 'line-height': '20px !important', + 'word-break': 'break-word !important', + 'padding-top': '17px', + 'padding-bottom': '17px' + } + }; + + // Override with component-specific settings + this.featureTableGridOptions = { + ...baseGridOptions, + columnDefs: this.columnDefs, + rowData: this.rowData, + defaultColDef: this.defaultColDef, + // Pagination settings + pagination: true, + paginationPageSize: this.paginationPageSize, + paginationPageSizeSelector: this.paginationPageSizeSelector, + // Grid features + suppressRowClickSelection: true, + rowSelection: 'multiple', + enableCellTextSelection: true, + ensureDomOrder: true, + suppressColumnVirtualisation: true, + // enables undo / redo + undoRedoCellEditing: true, + undoRedoCellEditingLimit: 10, + // enables flashing to help see cell changes + enableCellChangeFlash: true, + onGridReady: (params: any) => { + this.gridApi = params.api; + this.columnApi = params.columnApi; + }, + onFirstDataRendered: () => { + this.headerHeightSetter(); + this.registerFeatureTableClickHandlers(); + }, + onColumnResized: () => { + this.headerHeightSetter(); + } + }; + + + } + + onEditSpatialUnitFeatures(spatialUnitDataset: any): void { + if (this.currentSpatialUnitDataset && + this.currentSpatialUnitDataset.spatialUnitLevel === spatialUnitDataset.spatialUnitLevel) { + return; + } + + this.currentSpatialUnitDataset = spatialUnitDataset; + this.resetForm(); + this.buildFeatureTable(); + } + + resetForm(): void { + // Reset edit banners + if (this.kommonitorDataGridHelperService) { + this.kommonitorDataGridHelperService.featureTable_spatialUnit_lastUpdate_timestamp_success = undefined; + this.kommonitorDataGridHelperService.featureTable_spatialUnit_lastUpdate_timestamp_failure = undefined; + } + + // Reset form data + this.spatialUnitFeaturesGeoJSON = null; + this.remainingFeatureHeaders = []; + this.periodOfValidity = { startDate: '', endDate: '' }; + this.periodOfValidityInvalid = false; + this.geoJsonString = ''; + this.spatialUnit_asGeoJson = null; + this.spatialUnitEditFeaturesDataSourceInputInvalidReason = ''; + this.spatialUnitEditFeaturesDataSourceInputInvalid = false; + this.spatialUnitDataSourceIdProperty = ''; + this.spatialUnitDataSourceNameProperty = ''; + this.converter = null; + this.schema = ''; + this.mimeType = ''; + this.datasourceType = null; + this.converterDefinition = null; + this.datasourceTypeDefinition = null; + this.propertyMappingDefinition = null; + this.putBody_spatialUnits = null; + this.validityEndDate_perFeature = ''; + this.validityStartDate_perFeature = ''; + this.attributeMapping_sourceAttributeName = ''; + this.attributeMapping_destinationAttributeName = ''; + this.attributeMapping_data = null; + this.attributeMapping_attributeType = this.kommonitorImporterHelperService?.attributeMapping_attributeTypes?.[0]; + this.attributeMappings_adminView = []; + this.keepAttributes = true; + this.keepMissingValues = true; + this.isPartialUpdate = false; + this.enableDeleteFeatures = false; + this.fileSelected = false; + this.importerErrors = []; + this.successMessagePart = ''; + this.errorMessagePart = ''; + + // Hide alerts + this.hideSuccessAlert(); + this.hideErrorAlert(); + } + + onChangeConverter(schema?: any): void { + if (this.converter) { + // Initialize defaults like in Add modal + this.schema = this.converter.schemas ? this.converter.schemas[0] : ''; + this.mimeType = this.converter.mimeTypes ? this.converter.mimeTypes[0] : ''; + console.log('[EditFeatures] onChangeConverter', { + converter: this.converter?.name, + schema: this.schema, + mimeType: this.mimeType + }); + + // Update available datasource types. If converter doesn't declare supported datasources, + // fall back to all available types (matches Add modal behavior) + const allDatasourceTypes = this.kommonitorImporterHelperService?.getAvailableDatasourceTypes() || []; + const declared = (this.converter as any)?.datasources as string[] | undefined; + if (Array.isArray(declared) && declared.length > 0) { + this.availableDatasourceTypes = allDatasourceTypes.filter(dt => declared.includes(dt.type)); + } else { + this.availableDatasourceTypes = allDatasourceTypes; + } + + // Auto-select if only one datasource type is available + if (this.availableDatasourceTypes.length === 1) { + this.datasourceType = this.availableDatasourceTypes[0]; + this.onChangeDatasourceType(this.datasourceType); + } + } + } + + onChangeMimeType(mimeType: any): void { + this.mimeType = mimeType; + } + + onChangeDatasourceType(datasourceType: any): void { + this.datasourceType = datasourceType; + console.log('[EditFeatures] onChangeDatasourceType', { datasourceType: this.datasourceType?.type }); + + if (this.datasourceType && this.datasourceType.type === "OGCAPI_FEATURES") { + // Use array of available spatial units like in Add modal + this.availableSpatialUnits = this.kommonitorDataExchangeService?.availableSpatialUnits || []; + } + // reset DS param cache on type change + this.datasourceTypeParameters = {}; + this.bboxType = ''; + this.bboxRefSpatialUnitLevel = ''; + this.bbox_minx = this.bbox_miny = this.bbox_maxx = this.bbox_maxy = null; + } + + refreshSpatialUnitEditFeaturesOverviewTable(): void { + if (!this.currentSpatialUnitDataset) { + console.warn('No current spatial unit dataset selected'); + return; + } + + this.loadingData = true; + this.hideSuccessAlert(); + this.hideErrorAlert(); + + const url = `${this.kommonitorDataExchangeService.getBaseUrlToKomMonitorDataAPI_spatialResource()}/spatial-units/${this.currentSpatialUnitDataset.spatialUnitId}/allFeatures`; + + this.http.get(url).subscribe({ + next: (response: any) => { + this.spatialUnitFeaturesGeoJSON = response; + + // Use service method to extract remaining headers + this.remainingFeatureHeaders = this.kommonitorDataExchangeService.extractRemainingHeaders( + this.spatialUnitFeaturesGeoJSON?.features || [] + ); + + // Rebuild the grid options with new data + this.buildFeatureTable(); + + // Update the grid with new data + this.updateGridWithData(); + + // Register click handlers if delete features is enabled + if (this.enableDeleteFeatures) { + setTimeout(() => { + this.kommonitorDataGridHelperService.registerFeatureTableClickHandlers( + this.currentSpatialUnitDataset?.spatialUnitId, + this.kommonitorDataGridHelperService.resourceType_spatialUnit, + this.enableDeleteFeatures + ); + }, 100); + } + + // Use setTimeout to ensure proper change detection and DOM updates + setTimeout(() => { + this.loadingData = false; + + // If grid API is still not available, try to rebuild the grid + if (!this.gridApi && this.spatialUnitFeatureTable) { + this.buildFeatureTable(); + } + }, 500); // Increased timeout to show loading state longer + }, + error: (error) => { + console.error('Error fetching spatial unit features:', error); + this.handleError(error); + setTimeout(() => { + this.loadingData = false; + }, 500); // Increased timeout to show loading state longer + } + }); + } + + clearAllSpatialUnitFeatures(): void { + if (!this.currentSpatialUnitDataset) return; + + this.loadingData = true; + this.hideSuccessAlert(); + this.hideErrorAlert(); + + const url = `${this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI}/spatial-units/${this.currentSpatialUnitDataset.spatialUnitId}/allFeatures`; + + this.http.delete(url).subscribe({ + next: (response: any) => { + this.spatialUnitFeaturesGeoJSON = null; + this.remainingFeatureHeaders = []; + this.broadcastService.broadcast('refreshSpatialUnitOverviewTable', ['edit', this.currentSpatialUnitDataset.spatialUnitId]); + + // Clear the grid data + this.spatialUnitFeaturesGeoJSON = null; + this.remainingFeatureHeaders = []; + this.buildFeatureTable(); + + if (this.gridApi) { + this.gridApi.setRowData([]); + } + + this.successMessagePart = this.currentSpatialUnitDataset.spatialUnitLevel; + this.showSuccessAlert(); + + setTimeout(() => { + this.loadingData = false; + }, 500); // Increased timeout to show loading state longer + }, + error: (error) => { + this.handleError(error); + setTimeout(() => { + this.loadingData = false; + }, 500); // Increased timeout to show loading state longer + } + }); + } + + checkPeriodOfValidity(): void { + // Use service method for validation + const validation = this.kommonitorDataExchangeService.validatePeriodOfValidity( + this.periodOfValidity.startDate, + this.periodOfValidity.endDate + ); + + this.periodOfValidityInvalid = !validation.isValid; + + if (!validation.isValid && validation.error) { + console.warn('Period of validity validation error:', validation.error); + } + } + + // Date input helpers to support keyboard entry similar to Add modal + private getTodayDateString(): string { + const now = new Date(); + const y = now.getFullYear(); + const m = String(now.getMonth() + 1).padStart(2, '0'); + const d = String(now.getDate()).padStart(2, '0'); + return `${y}-${m}-${d}`; + } + + private isValidDateString(value: string): boolean { + if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { + return false; + } + const [yStr, mStr, dStr] = value.split('-'); + const y = Number(yStr); + const m = Number(mStr); + const d = Number(dStr); + if (m < 1 || m > 12 || d < 1 || d > 31) { + return false; + } + const dt = new Date(y, m - 1, d); + return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d; + } + + private toIsoDateString(value: any): string | null { + if (!value) { + return null; + } + if (typeof value === 'string') { + return value; + } + const maybeStruct = value as { year?: number; month?: number; day?: number }; + if ( + maybeStruct && + typeof maybeStruct.year === 'number' && + typeof maybeStruct.month === 'number' && + typeof maybeStruct.day === 'number' + ) { + const y = maybeStruct.year; + const m = String(maybeStruct.month).padStart(2, '0'); + const d = String(maybeStruct.day).padStart(2, '0'); + return `${y}-${m}-${d}`; + } + return null; + } + + private ensureValidDateOrToday(value: any): string { + if (!value) { + return this.getTodayDateString(); + } + if (typeof value === 'string') { + return this.isValidDateString(value) ? value : this.getTodayDateString(); + } + const asIso = this.toIsoDateString(value); + return asIso ?? this.getTodayDateString(); + } + + onPeriodStartBlur(): void { + this.periodOfValidity.startDate = this.ensureValidDateOrToday(this.periodOfValidity.startDate); + this.checkPeriodOfValidity(); + } + + onPeriodEndBlur(): void { + if (this.periodOfValidity.endDate) { + this.periodOfValidity.endDate = this.ensureValidDateOrToday(this.periodOfValidity.endDate); + } + this.checkPeriodOfValidity(); + } + + onAddOrUpdateAttributeMapping(): void { + const tmpAttributeMapping = { + sourceName: this.attributeMapping_sourceAttributeName, + destinationName: this.attributeMapping_destinationAttributeName, + dataType: this.attributeMapping_attributeType + }; + + let processed = false; + for (let index = 0; index < this.attributeMappings_adminView.length; index++) { + const attributeMappingEntry = this.attributeMappings_adminView[index]; + if (attributeMappingEntry.sourceName === tmpAttributeMapping.sourceName) { + this.attributeMappings_adminView[index] = tmpAttributeMapping; + processed = true; + break; + } + } + + if (!processed) { + this.attributeMappings_adminView.push(tmpAttributeMapping); + } + + this.attributeMapping_sourceAttributeName = ''; + this.attributeMapping_destinationAttributeName = ''; + this.attributeMapping_attributeType = this.kommonitorImporterHelperService?.attributeMapping_attributeTypes?.[0]; + } + + onClickEditAttributeMapping(attributeMappingEntry: any): void { + this.attributeMapping_sourceAttributeName = attributeMappingEntry.sourceName; + this.attributeMapping_destinationAttributeName = attributeMappingEntry.destinationName; + this.attributeMapping_attributeType = attributeMappingEntry.dataType; + } + + onClickDeleteAttributeMapping(attributeMappingEntry: any): void { + for (let index = 0; index < this.attributeMappings_adminView.length; index++) { + if (this.attributeMappings_adminView[index].sourceName === attributeMappingEntry.sourceName) { + this.attributeMappings_adminView.splice(index, 1); + break; + } + } + } + + async buildImporterObjects(): Promise { + this.converterDefinition = this.buildConverterDefinition(); + this.datasourceTypeDefinition = await this.buildDatasourceTypeDefinition(); + this.propertyMappingDefinition = this.buildPropertyMappingDefinition(); + this.putBody_spatialUnits = this.buildPutBody_spatialUnits(); + + return !!(this.converterDefinition && this.datasourceTypeDefinition && this.propertyMappingDefinition && this.putBody_spatialUnits); + } + + buildConverterDefinition(): any { + return this.kommonitorImporterHelperService?.buildConverterDefinition( + this.converter, + "converterParameter_spatialUnitEditFeatures_", + this.schema, + this.mimeType, + this.converterParameters + ); + } + + async buildDatasourceTypeDefinition(): Promise { + try { + // Prefer robust Angular-native handling for FILE uploads (like Add modal) + if (this.datasourceType?.type === 'FILE') { + const inputEl = this.spatialUnitDataSourceInput?.nativeElement as HTMLInputElement | undefined; + const file = inputEl?.files?.[0]; + if (!file) { + console.warn('[EditFeatures] buildDatasourceTypeDefinition - no file selected'); + return null; + } + const uploadedName = await this.kommonitorImporterHelperService.uploadNewFile(file, file.name); + return { + type: 'FILE', + parameters: [ + { name: 'NAME', value: uploadedName } + ] + }; + } + + const formValues: { [key: string]: string } = { ...this.datasourceTypeParameters } as any; + if (this.datasourceType && this.datasourceType.type === 'OGCAPI_FEATURES') { + if (this.bboxType) { + formValues['bboxType'] = this.bboxType; + if (this.bboxType === 'ref' && this.bboxRefSpatialUnitLevel) { + formValues['bboxRef'] = this.bboxRefSpatialUnitLevel; + } else if (this.bboxType === 'literal') { + formValues['bbox_minx'] = this.bbox_minx as any; + formValues['bbox_miny'] = this.bbox_miny as any; + formValues['bbox_maxx'] = this.bbox_maxx as any; + formValues['bbox_maxy'] = this.bbox_maxy as any; + } + } + } + + return await this.kommonitorImporterHelperService?.buildDatasourceTypeDefinition( + this.datasourceType, + 'datasourceTypeParameter_spatialUnitEditFeatures_', + 'spatialUnitDataSourceInput_editFeatures', + Object.keys(formValues).length ? formValues : undefined + ); + } catch (error) { + this.handleError(error); + return null; + } + } + + buildPropertyMappingDefinition(): any { + return this.kommonitorImporterHelperService?.buildPropertyMapping_spatialResource( + this.spatialUnitDataSourceNameProperty, + this.spatialUnitDataSourceIdProperty, + this.validityStartDate_perFeature, + this.validityEndDate_perFeature, + '', // empty string instead of undefined + this.keepAttributes, + this.keepMissingValues, + this.attributeMappings_adminView + ); + } + + buildPutBody_spatialUnits(): any { + return { + geoJsonString: "", // will be set by importer + periodOfValidity: { + endDate: this.periodOfValidity.endDate, + startDate: this.periodOfValidity.startDate + }, + isPartialUpdate: this.isPartialUpdate + }; + } + + async editSpatialUnitFeatures(): Promise { + console.log('[EditFeatures] editSpatialUnitFeatures - start', { + currentSpatialUnitId: this.currentSpatialUnitDataset?.spatialUnitId, + converter: this.converter?.name, + schema: this.schema, + mimeType: this.mimeType, + datasourceType: this.datasourceType?.type, + idProperty: this.spatialUnitDataSourceIdProperty, + nameProperty: this.spatialUnitDataSourceNameProperty, + periodStart: this.periodOfValidity?.startDate, + periodEnd: this.periodOfValidity?.endDate, + periodInvalid: this.periodOfValidityInvalid + }); + this.loadingData = true; + this.importerErrors = []; + this.successMessagePart = ''; + this.errorMessagePart = ''; + + // Pre-validate like legacy component (show precise issues) + const missing: string[] = []; + if (!this.converter) { + missing.push('Konverter'); + } else { + if (Array.isArray(this.converter.schemas) && this.converter.schemas.length > 0 && !this.schema) { + missing.push('Schema'); + } + if (Array.isArray(this.converter.mimeTypes) && this.converter.mimeTypes.length > 0 && !this.mimeType) { + missing.push('Quellformat'); + } + if (Array.isArray(this.converter.parameters) && this.converter.parameters.length > 0) { + for (const p of this.converter.parameters) { + if (p.mandatory && (!this.converterParameters || !this.converterParameters[p.name])) { + missing.push(`Konverter-Parameter '${p.name}'`); + } + } + } + } + + if (!this.datasourceType) { + missing.push('Datenquelltyp'); + } else if (this.datasourceType.type === 'FILE') { + let hasFile = this.fileSelected; + const fileInputEl = this.spatialUnitDataSourceInput?.nativeElement as HTMLInputElement | undefined; + if (!hasFile && fileInputEl && fileInputEl.files && fileInputEl.files.length > 0) { + hasFile = true; + } + if (!hasFile) { + const fallbackEl = document.getElementById('spatialUnitDataSourceInput_editFeatures') as HTMLInputElement | null; + if (fallbackEl && fallbackEl.files && fallbackEl.files.length > 0) { + hasFile = true; + } + } + console.log('[EditFeatures] FILE datasource - fileSelected status', { + fileSelectedFlag: this.fileSelected, + viewChildHasFile: !!(fileInputEl && fileInputEl.files && fileInputEl.files.length > 0), + domIdHasFile: !!((document.getElementById('spatialUnitDataSourceInput_editFeatures') as HTMLInputElement | null)?.files?.length) + }); + if (!hasFile) { + missing.push('Datei'); + } + } else if (this.datasourceType.type === 'OGCAPI_FEATURES') { + if (!this.bboxType) { + missing.push('Räumlicher Filter'); + } else if (this.bboxType === 'ref' && !this.bboxRefSpatialUnitLevel) { + missing.push('Referenzraumebene für Begrenzungsrahmen'); + } else if (this.bboxType === 'literal') { + if (this.bbox_minx === null || this.bbox_miny === null || this.bbox_maxx === null || this.bbox_maxy === null) { + missing.push('Begrenzungsrahmen (minx, miny, maxx, maxy)'); + } + } + console.log('[EditFeatures] OGCAPI_FEATURES params', { + bboxType: this.bboxType, + bboxRefSpatialUnitLevel: this.bboxRefSpatialUnitLevel, + bbox: [this.bbox_minx, this.bbox_miny, this.bbox_maxx, this.bbox_maxy] + }); + // Other datasourceType parameters + if (Array.isArray(this.datasourceType.parameters) && this.datasourceType.parameters.length > 0) { + for (const p of this.datasourceType.parameters) { + if (p.name === 'bbox') { continue; } + const v = this.datasourceTypeParameters ? this.datasourceTypeParameters[p.name] : undefined; + if (p.mandatory && (v === undefined || v === null || v === '')) { + missing.push(`Datenquelle-Parameter '${p.name}'`); + } + } + } + } else { + // Generic datasourceType params + if (Array.isArray(this.datasourceType.parameters) && this.datasourceType.parameters.length > 0) { + for (const p of this.datasourceType.parameters) { + const v = this.datasourceTypeParameters ? this.datasourceTypeParameters[p.name] : undefined; + if (p.mandatory && (v === undefined || v === null || v === '')) { + missing.push(`Datenquelle-Parameter '${p.name}'`); + } + } + } + } + + if (!this.spatialUnitDataSourceIdProperty) { + missing.push('ID Attributname'); + } + if (!this.spatialUnitDataSourceNameProperty) { + missing.push('NAME Attributname'); + } + if (!this.periodOfValidity.startDate) { + missing.push("Gültig seit (Periodenbeginn)"); + } + if (this.periodOfValidityInvalid) { + missing.push('Gültigkeitszeitraum ist ungültig'); + } + + if (missing.length > 0) { + console.warn('[EditFeatures] Validation failed - missing fields', missing); + this.loadingData = false; + this.errorMessage = `Bitte füllen Sie alle Pflichtfelder in Schritt 2 aus. Fehlend: ${missing.join(', ')}.`; + this.showErrorAlert(); + return; + } + + const allDataSpecified = await this.buildImporterObjects(); + if (!allDataSpecified) { + console.warn('[EditFeatures] buildImporterObjects returned false', { + converterDefinition: !!this.converterDefinition, + datasourceTypeDefinition: !!this.datasourceTypeDefinition, + propertyMappingDefinition: !!this.propertyMappingDefinition, + putBody_spatialUnits: !!this.putBody_spatialUnits + }); + this.loadingData = false; + this.errorMessage = 'Bitte füllen Sie alle Pflichtfelder in Schritt 2 aus.'; + this.showErrorAlert(); + return; + } + + try { + console.log('[EditFeatures] Dry-run updateSpatialUnit POST about to fire', { + spatialUnitId: this.currentSpatialUnitDataset.spatialUnitId, + converterDefinition: this.converterDefinition?.name, + datasourceTypeDefinition: this.datasourceTypeDefinition?.type, + hasPropertyMapping: !!this.propertyMappingDefinition, + putBody: this.putBody_spatialUnits + }); + const updateSpatialUnitResponse_dryRun = await this.kommonitorImporterHelperService?.updateSpatialUnit( + this.converterDefinition, + this.datasourceTypeDefinition, + this.propertyMappingDefinition, + this.currentSpatialUnitDataset.spatialUnitId, + this.putBody_spatialUnits, + true + ); + + if (!this.kommonitorImporterHelperService?.importerResponseContainsErrors(updateSpatialUnitResponse_dryRun)) { + console.log('[EditFeatures] Dry-run successful, firing real POST'); + const updateSpatialUnitResponse = await this.kommonitorImporterHelperService?.updateSpatialUnit( + this.converterDefinition, + this.datasourceTypeDefinition, + this.propertyMappingDefinition, + this.currentSpatialUnitDataset.spatialUnitId, + this.putBody_spatialUnits, + false + ); + + this.successMessagePart = this.currentSpatialUnitDataset.spatialUnitLevel; + this.broadcastService.broadcast('refreshSpatialUnitOverviewTable', ['edit', this.currentSpatialUnitDataset.spatialUnitId]); + this.showSuccessAlert(); + this.loadingData = false; + } else { + console.warn('[EditFeatures] Dry-run returned errors', updateSpatialUnitResponse_dryRun); + this.errorMessagePart = "Einige der zu importierenden Features des Datensatzes weisen kritische Fehler auf"; + this.importerErrors = this.kommonitorImporterHelperService?.getErrorsFromImporterResponse(updateSpatialUnitResponse_dryRun) || []; + this.showErrorAlert(); + this.loadingData = false; + } + } catch (error) { + console.error('[EditFeatures] Exception during POST', error); + this.handleError(error); + this.loadingData = false; + } + } + + onFileSelected(event: any): void { + const input = event?.target as HTMLInputElement; + if (input && input.files && input.files.length > 0) { + console.log('[EditFeatures] onFileSelected', { name: input.files[0].name, size: input.files[0].size }); + } else { + console.log('[EditFeatures] onFileSelected - no file'); + } + this.fileSelected = !!(input && input.files && input.files.length > 0); + } + + // Import/Export functionality + onImportSpatialUnitEditFeaturesMappingConfig(): void { + this.spatialUnitMappingConfigImportError = ''; + if (this.mappingConfigImportFile) { + this.mappingConfigImportFile.nativeElement.click(); + } + } + + onMappingConfigFileSelected(event: any): void { + const file = event.target.files[0]; + if (file) { + this.parseMappingConfigFromFile(file); + } + } + + parseMappingConfigFromFile(file: File): void { + const fileReader = new FileReader(); + fileReader.onload = (event: any) => { + try { + this.parseFromMappingConfigFile(event); + } catch (error) { + console.error('Uploaded MappingConfig File cannot be parsed.', error); + this.spatialUnitMappingConfigImportError = 'Uploaded MappingConfig File cannot be parsed correctly'; + this.showMappingConfigErrorAlert(); + } + }; + fileReader.readAsText(file); + } + + parseFromMappingConfigFile(event: any): void { + this.mappingConfigImportSettings = JSON.parse(event.target.result); + + // Use service method to validate import structure + const validation = this.kommonitorDataExchangeService.validateMappingConfigImport(this.mappingConfigImportSettings); + if (!validation.isValid) { + this.spatialUnitMappingConfigImportError = validation.error || 'Struktur der Datei stimmt nicht mit erwartetem Muster überein.'; + this.showMappingConfigErrorAlert(); + return; + } + + // Set converter (use cached list to keep object identity stable) + const converters = this.availableConverters; + this.converter = converters?.find( + (converter: any) => converter.name === this.mappingConfigImportSettings.converter.name + ); + + // Set schema and mimeType + if (this.converter?.schemas && this.mappingConfigImportSettings.converter.schema) { + this.schema = this.converter.schemas.find( + (schema: string) => schema === this.mappingConfigImportSettings.converter.schema + ) || ''; + } + + if (this.converter?.mimeTypes && this.mappingConfigImportSettings.converter.mimeType) { + this.mimeType = this.converter.mimeTypes.find( + (mimeType: string) => mimeType === this.mappingConfigImportSettings.converter.mimeType + ) || ''; + } + + // Set datasource type + const datasourceTypes = this.kommonitorImporterHelperService?.getAvailableDatasourceTypes(); + this.datasourceType = datasourceTypes?.find( + (datasourceType: any) => datasourceType.type === this.mappingConfigImportSettings.dataSource.type + ); + + // Set property mapping + this.spatialUnitDataSourceNameProperty = this.mappingConfigImportSettings.propertyMapping.nameProperty; + this.spatialUnitDataSourceIdProperty = this.mappingConfigImportSettings.propertyMapping.identifierProperty; + this.validityStartDate_perFeature = this.mappingConfigImportSettings.propertyMapping.validStartDateProperty; + this.validityEndDate_perFeature = this.mappingConfigImportSettings.propertyMapping.validEndDateProperty; + this.keepAttributes = this.mappingConfigImportSettings.propertyMapping.keepAttributes; + this.keepMissingValues = this.mappingConfigImportSettings.propertyMapping.keepMissingOrNullValueAttributes; + + // Set attribute mappings + this.attributeMappings_adminView = this.mappingConfigImportSettings.propertyMapping.attributes?.map((attr: any) => ({ + sourceName: attr.name, + destinationName: attr.mappingName, + dataType: this.kommonitorImporterHelperService?.attributeMapping_attributeTypes?.find( + (dataType: any) => dataType.apiName === attr.type + ) + })) || []; + + // Set period of validity + if (this.mappingConfigImportSettings.periodOfValidity) { + this.periodOfValidity = { + startDate: this.mappingConfigImportSettings.periodOfValidity.startDate, + endDate: this.mappingConfigImportSettings.periodOfValidity.endDate + }; + this.checkPeriodOfValidity(); + } + + // Set converter parameters (e.g., CRS) + this.converterParameters = {}; + if (this.mappingConfigImportSettings.converter?.parameters?.length) { + for (const param of this.mappingConfigImportSettings.converter.parameters) { + if (param?.name) { + this.converterParameters[param.name] = param.value; + } + } + } + + // Set datasource parameters for OGC API Features (bbox) + if (this.datasourceType?.type === 'OGCAPI_FEATURES' && Array.isArray(this.mappingConfigImportSettings.dataSource?.parameters)) { + const bboxParam = this.mappingConfigImportSettings.dataSource.parameters.find((p: any) => p?.name === 'bbox'); + if (bboxParam && typeof bboxParam.value === 'string') { + const value = bboxParam.value; + const parts = value.split(',').map((v: string) => v.trim()); + if (parts.length === 4 && parts.every((p: string) => p !== '')) { + // literal bbox + this.bboxType = 'literal'; + this.bbox_minx = parts[0]; + this.bbox_miny = parts[1]; + this.bbox_maxx = parts[2]; + this.bbox_maxy = parts[3]; + } else { + // ref bbox (value is spatial unit level) + this.bboxType = 'ref'; + this.bboxRefSpatialUnitLevel = value; + } + } + } + + // Populate datasource type generic parameters + this.datasourceTypeParameters = {}; + const params = this.mappingConfigImportSettings?.dataSource?.parameters || []; + for (const p of params) { + if (p?.name && p.name !== 'bbox' && p.name !== 'bboxType') { + this.datasourceTypeParameters[p.name] = p.value; + } + } + } + + async onExportSpatialUnitEditFeaturesMappingConfig(): Promise { + const converterDefinition = this.buildConverterDefinition(); + const datasourceTypeDefinition = await this.buildDatasourceTypeDefinition(); + const propertyMappingDefinition = this.buildPropertyMappingDefinition(); + + // Use service method to build export structure + const mappingConfigExport = this.kommonitorDataExchangeService.buildMappingConfigExport( + converterDefinition, + datasourceTypeDefinition, + propertyMappingDefinition, + this.periodOfValidity + ); + + const fileName = `KomMonitor-Import-Mapping-Konfiguration_Export-${this.currentSpatialUnitDataset?.spatialUnitLevel || 'SpatialUnit'}.json`; + const metadataJSON = JSON.stringify(mappingConfigExport); + const blob = new Blob([metadataJSON], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.download = fileName; + a.href = url; + a.click(); + a.remove(); + URL.revokeObjectURL(url); + } + + onChangeEnableDeleteFeatures(): void { + // Rebuild the grid with updated delete settings + this.buildFeatureTable(); + + // Update grid column definitions and data if API is available + if (this.gridApi && this.columnDefs?.length) { + // Update column definitions + this.gridApi.setColumnDefs(this.columnDefs); + + // Update data if we have features + if (this.spatialUnitFeaturesGeoJSON?.features) { + // Use service method to transform data for grid display + const transformedData = this.kommonitorDataExchangeService.transformFeaturesForGrid( + this.spatialUnitFeaturesGeoJSON.features + ); + this.rowData = transformedData; + this.gridApi.setRowData(this.rowData); + } + + // Force refresh of the grid to show/hide delete buttons + this.gridApi.refreshCells(); + + // Register click handlers after grid update + setTimeout(() => { + this.kommonitorDataGridHelperService.registerFeatureTableClickHandlers( + this.currentSpatialUnitDataset?.spatialUnitId, + this.kommonitorDataGridHelperService.resourceType_spatialUnit, + this.enableDeleteFeatures + ); + }, 100); + } + } + + // Filtering is now handled by the service method extractRemainingHeaders + + getFeatureId(geojsonFeature: any): string { + return geojsonFeature.properties?.['ID'] || ''; + } + + getFeatureName(geojsonFeature: any): string { + return geojsonFeature.properties?.['NAME'] || ''; + } + + // Navigation methods + nextStep(): void { + if (this.currentStep < this.totalSteps) { + this.currentStep++; + } + } + + previousStep(): void { + if (this.currentStep > 1) { + this.currentStep--; + } + } + + goToStep(step: number): void { + if (step >= 1 && step <= this.totalSteps) { + this.currentStep = step; + } + } + + // AG Grid event handlers + onGridReady(event: GridReadyEvent): void { + this.gridApi = event.api; + this.columnApi = event.columnApi; + + // Force refresh grid configuration after a short delay + setTimeout(() => { + this.forceRefreshGridConfiguration(); + }, 100); + + // If we have data already, update the grid + if (this.spatialUnitFeaturesGeoJSON?.features && this.remainingFeatureHeaders.length > 0) { + this.updateGridWithData(); + } + } + + private forceRefreshGridConfiguration(): void { + if (!this.gridApi) return; + + // Force refresh of grid configuration + this.gridApi.refreshHeader(); + this.gridApi.refreshCells(); + + // Ensure pagination is visible + if (this.featureTableGridOptions.pagination) { + this.gridApi.paginationGoToPage(0); + } + } + + private headerHeightSetter(): void { + if (this.gridApi) { + const headerHeight = this.headerHeightGetter(); + this.gridApi.setHeaderHeight(headerHeight); + } + } + + private headerHeightGetter(): number { + const headerElement = document.querySelector('.ag-header'); + if (headerElement) { + const headerTextElements = headerElement.querySelectorAll('.ag-header-cell-text'); + let maxHeight = 0; + headerTextElements.forEach(element => { + const height = element.scrollHeight; + if (height > maxHeight) { + maxHeight = height; + } + }); + return Math.max(maxHeight + 20, 50); // Add padding and minimum height + } + return 50; + } + + private registerFeatureTableClickHandlers(): void { + if (!this.enableDeleteFeatures) return; + + setTimeout(() => { + this.kommonitorDataGridHelperService.registerFeatureTableClickHandlers( + this.currentSpatialUnitDataset?.spatialUnitId, + this.kommonitorDataGridHelperService.resourceType_spatialUnit, + this.enableDeleteFeatures + ); + }, 100); + } + + private updateGridWithData(): void { + if (!this.gridApi) { + return; + } + + // Update column definitions + if (this.columnDefs?.length) { + this.gridApi.setColumnDefs(this.columnDefs); + } + + // Transform and set data + const transformedData = this.kommonitorDataExchangeService.transformFeaturesForGrid( + this.spatialUnitFeaturesGeoJSON?.features || [] + ); + + this.rowData = transformedData; + this.gridApi.setRowData(this.rowData); + this.gridApi.refreshCells(); + this.gridApi.redrawRows(); + + // Force refresh of pagination and filtering + this.gridApi.paginationGoToPage(0); + this.gridApi.refreshHeader(); + } + + onFirstDataRendered(event: FirstDataRenderedEvent): void { + // Handle first data rendered event + } + + onColumnResized(event: ColumnResizedEvent): void { + // Handle column resize event + } + + onCellValueChanged(event: any): void { + // Handle cell value changes - this will be called by the grid + + // The actual API call and visual feedback is handled in the data grid helper service + // This method can be used for additional component-specific logic if needed + } + + // Alert methods + showSuccessAlert(): void { + this.successMessage = 'Operation completed successfully'; + setTimeout(() => this.hideSuccessAlert(), 5000); + } + + hideSuccessAlert(): void { + this.successMessage = ''; + } + + showErrorAlert(): void { + setTimeout(() => this.hideErrorAlert(), 10000); + } + + hideErrorAlert(): void { + this.errorMessage = ''; + this.errorMessagePart = ''; + } + + showMappingConfigErrorAlert(): void { + setTimeout(() => this.hideMappingConfigErrorAlert(), 10000); + } + + hideMappingConfigErrorAlert(): void { + this.spatialUnitMappingConfigImportError = ''; + } + + private handleError(error: any): void { + console.error('Error occurred:', error); + if (error.data) { + this.errorMessagePart = this.kommonitorDataExchangeService?.syntaxHighlightJSON(error.data) || 'An error occurred'; + } else { + this.errorMessagePart = this.kommonitorDataExchangeService?.syntaxHighlightJSON(error) || 'An error occurred'; + } + this.showErrorAlert(); + } + + // Modal control methods + closeModal(): void { + this.activeModal.dismiss(); + } + + saveAndClose(): void { + this.activeModal.close(); + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditMetadataModal/spatial-unit-edit-metadata-modal.component.css b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditMetadataModal/spatial-unit-edit-metadata-modal.component.css new file mode 100644 index 000000000..a48f0c04f --- /dev/null +++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditMetadataModal/spatial-unit-edit-metadata-modal.component.css @@ -0,0 +1,1089 @@ +/* Multi-step form styles */ +.multiStepForm { + margin: 0; + padding: 0; +} + +/* Progress Bar Styles - Matching Add Modal */ +#progressbar { + margin-bottom: 10px; + overflow: hidden; + /*CSS counters to number the steps*/ + counter-reset: step; +} + +#progressbar li { + list-style-type: none; + color: black; + text-transform: uppercase; + font-size: 9px; + float: left; + position: relative; + letter-spacing: 1px; + cursor: pointer; +} + +#progressbar li:before { + content: counter(step); + counter-increment: step; + width: 24px; + height: 24px; + line-height: 26px; + display: block; + font-size: 12px; + color: #333; + background: #cccc; + border-radius: 25px; + margin: 0 auto 10px auto; + transform: translateZ(-1px); +} + +/*progressbar connectors*/ +#progressbar li:after { + content: ''; + width: 100%; + height: 2px; + background: #cccc; + position: absolute; + left: -50%; + top: 9px; + /*put it behind the numbers */ + z-index: -1; +} + +#progressbar li:first-child:after { + /*connector not needed before the first step*/ + content: none; +} + +/*marking active/completed steps green*/ +/*The number of the step and the connector before it = green*/ +#progressbar li.active:before, #progressbar li.active:after { + background: var(--kommonitor-primary); + color: white; +} + +#progressbar li.clickable { + cursor: pointer; + transition: all 0.3s ease; +} + +#progressbar li.clickable:hover { + color: var(--kommonitor-primary); +} + +#progressbar li.clickable:hover:before { + background: var(--kommonitor-primary); + transform: scale(1.1); +} + +/* Form step styles */ +.fs-title { + font-size: 24px; + text-transform: uppercase; + color: #2C3E50; + margin-bottom: 10px; + text-align: center; +} + +.fs-subtitle { + font-weight: normal; + font-size: 13px; + color: #666; + margin-bottom: 20px; + text-align: center; +} + +/* Action buttons - Centered */ +.action-button { + width: 150px; + background: var(--kommonitor-primary); + font-weight: bold; + color: white; + border: 0 none; + border-radius: 1px; + cursor: pointer; + padding: 10px 5px; + margin: 10px 5px; + display: inline-block; +} + +.action-button:hover, .action-button:focus { + box-shadow: 0 0 0 2px white, 0 0 0 3px var(--kommonitor-primary); + background: var(--kommonitor-primary); + color: white; +} + +.action-button-previous { + width: 150px; + background: rgb(236, 138, 138); + font-weight: bold; + color: white; + border: 0 none; + border-radius: 1px; + cursor: pointer; + padding: 10px 5px; + margin: 10px 5px; + display: inline-block; +} + +.action-button-previous:hover, .action-button-previous:focus { + box-shadow: 0 0 0 2px white, 0 0 0 3px #C5C5F1; + background: rgb(236, 138, 138); + color: white; +} + +/* Center the button container */ +.button-container { + text-align: center; + margin-top: 20px; + clear: both; +} + +/* Form field styles */ +.form-group { + margin-bottom: 15px; +} + +.form-control { + width: 100%; + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; +} + +.form-control:focus { + border-color: #27AE60; + box-shadow: 0 0 0 2px rgba(39, 174, 96, 0.2); + outline: none; +} + +/* Switch styles */ +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.switchslider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: .4s; +} + +.switchslider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + transition: .4s; +} + +input:checked + .switchslider { + background-color: #27AE60; +} + +input:focus + .switchslider { + box-shadow: 0 0 1px #27AE60; +} + +input:checked + .switchslider:before { + transform: translateX(26px); +} + +.switchslider.round { + border-radius: 34px; +} + +.switchslider.round:before { + border-radius: 50%; +} + +/* Loading overlay */ +.loading-overlay-admin-panel { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; +} + +.loading-overlay-admin-panel .glyphicon { + font-size: 2rem; +} + +.ng-hide { + display: none !important; +} + +/* Glyphicon base class */ +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.glyphicon-refresh:before { + content: "\e031"; +} + +.icon-spin { + animation: spin 2s infinite linear; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +/* Alert styles */ +.alert { + padding: 0.75rem 3.25rem; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} + +.alert-success { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.alert-danger { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} + +.alert-dismissable .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} + +/* Dropdown styles */ +.dropdown-menu { + min-width: 200px; + z-index: 1050; + position: absolute; + top: 100%; + left: 0; + float: none; + background-color: #fff; + border: 1px solid rgba(0,0,0,.15); + border-radius: 4px; + box-shadow: 0 6px 12px rgba(0,0,0,.175); +} + +.dropdown-menu-center { + left: 50%; + transform: translateX(-50%); +} + +/* Ensure dropdown is visible when open */ +.dropdown.open .dropdown-menu { + display: block !important; +} + +/* SVG content styling for dropdown items */ +#outlineDashArrayDropdownItem-editMetadata-0, +#outlineDashArrayDropdownItem-editMetadata-1, +#outlineDashArrayDropdownItem-editMetadata-2, +#outlineDashArrayDropdownItem-editMetadata-3 { + padding: 8px 12px; + min-height: 60px; + display: block; + width: 100%; +} + +#outlineDashArrayDropdownItem-editMetadata-0 svg, +#outlineDashArrayDropdownItem-editMetadata-1 svg, +#outlineDashArrayDropdownItem-editMetadata-2 svg, +#outlineDashArrayDropdownItem-editMetadata-3 svg { + display: block !important; + width: 100% !important; + height: auto !important; + max-width: 100% !important; + border: 1px solid #ddd !important; + background: #f9f9f9 !important; +} + +/* Ensure dropdown items are visible */ +.dropdown-menu li a { + display: block; + padding: 0; + clear: both; + font-weight: normal; + line-height: 1.42857143; + color: #333; + white-space: nowrap; +} + +.dropdown-menu li a:hover { + color: #262626; + text-decoration: none; + background-color: #f5f5f5; +} + +/* Custom dropdown item styling */ +.dropdown-item { + cursor: pointer; + transition: background-color 0.2s ease; +} + +.dropdown-item:hover { + background-color: #f5f5f5; +} + +.dropdown-item:active { + background-color: #e9ecef; +} + +/* Vertical alignment helper */ +.vertical-align { + display: flex; + align-items: flex-start; +} + +.vertical-align .form-group { + flex: 1; + margin-right: 15px; +} + +.vertical-align .form-group:last-child { + margin-right: 0; +} + +/* Help block styles */ +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #737373; + font-size: 12px; +} + +.help-block.with-errors { + color: #a94442; +} + +/* Input group styles */ +.input-group { + position: relative; + display: table; + border-collapse: separate; +} + +.input-group-addon { + padding: 6px 12px; + font-size: 14px; + font-weight: normal; + line-height: 1; + color: #555; + text-align: center; + background-color: #eee; + border: 1px solid #ccc; + border-radius: 4px; + width: 1%; + white-space: nowrap; + vertical-align: middle; + display: table-cell; +} + +.input-group .form-control { + position: relative; + z-index: 2; + float: left; + width: 100%; + margin-bottom: 0; + display: table-cell; +} + +.input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; +} + +.input-group-addon:first-child { + border-right: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group .form-control:last-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +/* Modal specific styles */ +.modal-header .close { + margin-top: -2px; +} + +.modal-title { + margin: 0; + line-height: 1.42857143; +} + +.modal-footer { + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} + +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} + +.pull-left { + float: left !important; +} + +.pull-right { + float: right !important; +} + +/* Button styles */ +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + border: 1px solid transparent; + border-radius: 4px; + text-decoration: none; +} + +.btn:focus, +.btn:active:focus, +.btn.active:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn:hover, +.btn:focus { + color: #333; + text-decoration: none; +} + +.btn-default { + color: #333; + background-color: #fff; + border-color: #ccc; +} + +.btn-default:hover, +.btn-default:focus { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} + +.btn-info { + color: #fff; + background-color: #5bc0de; + border-color: #46b8da; +} + +.btn-info:hover, +.btn-info:focus { + color: #fff; + background-color: #31b0d5; + border-color: #269abc; +} + +.btn-success { + color: #fff; + background-color: #5cb85c; + border-color: #4cae4c; +} + +.btn-success:hover, +.btn-success:focus { + color: #fff; + background-color: #449d44; + border-color: #398439; +} + +.btn-danger { + color: #fff; + background-color: #d9534f; + border-color: #d43f3a; +} + +.btn-danger:hover, +.btn-danger:focus { + color: #fff; + background-color: #c9302c; + border-color: #ac2925; +} + +.btn:disabled, +.btn[disabled] { + cursor: not-allowed; + opacity: 0.65; + box-shadow: none; +} + +/* Pre styles for JSON display */ +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + color: #333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; +} + +/* Caret styles for dropdowns */ +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px dashed; + border-top: 4px solid \9; + border-right: 4px solid transparent; + border-left: 4px solid transparent; +} + +/* Additional Bootstrap-like styles */ +.btn-sm { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +.input-sm { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +/* Date input group */ +.input-group.date .input-group-addon { + cursor: pointer; +} + +/* Color input styling */ +input[type="color"] { + border: 1px solid #ccc; + border-radius: 4px; + padding: 2px; + cursor: pointer; +} + +/* Align center for form groups */ +.form-group[align="center"] { + text-align: center; +} + +.form-group[align="center"] label { + display: block; + margin-bottom: 5px; +} + +/* Color picker specific styles */ +.color-picker-container { + position: relative; + display: inline-block; +} + +.color-picker-button { + border: 2px solid #ddd; + border-radius: 4px; + cursor: pointer; + transition: border-color 0.3s ease; + overflow: hidden; +} + +.color-picker-button:hover { + border-color: #999; +} + +.color-picker-button:focus { + outline: 2px solid #27AE60; + outline-offset: 2px; +} + +.color-picker-overlay { + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + margin-top: 5px; + z-index: 99999999; + background: white; + border-radius: 8px; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); + padding: 10px; + animation: fadeIn 0.2s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateX(-50%) translateY(-10px); + } + to { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +} + +.color-display-text { + font-family: 'Courier New', monospace; + font-size: 11px; + font-weight: bold; + text-shadow: 1px 1px 1px rgba(0,0,0,0.7); + color: white; + user-select: none; +} + +.color-picker-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 99999998; /* Below the color picker overlay but above everything else */ + background: transparent; + cursor: default; +} + +/* ng-bootstrap Datepicker Styles */ +.datepicker-dropdown { + position: absolute !important; + z-index: 9999 !important; + background: white !important; + border: 1px solid #ccc !important; + border-radius: 4px !important; + box-shadow: 0 2px 10px rgba(0,0,0,0.1) !important; + margin-top: 2px !important; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important; + width: 320px !important; + left: 0 !important; + top: 100% !important; +} + +/* Position the datepicker relative to the input group */ +.input-group { + position: relative !important; +} + +.input-group .datepicker-dropdown { + position: absolute !important; + top: 100% !important; + left: 0 !important; + right: auto !important; + margin-top: 2px !important; + z-index: 9999 !important; +} + +/* Ensure datepicker renders above all other elements */ +.ngb-datepicker { + position: absolute !important; + z-index: 9999 !important; + background: white !important; + border: 1px solid #ccc !important; + border-radius: 4px !important; + box-shadow: 0 2px 10px rgba(0,0,0,0.1) !important; + margin-top: 2px !important; + width: 320px !important; + left: 0 !important; + top: 100% !important; +} + +/* Override any ng-bootstrap default positioning */ +.ngb-datepicker-picker { + position: absolute !important; + z-index: 9999 !important; + background: white !important; + border: 1px solid #ccc !important; + border-radius: 4px !important; + box-shadow: 0 2px 10px rgba(0,0,0,0.1) !important; + margin-top: 2px !important; + width: 320px !important; + left: 0 !important; + top: 100% !important; +} + +/* Ensure the datepicker container doesn't clip content */ +.date-input-group { + overflow: visible !important; + position: relative !important; +} + +/* Force datepicker to render outside button group */ +.input-group-btn { + position: relative !important; + overflow: visible !important; +} + +.input-group-btn .ngb-datepicker { + position: absolute !important; + z-index: 9999 !important; + left: 0 !important; + top: 100% !important; + margin-top: 2px !important; +} + +/* Date input group specific styling - matching original AngularJS version */ +.date-input-group { + border-radius: 4px !important; + overflow: visible !important; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) !important; + position: relative !important; +} + +/* Left button styling for calendar icon */ +.date-input-group .input-group-btn { + position: relative !important; +} + +.date-input-group .date-toggle-btn { + border-right: none !important; + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + background-color: #f8f9fa !important; + border-color: #ced4da !important; + color: #495057 !important; + padding: 8px 12px !important; + min-width: 40px !important; + transition: all 0.15s ease-in-out !important; + border-top-left-radius: 4px !important; + border-bottom-left-radius: 4px !important; +} + +.date-input-group .date-toggle-btn:hover { + background-color: #e9ecef !important; + border-color: #adb5bd !important; + color: #007bff !important; +} + +.date-input-group .date-toggle-btn:focus { + outline: none !important; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25) !important; +} + +.date-input-group .form-control { + border-left: none !important; + border-right: 1px solid #ced4da !important; + border-radius: 0 !important; + padding: 8px 42px !important; + font-size: 14px !important; + border-top-right-radius: 4px !important; + border-bottom-right-radius: 4px !important; + cursor: pointer !important; + transition: all 0.15s ease-in-out !important; +} + +.date-input-group .form-control:hover { + background-color: #f8f9fa !important; + border-color: #adb5bd !important; +} + +.date-input-group .form-control:focus { + border-color: #80bdff !important; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25) !important; + outline: none !important; + background-color: #fff !important; +} + +/* Enhanced visual feedback for clickable elements */ +.date-input-group .date-toggle-btn { + cursor: pointer !important; +} + +.date-input-group .date-toggle-btn:active { + background-color: #dee2e6 !important; + transform: translateY(1px) !important; +} + +.date-input-group .form-control:active { + background-color: #f8f9fa !important; +} + +/* Ensure proper spacing and alignment */ +.date-input-group .input-group-btn { + margin-right: 0 !important; +} + +.date-input-group .form-control { + margin-left: 0 !important; +} + +/* Align calendar button and input flush; remove unintended gaps */ +.date-input-group { + display: flex !important; + align-items: stretch !important; +} + +.date-input-group .input-group-btn { + flex: 0 0 auto !important; + margin: 0 !important; +} + +.date-input-group .date-toggle-btn { + height: 100% !important; + border-right: 0 !important; + z-index: 10; +} + +.date-input-group > div { + flex: 1 1 auto !important; + margin: 0 !important; + padding: 0 !important; +} + +.date-input-group > div > .form-control, +.date-input-group > .form-control { + width: 100% !important; + height: 100% !important; + border-left: 0 !important; +} + +/* Hover effect for the entire input group */ +.date-input-group:hover { + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15) !important; +} + +/* Enhanced datepicker styling with larger fonts - matching original design */ +.datepicker-dropdown .ngb-dp-header { + background-color: #f8f9fa !important; + border-bottom: 1px solid #dee2e6 !important; + padding: 18px 15px !important; + border-radius: 4px 4px 0 0 !important; +} + +.datepicker-dropdown .ngb-dp-month { + background: white !important; + padding: 15px !important; +} + +.datepicker-dropdown .ngb-dp-weekday { + color: #6c757d !important; + font-weight: 600 !important; + font-size: 18px !important; + padding: 12px 8px !important; + text-align: center !important; + text-transform: uppercase !important; + letter-spacing: 0.5px !important; +} + +.datepicker-dropdown .ngb-dp-day { + padding: 10px !important; + text-align: center !important; + cursor: pointer !important; + border-radius: 4px !important; + transition: all 0.15s ease-in-out !important; + font-weight: 500 !important; + font-size: 18px !important; + min-width: 45px !important; + height: 45px !important; + line-height: 25px !important; +} + +.datepicker-dropdown .ngb-dp-day:hover { + background-color: #e9ecef !important; + transform: scale(1.05) !important; +} + +.datepicker-dropdown .ngb-dp-day.selected { + background-color: #007bff !important; + color: white !important; + font-weight: bold !important; + box-shadow: 0 2px 4px rgba(0, 123, 255, 0.3) !important; +} + +.datepicker-dropdown .ngb-dp-day.focused { + background-color: #007bff !important; + color: white !important; + font-weight: bold !important; +} + +.datepicker-dropdown .ngb-dp-day.today { + background-color: #fff3cd !important; + color: #856404 !important; + font-weight: bold !important; + border: 2px solid #ffc107 !important; +} + +.datepicker-dropdown .ngb-dp-day.disabled { + color: #6c757d !important; + cursor: not-allowed !important; + opacity: 0.4 !important; +} + +.datepicker-dropdown .ngb-dp-day.outside { + color: #6c757d !important; + opacity: 0.5 !important; +} + +.datepicker-dropdown .ngb-dp-navigation-chevron { + border-style: solid !important; + border-width: 0.35em 0.35em 0 0 !important; + content: "" !important; + display: inline-block !important; + height: 0.7em !important; + transform: rotate(-45deg) !important; + vertical-align: top !important; + width: 0.7em !important; + color: #495057 !important; +} + +.datepicker-dropdown .ngb-dp-navigation-chevron.right { + transform: rotate(45deg) !important; +} + +.datepicker-dropdown .ngb-dp-month-name { + font-size: 20px !important; + font-weight: 600 !important; + color: #495057 !important; + text-transform: capitalize !important; +} + +.datepicker-dropdown .ngb-dp-arrow { + background: transparent !important; + border: none !important; + padding: 12px 18px !important; + cursor: pointer !important; + border-radius: 4px !important; + transition: all 0.15s ease-in-out !important; + min-width: 50px !important; +} + +.datepicker-dropdown .ngb-dp-arrow:hover { + background-color: #e9ecef !important; + transform: scale(1.1) !important; +} + +.datepicker-dropdown .ngb-dp-arrow:focus { + outline: 2px solid #007bff !important; + outline-offset: 2px !important; +} + +/* Basic input group button styling */ +.input-group-btn .btn { + border-left: 0 !important; + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} + +.input-group-btn .btn:hover { + background-color: #e9ecef !important; + border-color: #adb5bd !important; +} + +/* Ensure date input is properly styled */ +.input-group input[readonly] { + background-color: #fff !important; + cursor: pointer !important; +} + +.input-group input[readonly]:hover { + background-color: #f8f9fa !important; +} + +/* Input group addon styling */ +.input-group-addon { + background-color: #f8f9fa !important; + border-color: #ced4da !important; + color: #495057 !important; + cursor: pointer !important; + transition: all 0.15s ease-in-out !important; +} + +.input-group-addon:hover { + background-color: #e9ecef !important; + border-color: #adb5bd !important; + color: #212529 !important; +} + +/* Calendar icon styling */ +.input-group-addon i { + font-size: 16px !important; + transition: color 0.15s ease-in-out !important; +} + +.input-group-addon:hover i { + color: #007bff !important; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .datepicker-dropdown { + width: 300px !important; + left: 50% !important; + transform: translateX(-50%) !important; + } + + .input-group .datepicker-dropdown { + left: 50% !important; + transform: translateX(-50%) !important; + } + + .datepicker-dropdown .ngb-dp-day { + font-size: 16px !important; + min-width: 40px !important; + height: 40px !important; + } + + .datepicker-dropdown .ngb-dp-weekday { + font-size: 16px !important; + } + + .datepicker-dropdown .ngb-dp-month-name { + font-size: 18px !important; + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditMetadataModal/spatial-unit-edit-metadata-modal.component.html b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditMetadataModal/spatial-unit-edit-metadata-modal.component.html new file mode 100644 index 000000000..681dd1fdd --- /dev/null +++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditMetadataModal/spatial-unit-edit-metadata-modal.component.html @@ -0,0 +1,389 @@ + + + + + + + +
+ +

Raumebene aktualisiert

+ Die Metadaten der Raumebene mit Namen {{successMessagePart}} wurden in KomMonitor aktualisiert und in die Übersichtstabelle eingetragen. +
+ +
+ + +
+ +

Aktualisierung gescheitert

+ Bei der Aktualisierung der Metadaten der Raumebene ist ein Fehler aufgetreten. Fehlermeldung: +
+

+
+ + +
+ +

Metadata Import gescheitert

+ Beim Import der Metadaten aus einer Datei ist ein Fehler aufgetreten. Fehlermeldung: +
+
{{spatialUnitMetadataImportError}}
+
+
+

Bitte stellen Sie sicher, dass folgendes JSON-Format eingehalten wird:

+
{{spatialUnitMetadataStructure | json}}
+
\ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditMetadataModal/spatial-unit-edit-metadata-modal.component.ts b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditMetadataModal/spatial-unit-edit-metadata-modal.component.ts new file mode 100644 index 000000000..d3b69a88d --- /dev/null +++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditMetadataModal/spatial-unit-edit-metadata-modal.component.ts @@ -0,0 +1,665 @@ +import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewInit, ChangeDetectorRef, HostListener } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; +import { HttpClient } from '@angular/common/http'; +import { Subscription } from 'rxjs'; +import { KommonitorDataExchangeService } from 'services/adminSpatialUnit/kommonitor-data-exchange.service'; +import { KommonitorDataGridHelperService } from 'services/adminSpatialUnit/kommonitor-data-grid-helper.service'; +import { ColorEvent } from 'ngx-color'; +import { KmColorPickerComponent } from '../../../customElements/color-picker/km-color-picker.component'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; + +// Remove jQuery declaration - no longer needed +// declare var $: any; + +@Component({ + selector: 'spatial-unit-edit-metadata-modal-new', + templateUrl: './spatial-unit-edit-metadata-modal.component.html', + styleUrls: ['./spatial-unit-edit-metadata-modal.component.css'], + providers: [] +}) +export class SpatialUnitEditMetadataModalComponent implements OnInit, OnDestroy, AfterViewInit { + @ViewChild('metadataImportFile', { static: false }) metadataImportFile!: ElementRef; + @ViewChild('outlineDashArrayDropdown', { static: false }) outlineDashArrayDropdown!: ElementRef; + + // Multi-step form + currentStep = 1; + totalSteps = 2; + + // Form data + isSubmitting = false; + errorMessage = ''; + successMessage = ''; + loadingData = false; + + // Current dataset being edited + currentSpatialUnitDataset: any = null; + + // Basic form data + spatialUnitLevel = ''; + spatialUnitLevelInvalid = false; + metadata: any = { + description: '', + databasis: '', + datasource: '', + contact: '', + updateInterval: null, + lastUpdate: '', + literature: '', + note: '', + sridEPSG: 4326 + }; + + // Date picker model for ng-bootstrap - using string format directly + // Remove the custom visibility control since ng-bootstrap handles it + // showDatepicker = false; + + // Date picker visibility control + // showDatepicker = false; + + // Hierarchy + nextLowerHierarchySpatialUnit: any = null; + nextUpperHierarchySpatialUnit: any = null; + hierarchyInvalid = false; + + // Outline layer settings + isOutlineLayer = false; + outlineColor = '#bf3d2c'; + outlineWidth = 2; + selectedOutlineDashArrayObject: any = null; + selectedoutlineDashArrayObject: any = null; // Keep both for compatibility with original + + // Color picker handled by km-color-picker component + + // Dropdown state for outline dash array (Angular-native toggle) + showOutlineDashArrayDropdown = false; + + // Available options + availableSpatialUnits: any[] = []; + updateIntervalOptions: any[] = []; + availableLoiDashArrayObjects: any[] = []; + + // Import/Export functionality + metadataImportSettings: any = null; + spatialUnitMetadataImportError = ''; + + // Success/Error data + successMessagePart = ''; + errorMessagePart = ''; + + // Subscriptions + private subscriptions: Subscription[] = []; + + // Add flag to track if SVGs have been injected + private svgInjected = false; + + constructor( + public activeModal: NgbActiveModal, + public kommonitorDataExchangeService: KommonitorDataExchangeService, + private kommonitorDataGridHelperService: KommonitorDataGridHelperService, + private http: HttpClient, + private broadcastService: BroadcastService, + private sanitizer: DomSanitizer + ) { + } + + ngOnInit() { + this.loadInitialData(); + this.setupEventListeners(); + + // Remove jQuery date picker initialization - no longer needed + + // If currentSpatialUnitDataset is already set (from parent component), initialize form + if (this.currentSpatialUnitDataset) { + this.resetForm(); + } + } + + ngAfterViewInit() { + // Remove Bootstrap dropdown initialization - no longer needed for date picker + // setTimeout(() => { + // try { + // $('.dropdown-toggle').dropdown(); + // } catch (error) { + // // Bootstrap dropdown initialization failed + // } + // }, 300); + } + + // Remove manual SVG injection - now handled by Angular templates + private injectSvgContentSimple() { + console.log('SVG injection no longer needed - using Angular templates'); + } + + // Remove the complex injection methods - not needed + private injectSvgContent() { + console.log('SVG injection no longer needed - using Angular templates'); + } + + private checkElementsExist(): boolean { + return true; + } + + private performSvgInjection() { + console.log('SVG injection no longer needed - using Angular templates'); + } + + // Color picker logic removed; handled by km-color-picker + + ngOnDestroy() { + this.subscriptions.forEach(sub => sub.unsubscribe()); + } + + private setupEventListeners() { + // Listen for broadcast messages if needed + // Currently no role management in this version to match AngularJS + } + + private loadInitialData() { + this.loadingData = true; + + // Load available spatial units + if (this.kommonitorDataExchangeService.availableSpatialUnits) { + this.availableSpatialUnits = this.kommonitorDataExchangeService.availableSpatialUnits; + } + + // Load update interval options + if (this.kommonitorDataExchangeService.updateIntervalOptions) { + this.updateIntervalOptions = this.kommonitorDataExchangeService.updateIntervalOptions; + } + + // Load available dash array objects + if (this.kommonitorDataExchangeService.availableLoiDashArrayObjects) { + this.availableLoiDashArrayObjects = this.kommonitorDataExchangeService.availableLoiDashArrayObjects; + } + + // Always 2 steps to match AngularJS version + this.totalSteps = 2; + + this.loadingData = false; + } + + // Date picker change handler - now using ng-bootstrap's built-in functionality + // The datepicker will automatically handle the date selection and close + // No need for custom methods since ng-bootstrap handles everything + + // Remove custom click outside and escape key handlers since ng-bootstrap handles this + + resetForm() { + if (!this.currentSpatialUnitDataset) return; + + this.spatialUnitLevel = this.currentSpatialUnitDataset.spatialUnitLevel; + this.spatialUnitLevelInvalid = false; + + // Reset metadata with null checks + const metadata = this.currentSpatialUnitDataset.metadata || {}; + this.metadata = { + note: metadata.note || '', + literature: metadata.literature || '', + sridEPSG: 4326, + datasource: metadata.datasource || '', + databasis: metadata.databasis || '', + contact: metadata.contact || '', + description: metadata.description || '', + lastUpdate: metadata.lastUpdate || '', + updateInterval: null + }; + + // km-date-picker binds directly to string; no separate model needed + + // Set update interval with null check + if (metadata.updateInterval) { + this.updateIntervalOptions.forEach(option => { + if (option.apiName === metadata.updateInterval) { + this.metadata.updateInterval = option; + } + }); + } else { + // If no update interval is set, try to find a default one + if (this.updateIntervalOptions && this.updateIntervalOptions.length > 0) { + this.metadata.updateInterval = this.updateIntervalOptions[0]; + } + } + + // Set hierarchy + this.nextLowerHierarchySpatialUnit = null; + this.nextUpperHierarchySpatialUnit = null; + + this.availableSpatialUnits.forEach(spatialUnit => { + if (spatialUnit.spatialUnitLevel === this.currentSpatialUnitDataset.nextLowerHierarchyLevel) { + this.nextLowerHierarchySpatialUnit = spatialUnit; + } + if (spatialUnit.spatialUnitLevel === this.currentSpatialUnitDataset.nextUpperHierarchyLevel) { + this.nextUpperHierarchySpatialUnit = spatialUnit; + } + }); + + // Set outline layer settings - FIXED: Properly initialize outline layer properties + this.isOutlineLayer = this.currentSpatialUnitDataset.isOutlineLayer || false; + this.outlineColor = this.currentSpatialUnitDataset.outlineColor || '#bf3d2c'; + this.outlineWidth = this.currentSpatialUnitDataset.outlineWidth || 2; + + // Set dash array + this.selectedOutlineDashArrayObject = null; + this.selectedoutlineDashArrayObject = null; + if (this.availableLoiDashArrayObjects && this.availableLoiDashArrayObjects.length > 0) { + this.availableLoiDashArrayObjects.forEach(option => { + if (option.dashArrayValue === this.currentSpatialUnitDataset.outlineDashArrayString) { + this.selectedOutlineDashArrayObject = option; + this.selectedoutlineDashArrayObject = option; + } + }); + if (!this.selectedOutlineDashArrayObject) { + this.selectedOutlineDashArrayObject = this.availableLoiDashArrayObjects[0]; + this.selectedoutlineDashArrayObject = this.availableLoiDashArrayObjects[0]; + } + + // Update dropdown button display with selected SVG + setTimeout(() => { + this.updateDropdownButtonDisplay(); + }, 100); + } + + // Set date picker value with null check - now using ng-bootstrap + // The datepicker will automatically display the date from metadata.lastUpdate + + this.hierarchyInvalid = false; + this.successMessagePart = ''; + this.errorMessagePart = ''; + + // No role management in this version to match AngularJS + + // Reset to first step + this.currentStep = 1; + } + + checkSpatialUnitName() { + this.spatialUnitLevelInvalid = false; + this.availableSpatialUnits.forEach(spatialUnit => { + if (spatialUnit.spatialUnitLevel === this.spatialUnitLevel && + spatialUnit.spatialUnitId !== this.currentSpatialUnitDataset.spatialUnitId) { + this.spatialUnitLevelInvalid = true; + return; + } + }); + } + + checkSpatialUnitHierarchy() { + this.hierarchyInvalid = false; + + if (this.nextLowerHierarchySpatialUnit && this.nextUpperHierarchySpatialUnit) { + let indexOfLowerHierarchyUnit = -1; + let indexOfUpperHierarchyUnit = -1; + + for (let i = 0; i < this.availableSpatialUnits.length; i++) { + const spatialUnit = this.availableSpatialUnits[i]; + if (spatialUnit.spatialUnitLevel === this.nextLowerHierarchySpatialUnit.spatialUnitLevel) { + indexOfLowerHierarchyUnit = i; + } + if (spatialUnit.spatialUnitLevel === this.nextUpperHierarchySpatialUnit.spatialUnitLevel) { + indexOfUpperHierarchyUnit = i; + } + } + + if (indexOfLowerHierarchyUnit <= indexOfUpperHierarchyUnit) { + this.hierarchyInvalid = true; + } + } + } + + onChangeOutlineDashArray(outlineDashArrayObject: any) { + console.log('=== onChangeOutlineDashArray called ==='); + console.log('Selected object:', outlineDashArrayObject); + console.log('Object label:', outlineDashArrayObject?.label); + console.log('Object SVG string:', outlineDashArrayObject?.svgString?.substring(0, 50) + '...'); + + this.selectedOutlineDashArrayObject = outlineDashArrayObject; + this.selectedoutlineDashArrayObject = outlineDashArrayObject; // Keep both for compatibility + + console.log('Updated selectedOutlineDashArrayObject:', this.selectedOutlineDashArrayObject); + + // Update dropdown button display using helper method + this.updateDropdownButtonDisplay(); + + // Close the dropdown via Angular state + this.closeOutlineDashArrayDropdown(); + + console.log('=== onChangeOutlineDashArray completed ==='); + } + + + + // Deprecated inline color picker click handler removed + + async editSpatialUnitMetadata() { + if (!this.currentSpatialUnitDataset) return; + + // Prevent multiple submissions + if (this.loadingData) return; + + const spatialUnitName_old = this.currentSpatialUnitDataset.spatialUnitLevel; + const spatialUnitName_new = this.spatialUnitLevel; + + // Validate using service method + const validation = this.kommonitorDataExchangeService.validateSpatialUnitMetadata( + this.metadata, + this.spatialUnitLevel + ); + + if (!validation.isValid) { + this.errorMessage = validation.errors.join('\n'); + this.loadingData = false; + return; + } + + // Build patch body using service method + const patchBody = this.kommonitorDataExchangeService.buildSpatialUnitMetadataPatchBody( + this.spatialUnitLevel, + this.metadata, + this.nextLowerHierarchySpatialUnit ? this.nextLowerHierarchySpatialUnit.spatialUnitLevel : null, + this.nextUpperHierarchySpatialUnit ? this.nextUpperHierarchySpatialUnit.spatialUnitLevel : null, + this.isOutlineLayer, + this.outlineColor, + this.outlineWidth, + this.selectedOutlineDashArrayObject ? this.selectedOutlineDashArrayObject.dashArrayValue : null + ); + + // No role management in this version to match AngularJS + + this.loadingData = true; + this.errorMessage = ''; + this.successMessage = ''; + this.errorMessagePart = ''; + this.successMessagePart = ''; + + try { + const response = await this.http.patch( + `${this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI}/spatial-units/${this.currentSpatialUnitDataset.spatialUnitId}`, + patchBody + ).toPromise(); + + this.successMessagePart = this.currentSpatialUnitDataset.spatialUnitLevel; + this.successMessage = `Metadaten für Raumebene "${this.successMessagePart}" erfolgreich aktualisiert.`; + + // Broadcast refresh events with proper parameters + this.broadcastService.broadcast('refreshSpatialUnitOverviewTable', { + crudType: 'edit', + targetSpatialUnitId: this.currentSpatialUnitDataset.spatialUnitId + }); + if (spatialUnitName_old !== spatialUnitName_new) { + this.broadcastService.broadcast('refreshIndicatorOverviewTable'); + } + + this.loadingData = false; + + // Don't close modal immediately - let user see success message + // User can close manually or we can auto-close after a delay + setTimeout(() => { + this.activeModal.close({ action: 'updated', spatialUnitId: this.currentSpatialUnitDataset.spatialUnitId }); + }, 5000); // Close after 5 seconds + } catch (error: any) { + console.error('Error updating spatial unit metadata:', error); + console.error('Error response:', error.error); + console.error('Error status:', error.status); + + this.errorMessagePart = error.error ? + this.kommonitorDataExchangeService.syntaxHighlightJSON(error.error) : + this.kommonitorDataExchangeService.syntaxHighlightJSON(error); + this.errorMessage = 'Fehler beim Aktualisieren der Metadaten.'; + this.loadingData = false; + } + } + + // Multi-step form navigation + nextStep() { + if (this.currentStep < this.totalSteps) { + this.currentStep++; + } + } + + previousStep() { + if (this.currentStep > 1) { + this.currentStep--; + } + } + + goToStep(step: number) { + if (step >= 1 && step <= this.totalSteps) { + this.currentStep = step; + } + } + + // Import/Export functionality + onImportSpatialUnitEditMetadata() { + this.spatialUnitMetadataImportError = ''; + if (this.metadataImportFile) { + this.metadataImportFile.nativeElement.click(); + } + } + + onMetadataFileSelected(event: any) { + const file = event.target.files[0]; + if (file) { + this.parseMetadataFromFile(file); + } + } + + parseMetadataFromFile(file: File) { + const fileReader = new FileReader(); + + fileReader.onload = (event: any) => { + try { + this.parseFromMetadataFile(event); + } catch (error) { + console.error('Uploaded Metadata File cannot be parsed.'); + this.spatialUnitMetadataImportError = 'Uploaded Metadata File cannot be parsed correctly'; + } + }; + + fileReader.readAsText(file); + } + + parseFromMetadataFile(event: any) { + this.metadataImportSettings = JSON.parse(event.target.result); + + if (!this.metadataImportSettings.metadata) { + console.error('uploaded Metadata File cannot be parsed - wrong structure.'); + this.spatialUnitMetadataImportError = 'Struktur der Datei stimmt nicht mit erwartetem Muster überein.'; + return; + } + + // Apply imported metadata using service method for consistency + this.metadata = { + note: this.metadataImportSettings.metadata.note, + literature: this.metadataImportSettings.metadata.literature, + sridEPSG: this.metadataImportSettings.metadata.sridEPSG, + datasource: this.metadataImportSettings.metadata.datasource, + contact: this.metadataImportSettings.metadata.contact, + lastUpdate: this.metadataImportSettings.metadata.lastUpdate, + description: this.metadataImportSettings.metadata.description, + databasis: this.metadataImportSettings.metadata.databasis, + updateInterval: null + }; + + // km-date-picker binds directly to string; no separate model needed + + // Set update interval + this.updateIntervalOptions.forEach(option => { + if (option.apiName === this.metadataImportSettings.metadata.updateInterval) { + this.metadata.updateInterval = option; + } + }); + + // Set hierarchy + this.availableSpatialUnits.forEach(spatialUnit => { + if (spatialUnit.spatialUnitLevel === this.metadataImportSettings.nextLowerHierarchyLevel) { + this.nextLowerHierarchySpatialUnit = spatialUnit; + } + if (spatialUnit.spatialUnitLevel === this.metadataImportSettings.nextUpperHierarchyLevel) { + this.nextUpperHierarchySpatialUnit = spatialUnit; + } + }); + + this.spatialUnitLevel = this.metadataImportSettings.spatialUnitLevel; + + // Set outline layer settings from import + this.isOutlineLayer = this.metadataImportSettings.isOutlineLayer || false; + this.outlineColor = this.metadataImportSettings.outlineColor || '#bf3d2c'; + this.outlineWidth = this.metadataImportSettings.outlineWidth || 2; + + // Set dash array from import + if (this.metadataImportSettings.outlineDashArrayString && this.availableLoiDashArrayObjects) { + this.availableLoiDashArrayObjects.forEach(option => { + if (option.dashArrayValue === this.metadataImportSettings.outlineDashArrayString) { + this.selectedOutlineDashArrayObject = option; + this.selectedoutlineDashArrayObject = option; + } + }); + } + + // Set date picker value from import + // The datepicker will automatically display the imported date + + // No role management in this version to match AngularJS + } + + onExportSpatialUnitEditMetadata() { + // Build export data using service method + const metadataExport = this.kommonitorDataExchangeService.buildSpatialUnitMetadataExport( + this.metadata, + this.spatialUnitLevel, + this.nextLowerHierarchySpatialUnit ? this.nextLowerHierarchySpatialUnit.spatialUnitLevel : null, + this.nextUpperHierarchySpatialUnit ? this.nextUpperHierarchySpatialUnit.spatialUnitLevel : null, + this.isOutlineLayer, + this.outlineColor, + this.outlineWidth, + this.selectedOutlineDashArrayObject ? this.selectedOutlineDashArrayObject.dashArrayValue : null + ); + + // No role management in this version to match AngularJS + + const metadataJSON = JSON.stringify(metadataExport, null, 2); + const fileName = `Raumebene_Metadaten_Export${this.spatialUnitLevel ? '-' + this.spatialUnitLevel : ''}.json`; + this.downloadFile(metadataJSON, fileName); + } + + private downloadFile(content: string, fileName: string) { + const blob = new Blob([content], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.download = fileName; + a.href = url; + a.target = "_blank"; + a.rel = "noopener noreferrer"; + a.click(); + a.remove(); + URL.revokeObjectURL(url); + } + + // Metadata structure for export - now using service + get spatialUnitMetadataStructure() { + return this.kommonitorDataExchangeService.spatialUnitMetadataStructure; + } + + hideSuccessAlert() { + this.successMessage = ''; + } + + hideErrorAlert() { + this.errorMessage = ''; + } + + hideMetadataErrorAlert() { + this.spatialUnitMetadataImportError = ''; + } + + closeOnSuccess() { + this.activeModal.close({ action: 'updated', spatialUnitId: this.currentSpatialUnitDataset?.spatialUnitId }); + } + + cancel() { + this.activeModal.dismiss(); + } + + onSubmit(event?: Event) { + // Prevent default form submission behavior + if (event) { + event.preventDefault(); + } + + // Only proceed if not already loading + if (!this.loadingData) { + this.editSpatialUnitMetadata(); + } + } + + // Missing function for metadata export template + onExportSpatialUnitEditMetadataTemplate() { + const metadataStructure = this.spatialUnitMetadataStructure; + const metadataJSON = JSON.stringify(metadataStructure, null, 2); + const fileName = "Raumebene_Metadaten_Vorlage_Export.json"; + this.downloadFile(metadataJSON, fileName); + } + + // Method to safely sanitize SVG content + getSafeSvg(svgString: string): SafeHtml { + return this.sanitizer.bypassSecurityTrustHtml(svgString); + } + + // Cached sanitizer for performance and stability (align with Add modal) + private svgSanitizeCache: Map = new Map(); + + getSafeSvgCached(svgString: string): SafeHtml { + if (!svgString) { + return '' as unknown as SafeHtml; + } + const cached = this.svgSanitizeCache.get(svgString); + if (cached) { + return cached; + } + const trusted = this.sanitizer.bypassSecurityTrustHtml(svgString); + this.svgSanitizeCache.set(svgString, trusted); + return trusted; + } + + // Method to update dropdown button display + private updateDropdownButtonDisplay() { + // No-op: rendering handled via Angular bindings with cached SafeHtml + return; + } + + // trackBy for stable ngFor rendering (align with Add modal) + trackLoiOption(index: number, item: any) { + return item?.dashArrayValue ?? index; + } + + // km-date-picker handles validation and coercion itself; no blur handler needed + + // Toggle/close handlers for outline dash array dropdown + toggleOutlineDashArrayDropdown(event?: Event) { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + this.showOutlineDashArrayDropdown = !this.showOutlineDashArrayDropdown; + } + + closeOutlineDashArrayDropdown(event?: Event) { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + this.showOutlineDashArrayDropdown = false; + } + + @HostListener('document:click', ['$event']) + onDocumentClick(event: MouseEvent) { + const clickTarget = event.target as Node; + if (this.showOutlineDashArrayDropdown && this.outlineDashArrayDropdown && this.outlineDashArrayDropdown.nativeElement && this.outlineDashArrayDropdown.nativeElement.contains) { + if (this.outlineDashArrayDropdown.nativeElement.contains(clickTarget)) { + return; + } + } + this.closeOutlineDashArrayDropdown(); + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditUserRolesModal/spatial-unit-edit-user-roles-modal.component.css b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditUserRolesModal/spatial-unit-edit-user-roles-modal.component.css new file mode 100644 index 000000000..89598fb00 --- /dev/null +++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditUserRolesModal/spatial-unit-edit-user-roles-modal.component.css @@ -0,0 +1,622 @@ +/* Modal styling */ +.modal-header { + border-bottom: 1px solid #dee2e6; + padding: 1rem; + background-color: #f8f9fa; +} + +.modal-title { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 0; + color: #333; +} + +.modal-body { + padding: 1.5rem; + max-height: 80vh; + overflow-y: auto; +} + +.modal-footer { + border-top: 1px solid #dee2e6; + padding: 1rem; + background-color: #f8f9fa; +} + +/* Loading overlay */ +.loading-overlay-admin-panel { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; +} + +.loading-overlay-admin-panel .glyphicon { + font-size: 2rem; +} + +.ng-hide { + display: none !important; +} + +/* Glyphicon base class */ +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.glyphicon-refresh:before { + content: "\e031"; +} + +.icon-spin { + animation: spin 2s infinite linear; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.spinner-border { + width: 3rem; + height: 3rem; + border-width: 0.3rem; +} + +/* Progress Bar Styles - Matching Original */ +#progressbar { + margin-bottom: 10px; + overflow: hidden; + /*CSS counters to number the steps*/ + counter-reset: step; +} + +#progressbar li { + list-style-type: none; + color: black; + text-transform: uppercase; + font-size: 9px; + float: left; + position: relative; + letter-spacing: 1px; + cursor: pointer; +} + +#progressbar li:before { + content: counter(step); + counter-increment: step; + width: 24px; + height: 24px; + line-height: 26px; + display: block; + font-size: 12px; + color: #333; + background: #cccc; + border-radius: 25px; + margin: 0 auto 10px auto; + transform: translateZ(-1px); +} + +/*progressbar connectors*/ +#progressbar li:after { + content: ''; + width: 100%; + height: 2px; + background: #cccc; + position: absolute; + left: -50%; + top: 9px; + /*put it behind the numbers */ + z-index: -1; +} + +#progressbar li:first-child:after { + /*connector not needed before the first step*/ + content: none; +} + +/*marking active/completed steps green*/ +/*The number of the step and the connector before it = green*/ +#progressbar li.active:before, #progressbar li.active:after { + background: var(--kommonitor-primary); + color: white; +} + +#progressbar li.clickable { + cursor: pointer; + transition: all 0.3s ease; +} + +#progressbar li.clickable:hover { + color: var(--kommonitor-primary); +} + +#progressbar li.clickable:hover:before { + background: var(--kommonitor-primary); + transform: scale(1.1); +} + +/* Multi-step form styles - Matching Original */ +.multiStepForm { + text-align: center; + position: relative; + margin-top: 30px; + z-index: 11000; + font-size: 12px; +} + +.multiStepForm fieldset { + background: white; + border: 0 none; + border-radius: 0px; + box-shadow: 0 0 15px 1px rgba(0, 0, 0, 0.4); + padding: 0px 30px; + box-sizing: border-box; + /*stacking fieldsets above each other*/ + position: relative; + width: 100%; +} + +/*inputs*/ +.multiStepForm input, .multiStepForm textarea, .multiStepForm select { + border: 1px solid #ccc; + border-radius: 0px; + margin-bottom: 10px; + width: 100%; + box-sizing: border-box; + color: #2C3E50; + font-size: 13px; +} + +.multiStepForm input:focus, .multiStepForm textarea:focus { + -moz-box-shadow: none !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; + border: 1px solid var(--kommonitor-primary); + outline-width: 0; + transition: All 0.5s ease-in; + -webkit-transition: All 0.5s ease-in; + -moz-transition: All 0.5s ease-in; + -o-transition: All 0.5s ease-in; +} + +/*headings*/ +.fs-title { + font-size: 18px; + text-transform: uppercase; + color: #2C3E50; + margin-bottom: 10px; + letter-spacing: 2px; + font-weight: bold; +} + +.fs-subtitle { + font-weight: normal; + font-size: 13px; + color: #666; + margin-bottom: 20px; +} + +/* Form controls */ +.form-control, +.form-select { + border-radius: 0.375rem; + border: 1px solid #ced4da; + padding: 0.375rem 0.75rem; + font-size: 1rem; + line-height: 1.5; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.form-control:focus, +.form-select:focus { + border-color: #80bdff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.form-check-input:checked { + background-color: #007bff; + border-color: #007bff; +} + +.form-check-input:focus { + border-color: #80bdff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +/* AngularJS Switch styling */ +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.switchslider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: .4s; +} + +.switchslider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + transition: .4s; +} + +input:checked + .switchslider { + background-color: #27AE60; +} + +input:focus + .switchslider { + box-shadow: 0 0 1px #27AE60; +} + +input:checked + .switchslider:before { + transform: translateX(26px); +} + +.switchslider.round { + border-radius: 34px; +} + +.switchslider.round:before { + border-radius: 50%; +} + +/* Form switch styling - keeping for backward compatibility */ +.form-switch .form-check-input { + width: 2em; + margin-left: -2.5em; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%2855, 63, 71, 0.75%29'/%3e%3c/svg%3e"); + background-position: left center; + background-repeat: no-repeat; + background-size: contain; + border-radius: 1em; + transition: background-position 0.15s ease-in-out; +} + +.form-switch .form-check-input:checked { + background-position: right center; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 1.0%29'/%3e%3c/svg%3e"); +} + +/*buttons*/ +.multiStepForm .action-button { + width: auto; + background: var(--kommonitor-primary); + font-weight: bold; + color: white; + border: 0 none; + border-radius: 25px; + cursor: pointer; + padding: 10px 5px; + margin: 10px 5px; +} + +.multiStepForm .action-button:hover, .multiStepForm .action-button:focus { + box-shadow: 0 0 0 2px white, 0 0 0 3px var(--kommonitor-primary); +} + +.multiStepForm .action-button-previous { + width: 100px; + background: rgb(236, 138, 138); + font-weight: bold; + color: white; + border: 0 none; + border-radius: 25px; + cursor: pointer; + padding: 10px 5px; + margin: 10px 5px; +} + +.multiStepForm .action-button-previous:hover, .multiStepForm .action-button-previous:focus { + box-shadow: 0 0 0 2px white, 0 0 0 3px #C5C5F1; +} + +/* Alert styling */ +.alert { + padding: 0.75rem 3.25rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.375rem; + position: relative; +} + +.alert-info { + color: #0c5460; + background-color: #d1ecf1; + border-color: #bee5eb; +} + +.alert-danger { + color: #721c24; + background-color: #f8d7da; + border-color: #f5c6cb; +} + +.alert-success { + color: #155724; + background-color: #d4edda; + border-color: #c3e6cb; +} + +.alert-dismissible .btn-close { + position: absolute; + top: 0; + right: 0; + z-index: 2; + padding: 1.25rem 1rem; +} + +/* Make sure form fields have proper spacing */ +.row.vertical-align { + margin-bottom: 1.5rem; +} + +/* Ensure proper spacing between form groups */ +.form-group { + margin-bottom: 1.5rem; +} + +/* Vertical alignment utility */ +.vertical-align { + display: flex; + align-items: center; +} + +.margin-right { + margin-right: 0.5rem; +} + +/* Data grid styling */ +#spatialUnitEditRoleManagementTable { + border: 1px solid #dee2e6; + border-radius: 0.375rem; + overflow: hidden; +} + +/* AngularJS Input group styling */ +.input-group { + position: relative; + display: table; + border-collapse: separate; +} + +.input-group-addon { + padding: 6px 12px; + font-size: 14px; + font-weight: normal; + line-height: 1; + color: #555; + text-align: center; + background-color: #eee; + border: 1px solid #ccc; + border-radius: 4px; + width: 1%; + white-space: nowrap; + vertical-align: middle; + display: table-cell; +} + +.input-group .form-control { + position: relative; + z-index: 2; + float: left; + width: 100%; + margin-bottom: 0; + display: table-cell; +} + +.input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; +} + +.input-group-addon:first-child { + border-right: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group .form-control:last-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +/* Modern Input group styling - keeping for backward compatibility */ +.input-group-text { + display: flex; + align-items: center; + padding: 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #6c757d; + text-align: center; + white-space: nowrap; + background-color: #e9ecef; + border: 1px solid #ced4da; + border-radius: 0.375rem 0 0 0.375rem; +} + +/* Responsive design */ +@media (max-width: 768px) { + .modal-body { + padding: 1rem; + } + + #progressbar li { + font-size: 11px; + padding: 15px; + } + + .fs-title { + font-size: 1.25rem; + } + + .fs-subtitle { + font-size: 0.9rem; + } +} + +/* Pre tag styling for error messages */ +pre { + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 0.375rem; + padding: 1rem; + font-size: 0.875rem; + color: #495057; + white-space: pre-wrap; + word-wrap: break-word; +} + +/* AngularJS Help block styles */ +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #737373; + font-size: 12px; +} + +.help-block.with-errors { + color: #a94442; +} + +/* AngularJS button styles */ +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + border: 1px solid transparent; + border-radius: 4px; + text-decoration: none; +} + +.btn:focus, +.btn:active:focus, +.btn.active:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn:hover, +.btn:focus { + color: #333; + text-decoration: none; +} + +.btn-default { + color: #333; + background-color: #fff; + border-color: #ccc; +} + +.btn-default:hover, +.btn-default:focus { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} + +.btn-success { + color: #fff; + background-color: #5cb85c; + border-color: #4cae4c; +} + +.btn-success:hover, +.btn-success:focus { + color: #fff; + background-color: #449d44; + border-color: #398439; +} + +.btn-danger { + color: #fff; + background-color: #d9534f; + border-color: #d43f3a; +} + +.btn-danger:hover, +.btn-danger:focus { + color: #fff; + background-color: #c9302c; + border-color: #ac2925; +} + +.btn:disabled, +.btn[disabled] { + cursor: not-allowed; + opacity: 0.65; + box-shadow: none; +} + +.pull-left { + float: left !important; +} + +.pull-right { + float: right !important; +} + +/* Form text helper - keeping for backward compatibility */ +.form-text { + margin-top: 0.25rem; + font-size: 0.875em; + color: #6c757d; +} + +.text-danger { + color: #dc3545 !important; +} + +/* Loading state for buttons */ +.btn:disabled { + opacity: 0.65; + cursor: not-allowed; +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditUserRolesModal/spatial-unit-edit-user-roles-modal.component.html b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditUserRolesModal/spatial-unit-edit-user-roles-modal.component.html new file mode 100644 index 000000000..02dd6efb0 --- /dev/null +++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditUserRolesModal/spatial-unit-edit-user-roles-modal.component.html @@ -0,0 +1,149 @@ +
+
+ +
+
+ + + + + + +
+ +

Zugriffsschutz und Eigentümerschaft aktualisiert

+ Erfolgreiche Aktualisierung des Zugriffsschutzes und der Eigentümerschaft für die Raumeinheit '{{currentSpatialUnitDataset?.spatialUnitLevel}}' +
+ +
+ +

Aktualisierung gescheitert

+ Bei der Aktualisierung des Zugriffsschutzes und der Eigentümerschaft ist ein Fehler aufgetreten. Fehlermeldung: +
+

+
\ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditUserRolesModal/spatial-unit-edit-user-roles-modal.component.ts b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditUserRolesModal/spatial-unit-edit-user-roles-modal.component.ts new file mode 100644 index 000000000..46abd067f --- /dev/null +++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditUserRolesModal/spatial-unit-edit-user-roles-modal.component.ts @@ -0,0 +1,488 @@ +import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { HttpClient } from '@angular/common/http'; +import { Subscription } from 'rxjs'; +import { BroadcastService } from 'services/broadcast-service/broadcast.service'; +import { KommonitorDataExchangeService } from 'services/adminSpatialUnit/kommonitor-data-exchange.service'; +import { KommonitorDataGridHelperService } from 'services/adminSpatialUnit/kommonitor-data-grid-helper.service'; +import { GridOptions, GridReadyEvent, ColDef } from 'ag-grid-community'; + +@Component({ + selector: 'spatial-unit-edit-user-roles-modal', + templateUrl: './spatial-unit-edit-user-roles-modal.component.html', + styleUrls: ['./spatial-unit-edit-user-roles-modal.component.css'] +}) +export class SpatialUnitEditUserRolesModalComponent implements OnInit, OnDestroy, AfterViewInit { + @ViewChild('progressbar', { static: true }) progressBar!: ElementRef; + + private _currentSpatialUnitDataset: any = null; + + get currentSpatialUnitDataset(): any { + return this._currentSpatialUnitDataset; + } + + set currentSpatialUnitDataset(value: any) { + this._currentSpatialUnitDataset = value; + if (value) { + this.resetForm(); + // If access control data is available, refresh the table + if (this.kommonitorDataExchangeService.accessControl && this.kommonitorDataExchangeService.accessControl.length > 0) { + setTimeout(() => { + this.refreshRoleManagementTable(); + }, 100); + } + } + } + roleManagementTableOptions: any = undefined; + + // ag-Grid properties + roleManagementColumnDefs: ColDef[] = []; + roleManagementRowData: any[] = []; + roleManagementDefaultColDef: any = {}; + roleManagementGridOptions: GridOptions = {}; + roleManagementGridApi: any = null; + + successMessagePart: string = ''; + errorMessagePart: string = ''; + + ownerOrgFilter: string = ''; + ownerOrganization: string = ''; + activeRolesOnly: boolean = true; + permissions: any[] = []; + resourcesCreatorRights: any[] = []; + + loadingData: boolean = false; + currentStep: number = 1; + totalSteps: number = 2; + + private subscription: Subscription = new Subscription(); + + constructor( + public activeModal: NgbActiveModal, + public kommonitorDataExchangeService: KommonitorDataExchangeService, + public kommonitorDataGridHelperService: KommonitorDataGridHelperService, + private broadcastService: BroadcastService, + private http: HttpClient + ) {} + + ngOnInit(): void { + this.prepareCreatorList(); + this.setupBroadcastSubscription(); + this.loadAccessControlData(); + } + + ngAfterViewInit(): void { + this.updateProgressBar(); + // Initialize grid if data is already available + if (this.currentSpatialUnitDataset) { + setTimeout(() => { + this.refreshRoleManagementTable(); + }, 100); + } + } + + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } + + private setupBroadcastSubscription(): void { + this.subscription.add( + this.broadcastService.currentBroadcastMsg.subscribe((message: any) => { + if (message.key === 'availableRolesUpdate') { + this.refreshRoleManagementTable(); + } + }) + ); + } + + prepareCreatorList(): void { + if (this.kommonitorDataExchangeService.currentKomMonitorLoginRoleNames.length > 0) { + const creatorRights: string[] = []; + const creatorRightsChildren: string[] = []; + + this.kommonitorDataExchangeService.currentKomMonitorLoginRoleNames.forEach((roles: string) => { + const key = roles.split('.')[0]; + const role = roles.split('.')[1]; + + // case unit-resources-creator + if (role === 'unit-resources-creator' && !this.resourcesCreatorRights.includes(key)) { + creatorRights.push(key); + } + + // case client-resources-creator, gather unit-ids first, then fetch all unit-data + if (role === 'client-resources-creator' && !creatorRightsChildren.includes(key)) { + creatorRightsChildren.push(key); + } + }); + + // gather all children + this.gatherCreatorRightsChildren(creatorRights, creatorRightsChildren); + + this.resourcesCreatorRights = this.kommonitorDataExchangeService.accessControl.filter( + (elem: any) => creatorRights.includes(elem.name) + ); + } + } + + private gatherCreatorRightsChildren(creatorRights: string[], creatorRightsChildren: string[]): void { + if (creatorRightsChildren.length > 0) { + this.kommonitorDataExchangeService.accessControl + .filter((elem: any) => creatorRightsChildren.includes(elem.name)) + .flatMap((res: any) => res.children) + .forEach((child: any) => { + this.kommonitorDataExchangeService.accessControl + .filter((elem: any) => elem.organizationalUnitId === child) + .forEach((childData: any) => { + creatorRights.push(childData.name); + this.gatherCreatorRightsChildren(creatorRights, [childData.name]); + }); + }); + } + } + + refreshRoleManagementTable(): void { + this.permissions = this.currentSpatialUnitDataset ? this.currentSpatialUnitDataset.permissions : []; + + // Check if accessControl data is available + if (!this.kommonitorDataExchangeService.accessControl || this.kommonitorDataExchangeService.accessControl.length === 0) { + return; + } + + // set datasetOwner to disable checkboxes for owned datasets in permissions-table + this.kommonitorDataExchangeService.accessControl.forEach((item: any) => { + if (this.currentSpatialUnitDataset) { + if (item.organizationalUnitId === this.currentSpatialUnitDataset.ownerId) { + item.datasetOwner = true; + } else { + item.datasetOwner = false; + } + } + }); + + if (this.permissions.length === 0) { + this.activeRolesOnly = false; + } + + let access = this.kommonitorDataExchangeService.accessControl; + // Do not filter access here; always pass the full array to the grid helper + + this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid( + 'spatialUnitEditRoleManagementTable', + this.roleManagementTableOptions, + access, + this.permissions, + true + ); + + // Extract column definitions and row data for ag-grid-angular + if (this.roleManagementTableOptions) { + this.roleManagementColumnDefs = this.roleManagementTableOptions.columnDefs || []; + // Always get the full row data + let allRows = this.roleManagementTableOptions.rowData || []; + // If toggle is on, filter for at least one assigned right + if (this.activeRolesOnly) { + allRows = allRows.filter((row: any) => row.viewer || row.editor || row.creator); + } + this.roleManagementRowData = allRows; + // Build grid configuration + this.buildRoleManagementGridConfig(); + } + } + + private buildRoleManagementGridConfig(): void { + // Get base configuration from service + this.roleManagementDefaultColDef = this.kommonitorDataGridHelperService.buildRoleManagementDefaultColDef(); + + // Get base grid options from service + const baseGridOptions = this.kommonitorDataGridHelperService.buildRoleManagementGridOptionsPublic( + this.roleManagementTableOptions?.components + ); + + // Override with component-specific settings + this.roleManagementGridOptions = { + ...baseGridOptions, + onGridReady: (params) => { + this.onRoleManagementGridReady(params); + }, + onFirstDataRendered: (event) => { + this.onRoleManagementFirstDataRendered(event); + }, + onColumnResized: (event) => { + this.onRoleManagementColumnResized(event); + } + }; + } + + // Default column definition is now handled by the service + + // Grid options are now handled by the service with component-specific overrides + + onRoleManagementGridReady(params: GridReadyEvent): void { + this.roleManagementGridApi = params.api; + // Ensure helper service has the grid API to collect selected role IDs + this.kommonitorDataGridHelperService.setGridApi(params.api); + } + + onRoleManagementFirstDataRendered(event: any): void { + // Handle first data rendered event + } + + onRoleManagementColumnResized(event: any): void { + // Handle column resized event + } + + onActiveRolesOnlyChange(): void { + this.refreshRoleManagementTable(); + } + + onChangeOwner(ownerOrganization: string): void { + this.ownerOrganization = ownerOrganization; + this.refreshRoles(this.ownerOrganization); + } + + private refreshRoles(orgUnitId: string): void { + const accessControl = this.kommonitorDataExchangeService.getAccessControlById(orgUnitId); + const permissionIds_ownerUnit = orgUnitId && accessControl ? + accessControl.permissions + .filter((permission: any) => permission.permissionLevel === 'viewer' || permission.permissionLevel === 'editor') + .map((permission: any) => permission.permissionId) : []; + + // set datasetOwner to disable checkboxes for owned datasets in permissions-table + this.kommonitorDataExchangeService.accessControl.forEach((item: any) => { + if (item.organizationalUnitId === orgUnitId) { + item.datasetOwner = true; + } else { + item.datasetOwner = false; + } + }); + + this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid( + 'spatialUnitEditRoleManagementTable', + this.roleManagementTableOptions, + this.kommonitorDataExchangeService.accessControl, + permissionIds_ownerUnit, + true + ); + + // Extract column definitions and row data for ag-grid-angular and rebuild grid config + if (this.roleManagementTableOptions) { + this.roleManagementColumnDefs = this.roleManagementTableOptions.columnDefs || []; + this.roleManagementRowData = this.roleManagementTableOptions.rowData || []; + + // Build grid configuration (this will use the components from roleManagementTableOptions) + this.buildRoleManagementGridConfig(); + + // If grid is already initialized, update the data and grid options + if (this.roleManagementGridApi && !this.roleManagementGridApi.isDestroyed()) { + // Update data + this.roleManagementGridApi.setRowData(this.roleManagementRowData); + this.roleManagementGridApi.setColumnDefs(this.roleManagementColumnDefs); + + // Refresh the grid to ensure it updates + setTimeout(() => { + if (this.roleManagementGridApi && !this.roleManagementGridApi.isDestroyed()) { + this.roleManagementGridApi.refreshCells(); + this.roleManagementGridApi.redrawRows(); + } + }, 100); + } + } + } + + resetForm(): void { + if (this.currentSpatialUnitDataset) { + this.ownerOrganization = this.currentSpatialUnitDataset.ownerId; + // Ensure the grid is initialized after a short delay to allow the view to be ready + setTimeout(() => { + this.refreshRoleManagementTable(); + }, 100); + } + + this.ownerOrgFilter = ''; + this.successMessagePart = ''; + this.errorMessagePart = ''; + this.currentStep = 1; + this.updateProgressBar(); + } + + nextStep(): void { + if (this.currentStep < this.totalSteps) { + this.currentStep++; + this.updateProgressBar(); + } + } + + previousStep(): void { + if (this.currentStep > 1) { + this.currentStep--; + this.updateProgressBar(); + } + } + + goToStep(step: number): void { + if (step >= 1 && step <= this.totalSteps) { + this.currentStep = step; + this.updateProgressBar(); + } + } + + private updateProgressBar(): void { + if (this.progressBar && this.progressBar.nativeElement) { + const steps = this.progressBar.nativeElement.querySelectorAll('li'); + steps.forEach((step: any, index: number) => { + if (index < this.currentStep) { + step.classList.add('active'); + } else { + step.classList.remove('active'); + } + }); + } + } + + async editSpatialUnitUserRoles(): Promise { + if (this.ownerOrganization && this.ownerOrganization !== this.currentSpatialUnitDataset.ownerId) { + const confirmMessage = 'Sind Sie sicher, dass Sie den Eigentümerschaft an dieser Resource endgültig und unwiderruflich übertragen und damit abgeben wollen?'; + if (!window.confirm(confirmMessage)) { + return; + } + } + + await this.putUserRoles(); + await this.putOwnership(); + } + + private async putUserRoles(): Promise { + try { + this.loadingData = true; + this.errorMessagePart = ''; + + const putBody = { + permissions: this.kommonitorDataGridHelperService.getSelectedRoleIds_roleManagementGrid(this.roleManagementTableOptions), + isPublic: this.currentSpatialUnitDataset.isPublic + }; + + const response = await this.http.put( + `${this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI}/spatial-units/${this.currentSpatialUnitDataset.spatialUnitId}/permissions`, + putBody, + { headers: { 'Content-Type': 'application/json' } } + ).toPromise(); + + this.successMessagePart = this.currentSpatialUnitDataset.spatialUnitLevel; + this.broadcastService.broadcast('refreshSpatialUnitOverviewTable', ['edit', this.currentSpatialUnitDataset.spatialUnitId]); + // Persist latest selection locally so the grid reflects changes on refresh + this.permissions = putBody.permissions; + if (this.currentSpatialUnitDataset) { + this.currentSpatialUnitDataset.permissions = putBody.permissions; + } + // Optionally refresh the table to sync checkbox state + setTimeout(() => this.refreshRoleManagementTable(), 0); + + } catch (error: any) { + this.errorMessagePart = 'Fehler beim Aktualisieren der Zugriffsrechte. Fehler lautet: \n\n'; + if (error.error) { + this.errorMessagePart += this.kommonitorDataExchangeService.syntaxHighlightJSON(error.error); + } else { + this.errorMessagePart += this.kommonitorDataExchangeService.syntaxHighlightJSON(error); + } + } finally { + this.loadingData = false; + } + } + + private async putOwnership(): Promise { + try { + this.loadingData = true; + this.errorMessagePart = ''; + + const putBody = { + ownerId: this.ownerOrganization || this.currentSpatialUnitDataset.ownerId + }; + + const response = await this.http.put( + `${this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI}/spatial-units/${this.currentSpatialUnitDataset.spatialUnitId}/ownership`, + putBody, + { headers: { 'Content-Type': 'application/json' } } + ).toPromise(); + + this.successMessagePart = this.currentSpatialUnitDataset.spatialUnitLevel; + this.broadcastService.broadcast('refreshSpatialUnitOverviewTable', ['edit', this.currentSpatialUnitDataset.spatialUnitId]); + + } catch (error: any) { + this.errorMessagePart = 'Fehler beim Aktualisieren der Eigentümerschaft. Fehler lautet: \n\n'; + if (error.error) { + this.errorMessagePart += this.kommonitorDataExchangeService.syntaxHighlightJSON(error.error); + } else { + this.errorMessagePart += this.kommonitorDataExchangeService.syntaxHighlightJSON(error); + } + } finally { + this.loadingData = false; + } + } + + getCurrentOwnerName(): string { + if (this.currentSpatialUnitDataset && this.currentSpatialUnitDataset.ownerId) { + const owner = this.kommonitorDataExchangeService.getAccessControlById(this.currentSpatialUnitDataset.ownerId); + return owner?.name || ''; + } + return ''; + } + + isOwnershipChanging(): boolean { + return !!(this.ownerOrganization && this.ownerOrganization !== this.currentSpatialUnitDataset.ownerId); + } + + getFilteredOrganizations(): any[] { + if (!this.ownerOrgFilter) { + return this.kommonitorDataExchangeService.checkAdminPermission() ? + this.kommonitorDataExchangeService.accessControl : this.resourcesCreatorRights; + } + + const orgs = this.kommonitorDataExchangeService.checkAdminPermission() ? + this.kommonitorDataExchangeService.accessControl : this.resourcesCreatorRights; + + return orgs.filter((org: any) => + org.name.toLowerCase().includes(this.ownerOrgFilter.toLowerCase()) + ); + } + + hideSuccessAlert(): void { + this.successMessagePart = ''; + } + + hideErrorAlert(): void { + this.errorMessagePart = ''; + } + + onCancel(): void { + this.activeModal.dismiss(); + } + + // Method to initialize the component with data (called from parent) + initializeWithData(spatialUnitDataset: any): void { + this.currentSpatialUnitDataset = spatialUnitDataset; + this.resetForm(); + } + + private loadAccessControlData(): void { + // Check if access control data is already available + if (this.kommonitorDataExchangeService.accessControl && this.kommonitorDataExchangeService.accessControl.length > 0) { + // If we have data and a spatial unit dataset, refresh the table + if (this.currentSpatialUnitDataset) { + this.refreshRoleManagementTable(); + } + } else { + // Fetch access control data from server + this.kommonitorDataExchangeService.fetchAccessControlMetadata().subscribe({ + next: (data) => { + // If we have data and a spatial unit dataset, refresh the table + if (this.currentSpatialUnitDataset) { + this.refreshRoleManagementTable(); + } + }, + error: (error) => { + console.error('Error fetching access control data:', error); + } + }); + } + } +} \ No newline at end of file diff --git a/app/components/ngComponents/customElements/color-picker/km-color-picker.component.css b/app/components/ngComponents/customElements/color-picker/km-color-picker.component.css new file mode 100644 index 000000000..d4843918e --- /dev/null +++ b/app/components/ngComponents/customElements/color-picker/km-color-picker.component.css @@ -0,0 +1,36 @@ +.km-color-picker { position: relative; display: inline-block; } +.km-color-picker.disabled { opacity: 0.6; pointer-events: none; } + +.km-color-trigger { + width: 100px; + height: 35px; + border: 2px solid; + border-radius: 4px; + color: #fff; + text-shadow: 0 1px 2px rgba(0,0,0,0.35); +} + +.km-color-text { + background: rgba(0,0,0,0.25); + padding: 2px 6px; + border-radius: 2px; +} + +.km-color-overlay { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: #fff; + border: 1px solid #ccc; + border-radius: 8px; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); + padding: 10px; +} + +.km-color-backdrop { + position: fixed; + inset: 0; +} + + diff --git a/app/components/ngComponents/customElements/color-picker/km-color-picker.component.html b/app/components/ngComponents/customElements/color-picker/km-color-picker.component.html new file mode 100644 index 000000000..05b0c9870 --- /dev/null +++ b/app/components/ngComponents/customElements/color-picker/km-color-picker.component.html @@ -0,0 +1,26 @@ +
+ + +
+ + +
+ +
+
+ + diff --git a/app/components/ngComponents/customElements/color-picker/km-color-picker.component.ts b/app/components/ngComponents/customElements/color-picker/km-color-picker.component.ts new file mode 100644 index 000000000..e4080b528 --- /dev/null +++ b/app/components/ngComponents/customElements/color-picker/km-color-picker.component.ts @@ -0,0 +1,81 @@ +import { CommonModule } from '@angular/common'; +import { Component, ElementRef, EventEmitter, HostListener, Input, Output, ViewChild } from '@angular/core'; +import { ColorSketchModule } from 'ngx-color/sketch'; + +@Component({ + selector: 'km-color-picker', + standalone: true, + imports: [CommonModule, ColorSketchModule], + templateUrl: './km-color-picker.component.html', + styleUrls: ['./km-color-picker.component.css'] +}) +export class KmColorPickerComponent { + @Input() color: string = '#000000'; + @Output() colorChange = new EventEmitter(); + + @Input() label: string = 'Farbe wählen'; + @Input() disabled: boolean = false; + @Input() closeOnOutsideClick: boolean = true; + @Input() zIndex: number = 2000; + + @ViewChild('container', { static: true }) containerRef!: ElementRef; + + isOpen: boolean = false; + + toggle(event?: Event): void { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + if (this.disabled) { return; } + this.isOpen = !this.isOpen; + } + + close(event?: Event): void { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + this.isOpen = false; + } + + onContainerClick(event: Event): void { + event.preventDefault(); + event.stopPropagation(); + } + + onChange(event: any): void { + const next = event && event.color && event.color.hex ? event.color.hex : this.color; + if (typeof next === 'string') { + this.color = next; + this.colorChange.emit(this.color); + } + } + + onChangeComplete(event: any): void { + const next = event && event.color && event.color.hex ? event.color.hex : this.color; + if (typeof next === 'string') { + this.color = next; + this.colorChange.emit(this.color); + } + } + + @HostListener('document:click', ['$event']) + onDocumentClick(event: MouseEvent): void { + if (!this.isOpen || !this.closeOnOutsideClick) { + return; + } + + const targetNode = event.target as Node | null; + const hostEl = this.containerRef?.nativeElement; + if (!hostEl || !targetNode) { + this.isOpen = false; + return; + } + if (!hostEl.contains(targetNode)) { + this.isOpen = false; + } + } +} + + diff --git a/app/components/ngComponents/customElements/date-picker/km-date-picker.component.css b/app/components/ngComponents/customElements/date-picker/km-date-picker.component.css new file mode 100644 index 000000000..aa5d6a1de --- /dev/null +++ b/app/components/ngComponents/customElements/date-picker/km-date-picker.component.css @@ -0,0 +1,10 @@ +.km-date-picker .input-group { + width: 100%; +} + +.km-date-picker .btn + .btn { + margin-left: 4px; +} + + + diff --git a/app/components/ngComponents/customElements/date-picker/km-date-picker.component.html b/app/components/ngComponents/customElements/date-picker/km-date-picker.component.html new file mode 100644 index 000000000..eb14f7960 --- /dev/null +++ b/app/components/ngComponents/customElements/date-picker/km-date-picker.component.html @@ -0,0 +1,42 @@ +
+
+
+ +
+
+ +
+
+ +
+ Dieses Feld ist erforderlich. + Ungültiges Datum. Format: JJJJ-MM-TT. + Datum ist vor dem Minimum ({{ min }}). + Datum ist nach dem Maximum ({{ max }}). +
+
+ + diff --git a/app/components/ngComponents/customElements/date-picker/km-date-picker.component.ts b/app/components/ngComponents/customElements/date-picker/km-date-picker.component.ts new file mode 100644 index 000000000..0bb1f9cbd --- /dev/null +++ b/app/components/ngComponents/customElements/date-picker/km-date-picker.component.ts @@ -0,0 +1,271 @@ +import { Component, EventEmitter, forwardRef, Input, OnDestroy, OnInit, Output, ViewChild, ElementRef, Injectable, OnChanges, SimpleChanges } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormControl, FormsModule, NG_VALIDATORS, NG_VALUE_ACCESSOR, ReactiveFormsModule, ValidationErrors, Validator } from '@angular/forms'; +import { NgbDateAdapter, NgbDateParserFormatter, NgbDateStruct, NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; + +// ISO parser/formatter as in spatial unit component +@Injectable() +export class NgbDateISOParserFormatter extends NgbDateParserFormatter { + parse(value: string | null): NgbDateStruct | null { + if (!value) { return null; } + const trimmed = value.trim(); + if (!/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) { return null; } + const [yStr, mStr, dStr] = trimmed.split('-'); + const year = Number(yStr); + const month = Number(mStr); + const day = Number(dStr); + if (!year || month < 1 || month > 12 || day < 1 || day > 31) { return null; } + const dt = new Date(year, month - 1, day); + if (dt.getFullYear() !== year || dt.getMonth() !== month - 1 || dt.getDate() !== day) { return null; } + return { year, month, day }; + } + format(date: NgbDateStruct | null): string { + if (!date) { return ''; } + const y = String(date.year).padStart(4, '0'); + const m = String(date.month).padStart(2, '0'); + const d = String(date.day).padStart(2, '0'); + return `${y}-${m}-${d}`; + } +} + +@Injectable() +export class NgbDateStringAdapter extends NgbDateAdapter { + fromModel(value: string | null): NgbDateStruct | null { + if (!value) { return null; } + const trimmed = value.trim(); + if (!/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) { return null; } + const [yStr, mStr, dStr] = trimmed.split('-'); + const year = Number(yStr); + const month = Number(mStr); + const day = Number(dStr); + const dt = new Date(year, month - 1, day); + if (dt.getFullYear() !== year || dt.getMonth() !== month - 1 || dt.getDate() !== day) { return null; } + return { year, month, day }; + } + toModel(date: NgbDateStruct | null): string | null { + if (!date) { return null; } + const y = String(date.year).padStart(4, '0'); + const m = String(date.month).padStart(2, '0'); + const d = String(date.day).padStart(2, '0'); + return `${y}-${m}-${d}`; + } + + static isValidIso(value: string): boolean { + if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { return false; } + const [yStr, mStr, dStr] = value.split('-'); + const y = Number(yStr), m = Number(mStr), d = Number(dStr); + if (m < 1 || m > 12 || d < 1 || d > 31) { return false; } + const dt = new Date(y, m - 1, d); + return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d; + } + + static compare(a: string, b: string): number { + // returns -1 if ab + return a === b ? 0 : (a < b ? -1 : 1); + } + + static todayIso(): string { + const now = new Date(); + const y = now.getFullYear(); + const m = String(now.getMonth() + 1).padStart(2, '0'); + const d = String(now.getDate()).padStart(2, '0'); + return `${y}-${m}-${d}`; + } +} + +@Component({ + selector: 'km-date-picker', + standalone: true, + imports: [CommonModule, FormsModule, ReactiveFormsModule, NgbDatepickerModule], + templateUrl: './km-date-picker.component.html', + styleUrls: ['./km-date-picker.component.css'], + providers: [ + { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => KmDatePickerComponent), multi: true }, + { provide: NG_VALIDATORS, useExisting: forwardRef(() => KmDatePickerComponent), multi: true }, + { provide: NgbDateParserFormatter, useClass: NgbDateISOParserFormatter }, + { provide: NgbDateAdapter, useClass: NgbDateStringAdapter } + ] +}) +export class KmDatePickerComponent implements OnInit, OnDestroy, OnChanges, Validator { + @Input() placeholder: string = 'YYYY-MM-DD'; + @Input() name: string = ''; + @Input() id: string = ''; + @Input() ariaLabel: string = 'Date'; + @Input() required: boolean = false; + @Input() disabled: boolean = false; + @Input() min: string | null = null; // 'YYYY-MM-DD' + @Input() max: string | null = null; // 'YYYY-MM-DD' + @Input() invalid: boolean | null = null; // external override + @Input() showTodayShortcut: boolean = true; + @Input() showClear: boolean = true; + @Input() size: 'sm' | 'md' | 'lg' = 'md'; + @Input() coerceEmptyToToday: boolean = true; // align with original Add/Edit components + @Input() coerceInvalidToToday: boolean = true; + + @Output() valueChange = new EventEmitter(); + @Output() blur = new EventEmitter(); + @Output() focus = new EventEmitter(); + @Output() validityChange = new EventEmitter(); + + control = new FormControl(null, { nonNullable: false }); + + @ViewChild('inputEl', { static: true }) inputEl!: ElementRef; + + private onChange: (value: string | null) => void = () => {}; + private onTouched: () => void = () => {}; + + ngOnInit(): void { + this.control.valueChanges.subscribe(value => { + if (this.disabled) { + return; + } + this.onChange(value ?? null); + this.valueChange.emit(value ?? null); + this.emitValidity(); + }); + } + + ngOnChanges(changes: SimpleChanges): void { + if (Object.prototype.hasOwnProperty.call(changes, 'disabled')) { + this.setDisabledState(!!this.disabled); + } + } + + ngOnDestroy(): void {} + + // ControlValueAccessor + writeValue(value: string | null): void { + this.control.setValue(value ?? null, { emitEvent: false }); + this.emitValidity(); + } + + registerOnChange(fn: (value: string | null) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.control.disable({ emitEvent: false }); + } else { + this.control.enable({ emitEvent: false }); + } + } + + // Validator + validate(): ValidationErrors | null { + const value = this.control.value as unknown; + + // Required check: handle non-string values safely + if (this.required) { + if (value == null) { + return { required: true }; + } + if (typeof value === 'string' && value.trim() === '') { + return { required: true }; + } + } + + // If there is a value, it must be a valid ISO string + if (value != null) { + if (typeof value !== 'string') { + return { dateFormat: 'Expected YYYY-MM-DD' }; + } + if (!NgbDateStringAdapter.isValidIso(value)) { + return { dateFormat: 'Expected YYYY-MM-DD' }; + } + if (this.min && NgbDateStringAdapter.compare(value, this.min) < 0) { + return { minDate: this.min }; + } + if (this.max && NgbDateStringAdapter.compare(value, this.max) > 0) { + return { maxDate: this.max }; + } + } + + return null; + } + + // UI helpers + onBlur(): void { + this.ensureValidOnBlur(); + this.onTouched(); + this.blur.emit(); + } + + onFocus(): void { + this.focus.emit(); + } + + clear(): void { + if (this.disabled) { return; } + this.control.setValue(null); + } + + setToday(): void { + if (this.disabled) { return; } + this.control.setValue(NgbDateStringAdapter.todayIso()); + } + + get inputClasses(): string { + const sizeClass = this.size === 'sm' ? 'form-control-sm' : this.size === 'lg' ? 'form-control-lg' : ''; + const invalidClass = this.shouldShowInvalid ? 'is-invalid' : ''; + return ['form-control', sizeClass, invalidClass].filter(Boolean).join(' '); + } + + get shouldShowInvalid(): boolean { + if (this.invalid !== null) { + return !!this.invalid; + } + const errors = this.validate(); + return !!errors && (this.control.touched || this.control.dirty); + } + + private emitValidity(): void { + const valid = this.validate() === null; + this.validityChange.emit(valid); + } + + hasError(code: 'required' | 'dateFormat' | 'minDate' | 'maxDate'): boolean { + const errors = this.validate(); + return !!errors && !!(errors[code]); + } + + private ensureValidOnBlur(): void { + // Defer until after ngbDatepicker's own blur handling + setTimeout(() => { + if (this.disabled) { + return; + } + + // Prefer inspecting the raw input value for robustness + const rawVal = (this.inputEl && this.inputEl.nativeElement) ? this.inputEl.nativeElement.value : (this.control.value ?? ''); + const raw = (rawVal ?? '').toString(); + const trimmed = raw.trim(); + + const controlValue = this.control.value; + const controlEmpty = controlValue == null || (typeof controlValue === 'string' && controlValue.trim() === ''); + const rawEmpty = trimmed === ''; + + if (rawEmpty || controlEmpty) { + if (this.coerceEmptyToToday) { + this.control.setValue(NgbDateStringAdapter.todayIso()); + } else { + this.control.setValue(null); + } + return; + } + + // If user typed an invalid date string, coerce to today when enabled + const invalidRaw = !NgbDateStringAdapter.isValidIso(trimmed); + const invalidControl = typeof controlValue === 'string' && !NgbDateStringAdapter.isValidIso(controlValue); + if ((invalidRaw || invalidControl) && this.coerceInvalidToToday) { + this.control.setValue(NgbDateStringAdapter.todayIso()); + } + }, 0); + } +} + diff --git a/app/pipes/filter.pipe.ts b/app/pipes/filter.pipe.ts new file mode 100644 index 000000000..dc7b8b6b2 --- /dev/null +++ b/app/pipes/filter.pipe.ts @@ -0,0 +1,32 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'filter' +}) +export class FilterPipe implements PipeTransform { + transform(items: any[], searchText: string, property?: string): any[] { + if (!items) return []; + if (!searchText) return items; + + searchText = searchText.toLowerCase(); + + return items.filter(item => { + if (property && item[property]) { + return item[property].toLowerCase().includes(searchText); + } + if (item.indicatorName) { + return item.indicatorName.toLowerCase().includes(searchText); + } + if (item.georesourceName) { + return item.georesourceName.toLowerCase().includes(searchText); + } + if (item.datasetName) { + return item.datasetName.toLowerCase().includes(searchText); + } + if (item.topicName) { + return item.topicName.toLowerCase().includes(searchText); + } + return false; + }); + } +} \ No newline at end of file diff --git a/app/pipes/order-by.pipe.ts b/app/pipes/order-by.pipe.ts new file mode 100644 index 000000000..47d610f25 --- /dev/null +++ b/app/pipes/order-by.pipe.ts @@ -0,0 +1,25 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'orderBy' +}) +export class OrderByPipe implements PipeTransform { + transform(array: any[], field: string): any[] { + if (!Array.isArray(array)) { + return array; + } + + return array.sort((a: any, b: any) => { + const aValue = a[field]; + const bValue = b[field]; + + if (aValue < bValue) { + return -1; + } + if (aValue > bValue) { + return 1; + } + return 0; + }); + } +} \ No newline at end of file diff --git a/app/services/adminGeoresourceUnit/kommonitor-cache-helper.service.ts b/app/services/adminGeoresourceUnit/kommonitor-cache-helper.service.ts new file mode 100644 index 000000000..f7e9ec2da --- /dev/null +++ b/app/services/adminGeoresourceUnit/kommonitor-cache-helper.service.ts @@ -0,0 +1,18 @@ +import { Injectable, Inject } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class KommonitorGeoresourceCacheHelperService { + + constructor( + @Inject('kommonitorCacheHelperService') private angularJsCacheHelperService: any + ) {} + + /** + * Fetches single georesource metadata - delegates to AngularJS service + */ + async fetchSingleGeoresourceMetadata(georesourceId: string, keycloakRolesArray: string[]): Promise { + return this.angularJsCacheHelperService.fetchSingleGeoresourceMetadata(georesourceId, keycloakRolesArray); + } +} \ No newline at end of file diff --git a/app/services/adminGeoresourceUnit/kommonitor-data-exchange.service.ts b/app/services/adminGeoresourceUnit/kommonitor-data-exchange.service.ts new file mode 100644 index 000000000..e06a8f5a5 --- /dev/null +++ b/app/services/adminGeoresourceUnit/kommonitor-data-exchange.service.ts @@ -0,0 +1,120 @@ +import { Injectable, Inject } from '@angular/core'; +import { Observable, BehaviorSubject } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class KommonitorGeoresourceDataExchangeService { + // Private subjects for reactive updates if needed in the future + private georesourcesSubject = new BehaviorSubject([]); + public georesources$ = this.georesourcesSubject.asObservable(); + + constructor( + @Inject('kommonitorDataExchangeService') private angularJsDataExchangeService: any + ) {} + + /** + * Get available georesources - delegates to AngularJS service + */ + get availableGeoresources(): any[] { + return this.angularJsDataExchangeService.availableGeoresources || []; + } + + /** + * Get current Keycloak login roles - delegates to AngularJS service + */ + get currentKeycloakLoginRoles(): string[] { + return this.angularJsDataExchangeService.currentKeycloakLoginRoles || []; + } + + /** + * Check create permission - delegates to AngularJS service + */ + checkCreatePermission(): boolean { + return this.angularJsDataExchangeService.checkCreatePermission(); + } + + /** + * Check editor permission - delegates to AngularJS service + */ + checkEditorPermission(): boolean { + return this.angularJsDataExchangeService.checkEditorPermission(); + } + + /** + * Check delete permission - delegates to AngularJS service + */ + checkDeletePermission(): boolean { + return this.angularJsDataExchangeService.checkDeletePermission(); + } + + /** + * Fetch georesources metadata - delegates to AngularJS service + */ + async fetchGeoresourcesMetadata(keycloakRolesArray: string[], filter?: any): Promise { + return this.angularJsDataExchangeService.fetchGeoresourcesMetadata(keycloakRolesArray, filter); + } + + /** + * Add single georesource metadata - delegates to AngularJS service + */ + addSingleGeoresourceMetadata(georesourceMetadata: any): void { + this.angularJsDataExchangeService.addSingleGeoresourceMetadata(georesourceMetadata); + } + + /** + * Replace single georesource metadata - delegates to AngularJS service + */ + replaceSingleGeoresourceMetadata(georesourceMetadata: any): void { + this.angularJsDataExchangeService.replaceSingleGeoresourceMetadata(georesourceMetadata); + } + + /** + * Delete single georesource metadata - delegates to AngularJS service + */ + deleteSingleGeoresourceMetadata(georesourceId: string): void { + this.angularJsDataExchangeService.deleteSingleGeoresourceMetadata(georesourceId); + } + + /** + * Get georesource metadata by ID - delegates to AngularJS service + */ + getGeoresourceMetadataById(georesourceId: string): any { + return this.angularJsDataExchangeService.getGeoresourceMetadataById(georesourceId); + } + + /** + * Get base URL to KomMonitor Data API for spatial resources - delegates to AngularJS service + */ + getBaseUrlToKomMonitorDataAPI_spatialResource(): string { + return this.angularJsDataExchangeService.getBaseUrlToKomMonitorDataAPI_spatialResource() || ''; + } + + /** + * Get role title - delegates to AngularJS service + */ + getRoleTitle(roleId: string): string { + return this.angularJsDataExchangeService.getRoleTitle(roleId); + } + + /** + * Get topic hierarchy display string - delegates to AngularJS service + */ + getTopicHierarchyDisplayString(topicReference: any): string { + return this.angularJsDataExchangeService.getTopicHierarchyDisplayString(topicReference); + } + + /** + * Get all allowed roles string - delegates to AngularJS service + */ + getAllowedRolesString(permissions: any): string { + return this.angularJsDataExchangeService.getAllowedRolesString(permissions); + } + + /** + * Get LOI dash SVG from string value - delegates to AngularJS service + */ + getLoiDashSvgFromStringValue(dashArrayString: string): string { + return this.angularJsDataExchangeService.getLoiDashSvgFromStringValue(dashArrayString); + } +} \ No newline at end of file diff --git a/app/services/adminGeoresourceUnit/kommonitor-data-grid-helper.service.ts b/app/services/adminGeoresourceUnit/kommonitor-data-grid-helper.service.ts new file mode 100644 index 000000000..e05bc1c83 --- /dev/null +++ b/app/services/adminGeoresourceUnit/kommonitor-data-grid-helper.service.ts @@ -0,0 +1,798 @@ +import { Injectable } from '@angular/core'; +import { BroadcastService } from '../broadcast-service/broadcast.service'; +import { KommonitorGeoresourceDataExchangeService } from './kommonitor-data-exchange.service'; +import { AgGridAngular } from 'ag-grid-angular'; +import { + GridOptions, + ColDef, + GridApi, + ColumnApi, + ICellRendererParams, + ICellRendererComp, + GridReadyEvent, + RowSelectedEvent, + CellClickedEvent +} from 'ag-grid-community'; + +@Injectable({ + providedIn: 'root' +}) +export class KommonitorGeoresourceDataGridHelperService { + + // Grid references + private poiGrid: AgGridAngular | null = null; + private loiGrid: AgGridAngular | null = null; + private aoiGrid: AgGridAngular | null = null; + + // Component reference for callbacks + private componentRef: any = null; + + constructor( + private broadcastService: BroadcastService, + private kommonitorDataExchangeService: KommonitorGeoresourceDataExchangeService + ) {} + + /** + * Simple function-based cell renderer for edit buttons (like original) + */ + private displayEditButtons_georesources = (params: any) => { + if (!params.data || !params.data.georesourceId) { + return '
No data
'; + } + + const editMetadataButtonId = 'btn_georesource_editMetadata_' + params.data.georesourceId; + const editFeaturesButtonId = 'btn_georesource_editFeatures_' + params.data.georesourceId; + const editUserRolesButtonId = 'btn_georesource_editUserRoles_' + params.data.georesourceId; + const deleteButtonId = 'btn_georesource_deleteGeoresource_' + params.data.georesourceId; + + // Check user permissions (handle both array and potential undefined) + const userPermissions = params.data.userPermissions || []; + const hasEditorPermission = Array.isArray(userPermissions) ? + (userPermissions.includes("editor") || userPermissions.includes("creator")) : false; + const hasCreatorPermission = Array.isArray(userPermissions) ? + userPermissions.includes("creator") : false; + + let html = '
'; + html += ''; + html += ''; + html += ''; + html += ''; + html += '
'; + + return html; + } + + /** + * Initialize the grid references + */ + initializeGrids(poiGrid: AgGridAngular, loiGrid: AgGridAngular, aoiGrid: AgGridAngular): void { + this.poiGrid = poiGrid; + this.loiGrid = loiGrid; + this.aoiGrid = aoiGrid; + } + + /** + * Set component reference for callbacks + */ + setComponentRef(componentRef: any): void { + this.componentRef = componentRef; + + // Update column definitions with the component reference for all grids + this.updateColumnDefinitions(); + } + + /** + * Update column definitions with the current component reference + */ + private updateColumnDefinitions(): void { + if (this.poiGrid && this.poiGrid.api) { + const poiColumnDefs = this.getPoiColumnDefinitions(); + this.poiGrid.api.setColumnDefs(poiColumnDefs); + } + + if (this.loiGrid && this.loiGrid.api) { + const loiColumnDefs = this.getLoiColumnDefinitions(); + this.loiGrid.api.setColumnDefs(loiColumnDefs); + } + + if (this.aoiGrid && this.aoiGrid.api) { + const aoiColumnDefs = this.getAoiColumnDefinitions(); + this.aoiGrid.api.setColumnDefs(aoiColumnDefs); + } + } + + /** + * Build data grid for georesources + */ + buildDataGrid_georesources(georesourcesArray: any[]): void { + if (!georesourcesArray || georesourcesArray.length === 0) { + console.warn('No georesources data provided to buildDataGrid_georesources'); + return; + } + + if (!this.poiGrid || !this.loiGrid || !this.aoiGrid) { + console.warn('Grid references not initialized'); + return; + } + + this.buildPoiGrid(georesourcesArray); + this.buildLoiGrid(georesourcesArray); + this.buildAoiGrid(georesourcesArray); + } + + /** + * Build POI grid + */ + private buildPoiGrid(georesourcesArray: any[]): void { + if (!this.poiGrid) { + return; + } + + const poiData = georesourcesArray.filter(item => item.isPOI); + const columnDefs = this.getPoiColumnDefinitions(); + + try { + this.poiGrid.api?.setRowData(poiData); + this.poiGrid.api?.setColumnDefs(columnDefs); + + // Register click handlers after a short delay + setTimeout(() => { + this.registerClickHandler_georesources(georesourcesArray); + }, 200); + } catch (error) { + console.error('Error updating POI grid:', error); + } + } + + /** + * Build LOI grid + */ + private buildLoiGrid(georesourcesArray: any[]): void { + if (!this.loiGrid) { + return; + } + + const loiData = georesourcesArray.filter(item => item.isLOI); + const columnDefs = this.getLoiColumnDefinitions(); + + try { + this.loiGrid.api?.setRowData(loiData); + this.loiGrid.api?.setColumnDefs(columnDefs); + + // Register click handlers after a short delay + setTimeout(() => { + this.registerClickHandler_georesources(georesourcesArray); + }, 200); + } catch (error) { + console.error('Error updating LOI grid:', error); + } + } + + /** + * Build AOI grid + */ + private buildAoiGrid(georesourcesArray: any[]): void { + if (!this.aoiGrid) { + return; + } + + const aoiData = georesourcesArray.filter(item => item.isAOI); + const columnDefs = this.getAoiColumnDefinitions(); + + try { + this.aoiGrid.api?.setRowData(aoiData); + this.aoiGrid.api?.setColumnDefs(columnDefs); + + // Register click handlers after a short delay + setTimeout(() => { + this.registerClickHandler_georesources(georesourcesArray); + }, 200); + } catch (error) { + console.error('Error updating AOI grid:', error); + } + } + + /** + * Register click handlers for georesource buttons + */ + private registerClickHandler_georesources(georesourceMetadataArray: any[]): void { + // Edit Metadata Button + const editMetadataButtons = document.querySelectorAll('.georesourceEditMetadataBtn'); + editMetadataButtons.forEach((button: any) => { + button.removeEventListener('click', this.handleEditMetadataClick); + button.addEventListener('click', this.handleEditMetadataClick); + }); + + // Edit Features Button + const editFeaturesButtons = document.querySelectorAll('.georesourceEditFeaturesBtn'); + editFeaturesButtons.forEach((button: any) => { + button.removeEventListener('click', this.handleEditFeaturesClick); + button.addEventListener('click', this.handleEditFeaturesClick); + }); + + // Edit User Roles Button + const editUserRolesButtons = document.querySelectorAll('.georesourceEditUserRolesBtn'); + editUserRolesButtons.forEach((button: any) => { + button.removeEventListener('click', this.handleEditUserRolesClick); + button.addEventListener('click', this.handleEditUserRolesClick); + }); + + // Delete Button + const deleteButtons = document.querySelectorAll('.georesourceDeleteBtn'); + deleteButtons.forEach((button: any) => { + button.removeEventListener('click', this.handleDeleteClick); + button.addEventListener('click', this.handleDeleteClick); + }); + } + + /** + * Handle edit metadata button click + */ + private handleEditMetadataClick = (event: any): void => { + event.stopPropagation(); + + const georesourceId = event.target.id.split('_')[3] || event.target.closest('button').id.split('_')[3]; + const georesourceMetadata = this.kommonitorDataExchangeService.getGeoresourceMetadataById(georesourceId); + + if (this.componentRef) { + this.componentRef.onClickEditMetadata(georesourceMetadata); + } + } + + /** + * Handle edit features button click + */ + private handleEditFeaturesClick = (event: any): void => { + event.stopPropagation(); + + const georesourceId = event.target.id.split('_')[3] || event.target.closest('button').id.split('_')[3]; + const georesourceMetadata = this.kommonitorDataExchangeService.getGeoresourceMetadataById(georesourceId); + + if (this.componentRef) { + this.componentRef.onClickEditFeatures(georesourceMetadata); + } + } + + /** + * Handle edit user roles button click + */ + private handleEditUserRolesClick = (event: any): void => { + event.stopPropagation(); + + const georesourceId = event.target.id.split('_')[3] || event.target.closest('button').id.split('_')[3]; + const georesourceMetadata = this.kommonitorDataExchangeService.getGeoresourceMetadataById(georesourceId); + + if (this.componentRef) { + this.componentRef.onClickEditUserRoles(georesourceMetadata); + } + } + + /** + * Handle delete button click + */ + private handleDeleteClick = (event: any): void => { + event.stopPropagation(); + + const georesourceId = event.target.id.split('_')[3] || event.target.closest('button').id.split('_')[3]; + const georesourceMetadata = this.kommonitorDataExchangeService.getGeoresourceMetadataById(georesourceId); + + if (this.componentRef) { + this.componentRef.onClickDeleteGeoresource(georesourceMetadata); + } + } + + /** + * Get POI column definitions + */ + private getPoiColumnDefinitions(): ColDef[] { + return [ + { + headerName: 'Editierfunktionen', + pinned: 'left', + maxWidth: 200, + minWidth: 180, + checkboxSelection: false, + headerCheckboxSelection: false, + headerCheckboxSelectionFilteredOnly: true, + filter: false, + sortable: false, + cellRenderer: 'displayEditButtons_georesources' + }, + { headerName: 'Id', field: "georesourceId", pinned: 'left', maxWidth: 125 }, + { headerName: 'Name', field: "datasetName", pinned: 'left', minWidth: 300 }, + { + headerName: 'Symbolfarbe', + field: "poiSymbolColor", + maxWidth: 125, + filter: false, + sortable: false, + cellRenderer: (params: any) => { + const color = params.data.poiSymbolColor || '#000000'; + return `
${color}

`; + } + }, + { + headerName: 'Symbolname', + field: "poiSymbolBootstrap3Name", + maxWidth: 125, + cellRenderer: (params: any) => { + const symbolName = params.data.poiSymbolBootstrap3Name || 'home'; + return `${symbolName}

`; + } + }, + { + headerName: 'Markerfarbe', + field: "poiMarkerColor", + maxWidth: 125, + filter: false, + sortable: false, + cellRenderer: (params: any) => { + const color = params.data.poiMarkerColor || '#000000'; + return `
${color}

`; + } + }, + { + headerName: 'Beschreibung', + minWidth: 400, + cellRenderer: (params: any) => { + return params.data.metadata?.description || ''; + } + }, + { + headerName: 'Gültigkeitszeitraum', + minWidth: 400, + cellRenderer: (params: any) => { + let html = '
    '; + for (const periodOfValidity of params.data.availablePeriodsOfValidity || []) { + html += '
  • '; + if(periodOfValidity.endDate){ + html += "

    " + periodOfValidity.startDate + " ‐ " + periodOfValidity.endDate + "

    "; + } else { + html += "

    " + periodOfValidity.startDate + " ‐ heute

    "; + } + html += '
  • '; + } + html += '
'; + return html; + } + }, + { + headerName: 'Themenhierarchie', + minWidth: 400, + cellRenderer: (params: any) => { + return this.kommonitorDataExchangeService.getTopicHierarchyDisplayString(params.data.topicReference); + } + }, + { + headerName: 'Datenquelle', + minWidth: 400, + cellRenderer: (params: any) => { + return params.data.metadata?.datasource || ''; + } + }, + { + headerName: 'Datenhalter und Kontakt', + minWidth: 400, + cellRenderer: (params: any) => { + return params.data.metadata?.contact || ''; + } + }, + { + headerName: 'Rollen', + minWidth: 400, + cellRenderer: (params: any) => { + return this.kommonitorDataExchangeService.getAllowedRolesString(params.data.permissions); + } + }, + { + headerName: 'Öffentlich sichtbar', + minWidth: 400, + cellRenderer: (params: any) => { + return params.data.isPublic ? 'ja' : 'nein'; + } + }, + { + headerName: 'Eigentümer', + minWidth: 400, + cellRenderer: (params: any) => { + return this.kommonitorDataExchangeService.getRoleTitle(params.data.ownerId); + } + } + ]; + } + + /** + * Get LOI column definitions + */ + private getLoiColumnDefinitions(): ColDef[] { + return [ + { + headerName: 'Editierfunktionen', + pinned: 'left', + maxWidth: 200, + minWidth: 180, + checkboxSelection: false, + headerCheckboxSelection: false, + headerCheckboxSelectionFilteredOnly: true, + filter: false, + sortable: false, + cellRenderer: 'displayEditButtons_georesources' + }, + { headerName: 'Id', field: "georesourceId", pinned: 'left', maxWidth: 125 }, + { headerName: 'Name', field: "datasetName", pinned: 'left', minWidth: 300 }, + { + headerName: 'Linienfarbe', + field: "loiColor", + maxWidth: 125, + filter: false, + sortable: false, + cellRenderer: (params: any) => { + const color = params.data.loiColor || '#000000'; + return `
${color}

`; + } + }, + { headerName: 'Linienbreite', field: "loiWidth", maxWidth: 125 }, + { + headerName: 'Linienmuster', + field: "loiDashArrayString", + maxWidth: 125, + filter: false, + sortable: false, + cellRenderer: (params: any) => { + return this.kommonitorDataExchangeService.getLoiDashSvgFromStringValue(params.data.loiDashArrayString); + } + }, + { + headerName: 'Beschreibung', + minWidth: 400, + cellRenderer: (params: any) => { + return params.data.metadata?.description || ''; + } + }, + { + headerName: 'Gültigkeitszeitraum', + minWidth: 400, + cellRenderer: (params: any) => { + let html = '
    '; + for (const periodOfValidity of params.data.availablePeriodsOfValidity || []) { + html += '
  • '; + if(periodOfValidity.endDate){ + html += "

    " + periodOfValidity.startDate + " ‐ " + periodOfValidity.endDate + "

    "; + } else { + html += "

    " + periodOfValidity.startDate + " ‐ heute

    "; + } + html += '
  • '; + } + html += '
'; + return html; + } + }, + { + headerName: 'Themenhierarchie', + minWidth: 400, + cellRenderer: (params: any) => { + return this.kommonitorDataExchangeService.getTopicHierarchyDisplayString(params.data.topicReference); + } + }, + { + headerName: 'Datenquelle', + minWidth: 400, + cellRenderer: (params: any) => { + return params.data.metadata?.datasource || ''; + } + }, + { + headerName: 'Datenhalter und Kontakt', + minWidth: 400, + cellRenderer: (params: any) => { + return params.data.metadata?.contact || ''; + } + }, + { + headerName: 'Rollen', + minWidth: 400, + cellRenderer: (params: any) => { + return this.kommonitorDataExchangeService.getAllowedRolesString(params.data.permissions); + } + }, + { + headerName: 'Öffentlich sichtbar', + minWidth: 400, + cellRenderer: (params: any) => { + return params.data.isPublic ? 'ja' : 'nein'; + } + }, + { + headerName: 'Eigentümer', + minWidth: 400, + cellRenderer: (params: any) => { + return this.kommonitorDataExchangeService.getRoleTitle(params.data.ownerId); + } + } + ]; + } + + /** + * Get AOI column definitions + */ + private getAoiColumnDefinitions(): ColDef[] { + return [ + { + headerName: 'Editierfunktionen', + pinned: 'left', + maxWidth: 200, + minWidth: 180, + checkboxSelection: false, + headerCheckboxSelection: false, + headerCheckboxSelectionFilteredOnly: true, + filter: false, + sortable: false, + cellRenderer: 'displayEditButtons_georesources' + }, + { headerName: 'Id', field: "georesourceId", pinned: 'left', maxWidth: 125 }, + { headerName: 'Name', field: "datasetName", pinned: 'left', minWidth: 300 }, + { + headerName: 'Polygonfarbe', + field: "aoiColor", + maxWidth: 125, + filter: false, + sortable: false, + cellRenderer: (params: any) => { + const color = params.data.aoiColor || '#000000'; + return `
${color}

`; + } + }, + { + headerName: 'Beschreibung', + minWidth: 400, + cellRenderer: (params: any) => { + return params.data.metadata?.description || ''; + } + }, + { + headerName: 'Gültigkeitszeitraum', + minWidth: 400, + cellRenderer: (params: any) => { + let html = '
    '; + for (const periodOfValidity of params.data.availablePeriodsOfValidity || []) { + html += '
  • '; + if(periodOfValidity.endDate){ + html += "

    " + periodOfValidity.startDate + " ‐ " + periodOfValidity.endDate + "

    "; + } else { + html += "

    " + periodOfValidity.startDate + " ‐ heute

    "; + } + html += '
  • '; + } + html += '
'; + return html; + } + }, + { + headerName: 'Themenhierarchie', + minWidth: 400, + cellRenderer: (params: any) => { + return this.kommonitorDataExchangeService.getTopicHierarchyDisplayString(params.data.topicReference); + } + }, + { + headerName: 'Datenquelle', + minWidth: 400, + cellRenderer: (params: any) => { + return params.data.metadata?.datasource || ''; + } + }, + { + headerName: 'Datenhalter und Kontakt', + minWidth: 400, + cellRenderer: (params: any) => { + return params.data.metadata?.contact || ''; + } + }, + { + headerName: 'Rollen', + minWidth: 400, + cellRenderer: (params: any) => { + return this.kommonitorDataExchangeService.getAllowedRolesString(params.data.permissions); + } + }, + { + headerName: 'Öffentlich sichtbar', + minWidth: 400, + cellRenderer: (params: any) => { + return params.data.isPublic ? 'ja' : 'nein'; + } + }, + { + headerName: 'Eigentümer', + minWidth: 400, + cellRenderer: (params: any) => { + return this.kommonitorDataExchangeService.getRoleTitle(params.data.ownerId); + } + } + ]; + } + + /** + * Get selected georesources metadata from all grids + */ + getSelectedGeoresourcesMetadata(): any[] { + const selectedRows: any[] = []; + + if (this.poiGrid?.api) { + selectedRows.push(...this.poiGrid.api.getSelectedRows()); + } + if (this.loiGrid?.api) { + selectedRows.push(...this.loiGrid.api.getSelectedRows()); + } + if (this.aoiGrid?.api) { + selectedRows.push(...this.aoiGrid.api.getSelectedRows()); + } + + return selectedRows; + } + + /** + * Get current timestamp string + */ + getCurrentTimestampString(): string { + return new Date().toISOString(); + } + + /** + * Clear all grid selections + */ + clearAllSelections(): void { + this.poiGrid?.api?.deselectAll(); + this.loiGrid?.api?.deselectAll(); + this.aoiGrid?.api?.deselectAll(); + } + + /** + * Refresh all grids + */ + refreshAllGrids(): void { + this.poiGrid?.api?.refreshCells(); + this.loiGrid?.api?.refreshCells(); + this.aoiGrid?.api?.refreshCells(); + } + + /** + * Export grid data to CSV + */ + exportToCsv(gridType: 'poi' | 'loi' | 'aoi'): void { + let gridApi: GridApi | undefined = undefined; + + switch (gridType) { + case 'poi': + gridApi = this.poiGrid?.api; + break; + case 'loi': + gridApi = this.loiGrid?.api; + break; + case 'aoi': + gridApi = this.aoiGrid?.api; + break; + } + + if (gridApi) { + gridApi.exportDataAsCsv({ + fileName: `georesources_${gridType}_${this.getCurrentTimestampString()}.csv` + }); + } + } + + /** + * Get grid options for POI grid (for ag-grid-angular) + */ + getPoiGridOptions(): any { + return { + components: { + displayEditButtons_georesources: this.displayEditButtons_georesources + }, + defaultColDef: { + editable: false, + sortable: true, + filter: true, + floatingFilter: true, + resizable: true, + wrapText: true, + autoHeight: true + }, + suppressRowClickSelection: true, + rowSelection: 'multiple', + enableCellTextSelection: true, + ensureDomOrder: true, + pagination: true, + paginationPageSize: 10, + suppressColumnVirtualisation: true, + onModelUpdated: () => { + setTimeout(() => { + this.registerClickHandler_georesources([]); + }, 100); + }, + onRowDataChanged: () => { + setTimeout(() => { + this.registerClickHandler_georesources([]); + }, 100); + } + }; + } + + /** + * Get grid options for LOI grid (for ag-grid-angular) + */ + getLoiGridOptions(): any { + return { + components: { + displayEditButtons_georesources: this.displayEditButtons_georesources + }, + defaultColDef: { + editable: false, + sortable: true, + filter: true, + floatingFilter: true, + resizable: true, + wrapText: true, + autoHeight: true + }, + suppressRowClickSelection: true, + rowSelection: 'multiple', + enableCellTextSelection: true, + ensureDomOrder: true, + pagination: true, + paginationPageSize: 10, + suppressColumnVirtualisation: true, + onModelUpdated: () => { + setTimeout(() => { + this.registerClickHandler_georesources([]); + }, 100); + }, + onRowDataChanged: () => { + setTimeout(() => { + this.registerClickHandler_georesources([]); + }, 100); + } + }; + } + + /** + * Get grid options for AOI grid (for ag-grid-angular) + */ + getAoiGridOptions(): any { + return { + components: { + displayEditButtons_georesources: this.displayEditButtons_georesources + }, + defaultColDef: { + editable: false, + sortable: true, + filter: true, + floatingFilter: true, + resizable: true, + wrapText: true, + autoHeight: true + }, + suppressRowClickSelection: true, + rowSelection: 'multiple', + enableCellTextSelection: true, + ensureDomOrder: true, + pagination: true, + paginationPageSize: 10, + suppressColumnVirtualisation: true, + onModelUpdated: () => { + setTimeout(() => { + this.registerClickHandler_georesources([]); + }, 100); + }, + onRowDataChanged: () => { + setTimeout(() => { + this.registerClickHandler_georesources([]); + }, 100); + } + }; + } +} \ No newline at end of file diff --git a/app/services/adminIndicatorUnit/kommonitor-cache-helper.service.ts b/app/services/adminIndicatorUnit/kommonitor-cache-helper.service.ts new file mode 100644 index 000000000..1f6feae81 --- /dev/null +++ b/app/services/adminIndicatorUnit/kommonitor-cache-helper.service.ts @@ -0,0 +1,18 @@ +import { Injectable, Inject } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class KommonitorIndicatorCacheHelperService { + + constructor( + @Inject('kommonitorCacheHelperService') private angularJsCacheHelperService: any + ) {} + + /** + * Fetches single indicator metadata - delegates to AngularJS service + */ + async fetchSingleIndicatorMetadata(indicatorId: string, keycloakRolesArray: string[]): Promise { + return this.angularJsCacheHelperService.fetchSingleIndicatorMetadata(indicatorId, keycloakRolesArray); + } +} \ No newline at end of file diff --git a/app/services/adminIndicatorUnit/kommonitor-data-exchange.service.ts b/app/services/adminIndicatorUnit/kommonitor-data-exchange.service.ts new file mode 100644 index 000000000..23bb16fe6 --- /dev/null +++ b/app/services/adminIndicatorUnit/kommonitor-data-exchange.service.ts @@ -0,0 +1,241 @@ +import { Injectable, Inject } from '@angular/core'; +import { Observable, BehaviorSubject } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class KommonitorIndicatorDataExchangeService { + // Private subjects for reactive updates if needed in the future + private indicatorsSubject = new BehaviorSubject([]); + public indicators$ = this.indicatorsSubject.asObservable(); + + constructor( + @Inject('kommonitorDataExchangeService') private angularJsDataExchangeService: any + ) {} + + /** + * Get available indicators - delegates to AngularJS service + */ + get availableIndicators(): any[] { + console.log('KommonitorIndicatorDataExchangeService.availableIndicators called'); + console.log('angularJsDataExchangeService:', this.angularJsDataExchangeService); + console.log('angularJsDataExchangeService.availableIndicators:', this.angularJsDataExchangeService?.availableIndicators); + return this.angularJsDataExchangeService?.availableIndicators || []; + } + + /** + * Get available spatial units - delegates to AngularJS service + */ + get availableSpatialUnits(): any[] { + return this.angularJsDataExchangeService.availableSpatialUnits || []; + } + + /** + * Get available georesources - delegates to AngularJS service + */ + get availableGeoresources(): any[] { + return this.angularJsDataExchangeService.availableGeoresources || []; + } + + /** + * Get available topics - delegates to AngularJS service + */ + get availableTopics(): any[] { + return this.angularJsDataExchangeService.availableTopics || []; + } + + /** + * Get topic indicator hierarchy for order view - delegates to AngularJS service + */ + get topicIndicatorHierarchy_forOrderView(): any[] { + return this.angularJsDataExchangeService.topicIndicatorHierarchy_forOrderView || []; + } + + /** + * Get access control - delegates to AngularJS service + */ + get accessControl(): any[] { + return this.angularJsDataExchangeService.accessControl || []; + } + + /** + * Get update interval options - delegates to AngularJS service + */ + get updateIntervalOptions(): any[] { + return this.angularJsDataExchangeService.updateIntervalOptions || []; + } + + /** + * Get indicator type options - delegates to AngularJS service + */ + get indicatorTypeOptions(): any[] { + return this.angularJsDataExchangeService.indicatorTypeOptions || []; + } + + /** + * Get indicator unit options - delegates to AngularJS service + */ + get indicatorUnitOptions(): any[] { + return this.angularJsDataExchangeService.indicatorUnitOptions || []; + } + + /** + * Get indicator creation type options - delegates to AngularJS service + */ + get indicatorCreationTypeOptions(): any[] { + return this.angularJsDataExchangeService.indicatorCreationTypeOptions || []; + } + + /** + * Get enable Keycloak security flag - delegates to AngularJS service + */ + get enableKeycloakSecurity(): boolean { + return this.angularJsDataExchangeService.enableKeycloakSecurity || false; + } + + /** + * Get current Keycloak login roles - delegates to AngularJS service + */ + get currentKeycloakLoginRoles(): string[] { + return this.angularJsDataExchangeService.currentKeycloakLoginRoles || []; + } + + /** + * Get base URL to KomMonitor Data API - delegates to AngularJS service + */ + get baseUrlToKomMonitorDataAPI(): string { + return this.angularJsDataExchangeService.baseUrlToKomMonitorDataAPI || ''; + } + + /** + * Fetches indicators metadata - delegates to AngularJS service + */ + async fetchIndicatorsMetadata(keycloakRolesArray: string[]): Promise { + return this.angularJsDataExchangeService.fetchIndicatorsMetadata(keycloakRolesArray); + } + + /** + * Adds a single indicator metadata - delegates to AngularJS service + */ + addSingleIndicatorMetadata(indicatorMetadata: any): void { + this.angularJsDataExchangeService.addSingleIndicatorMetadata(indicatorMetadata); + // Emit the updated data for any reactive components + this.indicatorsSubject.next(this.availableIndicators); + } + + /** + * Replaces a single indicator metadata - delegates to AngularJS service + */ + replaceSingleIndicatorMetadata(indicatorMetadata: any): void { + this.angularJsDataExchangeService.replaceSingleIndicatorMetadata(indicatorMetadata); + // Emit the updated data for any reactive components + this.indicatorsSubject.next(this.availableIndicators); + } + + /** + * Deletes a single indicator metadata - delegates to AngularJS service + */ + deleteSingleIndicatorMetadata(indicatorId: string): void { + this.angularJsDataExchangeService.deleteSingleIndicatorMetadata(indicatorId); + // Emit the updated data for any reactive components + this.indicatorsSubject.next(this.availableIndicators); + } + + /** + * Gets indicator metadata by ID - delegates to AngularJS service + */ + getIndicatorMetadataById(indicatorId: string): any { + return this.angularJsDataExchangeService.getIndicatorMetadataById(indicatorId); + } + + /** + * Gets georesource metadata by ID - delegates to AngularJS service + */ + getGeoresourceMetadataById(georesourceId: string): any { + return this.angularJsDataExchangeService.getGeoresourceMetadataById(georesourceId); + } + + /** + * Gets topic hierarchy for topic ID - delegates to AngularJS service + */ + getTopicHierarchyForTopicId(topicId: string): any { + return this.angularJsDataExchangeService.getTopicHierarchyForTopicId(topicId); + } + + /** + * Gets spatial unit metadata by ID - delegates to AngularJS service + */ + getSpatialUnitMetadataById(spatialUnitId: string): any { + return this.angularJsDataExchangeService.getSpatialUnitMetadataById(spatialUnitId); + } + + /** + * Checks if the current user has create permissions - delegates to AngularJS service + */ + checkCreatePermission(): boolean { + return this.angularJsDataExchangeService.checkCreatePermission(); + } + + /** + * Checks if the current user has editor permissions - delegates to AngularJS service + */ + checkEditorPermission(): boolean { + return this.angularJsDataExchangeService.checkEditorPermission(); + } + + /** + * Checks if the current user has delete permissions - delegates to AngularJS service + */ + checkDeletePermission(): boolean { + return this.angularJsDataExchangeService.checkDeletePermission(); + } + + /** + * Sets the current Keycloak login roles - delegates to AngularJS service + */ + setCurrentKeycloakLoginRoles(roles: string[]): void { + this.angularJsDataExchangeService.currentKeycloakLoginRoles = roles; + } + + /** + * Display map application error - delegates to AngularJS service + */ + displayMapApplicationError(error: any): void { + this.angularJsDataExchangeService.displayMapApplicationError(error); + } + + /** + * Get all allowed roles string - delegates to AngularJS service + */ + getAllowedRolesString(permissions: any): string { + return this.angularJsDataExchangeService.getAllowedRolesString(permissions); + } + + /** + * Get role title - delegates to AngularJS service + */ + getRoleTitle(roleId: string): string { + return this.angularJsDataExchangeService.getRoleTitle(roleId); + } + + /** + * Get indicator string from indicator type - delegates to AngularJS service + */ + getIndicatorStringFromIndicatorType(indicatorType: any): string { + return this.angularJsDataExchangeService.getIndicatorStringFromIndicatorType(indicatorType); + } + + /** + * Get topic hierarchy display string - delegates to AngularJS service + */ + getTopicHierarchyDisplayString(topicReference: any): string { + return this.angularJsDataExchangeService.getTopicHierarchyDisplayString(topicReference); + } + + /** + * Syntax highlight JSON - delegates to AngularJS service + */ + syntaxHighlightJSON(json: any): string { + return this.angularJsDataExchangeService.syntaxHighlightJSON(json); + } +} \ No newline at end of file diff --git a/app/services/adminIndicatorUnit/kommonitor-data-grid-helper.service.ts b/app/services/adminIndicatorUnit/kommonitor-data-grid-helper.service.ts new file mode 100644 index 000000000..455ec82f2 --- /dev/null +++ b/app/services/adminIndicatorUnit/kommonitor-data-grid-helper.service.ts @@ -0,0 +1,399 @@ +import { Injectable, Inject } from '@angular/core'; +import { ColDef } from 'ag-grid-community'; +import { KommonitorIndicatorDataExchangeService } from './kommonitor-data-exchange.service'; + +declare const $: any; +declare const MathJax: any; + +@Injectable({ + providedIn: 'root' +}) +export class KommonitorIndicatorDataGridHelperService { + + + + constructor( + @Inject('kommonitorDataExchangeService') private angularJsDataExchangeService: any + ) {} + + /** + * Builds data grid for indicators - now returns column definitions and row data for AG Grid Angular + */ + buildDataGrid_indicators(indicatorMetadataArray: any[]): { columnDefs: ColDef[], rowData: any[] } { + const columnDefs = this.buildDataGridColumnConfig_indicators(indicatorMetadataArray); + const rowData = this.buildDataGridRowData_indicators(indicatorMetadataArray); + + return { columnDefs, rowData }; + } + + + + /** + * Builds column configuration for indicators + */ + buildDataGridColumnConfig_indicators(indicatorMetadataArray: any[]): any[] { + const columnDefs = [ + { + headerName: 'Editierfunktionen', + pinned: 'left', + maxWidth: 150, + checkboxSelection: false, + filter: false, + sortable: false, + cellRenderer: (params: any) => this.displayEditButtons_indicators(params) + }, + { headerName: 'Id', field: "indicatorId", pinned: 'left', maxWidth: 125 }, + { headerName: 'Name', field: "indicatorName", pinned: 'left', minWidth: 300 }, + { headerName: 'Einheit', field: "unit", minWidth: 200 }, + { + headerName: 'Beschreibung', + minWidth: 400, + cellRenderer: (params: any) => { + return params.data.metadata.description; + }, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => { + return "" + params.data.metadata.description; + } + }, + { + headerName: 'Methodik', + minWidth: 400, + cellRenderer: (params: any) => { + if(params.data.processDescription && params.data.processDescription.includes("$$")){ + let splitArray = params.data.processDescription.split("$$"); + for (let index = 0; index < splitArray.length; index++) { + if((index % 2) == 0){ + params.data.processDescription += "
"; + } + } + } + return params.data.processDescription; + }, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => { + return "" + params.data.processDescription; + } + }, + { + headerName: 'Verfügbare Raumebenen', + field: "applicableSpatialUnits", + minWidth: 400, + cellRenderer: (params: any) => { + let html = '
    '; + for (const applicableSpatialUnit of params.data.applicableSpatialUnits) { + html += '
  • '; + html += applicableSpatialUnit.spatialUnitName; + html += '
  • '; + } + html += '
'; + return html; + }, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => { + if (params.data.applicableSpatialUnits && params.data.applicableSpatialUnits.length > 1){ + return "" + JSON.stringify(params.data.applicableSpatialUnits); + } + return params.data.applicableSpatialUnits; + } + }, + { + headerName: 'Verfügbare Zeitschnitte', + field: "applicableDates", + minWidth: 400, + cellRenderer: (params: any) => { + let html = '
    '; + for (const timestamp of params.data.applicableDates) { + html += '
  • '; + html += timestamp; + html += '
  • '; + } + html += '
'; + return html; + }, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => { + if (params.data.applicableDates && params.data.applicableDates.length > 1){ + return "" + JSON.stringify(params.data.applicableDates); + } + return params.data.applicableDates; + } + }, + { headerName: 'Kürzel', field: "abbreviation" }, + { headerName: 'Leitindikator', field: "isHeadlineIndicator" }, + { + headerName: 'Indikator-Typ', + minWidth: 200, + cellRenderer: (params: any) => { + return this.angularJsDataExchangeService.getIndicatorStringFromIndicatorType(params.data.indicatorType); + }, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => { + return "" + this.angularJsDataExchangeService.getIndicatorStringFromIndicatorType(params.data.indicatorType); + } + }, + { headerName: 'Merkmal', field: "characteristicValue", minWidth: 200 }, + { headerName: 'Art der Fortführung', field: "creationType", minWidth: 200 }, + { headerName: 'Tags/Stichworte', field: "tags", minWidth: 250 }, + { + headerName: 'Themenhierarchie', + minWidth: 400, + cellRenderer: (params: any) => { + return this.angularJsDataExchangeService.getTopicHierarchyDisplayString(params.data.topicReference); + }, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => { + return "" + this.angularJsDataExchangeService.getTopicHierarchyDisplayString(params.data.topicReference); + } + }, + { + headerName: 'Datenquelle', + minWidth: 400, + cellRenderer: (params: any) => { + return params.data.metadata.datasource; + }, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => { + return "" + params.data.metadata.datasource; + } + }, + { + headerName: 'Datenhalter und Kontakt', + minWidth: 400, + cellRenderer: (params: any) => { + return params.data.metadata.contact; + }, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => { + return "" + params.data.metadata.contact; + } + }, + { + headerName: 'Rollen', + minWidth: 400, + cellRenderer: (params: any) => { + return this.angularJsDataExchangeService.getAllowedRolesString(params.data.permissions); + }, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => { + return "" + this.angularJsDataExchangeService.getAllowedRolesString(params.data.permissions); + } + }, + { + headerName: 'Öffentlich sichtbar', + minWidth: 400, + cellRenderer: (params: any) => { + return params.data.isPublic ? 'ja' : 'nein'; + }, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => { + return "" + (params.data.isPublic ? 'ja' : 'nein'); + } + }, + { + headerName: 'Eigentümer', + minWidth: 400, + cellRenderer: (params: any) => { + return this.angularJsDataExchangeService.getRoleTitle(params.data.ownerId); + }, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => { + return "" + this.angularJsDataExchangeService.getRoleTitle(params.data.ownerId); + } + }, + { + headerName: 'Nachkommastellen', + minWidth: 200, + cellRenderer: (params: any) => { + return params.data.precision; + }, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => { + return "" + params.data.precision; + } + } + ]; + + return columnDefs; + } + + /** + * Builds row data for indicators + */ + buildDataGridRowData_indicators(indicatorMetadataArray: any[]): any[] { + return indicatorMetadataArray; + } + + /** + * Display edit buttons component for indicators + */ + displayEditButtons_indicators = (params: any) => { + // Safety check for data + if (!params.data || !params.data.indicatorId) { + return '
No data
'; + } + + let disabledEditButtons = !(params.data.userPermissions && Array.isArray(params.data.userPermissions) && params.data.userPermissions.includes("editor")); + let editMetadataButtonId = 'btn_indicator_editMetadata_' + params.data.indicatorId; + let editFeaturesButtonId = 'btn_indicator_editFeatures_' + params.data.indicatorId; + + let html = '
'; + html += ''; + html += ''; + + if(!disabledEditButtons){ + html = html.replaceAll("disabled", ""); // enabled + } + + if (this.angularJsDataExchangeService.enableKeycloakSecurity) { + let disabled = !(params.data.userPermissions && Array.isArray(params.data.userPermissions) && params.data.userPermissions.includes("creator")); + html += '`; + } catch { return 'Dieser Job umfasst keine Logs'; } + }; + } + + private renderDefaultJobSummary(params: any): string { + try { + const summary = params?.data?.spatialUnitIntegrationSummary; + if (!summary || summary.length === 0) { + return 'Dieser Job umfasst keine Informationen zur erfolgreichen/gescheiterten Datenintegration'; + } + let html = ''; + for (const item of summary) { + html += ''; + html += ``; + html += ``; + html += ``; + html += ``; + html += ``; + if (item.errorsOccurred && item.errorsOccurred.length > 0) { + html += ``; + } else { + html += ''; + } + html += ''; + } + html += '
Raumebenen-IdRaumebenen-NameAnzahl integrierter Indikatoren-FeaturesAnzahl integrierter Zeitstempelintegrierte ZeitstempelFehlermeldung
${item.spatialUnitId}${item.spatialUnitName}${item.numberOfIntegratedIndicatorFeatures}${item.numberOfIntegratedTargetDates}${item.integratedTargetDates}${this.dataExchange.syntaxHighlightJSON(item.errorsOccurred)}keine Fehlermeldungen vorhanden
'; + return html; + } catch { return ''; } + } +} + + diff --git a/app/services/adminSpatialUnit/kommonitor-cache-helper.service.ts b/app/services/adminSpatialUnit/kommonitor-cache-helper.service.ts new file mode 100644 index 000000000..987694e09 --- /dev/null +++ b/app/services/adminSpatialUnit/kommonitor-cache-helper.service.ts @@ -0,0 +1,427 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { + Observable, + BehaviorSubject, + throwError, + of, + timer, + catchError, + retry, + shareReplay, + switchMap, + tap, + map +} from 'rxjs'; + +// TypeScript interfaces for better type safety +export interface DatabaseModificationInfo { + 'access-control': string; + 'topics': string; + 'spatial-units': string; + 'georesources': string; + 'indicators': string; + 'process-scripts': string; +} + +export interface CacheEntry { + data: T; + timestamp: string; + lastModified: string; +} + +export interface SpatialUnitMetadata { + spatialUnitId: string; + spatialUnitLevel: string; + metadata: { + description: string; + datasource: string; + contact: string; + note?: string; + literature?: string; + updateInterval?: string; + lastUpdate?: string; + databasis?: string; + sridEPSG?: number; + }; + nextLowerHierarchyLevel?: string; + nextUpperHierarchyLevel?: string; + availablePeriodsOfValidity: Array<{ + startDate: string; + endDate?: string; + }>; + permissions: string[]; + isPublic: boolean; + ownerId: string; + userPermissions?: string[]; +} + +@Injectable({ + providedIn: 'root' +}) +export class KommonitorCacheHelperService { + private baseUrlToKomMonitorDataAPI: string = ''; + private lastDatabaseModificationInfo: DatabaseModificationInfo | null = null; + + // Endpoints + private spatialUnitsPublicEndpoint = '/public/spatial-units'; + private spatialUnitsProtectedEndpoint = '/spatial-units'; + private spatialUnitsEndpoint = this.spatialUnitsProtectedEndpoint; + + // Local storage keys + private localStorageKey_prefix: string = ''; + private localStorageKey_spatialUnits: string = ''; + + // Reactive subjects for state management + private spatialUnitsSubject = new BehaviorSubject([]); + private loadingSubject = new BehaviorSubject(false); + private errorSubject = new BehaviorSubject(null); + private lastModificationSubject = new BehaviorSubject(null); + + // Public observables + public spatialUnits$ = this.spatialUnitsSubject.asObservable(); + public loading$ = this.loadingSubject.asObservable(); + public error$ = this.errorSubject.asObservable(); + public lastModification$ = this.lastModificationSubject.asObservable(); + + constructor(private http: HttpClient) { + this.initializeService(); + } + + /** + * Initialize the service with configuration + */ + private initializeService(): void { + // Get configuration from environment + const env = (window as any).__env; + this.baseUrlToKomMonitorDataAPI = env?.apiUrl + env?.basePath || ''; + this.localStorageKey_prefix = env?.localStoragePrefix || 'kommonitor'; + this.localStorageKey_spatialUnits = this.localStorageKey_prefix + '_lastModification_spatialUnits'; + + console.log('KommonitorCacheHelperService initialized with base URL:', this.baseUrlToKomMonitorDataAPI); + + // Check authentication and set appropriate endpoints + this.checkAuthentication(); + + // Fetch initial database modification info + this.fetchLastDatabaseModificationObject(); + } + + /** + * Check authentication status and set appropriate endpoints + */ + private checkAuthentication(): void { + // This would integrate with your authentication service + // For now, we'll assume authenticated and use protected endpoints + const isAuthenticated = this.isUserAuthenticated(); + + if (isAuthenticated) { + this.spatialUnitsEndpoint = this.spatialUnitsProtectedEndpoint; + } else { + this.spatialUnitsEndpoint = this.spatialUnitsPublicEndpoint; + } + + console.log('Authentication check completed. Using endpoint:', this.spatialUnitsEndpoint); + } + + /** + * Check if user is authenticated + * This is a placeholder method that should integrate with your auth service + */ + private isUserAuthenticated(): boolean { + // This would integrate with your authentication service (Keycloak, etc.) + // For now, return true as a placeholder + return true; + } + + /** + * Fetch last database modification info from server + */ + private fetchLastDatabaseModificationObject(): Observable { + const url = `${this.baseUrlToKomMonitorDataAPI}/public/database/last-modification`; + + return this.http.get(url).pipe( + tap(info => { + this.lastDatabaseModificationInfo = info; + this.lastModificationSubject.next(info); + console.log('Database modification info fetched:', info); + }), + catchError(this.handleError) + ); + } + + /** + * Fetch spatial units metadata with caching + */ + fetchSpatialUnitsMetadata(keycloakRolesArray: string[]): Observable { + console.log('Fetching spatial units metadata with roles:', keycloakRolesArray); + + // Check cache first + const cachedData = this.getCachedSpatialUnits(keycloakRolesArray); + if (cachedData) { + console.log('Returning cached spatial units data'); + this.spatialUnitsSubject.next(cachedData); + return of(cachedData); + } + + // Fetch from server + console.log('Cache miss, fetching from server...'); + this.setLoading(true); + this.clearError(); + + return this.fetchResourceFromServer( + this.localStorageKey_spatialUnits, + this.spatialUnitsEndpoint, + 'spatial-units', + keycloakRolesArray + ).pipe( + tap((data: SpatialUnitMetadata[]) => { + this.spatialUnitsSubject.next(data); + this.setLoading(false); + console.log('Spatial units data fetched from server:', data.length, 'items'); + }), + catchError(error => { + this.setError(error); + this.setLoading(false); + return throwError(() => error); + }) + ); + } + + /** + * Fetch single spatial unit metadata + */ + fetchSingleSpatialUnitMetadata(spatialUnitId: string, keycloakRolesArray: string[]): Observable { + const url = `${this.baseUrlToKomMonitorDataAPI}${this.spatialUnitsEndpoint}/${spatialUnitId}`; + + return this.http.get(url).pipe( + tap(() => { + // Refresh the full list in the background + this.fetchSpatialUnitsMetadata(keycloakRolesArray).subscribe(); + }), + catchError(this.handleError) + ); + } + + /** + * Fetch resource from server with optional filtering + */ + private fetchResourceFromServer( + localStorageKey: string, + resourceEndpoint: string, + lastModificationResourceName: string, + keycloakRolesArray: string[], + filter?: any + ): Observable { + const url = `${this.baseUrlToKomMonitorDataAPI}${resourceEndpoint}`; + + if (filter) { + // POST request with filter + return this.http.post(`${url}/filter`, filter).pipe( + tap((data: T[]) => this.updateCache(localStorageKey, data, lastModificationResourceName, keycloakRolesArray)), + catchError(this.handleError) + ); + } else { + // Standard GET request + return this.http.get(url).pipe( + tap((data: T[]) => this.updateCache(localStorageKey, data, lastModificationResourceName, keycloakRolesArray)), + catchError(this.handleError) + ); + } + } + + /** + * Get cached spatial units data + */ + private getCachedSpatialUnits(keycloakRolesArray: string[]): SpatialUnitMetadata[] | null { + if (!this.lastDatabaseModificationInfo) { + return null; + } + + const { timestampKey, metadataKey } = this.getCacheKeys(keycloakRolesArray); + + const cachedTimestamp = localStorage.getItem(timestampKey); + if (!cachedTimestamp) { + return null; + } + + const cachedLastModified = JSON.parse(cachedTimestamp); + const serverLastModified = this.lastDatabaseModificationInfo['spatial-units']; + + if (cachedLastModified !== serverLastModified) { + console.log('Cache invalid - timestamps differ'); + return null; + } + + const cachedData = localStorage.getItem(metadataKey); + if (!cachedData) { + return null; + } + + try { + const parsedData = JSON.parse(cachedData); + console.log('Valid cache found, returning cached data'); + return parsedData; + } catch (error) { + console.error('Error parsing cached data:', error); + return null; + } + } + + /** + * Update cache with new data + */ + private updateCache( + localStorageKey: string, + data: T[], + lastModificationResourceName: string, + keycloakRolesArray: string[] + ): void { + if (!this.lastDatabaseModificationInfo) { + return; + } + + const { timestampKey, metadataKey } = this.getCacheKeys(keycloakRolesArray); + + // Store timestamp + const timestamp = this.lastDatabaseModificationInfo[lastModificationResourceName as keyof DatabaseModificationInfo]; + localStorage.setItem(timestampKey, JSON.stringify(timestamp)); + + // Store data + localStorage.setItem(metadataKey, JSON.stringify(data)); + + console.log('Cache updated for', lastModificationResourceName); + } + + /** + * Get cache keys based on roles + */ + private getCacheKeys(keycloakRolesArray: string[]): { timestampKey: string; metadataKey: string } { + const env = (window as any).__env; + let suffix = '_public'; + + if (keycloakRolesArray && keycloakRolesArray.length > 0) { + if (keycloakRolesArray.includes(env?.keycloakKomMonitorAdminRoleName)) { + suffix = '_' + env.keycloakKomMonitorAdminRoleName; + } else { + suffix = '_' + JSON.stringify(keycloakRolesArray); + } + } + + const timestampKey = this.localStorageKey_spatialUnits + '_timestamp' + suffix; + const metadataKey = this.localStorageKey_spatialUnits + '_metadata' + suffix; + + return { timestampKey, metadataKey }; + } + + /** + * Clear cache for spatial units + */ + clearSpatialUnitsCache(keycloakRolesArray: string[]): void { + const { timestampKey, metadataKey } = this.getCacheKeys(keycloakRolesArray); + localStorage.removeItem(timestampKey); + localStorage.removeItem(metadataKey); + console.log('Spatial units cache cleared'); + } + + /** + * Clear all cache + */ + clearAllCache(): void { + const keys = Object.keys(localStorage); + const cacheKeys = keys.filter(key => key.startsWith(this.localStorageKey_prefix)); + cacheKeys.forEach(key => localStorage.removeItem(key)); + console.log('All cache cleared'); + } + + /** + * Get current spatial units data + */ + get availableSpatialUnits(): SpatialUnitMetadata[] { + return this.spatialUnitsSubject.value; + } + + /** + * Get current loading state + */ + get isLoading(): boolean { + return this.loadingSubject.value; + } + + /** + * Get current error state + */ + get currentError(): string | null { + return this.errorSubject.value; + } + + /** + * Get base URL + */ + get baseUrl(): string { + return this.baseUrlToKomMonitorDataAPI; + } + + /** + * Get spatial units endpoint + */ + get spatialUnitsEndpointPath(): string { + return this.spatialUnitsEndpoint; + } + + /** + * Set loading state + */ + private setLoading(loading: boolean): void { + this.loadingSubject.next(loading); + } + + /** + * Set error state + */ + private setError(error: any): void { + const errorMessage = error?.error?.message || error?.message || 'An unknown error occurred'; + this.errorSubject.next(errorMessage); + } + + /** + * Clear error state + */ + private clearError(): void { + this.errorSubject.next(null); + } + + /** + * Handle HTTP errors + */ + private handleError(error: HttpErrorResponse): Observable { + let errorMessage = 'An error occurred'; + + if (error.error instanceof ErrorEvent) { + // Client-side error + errorMessage = `Error: ${error.error.message}`; + } else { + // Server-side error + errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`; + } + + console.error('HTTP Error:', errorMessage); + return throwError(() => new Error(errorMessage)); + } + + /** + * Initialize the service + */ + async init(): Promise { + this.checkAuthentication(); + await this.fetchLastDatabaseModificationObject().toPromise(); + } + + /** + * Refresh spatial units data + */ + refreshSpatialUnits(keycloakRolesArray: string[]): Observable { + this.clearSpatialUnitsCache(keycloakRolesArray); + return this.fetchSpatialUnitsMetadata(keycloakRolesArray); + } +} \ No newline at end of file diff --git a/app/services/adminSpatialUnit/kommonitor-data-exchange.service.ts b/app/services/adminSpatialUnit/kommonitor-data-exchange.service.ts new file mode 100644 index 000000000..de49c75a6 --- /dev/null +++ b/app/services/adminSpatialUnit/kommonitor-data-exchange.service.ts @@ -0,0 +1,1172 @@ +import { Injectable, Inject, OnDestroy } from '@angular/core'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { + Observable, + BehaviorSubject, + throwError, + of, + timer, + combineLatest, + catchError, + retry, + shareReplay, + switchMap, + tap, + map, + filter, + takeUntil, + Subject +} from 'rxjs'; +import { AuthService } from '../auth-service/auth.service'; + +// TypeScript interfaces for better type safety +export interface SpatialUnitMetadata { + spatialUnitId: string; + spatialUnitLevel: string; + metadata: { + description: string; + datasource: string; + contact: string; + note?: string; + literature?: string; + updateInterval?: string; + lastUpdate?: string; + databasis?: string; + sridEPSG?: number; + }; + nextLowerHierarchyLevel?: string; + nextUpperHierarchyLevel?: string; + availablePeriodsOfValidity: Array<{ + startDate: string; + endDate?: string; + }>; + permissions: any[]; + isPublic: boolean; + ownerId: string; + userPermissions?: string[]; + isOutlineLayer?: boolean; + outlineColor?: string; + outlineWidth?: number; + outlineDashArrayString?: string; +} + +export interface AccessControlMetadata { + organizationalUnitId: string; + name: string; + permissions: Array<{ + permissionId: string; + permissionLevel: string; + isChecked: boolean; + }>; + datasetOwner?: boolean; + children?: string[]; + parentId?: string; + description?: string; + contact?: string; + mandant?: boolean; + keycloakId?: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class KommonitorDataExchangeService implements OnDestroy { + // Reactive subjects for state management + private spatialUnitsSubject = new BehaviorSubject([]); + private accessControlSubject = new BehaviorSubject([]); + private currentRolesSubject = new BehaviorSubject([]); + private komMonitorRolesSubject = new BehaviorSubject([]); + private loadingSubject = new BehaviorSubject(false); + private errorSubject = new BehaviorSubject(null); + private authenticationStateSubject = new BehaviorSubject(false); + + // Destroy subject for cleanup + private destroy$ = new Subject(); + + // Public observables + public spatialUnits$ = this.spatialUnitsSubject.asObservable(); + public accessControl$ = this.accessControlSubject.asObservable(); + public currentRoles$ = this.currentRolesSubject.asObservable(); + public komMonitorRoles$ = this.komMonitorRolesSubject.asObservable(); + public loading$ = this.loadingSubject.asObservable(); + public error$ = this.errorSubject.asObservable(); + public authenticationState$ = this.authenticationStateSubject.asObservable(); + + // Cache for spatial units with expiration + private spatialUnitsCache: { + data: SpatialUnitMetadata[]; + timestamp: number; + expiresAt: number; + } | null = null; + + // Cache for access control with expiration + private accessControlCache: { + data: AccessControlMetadata[]; + timestamp: number; + expiresAt: number; + } | null = null; + + // Cache duration in milliseconds (5 minutes) + private readonly CACHE_DURATION = 5 * 60 * 1000; + + // Base URL for API calls + private readonly baseUrl: string; + + // API endpoints + private readonly endpoints = { + spatialUnits: '/spatial-units', + spatialUnitsPublic: '/public/spatial-units', + accessControl: '/organizationalUnits', + indicators: '/indicators', + indicatorsPublic: '/public/indicators' + }; + + // Environment configuration + private readonly env: any; + + constructor( + private http: HttpClient, + private authService: AuthService + ) { + // Get environment configuration + this.env = (window as any).__env; + this.baseUrl = this.getBaseApiUrl(); + + // Initialize the service + this.initializeService(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + /** + * Initialize the service with proper race condition handling + */ + private initializeService(): void { + // Set up authentication listeners + this.setupAuthenticationListeners(); + + // Initial role extraction (with retry logic for race conditions) + this.extractAndSetRolesWithRetry(); + + // Set up periodic role checking to handle token refreshes + this.setupPeriodicRoleCheck(); + } + + /** + * Set up authentication state listeners + */ + private setupAuthenticationListeners(): void { + // Listen for authentication state changes + timer(0, 1000) // Check every second + .pipe( + takeUntil(this.destroy$), + map(() => this.isAuthenticated()), + filter((isAuth, index) => { + const currentState = this.authenticationStateSubject.value; + return isAuth !== currentState; // Only emit when state changes + }) + ) + .subscribe(isAuthenticated => { + this.authenticationStateSubject.next(isAuthenticated); + + if (isAuthenticated) { + // User just authenticated, extract roles + this.extractAndSetRoles(); + } else { + // User logged out, clear roles + this.clearRoles(); + } + }); + } + + /** + * Extract roles with retry logic to handle race conditions + */ + private extractAndSetRolesWithRetry(): void { + const maxRetries = 10; + let retryCount = 0; + + const attemptRoleExtraction = () => { + const roles = this.extractRolesFromKeycloak(); + + if (roles.length > 0 || retryCount >= maxRetries) { + this.setCurrentKeycloakLoginRoles(roles); + } else { + retryCount++; + setTimeout(attemptRoleExtraction, 500); // Retry after 500ms + } + }; + + attemptRoleExtraction(); + } + + /** + * Set up periodic role checking for token refreshes + */ + private setupPeriodicRoleCheck(): void { + timer(30000, 30000) // Check every 30 seconds + .pipe( + takeUntil(this.destroy$), + filter(() => this.isAuthenticated()) + ) + .subscribe(() => { + const currentRoles = this.currentRolesSubject.value; + const newRoles = this.extractRolesFromKeycloak(); + + // Only update if roles have changed + if (JSON.stringify(currentRoles) !== JSON.stringify(newRoles)) { + this.setCurrentKeycloakLoginRoles(newRoles); + } + }); + } + + /** + * Extract roles directly from Keycloak JWT token + */ + private extractRolesFromKeycloak(): string[] { + try { + const keycloak = this.authService.Auth?.keycloak; + + if (!keycloak) { + return []; + } + + if (!keycloak.authenticated) { + return []; + } + + const tokenParsed = keycloak.tokenParsed; + if (!tokenParsed?.realm_access?.roles) { + return []; + } + + const roles = tokenParsed.realm_access.roles; + return roles; + } catch (error) { + return []; + } + } + + /** + * Extract and set roles from Keycloak + */ + private extractAndSetRoles(): void { + const roles = this.extractRolesFromKeycloak(); + this.setCurrentKeycloakLoginRoles(roles); + } + + /** + * Filter roles to only include KomMonitor-specific roles + */ + private filterKomMonitorRoles(allRoles: string[]): string[] { + if (!allRoles || allRoles.length === 0) { + return []; + } + + // Get environment configuration for role suffixes + const roleSuffixes = [ + ...(this.env?.keycloakKomMonitorGroupsEditRoleNames || []), + ...(this.env?.keycloakKomMonitorThemesEditRoleNames || []), + ...(this.env?.keycloakKomMonitorGeodataEditRoleNames || []) + ]; + + // Always include admin role + const possibleRoles = [this.env?.keycloakKomMonitorAdminRoleName || 'kommonitor-creator']; + + // Add organizational unit roles based on access control data + const accessControl = this.accessControlSubject.value; + accessControl.forEach(organizationalUnit => { + for (const roleSuffix of roleSuffixes) { + possibleRoles.push(organizationalUnit.name + "." + roleSuffix); + } + }); + + // Filter roles to only include KomMonitor-specific ones + const komMonitorRoles = allRoles.filter(role => possibleRoles.includes(role)); + + return komMonitorRoles; + } + + /** + * Check if user is authenticated + */ + private isAuthenticated(): boolean { + try { + const keycloak = this.authService.Auth?.keycloak; + return keycloak?.authenticated || false; + } catch (error) { + return false; + } + } + + /** + * Clear roles when user logs out + */ + private clearRoles(): void { + this.currentRolesSubject.next([]); + this.komMonitorRolesSubject.next([]); + } + + /** + * Gets the base API URL from environment configuration + */ + private getBaseApiUrl(): string { + if (this.env?.apiUrl && this.env?.basePath) { + return `${this.env.apiUrl}${this.env.basePath}`; + } + // Fallback to default values + return 'http://localhost:8085/management'; + } + + /** + * Get available spatial units with caching + */ + get availableSpatialUnits(): SpatialUnitMetadata[] { + return this.spatialUnitsSubject.value; + } + + /** + * Get current Keycloak login roles + */ + get currentKeycloakLoginRoles(): string[] { + return this.currentRolesSubject.value; + } + + /** + * Get KomMonitor-specific roles + */ + get currentKomMonitorLoginRoleNames(): string[] { + return this.komMonitorRolesSubject.value; + } + + /** + * Get spatial units map for quick lookup + */ + get availableSpatialUnits_map(): Map { + const spatialUnits = this.availableSpatialUnits; + const map = new Map(); + spatialUnits.forEach(unit => { + map.set(unit.spatialUnitId, unit); + }); + return map; + } + + /** + * Get access control data + */ + get accessControl(): AccessControlMetadata[] { + return this.accessControlSubject.value; + } + + /** + * Get base URL to KomMonitor Data API + */ + get baseUrlToKomMonitorDataAPI(): string { + return this.baseUrl; + } + + /** + * Get base URL to KomMonitor Data API for spatial resources + * This includes the authentication path based on user authentication state + */ + getBaseUrlToKomMonitorDataAPI_spatialResource(): string { + // For now, we'll use "/public" as the default path for spatial resources + // This should be configurable based on authentication state + const spatialResourcePath = this.isAuthenticated() ? "" : "/public"; + return this.baseUrl + spatialResourcePath; + } + + /** + * Check if Keycloak security is enabled + */ + get enableKeycloakSecurity(): boolean { + return this.env?.enableKeycloakSecurity || false; + } + + /** + * Get date picker options + */ + get datePickerOptions(): any { + return { + format: 'dd.mm.yyyy', + autoclose: true, + todayBtn: 'linked', + todayHighlight: true, + assumeNearbyYear: true, + startView: 2, + minView: 2 + }; + } + + /** + * Get update interval options + */ + get updateIntervalOptions(): any[] { + return [ + { + displayName: "jährlich", + apiName: "YEARLY" + }, + { + displayName: "halbjährlich", + apiName: "HALF_YEARLY" + }, + { + displayName: "vierteljährlich", + apiName: "QUARTERLY" + }, + { + displayName: "monatlich", + apiName: "MONTHLY" + }, + { + displayName: "wöchentlich", + apiName: "WEEKLY" + }, + { + displayName: "täglich", + apiName: "DAILY" + }, + { + displayName: "beliebig", + apiName: "ARBITRARY" + } + ]; + } + + /** + * Get available line of interest dash array objects + */ + get availableLoiDashArrayObjects(): any[] { + // Align with legacy AngularJS values so persisted datasets map correctly + return [ + { + label: 'Durchgezogen', + dashArrayValue: '', + svgString: '' + }, + { + label: 'Gestrichelt (20)', + dashArrayValue: '20', + svgString: '' + }, + { + label: 'Gestrichelt (20 10)', + dashArrayValue: '20 10', + svgString: '' + }, + { + label: 'Strich-Punkt (20 10 5 10)', + dashArrayValue: '20 10 5 10', + svgString: '' + }, + { + label: 'Gepunktet (5)', + dashArrayValue: '5', + svgString: '' + } + ]; + } + + /** + * Fetches spatial units metadata with caching and error handling + */ + fetchSpatialUnitsMetadata(keycloakRolesArray: string[]): Observable { + + // Check cache first + if (this.isCacheValid(this.spatialUnitsCache)) { + this.spatialUnitsSubject.next(this.spatialUnitsCache!.data); + return of(this.spatialUnitsCache!.data); + } + + this.setLoading(true); + this.clearError(); + + const endpoint = this.getSpatialUnitsEndpoint(); + const url = `${this.baseUrl}${endpoint}`; + + return this.http.get(url).pipe( + tap(data => { + this.spatialUnitsSubject.next(data); + this.updateSpatialUnitsCache(data); + this.setLoading(false); + }), + catchError(error => { + this.setError(this.handleHttpError(error)); + this.setLoading(false); + return throwError(() => error); + }), + retry(2), + shareReplay(1) + ); + } + + /** + * Fetches access control metadata + */ + fetchAccessControlMetadata(): Observable { + + // Check cache first + if (this.isCacheValid(this.accessControlCache)) { + this.accessControlSubject.next(this.accessControlCache!.data); + return of(this.accessControlCache!.data); + } + + this.setLoading(true); + this.clearError(); + + const url = `${this.baseUrl}${this.endpoints.accessControl}`; + + return this.http.get(url).pipe( + tap(data => { + this.accessControlSubject.next(data); + this.updateAccessControlCache(data); + + // Update KomMonitor roles after access control is loaded + this.updateKomMonitorRoles(); + + // Reset loading state after successful fetch + this.setLoading(false); + }), + catchError(error => { + this.setError(this.handleHttpError(error)); + this.setLoading(false); + return throwError(() => error); + }), + retry(2), + shareReplay(1) + ); + } + + /** + * Fetches indicators metadata + */ + fetchIndicatorsMetadata(keycloakRolesArray: string[]): Observable { + + this.setLoading(true); + this.clearError(); + + const endpoint = this.getIndicatorsEndpoint(); + const url = `${this.baseUrl}${endpoint}`; + + return this.http.get(url).pipe( + tap(data => { + this.setLoading(false); + }), + catchError(error => { + this.setError(this.handleHttpError(error)); + this.setLoading(false); + return throwError(() => error); + }), + retry(2) + ); + } + + /** + * Get spatial unit metadata by ID + */ + getSpatialUnitMetadataById(spatialUnitId: string): SpatialUnitMetadata | null { + const spatialUnits = this.availableSpatialUnits; + return spatialUnits.find(unit => unit.spatialUnitId === spatialUnitId) || null; + } + + /** + * Add single spatial unit metadata to the list + */ + addSingleSpatialUnitMetadata(spatialUnitMetadata: SpatialUnitMetadata): void { + // Ensure userPermissions is always an array + const metadataWithDefaults = { + ...spatialUnitMetadata, + userPermissions: spatialUnitMetadata.userPermissions || [] + }; + + const currentSpatialUnits = [...this.availableSpatialUnits]; + currentSpatialUnits.unshift(metadataWithDefaults); + this.spatialUnitsSubject.next(currentSpatialUnits); + this.updateSpatialUnitsCache(currentSpatialUnits); + } + + /** + * Replace single spatial unit metadata in the list + */ + replaceSingleSpatialUnitMetadata(spatialUnitMetadata: SpatialUnitMetadata): void { + // Ensure userPermissions is always an array + const metadataWithDefaults = { + ...spatialUnitMetadata, + userPermissions: spatialUnitMetadata.userPermissions || [] + }; + + const currentSpatialUnits = [...this.availableSpatialUnits]; + const index = currentSpatialUnits.findIndex(unit => unit.spatialUnitId === spatialUnitMetadata.spatialUnitId); + + if (index !== -1) { + currentSpatialUnits[index] = metadataWithDefaults; + this.spatialUnitsSubject.next(currentSpatialUnits); + this.updateSpatialUnitsCache(currentSpatialUnits); + } + } + + /** + * Delete single spatial unit metadata from the list + */ + deleteSingleSpatialUnitMetadata(spatialUnitId: string): void { + const currentSpatialUnits = [...this.availableSpatialUnits]; + const index = currentSpatialUnits.findIndex(unit => unit.spatialUnitId === spatialUnitId); + + if (index !== -1) { + currentSpatialUnits.splice(index, 1); + this.spatialUnitsSubject.next(currentSpatialUnits); + this.updateSpatialUnitsCache(currentSpatialUnits); + } + } + + /** + * Sets the current Keycloak login roles + */ + setCurrentKeycloakLoginRoles(roles: string[]): void { + this.currentRolesSubject.next([...roles]); + this.komMonitorRolesSubject.next(this.filterKomMonitorRoles(roles)); + } + + /** + * Check if user has permission to create spatial units + */ + checkCreatePermission(): boolean { + const roles = this.currentKeycloakLoginRoles; + const komMonitorRoles = this.currentKomMonitorLoginRoleNames; + + // Check for admin role + if (roles.includes(this.env?.keycloakKomMonitorAdminRoleName || 'kommonitor-creator')) { + return true; + } + + // Check for creator roles + const hasCreatorRole = komMonitorRoles.some(role => role.endsWith('-creator')); + + return hasCreatorRole; + } + + /** + * Get allowed roles string for display + */ + getAllowedRolesString(permissions: any): string { + if (!permissions || !Array.isArray(permissions)) { + return ''; + } + + const accessControl = this.accessControl; + const roleNames = permissions.map((permissionId: string) => { + for (const unit of accessControl) { + const permission = unit.permissions.find(p => p.permissionId === permissionId); + if (permission) { + return unit.name + '.' + permission.permissionLevel; + } + } + return permissionId; + }); + + return roleNames.join(', '); + } + + /** + * Get role title by role ID + */ + getRoleTitle(roleId: string): string { + const accessControl = this.accessControl; + const unit = accessControl.find(u => u.organizationalUnitId === roleId); + return unit ? unit.name : roleId; + } + + /** + * Syntax highlight JSON for error display + */ + syntaxHighlightJSON(json: any): string { + if (typeof json !== 'string') { + json = JSON.stringify(json, null, 2); + } + json = json.replace(/&/g, '&').replace(//g, '>'); + return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { + let cls = 'number'; + if (/^"/.test(match)) { + if (/:$/.test(match)) { + cls = 'key'; + } else { + cls = 'string'; + } + } else if (/true|false/.test(match)) { + cls = 'boolean'; + } else if (/null/.test(match)) { + cls = 'null'; + } + return '' + match + ''; + }); + } + + /** + * Display map application error + */ + displayMapApplicationError(error: any): void { + this.setError(typeof error === 'string' ? error : JSON.stringify(error)); + } + + /** + * Refresh spatial units data + */ + refreshSpatialUnits(): Observable { + this.invalidateSpatialUnitsCache(); + return this.fetchSpatialUnitsMetadata(this.currentKeycloakLoginRoles); + } + + /** + * Clear all caches + */ + clearAllCaches(): void { + this.invalidateSpatialUnitsCache(); + this.accessControlCache = null; + } + + /** + * Get the appropriate spatial units endpoint based on authentication + */ + private getSpatialUnitsEndpoint(): string { + const endpoint = this.enableKeycloakSecurity ? + this.endpoints.spatialUnits : + this.endpoints.spatialUnitsPublic; + return endpoint; + } + + /** + * Get the appropriate indicators endpoint based on authentication + */ + private getIndicatorsEndpoint(): string { + const endpoint = this.enableKeycloakSecurity ? + this.endpoints.indicators : + this.endpoints.indicatorsPublic; + return endpoint; + } + + /** + * Check if cache is valid + */ + private isCacheValid(cache: any): boolean { + return cache && cache.data && cache.expiresAt > Date.now(); + } + + /** + * Update spatial units cache + */ + private updateSpatialUnitsCache(data: SpatialUnitMetadata[]): void { + this.spatialUnitsCache = { + data: [...data], + timestamp: Date.now(), + expiresAt: Date.now() + this.CACHE_DURATION + }; + } + + /** + * Update access control cache + */ + private updateAccessControlCache(data: AccessControlMetadata[]): void { + this.accessControlCache = { + data: [...data], + timestamp: Date.now(), + expiresAt: Date.now() + this.CACHE_DURATION + }; + } + + /** + * Invalidate spatial units cache + */ + private invalidateSpatialUnitsCache(): void { + this.spatialUnitsCache = null; + } + + /** + * Update KomMonitor roles after access control is loaded + */ + private updateKomMonitorRoles(): void { + const currentRoles = this.currentRolesSubject.value; + const komMonitorRoles = this.filterKomMonitorRoles(currentRoles); + this.komMonitorRolesSubject.next(komMonitorRoles); + } + + /** + * Set loading state + */ + private setLoading(loading: boolean): void { + this.loadingSubject.next(loading); + } + + /** + * Set error state + */ + private setError(error: string): void { + this.errorSubject.next(error); + } + + /** + * Clear error state + */ + private clearError(): void { + this.errorSubject.next(null); + } + + /** + * Handle HTTP errors + */ + private handleHttpError(error: HttpErrorResponse): string { + let errorMessage = 'An error occurred'; + + if (error.error instanceof ErrorEvent) { + // Client-side error + errorMessage = `Error: ${error.error.message}`; + } else { + // Server-side error + errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`; + if (error.error && typeof error.error === 'object') { + errorMessage += `\nDetails: ${JSON.stringify(error.error)}`; + } + } + + return errorMessage; + } + + /** + * Check if current user has admin permission + */ + checkAdminPermission(): boolean { + const currentRoles = this.currentRolesSubject.value; + const adminRoleName = this.env?.keycloakKomMonitorAdminRoleName; + + if (!adminRoleName || !currentRoles || currentRoles.length === 0) { + return false; + } + + return currentRoles.includes(adminRoleName); + } + + /** + * Get access control metadata by organizational unit ID + */ + getAccessControlById(id: string): AccessControlMetadata | null { + return this.accessControl.find(unit => unit.organizationalUnitId === id) || null; + } + + /** + * Get access control metadata by organizational unit name + */ + getAccessControlByName(name: string): AccessControlMetadata | null { + return this.accessControl.find(unit => unit.name === name) || null; + } + + /** + * Filter child or self organizational units + */ + filterChildOrSelfOrganizationalUnits(organizationalUnitReferenceItem: AccessControlMetadata | null): (organizationalUnit: AccessControlMetadata) => boolean { + return (organizationalUnit: AccessControlMetadata) => { + if (!organizationalUnitReferenceItem) { + return true; + } + + if (organizationalUnit.organizationalUnitId === organizationalUnitReferenceItem.organizationalUnitId) { + return false; + } + + if (organizationalUnitReferenceItem.children && organizationalUnitReferenceItem.children.length > 0) { + return !this.isDescendantOfReferenceItem(organizationalUnitReferenceItem, organizationalUnit); + } + + return true; + }; + } + + /** + * Check if an organizational unit is a descendant of a reference item + */ + isDescendantOfReferenceItem(organizationalUnitReferenceItem: AccessControlMetadata, organizationalUnitCandidate: AccessControlMetadata): boolean { + if (organizationalUnitReferenceItem.children && organizationalUnitReferenceItem.children.includes(organizationalUnitCandidate.organizationalUnitId)) { + return true; + } + + // Check all further descendants + if (organizationalUnitReferenceItem.children) { + for (const childOrganizationalUnitId of organizationalUnitReferenceItem.children) { + const childOrganizationalUnit = this.getAccessControlById(childOrganizationalUnitId); + if (childOrganizationalUnit && childOrganizationalUnit.children && childOrganizationalUnit.children.length > 0) { + if (this.isDescendantOfReferenceItem(childOrganizationalUnit, organizationalUnitCandidate)) { + return true; + } + } + } + } + + return false; + } + + /** + * Validate spatial unit metadata form data + */ + validateSpatialUnitMetadata(metadata: any, spatialUnitLevel: string): { isValid: boolean; errors: string[] } { + const errors: string[] = []; + + // Check required fields + if (!spatialUnitLevel || spatialUnitLevel.trim() === '') { + errors.push('Raumebene Name ist erforderlich.'); + } + + // Check hierarchy validity + if (metadata.nextLowerHierarchyLevel && metadata.nextUpperHierarchyLevel) { + // This would need access to availableSpatialUnits to fully validate + // For now, just check if both are set + } + + return { + isValid: errors.length === 0, + errors + }; + } + + /** + * Convert empty strings to null for API calls + */ + convertEmptyToNull(value: any): any { + return value === '' || value === undefined || value === null ? null : value; + } + + /** + * Build patch body for spatial unit metadata update + */ + buildSpatialUnitMetadataPatchBody( + spatialUnitLevel: string, + metadata: any, + nextLowerHierarchyLevel: string | null, + nextUpperHierarchyLevel: string | null, + isOutlineLayer: boolean, + outlineColor: string, + outlineWidth: number, + outlineDashArrayString: string | null + ): any { + return { + datasetName: spatialUnitLevel.trim(), + metadata: { + note: this.convertEmptyToNull(metadata.note), + literature: this.convertEmptyToNull(metadata.literature), + updateInterval: metadata.updateInterval && metadata.updateInterval.apiName ? metadata.updateInterval.apiName : null, + sridEPSG: metadata.sridEPSG || 4326, + datasource: this.convertEmptyToNull(metadata.datasource), + contact: this.convertEmptyToNull(metadata.contact), + lastUpdate: this.convertEmptyToNull(metadata.lastUpdate), + description: this.convertEmptyToNull(metadata.description), + databasis: this.convertEmptyToNull(metadata.databasis) + }, + nextLowerHierarchyLevel, + nextUpperHierarchyLevel, + isOutlineLayer, + outlineColor: outlineColor || '#bf3d2c', + outlineWidth: outlineWidth || 2, + outlineDashArrayString + }; + } + + /** + * Build export data for spatial unit metadata + */ + buildSpatialUnitMetadataExport( + metadata: any, + spatialUnitLevel: string, + nextLowerHierarchyLevel: string | null, + nextUpperHierarchyLevel: string | null, + isOutlineLayer: boolean, + outlineColor: string, + outlineWidth: number, + outlineDashArrayString: string | null + ): any { + return { + metadata: { + note: this.convertEmptyToNull(metadata.note), + literature: this.convertEmptyToNull(metadata.literature), + updateInterval: metadata.updateInterval ? metadata.updateInterval.apiName : null, + sridEPSG: metadata.sridEPSG || 4326, + datasource: this.convertEmptyToNull(metadata.datasource), + contact: this.convertEmptyToNull(metadata.contact), + lastUpdate: this.convertEmptyToNull(metadata.lastUpdate), + description: this.convertEmptyToNull(metadata.description), + databasis: this.convertEmptyToNull(metadata.databasis) + }, + allowedRoles: ['roleId'], + spatialUnitLevel: spatialUnitLevel || null, + nextLowerHierarchyLevel, + nextUpperHierarchyLevel, + isOutlineLayer, + outlineColor, + outlineWidth, + outlineDashArrayString + }; + } + + /** + * Get metadata structure template for export + */ + get spatialUnitMetadataStructure() { + return { + "metadata": { + "note": "an optional note", + "literature": "optional text about literature", + "updateInterval": "YEARLY|HALF_YEARLY|QUARTERLY|MONTHLY|ARBITRARY", + "sridEPSG": 4326, + "datasource": "text about data source", + "contact": "text about contact details", + "lastUpdate": "YYYY-MM-DD", + "description": "description about spatial unit dataset", + "databasis": "text about data basis" + }, + "allowedRoles": ['roleId'], + "nextLowerHierarchyLevel": "Name of lower hierarchy level", + "spatialUnitLevel": "Name of spatial unit dataset", + "nextUpperHierarchyLevel": "Name of upper hierarchy level" + }; + } + + /** + * Validate period of validity dates + */ + validatePeriodOfValidity(startDate: string, endDate: string): { isValid: boolean; error?: string } { + if (!startDate || !endDate) { + return { isValid: true }; // Both dates are optional + } + + const start = new Date(startDate as any); + const end = new Date(endDate as any); + + // If either date is invalid, do not block submission here + const startTime = start.getTime(); + const endTime = end.getTime(); + if (isNaN(startTime) || isNaN(endTime)) { + return { isValid: true }; + } + + if (startTime >= endTime) { + return { + isValid: false, + error: 'Start date must be before end date and they cannot be the same' + }; + } + + return { isValid: true }; + } + + /** + * Transform GeoJSON features for grid display + */ + transformFeaturesForGrid(features: any[]): any[] { + return (features || []).map((feature: any) => { + if (feature.properties) { + // Add geometry and record ID to properties for grid display + feature.properties.kommonitorGeometry = feature.geometry; + feature.properties.kommonitorRecordId = feature.id; + return feature.properties; + } + return feature; + }); + } + + /** + * Extract remaining headers from GeoJSON features + */ + extractRemainingHeaders(features: any[]): string[] { + if (!features || features.length === 0) return []; + + const firstFeature = features[0]; + if (!firstFeature.properties) return []; + + const komMonitorProperties = ['ID', 'NAME', 'validStartDate', 'validEndDate']; + return Object.keys(firstFeature.properties).filter( + property => !komMonitorProperties.includes(property) + ); + } + + /** + * Build mapping config export structure + */ + buildMappingConfigExport( + converterDefinition: any, + datasourceTypeDefinition: any, + propertyMappingDefinition: any, + periodOfValidity: any + ): any { + return { + converter: converterDefinition, + dataSource: datasourceTypeDefinition, + propertyMapping: propertyMappingDefinition, + periodOfValidity + }; + } + + /** + * Validate mapping config import structure + */ + validateMappingConfigImport(config: any): { isValid: boolean; error?: string } { + if (!config.converter || !config.dataSource || !config.propertyMapping) { + return { + isValid: false, + error: 'Struktur der Datei stimmt nicht mit erwartetem Muster überein.' + }; + } + return { isValid: true }; + } + + /** + * Delete a spatial unit by ID + */ + async deleteSpatialUnit(spatialUnitId: string): Promise { + try { + const url = `${this.baseUrl}/spatial-units/${spatialUnitId}`; + await this.http.delete(url).toPromise(); + return true; + } catch (error) { + console.error(`Failed to delete spatial unit ${spatialUnitId}:`, error); + return false; + } + } + + /** + * Format error message consistently across components + */ + formatErrorMessage(error: any): string { + if (error && (error as any).error) { + return this.syntaxHighlightJSON((error as any).error); + } + return this.syntaxHighlightJSON(error); + } + + /** + * Bulk delete spatial units with error handling + */ + async bulkDeleteSpatialUnits(spatialUnitIds: string[]): Promise<{ + successful: string[], + failed: Array<{ id: string, error: string }> + }> { + const successful: string[] = []; + const failed: Array<{ id: string, error: string }> = []; + + for (const id of spatialUnitIds) { + try { + const success = await this.deleteSpatialUnit(id); + if (success) { + successful.push(id); + // Remove from local cache + this.deleteSingleSpatialUnitMetadata(id); + } else { + failed.push({ id, error: 'Deletion failed' }); + } + } catch (error) { + failed.push({ id, error: this.formatErrorMessage(error) }); + } + } + + return { successful, failed }; + } +} \ No newline at end of file diff --git a/app/services/adminSpatialUnit/kommonitor-data-grid-helper.service.ts b/app/services/adminSpatialUnit/kommonitor-data-grid-helper.service.ts new file mode 100644 index 000000000..efff6bfd5 --- /dev/null +++ b/app/services/adminSpatialUnit/kommonitor-data-grid-helper.service.ts @@ -0,0 +1,1500 @@ +import { Injectable, Inject } from '@angular/core'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { BroadcastService } from '../broadcast-service/broadcast.service'; +import { KommonitorDataExchangeService } from './kommonitor-data-exchange.service'; +import { + GridOptions, + ColDef, + GridApi, + ColumnApi, + ICellRendererParams, + ICellRendererComp, + GridReadyEvent +} from 'ag-grid-community'; +import { AgGridAngular } from 'ag-grid-angular'; +import { HttpClient } from '@angular/common/http'; + +// Declare environment variables +declare const __env: any; + +@Injectable({ + providedIn: 'root' +}) +export class KommonitorDataGridHelperService { + + // Store the data grid options + private dataGridOptions_spatialUnits: GridOptions | null = null; + private dataGridOptions_featureTable: GridOptions | null = null; + private gridApi_spatialUnits: GridApi | null = null; + private gridApi_featureTable: GridApi | null = null; + + // Resource type constants + readonly resourceType_spatialUnit = 'spatialUnit'; + readonly resourceType_georesource = 'georesource'; + readonly resourceType_indicator = 'indicator'; + + // Timestamp properties for feature table updates + featureTable_spatialUnit_lastUpdate_timestamp_success: Date | undefined = undefined; + featureTable_spatialUnit_lastUpdate_timestamp_failure: Date | undefined = undefined; + featureTable_georesource_lastUpdate_timestamp_success: Date | undefined = undefined; + featureTable_georesource_lastUpdate_timestamp_failure: Date | undefined = undefined; + featureTable_indicator_lastUpdate_timestamp_success: Date | undefined = undefined; + featureTable_indicator_lastUpdate_timestamp_failure: Date | undefined = undefined; + + constructor( + private modalService: NgbModal, + private broadcastService: BroadcastService, + private kommonitorDataExchangeService: KommonitorDataExchangeService, + private http: HttpClient + ) {} + + /** + * Main method to build the spatial units data grid + * Returns GridOptions for use in Angular templates with ag-grid-angular + */ + buildDataGrid_spatialUnits(spatialUnitMetadataArray: any[]): GridOptions { + // Store the data for future use + this.currentSpatialUnitsData = spatialUnitMetadataArray; + + // Build and return grid options for use in Angular template + this.dataGridOptions_spatialUnits = this.buildDataGridOptions_spatialUnits(spatialUnitMetadataArray); + + return this.dataGridOptions_spatialUnits; + } + + + + // Store current spatial units data + private currentSpatialUnitsData: any[] = []; + + /** + * Build the grid options configuration for ag-grid-angular + * Returns base configuration that component can extend + */ + buildDataGridOptions_spatialUnits(spatialUnitMetadataArray: any[]): GridOptions { + const columnDefs = this.buildDataGridColumnConfig_spatialUnits(spatialUnitMetadataArray); + const rowData = this.buildDataGridRowData_spatialUnits(spatialUnitMetadataArray); + + const gridOptions: GridOptions = { + columnDefs: columnDefs, + rowData: rowData, + defaultColDef: this.buildDefaultColDef(), + suppressRowClickSelection: true, + rowSelection: 'multiple', + enableCellTextSelection: true, + ensureDomOrder: true, + pagination: true, + paginationPageSize: 10, + suppressColumnVirtualisation: true + }; + + return gridOptions; + } + + /** + * Build default column definition + */ + buildDefaultColDef(): ColDef { + return { + editable: false, + sortable: true, + flex: 1, + minWidth: 200, + filter: true, + floatingFilter: true, + resizable: true, + wrapText: true, + autoHeight: true, + cellStyle: { + 'font-size': '12px', + 'white-space': 'normal !important', + 'line-height': '20px !important', + 'word-break': 'break-word !important', + 'padding-top': '17px', + 'padding-bottom': '17px' + } + }; + } + + /** + * Build column configuration for spatial units with proper cell renderers + */ + buildDataGridColumnConfig_spatialUnits(spatialUnitMetadataArray: any[]): ColDef[] { + const columnDefs: ColDef[] = [ + { + headerName: 'Editierfunktionen', + pinned: 'left', + maxWidth: 170, + checkboxSelection: false, + headerCheckboxSelection: false, + headerCheckboxSelectionFilteredOnly: true, + filter: false, + sortable: false, + cellRenderer: (params: any) => this.displayEditButtons_spatialUnits(params) + }, + { headerName: 'Id', field: 'spatialUnitId', pinned: 'left', maxWidth: 125 }, + { headerName: 'Name', field: 'spatialUnitLevel', pinned: 'left', minWidth: 300 }, + { + headerName: 'Beschreibung', + minWidth: 400, + cellRenderer: (params: ICellRendererParams) => params.data.metadata.description, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + params.data.metadata.description + }, + { headerName: 'Nächst niedrigere Raumebene', field: 'nextLowerHierarchyLevel', minWidth: 250 }, + { headerName: 'Nächst höhere Raumebene', field: 'nextUpperHierarchyLevel', minWidth: 250 }, + { + headerName: 'Gültigkeitszeitraum', + minWidth: 400, + cellRenderer: (params: ICellRendererParams) => { + let html = '
    '; + for (const periodOfValidity of params.data.availablePeriodsOfValidity) { + html += '
  • '; + if (periodOfValidity.endDate) { + html += '

    ' + periodOfValidity.startDate + ' ‐ ' + periodOfValidity.endDate + '

    '; + } else { + html += '

    ' + periodOfValidity.startDate + ' ‐ heute

    '; + } + html += '
  • '; + } + html += '
'; + return html; + }, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => { + if (params.data.availablePeriodsOfValidity && params.data.availablePeriodsOfValidity.length > 1) { + return '' + JSON.stringify(params.data.availablePeriodsOfValidity); + } + return params.data.availablePeriodsOfValidity; + } + }, + { + headerName: 'Datenquelle', + minWidth: 400, + cellRenderer: (params: ICellRendererParams) => params.data.metadata.datasource, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + params.data.metadata.datasource + }, + { + headerName: 'Datenhalter und Kontakt', + minWidth: 400, + cellRenderer: (params: ICellRendererParams) => params.data.metadata.contact, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + params.data.metadata.contact + }, + { + headerName: 'Rollen', + minWidth: 400, + cellRenderer: (params: ICellRendererParams) => this.kommonitorDataExchangeService.getAllowedRolesString(params.data.permissions), + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + this.kommonitorDataExchangeService.getAllowedRolesString(params.data.permissions) + }, + { + headerName: 'Öffentlich sichtbar', + minWidth: 400, + cellRenderer: (params: ICellRendererParams) => params.data.isPublic ? 'ja' : 'nein', + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + (params.data.isPublic ? 'ja' : 'nein') + }, + { + headerName: 'Eigentümer', + minWidth: 400, + cellRenderer: (params: ICellRendererParams) => this.kommonitorDataExchangeService.getRoleTitle(params.data.ownerId), + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + this.kommonitorDataExchangeService.getRoleTitle(params.data.ownerId) + } + ]; + + return columnDefs; + } + + /** + * Build row data for spatial units (just return the input array) + */ + buildDataGridRowData_spatialUnits(spatialUnitMetadataArray: any[]): any[] { + return spatialUnitMetadataArray; + } + + /** + * Build grid options for spatial units + */ + buildGridOptions(): GridOptions { + return { + suppressRowClickSelection: true, + rowSelection: 'multiple', + enableCellTextSelection: true, + ensureDomOrder: true, + pagination: true, + paginationPageSize: 10, + suppressColumnVirtualisation: true + }; + } + + /** + * Build default column definition for role management grids + */ + buildRoleManagementDefaultColDef(): any { + return { + editable: false, + sortable: true, + flex: 1, + minWidth: 100, + filter: false, + floatingFilter: false, + resizable: true, + wrapText: true, + autoHeight: true, + cellStyle: { + 'font-size': '12px', + 'white-space': 'normal !important', + 'line-height': '20px !important', + 'word-break': 'break-word !important', + 'padding-top': '17px', + 'padding-bottom': '17px' + }, + headerComponentParams: { + template: + '', + }, + }; + } + + /** + * Build grid options for role management grids (public method for components) + */ + buildRoleManagementGridOptionsPublic(components?: any): GridOptions { + return { + components: components || {}, + suppressRowClickSelection: true, + rowSelection: 'multiple', + enableCellTextSelection: true, + ensureDomOrder: true, + pagination: true, + paginationPageSize: 10, + suppressColumnVirtualisation: true, + headerHeight: 40, + rowHeight: 35 + }; + } + + + + /** + * Cell renderer for edit buttons + */ + displayEditButtons_spatialUnits(params: any): string { + const data = params.data; + let html = '
'; + + // Edit Metadata Button + html += ''; + + // Edit Features Button + html += ''; + + // Edit User Roles Button + html += ''; + + // Delete Button + html += ''; + + html += '
'; + return html; + } + + /** + * Register click handlers for buttons + */ + private registerClickHandler_spatialUnits(): void { + // Use native DOM methods instead of jQuery + setTimeout(() => { + // Edit Metadata Button + const editMetadataButtons = document.querySelectorAll('.spatialUnitEditMetadataBtn'); + editMetadataButtons.forEach((button: any) => { + button.removeEventListener('click', this.handleEditMetadataClick); + button.addEventListener('click', this.handleEditMetadataClick); + }); + + // Edit Features Button + const editFeaturesButtons = document.querySelectorAll('.spatialUnitEditFeaturesBtn'); + editFeaturesButtons.forEach((button: any) => { + button.removeEventListener('click', this.handleEditFeaturesClick); + button.addEventListener('click', this.handleEditFeaturesClick); + }); + + // Edit User Roles Button + const editUserRolesButtons = document.querySelectorAll('.spatialUnitEditUserRolesBtn'); + editUserRolesButtons.forEach((button: any) => { + button.removeEventListener('click', this.handleEditUserRolesClick); + button.addEventListener('click', this.handleEditUserRolesClick); + }); + + // Delete Button + const deleteButtons = document.querySelectorAll('.spatialUnitDeleteBtn'); + deleteButtons.forEach((button: any) => { + button.removeEventListener('click', this.handleDeleteClick); + button.addEventListener('click', this.handleDeleteClick); + }); + }, 100); + } + + /** + * Handle edit metadata button click + */ + private handleEditMetadataClick = (event: any): void => { + event.stopPropagation(); + + const spatialUnitId = event.target.id.split('_')[3]; + const spatialUnitMetadata = this.kommonitorDataExchangeService.getSpatialUnitMetadataById(spatialUnitId); + + this.broadcastService.broadcast('onEditSpatialUnitMetadata', spatialUnitMetadata); + } + + /** + * Handle edit features button click + */ + private handleEditFeaturesClick = (event: any): void => { + event.stopPropagation(); + + const spatialUnitId = event.target.id.split('_')[3]; + const spatialUnitMetadata = this.kommonitorDataExchangeService.getSpatialUnitMetadataById(spatialUnitId); + + this.broadcastService.broadcast('onEditSpatialUnitFeatures', spatialUnitMetadata); + } + + /** + * Handle edit user roles button click + */ + private handleEditUserRolesClick = (event: any): void => { + event.stopPropagation(); + + const spatialUnitId = event.target.id.split('_')[3]; + const spatialUnitMetadata = this.kommonitorDataExchangeService.getSpatialUnitMetadataById(spatialUnitId); + + this.broadcastService.broadcast('onEditSpatialUnitUserRoles', spatialUnitMetadata); + } + + /** + * Handle delete button click + */ + private handleDeleteClick = (event: any): void => { + event.stopPropagation(); + + const spatialUnitId = event.target.id.split('_')[3]; + const spatialUnitMetadata = this.kommonitorDataExchangeService.getSpatialUnitMetadataById(spatialUnitId); + + this.broadcastService.broadcast('onDeleteSpatialUnits', [spatialUnitMetadata]); + } + + /** + * Get selected spatial units metadata + */ + getSelectedSpatialUnitsMetadata(): any[] { + const spatialUnitsMetadataArray: any[] = []; + + if (this.dataGridOptions_spatialUnits && this.gridApi_spatialUnits) { + const selectedNodes = this.gridApi_spatialUnits.getSelectedNodes(); + for (const selectedNode of selectedNodes) { + spatialUnitsMetadataArray.push(selectedNode.data); + } + } + + return spatialUnitsMetadataArray; + } + + /** + * Save grid state (for preserving selection/filters when updating data) + */ + private saveGridStore(gridOptions: any): void { + if (gridOptions && this.gridApi_spatialUnits) { + // Store selection state + const selectedNodes = this.gridApi_spatialUnits.getSelectedNodes(); + gridOptions._savedState = { + selectedIds: selectedNodes.map((node: any) => node.data.spatialUnitId) + }; + } + } + + /** + * Restore grid state (for preserving selection/filters when updating data) + */ + private restoreGridStore(gridOptions: any): void { + if (gridOptions && this.gridApi_spatialUnits && gridOptions._savedState) { + setTimeout(() => { + // Restore selection + this.gridApi_spatialUnits?.forEachNode((node: any) => { + if (gridOptions._savedState.selectedIds.includes(node.data.spatialUnitId)) { + node.setSelected(true); + } + }); + }, 100); + } + } + + /** + * Set header height for proper display + */ + private headerHeightSetter(): void { + if (this.gridApi_spatialUnits) { + const headerHeight = this.headerHeightGetter(); + this.gridApi_spatialUnits.setHeaderHeight(headerHeight); + } + } + + /** + * Calculate header height based on content + */ + private headerHeightGetter(): number { + const columnHeaderTexts = document.querySelectorAll('.ag-header-cell-text'); + let maxHeight = 0; + + columnHeaderTexts.forEach((element: any) => { + const height = element.offsetHeight; + if (height > maxHeight) { + maxHeight = height; + } + }); + + return Math.max(maxHeight + 20, 50); // Add padding, minimum 50px + } + + /** + * Build role management grid for spatial units + */ + buildRoleManagementGrid(tableDOMId: string, currentTableOptionsObject: any, accessControlMetadata: any[], selectedPermissionIds: string[], reducedRoleManagement: boolean = false): any { + if (currentTableOptionsObject && this.gridApi_spatialUnits) { + // Grid already exists, just update the data + const newRowData = this.buildRoleManagementGridRowData(accessControlMetadata, selectedPermissionIds); + // update underlying options so callers get the latest data + currentTableOptionsObject.rowData = newRowData; + this.gridApi_spatialUnits.setRowData(newRowData); + // ensure cells re-render to apply disabled state and checks + setTimeout(() => { + try { + this.gridApi_spatialUnits?.refreshCells({ force: true }); + this.gridApi_spatialUnits?.redrawRows(); + } catch (e) {} + }, 0); + } else { + // Create new grid options + currentTableOptionsObject = this.buildRoleManagementGridOptions(accessControlMetadata, selectedPermissionIds, reducedRoleManagement); + console.log('Role management grid options created. Use in component template.'); + } + return currentTableOptionsObject; + } + + /** + * Build role management grid row data + */ + private buildRoleManagementGridRowData(accessControlMetadata: any[], permissionIds: string[]): any[] { + // Flatten permissions into boolean fields for ag-Grid built-in checkbox renderer + const data = JSON.parse(JSON.stringify(accessControlMetadata)); + for (const elem of data) { + if (elem.name === 'public') { + elem.name = 'Öffentlicher Zugriff'; + } + // Flatten permissions + elem.viewer = false; + elem.editor = false; + elem.creator = false; + if (elem.permissions && Array.isArray(elem.permissions)) { + for (const permission of elem.permissions) { + const isChecked = !!(permissionIds && permissionIds.includes(permission.permissionId)); + // keep permissions[] state in sync (as in AngularJS) + permission.isChecked = isChecked; + + if (permission.permissionLevel === 'viewer') { + elem.viewer = isChecked; + } + if (permission.permissionLevel === 'editor') { + elem.editor = isChecked; + } + if (permission.permissionLevel === 'creator') { + elem.creator = isChecked; + } + } + } + } + // Keep the original sorting logic + const array: any[] = []; + array.push(data[0]); + array.push(data[1]); + data.splice(0, 2); + data.sort((a, b) => { + if (a.name < b.name) { + return -1; + } + if (a.name > b.name) { + return 1; + } + return 0; + }); + return array.concat(data); + } + + private buildRoleManagementGridColumnConfig(reducedRoleManagement: boolean = false): any[] { + const columnDefs = [ + { + headerName: 'Organisationseinheit', + field: 'name', + minWidth: 200, + cellClass: 'user-roles-normal' + }, + { + headerName: 'Lesen', + field: 'viewer', + filter: false, + sortable: false, + width: 100, + cellRenderer: 'CheckboxRenderer_viewer', + editable: true + }, + { + headerName: 'Editieren', + field: 'editor', + filter: false, + sortable: false, + width: 100, + cellRenderer: 'CheckboxRenderer_editor', + editable: true + } + ]; + if (!reducedRoleManagement) { + columnDefs.push({ + headerName: 'Löschen', + field: 'creator', + filter: false, + sortable: false, + width: 100, + cellRenderer: 'CheckboxRenderer_creator', + editable: true + }); + } + return columnDefs; + } + + private buildRoleManagementGridOptions(accessControlMetadata: any[], selectedPermissionIds: string[], reducedRoleManagement: boolean = false): any { + const columnDefs = this.buildRoleManagementGridColumnConfig(reducedRoleManagement); + const rowData = this.buildRoleManagementGridRowData(accessControlMetadata, selectedPermissionIds); + const gridOptions = { + components: { + CheckboxRenderer_viewer: this.CheckboxRenderer_viewer, + CheckboxRenderer_editor: this.CheckboxRenderer_editor, + CheckboxRenderer_creator: this.CheckboxRenderer_creator + }, + defaultColDef: { + editable: false, + sortable: true, + flex: 1, + minWidth: 100, + filter: true, + floatingFilter: false, + resizable: true, + wrapText: true, + autoHeight: true, + cellStyle: { + 'font-size': '12px', + 'white-space': 'normal !important', + 'line-height': '20px !important', + 'word-break': 'break-word !important', + 'padding-top': '17px', + 'padding-bottom': '17px' + }, + headerComponentParams: { + template: + '', + }, + }, + columnDefs: columnDefs, + rowData: rowData, + suppressRowClickSelection: true, + rowSelection: 'multiple', + enableCellTextSelection: true, + ensureDomOrder: true, + pagination: true, + paginationPageSize: 10, + suppressColumnVirtualisation: true, + onFirstDataRendered: () => { + this.headerHeightSetter(); + }, + onColumnResized: () => { + this.headerHeightSetter(); + }, + onGridReady: (params: GridReadyEvent) => { + this.gridApi_spatialUnits = params.api; + } + }; + return gridOptions; + } + + /** + * Expose role management checkbox renderer components for early binding in templates + */ + public getRoleManagementComponents(): any { + return { + CheckboxRenderer_viewer: this.CheckboxRenderer_viewer, + CheckboxRenderer_editor: this.CheckboxRenderer_editor, + CheckboxRenderer_creator: this.CheckboxRenderer_creator + }; + } + + /** + * Get selected role IDs from role management grid + */ + getSelectedRoleIds_roleManagementGrid(roleManagementTableOptions: any): string[] { + const selectedIds = new Set(); + + const collectFromRow = (row: any) => { + if (!row || !row.permissions) return; + for (const permission of row.permissions) { + if (permission && permission.isChecked && permission.permissionId) { + selectedIds.add(permission.permissionId); + } + } + }; + + // Prefer live grid data when API is available + if (this.gridApi_spatialUnits && !(this.gridApi_spatialUnits as any).isDestroyed?.()) { + this.gridApi_spatialUnits.forEachNode((node: any) => collectFromRow(node.data)); + } else if (roleManagementTableOptions && Array.isArray(roleManagementTableOptions.rowData)) { + // Fallback to current table options rowData + for (const row of roleManagementTableOptions.rowData) { + collectFromRow(row); + } + } + + return Array.from(selectedIds); + } + + /** + * Checkbox renderer for viewer permissions + */ + private CheckboxRenderer_viewer = class { + private params: any; + private eGui: HTMLElement | null = null; + private boundCheckedHandler: any; + + init(params: any) { + this.params = params; + + let isChecked = false; + let exists = false; + let className; + if (params && params.data) { + for (const permission of params.data.permissions) { + if (permission.permissionLevel == "viewer"){ + exists = true; + isChecked = permission.isChecked; + className = permission.permissionId; + break; + } + } + } + + if(exists){ + const input = document.createElement('input') as HTMLInputElement; + this.eGui = input; + input.className = className; + input.type = 'checkbox'; + input.checked = isChecked; + + // Disable viewer if dataset owner or if editor/creator selection implies viewer + if (this.params.data.datasetOwner === true || this.params.data._viewerDisabledBecauseOfEditor === true || this.params.data._viewerDisabledBecauseOfCreator === true) { + input.disabled = true; + } else { + input.disabled = false; + } + + this.boundCheckedHandler = this.checkedHandler.bind(this); + input.addEventListener('click', this.boundCheckedHandler); + } else { + // If permission does not exist for this row, render empty content to avoid displaying boolean values like "false" + this.eGui = document.createElement('span'); + } + } + + checkedHandler(e: any) { + let checked = e.target.checked; + + for (const permission of this.params.data.permissions) { + if (permission.permissionLevel == "viewer"){ + permission.isChecked = checked; + break; + } + } + } + + getGui() { return this.eGui; } + + destroy() { + if(this.eGui && this.boundCheckedHandler){ + this.eGui.removeEventListener('click', this.boundCheckedHandler); + } + } + }; + + /** + * Checkbox renderer for editor permissions + */ + private CheckboxRenderer_editor = class { + private params: any; + private eGui: HTMLElement | null = null; + private boundCheckedHandler: any; + + init(params: any) { + this.params = params; + + let isChecked = false; + let exists = false; + let className; + if (params && params.data) { + for (const permission of params.data.permissions) { + if (permission.permissionLevel == "editor"){ + exists = true; + isChecked = permission.isChecked; + className = permission.permissionId; + break; + } + } + } + + if(exists){ + const input = document.createElement('input') as HTMLInputElement; + this.eGui = input; + input.className = className; + input.type = 'checkbox'; + input.checked = isChecked; + + // Disable editor if dataset owner or if creator selection implies editor + if (this.params.data.datasetOwner === true || this.params.data._editorDisabledBecauseOfCreator === true) { + input.disabled = true; + } else { + input.disabled = false; + } + + this.boundCheckedHandler = this.checkedHandler.bind(this); + input.addEventListener('click', this.boundCheckedHandler); + } else { + // If permission does not exist for this row, render empty content to avoid displaying boolean values like "false" + this.eGui = document.createElement('span'); + } + } + + checkedHandler(e: any) { + let checked = e.target.checked; + for (const permission of this.params.data.permissions) { + if (permission.permissionLevel == "viewer"){ + if (checked){ + permission.isChecked = true; + } else { + permission.isChecked = false; + } + } + else if (permission.permissionLevel == "editor"){ + permission.isChecked = checked; + } + } + // If editor is checked, enforce viewer checked+disabled + if (checked) { + this.params.data._viewerDisabledBecauseOfEditor = true; + for (const permission of this.params.data.permissions) { + if (permission.permissionLevel == "viewer"){ + permission.isChecked = true; + } + } + } else { + this.params.data._viewerDisabledBecauseOfEditor = false; + } + // Ask grid to refresh this row to update disabled state of viewer column + if (this.params.api && this.params.node) { + this.params.api.refreshCells({ force: true, rowNodes: [this.params.node] }); + } + } + + getGui() { return this.eGui; } + + destroy() { + if(this.eGui && this.boundCheckedHandler){ + this.eGui.removeEventListener('click', this.boundCheckedHandler); + } + } + }; + + /** + * Checkbox renderer for creator permissions + */ + private CheckboxRenderer_creator = class { + private params: any; + private eGui: HTMLElement | null = null; + private boundCheckedHandler: any; + + init(params: any) { + this.params = params; + + let isChecked = false; + let exists = false; + let className; + for (const permission of params.data.permissions) { + if (permission.permissionLevel == "creator"){ + exists = true; + isChecked = permission.isChecked; + className = permission.permissionId; + break; + } + } + + if(exists){ + const input = document.createElement('input') as HTMLInputElement; + this.eGui = input; + input.className = className; + input.type = 'checkbox'; + input.checked = isChecked; + + // Disable creator if dataset owner is true + if (this.params.data.datasetOwner === true) { + input.disabled = true; + } else { + input.disabled = false; + } + + this.boundCheckedHandler = this.checkedHandler.bind(this); + input.addEventListener('click', this.boundCheckedHandler); + } else { + // If permission does not exist for this row, render empty content to avoid displaying boolean values like "false" + this.eGui = document.createElement('span'); + } + } + + checkedHandler(e: any) { + let checked = e.target.checked; + for (const permission of this.params.data.permissions) { + if (permission.permissionLevel == "creator" || permission.permissionLevel == "editor" || permission.permissionLevel == "viewer"){ + permission.isChecked = checked; + } + } + // If creator is checked, enforce editor and viewer checked+disabled + if (checked) { + this.params.data._editorDisabledBecauseOfCreator = true; + this.params.data._viewerDisabledBecauseOfCreator = true; + for (const permission of this.params.data.permissions) { + if (permission.permissionLevel == "editor" || permission.permissionLevel == "viewer"){ + permission.isChecked = true; + } + } + } else { + this.params.data._editorDisabledBecauseOfCreator = false; + this.params.data._viewerDisabledBecauseOfCreator = false; + } + // Ask grid to refresh this row to update disabled state of editor/viewer columns + if (this.params.api && this.params.node) { + this.params.api.refreshCells({ force: true, rowNodes: [this.params.node] }); + } + } + + getGui() { return this.eGui; } + + destroy() { + if(this.eGui && this.boundCheckedHandler){ + this.eGui.removeEventListener('click', this.boundCheckedHandler); + } + } + }; + + /** + * Build feature table data grid for spatial resources + * @param tableId - DOM ID of the table container + * @param headers - Array of column headers + * @param features - Array of GeoJSON features + * @param resourceId - ID of the spatial resource + * @param resourceType - Type of resource (spatialUnit, georesource, indicator) + * @param enableDelete - Whether to enable delete functionality + */ + buildDataGrid_featureTable_spatialResource( + tableId: string, + headers: string[], + features: any[] = [], + resourceId?: string, + resourceType?: string, + enableDelete: boolean = false + ): GridOptions { + // Store current resource ID for delete handlers + this.currentResourceId = resourceId; + + const gridContainer = document.querySelector('#' + tableId); + if (!gridContainer) { + console.error(`Grid container #${tableId} not found`); + return this.buildFeatureTableGridOptions(headers, features, resourceId, resourceType, enableDelete); + } + + if (this.dataGridOptions_featureTable && this.gridApi_featureTable && gridContainer.childElementCount > 0) { + // Grid already exists, just update the data + this.saveGridStore_featureTable(this.dataGridOptions_featureTable); + const newRowData = this.buildFeatureTableRowData(features); + this.gridApi_featureTable.setRowData(newRowData); + this.restoreGridStore_featureTable(this.dataGridOptions_featureTable); + } else { + // Create new grid options + this.dataGridOptions_featureTable = this.buildFeatureTableGridOptions( + headers, + features, + resourceId, + resourceType, + enableDelete + ); + + // The actual grid creation should be done in the component template + console.log('Feature table grid options created. Use in component template.'); + } + + return this.dataGridOptions_featureTable!; + } + + /** + * Build grid options for feature table + */ + private buildFeatureTableGridOptions( + headers: string[], + features: any[], + resourceId?: string, + resourceType?: string, + enableDelete: boolean = false + ): any { + const columnDefs = this.buildFeatureTableColumnConfig(headers, enableDelete, resourceType); + const rowData = this.buildFeatureTableRowData(features); + + const gridOptions = { + defaultColDef: { + editable: true, + sortable: true, + flex: 1, + minWidth: 150, + filter: true, + floatingFilter: true, + resizable: true, + wrapText: true, + autoHeight: true, + cellEditor: 'agLargeTextCellEditor', + cellStyle: { + 'font-size': '12px', + 'white-space': 'normal !important', + 'line-height': '20px !important', + 'word-break': 'break-word !important', + 'padding-top': '17px', + 'padding-bottom': '17px' + }, + onCellValueChanged: (newValueParams: any) => { + // Handle cell value changes for date validation and API updates + this.handleCellValueChanged(newValueParams, resourceId, resourceType); + } + }, + components: { + deleteButtonRenderer: this.deleteButtonRenderer.bind(this) + }, + columnDefs: columnDefs, + rowData: rowData, + // enables undo / redo + undoRedoCellEditing: true, + // restricts the number of undo / redo steps to 10 + undoRedoCellEditingLimit: 10, + // enables flashing to help see cell changes + enableCellChangeFlash: true, + suppressRowClickSelection: true, + rowSelection: 'multiple', + enableCellTextSelection: true, + ensureDomOrder: true, + // Pagination settings + pagination: true, + paginationPageSize: 20, + paginationPageSizeSelector: [10, 20, 50, 100], + // Filtering is controlled via defaultColDef.filter and per-column filters + // Grid features + suppressColumnVirtualisation: true, + onFirstDataRendered: () => { + this.headerHeightSetter(); + this.registerFeatureTableClickHandlers(resourceId, resourceType, enableDelete); + }, + onColumnResized: () => { + this.headerHeightSetter(); + }, + onGridReady: (params: GridReadyEvent) => { + this.gridApi_featureTable = params.api; + }, + onRowDataChanged: () => { + this.registerFeatureTableClickHandlers(resourceId, resourceType, enableDelete); + }, + onModelUpdated: () => { + this.registerFeatureTableClickHandlers(resourceId, resourceType, enableDelete); + }, + onViewportChanged: () => { + this.registerFeatureTableClickHandlers(resourceId, resourceType, enableDelete); + } + }; + + return gridOptions; + } + + /** + * Build column configuration for feature table + */ + private buildFeatureTableColumnConfig(headers: string[], enableDelete: boolean, resourceType?: string): any[] { + const columnDefs: any[] = []; + + // Add DB-Record-Id column with delete button (always first, combines both functionalities) + columnDefs.push({ + headerName: 'DB-Record-Id', + field: 'kommonitorRecordId', + pinned: 'left', + editable: false, + maxWidth: 125, + cellClass: 'grid-non-editable', + cellRenderer: (params: any) => { + let html = ''; + + // Add delete button if enabled + if (enableDelete) { + const datasetId = this.currentResourceId || ''; + const featureId = params.data['ID'] || params.data['featureId'] || ''; + const recordId = params.data.kommonitorRecordId || params.data.id || ''; + + if (resourceType === this.resourceType_spatialUnit) { + html += ``; + } else { + html += ``; + } + html += '
'; + } + + // Add the record ID + html += params.data.kommonitorRecordId || params.data.id || ''; + + return html; + } + }); + + // Add Feature-Id column + columnDefs.push({ + headerName: 'Feature-Id', + field: 'ID', + pinned: 'left', + editable: false, + cellClass: 'grid-non-editable', + maxWidth: 125 + }); + + // Add Name column + columnDefs.push({ + headerName: 'Name', + field: 'NAME', + pinned: 'left', + minWidth: 150 + }); + + // Add validity date columns + columnDefs.push({ + headerName: 'Lebenszeitbeginn', + field: 'validStartDate', + minWidth: 150 + }); + + columnDefs.push({ + headerName: 'Lebenszeitende', + field: 'validEndDate', + minWidth: 150 + }); + + // Add dynamic headers + for (const header of headers) { + columnDefs.push({ + headerName: header, + field: header, + minWidth: 125 + }); + } + + return columnDefs; + } + + /** + * Build row data for feature table + */ + private buildFeatureTableRowData(features: any[]): any[] { + if (!features || !Array.isArray(features)) { + return []; + } + + return features.map(feature => { + // If the feature has properties (GeoJSON format), add geometry and record ID to properties + if (feature.properties) { + // Add geometry and database record ID to properties to be available within data grid object + feature.properties.kommonitorGeometry = feature.geometry; + feature.properties.kommonitorRecordId = feature.id; + return feature.properties; + } + + // If it's already a flat object, ensure it has the required fields + if (feature.id && !feature.kommonitorRecordId) { + feature.kommonitorRecordId = feature.id; + } + + return feature; + }); + } + + /** + * Delete button renderer for feature table + */ + private deleteButtonRenderer(params: any): string { + const featureId = params.data.properties?.[__env?.FEATURE_ID_PROPERTY_NAME] || + params.data[__env?.FEATURE_ID_PROPERTY_NAME] || ''; + const resourceType = params.resourceType || 'spatialUnit'; + + return ``; + } + + /** + * Register click handlers for feature table delete buttons + */ + registerFeatureTableClickHandlers(resourceId?: string, resourceType?: string, enableDelete?: boolean): void { + if (!enableDelete) return; + + setTimeout(() => { + // Remove existing handlers to prevent duplicates + const deleteButtons = document.querySelectorAll('.spatialUnitDeleteFeatureRecordBtn, .georesourceDeleteFeatureRecordBtn'); + deleteButtons.forEach(button => { + button.removeEventListener('click', this.handleFeatureDeleteClick); + }); + + // Add new handlers + deleteButtons.forEach(button => { + button.addEventListener('click', this.handleFeatureDeleteClick); + }); + }, 100); + } + + /** + * Handle delete button click for feature table + */ + private handleFeatureDeleteClick = (event: Event): void => { + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); + + const button = event.target as HTMLElement; + const buttonElement = button.closest('button') || button; + const buttonId = buttonElement.id; + + // Parse button ID: btn__spatialUnit__deleteFeatureEntry__{datasetId}__{featureId}__{recordId} + const idParts = buttonId.split('__'); + if (idParts.length < 6) { + console.error('Invalid button ID format:', buttonId); + return; + } + + const resourceType = idParts[1]; // spatialUnit or georesource + const datasetId = idParts[3]; + const featureId = idParts[4]; + const recordId = idParts[5]; + + // Broadcast loading event + this.broadcastService.broadcast(`showLoadingIcon_${resourceType}`, {}); + + // Determine URL based on resource type + let url = `${this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI}`; + if (resourceType === 'spatialUnit') { + url += `/spatial-units/${datasetId}/singleFeature/${featureId}/singleFeatureRecord/${recordId}`; + } else if (resourceType === 'georesource') { + url += `/georesources/${datasetId}/singleFeature/${featureId}/singleFeatureRecord/${recordId}`; + } else { + console.error('Unknown resource type:', resourceType); + return; + } + + // Make DELETE request + this.http.delete(url).subscribe({ + next: (response: any) => { + console.log('Successfully deleted database record'); + + // Update timestamps + if (resourceType === 'georesource') { + this.featureTable_georesource_lastUpdate_timestamp_success = this.getCurrentTimestamp(); + } else { + this.featureTable_spatialUnit_lastUpdate_timestamp_success = this.getCurrentTimestamp(); + } + + // Broadcast delete event + this.broadcastService.broadcast(`onDeleteFeatureEntry_${resourceType}`, { + datasetId, + featureId, + recordId + }); + }, + error: (error) => { + console.error('Error while deleting database record:', error); + + // Broadcast hide loading event + this.broadcastService.broadcast(`hideLoadingIcon_${resourceType}`, {}); + + // Update failure timestamps + if (resourceType === 'georesource') { + this.featureTable_georesource_lastUpdate_timestamp_failure = this.getCurrentTimestamp(); + } else { + this.featureTable_spatialUnit_lastUpdate_timestamp_failure = this.getCurrentTimestamp(); + } + } + }); + }; + + /** + * Get current timestamp + */ + private getCurrentTimestamp(): Date { + return new Date(); + } + + // Store current resource ID for delete handlers + private currentResourceId: string | undefined; + + /** + * Save grid state for feature table + */ + private saveGridStore_featureTable(gridOptions: any): void { + if (gridOptions && this.gridApi_featureTable) { + const selectedNodes = this.gridApi_featureTable.getSelectedNodes(); + gridOptions._savedState = { + selectedIds: selectedNodes.map((node: any) => { + const featureId = node.data.properties?.[__env?.FEATURE_ID_PROPERTY_NAME] || + node.data[__env?.FEATURE_ID_PROPERTY_NAME] || ''; + return featureId; + }) + }; + } + } + + /** + * Restore grid state for feature table + */ + private restoreGridStore_featureTable(gridOptions: any): void { + if (gridOptions && this.gridApi_featureTable && gridOptions._savedState) { + setTimeout(() => { + this.gridApi_featureTable?.forEachNode((node: any) => { + const featureId = node.data.properties?.[__env?.FEATURE_ID_PROPERTY_NAME] || + node.data[__env?.FEATURE_ID_PROPERTY_NAME] || ''; + if (gridOptions._savedState.selectedIds.includes(featureId)) { + node.setSelected(true); + } + }); + }, 100); + } + } + + /** + * Get currently selected features from feature table + */ + getSelectedFeatures(): any[] { + const selectedFeatures: any[] = []; + + if (this.dataGridOptions_featureTable && this.gridApi_featureTable) { + const selectedNodes = this.gridApi_featureTable.getSelectedNodes(); + for (const selectedNode of selectedNodes) { + selectedFeatures.push(selectedNode.data); + } + } + + return selectedFeatures; + } + + /** + * Clear feature table data + */ + clearFeatureTable(): void { + if (this.dataGridOptions_featureTable && this.gridApi_featureTable) { + this.gridApi_featureTable.setRowData([]); + } + } + + /** + * Refresh feature table with new data + */ + refreshFeatureTable(features: any[]): void { + if (this.dataGridOptions_featureTable && this.gridApi_featureTable) { + const newRowData = this.buildFeatureTableRowData(features); + this.gridApi_featureTable.setRowData(newRowData); + } + } + + /** + * Refresh spatial units grid with new data + */ + refreshSpatialUnitsGrid(spatialUnitMetadataArray: any[]): void { + this.currentSpatialUnitsData = spatialUnitMetadataArray; + + if (this.gridApi_spatialUnits) { + const newRowData = this.buildDataGridRowData_spatialUnits(spatialUnitMetadataArray); + this.gridApi_spatialUnits.setRowData(newRowData); + // Re-register click handlers after data update + setTimeout(() => this.registerClickHandler_spatialUnits(), 100); + } + } + + /** + * Get current spatial units grid options + */ + getSpatialUnitsGridOptions(): GridOptions | null { + return this.dataGridOptions_spatialUnits; + } + + /** + * Get current feature table grid options + */ + getFeatureTableGridOptions(): GridOptions | null { + return this.dataGridOptions_featureTable; + } + + /** + * Set the grid API for role management operations + */ + setGridApi(gridApi: GridApi): void { + this.gridApi_spatialUnits = gridApi; + } + + /** + * Handle cell value changes for feature table + */ + private handleCellValueChanged(newValueParams: any, resourceId?: string, resourceType?: string): void { + // Validate date properties + if (!newValueParams.data.validStartDate) { + newValueParams.data.validStartDate = newValueParams.oldValue; + } + + const isDate = (date: any) => { + const dateObj = new Date(date); + return dateObj.toString() !== "Invalid Date" && !isNaN(dateObj.getTime()); + }; + + if (!isDate(newValueParams.data.validStartDate)) { + newValueParams.data.validStartDate = newValueParams.oldValue; + } + + if (newValueParams.data.validEndDate === "") { + newValueParams.data.validEndDate = undefined; + } + + if (newValueParams.data.validEndDate) { + if (!isDate(newValueParams.data.validEndDate)) { + newValueParams.data.validEndDate = newValueParams.oldValue; + } + } + + // Build GeoJSON for API request + const geoJSON: any = { + "type": "Feature", + geometry: null, + properties: null, + id: null + }; + + // Clone properties and extract geometry/ID + geoJSON.geometry = JSON.parse(JSON.stringify(newValueParams.data.kommonitorGeometry)); + geoJSON.id = JSON.parse(JSON.stringify(newValueParams.data.kommonitorRecordId)); + geoJSON.properties = JSON.parse(JSON.stringify(newValueParams.data)); + + // Remove internal properties + delete geoJSON.properties.kommonitorGeometry; + delete geoJSON.properties.kommonitorRecordId; + + // Build URL + let url = `${this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI}`; + if (resourceType === this.resourceType_georesource) { + url += "/georesources/"; + } else { + url += "/spatial-units/"; + } + + url += `${resourceId}/singleFeature/${newValueParams.data.ID}/singleFeatureRecord/${newValueParams.data.kommonitorRecordId}`; + + // Make HTTP PUT request + this.http.put(url, geoJSON, { + headers: { + 'Content-Type': 'application/json' + } + }).subscribe({ + next: (response: any) => { + console.log("Successfully updated database record"); + + // On success: mark grid cell with green background + newValueParams.colDef.cellStyle = (p: any) => + p.rowIndex.toString() === newValueParams.node.id ? {'background-color': '#9DC89F'} : ""; + + newValueParams.api.refreshCells({ + force: true, + columns: [newValueParams.column.getId()], + rowNodes: [newValueParams.node] + }); + + // Update success timestamp + if (resourceType === this.resourceType_georesource) { + this.featureTable_georesource_lastUpdate_timestamp_success = this.getCurrentTimestamp(); + } else { + this.featureTable_spatialUnit_lastUpdate_timestamp_success = this.getCurrentTimestamp(); + } + }, + error: (error) => { + console.error("Error while updating database record:", error); + + // Reset cell value as an error occurred + newValueParams.data[newValueParams.column.colId] = newValueParams.oldValue; + + // On failure: mark grid cell with red background + newValueParams.colDef.cellStyle = (p: any) => + p.rowIndex.toString() === newValueParams.node.id ? {'background-color': '#E79595'} : ""; + + newValueParams.api.refreshCells({ + force: true, + columns: [newValueParams.column.getId()], + rowNodes: [newValueParams.node] + }); + + // Update failure timestamp + if (resourceType === this.resourceType_georesource) { + this.featureTable_georesource_lastUpdate_timestamp_failure = this.getCurrentTimestamp(); + } else { + this.featureTable_spatialUnit_lastUpdate_timestamp_failure = this.getCurrentTimestamp(); + } + } + }); + } +} \ No newline at end of file diff --git a/app/services/adminSpatialUnit/kommonitor-importer-helper.service.ts b/app/services/adminSpatialUnit/kommonitor-importer-helper.service.ts new file mode 100644 index 000000000..6cac3ac5c --- /dev/null +++ b/app/services/adminSpatialUnit/kommonitor-importer-helper.service.ts @@ -0,0 +1,885 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, of, throwError } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; + +// TypeScript interfaces for better type safety +export interface ConverterDefinition { + encoding: string; + mimeType: string; + name: string; + parameters: Array<{ + name: string; + value: string; + }>; + schema?: string; +} + +export interface DatasourceTypeDefinition { + parameters: Array<{ + name: string; + value: string; + }>; + type: string; +} + +export interface PropertyMappingDefinition { + identifierProperty: string; + nameProperty: string; + validStartDateProperty?: string; + validEndDateProperty?: string; + arisenFromProperty?: string; + keepAttributes: boolean; + keepMissingOrNullValueAttributes: boolean; + attributes: Array<{ + name: string; + mappingName: string; + type: string; + }>; +} + +export interface AttributeMappingType { + displayName: string; + apiName: string; +} + +export interface Converter { + name: string; + type: string; + mimeTypes: string[]; + encodings: string[]; + schemas?: string[]; + parameters?: Array<{ + name: string; + mandatory: boolean; + }>; +} + +export interface DatasourceType { + type: string; + parameters: Array<{ + name: string; + mandatory: boolean; + }>; +} + +export interface MappingConfigStructure { + converter: { + encoding: string; + mimeType: string; + name: string; + parameters: Array<{ + name: string; + value: string; + }>; + schema: string; + }; + dataSource: { + parameters: Array<{ + name: string; + value: string; + }>; + type: string; + }; + propertyMapping: { + arisenFromProperty: string; + attributes: Array<{ + mappingName: string; + name: string; + type: string; + }>; + identifierProperty: string; + keepAttributes: boolean; + nameProperty: string; + validEndDateProperty: string; + validStartDateProperty: string; + }; + periodOfValidity: { + startDate: string; + endDate: string; + }; +} + +export interface ImporterResponse { + uri?: string; + errors?: any[]; + importedFeatures?: any[]; +} + +@Injectable({ + providedIn: 'root' +}) +export class KommonitorImporterHelperService { + private targetUrlToImporterService: string; + public availableConverters: Converter[] = []; + public availableDatasourceTypes: DatasourceType[] = []; + + // Static data structures + public readonly attributeMapping_attributeTypes: AttributeMappingType[] = [ + { + displayName: "Text/String", + apiName: "string" + }, + { + displayName: "Ganzzahl", + apiName: "integer" + }, + { + displayName: "Gleitkommazahl", + apiName: "float" + }, + { + displayName: "Datum", + apiName: "date" + } + ]; + + public readonly mappingConfigStructure: MappingConfigStructure = { + "converter": { + "encoding": "string", + "mimeType": "string", + "name": "string", + "parameters": [ + { + "name": "string", + "value": "string" + } + ], + "schema": "string" + }, + "dataSource": { + "parameters": [ + { + "name": "string", + "value": "string" + } + ], + "type": "FILE" + }, + "propertyMapping": { + "arisenFromProperty": "string", + "attributes": [ + { + "mappingName": "string", + "name": "string", + "type": "string" + } + ], + "identifierProperty": "string", + "keepAttributes": true, + "nameProperty": "string", + "validEndDateProperty": "string", + "validStartDateProperty": "string" + }, + "periodOfValidity": { + "startDate": "yyyy-mm-dd", + "endDate": "yyyy-mm-dd" + } + }; + + public readonly mappingConfigStructure_indicator = { + "converter": { + "encoding": "string", + "mimeType": "string", + "name": "string", + "parameters": [ + { + "name": "string", + "value": "string" + } + ], + "schema": "string" + }, + "dataSource": { + "parameters": [ + { + "name": "string", + "value": "string" + } + ], + "type": "FILE" + }, + "propertyMapping": { + "attributeMappings": [ + { + "mappingName": "string", + "name": "string", + "type": "string" + } + ], + "spatialReferenceKeyProperty": "string", + "timeseriesMappings": [ + { + "indicatorValueProperty": "string", + "timestamp": "string", + "timestampProperty": "string" + } + ] + }, + "targetSpatialUnitName": "string" + }; + + public readonly converterDefinition_singleFeatureImport: ConverterDefinition = { + "encoding": "UTF-8", + "mimeType": "application/geo+json", + "name": "GeoJSON", + "parameters": [ + { + "name": "CRS", + "value": "EPSG:4326" + } + ] + }; + + public readonly datasourceDefinition_singleFeatureImport: DatasourceTypeDefinition = { + "parameters": [ + { + "name": "payload", + "value": "geojsonValue" + } + ], + "type": "INLINE" + }; + + public readonly propertyMappingDefinition_singleFeatureImport: PropertyMappingDefinition = { + "identifierProperty": "ID", + "nameProperty": "NAME", + "keepAttributes": true, + "keepMissingOrNullValueAttributes": true, + "attributes": [] + }; + + constructor(private http: HttpClient) { + // Get the target URL from environment or configuration + this.targetUrlToImporterService = (window as any).__env?.targetUrlToImporterService || '/api/importer/'; + + // Initialize resources + this.fetchResourcesFromImporter(); + } + + /** + * Fetch all resources from importer service + */ + async fetchResourcesFromImporter(): Promise { + try { + console.log("Trying to fetch converters and datasourceTypes from importer service"); + + this.availableConverters = await this.fetchConverters(); + this.availableDatasourceTypes = await this.fetchDatasourceTypes(); + + if (!this.availableConverters || !this.availableDatasourceTypes) { + throw new Error("Notwendige Anbindung an Importer-Service ist fehlerhaft. Bitte wenden Sie sich an Ihren Administrator."); + } + + // Fetch details for each converter + for (let index = 0; index < this.availableConverters.length; index++) { + const converter = this.availableConverters[index]; + this.availableConverters[index] = await this.fetchConverterDetails(converter); + } + + // Fetch details for each datasource type + for (let k = 0; k < this.availableDatasourceTypes.length; k++) { + this.availableDatasourceTypes[k] = await this.fetchDatasourceTypeDetails(this.availableDatasourceTypes[k]); + } + } catch (error) { + console.error("Error fetching resources from importer:", error); + throw error; + } + } + + /** + * Filter converters based on resource type + */ + filterConverters(resourceType: string): (converter: Converter) => boolean { + return (converter: Converter) => { + if (resourceType === "georesource" && converter.name.includes("Indikator")) { + return false; + } + if (resourceType === "spatialUnit" && (converter.name.includes("Indikator") || converter.name.includes("Tabelle"))) { + return false; + } + if (resourceType === "indicator" && (converter.name.includes("Geokodierung") || converter.name.includes("Koordinate"))) { + return false; + } + return true; + }; + } + + /** + * Fetch converters from importer service + */ + async fetchConverters(): Promise { + return this.http.get(`${this.targetUrlToImporterService}converters`).toPromise() + .then(result => result || []) + .catch(error => { + console.error("Error while fetching converters from importer.", error); + throw error; + }); + } + + /** + * Fetch converter details from importer service + */ + async fetchConverterDetails(converter: Converter): Promise { + return this.http.get(`${this.targetUrlToImporterService}converters/${converter.name}`).toPromise() + .then(result => { + if (!result) { + throw new Error(`Converter ${converter.name} not found`); + } + return result; + }) + .catch(error => { + console.error(`Error while fetching converter for name '${converter.name}' from importer.`, error); + throw error; + }); + } + + /** + * Fetch datasource types from importer service + */ + async fetchDatasourceTypes(): Promise { + return this.http.get(`${this.targetUrlToImporterService}datasourceTypes`).toPromise() + .then(result => result || []) + .catch(error => { + console.error("Error while fetching datasourceTypes from importer.", error); + throw error; + }); + } + + /** + * Fetch datasource type details from importer service + */ + async fetchDatasourceTypeDetails(datasourceType: DatasourceType): Promise { + return this.http.get(`${this.targetUrlToImporterService}datasourceTypes/${datasourceType.type}`).toPromise() + .then(result => { + if (!result) { + throw new Error(`DatasourceType ${datasourceType.type} not found`); + } + return result; + }) + .catch(error => { + console.error(`Error while fetching datasourceType for type '${datasourceType.type}' from importer.`, error); + throw error; + }); + } + + /** + * Upload a new file to importer service + */ + async uploadNewFile(fileData: File, fileName: string): Promise { + console.log("Trying to POST to importer service to upload a new file."); + + const formdata = new FormData(); + formdata.append("filename", fileName); + formdata.append("file", fileData); + + return this.http.post(`${this.targetUrlToImporterService}upload`, formdata, { + responseType: 'text' + }).toPromise() + .then(result => result || '') + .catch(error => { + console.error("Error while posting to importer service.", error); + throw error; + }); + } + + /** + * Build converter definition from form values + */ + buildConverterDefinition( + selectedConverter: Converter, + converterParameterPrefix: string, + schema: string, + mimeType: string, + formValues?: { [key: string]: string } + ): ConverterDefinition | null { + const converterDefinition: ConverterDefinition = { + "encoding": selectedConverter.encodings[0], + "mimeType": selectedConverter.mimeTypes.filter(element => element === mimeType)[0], + "name": selectedConverter.name, + "parameters": [], + "schema": undefined + }; + + if (selectedConverter.schemas) { + if (schema === undefined || schema === null) { + return null; + } else { + converterDefinition.schema = schema; + } + } + + // Track whether CRS was provided explicitly + let hasExplicitCRS = false; + + if (selectedConverter.parameters && selectedConverter.parameters.length > 0) { + for (const parameter of selectedConverter.parameters) { + const parameterName = parameter.name; + const parameterValue = formValues ? formValues[parameterName] : + (document.getElementById(converterParameterPrefix + parameterName) as HTMLInputElement)?.value; + + if (parameter.mandatory && (parameterValue === undefined || parameterValue === null || parameterValue === "")) { + return null; + } else { + if (parameterValue && !(parameterValue === "")) { + converterDefinition.parameters.push({ + "name": parameterName, + "value": parameterValue + }); + if (parameterName === 'CRS') { + hasExplicitCRS = true; + } + } + } + } + } + + // If converter is OGC API - Features and CRS not provided, set sensible default + if (selectedConverter.name === "OGC API - Features" && !hasExplicitCRS) { + converterDefinition.parameters.push({ + name: 'CRS', + value: 'EPSG:4326' + }); + } + + return converterDefinition; + } + + /** + * Build datasource type definition from form values + */ + async buildDatasourceTypeDefinition( + selectedDatasourceType: DatasourceType, + datasourceTypeParameterPrefix: string, + datasourceFileInputId: string, + formValues?: { [key: string]: string } + ): Promise { + const datasourceTypeDefinition: DatasourceTypeDefinition = { + "parameters": [], + "type": selectedDatasourceType.type + }; + + if (selectedDatasourceType.type === "FILE") { + const fileInput = document.getElementById(datasourceFileInputId) as HTMLInputElement; + const file = fileInput?.files?.[0]; + + if (file === null || file === undefined) { + return null; + } + + let fileUploadName: string; + try { + fileUploadName = await this.uploadNewFile(file, file.name); + } catch (error) { + console.error("Error while uploading file to importer.", error); + throw error; + } + + datasourceTypeDefinition.parameters.push({ + "name": "NAME", + "value": fileUploadName + }); + } else { + if (selectedDatasourceType.parameters.length > 0) { + for (const parameter of selectedDatasourceType.parameters) { + const parameterName = parameter.name; + if (parameterName === "bbox") { + const bboxType = formValues ? formValues['bboxType'] : + (document.getElementById(datasourceTypeParameterPrefix + "bboxType") as HTMLInputElement)?.value; + + datasourceTypeDefinition.parameters.push({ + "name": "bboxType", + "value": bboxType + }); + + let value: string | undefined; + if (bboxType === 'ref') { + value = formValues ? formValues['bboxRef'] : + (document.getElementById(datasourceTypeParameterPrefix + "bboxRef") as HTMLInputElement)?.value; + } else { + const minx = formValues ? formValues['bbox_minx'] : + (document.getElementById(datasourceTypeParameterPrefix + "bbox_minx") as HTMLInputElement)?.value; + const miny = formValues ? formValues['bbox_miny'] : + (document.getElementById(datasourceTypeParameterPrefix + "bbox_miny") as HTMLInputElement)?.value; + const maxx = formValues ? formValues['bbox_maxx'] : + (document.getElementById(datasourceTypeParameterPrefix + "bbox_maxx") as HTMLInputElement)?.value; + const maxy = formValues ? formValues['bbox_maxy'] : + (document.getElementById(datasourceTypeParameterPrefix + "bbox_maxy") as HTMLInputElement)?.value; + value = minx + "," + miny + "," + maxx + "," + maxy; + } + + datasourceTypeDefinition.parameters.push({ + "name": "bbox", + "value": value + }); + } else { + const parameterValue = formValues ? formValues[parameterName] : + (document.getElementById(datasourceTypeParameterPrefix + parameterName) as HTMLInputElement)?.value; + + if (parameterValue === undefined || parameterValue === null) { + return datasourceTypeDefinition; + } else { + datasourceTypeDefinition.parameters.push({ + "name": parameterName, + "value": parameterValue + }); + } + } + } + } + } + + return datasourceTypeDefinition; + } + + /** + * Build property mapping for spatial resources + */ + buildPropertyMapping_spatialResource( + nameProperty: string, + idProperty: string, + validStartDateProperty: string, + validEndDateProperty: string, + arisenFromProperty: string, + keepAttributes: boolean, + keepMissingValues: boolean, + attributeMappings_adminView: any[] + ): PropertyMappingDefinition { + const finalValidStartDateProperty = validStartDateProperty === "" ? undefined : validStartDateProperty; + const finalValidEndDateProperty = validEndDateProperty === "" ? undefined : validEndDateProperty; + const finalArisenFromProperty = arisenFromProperty === "" ? undefined : arisenFromProperty; + + const propertyMapping: PropertyMappingDefinition = { + "arisenFromProperty": finalArisenFromProperty, + "identifierProperty": idProperty, + "nameProperty": nameProperty, + "validEndDateProperty": finalValidEndDateProperty, + "validStartDateProperty": finalValidStartDateProperty, + "keepAttributes": keepAttributes, + "keepMissingOrNullValueAttributes": keepMissingValues, + "attributes": [] + }; + + if (!keepAttributes) { + // add attribute mappings + attributeMappings_adminView.forEach(attributeMapping_adminView => { + propertyMapping.attributes.push({ + name: attributeMapping_adminView.sourceName, + mappingName: attributeMapping_adminView.destinationName, + type: attributeMapping_adminView.dataType.apiName + }); + }); + } + + return propertyMapping; + } + + /** + * Build property mapping for indicator resources + */ + buildPropertyMapping_indicatorResource( + spatialReferenceKeyProperty: string, + timeseriesMappings: any[], + keepMissingOrNullValueIndicator: boolean + ): any { + console.log(spatialReferenceKeyProperty); + console.log(timeseriesMappings); + console.log(keepMissingOrNullValueIndicator); + + return { + "spatialReferenceKeyProperty": spatialReferenceKeyProperty, + "timeseriesMappings": timeseriesMappings, + "keepMissingOrNullValueIndicator": keepMissingOrNullValueIndicator, + "attributeMappings": undefined + }; + } + + /** + * Register new spatial unit + */ + async registerNewSpatialUnit( + converterDefinition: ConverterDefinition, + datasourceTypeDefinition: DatasourceTypeDefinition, + propertyMappingDefinition: PropertyMappingDefinition, + spatialUnitPostBody_managementAPI: any, + isDryRun: boolean + ): Promise { + console.log("Trying to POST to importer service to register new spatial unit."); + + const postBody = { + "converter": converterDefinition, + "dataSource": datasourceTypeDefinition, + "propertyMapping": propertyMappingDefinition, + "spatialUnitPostBody": spatialUnitPostBody_managementAPI, + "dryRun": isDryRun + }; + + return this.http.post(`${this.targetUrlToImporterService}spatial-units`, postBody, { + headers: { + 'Content-Type': "application/json" + } + }).toPromise() + .then(result => { + if (!result) { + throw new Error("No response from importer service"); + } + return result; + }) + .catch(error => { + console.error("Error while posting to importer service.", error); + throw error; + }); + } + + /** + * Update spatial unit + */ + async updateSpatialUnit( + converterDefinition: ConverterDefinition, + datasourceTypeDefinition: DatasourceTypeDefinition, + propertyMappingDefinition: PropertyMappingDefinition, + spatialUnitId: string, + spatialUnitPutBody_managementAPI: any, + isDryRun: boolean + ): Promise { + console.log(`Trying to POST to importer service to update spatial unit with id '${spatialUnitId}'`); + + const postBody = { + "converter": converterDefinition, + "dataSource": datasourceTypeDefinition, + "propertyMapping": propertyMappingDefinition, + "spatialUnitId": spatialUnitId, + "spatialUnitPutBody": spatialUnitPutBody_managementAPI, + "dryRun": isDryRun + }; + + return this.http.post(`${this.targetUrlToImporterService}spatial-units/update`, postBody, { + headers: { + 'Content-Type': "application/json" + } + }).toPromise() + .then(result => { + if (!result) { + throw new Error("No response from importer service"); + } + return result; + }) + .catch(error => { + console.error("Error while posting to importer service.", error); + throw error; + }); + } + + /** + * Register new georesource + */ + async registerNewGeoresource( + converterDefinition: ConverterDefinition, + datasourceTypeDefinition: DatasourceTypeDefinition, + propertyMappingDefinition: PropertyMappingDefinition, + georesourcePostBody_managementAPI: any, + isDryRun: boolean + ): Promise { + console.log("Trying to POST to importer service to register new georesource."); + + const postBody = { + "converter": converterDefinition, + "dataSource": datasourceTypeDefinition, + "propertyMapping": propertyMappingDefinition, + "georesourcePostBody": georesourcePostBody_managementAPI, + "dryRun": isDryRun + }; + + return this.http.post(`${this.targetUrlToImporterService}georesources`, postBody, { + headers: { + 'Content-Type': "application/json" + } + }).toPromise() + .then(result => { + if (!result) { + throw new Error("No response from importer service"); + } + return result; + }) + .catch(error => { + console.error("Error while posting to importer service.", error); + throw error; + }); + } + + /** + * Update georesource + */ + async updateGeoresource( + converterDefinition: ConverterDefinition, + datasourceTypeDefinition: DatasourceTypeDefinition, + propertyMappingDefinition: PropertyMappingDefinition, + georesourceId: string, + georesourcePutBody_managementAPI: any, + isDryRun: boolean + ): Promise { + console.log(`Trying to POST to importer service to update georesource with id '${georesourceId}'`); + + const postBody = { + "converter": converterDefinition, + "dataSource": datasourceTypeDefinition, + "propertyMapping": propertyMappingDefinition, + "georesourceId": georesourceId, + "georesourcePutBody": georesourcePutBody_managementAPI, + "dryRun": isDryRun + }; + + return this.http.post(`${this.targetUrlToImporterService}georesources/update`, postBody, { + headers: { + 'Content-Type': "application/json" + } + }).toPromise() + .then(result => { + if (!result) { + throw new Error("No response from importer service"); + } + return result; + }) + .catch(error => { + console.error("Error while posting to importer service.", error); + throw error; + }); + } + + /** + * Register new indicator + */ + async registerNewIndicator( + converterDefinition: ConverterDefinition, + datasourceTypeDefinition: DatasourceTypeDefinition, + propertyMappingDefinition: PropertyMappingDefinition, + indicatorPostBody_managementAPI: any, + isDryRun: boolean + ): Promise { + console.log("Trying to POST to importer service to register new indicator."); + + const postBody = { + "converter": converterDefinition, + "dataSource": datasourceTypeDefinition, + "propertyMapping": propertyMappingDefinition, + "indicatorPostBody": indicatorPostBody_managementAPI, + "dryRun": isDryRun + }; + + return this.http.post(`${this.targetUrlToImporterService}indicators`, postBody, { + headers: { + 'Content-Type': "application/json" + } + }).toPromise() + .then(result => { + if (!result) { + throw new Error("No response from importer service"); + } + return result; + }) + .catch(error => { + console.error("Error while posting to importer service.", error); + throw error; + }); + } + + /** + * Update indicator + */ + async updateIndicator( + converterDefinition: ConverterDefinition, + datasourceTypeDefinition: DatasourceTypeDefinition, + propertyMappingDefinition: PropertyMappingDefinition, + indicatorId: string, + indicatorPutBody_managementAPI: any, + isDryRun: boolean + ): Promise { + console.log(`Trying to POST to importer service to update indicator with id '${indicatorId}'`); + + const postBody = { + "converter": converterDefinition, + "dataSource": datasourceTypeDefinition, + "propertyMapping": propertyMappingDefinition, + "indicatorId": indicatorId, + "indicatorPutBody": indicatorPutBody_managementAPI, + "dryRun": isDryRun + }; + + return this.http.post(`${this.targetUrlToImporterService}indicators/update`, postBody, { + headers: { + 'Content-Type': "application/json" + } + }).toPromise() + .then(result => { + if (!result) { + throw new Error("No response from importer service"); + } + return result; + }) + .catch(error => { + console.error("Error while posting to importer service.", error); + throw error; + }); + } + + /** + * Check if importer response contains errors + */ + importerResponseContainsErrors(importerResponse: ImporterResponse): boolean { + if (importerResponse.errors && importerResponse.errors.length > 0) { + return true; + } + return false; + } + + /** + * Get ID from importer response + */ + getIdFromImporterResponse(importerResponse: ImporterResponse): string | undefined { + if (importerResponse.uri) { + return importerResponse.uri; + } + return undefined; + } + + /** + * Get errors from importer response + */ + getErrorsFromImporterResponse(importerResponse: ImporterResponse): any[] | undefined { + if (importerResponse.errors) { + return importerResponse.errors; + } + return undefined; + } + + /** + * Get imported features from importer response + */ + getImportedFeaturesFromImporterResponse(importerResponse: ImporterResponse): any[] | undefined { + if (importerResponse.importedFeatures) { + return importerResponse.importedFeatures; + } + return undefined; + } + + /** + * Get available converters + */ + getAvailableConverters(): Converter[] { + return this.availableConverters; + } + + /** + * Get available datasource types + */ + getAvailableDatasourceTypes(): DatasourceType[] { + return this.availableDatasourceTypes; + } + + /** + * Get attribute mapping types + */ + getAttributeMappingTypes(): AttributeMappingType[] { + return this.attributeMapping_attributeTypes; + } +} \ No newline at end of file diff --git a/app/services/script-management/kommonitor-script-helper.service.ts b/app/services/script-management/kommonitor-script-helper.service.ts new file mode 100644 index 000000000..9c0f9029b --- /dev/null +++ b/app/services/script-management/kommonitor-script-helper.service.ts @@ -0,0 +1,249 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { KommonitorIndicatorDataExchangeService } from 'services/adminIndicatorUnit/kommonitor-data-exchange.service'; + +export interface ScriptDataTypeOption { + apiName: 'string' | 'integer' | 'double' | 'boolean'; + displayName: string; +} + +export interface ScriptParameter { + name: string; + description: string; + dataType: ScriptDataTypeOption['apiName'] | string; + defaultValue: any; + minParameterValueForNumericInputs?: number | null; + maxParameterValueForNumericInputs?: number | null; +} + +export interface IndicatorSummary { + indicatorId: string; + indicatorName: string; + unit?: string; + metadata?: any; +} + +export interface GeoresourceSummary { + georesourceId: string; + datasetName: string; + metadata?: any; +} + +@Injectable({ providedIn: 'root' }) +export class KommonitorScriptHelperService { + + // State exposed to component/template + targetIndicator: IndicatorSummary | null = null; + requiredIndicators_tmp: IndicatorSummary[] = []; + requiredGeoresources_tmp: GeoresourceSummary[] = []; + requiredScriptParameters_tmp: ScriptParameter[] = []; + + scriptCode_readableString: string | undefined = undefined; + + scriptFormulaHTML_overwriteTargetIndicatorMethod: boolean = false; + scriptFormulaHTML: string = ''; + + // Options + availableScriptDataTypes: ScriptDataTypeOption[] = [ + { apiName: 'string', displayName: 'Text' }, + { apiName: 'integer', displayName: 'Ganzzahl' }, + { apiName: 'double', displayName: 'Gleitkommazahl' }, + { apiName: 'boolean', displayName: 'Boolean' } + ]; + + availableScriptTypeOptions: any[] = []; + + constructor( + private http: HttpClient, + private indicatorExchange: KommonitorIndicatorDataExchangeService + ) {} + + // Lifecycle + reset(): void { + this.targetIndicator = null; + this.requiredIndicators_tmp = []; + this.requiredGeoresources_tmp = []; + this.requiredScriptParameters_tmp = []; + this.scriptCode_readableString = undefined; + this.scriptFormulaHTML_overwriteTargetIndicatorMethod = false; + this.scriptFormulaHTML = ''; + } + + // Indicators + addBaseIndicator(indicator: IndicatorSummary): void { + if (!indicator) { return; } + const exists = this.requiredIndicators_tmp.some(x => x?.indicatorId === indicator?.indicatorId); + if (!exists) { + this.requiredIndicators_tmp = [...this.requiredIndicators_tmp, indicator]; + } + } + + removeBaseIndicator(indicator: IndicatorSummary): void { + if (!indicator) { return; } + this.requiredIndicators_tmp = (this.requiredIndicators_tmp || []).filter(x => x?.indicatorId !== indicator?.indicatorId); + } + + // Georesources + addBaseGeoresource(geo: GeoresourceSummary): void { + if (!geo) { return; } + const exists = this.requiredGeoresources_tmp.some(x => x?.georesourceId === geo?.georesourceId); + if (!exists) { + this.requiredGeoresources_tmp = [...this.requiredGeoresources_tmp, geo]; + } + } + + removeBaseGeoresource(geo: GeoresourceSummary): void { + if (!geo) { return; } + this.requiredGeoresources_tmp = (this.requiredGeoresources_tmp || []).filter(x => x?.georesourceId !== geo?.georesourceId); + } + + // Parameters + addScriptParameter( + name: string, + description: string, + dataType: ScriptParameter['dataType'], + defaultValue: any, + minParameterValueForNumericInputs: number | null, + maxParameterValueForNumericInputs: number | null + ): void { + const param: ScriptParameter = { + name, + description, + dataType, + defaultValue, + minParameterValueForNumericInputs: minParameterValueForNumericInputs ?? undefined, + maxParameterValueForNumericInputs: maxParameterValueForNumericInputs ?? undefined + }; + // replace if same name exists + const idx = this.requiredScriptParameters_tmp.findIndex(p => p.name === name); + if (idx >= 0) { + const copy = [...this.requiredScriptParameters_tmp]; + copy[idx] = param; + this.requiredScriptParameters_tmp = copy; + } else { + this.requiredScriptParameters_tmp = [...this.requiredScriptParameters_tmp, param]; + } + } + + removeScriptParameter(param: ScriptParameter): void { + if (!param) { return; } + this.requiredScriptParameters_tmp = (this.requiredScriptParameters_tmp || []).filter(p => p.name !== param.name); + } + + removeScriptParameter_byName(name: string): void { + if (!name) { return; } + this.requiredScriptParameters_tmp = (this.requiredScriptParameters_tmp || []).filter(p => p.name !== name); + } + + // Remote operations (temporarily delegate to legacy helper if available) + async postNewScript(datasetName: string, description: string, targetIndicator: any): Promise { + const baseUrl = this.getManagementServiceBaseUrl(); + if (!baseUrl) { throw new Error('Management service baseUrl not configured'); } + if (!this.scriptCode_readableString) { throw new Error('Script code is empty'); } + + const postBody = { + name: datasetName, + description, + associatedIndicatorId: targetIndicator?.indicatorId, + requiredIndicatorIds: (this.requiredIndicators_tmp || []).map(ind => ind.indicatorId), + requiredGeoresourceIds: (this.requiredGeoresources_tmp || []).map(geo => geo.georesourceId), + variableProcessParameters: this.requiredScriptParameters_tmp || [], + scriptCodeBase64: this.encodeBase64(this.scriptCode_readableString) + } as any; + + return await this.http.post(`${baseUrl}process-scripts`, postBody, { + headers: { 'Content-Type': 'application/json' } + }).toPromise(); + } + + async replaceMethodMetadataForTargetIndicator(targetIndicator: any): Promise { + const baseDataApi = this.indicatorExchange.baseUrlToKomMonitorDataAPI; + if (!baseDataApi) { throw new Error('Data API baseUrl not configured'); } + if (!targetIndicator?.indicatorId) { throw new Error('Target indicator is missing id'); } + + const patchBody = this.buildIndicatorPatchBody(targetIndicator); + return await this.http.patch(`${baseDataApi}/indicators/${targetIndicator.indicatorId}`, patchBody).toPromise(); + } + + // Helpers + prettifyScriptCodePreview(htmlDomElementId: string): void { + try { + setTimeout(() => { + try { + const PR: any = (window as any).PR; + if (!PR || !PR.prettyPrint) { return; } + const el = document.querySelector(htmlDomElementId) as HTMLElement | null; + if (el) { el.classList.remove('prettyprinted'); } + PR.prettyPrint(); + } catch {} + }, 250); + } catch {} + } + + private getManagementServiceBaseUrl(): string { + try { + const env: any = (window as any).__env || {}; + const apiUrl: string = env.apiUrl || ''; + const basePath: string = env.basePath || ''; + const base = `${apiUrl}${basePath}/`; + return base; + } catch { return ''; } + } + + private encodeBase64(data: string): string { + try { return window.btoa(data); } catch { return ''; } + } + + private buildIndicatorPatchBody(targetIndicatorMetadata: any): any { + const md = targetIndicatorMetadata?.metadata || {}; + const patchBody: any = { + metadata: { + note: md.note ?? null, + literature: md.literature ?? null, + updateInterval: md.updateInterval, + sridEPSG: md.sridEPSG ?? 4326, + datasource: md.datasource, + contact: md.contact, + lastUpdate: md.lastUpdate, + description: md.description ?? null, + databasis: md.databasis ?? null + }, + refrencesToOtherIndicators: [], + permissions: targetIndicatorMetadata?.permissions, + datasetName: targetIndicatorMetadata?.indicatorName, + abbreviation: targetIndicatorMetadata?.abbreviation ?? null, + characteristicValue: targetIndicatorMetadata?.characteristicValue ?? null, + tags: targetIndicatorMetadata?.tags, + creationType: targetIndicatorMetadata?.creationType, + unit: targetIndicatorMetadata?.unit, + topicReference: targetIndicatorMetadata?.topicReference, + refrencesToGeoresources: [], + indicatorType: targetIndicatorMetadata?.indicatorType, + interpretation: targetIndicatorMetadata?.interpretation ?? '', + isHeadlineIndicator: targetIndicatorMetadata?.isHeadlineIndicator ?? false, + processDescription: this.scriptFormulaHTML || targetIndicatorMetadata?.processDescription, + lowestSpatialUnitForComputation: targetIndicatorMetadata?.lowestSpatialUnitForComputation, + defaultClassificationMapping: targetIndicatorMetadata?.defaultClassificationMapping + }; + + const refInds = targetIndicatorMetadata?.referencedIndicators || []; + for (const indicRef of refInds) { + patchBody.refrencesToOtherIndicators.push({ + indicatorId: indicRef.referencedIndicatorId, + referenceDescription: indicRef.referencedIndicatorDescription + }); + } + + const refGeos = targetIndicatorMetadata?.referencedGeoresources || []; + for (const geoRef of refGeos) { + patchBody.refrencesToGeoresources.push({ + georesourceId: geoRef.referencedGeoresourceId, + referenceDescription: geoRef.referencedGeoresourceDescription + }); + } + + return patchBody; + } +} + + diff --git a/app/services/script-management/kommonitor-script-management-data-exchange.service.ts b/app/services/script-management/kommonitor-script-management-data-exchange.service.ts new file mode 100644 index 000000000..afd0550bf --- /dev/null +++ b/app/services/script-management/kommonitor-script-management-data-exchange.service.ts @@ -0,0 +1,62 @@ +import { Injectable, Inject } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class KommonitorScriptManagementDataExchangeService { + + constructor( + @Inject('kommonitorDataExchangeService') private angularJsDataExchangeService: any + ) {} + + // expose scripts list + get availableProcessScripts(): any[] { + const arr = this.angularJsDataExchangeService?.availableProcessScripts || []; + try { console.debug('[ScriptMgmtExchange] availableProcessScripts length:', Array.isArray(arr) ? arr.length : 0); } catch {} + return arr; + } + + // roles passthrough + get currentKeycloakLoginRoles(): string[] { + return this.angularJsDataExchangeService?.currentKeycloakLoginRoles || []; + } + + // metadata fetching + async fetchIndicatorScriptsMetadata(keycloakRolesArray: string[]): Promise { + try { + console.debug('[ScriptMgmtExchange] fetchIndicatorScriptsMetadata called with roles:', keycloakRolesArray); + const res = await this.angularJsDataExchangeService?.fetchIndicatorScriptsMetadata?.(keycloakRolesArray); + try { console.debug('[ScriptMgmtExchange] fetchIndicatorScriptsMetadata resolved. Now have length:', (this.availableProcessScripts || []).length); } catch {} + return res; + } catch (e) { + console.error('[ScriptMgmtExchange] fetchIndicatorScriptsMetadata failed:', e); + throw e; + } + } + + // CRUD local cache operations (delegated) + addSingleProcessScriptMetadata(scriptMetadata: any): void { + this.angularJsDataExchangeService?.addSingleProcessScriptMetadata?.(scriptMetadata); + } + + replaceSingleProcessScriptMetadata(scriptMetadata: any): void { + this.angularJsDataExchangeService?.replaceSingleProcessScriptMetadata?.(scriptMetadata); + } + + deleteSingleProcessScriptMetadata(scriptId: string): void { + this.angularJsDataExchangeService?.deleteSingleProcessScriptMetadata?.(scriptId); + } + + getProcessScriptMetadataById(scriptId: string): any { + return this.angularJsDataExchangeService?.getProcessScriptMetadataById?.(scriptId); + } + + // permissions + checkCreatePermission(): boolean { + try { return !!this.angularJsDataExchangeService?.checkCreatePermission?.(); } catch { return false; } + } + + checkDeletePermission(): boolean { + try { return !!this.angularJsDataExchangeService?.checkDeletePermission?.(); } catch { return false; } + } +} + + diff --git a/app/services/script-management/kommonitor-script-management-data-grid-helper.service.ts b/app/services/script-management/kommonitor-script-management-data-grid-helper.service.ts new file mode 100644 index 000000000..43ec4b69b --- /dev/null +++ b/app/services/script-management/kommonitor-script-management-data-grid-helper.service.ts @@ -0,0 +1,141 @@ +import { Injectable } from '@angular/core'; +import { ColDef, GridOptions } from 'ag-grid-community'; +import { KommonitorIndicatorDataExchangeService } from 'services/adminIndicatorUnit/kommonitor-data-exchange.service'; + +@Injectable({ providedIn: 'root' }) +export class KommonitorScriptManagementDataGridHelperService { + + constructor(private indicatorExchange: KommonitorIndicatorDataExchangeService) {} + + buildScriptsColumnDefs(): ColDef[] { + return [ + { headerName: 'Id', field: 'scriptId', pinned: 'left', maxWidth: 125, checkboxSelection: true, headerCheckboxSelection: true, headerCheckboxSelectionFilteredOnly: true }, + { headerName: 'Name', field: 'name', pinned: 'left', maxWidth: 300 }, + { headerName: 'Ziel-Indikatoren-Id', field: 'indicatorId', maxWidth: 125 }, + { headerName: 'Ziel-Indikatoren-Name', minWidth: 200, cellRenderer: (params: any) => this.getIndicatorName(params?.data?.indicatorId), + filter: 'agTextColumnFilter', filterValueGetter: (p: any) => this.getIndicatorName(p?.data?.indicatorId) }, + { headerName: 'Beschreibung', field: 'description', minWidth: 300 }, + { headerName: 'notwendige Basis-Indikatoren', minWidth: 300, cellRenderer: (params: any) => this.renderRequiredIndicators(params), + filter: 'agTextColumnFilter', filterValueGetter: (p: any) => this.requiredIndicatorsFilterValue(p) }, + { headerName: 'notwendige Basis-Georessourcen', minWidth: 300, cellRenderer: (params: any) => this.renderRequiredGeoresources(params), + filter: 'agTextColumnFilter', filterValueGetter: (p: any) => this.requiredGeoresourcesFilterValue(p) }, + { headerName: 'Prozessparameter', minWidth: 1000, cellRenderer: (params: any) => this.renderProcessParameters(params), + filter: 'agTextColumnFilter', filterValueGetter: (p: any) => JSON.stringify(p?.data?.variableProcessParameters || []) } + ]; + } + + buildGridOptions(columnDefs: ColDef[], rowData: any[]): GridOptions { + return { + columnDefs, + rowData, + defaultColDef: { + editable: false, + sortable: true, + flex: 1, + minWidth: 200, + filter: true, + floatingFilter: true, + resizable: true, + wrapText: true, + autoHeight: true, + cellStyle: { 'font-size': '12px', 'white-space': 'normal !important', 'line-height': '20px !important', 'word-break': 'break-word !important', 'padding-top': '17px', 'padding-bottom': '17px' } + }, + suppressRowClickSelection: true, + rowSelection: 'multiple', + enableCellTextSelection: true, + ensureDomOrder: true, + pagination: true, + paginationPageSize: 10, + suppressColumnVirtualisation: true + } as GridOptions; + } + + getSelectedScriptsFromApi(gridApi: any): any[] { + try { + const selectedNodes = gridApi?.getSelectedNodes?.() || []; + return selectedNodes.map((n: any) => n.data); + } catch { return []; } + } + + private getIndicatorName(indicatorId: string): string { + try { + const md = this.indicatorExchange.getIndicatorMetadataById(indicatorId); + return md ? (md.indicatorName || md.indicatorLabel || '') : ''; + } catch { return ''; } + } + + private renderRequiredIndicators(params: any): string { + const ids: string[] = params?.data?.requiredIndicatorIds || []; + if (!ids.length) { return 'keine'; } + let html = ''; + for (const id of ids) { + html += ''; + html += ``; + html += ``; + html += ''; + } + html += '
IdName
${id}${this.getIndicatorName(id)}
'; + return html; + } + + private requiredIndicatorsFilterValue(p: any): string { + const ids: string[] = p?.data?.requiredIndicatorIds || []; + if (!ids.length) { return 'keine'; } + let value = JSON.stringify(ids); + for (const id of ids) { value += this.getIndicatorName(id); } + return value; + } + + private renderRequiredGeoresources(params: any): string { + const ids: string[] = params?.data?.requiredGeoresourceIds || []; + if (!ids.length) { return 'keine'; } + let html = ''; + for (const id of ids) { + html += ''; + html += ``; + html += ``; + html += ''; + } + html += '
IdName
${id}${this.getGeoresourceName(id)}
'; + return html; + } + + private requiredGeoresourcesFilterValue(p: any): string { + const ids: string[] = p?.data?.requiredGeoresourceIds || []; + if (!ids.length) { return 'keine'; } + let value = JSON.stringify(ids); + for (const id of ids) { value += this.getGeoresourceName(id); } + return value; + } + + private getGeoresourceName(georesourceId: string): string { + try { + const md = this.indicatorExchange.getGeoresourceMetadataById(georesourceId); + return md ? (md.datasetName || md.name || '') : ''; + } catch { return ''; } + } + + private renderProcessParameters(params: any): string { + const list = params?.data?.variableProcessParameters || []; + if (!list.length) { return 'keine'; } + let html = ''; + for (const p of list) { + html += ''; + html += ``; + html += ``; + html += ``; + html += ``; + html += ''; + html += ''; + } + html += '
NameBeschreibungDatentypStandard-Werterlaubter Wertebereich
${p?.name ?? ''}${p?.description ?? ''}${p?.dataType ?? ''}${p?.defaultValue ?? ''}'; + if (p?.dataType === 'integer' || p?.dataType === 'double') { + html += 'erlaubter Wertebereich

'; + html += `${p?.minParameterValueForNumericInputs ?? ''} – ${p?.maxParameterValueForNumericInputs ?? ''}`; + } + html += '
'; + return html; + } +} + + diff --git a/app/util/genericServices/kommonitorDataGridHelperService/kommonitor-data-grid-helper-service.module.js b/app/util/genericServices/kommonitorDataGridHelperService/kommonitor-data-grid-helper-service.module.js index 56c2632e6..117673560 100644 --- a/app/util/genericServices/kommonitorDataGridHelperService/kommonitor-data-grid-helper-service.module.js +++ b/app/util/genericServices/kommonitorDataGridHelperService/kommonitor-data-grid-helper-service.module.js @@ -108,7 +108,7 @@ angular html += ''; html += ''; html += '' - html += '' + html += '' html += '
'; return html; @@ -1005,16 +1005,28 @@ angular $(".georesourceDeleteBtn").off(); $(".georesourceDeleteBtn").on("click", function (event) { // ensure that only the target button gets clicked - // manually open modal event.stopPropagation(); - let modalId = document.getElementById(this.id).getAttribute("data-target"); - $(modalId).modal('show'); - + let georesourceId = this.id.split("_")[3]; - let georesourceMetadata = kommonitorDataExchangeService.getGeoresourceMetadataById(georesourceId); - $rootScope.$broadcast("onDeleteGeoresources", [georesourceMetadata]); //handler function takes an array + // Try to use the new Angular component method first + try { + let angularComponent = angular.element(document.querySelector('admin-georesources-management-new')).controller('admin-georesources-management-new'); + if (angularComponent && angularComponent.onClickDeleteGeoresource) { + angularComponent.onClickDeleteGeoresource(georesourceMetadata); + } else { + // Fallback to AngularJS broadcast + let modalId = document.getElementById(this.id).getAttribute("data-target"); + $(modalId).modal('show'); + $rootScope.$broadcast("onDeleteGeoresources", [georesourceMetadata]); + } + } catch (error) { + // Fallback to AngularJS broadcast + let modalId = document.getElementById(this.id).getAttribute("data-target"); + $(modalId).modal('show'); + $rootScope.$broadcast("onDeleteGeoresources", [georesourceMetadata]); + } }); }; @@ -1338,16 +1350,28 @@ angular $(".spatialUnitEditUserRolesBtn").off(); $(".spatialUnitEditUserRolesBtn").on("click", function (event) { // ensure that only the target button gets clicked - // manually open modal event.stopPropagation(); - let modalId = document.getElementById(this.id).getAttribute("data-target"); - $(modalId).modal('show'); let spatialUnitId = this.id.split("_")[3]; - let spatialUnitMetadata = kommonitorDataExchangeService.getSpatialUnitMetadataById(spatialUnitId); - $rootScope.$broadcast("onEditSpatialUnitUserRoles", spatialUnitMetadata); + // Try to use the new Angular component method first + try { + let angularComponent = angular.element(document.querySelector('admin-spatial-units-management-new')).controller('admin-spatial-units-management-new'); + if (angularComponent && angularComponent.onClickEditUserRoles) { + angularComponent.onClickEditUserRoles(spatialUnitMetadata); + } else { + // Fallback to AngularJS broadcast + let modalId = document.getElementById(this.id).getAttribute("data-target"); + $(modalId).modal('show'); + $rootScope.$broadcast("onEditSpatialUnitUserRoles", spatialUnitMetadata); + } + } catch (error) { + // Fallback to AngularJS broadcast + let modalId = document.getElementById(this.id).getAttribute("data-target"); + $(modalId).modal('show'); + $rootScope.$broadcast("onEditSpatialUnitUserRoles", spatialUnitMetadata); + } }); $(".spatialUnitDeleteBtn").off(); diff --git a/package-lock.json b/package-lock.json index bd7788dfa..4c3b4455b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache 2.0", "dependencies": { "@angular/animations": "^16.1.3", + "@angular/cdk": "^16.2.12", "@angular/common": "^16.1.3", "@angular/compiler": "^16.1.3", "@angular/core": "^16.1.4", @@ -24,6 +25,7 @@ "@popperjs/core": "^2.11.8", "@turf/turf": "^7.2.0", "admin-lte": "^2.4.15", + "ag-grid-angular": "^31.3.4", "ag-grid-community": "^31.3.2", "angular": "^1.8.3", "angular-animations": "^0.11.0", @@ -78,6 +80,7 @@ "mathjax": "^3.2.2", "ng2-ion-range-slider": "^2.0.0", "ng2-nouislider": "^2.0.0", + "ngx-color": "^8.0.3", "nouislider": "^15.8.1", "papaparse": "^5.4.1", "rangeslide.js": "^0.13.0", @@ -578,6 +581,49 @@ "@angular/core": "16.2.12" } }, + "node_modules/@angular/cdk": { + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.2.12.tgz", + "integrity": "sha512-wT8/265zm2WKY0BDaRoYbrAT4kadrmejTRLjuimQIEUKnw4vBsJMWCwQkpFo3s6zr6eznGqYVAFb8KKPVLKGBg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@angular/common": "^16.0.0 || ^17.0.0", + "@angular/core": "^16.0.0 || ^17.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cdk/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@angular/cdk/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "optional": true, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/@angular/cli": { "version": "16.2.16", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.2.16.tgz", @@ -2884,6 +2930,15 @@ "node": ">=6.9.0" } }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -7479,6 +7534,20 @@ "slimscroll": "^0.9.1" } }, + "node_modules/ag-grid-angular": { + "version": "31.3.4", + "resolved": "https://registry.npmjs.org/ag-grid-angular/-/ag-grid-angular-31.3.4.tgz", + "integrity": "sha512-ELDqSc0R1fZRQBPTJgYWWF3Gbe7EbenmwzH3cNaQp38HbBQFkUcAvDqKHgSfmESe1GM76PoMHMxpCdqaWM3SmQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">= 14.0.0", + "@angular/core": ">= 14.0.0", + "ag-grid-community": "31.3.4" + } + }, "node_modules/ag-grid-community": { "version": "31.3.4", "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-31.3.4.tgz", @@ -19335,6 +19404,12 @@ "resolved": "https://registry.npmjs.org/marchingsquares/-/marchingsquares-1.3.3.tgz", "integrity": "sha512-gz6nNQoVK7Lkh2pZulrT4qd4347S/toG9RXH2pyzhLgkL5mLkBoqgv4EvAGXcV0ikDW72n/OQb3Xe8bGagQZCg==" }, + "node_modules/material-colors": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==", + "license": "ISC" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -20162,6 +20237,21 @@ "nouislider": ">=15.x" } }, + "node_modules/ngx-color": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/ngx-color/-/ngx-color-8.0.3.tgz", + "integrity": "sha512-tuLP+uIoDEu2m0bh711kb2P1M1bh/oIrOn8mJd9mb8xGL2v+OcokcxPmVvWRn0avMG1lXL53CjSlWXGkdV4CDA==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^3.4.1", + "material-colors": "^1.2.6", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=14.0.0-0", + "@angular/core": ">=14.0.0-0" + } + }, "node_modules/nice-napi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", diff --git a/package.json b/package.json index c9fe73fb7..5c20206cd 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ }, "dependencies": { "@angular/animations": "^16.1.3", + "@angular/cdk": "^16.2.12", "@angular/common": "^16.1.3", "@angular/compiler": "^16.1.3", "@angular/core": "^16.1.4", @@ -83,6 +84,7 @@ "@popperjs/core": "^2.11.8", "@turf/turf": "^7.2.0", "admin-lte": "^2.4.15", + "ag-grid-angular": "^31.3.4", "ag-grid-community": "^31.3.2", "angular": "^1.8.3", "angular-animations": "^0.11.0", @@ -137,6 +139,7 @@ "mathjax": "^3.2.2", "ng2-ion-range-slider": "^2.0.0", "ng2-nouislider": "^2.0.0", + "ngx-color": "^8.0.3", "nouislider": "^15.8.1", "papaparse": "^5.4.1", "rangeslide.js": "^0.13.0",