diff --git a/angular.json b/angular.json
index 0ae5f575d..ab6da1d69 100644
--- a/angular.json
+++ b/angular.json
@@ -27,7 +27,10 @@
"styles": [
"app/app.css",
"node_modules/bootstrap/dist/css/bootstrap.min.css",
- "node_modules/nouislider/dist/nouislider.css"
+ "node_modules/@fortawesome/fontawesome-free/css/all.min.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..1ee876a80 100644
--- a/app/app-upgraded-providers.ts
+++ b/app/app-upgraded-providers.ts
@@ -91,6 +91,17 @@ export const ajskommonitorFilterHelperServiceProvider: any = {
useFactory:kommonitorFilterHelperServiceFactory,
};
+//importer helper
+export function kommonitorImporterHelperServiceFactory (injector:any){
+ return injector.get('kommonitorImporterHelperService')
+}
+
+export const ajskommonitorImporterHelperServiceProvider: any = {
+ deps: ['$injector'],
+ provide: 'kommonitorImporterHelperService',
+ useFactory:kommonitorImporterHelperServiceFactory,
+ };
+
//keycloack helper
export function kommonitorKeycloackHelperServiceFactory (injector:any){
return injector.get('kommonitorKeycloackHelperService')
diff --git a/app/app.css b/app/app.css
index cc36431c4..99b44d30f 100644
--- a/app/app.css
+++ b/app/app.css
@@ -1,3 +1,76 @@
+/* Georesource Edit User Roles Modal: independent wide styling */
+body .modal-holder.georesource-edit-user-roles-modal-window .modal-dialog,
+body .georesource-edit-user-roles-modal-window .modal-dialog,
+body .modal.georesource-edit-user-roles-modal-window .modal-dialog,
+.modal-dialog.georesource-edit-user-roles-modal {
+ max-width: 95vw;
+ width: 95vw;
+}
+
+.georesource-edit-user-roles-modal .modal-content {
+ width: 100%;
+}
+/* Georesource Edit Metadata Modal: independent wide styling */
+body .modal-holder.georesource-edit-metadata-modal-window .modal-dialog,
+body .georesource-edit-metadata-modal-window .modal-dialog,
+body .modal.georesource-edit-metadata-modal-window .modal-dialog,
+.modal-dialog.georesource-edit-metadata-modal {
+ max-width: 95vw;
+ width: 95vw;
+}
+
+.georesource-edit-metadata-modal .modal-content {
+ width: 100%;
+}
+/* Georesource Edit Features Modal: independent wide styling */
+body .modal-holder.georesource-edit-features-modal-window .modal-dialog,
+body .georesource-edit-features-modal-window .modal-dialog,
+body .modal.georesource-edit-features-modal-window .modal-dialog,
+.modal-dialog.georesource-edit-features-modal {
+ max-width: 95vw;
+ width: 95vw;
+}
+
+.georesource-edit-features-modal .modal-content {
+ width: 100%;
+}
+/* Georesource Delete Modal: independent wide styling */
+body .modal-holder.georesource-delete-modal-window .modal-dialog,
+body .georesource-delete-modal-window .modal-dialog,
+body .modal.georesource-delete-modal-window .modal-dialog,
+.modal-dialog.georesource-delete-modal {
+ max-width: 95vw;
+ width: 95vw;
+}
+
+.georesource-delete-modal .modal-content {
+ width: 100%;
+}
+/* Georesource Batch Update Modal: independent wide styling */
+body .modal-holder.georesource-batch-update-modal-window .modal-dialog,
+body .georesource-batch-update-modal-window .modal-dialog,
+body .modal.georesource-batch-update-modal-window .modal-dialog,
+.modal-dialog.georesource-batch-update-modal {
+ max-width: 95vw;
+ width: 95vw;
+}
+
+.georesource-batch-update-modal .modal-content {
+ width: 100%;
+}
+/* Georesource Add Modal: match Spatial Unit modal sizing but with independent classes */
+body .modal-holder.georesource-add-modal-window .modal-dialog,
+body .georesource-add-modal-window .modal-dialog,
+body .modal.georesource-add-modal-window .modal-dialog,
+.modal-dialog.georesource-add-modal {
+ max-width: 95vw;
+ width: 95vw;
+}
+
+/* Optional: ensure content stretches appropriately */
+.georesource-add-modal .modal-content {
+ width: 100%;
+}
/* app css stylesheet */
:root {
@@ -1649,6 +1722,28 @@ 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;
+ }
+}
+
select {
overflow: auto;
}
diff --git a/app/app.module.ts b/app/app.module.ts
index 2559bb8b9..5faa7311f 100644
--- a/app/app.module.ts
+++ b/app/app.module.ts
@@ -4,6 +4,7 @@ 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';
@@ -21,6 +22,7 @@ import {
ajskommonitorDataGridHelperServiceProvider,
ajskommonitorDiagramHelperServiceProvider,
ajskommonitorFilterHelperServiceProvider,
+ ajskommonitorImporterHelperServiceProvider,
ajskommonitorKeycloackHelperServiceProvider,
ajskommonitorMultiStepFormHelperServiceProvider,
ajskommonitorSingleFeatureMapServiceProvider,
@@ -40,6 +42,7 @@ import { KommonitorLegendComponent } from 'components/ngComponents/userInterface
import { NgbCalendar, NgbDatepickerModule, NgbDateStruct, NgbAccordionModule, NgbModule } 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';
@@ -48,6 +51,9 @@ 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 { KmLinePatternPickerComponent } from './components/ngComponents/customElements/line-pattern-picker/km-line-pattern-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';
@@ -70,13 +76,39 @@ 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 { 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 { 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 { SingleFeatureEditComponent } from './components/ngComponents/common/single-feature-edit/single-feature-edit.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 { UserLoginComponent } from './components/ngComponents/userInterface/userLogin/user-login.component';
+import { ColorSketchModule } from 'ngx-color/sketch';
+import { IconPickerModule } from 'ngx-icon-picker';
+
// currently the AngularJS routing is still used as part of kommonitorClient module
const routes: Routes = [];
@@ -100,7 +132,17 @@ export function HttpLoaderFactory(http: HttpClient) {
JsonPipe,
NouisliderModule,
NgbCollapseModule,
+ DragDropModule,
DualListBoxComponent,
+ AgGridAngular,
+ ColorSketchModule,
+ IconPickerModule,
+ KmDatePickerComponent,
+ KmColorPickerComponent,
+ KmLinePatternPickerComponent,
+ GeoresourceAddModalComponent,
+ GeoresourceEditMetadataModalComponent,
+ AdminTopicsManagementComponent,
TranslateModule.forRoot({
defaultLanguage: 'de',
loader: {
@@ -122,6 +164,7 @@ export function HttpLoaderFactory(http: HttpClient) {
ajskommonitorSingleFeatureMapServiceProvider,
ajskommonitorDiagramHelperServiceProvider,
ajskommonitorFilterHelperServiceProvider,
+ ajskommonitorImporterHelperServiceProvider,
ajskommonitorElementVisibilityHelperServiceProvider,
ajskommonitorShareHelperServiceProvider,
ajskommonitorVisualStyleHelperServiceProvider,
@@ -160,18 +203,37 @@ export function HttpLoaderFactory(http: HttpClient) {
IndicatorFavFilter,
GeoFavFilter,
GeoFavItemFilter,
+ FilterPipe,
+ OrderByPipe,
BaseIndicatorOfComputedIndicatorFilter,
BaseIndicatorOfHeadlineIndicatorFilter,
RegressionDiagramComponent,
KommonitorReachabilityComponent,
LanguageSwitcherComponent,
- AdminTopicsManagementComponent,
TopicEditModalComponent,
TopicDeleteModalComponent,
AdminAppConfigComponent,
AdminControlsConfigComponent,
AdminRoleExplanationComponent,
AdminDashboardManagementComponent,
+ AdminSpatialUnitsManagementComponent,
+ SpatialUnitAddModalComponent,
+ SpatialUnitEditMetadataModalComponent,
+ SpatialUnitEditFeaturesModalComponent,
+ SpatialUnitEditUserRolesModalComponent,
+ SpatialUnitDeleteModalComponent,
+ AdminIndicatorsManagementComponent,
+ IndicatorAddModalComponent,
+ IndicatorEditMetadataModalComponent,
+ IndicatorEditFeaturesModalComponent,
+ IndicatorEditIndicatorSpatialUnitRolesModalComponent,
+ IndicatorDeleteModalComponent,
+ IndicatorBatchUpdateModalComponent,
+ AdminGeoresourcesManagementComponent,
+ GeoresourceBatchUpdateModalComponent,
+
+ GeoresourceEditUserRolesModalComponent,
+ GeoresourceDeleteModalComponent,
UserLoginComponent
],
schemas: [
@@ -309,6 +371,71 @@ export class AppModule implements DoBootstrap {
component: LanguageSwitcherComponent
}) 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('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);
+
console.log("registered downgraded Angular components for AngularJS usage");
}
diff --git a/app/components/kommonitorAdmin/kommonitor-admin.template.html b/app/components/kommonitorAdmin/kommonitor-admin.template.html
index 7940a2967..d7081d5fb 100644
--- a/app/components/kommonitorAdmin/kommonitor-admin.template.html
+++ b/app/components/kommonitorAdmin/kommonitor-admin.template.html
@@ -123,19 +123,19 @@
@@ -222,6 +222,9 @@
+
+
+
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..d38451fac
--- /dev/null
+++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/admin-georesources-management.component.html
@@ -0,0 +1,198 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..5c76190df
--- /dev/null
+++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/admin-georesources-management.component.ts
@@ -0,0 +1,636 @@
+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();
+
+ // Subscribe to service observables for reactive updates
+ this.subscribeToServiceObservables();
+ }
+
+ 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();
+ }
+
+ // Re-register click handlers after a delay to ensure DOM is ready
+ setTimeout(() => {
+ this.reRegisterClickHandlers();
+ }, 1000);
+ }
+
+ 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);
+ }
+
+ /**
+ * Subscribe to service observables for reactive updates
+ */
+ private subscribeToServiceObservables(): void {
+ // Subscribe to georesources updates
+ const georesourcesSub = this.kommonitorDataExchangeService.georesources$.subscribe(georesources => {
+ if (georesources && georesources.length > 0) {
+ this.initializeOrRefreshOverviewTable();
+ }
+ });
+
+ // Subscribe to loading state
+ const loadingSub = this.kommonitorDataExchangeService.loading$.subscribe(loading => {
+ this.loadingData = loading;
+ });
+
+ // Subscribe to error state
+ const errorSub = this.kommonitorDataExchangeService.error$.subscribe(error => {
+ if (error) {
+ console.error('Data exchange service error:', error);
+ // You could show a toast notification here
+ }
+ });
+
+ // Subscribe to cache helper service observables
+ const cacheLoadingSub = this.kommonitorCacheHelperService.loading$.subscribe(loading => {
+ if (loading) {
+ this.loadingData = true;
+ }
+ });
+
+ const cacheErrorSub = this.kommonitorCacheHelperService.error$.subscribe(error => {
+ if (error) {
+ console.error('Cache helper service error:', error);
+ // You could show a toast notification here
+ }
+ });
+
+ // Add all subscriptions to the array for cleanup
+ this.subscriptions.push(
+ georesourcesSub,
+ loadingSub,
+ errorSub,
+ cacheLoadingSub,
+ cacheErrorSub
+ );
+ }
+
+ 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;
+
+ // Re-register click handlers after grid is built
+ setTimeout(() => {
+ this.reRegisterClickHandlers();
+ }, 600);
+ }, 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((error: any) => {
+ console.error('Error refreshing georesource overview table:', error);
+ 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((error: any) => {
+ console.error('Error adding single georesource metadata:', error);
+ 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((error: any) => {
+ console.error('Error editing single georesource metadata:', error);
+ 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, {
+ // omit size to avoid Bootstrap max-width caps like modal-lg
+ backdrop: true,
+ keyboard: false,
+ container: 'body',
+ animation: false,
+ modalDialogClass: 'georesource-add-modal',
+ windowClass: 'georesource-add-modal-window'
+ });
+
+ modalRef.result.then((result) => {
+ if (result) {
+ // Handle successful add
+ this.refreshGeoresourceOverviewTable('add', result.georesourceId);
+ }
+ }).catch(() => {
+ // Modal dismissed
+ });
+ }
+
+ onClickBatchUpdateGeoresource(): void {
+ const modalRef = this.modalService.open(GeoresourceBatchUpdateModalComponent, {
+ // omit size to avoid Bootstrap max-width caps like modal-lg
+ backdrop: true,
+ keyboard: false,
+ container: 'body',
+ animation: false,
+ modalDialogClass: 'georesource-batch-update-modal',
+ windowClass: 'georesource-batch-update-modal-window'
+ });
+
+ modalRef.result.then((result) => {
+ if (result) {
+ this.initializeOrRefreshOverviewTable();
+ }
+ }).catch(() => {
+ // Modal dismissed
+ });
+ }
+
+ public onClickEditMetadata(georesourceDataset: any): void {
+ const modalRef = this.modalService.open(GeoresourceEditMetadataModalComponent, {
+ // omit size to avoid Bootstrap max-width caps like modal-lg
+ backdrop: true,
+ keyboard: false,
+ container: 'body',
+ animation: false,
+ modalDialogClass: 'georesource-edit-metadata-modal',
+ windowClass: 'georesource-edit-metadata-modal-window'
+ });
+
+ // 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, {
+ // omit size to avoid Bootstrap max-width caps like modal-lg
+ backdrop: true,
+ keyboard: false,
+ container: 'body',
+ animation: false,
+ modalDialogClass: 'georesource-edit-features-modal',
+ windowClass: 'georesource-edit-features-modal-window'
+ });
+
+ // 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, {
+ // omit size to avoid Bootstrap max-width caps like modal-lg
+ backdrop: true,
+ keyboard: false,
+ container: 'body',
+ animation: false,
+ modalDialogClass: 'georesource-edit-user-roles-modal',
+ windowClass: 'georesource-edit-user-roles-modal-window'
+ });
+ 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, {
+ // omit size to avoid Bootstrap max-width caps like modal-lg
+ backdrop: true,
+ keyboard: false,
+ container: 'body',
+ animation: false,
+ modalDialogClass: 'georesource-delete-modal',
+ windowClass: 'georesource-delete-modal-window'
+ });
+
+ // Pass the georesource dataset directly to the modal (array like original)
+ (modalRef.componentInstance as any).datasetsToDelete = [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 {
+ // Broadcast the event like the original AngularJS component
+ this.broadcastService.broadcast('onEditGeoresourceMetadata', georesourceDataset);
+
+ // Then open the modal
+ this.onClickEditMetadata(georesourceDataset);
+ }
+
+ onEditFeatures(georesourceDataset: any): void {
+ // Open the modal directly
+ this.onClickEditFeatures(georesourceDataset);
+ }
+
+ onEditUserRoles(georesourceDataset: any): void {
+ // Broadcast the event like the original AngularJS component
+ this.broadcastService.broadcast('onEditGeoresourceUserRoles', georesourceDataset);
+
+ // Then open the modal
+ this.onClickEditUserRoles(georesourceDataset);
+ }
+
+ /**
+ * Handle bulk deletion of selected georesources (like original AngularJS component)
+ */
+ onClickDeleteDatasets(): void {
+ this.loadingData = true;
+
+ const markedEntriesForDeletion = this.kommonitorDataGridHelperService.getSelectedGeoresourcesMetadata();
+
+ if (markedEntriesForDeletion && markedEntriesForDeletion.length > 0) {
+ // Submit selected georesources to modal controller
+ this.broadcastService.broadcast('onDeleteGeoresources', markedEntriesForDeletion);
+
+ // Refresh the table after deletion
+ setTimeout(() => {
+ this.initializeOrRefreshOverviewTable();
+ this.loadingData = false;
+ }, 100);
+ } else {
+ // No items selected
+ this.loadingData = false;
+ console.warn('No georesources selected for deletion');
+ }
+ }
+
+ /**
+ * Get selected georesources for bulk operations
+ */
+ getSelectedGeoresources(): any[] {
+ return this.kommonitorDataGridHelperService.getSelectedGeoresourcesMetadata();
+ }
+
+ /**
+ * Clear all grid selections
+ */
+ clearAllSelections(): void {
+ this.kommonitorDataGridHelperService.clearAllSelections();
+ }
+
+ /**
+ * Export grid data to CSV
+ */
+ exportGridToCsv(gridType: 'poi' | 'loi' | 'aoi'): void {
+ this.kommonitorDataGridHelperService.exportToCsv(gridType);
+ }
+
+ /**
+ * Save grid state for persistence
+ */
+ saveGridState(gridType: 'poi' | 'loi' | 'aoi'): void {
+ this.kommonitorDataGridHelperService.saveGridState(gridType);
+ }
+
+ /**
+ * Restore grid state from persistence
+ */
+ restoreGridState(gridType: 'poi' | 'loi' | 'aoi'): void {
+ this.kommonitorDataGridHelperService.restoreGridState(gridType);
+ }
+
+ /**
+ * Refresh all data from cache helper service
+ */
+ async refreshAllData(): Promise {
+ try {
+ this.loadingData = true;
+ await this.kommonitorCacheHelperService.refreshAllData(
+ this.kommonitorDataExchangeService.currentKeycloakLoginRoles
+ );
+ this.initializeOrRefreshOverviewTable();
+ } catch (error) {
+ console.error('Error refreshing all data:', error);
+ } finally {
+ this.loadingData = false;
+ }
+ }
+
+ /**
+ * Clear all caches
+ */
+ clearAllCaches(): void {
+ this.kommonitorCacheHelperService.clearAllCaches();
+ console.log('All caches cleared');
+ }
+
+ /**
+ * Manually re-register click handlers for grid buttons
+ */
+ reRegisterClickHandlers(): void {
+ this.kommonitorDataGridHelperService.reRegisterClickHandlers();
+ }
+
+ /**
+ * Force refresh grid data and re-register handlers
+ */
+ forceRefreshGrids(): void {
+ console.log('Force refreshing grids...');
+ this.loadingData = true;
+
+ // Re-register click handlers
+ this.reRegisterClickHandlers();
+
+ // Refresh the overview table
+ setTimeout(() => {
+ this.initializeOrRefreshOverviewTable();
+ this.loadingData = false;
+ }, 1000);
+ }
+
+ /**
+ * Debug method to check button state in DOM
+ */
+ debugButtonState(): void {
+ console.log('=== Debugging Button State ===');
+
+ const editMetadataButtons = document.querySelectorAll('.georesourceEditMetadataBtn');
+ const editFeaturesButtons = document.querySelectorAll('.georesourceEditFeaturesBtn');
+ const editUserRolesButtons = document.querySelectorAll('.georesourceEditUserRolesBtn');
+ const deleteButtons = document.querySelectorAll('.georesourceDeleteBtn');
+
+ console.log('Edit Metadata Buttons:', editMetadataButtons.length);
+ editMetadataButtons.forEach((btn: any, index) => {
+ console.log(` ${index}:`, btn.id, btn.className, btn.disabled);
+ });
+
+ console.log('Edit Features Buttons:', editFeaturesButtons.length);
+ editFeaturesButtons.forEach((btn: any, index) => {
+ console.log(` ${index}:`, btn.id, btn.className, btn.disabled);
+ });
+
+ console.log('Edit User Roles Buttons:', editUserRolesButtons.length);
+ editUserRolesButtons.forEach((btn: any, index) => {
+ console.log(` ${index}:`, btn.id, btn.className, btn.disabled);
+ });
+
+ console.log('Delete Buttons:', deleteButtons.length);
+ deleteButtons.forEach((btn: any, index) => {
+ console.log(` ${index}:`, btn.id, btn.className, btn.disabled);
+ });
+
+ console.log('=== End Debug ===');
+ }
+}
\ 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..1d6b6da19
--- /dev/null
+++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceAddModal/georesource-add-modal.component.css
@@ -0,0 +1,697 @@
+/* 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.9);
+ z-index: 99999999; /* ensure above any popovers/dialog content */
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ pointer-events: all;
+ color: #2C3E50;
+}
+
+.loading-overlay-admin-panel.ng-hide {
+ display: none !important;
+}
+
+/* Make sure the spinner is visible over the overlay */
+.loading-overlay-admin-panel .glyphicon {
+ font-size: 28px;
+ color: #2C3E50;
+}
+
+/* Multi-step form styles */
+.multiStepForm {
+ margin-bottom: 0px;
+}
+
+/*progressbar*/
+#progressbar {
+ margin-bottom: 10px;
+ overflow: hidden;
+ /*CSS counters to number the steps*/
+ counter-reset: step;
+ /* z-index: 10000; */
+}
+
+#progressbar li {
+ list-style-type: none;
+ color: black;
+ text-transform: uppercase;
+ font-size: 9px;
+ /* width: 33.33%; */
+ float: left;
+ position: relative;
+ letter-spacing: 1px;
+ /* transform-style: preserve-3d; */
+ /* z-index: 1; */
+ 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);
+ /* z-index: +1; */
+}
+
+/*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;
+ /* transform: translateZ(-2px); */
+}
+
+#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;
+}
+
+/* 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 span {
+ padding: 5px 10px;
+ display: block;
+ cursor: pointer;
+}
+
+.customColorPicker .dropdown-menu li span i {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ margin-right: 10px;
+ border: 1px solid #ccc;
+ vertical-align: middle;
+}
+
+/* Ensure dropdown items are properly styled */
+.customColorPicker .dropdown-menu li {
+ cursor: pointer;
+}
+
+.customColorPicker .dropdown-menu li:hover {
+ background-color: #f5f5f5;
+}
+
+/* Style for the marker style dropdown items */
+.dropdown-menu li span {
+ display: block;
+ padding: 5px 10px;
+ cursor: pointer;
+}
+
+.dropdown-menu li span:hover {
+ background-color: #f5f5f5;
+}
+
+/* Ensure dropdown is visible when show class is applied */
+.dropdown-menu.show {
+ display: block !important;
+}
+
+/* Additional styling for custom dropdown */
+.customColorPicker .dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 1000;
+ float: left;
+ min-width: 160px;
+ padding: 5px 0;
+ margin: 2px 0 0;
+ font-size: 14px;
+ text-align: left;
+ list-style: none;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 1px solid #ccc;
+ border: 1px solid rgba(0,0,0,.15);
+ border-radius: 4px;
+ box-shadow: 0 6px 12px rgba(0,0,0,.175);
+}
+
+/* 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 {
+
+ 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;
+ }
+}
+
+/* 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;
+}
+
+
+/* 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;
+}
+
+/* Progress bar step content styling */
+fieldset {
+ display: block;
+ margin: 0;
+ padding: 1rem 3.5rem;
+ border: 0;
+ outline: 0;
+ min-height: 400px;
+}
+
+/* Progress bar step navigation */
+#progressbar li {
+ transition: all 0.3s ease;
+}
+
+#progressbar li:hover {
+ color: var(--kommonitor-primary);
+}
+
+#progressbar li:hover:before {
+ background: var(--kommonitor-primary);
+ color: white;
+}
+
+/* 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;
+}
+
+/* Icon picker styling */
+#poiSymbolPicker {
+ min-width: 120px;
+ text-align: left;
+}
+
+#poiSymbolPicker .glyphicon {
+ margin-right: 5px;
+}
+
+/* Ensure glyphicons are visible */
+.glyphicon {
+ font-family: 'Glyphicons Halflings';
+ font-style: normal;
+ font-weight: normal;
+ line-height: 1;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.glyphicon-home:before {
+ content: "\e021";
+}
+
+.glyphicon-star:before {
+ content: "\e050";
+}
+
+.glyphicon-heart:before {
+ content: "\e005";
+}
+
+.glyphicon-user:before {
+ content: "\e008";
+}
+
+.glyphicon-cog:before {
+ content: "\e019";
+}
+
+/* Ensure the icon picker button is properly styled */
+#poiSymbolPicker {
+ position: relative;
+ min-width: 150px;
+ text-align: left;
+ padding: 8px 12px;
+}
+
+#poiSymbolPicker:hover {
+ background-color: #31b0d5;
+ border-color: #269abc;
+}
+
+/* Ensure Bootstrap Icon Picker dropdown is visible */
+.iconpicker-popover {
+ z-index: 9999999 !important;
+ position: absolute !important;
+ display: block !important;
+ visibility: visible !important;
+ opacity: 1 !important;
+}
+
+.iconpicker-popover.popover {
+ z-index: 9999999 !important;
+ display: block !important;
+ visibility: visible !important;
+ opacity: 1 !important;
+}
+
+.iconpicker-popover .popover-content {
+ z-index: 9999999 !important;
+ display: block !important;
+ visibility: visible !important;
+ opacity: 1 !important;
+}
+
+/* Ensure the popover is not clipped by parent containers */
+.iconpicker-popover,
+.iconpicker-popover.popover,
+.iconpicker-popover .popover-content {
+ overflow: visible !important;
+ clip: auto !important;
+ clip-path: none !important;
+}
+
+/* Ensure the modal doesn't clip the popover */
+.modal-body {
+ overflow: visible !important;
+}
+
+/* Ensure modal has proper positioning context */
+.modal {
+ position: relative !important;
+ overflow: visible !important;
+}
+
+/* Ensure modal-body doesn't clip content */
+.modal-body {
+ overflow: visible !important;
+ position: relative !important;
+}
+
+/* Ensure the form fieldset has proper positioning */
+fieldset {
+ position: relative !important;
+ overflow: visible !important;
+}
+
+/* Ensure the button's parent container has proper positioning */
+.form-group {
+ position: relative !important;
+ overflow: visible !important;
+}
+
+/* Ensure the button container has proper positioning */
+.col-md-3, .col-sm-6, .col-xs-12 {
+ position: relative !important;
+ overflow: visible !important;
+}
+
+/* Manual icon picker styling */
+.manual-icon-picker {
+ position: absolute !important;
+ z-index: 9999999 !important;
+ background: white !important;
+ border: 1px solid #ccc !important;
+ border-radius: 4px !important;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.2) !important;
+ max-width: 400px !important;
+ min-width: 300px !important;
+ pointer-events: auto !important;
+}
+
+/* Ensure the icon picker appears above other elements */
+.manual-icon-picker.popover {
+ z-index: 9999999 !important;
+}
+
+/* Ensure the icon picker content is visible */
+.manual-icon-picker .popover-content {
+ background: white !important;
+ border: none !important;
+ padding: 10px !important;
+}
+
+/* Ensure the form doesn't clip the popover */
+.multiStepForm {
+ overflow: visible !important;
+}
+
+/* Ensure the fieldset doesn't clip the popover */
+fieldset {
+ overflow: visible !important;
+}
+
+/* Ensure the icon picker popover appears above Bootstrap modal */
+.modal {
+ z-index: 1050 !important;
+}
+
+.iconpicker-popover {
+ z-index: 9999999 !important;
+}
+
+/* Additional fixes for icon picker visibility */
+.iconpicker-popover .table-icons {
+ display: table !important;
+ visibility: visible !important;
+}
+
+.iconpicker-popover .table-icons tbody {
+ display: table-row-group !important;
+ visibility: visible !important;
+}
+
+.iconpicker-popover .table-icons tr {
+ display: table-row !important;
+ visibility: visible !important;
+}
+
+.iconpicker-popover .table-icons td {
+ display: table-cell !important;
+ visibility: visible !important;
+}
+
+.iconpicker-popover .table-icons .btn {
+ display: inline-block !important;
+ visibility: visible !important;
+}
\ 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..15a6f049c
--- /dev/null
+++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceAddModal/georesource-add-modal.component.html
@@ -0,0 +1,909 @@
+
+
+
+
+
+
+
+ {{ loadingTopics && loadingAccessControl ? 'Lade Themen und Organisationseinheiten...' : (loadingTopics ? 'Lade Themen...' : (loadingAccessControl ? 'Lade Organisationseinheiten...' : 'Bitte warten...')) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
×
+
Georessource registriert
+
Eine neue Georessource mit Namen {{successMessagePart}} wurde in KomMonitor registriert und in die Übersichtstabelle eingetragen.
+ 0">
+ {{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:
+
+
+
+
Bitte beheben Sie die angezeigten Fehler im Datensatz und wiederholen den Prozess.
+
+
+
+
+
+
+
+
+
×
+
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..d9f1eb37c
--- /dev/null
+++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceAddModal/georesource-add-modal.component.ts
@@ -0,0 +1,2272 @@
+import { Component, OnInit, ViewChild, ElementRef, OnDestroy, ChangeDetectorRef, NgZone, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { BroadcastService } from 'services/broadcast-service/broadcast.service';
+import { HttpClient } from '@angular/common/http';
+import { AgGridAngular } from 'ag-grid-angular';
+import { Subscription } from 'rxjs';
+import { KommonitorGeoresourceDataExchangeService } from 'services/adminGeoresourceUnit/kommonitor-data-exchange.service';
+import { KommonitorImporterHelperService } from 'services/adminSpatialUnit/kommonitor-importer-helper.service';
+import { KommonitorMultiStepFormHelperService } from 'services/adminGeoresourceUnit/kommonitor-multi-step-form-helper.service';
+import { KommonitorGeoresourceDataGridHelperService } from 'services/adminGeoresourceUnit/kommonitor-data-grid-helper.service';
+import { IconPickerComponent } from 'components/ngComponents/customElements/icon-picker/icon-picker.component';
+import { KmDatePickerComponent } from 'components/ngComponents/customElements/date-picker/km-date-picker.component';
+import { KmLinePatternPickerComponent, LinePatternOption } from 'components/ngComponents/customElements/line-pattern-picker/km-line-pattern-picker.component';
+import { KmColorPickerComponent } from 'components/ngComponents/customElements/color-picker/km-color-picker.component';
+import { AdminTopicsManagementComponent } from '../../adminTopicsManagement/admin-topics-management.component';
+
+@Component({
+ selector: 'georesource-add-modal-new',
+ templateUrl: './georesource-add-modal.component.html',
+ styleUrls: ['./georesource-add-modal.component.css'],
+ providers: [],
+ standalone: true,
+ imports: [CommonModule, FormsModule, IconPickerComponent, AgGridAngular, KmDatePickerComponent, KmLinePatternPickerComponent, KmColorPickerComponent, AdminTopicsManagementComponent],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA]
+})
+export class GeoresourceAddModalComponent implements OnInit {
+ @ViewChild('metadataImportFile', { static: false }) metadataImportFile!: ElementRef;
+ @ViewChild('mappingConfigImportFile', { static: false }) mappingConfigImportFile!: ElementRef;
+ @ViewChild('georesourceDataSourceInput', { static: false }) georesourceDataSourceInput!: ElementRef;
+ @ViewChild('roleManagementGrid', { static: false }) roleManagementGrid!: AgGridAngular;
+
+ // 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;
+ selectedLoiPattern: LinePatternOption | null = null;
+ loiColor = '#bf3d2c';
+ loiWidth = 3;
+ aoiColor = '#bf3d2c';
+ selectedPoiIconName = 'home';
+ selectedPoiMarkerStyle = 'symbol';
+ poiMarkerText = '';
+ poiMarkerTextInvalid = false;
+
+ // Custom dropdown state
+ isMarkerStyleDropdownOpen = false;
+
+
+
+ // Period of validity
+ periodOfValidity: { startDate: string; endDate: string } = {
+ startDate: '',
+ endDate: ''
+ };
+ periodOfValidityInvalid = false;
+
+ // Available options
+ availableTopics: any[] = [];
+ updateIntervalOptions: any[] = [];
+ availablePoiMarkerColors: any[] = [];
+ availableLoiDashArrayObjects: any[] = [];
+ linePatternOptions: LinePatternOption[] = [];
+ availableDatasourceTypes: any[] = [];
+
+ // Loading states
+ loadingTopics = false;
+ loadingAccessControl = false;
+
+ get isModalLoading(): boolean {
+ return this.loadingData || this.loadingTopics || this.loadingAccessControl;
+ }
+
+ // Importer functionality
+ converter: any = null;
+ schema: string = '';
+ mimeType: string = '';
+ encoding: string = 'UTF-8';
+ datasourceType: any = null;
+ georesourceDataSourceIdProperty = '';
+ georesourceDataSourceIdPropertyInvalid = false;
+ georesourceDataSourceNameProperty = '';
+ georesourceDataSourceNamePropertyInvalid = false;
+ selectedDataSourceFile: File | null = null;
+ selectedDataSourceFileName: string = '';
+
+ // 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;
+
+ // Persisted converter/datasource parameter values
+ converterParameterValues: { [key: string]: string } = {};
+ datasourceTypeParameterValues: { [key: string]: string } = {};
+
+ // Validity dates per feature
+ validityStartDate_perFeature = '';
+ validityEndDate_perFeature = '';
+
+ // Event subscriptions for role management (like AngularJS component)
+ private roleUpdateSubscription?: Subscription;
+ private metadataLoadingSubscription?: Subscription;
+ private fileInputChangeHandler?: (e: Event) => void;
+
+ // Grid API references for role management
+ roleManagementGridApi: any = null;
+ roleManagementColumnApi: any = null;
+
+ // Role management
+ roleManagementTableOptions: any = null;
+ ownerOrganization = '';
+ ownerOrgFilter = '';
+ isPublic = false;
+ resourcesCreatorRights: any[] = [];
+ filteredOrganizations: any[] = [];
+ showRoleManagementForm = false;
+
+ // 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",
+ },
+ // legacy naming used in AngularJS example
+ "permissions": ['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 = '';
+
+ // Date helpers
+ 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 syncLinePatternOptionsAndSelection(): void {
+ // Map availableLoiDashArrayObjects into LinePatternOption[] used by km-line-pattern-picker
+ this.linePatternOptions = (this.availableLoiDashArrayObjects || []).map((o: any) => {
+ const display = o?.displayName || o?.dashArrayValue || '';
+ const dash = o?.dashArrayValue || '';
+ // Render an inline SVG showing the dash pattern
+ const svg = `
+
+
+
+ `;
+ return { label: display, dashArrayValue: dash, svgString: svg } as LinePatternOption;
+ });
+
+ // Align selected pattern with selectedLoiDashArrayObject
+ if (this.selectedLoiDashArrayObject) {
+ this.selectedLoiPattern = this.linePatternOptions.find(p => p.dashArrayValue === this.selectedLoiDashArrayObject.dashArrayValue) || null;
+ } else {
+ this.selectedLoiPattern = null;
+ }
+ }
+
+ 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 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();
+ }
+
+ onLastUpdateBlur(): void {
+ this.metadata.lastUpdate = this.ensureValidDateOrToday(this.metadata.lastUpdate);
+ }
+
+ 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();
+ }
+
+ 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;
+ }
+
+ // Icon picker configuration
+ iconPickerConfig = {
+ placeholder: 'Select Icon',
+ buttonClass: 'btn btn-info',
+ showSearch: true,
+ showHeader: true,
+ showFooter: true,
+ cols: 10,
+ rows: 6,
+ searchText: 'Search icons...',
+ labelHeader: '{0} of {1} pages',
+ labelFooter: '{0} - {1} of {2} icons'
+ };
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public kommonitorDataExchangeService: KommonitorGeoresourceDataExchangeService,
+ public kommonitorImporterHelperService: KommonitorImporterHelperService,
+ public kommonitorMultiStepFormHelperService: KommonitorMultiStepFormHelperService,
+ public kommonitorDataGridHelperService: KommonitorGeoresourceDataGridHelperService,
+ private broadcastService: BroadcastService,
+ private http: HttpClient,
+ private cdr: ChangeDetectorRef,
+ private ngZone: NgZone
+ ) {}
+
+ async ngOnInit(): Promise {
+ await this.initializeForm();
+ this.setupEventListeners();
+
+ // Add click outside handler for dropdown
+ document.addEventListener('click', this.onDocumentClick.bind(this));
+
+ // Attach file input listener in case template does not wire (change)
+ setTimeout(() => this.attachFileInputListener(), 0);
+ }
+
+
+
+ ngOnDestroy(): void {
+ // Clean up subscriptions
+ if (this.roleUpdateSubscription) {
+ this.roleUpdateSubscription.unsubscribe();
+ }
+ if (this.metadataLoadingSubscription) {
+ this.metadataLoadingSubscription.unsubscribe();
+ }
+
+ // Remove document click listener
+ document.removeEventListener('click', this.onDocumentClick.bind(this));
+
+ // Remove file input listener
+ try {
+ const inputEl = document.getElementById('georesourceDataSourceInput_add');
+ if (inputEl && this.fileInputChangeHandler) {
+ inputEl.removeEventListener('change', this.fileInputChangeHandler);
+ }
+ } catch {}
+ }
+
+ private async initializeForm(): Promise {
+ // Initialize form with default values and show overlay while bootstrapping
+ this.loadingData = true;
+ this.resetGeoresourceAddForm();
+
+ // Ensure importer resources (converters, datasource types) are fetched before binding options
+ try {
+ await this.kommonitorImporterHelperService.fetchResourcesFromImporter();
+ } catch (e) {
+ console.warn('[GeoresourceAddModal] Failed to fetch importer resources', e);
+ }
+
+ // Load available options (including topics)
+ await this.loadAvailableOptions();
+
+ // Initialize role management data (async)
+ await this.initializeResourcesCreatorRights();
+
+ // Adjust total steps based on security settings
+ this.totalSteps = this.kommonitorDataExchangeService.enableKeycloakSecurity ? 5 : 4;
+
+ // Initial load completed: hide overlay
+ this.loadingData = false;
+
+ // Reapply any dynamic importer fields that may have been set (e.g., from import)
+ setTimeout(() => this.reapplyDynamicImporterFields(), 0);
+ }
+
+ 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();
+ } else if (data.msg === 'topicsUpdated' || data.msg === 'refreshTopics') {
+ this.loadTopicsData();
+ }
+ });
+ }
+
+ /**
+ * Refresh topics data
+ */
+ async refreshTopics(): Promise {
+ await this.loadTopicsData();
+ }
+
+
+
+ // Initialize resources creator rights (for non-admin users)
+ private async initializeResourcesCreatorRights() {
+ try {
+ this.loadingAccessControl = true;
+ // Try to load real access control data first
+ await this.reloadAccessControlData();
+ } catch (error) {
+ console.warn('Failed to load access control data:', error);
+ // Do not inject test data; keep empty to avoid showing fake organizations
+ this.resourcesCreatorRights = [];
+ this.filteredOrganizations = [];
+ } finally {
+ this.loadingAccessControl = false;
+ }
+
+ // Initialize the role management table options with transformed data
+ this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid(
+ 'georesourceAddRoleManagementTable',
+ null,
+ this.resourcesCreatorRights,
+ []
+ );
+
+ // Initialize the role management table (like AngularJS component - initially hidden)
+ this.showRoleManagementForm = false;
+ this.refreshRoles();
+ }
+
+ private async loadAvailableOptions(): Promise {
+ // Load available options from services
+ this.updateIntervalOptions = this.kommonitorDataExchangeService.updateIntervalOptions || [];
+ this.availablePoiMarkerColors = this.kommonitorDataExchangeService.availablePoiMarkerColors || [];
+ this.availableLoiDashArrayObjects = this.kommonitorDataExchangeService.availableLoiDashArrayObjects || [];
+ this.availableDatasourceTypes = this.kommonitorImporterHelperService.getAvailableDatasourceTypes();
+ if (!this.availableDatasourceTypes || this.availableDatasourceTypes.length === 0) {
+ console.warn('[GeoresourceAddModal] No datasource types available from importer.');
+ }
+
+ // Ensure POI/LOI defaults after options are loaded
+ if (!this.selectedPoiMarkerColor && this.availablePoiMarkerColors.length > 0) {
+ this.selectedPoiMarkerColor = this.availablePoiMarkerColors[0];
+ }
+ if (!this.selectedPoiSymbolColor && this.availablePoiMarkerColors.length > 0) {
+ // Prefer the second entry if present (legacy behavior), else fall back to first
+ this.selectedPoiSymbolColor = this.availablePoiMarkerColors[1] || this.availablePoiMarkerColors[0];
+ }
+ if (!this.selectedLoiDashArrayObject && this.availableLoiDashArrayObjects.length > 0) {
+ this.selectedLoiDashArrayObject = this.availableLoiDashArrayObjects[0];
+ }
+ // Sync line pattern options and selection for LOI picker
+ this.syncLinePatternOptionsAndSelection();
+
+ // Initialize metadata structure pretty print
+ this.georesourceMetadataStructure_pretty = this.kommonitorDataExchangeService.syntaxHighlightJSON(this.georesourceMetadataStructure);
+ this.georesourceMappingConfigStructure_pretty = this.kommonitorDataExchangeService.syntaxHighlightJSON(this.kommonitorImporterHelperService.mappingConfigStructure);
+
+ // Load topics data
+ await this.loadTopicsData();
+ }
+
+ /**
+ * Load topics data from the API
+ */
+ private async loadTopicsData(): Promise {
+ try {
+ this.loadingTopics = true;
+
+ const roles = this.kommonitorDataExchangeService.currentKeycloakLoginRoles;
+ const topicsResult = await this.kommonitorDataExchangeService.fetchTopicsMetadata(roles);
+ // Prefer the service cache after fetch (AngularJS relied on service.availableTopics which preserves hierarchy)
+ const topics = (this.kommonitorDataExchangeService as any).availableTopics && Array.isArray((this.kommonitorDataExchangeService as any).availableTopics)
+ ? (this.kommonitorDataExchangeService as any).availableTopics
+ : topicsResult;
+
+ if (topics && Array.isArray(topics)) {
+ // Filter topics to only show main topics for georesources (like AngularJS component)
+ this.availableTopics = this.filterTopicsForGeoresources(topics);
+ // Normalize keys to ensure subtopic tree uses 'subTopics' recursively
+ this.availableTopics = this.normalizeTopics(this.availableTopics);
+
+ } else {
+ this.availableTopics = [];
+ }
+ } catch (error: any) {
+ console.error('Error loading topics data:', error);
+ this.availableTopics = [];
+ this.errorMessage = 'Fehler beim Laden der Themen. Verwende Testdaten.';
+ } finally {
+ this.loadingTopics = false;
+ }
+ }
+
+ /**
+ * Filter topics to only show main topics for georesources (like AngularJS component)
+ */
+ private filterTopicsForGeoresources(topics: any[]): any[] {
+ // Strictly enforce: only main topics with topicResource 'georesource'
+ const result = (topics || []).filter((topic: any) =>
+ topic && topic.topicType === 'main' && topic.topicResource === 'georesource'
+ );
+ return result;
+ }
+
+ // Normalize topic tree to always use 'subTopics' (maps 'subtopics' or 'children' etc.)
+ private normalizeTopics(topics: any[]): any[] {
+ return (topics || []).map(t => this.normalizeTopicNode(t));
+ }
+
+ private normalizeTopicNode(topic: any): any {
+ if (!topic || typeof topic !== 'object') { return topic; }
+ // Normalize label fallbacks (no changes applied to structure, just ensure presence for templates)
+ topic.topicName = topic.topicName || topic.name || topic.title || topic.label || topic.text || topic.topicname;
+ // Normalize child list
+ const children = topic.subTopics || topic.subtopics || topic.children || [];
+ topic.subTopics = Array.isArray(children) ? children.map((c: any) => this.normalizeTopicNode(c)) : [];
+ return topic;
+ }
+
+ // Called when the main topic changes to reset deeper selections and ensure normalization
+ onMainTopicChange(): void {
+ if (this.georesourceTopic_mainTopic) {
+ this.georesourceTopic_mainTopic = this.normalizeTopicNode(this.georesourceTopic_mainTopic);
+ }
+ this.georesourceTopic_subTopic = null;
+ this.georesourceTopic_subsubTopic = null;
+ this.georesourceTopic_subsubsubTopic = null;
+ }
+
+ onSubTopicChange(): void {
+ if (this.georesourceTopic_subTopic) {
+ this.georesourceTopic_subTopic = this.normalizeTopicNode(this.georesourceTopic_subTopic);
+ }
+ this.georesourceTopic_subsubTopic = null;
+ this.georesourceTopic_subsubsubTopic = null;
+ }
+
+ onSubSubTopicChange(): void {
+ if (this.georesourceTopic_subsubTopic) {
+ this.georesourceTopic_subsubTopic = this.normalizeTopicNode(this.georesourceTopic_subsubTopic);
+ }
+ this.georesourceTopic_subsubsubTopic = null;
+ }
+
+ /**
+ * Remove duplicates by displayed label (topicName/name), case-insensitive
+ */
+ private deduplicateTopicsByLabel(topics: any[]): any[] {
+ const map = new Map();
+ for (const t of topics) {
+ const label = ((t?.topicName ?? t?.name ?? '') + '').trim().toLowerCase();
+ const fallback = ((t?.topicId ?? t?.id ?? '') + '').trim().toLowerCase();
+ const key = label || fallback;
+ if (!key) { continue; }
+ if (!map.has(key)) {
+ map.set(key, t);
+ } else {
+ const current = map.get(key);
+ const currChildren = Array.isArray(current?.subTopics) ? current.subTopics.length : 0;
+ const newChildren = Array.isArray(t?.subTopics) ? t.subTopics.length : 0;
+ const currHasId = !!(current?.topicId || current?.id);
+ const newHasId = !!(t?.topicId || t?.id);
+ // Prefer the entry that has subTopics, or more children; fallback to one that has an id
+ if (newChildren > currChildren || (!currHasId && newHasId)) {
+ map.set(key, t);
+ }
+ }
+ }
+ return Array.from(map.values());
+ }
+
+ /**
+ * Remove duplicates by stable identifier (topicId | id | name fallback)
+ */
+ private deduplicateTopicsById(topics: any[]): any[] {
+ const map = new Map();
+ for (const t of topics) {
+ const key = ((t?.topicId ?? t?.id ?? t?.name) + '').trim();
+ if (!key) { continue; }
+ if (!map.has(key)) {
+ map.set(key, t);
+ } else {
+ const current = map.get(key);
+ const currChildren = Array.isArray(current?.subTopics) ? current.subTopics.length : 0;
+ const newChildren = Array.isArray(t?.subTopics) ? t.subTopics.length : 0;
+ if (newChildren > currChildren) {
+ map.set(key, t);
+ }
+ }
+ }
+ return Array.from(map.values());
+ }
+
+
+
+
+
+ // Handle role management grid ready event
+ onRoleManagementGridReady(params: any) {
+ // Store API references
+ this.roleManagementGridApi = params.api;
+ this.roleManagementColumnApi = params.columnApi;
+
+ // The grid is now ready and can be accessed via params.api
+ if (params.api) {
+ // Auto-size columns
+ params.api.sizeColumnsToFit();
+ // Ensure proper row heights
+ try {
+ params.api.resetRowHeights();
+ } catch {}
+
+ // Set the row data if we have it
+ if (this.roleManagementTableOptions && this.roleManagementTableOptions.rowData) {
+ params.api.setRowData(this.roleManagementTableOptions.rowData);
+ try {
+ params.api.resetRowHeights();
+ } catch {}
+ }
+ }
+ }
+
+ // Handle role management first data rendered event
+ onRoleManagementFirstDataRendered(params: any) {
+ try {
+ params.api.resetRowHeights();
+ params.api.sizeColumnsToFit();
+ } catch {}
+ }
+
+ // Handle role management column resized event
+ onRoleManagementColumnResized(params: any) {
+ try {
+ params.api.resetRowHeights();
+ } catch {}
+ }
+
+ // Handle role management model updated event
+ onRoleManagementModelUpdated() {
+ try {
+ this.roleManagementGridApi?.resetRowHeights();
+ } catch {}
+ }
+
+ // Handle role management viewport changed event
+ onRoleManagementViewportChanged() {
+ try {
+ this.roleManagementGridApi?.resetRowHeights();
+ } catch {}
+ }
+
+ // Refresh role management table
+ refreshRoleManagementTable() {
+ // Rebuild role management grid with current data
+ this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid(
+ 'georesourceAddRoleManagementTable',
+ this.roleManagementTableOptions,
+ this.resourcesCreatorRights,
+ this.kommonitorDataExchangeService.getCurrentKomMonitorLoginRoleIds()
+ );
+
+ // Refresh the grid if API is available
+ if (this.roleManagementGridApi) {
+ this.roleManagementGridApi.refreshCells();
+ this.roleManagementGridApi.redrawRows();
+ }
+ }
+
+ private refreshRoles(): void {
+ // Check if access control data is available
+ if (!this.kommonitorDataExchangeService.accessControl || this.kommonitorDataExchangeService.accessControl.length === 0) {
+ // Clear grid when no access control data is available; do not inject test data
+ this.roleManagementTableOptions = null;
+ setTimeout(() => {
+ if (this.roleManagementGrid && this.roleManagementGrid.api) {
+ this.roleManagementGrid.api.setRowData([]);
+ this.roleManagementGrid.api.refreshCells();
+ this.roleManagementGrid.api.redrawRows();
+ }
+ }, 100);
+ return;
+ }
+
+ // Get permission IDs for the selected organization (like AngularJS component)
+ let permissionIds: string[] = [];
+ if (this.ownerOrganization) {
+ const accessControlItem = this.kommonitorDataExchangeService.getAccessControlById(this.ownerOrganization);
+ if (accessControlItem && accessControlItem.permissions) {
+ permissionIds = accessControlItem.permissions
+ .filter((permission: any) => permission.permissionLevel === 'viewer' || permission.permissionLevel === 'editor')
+ .map((permission: any) => permission.permissionId);
+ }
+
+ // Set datasetOwner flag for the selected organization (like AngularJS component)
+ this.kommonitorDataExchangeService.accessControl.forEach((item: any) => {
+ if (item.organizationalUnitId === this.ownerOrganization) {
+ item.datasetOwner = true;
+ } else {
+ item.datasetOwner = false;
+ }
+ });
+ }
+
+ // Use transformed data for the grid
+ const transformedData = this.transformAccessControlData(this.kommonitorDataExchangeService.accessControl);
+
+ // Build role management grid with filtered data (like AngularJS component)
+ this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid(
+ 'georesourceAddRoleManagementTable',
+ this.roleManagementTableOptions,
+ transformedData,
+ permissionIds
+ );
+
+ // Force change detection by updating the options
+ setTimeout(() => {
+ if (this.roleManagementGrid && this.roleManagementGrid.api) {
+ // Update the grid data directly using the API
+ if (this.roleManagementTableOptions && this.roleManagementTableOptions.rowData) {
+ this.roleManagementGrid.api.setRowData(this.roleManagementTableOptions.rowData);
+ } else {
+ this.roleManagementGrid.api.setRowData([]);
+ }
+
+ // Refresh the grid to ensure it updates
+ this.roleManagementGrid.api.refreshCells();
+ this.roleManagementGrid.api.redrawRows();
+ }
+ }, 100);
+ }
+
+ // Multi-step form navigation
+ goToStep(step: number): void {
+ if (step >= 1 && step <= this.totalSteps) {
+ // Persist dynamic fields before leaving current step
+ this.persistDynamicImporterFields();
+ this.currentStep = step;
+ // Reapply after DOM updates
+ setTimeout(() => this.reapplyDynamicImporterFields(), 0);
+ }
+ }
+
+ nextStep(): void {
+ if (this.currentStep < this.totalSteps) {
+ // Persist dynamic fields before leaving current step
+ this.persistDynamicImporterFields();
+ this.currentStep++;
+ // Reapply after DOM updates
+ setTimeout(() => this.reapplyDynamicImporterFields(), 0);
+ }
+ }
+
+ previousStep(): void {
+ if (this.currentStep > 1) {
+ // Persist dynamic fields before leaving current step
+ this.persistDynamicImporterFields();
+ this.currentStep--;
+ // Reapply after DOM updates
+ setTimeout(() => this.reapplyDynamicImporterFields(), 0);
+ }
+ }
+
+ // 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;
+ }
+ }
+
+ // Create test access control data for development/testing
+ private createTestAccessControlData() {
+ // Create test data that matches the expected structure for buildRoleManagementGrid
+ const accessControlData = [
+ {
+ organizationalUnitId: 'org1',
+ name: 'Test Organisation 1',
+ organizationalUnitName: 'Test Organisation 1',
+ organizationDescription: 'Test Organisation 1 Description',
+ viewerPermissionId: 'view1',
+ editorPermissionId: 'edit1',
+ creatorPermissionId: 'create1',
+ datasetOwner: true,
+ permissions: [
+ { roleId: 'view1', roleName: 'Viewer', permissionLevel: 'viewer' },
+ { roleId: 'edit1', roleName: 'Editor', permissionLevel: 'editor' },
+ { roleId: 'create1', roleName: 'Creator', permissionLevel: 'creator' }
+ ]
+ },
+ {
+ organizationalUnitId: 'org2',
+ name: 'Test Organisation 2',
+ organizationalUnitName: 'Test Organisation 2',
+ organizationDescription: 'Test Organisation 2 Description',
+ viewerPermissionId: 'view2',
+ editorPermissionId: 'edit2',
+ creatorPermissionId: 'create2',
+ datasetOwner: false,
+ permissions: [
+ { roleId: 'view2', roleName: 'Viewer', permissionLevel: 'viewer' },
+ { roleId: 'edit2', roleName: 'Editor', permissionLevel: 'editor' }
+ ]
+ },
+ {
+ organizationalUnitId: 'org3',
+ name: 'Test Organisation 3',
+ organizationalUnitName: 'Test Organisation 3',
+ organizationDescription: 'Test Organisation 3 Description',
+ viewerPermissionId: 'view3',
+ editorPermissionId: 'edit3',
+ creatorPermissionId: 'create3',
+ datasetOwner: false,
+ permissions: [
+ { roleId: 'view3', roleName: 'Viewer', permissionLevel: 'viewer' }
+ ]
+ }
+ ];
+
+ // Update local references
+ this.resourcesCreatorRights = accessControlData;
+ this.filteredOrganizations = accessControlData;
+
+ // Also update the service's access control data
+ if (this.kommonitorDataExchangeService) {
+ (this.kommonitorDataExchangeService as any)._accessControl = accessControlData;
+ }
+ }
+
+ onChangeOwner(orgUnitId: string): void {
+ this.ownerOrganization = orgUnitId;
+
+ // Show role management form only when an organization is selected
+ this.showRoleManagementForm = !!orgUnitId;
+
+ this.refreshRoles();
+ }
+
+ // Handle owner organization change with proper validation
+ onChangeOwnerOrganization(ownerOrganization: any): void {
+ this.ownerOrganization = ownerOrganization;
+
+ // Show role management form only when an organization is selected
+ this.showRoleManagementForm = !!ownerOrganization;
+
+ // Refresh roles based on the selected owner organization
+ this.refreshRoles();
+ }
+
+ // Filter organizations based on search input
+ filterOrganizations() {
+ if (!this.ownerOrgFilter || this.ownerOrgFilter.trim() === '') {
+ // Reset to original access control data
+ this.reloadAccessControlData();
+ } else {
+ const filter = this.ownerOrgFilter.toLowerCase().trim();
+ const originalAccessControl = this.kommonitorDataExchangeService.accessControl || [];
+ const filteredAccessControl = originalAccessControl.filter(org =>
+ org.name && org.name.toLowerCase().includes(filter)
+ );
+ // Create a temporary filtered view without modifying the original data
+ this.filteredOrganizations = filteredAccessControl;
+ }
+ }
+
+ // Clear organization filter
+ clearOwnerFilter() {
+ this.ownerOrgFilter = '';
+ this.filteredOrganizations = [];
+ this.reloadAccessControlData();
+ }
+
+ // 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.roleManagementTableOptions || !this.roleManagementTableOptions.rowData || this.roleManagementTableOptions.rowData.length === 0)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ // Get selected role IDs for API
+ getSelectedRoleIds(): string[] {
+ if (this.roleManagementTableOptions && this.roleManagementTableOptions.rowData) {
+ return this.roleManagementTableOptions.rowData
+ .filter((row: any) => row.selected)
+ .map((row: any) => row.organizationalUnitId);
+ }
+ return [];
+ }
+
+ // Step validation methods for progress bar
+ isStepValid(step: number): boolean {
+ // Validation for specific steps
+ switch (step) {
+ case 1:
+ return !!this.datasetName && !!this.georesourceType;
+ case 2:
+ return !!this.metadata.description && !!this.metadata.datasource && !!this.metadata.contact && !!this.metadata.updateInterval && !!this.metadata.lastUpdate;
+ case 3:
+ return !!this.georesourceTopic_mainTopic;
+ case 4:
+ // Step 4 validation for access control
+ if (this.kommonitorDataExchangeService.enableKeycloakSecurity) {
+ return this.validateAccessControl();
+ }
+ return true;
+ case 5:
+ // Step 5 validation for spatial data
+ return !!this.converter && !!this.datasourceType && !!this.georesourceDataSourceIdProperty && !!this.georesourceDataSourceNameProperty;
+ default:
+ return true;
+ }
+ }
+
+ isCurrentStepValid(): boolean {
+ return this.isStepValid(this.currentStep);
+ }
+
+ onChangeIsPublic(isPublic: boolean): void {
+ this.isPublic = isPublic;
+ }
+
+ // Check if user has admin permissions (like AngularJS component)
+ checkAdminPermission(): boolean {
+ return this.kommonitorDataExchangeService.checkAdminPermission();
+ }
+
+ // Get filtered organizations based on admin permissions (like AngularJS component)
+ getFilteredOrganizations(): any[] {
+ if (this.checkAdminPermission()) {
+ return this.filteredOrganizations.length > 0 ? this.filteredOrganizations : this.kommonitorDataExchangeService.accessControl || [];
+ } else {
+ // For non-admin users, show only their creator rights
+ return this.resourcesCreatorRights || [];
+ }
+ }
+
+ // Method to manually reload access control data
+ async reloadAccessControlData() {
+ try {
+ this.loadingAccessControl = true;
+ // Try to fetch from API first
+ await this.kommonitorDataExchangeService.fetchAccessControlMetadata();
+
+ // Reload access control from service
+ if (this.kommonitorDataExchangeService.accessControl) {
+ // Transform API data to match the expected structure for the grid
+ const transformedData = this.transformAccessControlData(this.kommonitorDataExchangeService.accessControl);
+
+ // Update local references
+ this.resourcesCreatorRights = transformedData;
+ this.filteredOrganizations = transformedData;
+ } else {
+ throw new Error('No access control data returned from API');
+ }
+ } catch (error: any) {
+ console.warn('Failed to load access control data from API:', error);
+ // Do not inject test data; keep empty to avoid showing fake organizations
+ this.resourcesCreatorRights = [];
+ this.filteredOrganizations = [];
+ } finally {
+ this.loadingAccessControl = false;
+ }
+ }
+
+ // Transform API access control data to match the expected grid structure
+ private transformAccessControlData(apiData: any[]): any[] {
+ return apiData.map(org => {
+ // Extract permission IDs from the permissions array
+ const viewerPermission = org.permissions?.find((p: any) => p.permissionLevel === 'viewer');
+ const editorPermission = org.permissions?.find((p: any) => p.permissionLevel === 'editor');
+ const creatorPermission = org.permissions?.find((p: any) => p.permissionLevel === 'creator');
+
+ return {
+ organizationalUnitId: org.organizationalUnitId,
+ name: org.name,
+ organizationalUnitName: org.name,
+ organizationDescription: org.description || '',
+ viewerPermissionId: viewerPermission?.permissionId || '',
+ editorPermissionId: editorPermission?.permissionId || '',
+ creatorPermissionId: creatorPermission?.permissionId || '',
+ datasetOwner: org.datasetOwner || false,
+ permissions: org.permissions || []
+ };
+ });
+ }
+
+
+
+
+
+
+ // Importer methods
+ onChangeConverter(): void {
+ this.schema = this.converter?.schemas ? this.converter.schemas[0] : undefined;
+ this.mimeType = this.converter?.mimeTypes ? this.converter.mimeTypes[0] : undefined;
+ this.converterParameterValues = {};
+
+ // Filter available datasource types based on selected converter's supported datasources
+ const allTypes = this.kommonitorImporterHelperService.getAvailableDatasourceTypes() || [];
+ if (this.converter?.datasources && Array.isArray(this.converter.datasources) && this.converter.datasources.length > 0) {
+ this.availableDatasourceTypes = allTypes.filter((t: any) => this.converter.datasources.includes(t.type));
+ } else {
+ this.availableDatasourceTypes = allTypes;
+ }
+
+ // Auto-select if there is exactly one matching datasource type
+ if (this.availableDatasourceTypes.length === 1) {
+ this.datasourceType = this.availableDatasourceTypes[0];
+ this.onChangeDatasourceType(this.datasourceType);
+ } else {
+ // Reset selected datasourceType if current selection is not compatible anymore
+ if (this.datasourceType && !this.availableDatasourceTypes.find((t: any) => t.type === this.datasourceType.type)) {
+ this.datasourceType = null;
+ }
+ }
+ }
+
+ onChangeMimeType(mimeType: string): void {
+ this.mimeType = mimeType;
+ }
+
+ onChangeEncoding(encoding: string): void {
+ this.encoding = encoding;
+ }
+
+ onChangeDatasourceType(datasourceType: any): void {
+ this.datasourceType = datasourceType;
+ // Reset related fields when datasource type changes
+ this.selectedDataSourceFile = null;
+ this.georesourceDataSourceIdProperty = '';
+ this.georesourceDataSourceNameProperty = '';
+ this.bboxType = '';
+ this.bboxRefSpatialUnit = null;
+ this.datasourceTypeParameterValues = {};
+ }
+
+ // Color and styling methods
+ onChangeMarkerColor(markerColor: any, event?: Event): void {
+ // Prevent default behavior and stop propagation to avoid any navigation issues
+ if (event) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ this.selectedPoiMarkerColor = markerColor;
+ this.cdr.detectChanges();
+ }
+
+ onChangeSymbolColor(symbolColor: any, event?: Event): void {
+ // Prevent default behavior and stop propagation to avoid any navigation issues
+ if (event) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ this.selectedPoiSymbolColor = symbolColor;
+ this.cdr.detectChanges();
+ }
+
+ onChangeLoiDashArray(loiDashArrayObject: any, event?: Event): void {
+ // Prevent default behavior and stop propagation to avoid any navigation issues
+ if (event) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ this.selectedLoiDashArrayObject = loiDashArrayObject;
+ // Update selected line pattern for the picker component
+ if (this.linePatternOptions && this.linePatternOptions.length > 0) {
+ this.selectedLoiPattern = this.linePatternOptions.find(p => p.dashArrayValue === this.selectedLoiDashArrayObject?.dashArrayValue) || null;
+ }
+ this.cdr.detectChanges();
+ }
+
+ onDropdownButtonClick(event: Event): void {
+ // Toggle custom dropdown state
+ this.isMarkerStyleDropdownOpen = !this.isMarkerStyleDropdownOpen;
+ this.cdr.detectChanges();
+ }
+
+ closeMarkerStyleDropdown(): void {
+ this.isMarkerStyleDropdownOpen = false;
+ this.cdr.detectChanges();
+ }
+
+ onIconPickerChange(iconName: string): void {
+ this.selectedPoiIconName = iconName;
+ this.cdr.detectChanges();
+ }
+
+ onDocumentClick(event: Event): void {
+ // Close dropdown if clicking outside
+ const target = event.target as HTMLElement;
+ if (!target.closest('.customColorPicker')) {
+ this.isMarkerStyleDropdownOpen = false;
+ this.cdr.detectChanges();
+ }
+ }
+
+
+
+
+ onChangeMarkerStyle(markerStyle: string, event?: Event): void {
+ // Prevent default behavior and stop propagation to avoid any navigation issues
+ if (event) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ // Update the selected style
+ this.selectedPoiMarkerStyle = markerStyle;
+
+ // Force change detection to ensure the UI updates properly
+ this.cdr.detectChanges();
+ }
+
+ 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
+ onGeoresourceFileSelected(event: any): void {
+ const file = event?.target?.files?.[0] as File | undefined;
+ this.selectedDataSourceFile = file ?? null;
+ this.selectedDataSourceFileName = this.selectedDataSourceFile?.name || '';
+ }
+
+ onClickGeoresourceFileBrowse(): void {
+ try {
+ const inputEl = (this.georesourceDataSourceInput?.nativeElement as HTMLInputElement) || (document.getElementById('georesourceDataSourceInput_add') as HTMLInputElement | null);
+ inputEl?.click();
+ } catch {}
+ }
+
+ clearSelectedFile(): void {
+ try {
+ const inputEl = (this.georesourceDataSourceInput?.nativeElement as HTMLInputElement) || (document.getElementById('georesourceDataSourceInput_add') as HTMLInputElement | null);
+ if (inputEl) {
+ inputEl.value = '';
+ }
+ } catch {}
+ this.selectedDataSourceFile = null;
+ this.selectedDataSourceFileName = '';
+ }
+
+ onMetadataFileSelected(event: any): void {
+ const file = event.target.files[0];
+ if (file) {
+ this.parseMetadataFromFile(file);
+ }
+ }
+
+ onMappingConfigFileSelected(event: any): void {
+ const inputEl = event?.target as HTMLInputElement;
+ const file = inputEl?.files?.[0];
+ if (!file) {
+ this.georesourceMappingConfigImportError = 'Keine Datei ausgewählt oder ungültige Eingabe.';
+ this.showMappingConfigErrorAlert();
+ return;
+ }
+ try {
+ this.parseMappingConfigFromFile(file);
+ } catch (e) {
+ this.georesourceMappingConfigImportError = 'Fehler beim Lesen der Datei.';
+ this.showMappingConfigErrorAlert();
+ }
+ }
+
+ 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();
+ }
+ };
+
+ try {
+ fileReader.readAsText(file as Blob);
+ } catch (err) {
+ this.georesourceMappingConfigImportError = 'Fehler: Ungültiger Dateiinhalt.';
+ this.showMappingConfigErrorAlert();
+ }
+ }
+
+ 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;
+ }
+ });
+
+ if (!this.metadata.updateInterval && this.metadataImportSettings.metadata.updateInterval) {
+ // Fallback: add missing interval to options and select it
+ const fallbackInterval = {
+ apiName: this.metadataImportSettings.metadata.updateInterval,
+ displayName: this.metadataImportSettings.metadata.updateInterval
+ };
+ if (Array.isArray(this.updateIntervalOptions)) {
+ this.updateIntervalOptions = [...this.updateIntervalOptions, fallbackInterval];
+ } else {
+ this.updateIntervalOptions = [fallbackInterval];
+ }
+ this.metadata.updateInterval = fallbackInterval;
+ }
+
+ 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);
+ }
+ });
+ // Ensure LOI picker reflects imported selection
+ this.syncLinePatternOptionsAndSelection();
+
+ 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;
+ }
+ }
+ // Fallback: try to find by mimeType or by name similarity
+ if (!this.converter) {
+ const allConverters = this.kommonitorImporterHelperService.availableConverters || [];
+ const byMime = allConverters.find((c: any) => Array.isArray(c.mimeTypes) && c.mimeTypes.includes(this.mappingConfigImportSettings.converter.mimeType));
+ if (byMime) {
+ this.converter = byMime;
+ } else {
+ const wantedName = (this.mappingConfigImportSettings.converter.name || '').toLowerCase();
+ const byName = allConverters.find((c: any) => (c.name || '').toLowerCase().includes(wantedName));
+ if (byName) {
+ this.converter = byName;
+ } else {
+ // Heuristic for GeoJSON
+ const geojsonConv = allConverters.find((c: any) => Array.isArray(c.mimeTypes) && c.mimeTypes.some((m: string) => m.includes('geo+json')));
+ if (geojsonConv) {
+ this.converter = geojsonConv;
+ }
+ }
+ }
+ }
+
+ 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;
+ }
+ }
+ }
+
+ // Encoding from mapping if present
+ this.encoding = this.mappingConfigImportSettings.converter.encoding || this.encoding;
+
+ this.datasourceType = undefined;
+ for (const datasourceType of this.kommonitorImporterHelperService.availableDatasourceTypes) {
+ if (datasourceType.type === this.mappingConfigImportSettings.dataSource.type) {
+ this.datasourceType = datasourceType;
+ break;
+ }
+ }
+
+ // converter parameters
+ this.converterParameterValues = {};
+ if (Array.isArray(this.mappingConfigImportSettings.converter.parameters)) {
+ for (const convParameter of this.mappingConfigImportSettings.converter.parameters) {
+ const element = document.getElementById("converterParameter_georesourceAdd_" + convParameter.name) as HTMLInputElement;
+ if (element) {
+ element.value = convParameter.value ?? '';
+ }
+ this.converterParameterValues[convParameter.name] = convParameter.value ?? '';
+ }
+ }
+
+ // datasourceTypes parameters (persist + reflect bbox fields)
+ this.datasourceTypeParameterValues = {};
+ if (this.datasourceType && Array.isArray(this.mappingConfigImportSettings.dataSource.parameters)) {
+ for (const dsParameter of this.mappingConfigImportSettings.dataSource.parameters) {
+ const element = document.getElementById("datasourceTypeParameter_georesourceAdd_" + dsParameter.name) as HTMLInputElement;
+ if (element) {
+ element.value = dsParameter.value ?? '';
+ }
+ if (dsParameter.name === 'bboxType') {
+ this.bboxType = dsParameter.value || '';
+ } else if (dsParameter.name === 'bbox') {
+ if (this.bboxType === 'ref') {
+ this.bboxRefSpatialUnit = dsParameter.value;
+ }
+ } else {
+ this.datasourceTypeParameterValues[dsParameter.name] = 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;
+ }
+ // Reflect LOI dasharray selection in picker if available
+ this.syncLinePatternOptionsAndSelection();
+ }
+
+ 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 {
+ // Ensure pretty-print structure is available and scroll alert into view
+ if (!this.georesourceMetadataStructure_pretty) {
+ this.georesourceMetadataStructure_pretty = this.kommonitorDataExchangeService.syntaxHighlightJSON(this.georesourceMetadataStructure);
+ }
+ setTimeout(() => {
+ const el = document.getElementById('georesourceMetadataImportErrorAlert');
+ el?.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }, 0);
+ }
+
+ private showMappingConfigErrorAlert(): void {
+ // Ensure pretty-print structure is available and scroll alert into view
+ if (!this.georesourceMappingConfigStructure_pretty) {
+ this.georesourceMappingConfigStructure_pretty = this.kommonitorDataExchangeService.syntaxHighlightJSON(this.kommonitorImporterHelperService.mappingConfigStructure);
+ }
+ setTimeout(() => {
+ const el = document.getElementById('georesourceMappingConfigImportErrorAlert');
+ el?.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }, 0);
+ }
+
+ // 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',
+ [],
+ this.kommonitorDataExchangeService.accessControl,
+ []
+ );
+
+ 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.syncLinePatternOptionsAndSelection();
+ this.loiColor = '#bf3d2c';
+ this.loiWidth = 3;
+ this.aoiColor = '#bf3d2c';
+ this.selectedPoiIconName = 'home';
+ this.selectedPoiMarkerStyle = 'symbol';
+ this.poiMarkerText = '';
+ this.poiMarkerTextInvalid = false;
+
+ // Reset dropdown state
+ this.isMarkerStyleDropdownOpen = false;
+
+ // Icon picker will reset automatically through Angular binding
+
+
+
+ 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.selectedDataSourceFile = 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.showRoleManagementForm = false;
+
+ this.metadataImportSettings = null;
+ this.mappingConfigImportSettings = null;
+ this.georesourceMetadataImportError = '';
+ this.georesourceMappingConfigImportError = '';
+
+ // Reset persisted parameter maps and bbox refs
+ this.converterParameterValues = {};
+ this.datasourceTypeParameterValues = {};
+ this.bboxType = '';
+ this.bboxRefSpatialUnit = null;
+ }
+
+ // Build post body for API request
+ buildPostBody_georesources(): any {
+ const postBody: any = {
+ "geoJsonString": this.geoJsonString || "",
+ "permissions": [],
+ "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.toIsoDateString(this.metadata.lastUpdate),
+ "description": this.metadata.description,
+ "databasis": this.metadata.databasis
+ },
+ "jsonSchema": null,
+ "datasetName": this.datasetName,
+ "periodOfValidity": {
+ "endDate": this.toIsoDateString(this.periodOfValidity.endDate),
+ "startDate": this.toIsoDateString(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.permissions.push(roleId);
+ }
+ }
+ }
+
+ if (this.isPOI) {
+ // Fallback to defaults to avoid empty ColorType values
+ const symbolColorName = (this.selectedPoiSymbolColor as any)?.colorName
+ || this.availablePoiMarkerColors[1]?.colorName
+ || this.availablePoiMarkerColors[0]?.colorName
+ || 'red';
+ const markerColorName = (this.selectedPoiMarkerColor as any)?.colorName
+ || this.availablePoiMarkerColors[0]?.colorName
+ || 'red';
+
+ postBody["poiSymbolBootstrap3Name"] = this.selectedPoiIconName || 'home';
+ postBody["poiSymbolColor"] = symbolColorName;
+ postBody["poiMarkerColor"] = markerColorName;
+ 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 || this.selectedLoiPattern?.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) || [];
+
+ // Show success alert before closing the modal
+ this.successMessage = 'Georessource erfolgreich registriert';
+ this.loadingData = false;
+ this.cdr.detectChanges();
+
+ // Close modal after a short delay and pass the created georesourceId to parent
+ const createdId = this.kommonitorImporterHelperService.getIdFromImporterResponse(newGeoresourceResponse);
+ setTimeout(() => {
+ this.activeModal.close({ georesourceId: createdId });
+ }, 1500);
+ } 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();
+ if (!this.converterDefinition) {
+ this.errorMessage = 'Validierung fehlgeschlagen';
+ this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON({
+ cause: 'converterDefinition missing',
+ hint: 'Schema/Format wählen und alle Pflicht-Parameter (z.B. CRS) setzen'
+ });
+ return false;
+ }
+
+ this.datasourceTypeDefinition = await this.buildDatasourceTypeDefinition();
+ if (!this.datasourceTypeDefinition) {
+ this.errorMessage = 'Validierung fehlgeschlagen';
+ if (!this.errorMessagePart) {
+ this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON({
+ cause: 'datasourceTypeDefinition missing',
+ hint: 'Datenquelltyp wählen und alle Pflichtfelder (Datei/Parameter) ausfüllen'
+ });
+ }
+ return false;
+ }
+
+ this.propertyMappingDefinition = this.buildPropertyMappingDefinition();
+ if (!this.propertyMappingDefinition) {
+ this.errorMessage = 'Validierung fehlgeschlagen';
+ this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON({
+ cause: 'propertyMappingDefinition missing',
+ hint: 'ID-/NAME-Attributnamen angeben'
+ });
+ return false;
+ }
+
+ this.postBody_georesources = this.buildPostBody_georesources();
+ if (!this.postBody_georesources) {
+ this.errorMessage = 'Validierung fehlgeschlagen';
+ this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON({
+ cause: 'postBody missing',
+ hint: 'Pflichtfelder prüfen'
+ });
+ return false;
+ }
+
+
+ return true;
+ }
+
+ private buildConverterDefinition(): any {
+ const formValues: { [key: string]: string } = { ...this.converterParameterValues };
+ // Collect currently rendered converter parameter inputs (if any)
+ if (this.converter?.parameters && Array.isArray(this.converter.parameters)) {
+ for (const p of this.converter.parameters) {
+ const el = document.getElementById(`converterParameter_georesourceAdd_${p.name}`) as HTMLInputElement | null;
+ if (el && typeof el.value === 'string') {
+ formValues[p.name] = el.value;
+ }
+ }
+ }
+ const def = this.kommonitorImporterHelperService.buildConverterDefinition(
+ this.converter,
+ "converterParameter_georesourceAdd_",
+ this.schema,
+ this.mimeType,
+ formValues
+ );
+ return def;
+ }
+
+ private async buildDatasourceTypeDefinition(): Promise {
+ try {
+
+ // Pre-validate FILE datasource: require a selected file (persisted or from input)
+ if (this.datasourceType?.type === 'FILE') {
+ const fileInput: HTMLInputElement | null = (this.georesourceDataSourceInput?.nativeElement as HTMLInputElement) || (document.getElementById('georesourceDataSourceInput_add') as HTMLInputElement);
+ let file: File | undefined | null = this.selectedDataSourceFile;
+ if (!file) {
+ file = fileInput?.files?.[0];
+ }
+ const hasFile = !!file;
+ if (!hasFile) {
+ this.georesourceDataSourceInputInvalid = true;
+ this.georesourceDataSourceInputInvalidReason = 'Bitte eine Datei auswählen.';
+ this.cdr.detectChanges();
+ return null;
+ }
+ this.georesourceDataSourceInputInvalid = false;
+ this.georesourceDataSourceInputInvalidReason = '';
+
+ // Upload file immediately and build definition locally (robust approach used in SpatialUnit add)
+ const uploadedName = await this.kommonitorImporterHelperService.uploadNewFile(file as File, (file as File).name);
+ const localDef = {
+ type: 'FILE',
+ parameters: [
+ { name: 'NAME', value: uploadedName }
+ ]
+ };
+ return localDef;
+ }
+ const formValues: { [key: string]: string } = {
+ ...this.datasourceTypeParameterValues,
+ bboxType: this.bboxType as any,
+ bboxRef: this.bboxRefSpatialUnit as any
+ } as any;
+ const result = await this.kommonitorImporterHelperService.buildDatasourceTypeDefinition(
+ this.datasourceType,
+ 'datasourceTypeParameter_georesourceAdd_',
+ 'georesourceDataSourceInput_add',
+ formValues
+ );
+ return result;
+ } catch (error: any) {
+ console.error('[GeoresourceAddModal] buildDatasourceTypeDefinition error', error);
+ if (error.data) {
+ this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error.data);
+ } else {
+ this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON({ message: error?.message || error });
+ }
+
+ this.loadingData = false;
+ return null;
+ }
+ }
+
+ private buildPropertyMappingDefinition(): any {
+ const def = this.kommonitorImporterHelperService.buildPropertyMapping_spatialResource(
+ this.georesourceDataSourceNameProperty,
+ this.georesourceDataSourceIdProperty,
+ this.validityStartDate_perFeature,
+ this.validityEndDate_perFeature,
+ '',
+ this.keepAttributes,
+ this.keepMissingValues,
+ this.attributeMappings_adminView
+ );
+ return def;
+ }
+
+ // Modal control methods
+ cancel(): void {
+ this.activeModal.dismiss();
+ }
+
+ // Compute reasons that prevent enabling the register button and log them for diagnostics
+ getRegisterDisabledReasons(): string[] {
+ const reasons: string[] = [];
+ if (!this.datasetName) { reasons.push('datasetName'); }
+ if (!this.metadata?.description) { reasons.push('metadata.description'); }
+ if (!this.metadata?.datasource) { reasons.push('metadata.datasource'); }
+ if (!this.metadata?.contact) { reasons.push('metadata.contact'); }
+ if (!this.metadata?.updateInterval) { reasons.push('metadata.updateInterval'); }
+ if (!this.metadata?.lastUpdate) { reasons.push('metadata.lastUpdate'); }
+ if (!this.georesourceDataSourceIdProperty) { reasons.push('georesourceDataSourceIdProperty'); }
+ if (!this.georesourceDataSourceNameProperty) { reasons.push('georesourceDataSourceNameProperty'); }
+ if (!this.periodOfValidity?.startDate) { reasons.push('periodOfValidity.startDate'); }
+ if (!this.schema && this.converter?.schemas?.length > 0) { reasons.push('schema'); }
+ if (!this.mimeType && this.converter?.mimeTypes?.length > 0) { reasons.push('mimeType'); }
+ if (this.datasetNameInvalid) { reasons.push('datasetNameInvalid'); }
+ if (this.poiMarkerTextInvalid) { reasons.push('poiMarkerTextInvalid'); }
+ if (this.periodOfValidityInvalid) { reasons.push('periodOfValidityInvalid'); }
+ if (!this.converter) { reasons.push('converter'); }
+ if (!this.datasourceType) { reasons.push('datasourceType'); }
+ // Only require owner when security is enabled AND access control data is available
+ const hasAccessControl = Array.isArray(this.kommonitorDataExchangeService.accessControl) && this.kommonitorDataExchangeService.accessControl.length > 0;
+ if (this.kommonitorDataExchangeService.enableKeycloakSecurity && hasAccessControl && !this.ownerOrganization) { reasons.push('ownerOrganization'); }
+ return reasons;
+ }
+
+ isRegisterDisabled(): boolean {
+ const reasons = this.getRegisterDisabledReasons();
+ return reasons.length > 0;
+ }
+
+ private persistDynamicImporterFields(): void {
+ try {
+ // Persist converter parameter inputs
+ const convNodes = Array.from(document.querySelectorAll("[id^='converterParameter_georesourceAdd_']")) as HTMLInputElement[];
+ for (const el of convNodes) {
+ const id = el.id || '';
+ const key = id.replace('converterParameter_georesourceAdd_', '');
+ if (key) {
+ this.converterParameterValues[key] = el.value ?? '';
+ }
+ }
+ // Persist datasource type parameter inputs
+ const dsNodes = Array.from(document.querySelectorAll("[id^='datasourceTypeParameter_georesourceAdd_']")) as HTMLInputElement[];
+ for (const el of dsNodes) {
+ const id = el.id || '';
+ const key = id.replace('datasourceTypeParameter_georesourceAdd_', '');
+ if (key === 'bboxType') {
+ this.bboxType = el.value || '';
+ } else if (key === 'bboxRef') {
+ this.bboxRefSpatialUnit = el.value || null;
+ } else if (key) {
+ this.datasourceTypeParameterValues[key] = el.value ?? '';
+ }
+ }
+ // Persist selected file if present
+ const fileInput = (this.georesourceDataSourceInput?.nativeElement as HTMLInputElement) || (document.getElementById('georesourceDataSourceInput_add') as HTMLInputElement | null);
+ const file = fileInput?.files?.[0];
+ if (file) {
+ this.selectedDataSourceFile = file;
+ }
+ } catch {}
+ }
+
+ private reapplyDynamicImporterFields(): void {
+ try {
+ // Reapply converter parameter inputs
+ Object.keys(this.converterParameterValues || {}).forEach((key) => {
+ const el = document.getElementById(`converterParameter_georesourceAdd_${key}`) as HTMLInputElement | null;
+ if (el) {
+ el.value = this.converterParameterValues[key] ?? '';
+ }
+ });
+ // Reapply datasource type parameter inputs
+ Object.keys(this.datasourceTypeParameterValues || {}).forEach((key) => {
+ const el = document.getElementById(`datasourceTypeParameter_georesourceAdd_${key}`) as HTMLInputElement | null;
+ if (el) {
+ el.value = this.datasourceTypeParameterValues[key] ?? '';
+ }
+ });
+ // Reapply bbox fields if dedicated inputs exist
+ const bboxTypeEl = document.getElementById('datasourceTypeParameter_georesourceAdd_bboxType') as HTMLInputElement | null;
+ if (bboxTypeEl && this.bboxType) {
+ bboxTypeEl.value = this.bboxType;
+ }
+ const bboxRefEl = document.getElementById('datasourceTypeParameter_georesourceAdd_bboxRef') as HTMLInputElement | null;
+ if (bboxRefEl && this.bboxRefSpatialUnit) {
+ bboxRefEl.value = `${this.bboxRefSpatialUnit}`;
+ }
+ // Reattach file listener after DOM changes
+ this.attachFileInputListener();
+ // Note: File inputs cannot be programmatically set for security reasons; selectedDataSourceFile is used during upload.
+ } catch {}
+ }
+
+ private attachFileInputListener(): void {
+ try {
+ const inputEl = document.getElementById('georesourceDataSourceInput_add');
+ if (!inputEl) { return; }
+ if (this.fileInputChangeHandler) {
+ inputEl.removeEventListener('change', this.fileInputChangeHandler);
+ }
+ this.fileInputChangeHandler = (e: Event) => this.onGeoresourceFileSelected(e);
+ inputEl.addEventListener('change', this.fileInputChangeHandler);
+ } catch {}
+ }
+}
\ 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 @@
+
+
+
+
+
+
+
+
Diese Funktion ermöglicht es, die Mapping-Parameter mehrerer Georessoucen gleichzeitig zu aktualisieren.
+
* = Pflichtfeld
+
+
+
+
+
+
+
+
+ neue Zeile hinzufügen
+ ausgewählte Zeilen löschen
+
+
+
+
+
+
+
+
+
+
\ 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..74398a7a4
--- /dev/null
+++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceBatchUpdateModal/georesource-batch-update-modal.component.ts
@@ -0,0 +1,291 @@
+import { Component, OnInit, 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';
+import { KommonitorGeoresourceDataExchangeService } from 'services/adminGeoresourceUnit/kommonitor-data-exchange.service';
+import { KommonitorImporterHelperService } from 'services/adminGeoresourceUnit/kommonitor-importer-helper.service';
+import { KommonitorBatchUpdateHelperService } from 'services/adminGeoresourceUnit/kommonitor-batch-update-helper.service';
+
+@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,
+ public kommonitorDataExchangeService: KommonitorGeoresourceDataExchangeService,
+ public kommonitorImporterHelperService: KommonitorImporterHelperService,
+ public kommonitorBatchUpdateHelperService: KommonitorBatchUpdateHelperService,
+ 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.kommonitorBatchUpdateHelperService.datePickerOptions);
+ }
+ if (endDatePicker && (window as any).$) {
+ (window as any).$('#georesourceDefaultColumnDatePickerEnd').datepicker(this.kommonitorBatchUpdateHelperService.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 || null;
+
+ // 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 @@
+
+
+
+
+
+
+
+
+
Sollen die folgenden Georessourcen wirklich gelöscht werden?
+
+
+
kein Datensatz zum Löschen markiert. Mindestens ein Datensatz muss markiert werden.
+
+
+
0">
+
Points of Interest
+
+ {{dataset.datasetName}}
+
+
+
+
0">
+
Lines of Interest
+
+ {{dataset.datasetName}}
+
+
+
+
0">
+
Areas of Interest
+
+ {{dataset.datasetName}}
+
+
+
+
+
+
0">
+
ACHTUNG!
+
Dabei werden auch sämtliche Indikatoren-Referenzen auf die betroffenen Georessourcen dauerhaft aus dem System entfernt. Etwaige Skripte , in denen die betroffenen Georessourcen als Berechnungsgrundlage verwendet werden, werden ebenfalls ungültig und daher aus dem System gelöscht
+
+
Betroffene Indikatorenreferenzen
+
+
0">
+
+
+
+ Indikatoren-ID
+ Indikatoren-Name
+ Indikatoren-Merkmal
+ Indikatoren-Typ
+ Indikatoren-Beschreibung
+ referenzierte Georessource - ID
+ referenzierte Georessource - Name
+ referenzierte Georessource - Beschreibung
+
+
+
+
+ {{entry.indicatorMetadata.indicatorId}}
+ {{entry.indicatorMetadata.indicatorName}}
+ {{entry.indicatorMetadata.characteristicValue}}
+ {{entry.indicatorMetadata.indicatorType}}
+ {{entry.indicatorMetadata.description}}
+ {{entry.georesourceReference.referencedGeoresourceId}}
+ {{entry.georesourceReference.referencedGeoresourceName}}
+ {{entry.georesourceReference.referencedGeoresourceDescription}}
+
+
+
+
+
+
Betroffene Skripte
+
+
0">
+
+
+
+ Skript-ID
+ Skript-Name
+ Skript-Beschreibung
+ ID des berechneten Indikators
+
+
+
+
+ {{script.scriptId}}
+ {{script.name}}
+ {{script.description}}
+ {{script.indicatorId}}
+
+
+
+
+
+
+
+
+
×
+
{{successMessage}}
+
+
0">
+
Points of Interest
+
+ {{dataset.datasetName}}
+
+
+
+
0">
+
Lines of Interest
+
+ {{dataset.datasetName}}
+
+
+
+
0">
+
Areas of Interest
+
+ {{dataset.datasetName}}
+
+
+
+
+
+
0">
+
Referenzen zu Indikatoren
+
+ {{indicator.indicatorMetadata.indicatorName}}
+
+
+
+
+
+
+
×
+
{{errorMessage}}
+
Folgende Datensätze konnten nicht gelöscht werden.
+
+
+
+
+
+ Name
+ Fehlermeldung
+
+
+
+
+ {{datasetError[0].datasetName}}
+
+
+
+
+
+
+
+
\ 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..74b8b40c9
--- /dev/null
+++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceDeleteModal/georesource-delete-modal.component.ts
@@ -0,0 +1,282 @@
+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();
+ // If datasets were passed directly via component instance, initialize immediately
+ if (this.datasetsToDelete && this.datasetsToDelete.length > 0) {
+ this.onDeleteGeoresources(this.datasetsToDelete);
+ }
+ }
+
+ 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',
+ targetGeoresourceId: 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..0f8fde5fd
--- /dev/null
+++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditFeaturesModal/georesource-edit-features-modal.component.css
@@ -0,0 +1,317 @@
+/* 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*/
+#progressbar {
+ margin-bottom: 10px;
+ overflow: hidden;
+ /* remove default list padding to avoid left offset */
+ padding-left: 0;
+ margin-left: 0;
+ /* CSS counters to number the steps */
+ counter-reset: step;
+}
+
+#progressbar li {
+ list-style-type: none;
+ color: black;
+ text-transform: uppercase;
+ font-size: 9px;
+ /* ensure equal spacing for 3 steps */
+ width: 33.33%;
+ float: left;
+ position: relative;
+ letter-spacing: 1px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ text-align: center;
+}
+
+#progressbar li:before {
+ content: counter(step);
+ counter-increment: step;
+ width: 24px;
+ height: 24px;
+ line-height: 26px;
+ display: block;
+ font-size: 12px;
+ color: #333;
+ background: #ccc;
+ border-radius: 25px;
+ margin: 0 auto 10px auto;
+}
+
+/*progressbar connectors*/
+#progressbar li:after {
+ content: '';
+ width: 100%;
+ height: 2px;
+ background: #ccc;
+ position: absolute;
+ left: -50%;
+ top: 9px;
+ z-index: -1;
+}
+
+#progressbar li:first-child:after {
+ /*connector not needed before the first step*/
+ content: none;
+}
+
+/*marking active/completed steps with primary color*/
+#progressbar li.active:before,
+#progressbar li.active:after {
+ background: var(--kommonitor-primary);
+ color: white;
+}
+
+/* Hover behavior aligned with add modal */
+#progressbar li:hover {
+ color: var(--kommonitor-primary);
+}
+
+#progressbar li:hover:before {
+ background: var(--kommonitor-primary);
+ color: white;
+}
+
+/* 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..432d89565
--- /dev/null
+++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditFeaturesModal/georesource-edit-features-modal.component.html
@@ -0,0 +1,514 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Feature Übersicht
+ Import einzelner Features
+ Import mehrerer Features
+
+
+
+
+
+ Feature Übersicht
+ Optionale Anzeige der Feature-Details
+
+
+
+
+
+ Letztes gescheitertes Update eines Einzeleintrags
+ {{kommonitorDataGridHelperService.featureTable_georesource_lastUpdate_timestamp_failure}}
+
+
+
+
+
+
+
+
+
+ Import einzelner Features
+ Manuelles Erstellen neuer Feature-Objekte für den selektierten Datensatz. Nur die mit * markierten Elemente müssen verpflichtend angegeben werden
+
+
+
+
+
+
+
+
+
+
+
+ Mapping-Import
+
+ Mapping-Export
+
+
+ Räumlicher Datensatz
+ Angaben über den räumlichen Datensatz, aus dem die Features importiert werden
+ * = Pflichtfeld
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Übersicht der definierten Attribut-Mappings
+
+
+
+ Editierfunktionen
+ Quell-Attributname im Datensatz
+ Ziel-Attributname nach Import
+ Datentyp
+
+
+
+
+
+
+
+
+
+
+ {{attributeMappingEntry.sourceName}}
+ {{attributeMappingEntry.destinationName}}
+ {{attributeMappingEntry.dataType.displayName}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
×
+
Features erfolgreich fortgeführt
+
Fortführen der Features der Georessource mit Namen {{successMessagePart}} war erfolgreich.
+
0">
+
{{importedFeatures.length}} Features wurden dabei importiert.
+
+
+
+
+
×
+
Features fortführen gescheitert
+ Beim Fortführen der Features ist ein Fehler aufgetreten. Fehlermeldung:
+
+
+
+
+
+
+
Bei den {{importerErrors.length}} Features mit folgenden IDs scheitert der Import:
+
+
+
+
Bitte beheben Sie die angezeigten Fehler im Datensatz und wiederholen den Prozess.
+
+
+
+
+
×
+
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/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..564f5d807
--- /dev/null
+++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditFeaturesModal/georesource-edit-features-modal.component.ts
@@ -0,0 +1,1569 @@
+import { Component, OnInit, ViewChild, ElementRef, OnDestroy, Inject, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectorRef } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { NgbActiveModal, NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap';
+import { KmDatePickerComponent } from '../../../customElements/date-picker/km-date-picker.component';
+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 { AgGridModule } from 'ag-grid-angular';
+import { ajskommonitorSingleFeatureMapHelperServiceProvider } from '../../../../../app-upgraded-providers';
+import { KommonitorGeoresourceDataExchangeService } from 'services/adminGeoresourceUnit/kommonitor-data-exchange.service';
+import { KommonitorImporterHelperService } from 'services/adminGeoresourceUnit/kommonitor-importer-helper.service';
+import { KommonitorGeoresourceDataGridHelperService } from 'services/adminGeoresourceUnit/kommonitor-data-grid-helper.service';
+import { KommonitorMultiStepFormHelperService } from 'services/adminGeoresourceUnit/kommonitor-multi-step-form-helper.service';
+import { SingleFeatureEditComponent } from '../../../common/single-feature-edit/single-feature-edit.component';
+
+declare const $: any;
+declare const __env: any;
+
+// Removed ng-bootstrap date adapters and formatters in favor of km-date-picker
+
+@Component({
+ selector: 'georesource-edit-features-modal-new',
+ templateUrl: './georesource-edit-features-modal.component.html',
+ styleUrls: ['./georesource-edit-features-modal.component.css'],
+ standalone: true,
+ imports: [CommonModule, FormsModule, SingleFeatureEditComponent, AgGridModule, NgbDatepickerModule, KmDatePickerComponent],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
+ providers: []
+})
+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;
+
+ // If we have valid data and the view is initialized, refresh the table
+ if (value && value.georesourceId && this.featureTableGridOptions) {
+ // Defer to next tick to avoid change detection errors
+ setTimeout(() => {
+ this.refreshGeoresourceEditFeaturesOverviewTable();
+ // Mark for check after model update
+ this.cdr.detectChanges();
+ }, 0);
+ }
+ }
+
+ // Feature management
+ enableDeleteFeatures = false;
+ georesourceFeaturesGeoJSON: any;
+ remainingFeatureHeaders: any[] = [];
+
+ // AG-Grid configuration
+ featureTableGridOptions: GridOptions = {};
+ private gridApi!: GridApi;
+ private columnApi!: ColumnApi;
+
+ // Single feature variables
+ featureIdValue: any = 0;
+ featureIdExampleString: string = '';
+ featureIdIsValid = false;
+ featureNameValue: string = '';
+ featureGeometryValue: any;
+ featureStartDateValue: string = '';
+ featureEndDateValue: string = '';
+ featureSchemaProperties: any[] = [];
+ schemaObject: any;
+ featureInfoText_singleFeatureAddMenu: string = '';
+
+ // Multiple feature import variables
+ periodOfValidity: any = {
+ startDate: '',
+ endDate: ''
+ };
+ periodOfValidityInvalid = false;
+
+ // Data source variables
+ georesourceDataSourceInputInvalid = false;
+ georesourceDataSourceInputInvalidReason: string = '';
+ georesourceDataSourceIdProperty: string = '';
+ georesourceDataSourceNameProperty: string = '';
+ selectedDataSourceFile: File | null = null;
+ selectedDataSourceFileName: 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,
+ public kommonitorDataExchangeService: KommonitorGeoresourceDataExchangeService,
+ public kommonitorMultiStepFormHelperService: KommonitorMultiStepFormHelperService,
+ public kommonitorDataGridHelperService: KommonitorGeoresourceDataGridHelperService,
+ public kommonitorImporterHelperService: KommonitorImporterHelperService,
+ @Inject(ajskommonitorSingleFeatureMapHelperServiceProvider.provide) private kommonitorSingleFeatureMapHelperService: any,
+ private broadcastService: BroadcastService,
+ private http: HttpClient,
+ private cdr: ChangeDetectorRef
+ ) {
+ this.initializeDefaultValues();
+ }
+
+ // Date helpers (ISO YYYY-MM-DD)
+ 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 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();
+ }
+
+ 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;
+ }
+
+ 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();
+ }
+
+ ngOnInit(): void {
+ this.initializeDatePickers();
+ this.setupEventListeners();
+ this.initializeMappingConfigStructure();
+ this.buildFeatureTable();
+ }
+
+ ngAfterViewInit(): void {
+ // Defer to next tick to avoid ExpressionChangedAfterItHasBeenCheckedError
+ setTimeout(() => {
+ if (this.currentGeoresourceDataset && this.currentGeoresourceDataset.georesourceId) {
+ this.refreshGeoresourceEditFeaturesOverviewTable();
+ }
+ }, 0);
+ // Stabilize view after initial async scheduling
+ Promise.resolve().then(() => this.cdr.detectChanges());
+ }
+
+ ngOnDestroy(): void {
+ this.subscriptions.forEach(sub => sub.unsubscribe());
+ this.kommonitorSingleFeatureMapHelperService.invalidateMap();
+ }
+
+ 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).$('#georesourceSingleFeatureDatepickerStart').datepicker(
+ this.kommonitorDataExchangeService.datePickerOptions
+ );
+ (window as any).$('#georesourceSingleFeatureDatepickerEnd').datepicker(
+ this.kommonitorDataExchangeService.datePickerOptions
+ );
+ }
+ } catch (error) {
+ // Date picker initialization failed
+ }
+ }, 250);
+ }
+
+ private setupEventListeners(): void {
+ // Setup broadcast listeners
+ const broadcastSubscription = this.broadcastService.currentBroadcastMsg.subscribe(broadcastMsg => {
+ if (broadcastMsg) {
+ if (broadcastMsg.msg === 'onEditGeoresourceFeatures') {
+ // Defer handling to avoid changing bound values mid-cycle
+ setTimeout(() => { this.onEditGeoresourceFeatures(broadcastMsg.values); }, 0);
+ } else if (broadcastMsg.msg === 'showLoadingIcon_' + this.kommonitorDataGridHelperService?.resourceType_georesource) {
+ setTimeout(() => { this.loadingData = true; }, 0);
+ } else if (broadcastMsg.msg === 'hideLoadingIcon_' + this.kommonitorDataGridHelperService?.resourceType_georesource) {
+ setTimeout(() => { this.loadingData = false; }, 0);
+ } else if (broadcastMsg.msg === 'onDeleteFeatureEntry_' + this.kommonitorDataGridHelperService?.resourceType_georesource) {
+ this.broadcastService.broadcast('refreshGeoresourceOverviewTable', { crudType: 'edit', targetGeoresourceId: this.currentGeoresourceDataset?.georesourceId });
+ setTimeout(() => { this.refreshGeoresourceEditFeaturesOverviewTable(); }, 0);
+ } else if (broadcastMsg.msg === 'onUpdateSingleFeatureGeometry') {
+ this.onUpdateSingleFeatureGeometry(broadcastMsg.values);
+ }
+ }
+ });
+
+ this.subscriptions.push(broadcastSubscription);
+ }
+
+ // File handling for FILE datasource (align with Add modal)
+ onGeoresourceFileSelected(event: any): void {
+ const file = event?.target?.files?.[0] as File | undefined;
+ this.selectedDataSourceFile = file ?? null;
+ this.selectedDataSourceFileName = this.selectedDataSourceFile?.name || '';
+ try { this.cdr.detectChanges(); } catch {}
+ }
+
+ onEditGeoresourceFeatures(georesourceDataset: any): void {
+ this.kommonitorMultiStepFormHelperService?.registerClickHandler();
+
+ // Ensure we have valid data
+ if (!georesourceDataset || !georesourceDataset.georesourceId) {
+ return;
+ }
+
+ if (this.currentGeoresourceDataset &&
+ this.currentGeoresourceDataset.datasetName === georesourceDataset.datasetName) {
+ return;
+ }
+
+ this.currentGeoresourceDataset = georesourceDataset;
+
+ this.resetGeoresourceEditFeaturesForm();
+ this.buildFeatureTable();
+
+ // Load the georesource features immediately
+ this.refreshGeoresourceEditFeaturesOverviewTable();
+ }
+
+ // Single feature import methods
+ async initSingleFeatureAddMenu(): Promise {
+ if (!this.currentGeoresourceDataset) return;
+
+ // If we're in Step 2, broadcast to the single-feature-edit component
+ if (this.currentStep === 2) {
+ // SingleFeatureEditComponent expects an array [georesourceDataset, isReachabilityDatasetOnly]
+ this.broadcastService.broadcast('onEditGeoresourceFeatures', [this.currentGeoresourceDataset, false]);
+ return; // Let the single-feature-edit component handle the rest
+ }
+
+ // Initialize map for single feature import
+ const domId = "singleFeatureGeoMap";
+ let resourceType = this.kommonitorSingleFeatureMapHelperService.resourceType_point;
+
+ if (this.currentGeoresourceDataset.isLOI) {
+ resourceType = this.kommonitorSingleFeatureMapHelperService.resourceType_line;
+ } else if (this.currentGeoresourceDataset.isAOI) {
+ resourceType = this.kommonitorSingleFeatureMapHelperService.resourceType_polygon;
+ }
+
+ this.kommonitorSingleFeatureMapHelperService.initSingleFeatureGeoMap(domId, resourceType);
+
+ // Initialize feature schema
+ await this.initFeatureSchema();
+
+ // Load existing features and add to map
+ try {
+ const response = await this.http.get(
+ `${this.kommonitorDataExchangeService.getBaseUrlToKomMonitorDataAPI_spatialResource()}/georesources/${this.currentGeoresourceDataset.georesourceId}/allFeatures`
+ ).toPromise();
+
+ this.georesourceFeaturesGeoJSON = response;
+
+ if (this.georesourceFeaturesGeoJSON?.features) {
+ this.kommonitorSingleFeatureMapHelperService.addDataLayertoSingleFeatureGeoMap(this.georesourceFeaturesGeoJSON);
+
+ this.featureInfoText_singleFeatureAddMenu = `${this.georesourceFeaturesGeoJSON.features.length} weitere Features im Datensatz vorhanden`;
+
+ // Generate ID proposal
+ this.featureIdValue = this.generateIdProposalFromExistingFeatures();
+ this.addExampleValuesToSchemaProperties();
+ } else {
+ this.featureInfoText_singleFeatureAddMenu = "Keine weiteren Features im Datensatz vorhanden";
+ }
+ } catch (error) {
+ this.featureInfoText_singleFeatureAddMenu = "Fehler bei Abruf der Features";
+ }
+
+ // Validate the generated ID
+ this.validateSingleFeatureId();
+ }
+
+ private async initFeatureSchema(): Promise {
+ if (!this.currentGeoresourceDataset) return;
+
+ if (!this.currentGeoresourceDataset.georesourceId) {
+ return;
+ }
+
+ try {
+ const schemaResult = await this.initFeatureSchemaDirectly(
+ this.currentGeoresourceDataset.georesourceId
+ );
+
+ if (schemaResult) {
+ this.schemaObject = schemaResult.schemaObject;
+ this.featureSchemaProperties = schemaResult.featureSchemaProperties;
+ }
+ } catch (error) {
+ // Error initializing feature schema
+ }
+ }
+
+ private async initFeatureSchemaDirectly(georesourceId: string): Promise<{ schemaObject: any; featureSchemaProperties: any[] }> {
+ try {
+ const response = await this.http.get(
+ `${this.kommonitorDataExchangeService.getBaseUrlToKomMonitorDataAPI_spatialResource()}/georesources/${georesourceId}/schema`
+ ).toPromise();
+
+ const schemaObject = response as any;
+ const featureSchemaProperties: any[] = [];
+
+ // Environment constants (these should match your environment configuration)
+ const FEATURE_ID_PROPERTY_NAME = 'ID';
+ const FEATURE_NAME_PROPERTY_NAME = 'NAME';
+ const VALID_START_DATE_PROPERTY_NAME = 'validStartDate';
+ const VALID_END_DATE_PROPERTY_NAME = 'validEndDate';
+
+ for (const property in schemaObject) {
+ if (property !== FEATURE_ID_PROPERTY_NAME &&
+ property !== FEATURE_NAME_PROPERTY_NAME &&
+ property !== VALID_START_DATE_PROPERTY_NAME &&
+ property !== VALID_END_DATE_PROPERTY_NAME) {
+ featureSchemaProperties.push({
+ property: property,
+ value: undefined
+ });
+ }
+ }
+
+ return { schemaObject, featureSchemaProperties };
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ private generateIdProposalFromExistingFeatures(): any {
+ if (!this.georesourceFeaturesGeoJSON?.features || !this.schemaObject) {
+ return 0;
+ }
+
+ const idDataType = this.schemaObject['ID']; // Using the constant from initFeatureSchemaDirectly
+ const existingFeatureIds = this.georesourceFeaturesGeoJSON.features
+ .map((feature: any) => feature.properties?.['ID'])
+ .filter((id: any) => id !== undefined && id !== null);
+
+ if (existingFeatureIds.length > 0) {
+ const length = existingFeatureIds.length;
+ this.featureIdExampleString = `${existingFeatureIds[0]}; ${existingFeatureIds[Math.round(length/2)]}; ${existingFeatureIds[length - 1]}`;
+ }
+
+ return this.generateIdProposalFromExistingFeaturesDirectly(
+ existingFeatureIds,
+ idDataType
+ );
+ }
+
+ private generateIdProposalFromExistingFeaturesDirectly(existingFeatureIds: any[], idDataType: string): any {
+ if (existingFeatureIds.length === 0) {
+ return idDataType === 'Integer' || idDataType === 'Double' ? 1 : this.generateUUID();
+ }
+
+ if (idDataType === 'Integer' || idDataType === 'Double') {
+ const maxValue = Math.max(...existingFeatureIds);
+ return maxValue + 1;
+ } else {
+ return this.generateUUID();
+ }
+ }
+
+ private generateUUID(): string {
+ // Simple UUID generation - you might want to use a proper UUID library
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ const r = Math.random() * 16 | 0;
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
+ return v.toString(16);
+ });
+ }
+
+ private addExampleValuesToSchemaProperties(): void {
+ if (this.georesourceFeaturesGeoJSON?.features?.[0] && this.featureSchemaProperties) {
+ const exampleFeature = this.georesourceFeaturesGeoJSON.features[0];
+ this.addExampleValuesToSchemaPropertiesDirectly(
+ this.featureSchemaProperties,
+ exampleFeature
+ );
+ }
+ }
+
+ private addExampleValuesToSchemaPropertiesDirectly(featureSchemaProperties: any[], exampleFeature: any): void {
+ for (const element of featureSchemaProperties) {
+ element.exampleValue = exampleFeature.properties[element.property];
+ }
+ }
+
+ private validateSingleFeatureId(): void {
+ if (!this.georesourceFeaturesGeoJSON?.features || !this.featureIdValue) {
+ this.featureIdIsValid = false;
+ return;
+ }
+
+ this.featureIdIsValid = this.validateSingleFeatureIdDirectly(
+ this.featureIdValue,
+ this.georesourceFeaturesGeoJSON.features
+ );
+ }
+
+ private validateSingleFeatureIdDirectly(featureIdValue: any, features: any[]): boolean {
+ if (!features || !featureIdValue) {
+ return featureIdValue !== undefined && featureIdValue !== null;
+ }
+
+ const filteredFeatures = features.filter(
+ (feature: any) => feature.properties?.['ID'] === featureIdValue
+ );
+
+ return filteredFeatures.length === 0;
+ }
+
+ onUpdateSingleFeatureGeometry(geoJSONOrArray: any): void {
+ const geoJSON = Array.isArray(geoJSONOrArray) ? geoJSONOrArray[0] : geoJSONOrArray;
+ this.featureGeometryValue = geoJSON;
+ }
+
+ async addSingleGeoresourceFeature(): Promise {
+ if (!this.currentGeoresourceDataset || !this.featureGeometryValue) {
+ return;
+ }
+
+ this.loadingData = true;
+ this.importerErrors = null;
+ this.successMessagePart = '';
+ this.errorMessagePart = '';
+
+ try {
+ // Build importer objects for single feature import
+ const allDataSpecified = await this.buildImporterObjects_singleFeatureImport();
+
+ if (!allDataSpecified) {
+ this.loadingData = false;
+ return;
+ }
+
+ // Perform dry run first
+ const updateGeoresourceResponse_dryRun = await this.kommonitorImporterHelperService.updateGeoresource(
+ this.converterDefinition,
+ this.datasourceTypeDefinition,
+ this.propertyMappingDefinition,
+ this.currentGeoresourceDataset.georesourceId,
+ this.putBody_georesources,
+ true
+ );
+
+ if (!this.kommonitorImporterHelperService.importerResponseContainsErrors(updateGeoresourceResponse_dryRun)) {
+ // Execute the actual import
+ const updateGeoresourceResponse = await this.kommonitorImporterHelperService.updateGeoresource(
+ this.converterDefinition,
+ this.datasourceTypeDefinition,
+ this.propertyMappingDefinition,
+ this.currentGeoresourceDataset.georesourceId,
+ this.putBody_georesources,
+ false
+ );
+
+ // Handle success
+ this.broadcastService.broadcast('refreshGeoresourceOverviewTable', {
+ crudType: 'edit',
+ targetGeoresourceId: this.currentGeoresourceDataset.georesourceId
+ });
+
+ this.refreshGeoresourceEditFeaturesOverviewTable();
+ this.initSingleFeatureAddMenu();
+
+ this.successMessagePart = this.currentGeoresourceDataset.datasetName;
+ this.importedFeatures = this.kommonitorImporterHelperService.getImportedFeaturesFromImporterResponse(updateGeoresourceResponse);
+
+ // Prevent duplicate import
+ this.featureIdIsValid = false;
+
+ // Add new feature to current dataset
+ if (this.georesourceFeaturesGeoJSON) {
+ this.georesourceFeaturesGeoJSON.features.push(this.featureGeometryValue.features[0]);
+ } else {
+ this.georesourceFeaturesGeoJSON = {
+ type: 'FeatureCollection',
+ features: [this.featureGeometryValue.features[0]]
+ };
+ }
+
+ this.showSuccessAlert();
+ this.loadingData = false;
+ } else {
+ // Handle errors
+ this.errorMessagePart = "Das zu importierende Feature des Datensatzes weist kritische Fehler auf";
+ this.importerErrors = this.kommonitorImporterHelperService.getErrorsFromImporterResponse(updateGeoresourceResponse_dryRun);
+ this.showErrorAlert();
+ this.loadingData = false;
+ }
+ } catch (error) {
+ this.handleError(error);
+ this.loadingData = false;
+ }
+ }
+
+ private async buildImporterObjects_singleFeatureImport(): Promise {
+ this.converterDefinition = this.getSingleFeatureConverterDefinition();
+
+ const singleFeatureObjects = this.buildSingleFeatureImportObjects(
+ this.featureGeometryValue,
+ this.featureIdValue,
+ this.featureNameValue || '',
+ this.featureStartDateValue || '',
+ this.featureEndDateValue || '',
+ this.featureSchemaProperties
+ );
+
+ this.datasourceTypeDefinition = singleFeatureObjects.datasourceDefinition;
+ this.propertyMappingDefinition = singleFeatureObjects.propertyMappingDefinition;
+
+ // Build put body
+ const scopeProperties = {
+ periodOfValidity: {
+ endDate: this.featureEndDateValue || '',
+ startDate: this.featureStartDateValue || ''
+ },
+ isPartialUpdate: true
+ };
+
+ this.putBody_georesources = this.kommonitorImporterHelperService.buildPutBody_georesources(scopeProperties);
+
+ return !!(this.converterDefinition && this.datasourceTypeDefinition && this.propertyMappingDefinition && this.putBody_georesources);
+ }
+
+ // Step navigation
+ nextStep(): void {
+ if (this.currentStep < 3) {
+ this.currentStep++;
+
+ // Initialize Step 2 if moving to it
+ if (this.currentStep === 2) {
+ this.initSingleFeatureAddMenu();
+ }
+ }
+ }
+
+ previousStep(): void {
+ if (this.currentStep > 1) {
+ this.currentStep--;
+ }
+ }
+
+ goToStep(step: number): void {
+ if (step >= 1 && step <= 3) {
+ this.currentStep = step;
+
+ // Initialize Step 2 if moving to it
+ if (this.currentStep === 2) {
+ this.initSingleFeatureAddMenu();
+ }
+ }
+ }
+
+ // Feature table management
+ private buildFeatureTable(): void {
+ this.featureTableGridOptions = this.kommonitorDataGridHelperService.buildDataGrid_featureTable_spatialResource(
+ "georesourceFeatureTable",
+ [],
+ [],
+ undefined,
+ this.kommonitorDataGridHelperService.resourceType_georesource,
+ this.enableDeleteFeatures
+ );
+
+ // Initialize with empty data to show the grid structure
+ if (this.gridApi) {
+ this.gridApi.setRowData([]);
+ }
+ }
+
+ refreshGeoresourceEditFeaturesOverviewTable(): void {
+ if (!this.currentGeoresourceDataset) {
+ return;
+ }
+
+ if (!this.currentGeoresourceDataset.georesourceId) {
+ return;
+ }
+
+ // Set synchronously, but stabilize the view immediately
+ this.loadingData = true;
+ this.cdr.detectChanges();
+ this.hideSuccessAlert();
+ this.hideErrorAlert();
+
+ const url = `${this.kommonitorDataExchangeService.getBaseUrlToKomMonitorDataAPI_spatialResource()}/georesources/${this.currentGeoresourceDataset.georesourceId}/allFeatures`;
+
+ this.http.get(url).subscribe({
+ next: (response: any) => {
+ this.georesourceFeaturesGeoJSON = response;
+ const tmpRemainingHeaders: string[] = [];
+
+ // Extract headers from the first feature's properties
+ if (this.georesourceFeaturesGeoJSON?.features?.[0]?.properties) {
+ for (const property in this.georesourceFeaturesGeoJSON.features[0].properties) {
+ if (property !== (__env?.FEATURE_ID_PROPERTY_NAME || 'ID') &&
+ property !== (__env?.FEATURE_NAME_PROPERTY_NAME || 'NAME') &&
+ property !== (__env?.VALID_START_DATE_PROPERTY_NAME || 'validStartDate') &&
+ property !== (__env?.VALID_END_DATE_PROPERTY_NAME || 'validEndDate')) {
+ tmpRemainingHeaders.push(property);
+ }
+ }
+ }
+
+ this.remainingFeatureHeaders = tmpRemainingHeaders;
+
+ // 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) {
+ // 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;
+ });
+ this.gridApi.setRowData(transformedData);
+ // Force refresh of the grid
+ this.gridApi.refreshCells();
+ }
+
+ this.loadingData = false;
+ this.cdr.detectChanges();
+ },
+ error: (error) => {
+ this.handleError(error);
+ this.loadingData = false;
+ this.cdr.detectChanges();
+ }
+ });
+ }
+
+ 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);
+
+ // Update data if we have features
+ if (this.georesourceFeaturesGeoJSON?.features) {
+ 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;
+ });
+ this.gridApi.setRowData(transformedData);
+ }
+
+ // Force refresh of the grid to show/hide delete buttons
+ this.gridApi.refreshCells();
+
+ // Register click handlers after grid update
+ setTimeout(() => {
+ this.kommonitorDataGridHelperService.registerFeatureTableClickHandlers(
+ this.currentGeoresourceDataset?.georesourceId,
+ this.kommonitorDataGridHelperService.resourceType_georesource,
+ this.enableDeleteFeatures
+ );
+ }, 100);
+ }
+ }
+ }
+
+ 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.hideSuccessAlert();
+ this.hideErrorAlert();
+
+ this.http.delete(
+ `${this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI}/georesources/${this.currentGeoresourceDataset.georesourceId}/allFeatures`
+ ).subscribe({
+ next: (response: any) => {
+ this.georesourceFeaturesGeoJSON = null;
+ this.remainingFeatureHeaders = [];
+
+ this.broadcastService.broadcast('refreshGeoresourceOverviewTable', {
+ crudType: 'edit',
+ targetGeoresourceId: this.currentGeoresourceDataset.georesourceId
+ });
+
+ // Clear the grid data
+ this.featureTableGridOptions = this.kommonitorDataGridHelperService.buildDataGrid_featureTable_spatialResource(
+ "georesourceFeatureTable",
+ [],
+ []
+ );
+
+ if (this.gridApi) {
+ this.gridApi.setRowData([]);
+ }
+
+ this.successMessagePart = this.currentGeoresourceDataset.datasetName;
+ this.showSuccessAlert();
+
+ setTimeout(() => {
+ this.loadingData = false;
+ }, 500);
+ },
+ error: (error: any) => {
+ this.handleError(error);
+ setTimeout(() => {
+ this.loadingData = false;
+ }, 500);
+ }
+ });
+ }
+ }
+
+ // Converter and data source methods
+ onChangeConverter(): void {
+ if (this.converter) {
+ this.schema = this.converter.schemas ? this.converter.schemas[0] : '';
+ this.mimeType = this.converter.mimeTypes ? this.converter.mimeTypes[0] : '';
+ this.datasourceType = undefined;
+ }
+ }
+
+ onChangeMimeType(mimeType: string): void {
+ this.mimeType = mimeType;
+ }
+
+ onChangeDatasourceType(datasourceType: any): void {
+ this.datasourceType = datasourceType;
+ }
+
+ // Validation methods
+ checkPeriodOfValidity(): void {
+ this.periodOfValidityInvalid = false;
+
+ // Validate format first
+ if (this.periodOfValidity.startDate && !this.isValidDateString(String(this.periodOfValidity.startDate))) {
+ this.periodOfValidityInvalid = true;
+ return;
+ }
+ if (this.periodOfValidity.endDate && !this.isValidDateString(String(this.periodOfValidity.endDate))) {
+ this.periodOfValidityInvalid = true;
+ return;
+ }
+
+ 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 = '';
+ try {
+ const inputEl = document.getElementById('georesourceMappingConfigEditFeaturesImportFile_ng') as HTMLInputElement | null;
+ (inputEl || this.mappingConfigImportFile?.nativeElement)?.click();
+ } catch {
+ this.mappingConfigImportFile?.nativeElement?.click();
+ }
+ }
+
+ onMappingConfigFileSelected(event: any): void {
+ const inputEl = (event?.target as HTMLInputElement) || (document.getElementById('georesourceMappingConfigEditFeaturesImportFile_ng') as HTMLInputElement | null);
+ const file = inputEl?.files?.[0];
+ if (!file) {
+ this.georesourceMappingConfigImportError = 'Keine Datei ausgewählt oder ungültige Eingabe.';
+ this.showMappingConfigImportErrorAlert();
+ return;
+ }
+ this.parseMappingConfigFromFile(file as File);
+ }
+
+ private parseMappingConfigFromFile(file: File): void {
+ const fileReader = new FileReader();
+
+ fileReader.onload = (event: any) => {
+ try {
+ this.parseFromMappingConfigFile(event);
+ } catch (error) {
+ this.georesourceMappingConfigImportError = 'Uploaded Mapping Config File cannot be parsed correctly';
+ const preElement = document.getElementById('georesourcesEditFeaturesMappingConfigPre');
+ if (preElement) {
+ preElement.innerHTML = this.georesourceMappingConfigStructure_pretty;
+ }
+ this.showMappingConfigImportErrorAlert();
+ }
+ };
+
+ try {
+ fileReader.readAsText(file as Blob);
+ } catch (err) {
+ this.georesourceMappingConfigImportError = 'Fehler beim Lesen der Datei.';
+ this.showMappingConfigImportErrorAlert();
+ }
+ }
+
+ private parseFromMappingConfigFile(event: any): void {
+ const mappingConfig = JSON.parse(event.target.result);
+
+ // Basic structure validation (align with Add modal expectations)
+ if (!mappingConfig.converter || !mappingConfig.dataSource || !mappingConfig.propertyMapping) {
+ this.georesourceMappingConfigImportError = 'Struktur der Datei stimmt nicht mit erwartetem Muster überein.';
+ const pre = document.getElementById('georesourcesEditFeaturesMappingConfigPre');
+ if (pre) {
+ pre.innerHTML = this.georesourceMappingConfigStructure_pretty;
+ }
+ this.showMappingConfigImportErrorAlert();
+ return;
+ }
+
+ // 1) Resolve converter by name or fallbacks (mimeType/name heuristics)
+ this.converter = undefined as any;
+ const allConverters = this.kommonitorImporterHelperService.availableConverters || [];
+ for (const conv of allConverters) {
+ if (conv.name === mappingConfig.converter.name) {
+ this.converter = conv;
+ break;
+ }
+ }
+ if (!this.converter) {
+ const byMime = allConverters.find((c: any) => Array.isArray(c.mimeTypes) && c.mimeTypes.includes(mappingConfig.converter.mimeType));
+ if (byMime) {
+ this.converter = byMime;
+ } else {
+ const wantedName = (mappingConfig.converter.name || '').toLowerCase();
+ const byName = allConverters.find((c: any) => (c.name || '').toLowerCase().includes(wantedName));
+ if (byName) {
+ this.converter = byName;
+ } else {
+ // Heuristic for GeoJSON
+ const geojsonConv = allConverters.find((c: any) => Array.isArray(c.mimeTypes) && c.mimeTypes.some((m: string) => m.includes('geo+json')));
+ if (geojsonConv) {
+ this.converter = geojsonConv;
+ }
+ }
+ }
+ }
+
+ // 2) Schema and mimeType
+ this.schema = '';
+ if (this.converter && this.converter.schemas && mappingConfig.converter.schema) {
+ for (const sch of this.converter.schemas) {
+ if (sch === mappingConfig.converter.schema) {
+ this.schema = sch;
+ }
+ }
+ }
+
+ this.mimeType = '';
+ if (this.converter && this.converter.mimeTypes && mappingConfig.converter.mimeType) {
+ for (const mt of this.converter.mimeTypes) {
+ if (mt === mappingConfig.converter.mimeType) {
+ this.mimeType = mt;
+ }
+ }
+ }
+
+ // 3) Datasource type
+ this.datasourceType = undefined as any;
+ const allTypes = this.kommonitorImporterHelperService.availableDatasourceTypes || [];
+ for (const dsType of allTypes) {
+ if (dsType.type === mappingConfig.dataSource.type) {
+ this.datasourceType = dsType;
+ break;
+ }
+ }
+
+ // 4) Apply converter parameters to DOM inputs so helper can pick them up later
+ if (Array.isArray(mappingConfig.converter.parameters)) {
+ for (const p of mappingConfig.converter.parameters) {
+ const el = document.getElementById('converterParameter_georesourceEditFeatures_' + p.name) as HTMLInputElement | null;
+ if (el) {
+ el.value = p.value ?? '';
+ }
+ }
+ }
+
+ // 5) Apply datasource parameters and bbox specifics
+ this.bboxType = '';
+ this.bboxRefSpatialUnit = undefined as any;
+ if (this.datasourceType && Array.isArray(mappingConfig.dataSource.parameters)) {
+ for (const dsParam of mappingConfig.dataSource.parameters) {
+ const el = document.getElementById('datasourceTypeParameter_georesourceEditFeatures_' + dsParam.name) as HTMLInputElement | null;
+ if (el) {
+ el.value = dsParam.value ?? '';
+ }
+ if (dsParam.name === 'bboxType') {
+ this.bboxType = dsParam.value || '';
+ const bboxTypeEl = document.getElementById('datasourceTypeParameter_georesourceEditFeatures_bboxType') as HTMLInputElement | null;
+ if (bboxTypeEl) { bboxTypeEl.value = this.bboxType; }
+ } else if (dsParam.name === 'bbox') {
+ if (this.bboxType === 'ref') {
+ this.bboxRefSpatialUnit = dsParam.value;
+ const bboxRefEl = document.getElementById('datasourceTypeParameter_georesourceEditFeatures_bboxRef') as HTMLInputElement | null;
+ if (bboxRefEl) { bboxRefEl.value = `${this.bboxRefSpatialUnit}`; }
+ } else if (this.bboxType === 'literal' && typeof dsParam.value === 'string') {
+ // Try to split "minx,miny,maxx,maxy"
+ const parts = dsParam.value.split(/[,\s]+/).map((s: string) => s.trim()).filter((s: string) => s.length > 0);
+ if (parts.length >= 4) {
+ const [minx, miny, maxx, maxy] = parts;
+ const minxEl = document.getElementById('datasourceTypeParameter_georesourceEditFeatures_bbox_minx') as HTMLInputElement | null;
+ const minyEl = document.getElementById('datasourceTypeParameter_georesourceEditFeatures_bbox_miny') as HTMLInputElement | null;
+ const maxxEl = document.getElementById('datasourceTypeParameter_georesourceEditFeatures_bbox_maxx') as HTMLInputElement | null;
+ const maxyEl = document.getElementById('datasourceTypeParameter_georesourceEditFeatures_bbox_maxy') as HTMLInputElement | null;
+ if (minxEl) { minxEl.value = minx; }
+ if (minyEl) { minyEl.value = miny; }
+ if (maxxEl) { maxxEl.value = maxx; }
+ if (maxyEl) { maxyEl.value = maxy; }
+ }
+ }
+ }
+ }
+ }
+
+ // 6) Property mapping fields and flags
+ this.georesourceDataSourceNameProperty = mappingConfig.propertyMapping.nameProperty || '';
+ this.georesourceDataSourceIdProperty = mappingConfig.propertyMapping.identifierProperty || '';
+ this.validityStartDate_perFeature = mappingConfig.propertyMapping.validStartDateProperty || '';
+ this.validityEndDate_perFeature = mappingConfig.propertyMapping.validEndDateProperty || '';
+ this.keepAttributes = !!mappingConfig.propertyMapping.keepAttributes;
+ this.keepMissingValues = !!mappingConfig.propertyMapping.keepMissingOrNullValueAttributes;
+
+ this.attributeMappings_adminView = [];
+ if (Array.isArray(mappingConfig.propertyMapping.attributes)) {
+ for (const attr of mappingConfig.propertyMapping.attributes) {
+ const tmp: any = {
+ sourceName: attr.name,
+ destinationName: attr.mappingName
+ };
+ for (const dataType of this.kommonitorImporterHelperService.attributeMapping_attributeTypes) {
+ if (dataType.apiName === attr.type) {
+ tmp.dataType = dataType;
+ break;
+ }
+ }
+ this.attributeMappings_adminView.push(tmp);
+ }
+ }
+
+ // 7) Period of validity
+ if (mappingConfig.periodOfValidity) {
+ this.periodOfValidity = {
+ startDate: mappingConfig.periodOfValidity.startDate || '',
+ endDate: mappingConfig.periodOfValidity.endDate || ''
+ };
+ this.periodOfValidityInvalid = false;
+ }
+ }
+
+ 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
+ async editGeoresourceFeatures(): Promise {
+ if (!this.currentGeoresourceDataset || !this.converter || !this.datasourceType) {
+ return;
+ }
+
+ this.loadingData = true;
+ this.importerErrors = null;
+ this.successMessagePart = '';
+ this.errorMessagePart = '';
+
+ try {
+ // Build importer objects
+ const allDataSpecified = await this.buildImporterObjects();
+
+ if (!allDataSpecified) {
+ this.loadingData = false;
+ return;
+ }
+
+ // Perform dry run first
+ const updateGeoresourceResponse_dryRun = await this.kommonitorImporterHelperService.updateGeoresource(
+ this.converterDefinition,
+ this.datasourceTypeDefinition,
+ this.propertyMappingDefinition,
+ this.currentGeoresourceDataset.georesourceId,
+ this.putBody_georesources,
+ true
+ );
+
+ if (!this.kommonitorImporterHelperService.importerResponseContainsErrors(updateGeoresourceResponse_dryRun)) {
+ // Execute the actual import
+ const updateGeoresourceResponse = await this.kommonitorImporterHelperService.updateGeoresource(
+ this.converterDefinition,
+ this.datasourceTypeDefinition,
+ this.propertyMappingDefinition,
+ this.currentGeoresourceDataset.georesourceId,
+ this.putBody_georesources,
+ false
+ );
+
+ // Handle success
+ this.broadcastService.broadcast('refreshGeoresourceOverviewTable', {
+ crudType: 'edit',
+ targetGeoresourceId: this.currentGeoresourceDataset.georesourceId
+ });
+
+ this.refreshGeoresourceEditFeaturesOverviewTable();
+ this.initSingleFeatureAddMenu();
+
+ this.successMessagePart = this.currentGeoresourceDataset.datasetName;
+ this.importedFeatures = this.kommonitorImporterHelperService.getImportedFeaturesFromImporterResponse(updateGeoresourceResponse);
+
+ this.showSuccessAlert();
+ this.loadingData = false;
+ } else {
+ // Handle errors
+ this.errorMessagePart = "Einige der zu importierenden Features des Datensatzes weisen kritische Fehler auf";
+ this.importerErrors = this.kommonitorImporterHelperService.getErrorsFromImporterResponse(updateGeoresourceResponse_dryRun);
+ this.showErrorAlert();
+ this.loadingData = false;
+ }
+ } catch (error) {
+ this.handleError(error);
+ this.loadingData = false;
+ }
+ }
+
+ private async buildImporterObjects(): Promise {
+ this.converterDefinition = this.buildConverterDefinition();
+ this.datasourceTypeDefinition = await this.buildDatasourceTypeDefinition();
+ this.propertyMappingDefinition = this.buildPropertyMappingDefinition();
+
+ const scopeProperties = {
+ periodOfValidity: {
+ endDate: this.toIsoDateString(this.periodOfValidity.endDate),
+ startDate: this.toIsoDateString(this.periodOfValidity.startDate)
+ },
+ isPartialUpdate: this.isPartialUpdate
+ };
+
+ this.putBody_georesources = this.kommonitorImporterHelperService.buildPutBody_georesources(scopeProperties);
+
+ return !!(this.converterDefinition && this.datasourceTypeDefinition && this.propertyMappingDefinition && this.putBody_georesources);
+ }
+
+ private buildConverterDefinition(): any {
+ return this.kommonitorImporterHelperService.buildConverterDefinition(
+ this.converter,
+ "converterParameter_georesourceEditFeatures_",
+ this.schema,
+ this.mimeType
+ );
+ }
+
+ private async buildDatasourceTypeDefinition(): Promise {
+ try {
+ // Pre-validate FILE datasource: require a selected file and upload it first
+ if (this.datasourceType?.type === 'FILE') {
+ // Prefer selectedDataSourceFile captured by onGeoresourceFileSelected, fallback to input element
+ const fileInput = document.getElementById('georesourceDataSourceInput_editFeatures') as HTMLInputElement | null;
+ let file: File | undefined | null = this.selectedDataSourceFile as File | null | undefined;
+ if (!file) {
+ file = fileInput?.files?.[0];
+ }
+ const hasFile = !!file;
+ if (!hasFile) {
+ this.georesourceDataSourceInputInvalid = true;
+ this.georesourceDataSourceInputInvalidReason = 'Bitte eine Datei auswählen.';
+ return null;
+ }
+ this.georesourceDataSourceInputInvalid = false;
+ this.georesourceDataSourceInputInvalidReason = '';
+
+ const uploadedName = await this.kommonitorImporterHelperService.uploadNewFile(file as File, (file as File).name);
+ const localDef = {
+ type: 'FILE',
+ parameters: [
+ { name: 'NAME', value: uploadedName }
+ ]
+ };
+ return localDef;
+ }
+
+ // Non-FILE: let helper build from parameter inputs
+ return await this.kommonitorImporterHelperService.buildDatasourceTypeDefinition(
+ this.datasourceType,
+ 'datasourceTypeParameter_georesourceEditFeatures_',
+ 'georesourceDataSourceInput_editFeatures'
+ );
+ } catch (error) {
+ this.handleError(error);
+ 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
+ );
+ }
+
+ // 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 single feature variables
+ this.featureIdValue = 0;
+ this.featureIdExampleString = '';
+ this.featureIdIsValid = false;
+ this.featureNameValue = '';
+ this.featureGeometryValue = undefined;
+ this.featureStartDateValue = '';
+ this.featureEndDateValue = '';
+ this.featureSchemaProperties = [];
+ this.schemaObject = undefined;
+ this.featureInfoText_singleFeatureAddMenu = '';
+
+ // 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;
+ this.selectedDataSourceFile = null;
+ this.selectedDataSourceFileName = '';
+
+ // Reset messages
+ this.successMessagePart = '';
+ this.errorMessagePart = '';
+ this.importerErrors = undefined;
+ this.importedFeatures = [];
+
+ // Reinitialize single feature add menu
+ setTimeout(() => {
+ this.initSingleFeatureAddMenu();
+ }, 100);
+ }
+
+ // Alert methods
+ showSuccessAlert(): void {
+ const alertElement = document.getElementById('georesourceEditFeaturesSuccessAlert_ng');
+ if (alertElement) {
+ alertElement.hidden = false;
+ }
+ }
+
+ showErrorAlert(): void {
+ const alertElement = document.getElementById('georesourceEditFeaturesErrorAlert_ng');
+ if (alertElement) {
+ alertElement.hidden = false;
+ }
+ }
+
+ showMappingConfigImportErrorAlert(): void {
+ const alertElement = document.getElementById('georesourceEditFeaturesMappingConfigImportErrorAlert');
+ if (alertElement) {
+ alertElement.hidden = false;
+ }
+ }
+
+ hideSuccessAlert(): void {
+ const alertElement = document.getElementById('georesourceEditFeaturesSuccessAlert_ng');
+ if (alertElement) {
+ alertElement.hidden = true;
+ }
+ }
+
+ hideErrorAlert(): void {
+ const alertElement = document.getElementById('georesourceEditFeaturesErrorAlert_ng');
+ 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: any): void {
+ this.gridApi = event.api;
+ this.columnApi = event.columnApi;
+
+ // Auto-size columns to fit content
+ this.gridApi.sizeColumnsToFit();
+
+ // If we have data, load it into the grid
+ if (this.currentGeoresourceDataset && this.georesourceFeaturesGeoJSON) {
+ this.refreshGeoresourceEditFeaturesOverviewTable();
+ }
+ }
+
+ onFirstDataRendered(event: any): void {
+ // Handle first data rendered event
+
+ // Auto-size columns after data is rendered
+ if (this.gridApi) {
+ this.gridApi.sizeColumnsToFit();
+ }
+ }
+
+ onColumnResized(event: any): void {
+ // Handle column resize event
+ }
+
+ onCellValueChanged(params: any): void {
+ // Handle cell value changes here
+ // The actual API call is handled by the data grid helper service
+ // This method is called by the ag-grid component when a cell value changes
+
+ console.log('Cell value changed:', {
+ column: params.colDef?.field,
+ oldValue: params.oldValue,
+ newValue: params.newValue,
+ data: params.data
+ });
+
+ // Call the data grid helper service with the current georesource ID
+ this.kommonitorDataGridHelperService.handleCellValueChanged(
+ params,
+ this.currentGeoresourceDataset?.georesourceId,
+ this.kommonitorDataGridHelperService.resourceType_georesource
+ );
+ }
+
+ private handleError(error: any): void {
+ 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();
+ }
+
+
+
+ private getSingleFeatureConverterDefinition(): any {
+ // This should return the converter definition for single feature import
+ // You'll need to implement this based on your importer helper service
+ return {
+ name: 'singleFeatureImport',
+ // Add other converter properties as needed
+ };
+ }
+
+ private buildSingleFeatureImportObjects(
+ featureGeometryValue: any,
+ featureIdValue: any,
+ featureNameValue: string,
+ featureStartDateValue: string,
+ featureEndDateValue: string,
+ featureSchemaProperties: any[]
+ ): any {
+ // Set properties on the feature
+ featureGeometryValue.features[0].properties['ID'] = featureIdValue;
+ featureGeometryValue.features[0].properties['NAME'] = featureNameValue;
+ featureGeometryValue.features[0].properties['validStartDate'] = featureStartDateValue;
+ featureGeometryValue.features[0].properties['validEndDate'] = featureEndDateValue;
+
+ // Add schema properties
+ for (const element of featureSchemaProperties) {
+ featureGeometryValue.features[0].properties[element.property] = element.value;
+ }
+
+ // Build converter definition
+ const converterDefinition = this.getSingleFeatureConverterDefinition();
+
+ // Build datasource type definition
+ const datasourceTypeDefinition = {
+ type: 'singleFeature',
+ parameters: [{
+ name: 'geoJsonData',
+ value: JSON.stringify(featureGeometryValue)
+ }]
+ };
+
+ // Build property mapping definition
+ const propertyMappingDefinition = {
+ nameProperty: 'NAME',
+ identifierProperty: 'ID',
+ validStartDateProperty: 'validStartDate',
+ validEndDateProperty: 'validEndDate',
+ keepAttributes: true,
+ keepMissingOrNullValueAttributes: true,
+ attributes: []
+ };
+
+ // Build PUT body
+ const putBody = {
+ periodOfValidity: {
+ endDate: featureEndDateValue,
+ startDate: featureStartDateValue
+ },
+ isPartialUpdate: true
+ };
+
+ return {
+ converterDefinition,
+ datasourceDefinition: datasourceTypeDefinition,
+ propertyMappingDefinition,
+ putBody
+ };
+ }
+
+}
\ 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..027eab5ef
--- /dev/null
+++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditMetadataModal/georesource-edit-metadata-modal.component.css
@@ -0,0 +1,296 @@
+/* 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: 10px;
+ overflow: hidden;
+ /*CSS counters to number the steps*/
+ counter-reset: step;
+ /* z-index: 10000; */
+}
+
+#progressbar li {
+ list-style-type: none;
+ color: black;
+ text-transform: uppercase;
+ font-size: 9px;
+ /* width: 33.33%; */
+ float: left;
+ position: relative;
+ letter-spacing: 1px;
+ /* transform-style: preserve-3d; */
+ /* z-index: 1; */
+ 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);
+ /* z-index: +1; */
+}
+
+/*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;
+ /* transform: translateZ(-2px); */
+}
+
+#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;
+}
+
+/* Clickable step styling */
+#progressbar li.clickable {
+ cursor: pointer;
+}
+
+#progressbar li.clickable:hover {
+ color: var(--kommonitor-primary);
+}
+
+#progressbar li.clickable:hover:before {
+ background: var(--kommonitor-primary);
+ color: white;
+ transform: scale(1.1);
+}
+
+/* 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;
+}
+
+/* Modal positioning context for absolute positioned alerts */
+:host ::ng-deep .modal-content {
+ position: relative;
+}
+
+/* Alert styles */
+.alert {
+ position: relative;
+ padding: 0.75rem 3.25rem;
+ margin-bottom: 1rem;
+ border: 1px solid transparent;
+ border-radius: 0.25rem;
+}
+
+.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..f5ef5a79a
--- /dev/null
+++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditMetadataModal/georesource-edit-metadata-modal.component.html
@@ -0,0 +1,452 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ = 1" [class.clickable]="true" (click)="goToStep(1)">Metadaten der Georessource
+ = 2" [class.clickable]="true" (click)="goToStep(2)">Allgemeine Metadaten
+ = 3" [class.clickable]="true" (click)="goToStep(3)">Themenhierarchie
+
+
+
+
+
+
+ Metadaten der Georessource
+ Angaben über Metadaten der Georessource
+ * = Pflichtfeld
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Allgemeine Metadaten
+ Angaben über allgemeine Metadaten in KomMonitor
+ * = Pflichtfeld
+
+
+
+
+
+
+
+
+
+
+
+ Themenhierarchie
+ Angaben über die Themenhierarchie der Georessource
+
+ * = Pflichtfeld
+ Angabe der Themenhierarchie. Mindestens das Hauptthema muss gewählt werden. Vorhandene Unterthemen erscheinen nach Auswahl eines Hauptthemas. Bis zu vier Themen-Ebenen sind erlaubt.
+
+ Das Aufklappen der unteren Box ermöglicht die Verwaltung des Themenkatalogs.
+
+
+
+
+
0" class="form-group">
+
Unterthema - erste Ebene
+
+ -- Unterthema wählen --
+ {{subTopic.name || subTopic.title}}
+
+
+
+
+
+
0" class="form-group">
+
Unterthema - zweite Ebene
+
+ -- Unterthema wählen --
+ {{subsubTopic.name || subsubTopic.title}}
+
+
+
+
+
+
0" class="form-group">
+
Unterthema - dritte Ebene
+
+ -- Unterthema wählen --
+ {{subsubsubTopic.name || subsubsubTopic.title}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ×
+
Georessource aktualisiert
+ Die Metadaten der Georessource mit Namen {{successMessagePart}} wurden in KomMonitor aktualisiert und in
+ die Übersichtstabelle eingetragen.
+
+
+
+
+
+
+
\ 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..126ee2acd
--- /dev/null
+++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditMetadataModal/georesource-edit-metadata-modal.component.ts
@@ -0,0 +1,1134 @@
+import { Component, OnInit, Inject, ViewChild, ElementRef, OnDestroy, ChangeDetectorRef, NgZone } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+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 { KommonitorGeoresourceDataExchangeService } from 'services/adminGeoresourceUnit/kommonitor-data-exchange.service';
+import { KommonitorMultiStepFormHelperService } from 'services/adminGeoresourceUnit/kommonitor-multi-step-form-helper.service';
+import { KommonitorGeoresourceDataGridHelperService } from 'services/adminGeoresourceUnit/kommonitor-data-grid-helper.service';
+import { IconPickerComponent } from 'components/ngComponents/customElements/icon-picker/icon-picker.component';
+import { KmDatePickerComponent } from 'components/ngComponents/customElements/date-picker/km-date-picker.component';
+import { AdminTopicsManagementComponent } from '../../adminTopicsManagement/admin-topics-management.component';
+import { KmLinePatternPickerComponent, LinePatternOption } from 'components/ngComponents/customElements/line-pattern-picker/km-line-pattern-picker.component';
+import { KmColorPickerComponent } from 'components/ngComponents/customElements/color-picker/km-color-picker.component';
+
+@Component({
+ selector: 'georesource-edit-metadata-modal-new',
+ standalone: true,
+ templateUrl: './georesource-edit-metadata-modal.component.html',
+ styleUrls: ['./georesource-edit-metadata-modal.component.css'],
+ providers: [],
+ imports: [CommonModule, FormsModule, IconPickerComponent, KmDatePickerComponent, AdminTopicsManagementComponent, KmLinePatternPickerComponent, KmColorPickerComponent]
+})
+export class GeoresourceEditMetadataModalComponent implements OnInit, OnDestroy {
+ @ViewChild('metadataImportFile', { static: false }) metadataImportFile!: ElementRef;
+
+ // Component state
+ loadingData = false;
+ private _currentGeoresourceDataset: any;
+ get currentGeoresourceDataset(): any { return this._currentGeoresourceDataset; }
+ set currentGeoresourceDataset(value: any) {
+ this._currentGeoresourceDataset = value;
+ if (value) {
+ // Ensure form is populated whenever dataset is assigned programmatically or via broadcast
+ this.resetGeoresourceEditMetadataForm();
+ this.kommonitorMultiStepFormHelperService.registerClickHandler();
+ }
+ }
+ 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;
+ selectedLoiPattern: LinePatternOption | null = null;
+ linePatternOptions: LinePatternOption[] = [];
+ 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;
+ mainTopicsForGeoresource: any[] = [];
+ private topicsLoaded = false;
+ private topicsLoading = false;
+
+ // 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,
+ public kommonitorDataExchangeService: KommonitorGeoresourceDataExchangeService,
+ private kommonitorMultiStepFormHelperService: KommonitorMultiStepFormHelperService,
+ private kommonitorDataGridHelperService: KommonitorGeoresourceDataGridHelperService,
+ private broadcastService: BroadcastService,
+ private http: HttpClient,
+ private cdr: ChangeDetectorRef,
+ private ngZone: NgZone
+ ) {
+ this.initializeDefaultValues();
+ }
+
+ // Date helpers
+ 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 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();
+ }
+
+ 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;
+ }
+
+ onLastUpdateBlur(): void {
+ this.metadata.lastUpdate = this.ensureValidDateOrToday(this.metadata.lastUpdate);
+ }
+
+ ngOnInit(): void {
+ this.setupEventListeners();
+ this.initializeMetadataStructure();
+ // React to role changes and load topics once available
+ const rolesSub = this.kommonitorDataExchangeService.currentRoles$.subscribe(() => {
+ if (!this.topicsLoaded && !this.topicsLoading) {
+ this.loadTopicsData();
+ }
+ });
+ this.subscriptions.push(rolesSub);
+ // Try an initial load in case roles are already set
+ this.loadTopicsData();
+ this.updateMainTopicsForGeoresource();
+ // Reapply dynamic UI state after initial render
+ setTimeout(() => this.reapplyDynamicUiFields(), 0);
+
+
+ }
+
+ ngOnDestroy(): void {
+ this.subscriptions.forEach(sub => sub.unsubscribe());
+ }
+
+ /**
+ * Load topics data for the dropdowns
+ */
+ private async loadTopicsData(): Promise {
+ try {
+ if (this.topicsLoaded || this.topicsLoading) { return; }
+ this.topicsLoading = true;
+ const roles = this.kommonitorDataExchangeService.currentKeycloakLoginRoles || [];
+ console.log('[GeoresourceEditMetadataModal] loadTopicsData: fetching topics with roles', roles);
+ const topics = await this.kommonitorDataExchangeService.fetchTopicsMetadata(roles);
+ console.log('[GeoresourceEditMetadataModal] loadTopicsData: fetched topics length', Array.isArray(topics) ? topics.length : 'n/a');
+ this.updateMainTopicsForGeoresource();
+ // If a dataset is already selected, set its topic selection now
+ if (this.currentGeoresourceDataset?.topicReference) {
+ this.applyTopicSelectionFromDataset();
+ }
+ this.topicsLoaded = true;
+ } catch (error) {
+ console.warn('Could not load topics data:', error);
+ }
+ finally {
+ this.topicsLoading = false;
+ }
+ }
+
+ /**
+ * Public method to manually refresh topics (for debugging)
+ */
+ public refreshTopics(): void {
+ this.loadTopicsData();
+ }
+
+ /**
+ * Debug method to check topics state
+ */
+ public debugTopicsState(): void {
+ console.log('=== Topics Debug Info ===');
+ console.log('Available topics:', this.kommonitorDataExchangeService.availableTopics);
+ console.log('Topics length:', this.kommonitorDataExchangeService.availableTopics?.length);
+ console.log('Main topics for georesource:', this.mainTopicsForGeoresource);
+ console.log('Current main topic:', this.georesourceTopic_mainTopic);
+ console.log('Current sub topic:', this.georesourceTopic_subTopic);
+ console.log('Current subsub topic:', this.georesourceTopic_subsubTopic);
+ console.log('Current subsubsub topic:', this.georesourceTopic_subsubsubTopic);
+ console.log('========================');
+ }
+
+ // Filtered subtopics by topicResource === 'georesource' to align with backend hierarchy
+ get filteredSubTopicsLevel1(): any[] {
+ return this.filterSubTopicsByResource(this.georesourceTopic_mainTopic);
+ }
+
+ get filteredSubTopicsLevel2(): any[] {
+ return this.filterSubTopicsByResource(this.georesourceTopic_subTopic);
+ }
+
+ get filteredSubTopicsLevel3(): any[] {
+ return this.filterSubTopicsByResource(this.georesourceTopic_subsubTopic);
+ }
+
+ private filterSubTopicsByResource(parentTopic: any): any[] {
+ const subs = (parentTopic?.subTopics || []);
+ return subs.filter((t: any) => t?.topicResource === 'georesource');
+ }
+
+ private initializeDefaultValues(): void {
+ // Initialize with default values from the service
+ if (this.kommonitorDataExchangeService.availablePoiMarkerColors?.length > 0) {
+ this.selectedPoiMarkerColor = this.kommonitorDataExchangeService.availablePoiMarkerColors[0];
+ }
+ if (this.kommonitorDataExchangeService.availablePoiMarkerColors?.length > 1) {
+ this.selectedPoiSymbolColor = this.kommonitorDataExchangeService.availablePoiMarkerColors[1];
+ }
+ if (this.kommonitorDataExchangeService.availableLoiDashArrayObjects?.length > 0) {
+ this.selectedLoiDashArrayObject = this.kommonitorDataExchangeService.availableLoiDashArrayObjects[0];
+ }
+ this.syncLinePatternOptionsAndSelection();
+ }
+
+ 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.kommonitorDataExchangeService.syntaxHighlightJSON(this.georesourceMetadataStructure)
+ : JSON.stringify(this.georesourceMetadataStructure, null, 2);
+ }
+
+ private setupEventListeners(): void {
+ // Listen for edit georesource metadata event
+ const editSub = this.broadcastService.currentBroadcastMsg.subscribe((data: any) => {
+ if (data.msg === 'onEditGeoresourceMetadata') {
+ // Align with BroadcastService signature { msg, values }
+ const payload = (data && (data.values ?? data.georesourceDataset)) || null;
+ if (payload) {
+ this.currentGeoresourceDataset = payload;
+ }
+ }
+ });
+ 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;
+
+ // Load topics data if not already loaded
+ this.loadTopicsData();
+
+ // 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
+ if (this.kommonitorDataExchangeService.updateIntervalOptions) {
+ 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 || false;
+ this.isLOI = this.currentGeoresourceDataset.isLOI || false;
+ this.isAOI = this.currentGeoresourceDataset.isAOI || false;
+
+ if (this.isPOI) {
+ this.georesourceType = 'poi';
+ } else if (this.isLOI) {
+ this.georesourceType = 'loi';
+ } else {
+ this.georesourceType = 'aoi';
+ }
+
+ // Set POI colors
+ if (this.kommonitorDataExchangeService.availablePoiMarkerColors) {
+ 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
+ if (this.kommonitorDataExchangeService.availableLoiDashArrayObjects) {
+ this.kommonitorDataExchangeService.availableLoiDashArrayObjects.forEach((option: any) => {
+ if (option.dashArrayValue === this.currentGeoresourceDataset.loiDashArrayString) {
+ this.selectedLoiDashArrayObject = option;
+ this.onChangeLoiDashArray(this.selectedLoiDashArrayObject);
+ }
+ });
+ }
+ this.syncLinePatternOptionsAndSelection();
+
+ this.loiColor = this.currentGeoresourceDataset.loiColor || '#bf3d2c';
+ this.loiWidth = this.currentGeoresourceDataset.loiWidth || 3;
+ this.aoiColor = this.currentGeoresourceDataset.aoiColor || '#bf3d2c';
+ this.selectedPoiIconName = this.currentGeoresourceDataset.poiSymbolBootstrap3Name || 'home';
+
+ // Set topic hierarchy if topics are already loaded, otherwise defer until after load
+ if (this.topicsLoaded) {
+ this.applyTopicSelectionFromDataset();
+ }
+
+ // Clear any existing alert messages
+ this.successMessagePart = '';
+ this.errorMessagePart = '';
+ this.georesourceMetadataImportError = '';
+
+ // Initialize date picker
+ setTimeout(() => {
+ this.initializeDatePickers();
+ }, 250);
+ }
+
+ private initializeDatePickers(): void {
+ try {
+ // Datepicker initialization is handled by ngbDatepicker in the template.
+
+ // 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 LOI dash array dropdown
+ setTimeout(() => {
+ if (this.kommonitorDataExchangeService.availableLoiDashArrayObjects) {
+ 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;
+ if (this.kommonitorDataExchangeService.availableGeoresources) {
+ 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;
+ }
+
+ onIconSelect(iconName: string): void {
+ this.selectedPoiIconName = iconName;
+ }
+
+ // LOI methods
+ onChangeLoiDashArray(loiDashArrayObjectOrPattern: any): void {
+ const dash = loiDashArrayObjectOrPattern?.dashArrayValue;
+ if (dash) {
+ // Update selected pattern for the picker
+ this.selectedLoiPattern = this.linePatternOptions.find(p => p.dashArrayValue === dash) || null;
+ // Update legacy selected object from service list
+ const svcObj = (this.kommonitorDataExchangeService.availableLoiDashArrayObjects || []).find((o: any) => o?.dashArrayValue === dash);
+ this.selectedLoiDashArrayObject = svcObj || loiDashArrayObjectOrPattern;
+ const buttonElement = document.getElementById('loiDashArrayEditDropdownButton');
+ if (buttonElement && (svcObj?.svgString || this.selectedLoiPattern?.svgString)) {
+ buttonElement.innerHTML = (svcObj?.svgString || this.selectedLoiPattern?.svgString) as string;
+ }
+ }
+ }
+
+ private syncLinePatternOptionsAndSelection(): void {
+ // Map available LOI patterns to LinePatternOption[] for the picker
+ const src = this.kommonitorDataExchangeService.availableLoiDashArrayObjects || [];
+ this.linePatternOptions = src.map((o: any) => {
+ const display = o?.displayName || o?.dashArrayValue || '';
+ const dash = o?.dashArrayValue || '';
+ const svg = o?.svgString || `
+
+
+
+ `;
+ return { label: display, dashArrayValue: dash, svgString: svg } as LinePatternOption;
+ });
+ if (this.selectedLoiDashArrayObject) {
+ const dashSel = this.selectedLoiDashArrayObject.dashArrayValue;
+ this.selectedLoiPattern = this.linePatternOptions.find(p => p.dashArrayValue === dashSel) || null;
+ } else {
+ this.selectedLoiPattern = null;
+ }
+ }
+
+ // 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
+ if (this.kommonitorDataExchangeService.updateIntervalOptions) {
+ 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
+ if (this.kommonitorDataExchangeService.availablePoiMarkerColors) {
+ 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
+ if (this.kommonitorDataExchangeService.availableLoiDashArrayObjects) {
+ 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
+ if (this.kommonitorDataExchangeService.getTopicHierarchyForTopicId) {
+ 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) || (this.selectedLoiPattern?.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();
+ }
+
+ // Export template method (missing from AngularJS version)
+ onExportGeoresourceEditMetadataTemplate(): void {
+ const metadataJSON = JSON.stringify(this.georesourceMetadataStructure);
+ const fileName = "Georessource_Metadaten_Vorlage_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();
+ }
+
+ // Main edit method
+ editGeoresourceMetadata(): void {
+ // Set topic reference
+ let topicReference = '';
+ if (this.georesourceTopic_subsubsubTopic) {
+ topicReference = this.georesourceTopic_subsubsubTopic.topicId;
+ } else if (this.georesourceTopic_subsubTopic) {
+ topicReference = this.georesourceTopic_subsubTopic.topicId;
+ } else if (this.georesourceTopic_subTopic) {
+ topicReference = this.georesourceTopic_subTopic.topicId;
+ } else if (this.georesourceTopic_mainTopic) {
+ topicReference = this.georesourceTopic_mainTopic.topicId;
+ }
+
+ const patchBody: any = {
+ 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.toIsoDateString(this.metadata.lastUpdate) || '',
+ description: this.metadata.description || '',
+ databasis: this.metadata.databasis || ''
+ },
+ datasetName: this.datasetName || '',
+ isAOI: this.isAOI,
+ isLOI: this.isLOI,
+ isPOI: this.isPOI,
+ topicReference: topicReference,
+ poiSymbolBootstrap3Name: null,
+ poiSymbolColor: null,
+ poiMarkerColor: null,
+ poiMarkerStyle: null,
+ poiMarkerText: null,
+ loiDashArrayString: null,
+ loiColor: null,
+ loiWidth: null,
+ aoiColor: null
+ };
+
+ // Set georesource-specific fields based on type
+ if (this.isPOI) {
+ patchBody.poiSymbolBootstrap3Name = this.selectedPoiIconName || '';
+ patchBody.poiSymbolColor = this.selectedPoiSymbolColor?.colorName || '';
+ patchBody.poiMarkerColor = this.selectedPoiMarkerColor?.colorName || '';
+ patchBody.poiMarkerStyle = this.selectedPoiMarkerStyle || 'symbol';
+ patchBody.poiMarkerText = this.poiMarkerText || '';
+ } else if (this.isLOI) {
+ patchBody.loiDashArrayString = (this.selectedLoiDashArrayObject?.dashArrayValue) || (this.selectedLoiPattern?.dashArrayValue) || null;
+ patchBody.loiColor = this.loiColor || null;
+ patchBody.loiWidth = this.loiWidth || null;
+ } else if (this.isAOI) {
+ patchBody.aoiColor = this.aoiColor || null;
+ }
+
+ // Debug logging
+ console.log('PATCH Request Body:', JSON.stringify(patchBody, null, 2));
+ console.log('PATCH URL:', this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI + '/georesources/' + this.currentGeoresourceDataset.georesourceId);
+
+ this.loadingData = true;
+
+ this.http.patch(
+ this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI + '/georesources/' + this.currentGeoresourceDataset.georesourceId,
+ patchBody,
+ {
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }
+ ).subscribe({
+ next: (response: any) => {
+ console.log('PATCH Request Success:', response);
+ this.successMessagePart = this.datasetName;
+ console.log('Success message part set to:', this.successMessagePart);
+
+ this.broadcastService.broadcast('refreshGeoresourceOverviewTable', { crudType: 'edit', targetGeoresourceId: this.currentGeoresourceDataset.georesourceId });
+ console.log('Refresh broadcast sent');
+
+ // Success alert will be shown via *ngIf since successMessagePart is set
+ this.loadingData = false;
+
+ // Auto-hide success message after 5 seconds and close modal
+ setTimeout(() => {
+ console.log('Auto-hiding success alert and closing modal');
+ this.hideSuccessAlert();
+ this.activeModal.close();
+ }, 5000);
+ },
+ error: (error: any) => {
+ console.error('PATCH Request Error:', error);
+ console.error('Error Status:', error.status);
+ console.error('Error Message:', error.message);
+ console.error('Error Body:', error.error);
+
+ if (error.error) {
+ this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON
+ ? this.kommonitorDataExchangeService.syntaxHighlightJSON(error.error)
+ : JSON.stringify(error.error, null, 2);
+ } else if (error.data) {
+ this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON
+ ? this.kommonitorDataExchangeService.syntaxHighlightJSON(error.data)
+ : JSON.stringify(error.data, null, 2);
+ } else {
+ this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON
+ ? this.kommonitorDataExchangeService.syntaxHighlightJSON(error)
+ : JSON.stringify(error, null, 2);
+ }
+ // Error alert will be shown via *ngIf since errorMessagePart is set
+ this.loadingData = false;
+ }
+ });
+ }
+
+ // Alert methods - simplified since we now use *ngIf
+ showSuccessAlert(): void {
+ // Alerts are now shown/hidden via *ngIf based on message content
+ console.log('Success alert should be visible for:', this.successMessagePart);
+ }
+
+ showErrorAlert(): void {
+ // Alerts are now shown/hidden via *ngIf based on message content
+ console.log('Error alert should be visible for:', this.errorMessagePart);
+ }
+
+ showMetadataImportErrorAlert(): void {
+ // Alerts are now shown/hidden via *ngIf based on message content
+ console.log('Metadata import error alert should be visible');
+ }
+
+ hideSuccessAlert(): void {
+ this.successMessagePart = '';
+ console.log('Success alert hidden');
+ }
+
+ hideErrorAlert(): void {
+ this.errorMessagePart = '';
+ console.log('Error alert hidden');
+ }
+
+ hideMetadataErrorAlert(): void {
+ this.georesourceMetadataImportError = '';
+ console.log('Metadata import error alert hidden');
+ }
+
+ // Compute and cache filtered topics for georesource
+ private updateMainTopicsForGeoresource(): void {
+ const topics = this.kommonitorDataExchangeService.availableTopics;
+ if (!topics) {
+ this.mainTopicsForGeoresource = [];
+ return;
+ }
+ // 1) Filter to main topics for georesources (align with Add modal)
+ let filtered = this.filterTopicsForGeoresources(Array.isArray(topics) ? topics : []);
+ console.log('[GeoresourceEditMetadataModal] updateMainTopicsForGeoresource: after filter', {
+ inputLength: Array.isArray(topics) ? topics.length : 'n/a',
+ filteredLength: filtered.length,
+ sample: filtered.slice(0, 3)
+ });
+ // 2) Normalize to ensure consistent keys and child arrays
+ filtered = this.normalizeTopics(filtered);
+ console.log('[GeoresourceEditMetadataModal] updateMainTopicsForGeoresource: after normalize', {
+ normalizedLength: filtered.length,
+ sample: filtered.slice(0, 3)
+ });
+ // 3) Deduplicate by displayed label first (case-insensitive)
+ filtered = this.deduplicateTopicsByLabel(filtered);
+ // 4) Ensure uniqueness by ID as well
+ this.mainTopicsForGeoresource = this.deduplicateTopicsById(filtered);
+ }
+
+ /**
+ * Filter topics to only show main topics for georesources (like AngularJS component)
+ */
+ private filterTopicsForGeoresources(topics: any[]): any[] {
+ const result = (topics || []).filter((topic: any) =>
+ topic && topic.topicType === 'main' && topic.topicResource === 'georesource'
+ );
+ console.log('[GeoresourceEditMetadataModal] filterTopicsForGeoresources', {
+ inputLength: Array.isArray(topics) ? topics.length : 'n/a',
+ outputLength: result.length,
+ firstItem: result[0]
+ });
+ return result;
+ }
+
+ /**
+ * Remove duplicates by the displayed label (case-insensitive), e.g., topicName/name.
+ */
+ private deduplicateTopicsByLabel(topics: any[]): any[] {
+ const map = new Map();
+ for (const t of topics) {
+ const label = ((t?.topicName ?? t?.name ?? '') + '').trim().toLowerCase();
+ const fallback = ((t?.topicId ?? t?.id ?? '') + '').trim().toLowerCase();
+ const key = label || fallback;
+ if (!key) { continue; }
+ if (!map.has(key)) {
+ map.set(key, t);
+ } else {
+ const current = map.get(key);
+ const currChildren = Array.isArray(current?.subTopics) ? current.subTopics.length : 0;
+ const newChildren = Array.isArray(t?.subTopics) ? t.subTopics.length : 0;
+ const currHasId = !!(current?.topicId || current?.id);
+ const newHasId = !!(t?.topicId || t?.id);
+ if (newChildren > currChildren || (!currHasId && newHasId)) {
+ map.set(key, t);
+ }
+ }
+ }
+ return Array.from(map.values());
+ }
+
+ /**
+ * Remove duplicates from topics array by stable identifier (topicId | id | name fallback)
+ */
+ private deduplicateTopicsById(topics: any[]): any[] {
+ const map = new Map();
+ for (const t of topics) {
+ const key = ((t?.topicId ?? t?.id ?? t?.name) + '').trim();
+ if (!key) { continue; }
+ if (!map.has(key)) {
+ map.set(key, t);
+ } else {
+ const current = map.get(key);
+ const currChildren = Array.isArray(current?.subTopics) ? current.subTopics.length : 0;
+ const newChildren = Array.isArray(t?.subTopics) ? t.subTopics.length : 0;
+ if (newChildren > currChildren) {
+ map.set(key, t);
+ }
+ }
+ }
+ return Array.from(map.values());
+ }
+
+ // Normalize topic tree to always use 'subTopics' recursively and provide label fallback
+ private normalizeTopics(topics: any[]): any[] {
+ return (topics || []).map(t => this.normalizeTopicNode(t));
+ }
+
+ private normalizeTopicNode(topic: any): any {
+ if (!topic || typeof topic !== 'object') { return topic; }
+ topic.topicName = topic.topicName || topic.name || topic.title || topic.label || topic.text || topic.topicname;
+ const children = topic.subTopics || topic.subtopics || topic.children || [];
+ topic.subTopics = Array.isArray(children) ? children.map((c: any) => this.normalizeTopicNode(c)) : [];
+ return topic;
+ }
+
+ private applyTopicSelectionFromDataset(): void {
+ if (!this.currentGeoresourceDataset?.topicReference) { return; }
+ if (!this.kommonitorDataExchangeService.getTopicHierarchyForTopicId) { return; }
+ 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];
+ }
+ }
+
+ // 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++;
+ // Reapply dynamic UI after step change
+ setTimeout(() => this.reapplyDynamicUiFields(), 0);
+ }
+ }
+
+ previousStep(): void {
+ if (this.currentStep > 1) {
+ this.currentStep--;
+ // Reapply dynamic UI after step change
+ setTimeout(() => this.reapplyDynamicUiFields(), 0);
+ }
+ }
+
+ goToStep(step: number): void {
+ if (step >= 1 && step <= 3) {
+ this.currentStep = step;
+ // Reapply dynamic UI after step change
+ setTimeout(() => this.reapplyDynamicUiFields(), 0);
+ }
+ }
+
+
+ // Modal control
+ cancel(): void {
+ this.activeModal.dismiss();
+ }
+
+ // Reapply dynamic UI state (patterns, color pickers) after DOM updates
+ private reapplyDynamicUiFields(): void {
+ try {
+ // Ensure line pattern options and selection are in sync
+ this.syncLinePatternOptionsAndSelection();
+ // Reinitialize pickers and restore LOI button preview
+ this.initializeDatePickers();
+ const buttonElement = document.getElementById('loiDashArrayEditDropdownButton');
+ const svg = (this.selectedLoiDashArrayObject && this.selectedLoiDashArrayObject.svgString)
+ || (this.selectedLoiPattern && this.selectedLoiPattern.svgString);
+ if (buttonElement && svg) {
+ buttonElement.innerHTML = svg;
+ }
+ } catch {}
+ }
+
+}
\ 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..82700a246
--- /dev/null
+++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditUserRolesModal/georesource-edit-user-roles-modal.component.css
@@ -0,0 +1,261 @@
+/*progressbar*/
+#progressbar {
+ margin-bottom: 10px;
+ overflow: hidden;
+ /*CSS counters to number the steps*/
+ counter-reset: step;
+ /* z-index: 10000; */
+}
+
+#progressbar li {
+ list-style-type: none;
+ color: black;
+ text-transform: uppercase;
+ font-size: 9px;
+ /* width: 33.33%; */
+ float: left;
+ position: relative;
+ letter-spacing: 1px;
+ /* transform-style: preserve-3d; */
+ /* z-index: 1; */
+ 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);
+ /* z-index: +1; */
+}
+
+/*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;
+ /* transform: translateZ(-2px); */
+}
+
+#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;
+}
+
+/* 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..c52e167b1
--- /dev/null
+++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditUserRolesModal/georesource-edit-user-roles-modal.component.html
@@ -0,0 +1,160 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Zugriffsschutz
+ Eigentuümerschaft
+
+
+
+
+
+ Zugriffsschutz
+ Vergabe der Zugriffsrechte auf Datensatz-Metadaten und -Features pro Organisationseinheit
+
+ Zugriffsrechte (lesen, editieren) müssen explizit vergeben werden
+
+
+
+
+ Zeige nur zugewiesene Rechte
+
+
+
+
+
+
+
+
Öffentliche Lesefreigabe*
+
+
+
+
+
+
+ Öffentlich freigegebene Datensätze können ohne Login abgerufen werden.
+
+
+
+
+
+
+ Als Eigentümer-Organisation des Datensatzes können Sie Lese- und Editier-Rechte an die eigene und weitere Organisationseinheiten vergeben.
+
+
+
+
+
+
+
+
+ Eigentuümerschaft eines Datensatzes
+ Übetragen der Eigentuümerschaft von Datensätzen mit allen dazugehörigen Rechten
+
+
+ Bitte beachten Sie, dass Sie beim Übertragen einer Eigentuümerschaft einer Resource unter Umständen jegliche Rechte eben dieser verlieren. Die Rechte werden unwiderruflich und sofort an den neuen Eigentümer übertragen.
+
+
+
+
+
Eigentuümerschaft übertragen an
+
+
+
+
+
+ Eigentuümerschaft nicht übertragen
+ {{org.name}}
+
+
+ Eigentuümerschaft nicht übertragen
+ {{org.name}}
+
+
+ Lade Organisationseinheiten...
+
+
+
+
aktuelle Eigentümer-Organisationseinheit
+
{{getCurrentOwnerName()}}
+
Lade...
+
+
+ ACHTUNG: Sie sind dabei, die Eigentuümerschaft an diesem Datensatz zu ändern.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ×
+
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..d1b46f92b
--- /dev/null
+++ b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceEditUserRolesModal/georesource-edit-user-roles-modal.component.ts
@@ -0,0 +1,795 @@
+import { Component, OnInit, OnDestroy, 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 { KommonitorGeoresourceDataGridHelperService } from 'services/adminGeoresourceUnit/kommonitor-data-grid-helper.service';
+import { KommonitorGeoresourceDataExchangeService } from 'services/adminGeoresourceUnit/kommonitor-data-exchange.service';
+import { KommonitorMultiStepFormHelperService } from 'services/adminGeoresourceUnit/kommonitor-multi-step-form-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 = '';
+
+ // Track if access control data is available
+ accessControlDataAvailable = false;
+
+ // Current dataset being edited
+ private _currentGeoresourceDataset: any;
+
+ get currentGeoresourceDataset(): any {
+ return this._currentGeoresourceDataset;
+ }
+
+ set currentGeoresourceDataset(value: any) {
+ // Prefer freshest copy from service if available
+ const latest = value?.georesourceId ? this.kommonitorDataExchangeService.getGeoresourceMetadataById(value.georesourceId) : undefined;
+ this._currentGeoresourceDataset = latest || value;
+ }
+
+ // Role management
+ roleManagementTableOptions: any = undefined;
+
+ // ag-Grid properties (like spatial unit component)
+ roleManagementColumnDefs: ColDef[] = [];
+ roleManagementRowData: any[] = [];
+ roleManagementDefaultColDef: any = {};
+ roleManagementGridOptions: GridOptions = {};
+ roleManagementGridApi: any = null;
+
+ private gridApi!: GridApi;
+ private columnApi!: ColumnApi;
+
+ // Form fields
+ activeRolesOnly = true;
+ permissions: any[] = [];
+ resourcesCreatorRights: any[] = [];
+ ownerOrgFilter = '';
+ ownerOrganization: any;
+ filteredOrganizations: any[] = [];
+ filteredCreatorRights: any[] = [];
+
+ // Messages
+ successMessagePart = '';
+ errorMessagePart = '';
+
+ // Subscriptions
+ private subscriptions: Subscription[] = [];
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public kommonitorDataExchangeService: KommonitorGeoresourceDataExchangeService,
+ public kommonitorMultiStepFormHelperService: KommonitorMultiStepFormHelperService,
+ public kommonitorDataGridHelperService: KommonitorGeoresourceDataGridHelperService,
+ private broadcastService: BroadcastService,
+ private http: HttpClient
+ ) {}
+
+ async ngOnInit(): Promise {
+ this.setupEventListeners();
+
+ // Load access control data first, then prepare creator list
+ await this.loadAccessControlData();
+
+ // Now prepare creator list since access control data should be available
+ this.prepareCreatorList();
+ this.updateFilteredLists();
+ }
+
+ 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);
+ }
+
+ async onEditGeoresourcesUserRoles(georesourceDataset: any): Promise {
+ // Prefer freshest copy from service if available
+ const latest = georesourceDataset?.georesourceId ? this.kommonitorDataExchangeService.getGeoresourceMetadataById(georesourceDataset.georesourceId) : undefined;
+ this.currentGeoresourceDataset = latest || georesourceDataset;
+ // Force a re-resolve right before showing the form
+ this.currentGeoresourceDataset = this.resolveLatestDataset(this.currentGeoresourceDataset);
+ this.resetGeoresourceEditUserRolesForm();
+ this.kommonitorMultiStepFormHelperService?.registerClickHandler('georesourceEditUserRolesForm');
+
+ // Ensure access control data is loaded when the modal opens
+ await this.ensureAccessControlDataLoaded();
+ }
+
+ prepareCreatorList(): void {
+ // prepare creator list based on current login roles
+
+ if (!this.kommonitorDataExchangeService.accessControl || this.kommonitorDataExchangeService.accessControl.length === 0) {
+ return;
+ }
+
+ // Match AngularJS pattern: check if roles exist and process them
+ 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' && !creatorRights.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)
+ ) || [];
+ this.updateFilteredLists();
+ } else {
+ // Fallback: use all access control data if no specific creator rights are available
+ // This ensures the dropdown shows organizations even for users without specific creator roles
+ this.resourcesCreatorRights = this.kommonitorDataExchangeService.accessControl || [];
+ this.updateFilteredLists();
+ }
+ }
+
+ 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 {
+ // Ensure we operate on the freshest dataset
+ this.currentGeoresourceDataset = this.resolveLatestDataset(this.currentGeoresourceDataset);
+ this.permissions = this.currentGeoresourceDataset ? this.currentGeoresourceDataset.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
+ // Consider both current owner and selected new owner
+ const effectiveOwnerId = this.ownerOrganization !== undefined ? this.ownerOrganization : this.currentGeoresourceDataset?.ownerId;
+
+ this.kommonitorDataExchangeService.accessControl.forEach((item: any) => {
+ if (effectiveOwnerId) {
+ if (item.organizationalUnitId === effectiveOwnerId) {
+ item.datasetOwner = true;
+ } else {
+ item.datasetOwner = false;
+ }
+ }
+ });
+
+ // update filtered lists once access control is confirmed
+ this.updateFilteredLists();
+
+ // Match AngularJS logic: only reset if no permissions
+ if (this.permissions.length === 0) {
+ this.activeRolesOnly = false;
+ }
+
+ // Apply filtering based on activeRolesOnly toggle
+ let access = this.kommonitorDataExchangeService.accessControl;
+
+ if (this.activeRolesOnly && this.permissions.length > 0) {
+ // Filter to show only units that have at least one permission assigned
+ access = this.kommonitorDataExchangeService.accessControl.filter((unit: any) => {
+ // Check if this unit has any of the current permissions
+ return unit.permissions?.some((unitPermission: any) =>
+ this.permissions.includes(unitPermission.permissionId)
+ ) || false;
+ });
+
+ // filtered access logged intentionally removed to avoid noisy console
+ }
+
+ this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid(
+ 'georesourceEditRoleManagementTable',
+ this.roleManagementTableOptions,
+ access,
+ this.permissions
+ );
+
+ // Extract column definitions and row data for ag-grid-angular
+ if (this.roleManagementTableOptions) {
+ this.roleManagementColumnDefs = this.roleManagementTableOptions.columnDefs || [];
+ // Get the row data (already filtered by the grid helper if activeRolesOnly is true)
+ this.roleManagementRowData = this.roleManagementTableOptions.rowData || [];
+
+ // Build grid configuration
+ this.buildRoleManagementGridConfig();
+ }
+ }
+
+ onActiveRolesOnlyChange(): void {
+ // The toggle component handles its own state, so we don't need to reverse it
+ // Just refresh the table with the new filter setting
+ console.log('Active roles only toggle changed:', {
+ activeRolesOnly: this.activeRolesOnly,
+ permissions: this.permissions,
+ accessControlLength: this.kommonitorDataExchangeService.accessControl?.length || 0
+ });
+
+ // Refresh the table with the new filter setting
+ this.refreshRoleManagementTable();
+ }
+
+ onChangeOwner(ownerOrganization: any): void {
+ this.ownerOrganization = ownerOrganization;
+
+ console.log('Owner changed:', {
+ newOwnerId: ownerOrganization,
+ currentOwnerId: this.currentGeoresourceDataset?.ownerId,
+ activeRolesOnly: this.activeRolesOnly,
+ permissions: this.permissions,
+ permissionsCount: this.permissions?.length || 0
+ });
+
+ // Refresh the roles list to show current dataset permissions
+ this.refreshRoles(ownerOrganization);
+
+ // Also refresh the main role management table to ensure consistency
+ this.refreshRoleManagementTable();
+ }
+
+ onOwnerOrgFilterChange(): void {
+ this.updateFilteredLists();
+ }
+
+ onPublicPrivateChange(): void {
+ console.log('Public/Private toggle changed:', {
+ isPublic: this.currentGeoresourceDataset?.isPublic,
+ activeRolesOnly: this.activeRolesOnly
+ });
+
+ // Don't refresh the table here - the public/private setting doesn't affect the role management table
+ // Only refresh if we need to update other parts of the UI
+ }
+
+ // Method to handle when permissions change (e.g., when roles are added/removed)
+ onPermissionsChanged(): void {
+ console.log('Permissions changed:', {
+ permissions: this.permissions,
+ activeRolesOnly: this.activeRolesOnly
+ });
+
+ // Refresh the table to reflect permission changes
+ this.refreshRoleManagementTable();
+ }
+
+ private refreshRoles(orgUnitId: any): void {
+ const accessControl = this.kommonitorDataExchangeService.getAccessControlById(orgUnitId);
+
+ // Use the current dataset's permissions, not the new owner's permissions
+ // This ensures users can still see and manage the current dataset's roles
+ const permissionIds_toUse = this.permissions || [];
+
+ // keep filtered lists up to date
+ this.updateFilteredLists();
+
+ // 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;
+ }
+ });
+
+ // Apply the same filtering logic as refreshRoleManagementTable
+ let access = this.kommonitorDataExchangeService.accessControl || [];
+
+ if (this.activeRolesOnly && this.permissions.length > 0) {
+ // Filter to show only units that have at least one permission assigned
+ access = this.kommonitorDataExchangeService.accessControl.filter((unit: any) => {
+ // Check if this unit has any of the current permissions
+ return unit.permissions?.some((unitPermission: any) =>
+ this.permissions.includes(unitPermission.permissionId)
+ ) || false;
+ });
+ }
+
+ this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid(
+ 'georesourceEditRoleManagementTable',
+ this.roleManagementTableOptions,
+ access,
+ permissionIds_toUse // Use current dataset permissions, not new owner permissions
+ );
+
+ // 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();
+
+ // grid updated
+ }
+ }, 100);
+ }
+ }
+ }
+
+ // Step navigation
+ nextStep(): void {
+ if (this.currentStep < this.totalSteps) {
+ this.currentStep++;
+ // Ensure roles list is up to date when moving to next step
+ this.ensureRolesListUpToDate();
+ }
+ }
+
+ previousStep(): void {
+ if (this.currentStep > 1) {
+ this.currentStep--;
+ // Ensure roles list is up to date when moving to previous step
+ this.ensureRolesListUpToDate();
+ }
+ }
+
+ goToStep(step: number): void {
+ if (step >= 1 && step <= this.totalSteps) {
+ this.currentStep = step;
+ // Ensure roles list is up to date when changing steps
+ this.ensureRolesListUpToDate();
+ }
+ }
+
+ // Ensure that the roles list is properly updated when navigating between steps
+ private ensureRolesListUpToDate(): void {
+ // If we're on step 1 (Zugriffsschutz) and ownership has changed, refresh the roles list
+ if (this.currentStep === 1 && this.ownerOrganization !== this.currentGeoresourceDataset?.ownerId) {
+ console.log('Ensuring roles list is up to date for step 1 after ownership change');
+ this.refreshRoleManagementTable();
+ }
+
+ // If we're on step 2 (Eigentümerschaft), ensure access control data is loaded
+ if (this.currentStep === 2) {
+ console.log('Ensuring access control data is loaded for step 2');
+ this.ensureAccessControlDataLoaded();
+ }
+ }
+
+ // Ensure access control data is loaded for the dropdown
+ private async ensureAccessControlDataLoaded(): Promise {
+ if (!this.accessControlDataAvailable || !this.kommonitorDataExchangeService.accessControl || this.kommonitorDataExchangeService.accessControl.length === 0) {
+ await this.loadAccessControlData();
+
+ // After loading, prepare creator list again
+ this.prepareCreatorList();
+ this.updateFilteredLists();
+ }
+ }
+
+ // Handle checkbox changes
+ onCellValueChanged(event: any): void {
+ if (event.colDef.field === 'viewer' || event.colDef.field === 'editor' || event.colDef.field === 'creator') {
+ // Update the row data
+ const rowData = event.data;
+ const field = event.colDef.field;
+ rowData[field] = event.newValue;
+
+ // Update the grid options row data to keep it in sync
+ if (this.roleManagementTableOptions && this.roleManagementTableOptions.rowData) {
+ const gridRow = this.roleManagementTableOptions.rowData.find((row: any) => row.organizationalUnitId === rowData.organizationalUnitId);
+ if (gridRow) {
+ gridRow[field] = event.newValue;
+ }
+ }
+ }
+ }
+
+ // 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 selectedRoleIds = this.getSelectedRoleIds();
+
+ // put user roles
+
+ const putBody = {
+ permissions: selectedRoleIds,
+ 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
+ });
+ // Update local dataset and shared cache to prevent stale reopen
+ if (this.currentGeoresourceDataset) {
+ this.currentGeoresourceDataset.permissions = putBody.permissions;
+ this.currentGeoresourceDataset.isPublic = putBody.isPublic;
+ this.kommonitorDataExchangeService.replaceSingleGeoresourceMetadata(this.currentGeoresourceDataset);
+ }
+ 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
+ });
+ // Update local dataset and shared cache so owner and disabled states are fresh
+ if (this.currentGeoresourceDataset) {
+ this.currentGeoresourceDataset.ownerId = putBody.ownerId;
+ this.kommonitorDataExchangeService.replaceSingleGeoresourceMetadata(this.currentGeoresourceDataset);
+ }
+ 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 {
+ // Force a re-resolve when resetting to ensure UI reflects latest data
+ this.currentGeoresourceDataset = this.resolveLatestDataset(this.currentGeoresourceDataset);
+ this.ownerOrganization = this.currentGeoresourceDataset?.ownerId;
+ this.ownerOrgFilter = '';
+ this.activeRolesOnly = false; // Reset the toggle to false
+ this.successMessagePart = '';
+ this.errorMessagePart = '';
+ this.hideSuccessAlert();
+ this.hideErrorAlert();
+
+ // Refresh the table after resetting the toggle
+ this.refreshRoleManagementTable();
+ this.updateFilteredLists();
+ }
+
+ // Ensure we always use the freshest dataset instance from the service cache
+ private resolveLatestDataset(current: any): any {
+ try {
+ const id = current?.georesourceId;
+ if (!id) { return current; }
+ const latest = this.kommonitorDataExchangeService.getGeoresourceMetadataById(id);
+ return latest || current;
+ } catch {
+ return current;
+ }
+ }
+
+ // Helper methods
+ getFilteredOrganizations(): any[] {
+ // Deprecated for template usage. Kept for compatibility.
+ return this.filteredOrganizations;
+ }
+
+ getFilteredCreatorRights(): any[] {
+ // Deprecated for template usage. Kept for compatibility.
+ return this.filteredCreatorRights;
+ }
+
+ getCurrentOwnerName(): string {
+ // Return empty string silently if data is not available (template-safe)
+ if (!this.accessControlDataAvailable || !this.kommonitorDataExchangeService.accessControl || this.kommonitorDataExchangeService.accessControl.length === 0) {
+ return '';
+ }
+
+ 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 selectedIds = this.kommonitorDataGridHelperService.getSelectedRoleIds_roleManagementGrid(this.roleManagementTableOptions);
+
+ console.log('Getting selected role IDs:', {
+ selectedIds: selectedIds,
+ selectedIdsCount: selectedIds.length,
+ roleManagementTableOptions: this.roleManagementTableOptions,
+ hasRowData: !!this.roleManagementTableOptions?.rowData,
+ rowDataCount: this.roleManagementTableOptions?.rowData?.length || 0,
+ sampleRowData: this.roleManagementTableOptions?.rowData?.slice(0, 2) || []
+ });
+
+ return selectedIds;
+ }
+
+ // 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();
+ }
+
+ // Grid configuration methods (like spatial unit component)
+ private buildRoleManagementGridConfig(): void {
+ this.roleManagementDefaultColDef = this.buildRoleManagementDefaultColDef();
+ this.roleManagementGridOptions = this.buildRoleManagementGridOptions();
+ }
+
+ private buildRoleManagementDefaultColDef(): any {
+ return {
+ 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': '12px',
+ 'padding-bottom': '12px'
+ },
+ headerComponentParams: {
+ template:
+ '' +
+ ' ' +
+ ' ' +
+ '
',
+ },
+ };
+ }
+
+ private buildRoleManagementGridOptions(): GridOptions {
+ // Use components from the table options if available
+ const components = this.roleManagementTableOptions?.components || {};
+
+ return {
+ components: components,
+ suppressRowClickSelection: true,
+ rowSelection: 'multiple',
+ enableCellTextSelection: true,
+ ensureDomOrder: true,
+ pagination: true,
+ paginationPageSize: 10,
+ suppressColumnVirtualisation: true,
+ headerHeight: 40,
+ rowHeight: 35,
+ onGridReady: (params) => {
+ this.onRoleManagementGridReady(params);
+ // Add cell value changed event listener for checkboxes
+ if (params.api) {
+ params.api.addEventListener('cellValueChanged', this.onCellValueChanged.bind(this));
+ }
+ },
+ onFirstDataRendered: (event) => {
+ this.onRoleManagementFirstDataRendered(event);
+ },
+ onColumnResized: (event) => {
+ this.onRoleManagementColumnResized(event);
+ },
+ onRowDataUpdated: (event) => {
+ try {
+ event.api.resetRowHeights();
+ } catch {}
+ }
+ };
+ }
+
+ onRoleManagementGridReady(params: GridReadyEvent): void {
+ this.roleManagementGridApi = params.api;
+ try {
+ params.api.sizeColumnsToFit();
+ params.api.resetRowHeights();
+ } catch {}
+ }
+
+ onRoleManagementFirstDataRendered(event: any): void {
+ try {
+ event.api.resetRowHeights();
+ event.api.sizeColumnsToFit();
+ } catch {}
+ }
+
+ onRoleManagementColumnResized(event: any): void {
+ try {
+ event.api.resetRowHeights();
+ } catch {}
+ }
+
+ private async loadAccessControlData(): Promise {
+ // Check if access control data is already available
+ if (this.kommonitorDataExchangeService.accessControl && this.kommonitorDataExchangeService.accessControl.length > 0) {
+ // Set flag to true since we have data
+ this.accessControlDataAvailable = true;
+ this.updateFilteredLists();
+
+ // If we have data and a georesource dataset, refresh the table
+ if (this.currentGeoresourceDataset) {
+ this.refreshRoleManagementTable();
+ }
+ } else {
+ // Fetch access control data from server
+ try {
+ await this.kommonitorDataExchangeService.fetchAccessControlMetadata();
+
+ // Check if we successfully got data
+ if (this.kommonitorDataExchangeService.accessControl && this.kommonitorDataExchangeService.accessControl.length > 0) {
+ this.accessControlDataAvailable = true;
+ this.updateFilteredLists();
+ } else {
+ this.accessControlDataAvailable = false;
+ }
+
+ // If we have data and a georesource dataset, refresh the table
+ if (this.currentGeoresourceDataset) {
+ this.refreshRoleManagementTable();
+ }
+ } catch (error) {
+ this.accessControlDataAvailable = false;
+ }
+ }
+ }
+
+ private updateFilteredLists(): void {
+ if (!this.accessControlDataAvailable || !this.kommonitorDataExchangeService.accessControl || this.kommonitorDataExchangeService.accessControl.length === 0) {
+ this.filteredOrganizations = [];
+ this.filteredCreatorRights = [];
+ return;
+ }
+ const filter = (this.ownerOrgFilter || '').toLowerCase();
+ const all = this.kommonitorDataExchangeService.accessControl || [];
+ const creators = this.resourcesCreatorRights || [];
+ this.filteredOrganizations = !filter ? all.slice() : all.filter((org: any) => org.name?.toLowerCase().includes(filter));
+ this.filteredCreatorRights = !filter ? creators.slice() : creators.filter((org: any) => org.name?.toLowerCase().includes(filter));
+ }
+
+ // Debug methods for template
+ // (removed)
+}
\ 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..d36a721fa
--- /dev/null
+++ b/app/components/ngComponents/admin/adminIndicatorsManagement/admin-indicators-management.component.html
@@ -0,0 +1,351 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Erläuterungen zur Symbolik
+
+
+
+ Standardindikator (numerische Wertverteilung) [Einheitsbezeichnung]
+
+
+
+
+
+
+ Leitindikator (bewertende Aussage) [Einheitsbezeichnung]
+
+
+
+
+
+
Via Drag and Drop Einträge innerhalb eines (Unter-)Themas sortieren
+
+
+ Laden der Themenhierarchie...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..776b554fa
--- /dev/null
+++ b/app/components/ngComponents/admin/adminIndicatorsManagement/admin-indicators-management.component.ts
@@ -0,0 +1,894 @@
+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 { AuthService } from 'services/auth-service/auth.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 { IndicatorEditIndicatorSpatialUnitRolesModalComponent } from './indicatorEditIndicatorSpatialUnitRolesModal/indicator-edit-indicator-spatial-unit-roles-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 - refresh the data to reflect the new order
+ this.refreshDataAfterDragDrop();
+ },
+ error: (error: any) => {
+ this.kommonitorDataExchangeService.displayMapApplicationError(error);
+ }
+ });
+ }
+ };
+ private subscriptions: Subscription[] = [];
+
+ // Timeout properties for debouncing
+ private modelUpdateTimeout: any = null;
+ private viewportChangeTimeout: any = null;
+
+ // Polling control
+ private isPolling: boolean = false;
+
+ // Debouncing for initializeOrRefreshOverviewTable
+ private initializeTableTimeout: any = null;
+
+ 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,
+ private authService: AuthService
+ ) {}
+
+ 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 {
+ // Get roles from AuthService (like other Angular components)
+ let roles: string[] = [];
+
+ if (this.authService.Auth && this.authService.Auth.keycloak &&
+ this.authService.Auth.keycloak.tokenParsed &&
+ this.authService.Auth.keycloak.tokenParsed.realm_access &&
+ this.authService.Auth.keycloak.tokenParsed.realm_access.roles) {
+ roles = this.authService.Auth.keycloak.tokenParsed.realm_access.roles;
+ } else {
+ // If no roles available, try with empty array
+ roles = [];
+ }
+
+ await this.kommonitorDataExchangeService.fetchIndicatorsMetadata(roles);
+ // Force refresh the table after data is loaded
+ setTimeout(() => {
+ this.forceRefreshGrid();
+ }, 100);
+ } catch (error) {
+ console.error("Admin Component - Error fetching indicators:", error);
+ // Set loading to false to prevent infinite retries
+ this.loadingData = false;
+ this.initializationCompleted = true;
+ }
+ }
+ }
+
+ private forceRefreshGrid(): void {
+ const indicators = this.getFilteredIndicators();
+ if (!indicators || !Array.isArray(indicators)) {
+ this.loadingData = false;
+ return;
+ }
+
+ 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.setGridOption('rowData', this.rowData);
+ this.agGrid.api.setColumnDefs(this.columnDefs);
+ this.agGrid.api.refreshCells();
+ this.loadingData = false;
+ this.initializationCompleted = true;
+ }
+ } else {
+ this.loadingData = false;
+ }
+ }
+
+ ngOnDestroy(): void {
+ this.subscriptions.forEach(sub => sub.unsubscribe());
+
+ // Clean up timeouts
+ if (this.modelUpdateTimeout) {
+ clearTimeout(this.modelUpdateTimeout);
+ }
+ if (this.viewportChangeTimeout) {
+ clearTimeout(this.viewportChangeTimeout);
+ }
+ if (this.initializeTableTimeout) {
+ clearTimeout(this.initializeTableTimeout);
+ }
+
+ // 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);
+ });
+ }
+ });
+ this.subscriptions.push(sub);
+ }
+
+ public initializeOrRefreshOverviewTable(): void {
+ // Add debouncing to prevent excessive calls
+ if (this.initializeTableTimeout) {
+ clearTimeout(this.initializeTableTimeout);
+ }
+
+ this.initializeTableTimeout = setTimeout(() => {
+ 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.setGridOption('rowData', this.rowData);
+ this.agGrid.api.setColumnDefs(this.columnDefs);
+ this.agGrid.api.refreshCells();
+ }
+ }, 100);
+ } else {
+ // Check if we should stop trying to load data
+ const availableIndicators = this.kommonitorDataExchangeService.availableIndicators;
+ if (availableIndicators && Array.isArray(availableIndicators)) {
+ // Data is available but filtered out, stop loading
+ this.loadingData = false;
+ this.initializationCompleted = true;
+ } else {
+ // Data not ready yet, keep loading
+ this.loadingData = true;
+ this.initializationCompleted = false;
+ }
+ }
+ }, 100); // 100ms debounce
+ }
+
+ private setupGridOptions(indicatorMetadataArray: any[]): void {
+ this.gridOptions = {
+ defaultColDef: {
+ editable: false,
+ sortable: true,
+ flex: 1,
+ minWidth: 200,
+ filter: 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);
+ },
+ 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.setGridOption('rowData', 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(event: any): void {
+ this.registerClickHandler_indicators();
+ }
+
+ onColumnResized(event: any): void {
+ // Column resized
+ }
+
+ onRowDataChanged(): void {
+ this.registerClickHandler_indicators();
+ }
+
+ onModelUpdated(): void {
+ // Add debouncing to prevent excessive calls
+ if (this.modelUpdateTimeout) {
+ clearTimeout(this.modelUpdateTimeout);
+ }
+
+ this.modelUpdateTimeout = setTimeout(() => {
+ this.registerClickHandler_indicators();
+ }, 100);
+ }
+
+ onViewportChanged(): void {
+ // Add debouncing to prevent excessive calls
+ if (this.viewportChangeTimeout) {
+ clearTimeout(this.viewportChangeTimeout);
+ }
+
+ this.viewportChangeTimeout = setTimeout(() => {
+ this.registerClickHandler_indicators();
+ 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);
+ }, 100);
+ }
+
+ onSelectionChanged(event: SelectionChangedEvent): void {
+ this.selectedRows = event.api.getSelectedRows();
+ }
+
+ private registerClickHandler_indicators(): 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).$;
+
+ if (!$) {
+ console.error('jQuery not available');
+ return;
+ }
+
+ const gridContainer = $('#adminIndicatorsOverviewTable');
+
+ // Remove any existing handlers first to avoid duplicates
+ gridContainer.off('click', '.indicatorEditMetadataBtn');
+ gridContainer.off('click', '.indicatorEditFeaturesBtn');
+ gridContainer.off('click', '.indicatorEditRoleBasedAccessBtn');
+
+ // Edit Metadata Button - use event delegation
+ gridContainer.on('click', '.indicatorEditMetadataBtn', (event: any) => {
+ event.stopPropagation();
+ event.preventDefault();
+
+ // Get the button element (could be the icon inside)
+ const button = $(event.target).closest('.indicatorEditMetadataBtn')[0];
+
+ if (button && button.id) {
+ const indicatorId = button.id.split('_')[3];
+
+ const indicatorMetadata = this.kommonitorDataExchangeService.getIndicatorMetadataById(indicatorId);
+
+ if (indicatorMetadata) {
+ this.zone.run(() => {
+ this.onClickEditMetadata(indicatorMetadata);
+ });
+ } else {
+ console.error('No indicator metadata found for ID:', indicatorId);
+ }
+ } else {
+ console.error('Button element or ID not found');
+ }
+ });
+
+ // Edit Features Button - use event delegation
+ gridContainer.on('click', '.indicatorEditFeaturesBtn', (event: any) => {
+ event.stopPropagation();
+ event.preventDefault();
+
+ // Get the button element (could be the icon inside)
+ const button = $(event.target).closest('.indicatorEditFeaturesBtn')[0];
+
+ const indicatorId = button.id.split('_')[3];
+ const indicatorMetadata = this.kommonitorDataExchangeService.getIndicatorMetadataById(indicatorId);
+
+ if (indicatorMetadata) {
+ this.zone.run(() => {
+ this.onClickEditFeatures(indicatorMetadata);
+ });
+ }
+ });
+
+ // Edit Role-Based Access Button - use event delegation
+ gridContainer.on('click', '.indicatorEditRoleBasedAccessBtn', (event: any) => {
+ event.stopPropagation();
+ event.preventDefault();
+
+ // Get the button element (could be the icon inside)
+ const button = $(event.target).closest('.indicatorEditRoleBasedAccessBtn')[0];
+ const indicatorId = button.id.split('_')[3];
+ const indicatorMetadata = this.kommonitorDataExchangeService.getIndicatorMetadataById(indicatorId);
+
+ if (indicatorMetadata) {
+ this.zone.run(() => {
+ this.onClickEditIndicatorSpatialUnitRoles(indicatorMetadata);
+ });
+ }
+ });
+ }
+
+ private getFilteredIndicators(): any[] {
+ const allIndicators = this.kommonitorDataExchangeService.availableIndicators;
+
+ if (!allIndicators || !Array.isArray(allIndicators)) {
+ return [];
+ }
+
+ 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 {
+ const modalRef = this.modalService.open(IndicatorAddModalComponent, {
+ size: 'lg',
+ backdrop: 'static',
+ keyboard: false,
+ container: 'body',
+ animation: false
+ });
+
+ modalRef.result.then((result) => {
+ if (result) {
+ this.initializeOrRefreshOverviewTable();
+ }
+ }).catch(() => {
+ // Modal dismissed
+ });
+ }
+
+ onClickEditMetadata(indicatorMetadata: any): void {
+ const modalRef = this.modalService.open(IndicatorEditMetadataModalComponent, {
+ size: 'lg',
+ backdrop: 'static',
+ keyboard: false,
+ container: 'body',
+ animation: false
+ });
+
+ modalRef.componentInstance.currentIndicatorDataset = indicatorMetadata;
+
+ modalRef.result.then((result) => {
+ if (result) {
+ this.initializeOrRefreshOverviewTable();
+ }
+ }).catch((error) => {
+ // Modal dismissed
+ });
+ }
+
+ onClickEditFeatures(indicatorMetadata: any): void {
+ const modalRef = this.modalService.open(IndicatorEditFeaturesModalComponent, {
+ size: 'lg',
+ backdrop: 'static',
+ keyboard: false,
+ container: 'body',
+ animation: false
+ });
+
+ modalRef.componentInstance.currentIndicatorDataset = indicatorMetadata;
+
+ modalRef.result.then((result) => {
+ if (result) {
+ this.initializeOrRefreshOverviewTable();
+ }
+ }).catch(() => {
+ // Modal dismissed
+ });
+ }
+
+ onClickEditIndicatorSpatialUnitRoles(indicatorMetadata: any): void {
+ const modalRef = this.modalService.open(IndicatorEditIndicatorSpatialUnitRolesModalComponent, {
+ size: 'xl',
+ backdrop: 'static',
+ keyboard: false,
+ container: 'body',
+ animation: false
+ });
+
+ modalRef.componentInstance.currentIndicatorDataset = indicatorMetadata;
+
+ modalRef.result.then((result) => {
+ if (result) {
+ this.initializeOrRefreshOverviewTable();
+ }
+ }).catch(() => {
+ // Modal dismissed
+ });
+ }
+
+ 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
+ }
+ }
+
+ openDeleteIndicatorModal(indicatorDataset: any): void {
+ const modalRef = this.modalService.open(IndicatorDeleteModalComponent, {
+ size: 'lg',
+ backdrop: 'static',
+ keyboard: false,
+ container: 'body',
+ animation: false
+ });
+
+ modalRef.componentInstance.selectedIndicatorDataset = indicatorDataset;
+
+ modalRef.result.then((result) => {
+ if (result) {
+ this.initializeOrRefreshOverviewTable();
+ }
+ }).catch(() => {
+ // Modal dismissed
+ });
+ }
+
+ onClickBatchUpdate(): void {
+ const modalRef = this.modalService.open(IndicatorBatchUpdateModalComponent, {
+ size: 'lg',
+ backdrop: 'static',
+ keyboard: false,
+ container: 'body',
+ animation: false
+ });
+
+ modalRef.result.then((result) => {
+ if (result) {
+ this.initializeOrRefreshOverviewTable();
+ }
+ }).catch(() => {
+ // Modal dismissed
+ });
+ }
+
+ 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 {
+ if (this.isPolling) {
+ return;
+ }
+
+ let pollCount = 0;
+ const maxPolls = 20; // Maximum number of polls (10 seconds at 500ms intervals)
+
+ this.isPolling = true;
+
+ // Poll every 500ms for data availability
+ const pollInterval = setInterval(() => {
+ pollCount++;
+
+ if (this.loadingData && pollCount < maxPolls) {
+ this.initializeOrRefreshOverviewTable();
+
+ // If data is found, stop polling
+ if (!this.loadingData) {
+ clearInterval(pollInterval);
+ this.isPolling = false;
+ }
+ } else {
+ // Data loaded or max polls reached, stop polling
+ clearInterval(pollInterval);
+ this.isPolling = false;
+ }
+ }, 500);
+
+ // Stop polling after 10 seconds regardless
+ setTimeout(() => {
+ clearInterval(pollInterval);
+ this.isPolling = false;
+ // Force stop loading if polling times out
+ if (this.loadingData) {
+ this.loadingData = false;
+ this.initializationCompleted = true;
+ }
+ }, 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 - refresh the data
+ this.refreshDataAfterDragDrop();
+ },
+ error: (error: any) => {
+ this.kommonitorDataExchangeService.displayMapApplicationError(error);
+ }
+ });
+ }
+
+ private refreshDataAfterDragDrop(): void {
+ // Refresh the indicators data to reflect the new order
+ if (this.authService.Auth && this.authService.Auth.keycloak &&
+ this.authService.Auth.keycloak.tokenParsed &&
+ this.authService.Auth.keycloak.tokenParsed.realm_access &&
+ this.authService.Auth.keycloak.tokenParsed.realm_access.roles) {
+ const roles = this.authService.Auth.keycloak.tokenParsed.realm_access.roles;
+
+ // Fetch fresh data to reflect the new order
+ this.kommonitorDataExchangeService.fetchIndicatorsMetadata(roles).then(() => {
+ // Force refresh the table and topic hierarchy
+ this.initializeOrRefreshOverviewTable();
+ this.initializeCollapsedTopics();
+ }).catch((error) => {
+ console.error("Error refreshing data after drag drop:", 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..c9722375e
--- /dev/null
+++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorAddModal/indicator-add-modal.component.css
@@ -0,0 +1,1152 @@
+/* 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;
+}
+
+/* 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;
+}
+
+/* Fieldset Styles */
+fieldset {
+ border: 0 none;
+ border-radius: 0.5rem;
+ box-sizing: border-box;
+ margin: 0;
+ padding-bottom: 20px;
+ position: relative;
+}
+
+/* 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;
+}
+
+/* Action buttons - Centered */
+.action-button {
+ width: 150px;
+ background: var(--kommonitor-primary);
+ color: white;
+ border: 0 none;
+ border-radius: 5px;
+ cursor: pointer;
+ padding: 10px 5px;
+ margin: 5px 5px 10px 10px;
+ text-decoration: none;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.3s ease;
+}
+
+.action-button:hover, .action-button:focus {
+ background: var(--kommonitor-primary);
+ color: white;
+ text-decoration: none;
+ box-shadow: 0 0 0 2px white, 0 0 0 3px var(--kommonitor-primary);
+}
+
+.action-button-previous {
+ width: 150px;
+ background: #95a5a6;
+ color: white;
+ border: 0 none;
+ border-radius: 5px;
+ cursor: pointer;
+ padding: 10px 5px;
+ margin: 5px 5px 10px 10px;
+ text-decoration: none;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.3s ease;
+}
+
+.action-button-previous:hover, .action-button-previous:focus {
+ background: #7f8c8d;
+ color: white;
+ text-decoration: none;
+ box-shadow: 0 0 0 2px white, 0 0 0 3px #7f8c8d;
+}
+
+.button-container {
+ text-align: center;
+ margin-top: 20px;
+}
+
+/* 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;
+ }
+}
+
+/*progressbar*/
+#progressbar {
+ margin-bottom: 10px;
+ overflow: hidden;
+ /*CSS counters to number the steps*/
+ counter-reset: step;
+ /* z-index: 10000; */
+}
+
+#progressbar li {
+ list-style-type: none;
+ color: black;
+ text-transform: uppercase;
+ font-size: 9px;
+ /* width: 33.33%; */
+ float: left;
+ position: relative;
+ letter-spacing: 1px;
+ /* transform-style: preserve-3d; */
+ /* z-index: 1; */
+ 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);
+ /* z-index: +1; */
+}
+
+/*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;
+ /* transform: translateZ(-2px); */
+}
+
+#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;
+}
+
+/* Enhanced hover effects for better UX */
+#progressbar li:hover {
+ color: var(--kommonitor-primary);
+}
+
+#progressbar li:hover:before {
+ background: var(--kommonitor-primary);
+ transform: scale(1.1);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+}
+
+#progressbar li:active:before {
+ transform: scale(1.05);
+}
+
+/* Completed steps */
+#progressbar li.completed:before {
+ background: var(--kommonitor-primary);
+}
+
+#progressbar li.completed:after {
+ background: var(--kommonitor-primary);
+}
+
+/* Error states */
+#progressbar li.error:before {
+ background: #e74c3c;
+}
+
+#progressbar li.error:after {
+ background: #e74c3c;
+}
+
+#progressbar li.error {
+ color: #e74c3c;
+}
+
+/* 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;
+ }
+}
+
+/* SVG Color Preview Styles */
+.dropdown-menu-center {
+ text-align: center;
+}
+
+.dropdown-menu-center li {
+ display: inline-block;
+ margin: 5px;
+}
+
+.dropdown-menu-center li a {
+ display: block;
+ padding: 5px;
+ text-decoration: none;
+ color: #333;
+}
+
+.dropdown-menu-center li a:hover {
+ background-color: #f8f9fa;
+}
+
+/* Boxed Legend Styles */
+.boxedLegend {
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ padding: 15px;
+ background-color: white;
+}
+
+.boxedLegend p {
+ margin-bottom: 15px;
+ font-weight: 600;
+ color: #333;
+}
+
+.boxedLegend .row {
+ margin-bottom: 8px;
+ padding: 5px 0;
+ border-bottom: 1px solid #eee;
+}
+
+.boxedLegend .row:last-child {
+ border-bottom: none;
+}
+
+.boxedLegend .col-md-2 i {
+ width: 20px;
+ height: 20px;
+ display: inline-block;
+ border-radius: 3px;
+ border: 1px solid #ddd;
+}
+
+.boxedLegend .col-md-5 {
+ font-size: 12px;
+ color: #333;
+}
+
+.boxedLegend .col-md-10 {
+ font-size: 12px;
+ color: #333;
+}
+
+/* Just Padding Class */
+.just-padding {
+ padding: 20px;
+}
+
+/* Tab Error Styles */
+.nav-tabs > li.tab-error > a {
+ color: #e74c3c;
+ border-bottom-color: #e74c3c;
+}
+
+.nav-tabs > li.tab-completed > a {
+ color: #27AE60;
+ border-bottom-color: #27AE60;
+}
+
+/* Classification Method Select Container */
+.classificationMethodSelectContainer {
+ margin-bottom: 20px;
+}
+
+/* Enhanced Classification Method Selector */
+.classification-method-selector {
+ margin-bottom: 15px;
+}
+
+.method-options {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 15px;
+ margin-bottom: 15px;
+}
+
+.method-option {
+ display: flex;
+ align-items: center;
+ padding: 15px;
+ border: 2px solid #e0e0e0;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ background-color: #fafafa;
+}
+
+.method-option:hover {
+ border-color: #007bff;
+ background-color: #f8f9fa;
+ transform: translateY(-2px);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+.method-option.selected {
+ border-color: #007bff;
+ background-color: #e3f2fd;
+ box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2);
+}
+
+.method-icon {
+ margin-right: 15px;
+ flex-shrink: 0;
+}
+
+.method-icon-img {
+ width: 40px;
+ height: 40px;
+ object-fit: contain;
+}
+
+.method-info {
+ flex: 1;
+}
+
+.method-name {
+ font-weight: 600;
+ font-size: 14px;
+ color: #333;
+ margin-bottom: 5px;
+}
+
+.method-description {
+ font-size: 12px;
+ color: #666;
+ line-height: 1.4;
+}
+
+.fallback-select {
+ display: none;
+}
+
+/* Mobile responsive */
+@media (max-width: 768px) {
+ .method-options {
+ grid-template-columns: 1fr;
+ }
+
+ .method-option {
+ padding: 12px;
+ }
+
+ .method-icon-img {
+ width: 30px;
+ height: 30px;
+ }
+
+ .fallback-select {
+ display: block;
+ }
+
+ .method-options {
+ display: none;
+ }
+}
+
+/* Responsive adjustments for SVG previews */
+@media (max-width: 768px) {
+ .dropdown-menu-center {
+ columns: 2;
+ -webkit-columns: 2;
+ -moz-columns: 2;
+ }
+
+ .boxedLegend .row {
+ flex-direction: column;
+ }
+
+ .boxedLegend .col-md-2,
+ .boxedLegend .col-md-5,
+ .boxedLegend .col-md-10 {
+ width: 100%;
+ margin-bottom: 5px;
+ }
+}
\ 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..43bb151b7
--- /dev/null
+++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorAddModal/indicator-add-modal.component.html
@@ -0,0 +1,1758 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ = 1"
+ [class.error]="currentStep === 1 && !isCurrentStepValid()"
+ (click)="goToStep(1)"
+ style="width: 14.28%;"
+ title="Klicken Sie hier, um zu Schritt 1 zu navigieren">Metadaten des Indikators
+ = 2"
+ [class.error]="currentStep === 2 && !isCurrentStepValid()"
+ (click)="goToStep(2)"
+ style="width: 14.28%;"
+ title="Klicken Sie hier, um zu Schritt 2 zu navigieren">Allgemeine Metadaten
+ = 3"
+ [class.error]="currentStep === 3 && !isCurrentStepValid()"
+ (click)="goToStep(3)"
+ style="width: 14.28%;"
+ title="Klicken Sie hier, um zu Schritt 3 zu navigieren">Themenhierarchie
+ = 4"
+ (click)="goToStep(4)"
+ style="width: 14.28%;"
+ title="Klicken Sie hier, um zu Schritt 4 zu navigieren">Referenzen zu Indikatoren/Georessourcen
+ = 5"
+ [class.error]="currentStep === 5 && !isCurrentStepValid()"
+ (click)="goToStep(5)"
+ style="width: 14.28%;"
+ title="Klicken Sie hier, um zu Schritt 5 zu navigieren">Klassifizierungsoptionen
+ = 6"
+ (click)="goToStep(6)"
+ style="width: 14.28%;"
+ title="Klicken Sie hier, um zu Schritt 6 zu navigieren">regionale Vergleichswerte
+ = 7"
+ (click)="goToStep(7)"
+ style="width: 14.28%;"
+ title="Klicken Sie hier, um zu Schritt 7 zu navigieren">Zugriffsschutz und Eigentümerschaft
+
+
+
+
+
+
Metadaten des Indikators
+
Angaben über Metadaten des Indikators
+
* = Pflichtfeld
+
+
+
+
+
+
+
+
+
+
+
+
+
Allgemeine Metadaten
+
Angaben über allgemeine Metadaten in KomMonitor
+
* = Pflichtfeld
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Themenhierarchie
+
Angaben über die Themenhierarchie in KomMonitor
+
* = Pflichtfeld
+
+
+
+
+
+
+
+
+
+
+ Zusätzliches Thema
+
+ -- Zusätzliches Thema wählen --
+
+ {{topic.topicName}}
+
+
+
+
+
+
+ Zusätzliches Unterthema
+
+ -- Zusätzliches Unterthema wählen --
+
+ {{subTopic.subTopicName}}
+
+
+
+
+
+
+
+
+ Zuordnung hinzufügen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Referenzen zu Indikatoren/Georessourcen (optional)
+
Angaben über Referenzen des Indikators auf andere Indikatoren oder Georessourcen
+
+
Hier werden Angaben über assoziierte Indikatoren und Georessourcen getätigt. Sie eignen sich dazu, im System Querverbindungen
+ zwischen Indikatoren und Georessourcen darzustellen und dem Nutzer so die Möglichkeit zu geben, einen Themenkomplex,
+ der durch mehrere Datensätze abgebildet wird, besser zu verstehen.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
0">
+
+
Übersicht der definierten Indikatoren-Referenzen
+
+
+
+ Editierfunktionen
+ ID des Indikators
+ Name des Indikators
+ Kürzel/Kennziffer
+ Beschreibung der Verknüpfung
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{reference.indicatorMetadata.indicatorId}}
+ {{reference.indicatorMetadata.indicatorName || reference.indicatorMetadata.datasetName}}
+ {{reference.indicatorMetadata.abbreviation}}
+ {{reference.referenceDescription}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
0">
+
+
Übersicht der definierten Georessourcen-Referenzen
+
+
+
+ Editierfunktionen
+ ID der Georessource
+ Name der Georessource
+ Beschreibung der Verknüpfung
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{reference.georesourceMetadata.georesourceId}}
+ {{reference.georesourceMetadata.georesourceName || reference.georesourceMetadata.datasetName}}
+ {{reference.referenceDescription}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Klassifizierungsoptionen
+
+
+
+
Angaben über die Standard-Klassifizierung des Indikators
+
* = Pflichtfeld
+
+
+
+ Debug: Keine Farbpaletten geladen. Anzahl: {{colorbrewerPalettes.length}}
+
+
+
+
+
Sollte der Indikator negative und positive Werte enthalten, so wird in KomMonitor standardmäßig folgende zweifarbige Klassifizierung verwendet
+
+
+
+
+
+
+
Da ein dynamischer Indikatorentyp ('{{indicatorType?.displayName}}') konfiguriert wurde, ist hier keine Angabe erforderlich. Es wird folgende zweifarbige Standard-Klassifizierung verwendet
+
+
+
+
+
+
+
+
+
+
+
0) && classificationMethod === 'regional_default'">
+
+
+
+
0) && classificationMethod === 'regional_default'">
+
Vollständig ausgefüllte Tabs werden grün hervorgehoben.
+
Fehlerhaft ausgefüllte Tabs werden rot hervorgehoben. Die Klassengrenzen müssen von niedrig nach hoch sortiert sein.
+
+
+
+
+
+
+
+
+
Regionale Vergleichswerte
+
Angaben über regionale Vergleichswerte und Benchmarking
+
* = Pflichtfeld
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Typ
+
+ -- Typ wählen --
+ Zielwert
+ Durchschnittswert
+ Medianwert
+ Best Practice
+ Schwellenwert
+ Benutzerdefiniert
+
+
+
+
+
+
+
+
+
+ Hinzufügen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Zugriffsschutz und Eigentümerschaft
+
Angaben über Zugriffsrechte und Eigentümerschaft des Indikators
+
* = Pflichtfeld
+
+
+
+
+
+
+
+
+
+
+
Öffentliche Lesefreigabe*
+
+
+
+
+
+
Öffentlich freigegebene Datensätze können ohne Login abgerufen werden.
+
+
+
+
+
+ Als Eigentümer-Organisation des Datensatzes können Sie Lese- und Editier-Rechte an die eigene und weitere Organisationseinheiten vergeben.
+
+
+
+ Lese- und Editierrechte wurden für die von Ihnen selektierte Eigentümer-Organisationseinheit markiert.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
×
+
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:
+
+
+
+
+
+
+
+
\ 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..f110aded4
--- /dev/null
+++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorAddModal/indicator-add-modal.component.ts
@@ -0,0 +1,2406 @@
+import { Component, OnInit, OnDestroy, 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 { KommonitorIndicatorDataExchangeService } from 'services/adminIndicatorUnit/kommonitor-data-exchange.service';
+import { KommonitorIndicatorDataGridHelperService } from 'services/adminIndicatorUnit/kommonitor-data-grid-helper.service';
+import { KommonitorIndicatorImporterHelperService } from 'services/adminIndicatorUnit/kommonitor-importer-helper.service';
+import { KommonitorIndicatorCacheHelperService } from 'services/adminIndicatorUnit/kommonitor-cache-helper.service';
+import { Subscription } from 'rxjs';
+import { AgGridAngular } from 'ag-grid-angular';
+
+@Component({
+ selector: 'indicator-add-modal-new',
+ templateUrl: './indicator-add-modal.component.html',
+ styleUrls: ['./indicator-add-modal.component.css']
+})
+export class IndicatorAddModalComponent implements OnInit, OnDestroy {
+ @ViewChild('metadataImportFile', { static: false }) metadataImportFile!: ElementRef;
+ @ViewChild('mappingConfigImportFile', { static: false }) mappingConfigImportFile!: ElementRef;
+ @ViewChild('roleManagementGrid', { static: false }) roleManagementGrid!: AgGridAngular;
+
+ // Event subscriptions for role management (like AngularJS component)
+ private roleUpdateSubscription?: Subscription;
+ private metadataLoadingSubscription?: Subscription;
+
+ // Grid API references for role management
+ roleManagementGridApi: any = null;
+ roleManagementColumnApi: any = null;
+
+ // 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[] = [];
+
+ // Additional classification variables (missing from original)
+ classificationMethodOptions: any[] = [];
+ defaultClassificationMethod = 'jenks';
+ enableManualClassification = false;
+ enableRegionalClassification = false;
+
+ // Role management
+ roleManagementTableOptions: any = null;
+ ownerOrganization: any = null;
+ ownerOrgFilter = '';
+ isPublic = false;
+ resourcesCreatorRights: any[] = [];
+
+ // Initialize resources creator rights (for non-admin users)
+ private initializeResourcesCreatorRights() {
+ // For now, use the same as access control, but this should be filtered based on user permissions
+ this.resourcesCreatorRights = this.accessControl || [];
+ }
+
+ // 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;
+ decreaseBreaksLength = 0;
+ increaseBreaksLength = 0;
+
+ // Additional classification validation and color assignment variables
+ classificationValidationErrors: string[] = [];
+ enableColorValidation = false;
+ dynamicColorAssignmentEnabled = false;
+ negativeValueColorScheme = 'Reds';
+ positiveValueColorScheme = 'Blues';
+ zeroValueColor = '#bababa';
+ classificationBreakValidationEnabled = true;
+
+ // 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,
+ public kommonitorDataExchangeService: KommonitorIndicatorDataExchangeService,
+ public kommonitorImporterHelperService: KommonitorIndicatorImporterHelperService,
+ public kommonitorDataGridHelperService: KommonitorIndicatorDataGridHelperService,
+ private kommonitorCacheHelperService: KommonitorIndicatorCacheHelperService,
+ private http: HttpClient,
+ private broadcastService: BroadcastService
+ ) {
+ }
+
+ async ngOnInit() {
+ await this.loadInitialData();
+ this.initializeMultiStepForm();
+
+ // Ensure color palettes are loaded
+ if (this.colorbrewerPalettes.length === 0) {
+ this.loadColorBrewerSchemes();
+ }
+
+ // Initialize role management grid after a short delay to ensure DOM is ready
+ setTimeout(() => {
+ this.refreshRoles(); // Call refreshRoles() like AngularJS component
+ }, 100);
+
+ // Set up event listeners for role management (like AngularJS component)
+ this.setupEventListeners();
+ }
+
+ private async loadInitialData() {
+ this.loadingData = true;
+
+ // Ensure indicators and georesources data is loaded
+ if (!this.kommonitorDataExchangeService.availableIndicators || this.kommonitorDataExchangeService.availableIndicators.length === 0) {
+ await this.kommonitorDataExchangeService.fetchIndicatorsMetadata(this.kommonitorDataExchangeService.currentKeycloakLoginRoles);
+ }
+
+ if (!this.kommonitorDataExchangeService.availableGeoresources || this.kommonitorDataExchangeService.availableGeoresources.length === 0) {
+ await this.kommonitorDataExchangeService.fetchGeoresourcesMetadata(this.kommonitorDataExchangeService.currentKeycloakLoginRoles);
+ }
+
+ // Ensure access control data is loaded
+ if (!this.kommonitorDataExchangeService.accessControl || this.kommonitorDataExchangeService.accessControl.length === 0) {
+ try {
+ await this.kommonitorDataExchangeService.fetchAccessControlMetadata();
+ } catch (error) {
+ this.createTestAccessControlData();
+ }
+ }
+
+ // Load available spatial units
+ this.availableSpatialUnits = this.kommonitorDataExchangeService.availableSpatialUnits;
+ this.indicatorLowestSpatialUnitMetadataObjectForComputation = this.availableSpatialUnits.length > 0 ? this.availableSpatialUnits[0] : null;
+
+ // Load update interval options
+ this.updateIntervalOptions = this.kommonitorDataExchangeService.updateIntervalOptions;
+
+ // Load indicator type options
+ this.indicatorTypeOptions = this.kommonitorDataExchangeService.indicatorTypeOptions;
+ this.indicatorType = this.indicatorTypeOptions.length > 0 ? this.indicatorTypeOptions[0] : null;
+
+ // Load available indicators
+ this.availableIndicators = this.kommonitorDataExchangeService.availableIndicators;
+
+ // Load available georesources
+ this.availableGeoresources = this.kommonitorDataExchangeService.availableGeoresources;
+
+ // Load available topics
+ this.availableTopics = this.kommonitorDataExchangeService.availableTopics;
+
+ // Load access control
+ 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 || [];
+
+ // Initialize resources creator rights
+ this.initializeResourcesCreatorRights();
+
+ this.loadingData = false;
+ }
+
+ private initializeMultiStepForm() {
+ // Initialize multi-step form based on security settings
+ if (this.kommonitorDataExchangeService.enableKeycloakSecurity) {
+ this.totalSteps = 7; // Include role management step
+ } else {
+ this.totalSteps = 6;
+ }
+
+ // Initialize role management if available
+ this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid(
+ 'indicatorAddRoleManagementTable',
+ this.roleManagementTableOptions,
+ this.kommonitorDataExchangeService.accessControl,
+ this.kommonitorDataExchangeService.getCurrentKomMonitorLoginRoleIds()
+ );
+
+
+
+ // 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 || {};
+
+ // Fallback to default color schemes if colorbrewer is not available
+ if (!this.colorbrewerSchemes || Object.keys(this.colorbrewerSchemes).length === 0) {
+ console.warn('Colorbrewer library not found, using default color schemes');
+ this.colorbrewerSchemes = {
+ 'Blues': {
+ '3': ['#deebf7', '#9ecae1', '#3182bd'],
+ '4': ['#deebf7', '#9ecae1', '#3182bd', '#08519c'],
+ '5': ['#deebf7', '#9ecae1', '#3182bd', '#08519c', '#08306b'],
+ '6': ['#f7fbff', '#deebf7', '#9ecae1', '#3182bd', '#08519c', '#08306b'],
+ '7': ['#f7fbff', '#deebf7', '#c6dbef', '#9ecae1', '#3182bd', '#08519c', '#08306b'],
+ '8': ['#f7fbff', '#deebf7', '#c6dbef', '#9ecae1', '#6baed6', '#3182bd', '#08519c', '#08306b']
+ },
+ 'Reds': {
+ '3': ['#fee5d9', '#fcae91', '#de2d26'],
+ '4': ['#fee5d9', '#fcae91', '#de2d26', '#a50f15'],
+ '5': ['#fee5d9', '#fcae91', '#de2d26', '#a50f15', '#67000d'],
+ '6': ['#fff5f0', '#fee5d9', '#fcae91', '#de2d26', '#a50f15', '#67000d'],
+ '7': ['#fff5f0', '#fee5d9', '#fcbba1', '#fcae91', '#de2d26', '#a50f15', '#67000d'],
+ '8': ['#fff5f0', '#fee5d9', '#fcbba1', '#fcae91', '#fb6a4a', '#de2d26', '#a50f15', '#67000d']
+ },
+ 'Greens': {
+ '3': ['#e5f5e0', '#a1d99b', '#31a354'],
+ '4': ['#e5f5e0', '#a1d99b', '#31a354', '#006d2c'],
+ '5': ['#e5f5e0', '#a1d99b', '#31a354', '#006d2c', '#00441b'],
+ '6': ['#f7fcf5', '#e5f5e0', '#a1d99b', '#31a354', '#006d2c', '#00441b'],
+ '7': ['#f7fcf5', '#e5f5e0', '#c7e9c0', '#a1d99b', '#31a354', '#006d2c', '#00441b'],
+ '8': ['#f7fcf5', '#e5f5e0', '#c7e9c0', '#a1d99b', '#74c476', '#31a354', '#006d2c', '#00441b']
+ },
+ 'Oranges': {
+ '3': ['#fee6ce', '#fdd0a2', '#e6550d'],
+ '4': ['#fee6ce', '#fdd0a2', '#e6550d', '#a63603'],
+ '5': ['#fee6ce', '#fdd0a2', '#e6550d', '#a63603', '#7f2704'],
+ '6': ['#fff5eb', '#fee6ce', '#fdd0a2', '#e6550d', '#a63603', '#7f2704'],
+ '7': ['#fff5eb', '#fee6ce', '#fed98e', '#fdd0a2', '#e6550d', '#a63603', '#7f2704'],
+ '8': ['#fff5eb', '#fee6ce', '#fed98e', '#fdd0a2', '#fdbe85', '#e6550d', '#a63603', '#7f2704']
+ }
+ };
+ }
+
+ if (customColorSchemes) {
+ this.colorbrewerSchemes = Object.assign(customColorSchemes, this.colorbrewerSchemes);
+ }
+
+ // Load environment configuration for classification
+ this.loadEnvironmentConfiguration();
+
+ this.instantiateColorBrewerPalettes();
+ }
+
+ // Helper method to get color palette colors safely
+ getColorPaletteColors(paletteEntry: any, numColors: number): string[] {
+ if (!paletteEntry || !paletteEntry.paletteArrayObject) {
+ return ['#cccccc', '#cccccc', '#cccccc', '#cccccc', '#cccccc'];
+ }
+
+ const colors = paletteEntry.paletteArrayObject[numColors.toString()];
+ if (!colors || !Array.isArray(colors)) {
+ return ['#cccccc', '#cccccc', '#cccccc', '#cccccc', '#cccccc'];
+ }
+
+ return colors;
+ }
+
+ // Helper method to get color scheme colors safely
+ getColorSchemeColors(schemeName: string, numColors: number): string[] {
+ if (!this.colorbrewerSchemes || !this.colorbrewerSchemes[schemeName]) {
+ return ['#cccccc', '#cccccc', '#cccccc', '#cccccc', '#cccccc'];
+ }
+
+ const colors = this.colorbrewerSchemes[schemeName][numColors.toString()];
+ if (!colors || !Array.isArray(colors)) {
+ return ['#cccccc', '#cccccc', '#cccccc', '#cccccc', '#cccccc'];
+ }
+
+ return colors;
+ }
+
+ // Helper method to get dynamic color for classification legend
+ getDynamicColor(schemeName: string, breakLength: number, index: number, type: 'increase' | 'decrease'): string {
+ if (!this.colorbrewerSchemes || !this.colorbrewerSchemes[schemeName]) {
+ return '#cccccc';
+ }
+
+ const colors = this.colorbrewerSchemes[schemeName][(breakLength + 1).toString()];
+ if (!colors || !Array.isArray(colors)) {
+ return '#cccccc';
+ }
+
+ let colorIndex: number;
+ if (type === 'decrease') {
+ colorIndex = Math.max(0, breakLength - index - 1);
+ } else {
+ colorIndex = Math.max(0, breakLength - index - 1);
+ }
+
+ return colors[colorIndex] || '#cccccc';
+ }
+
+ // Method to manually reload color palettes
+ reloadColorPalettes() {
+ this.loadColorBrewerSchemes();
+ }
+
+ // Method to manually reload access control data
+ async reloadAccessControlData() {
+ try {
+ // Try to fetch from API first
+ await this.kommonitorDataExchangeService.fetchAccessControlMetadata();
+
+ // Reload access control from service
+ if (this.kommonitorDataExchangeService.accessControl) {
+ this.accessControl = this.kommonitorDataExchangeService.accessControl;
+ this.filteredOrganizations = this.accessControl || [];
+ this.filteredRoles = this.accessControl || [];
+ } else {
+ throw new Error('No access control data returned from API');
+ }
+ } catch (error) {
+ this.createTestAccessControlData();
+ }
+ }
+
+ // Check if user has admin permissions (like AngularJS component)
+ checkAdminPermission(): boolean {
+ return this.kommonitorDataExchangeService.checkAdminPermission();
+ }
+
+ // Handle role management grid ready event
+ onRoleManagementGridReady(params: any) {
+ // Store API references
+ this.roleManagementGridApi = params.api;
+ this.roleManagementColumnApi = params.columnApi;
+
+ // The grid is now ready and can be accessed via params.api
+ if (params.api) {
+ // Auto-size columns
+ params.api.sizeColumnsToFit();
+ }
+ }
+
+ // Handle role management first data rendered event
+ onRoleManagementFirstDataRendered(params: any) {
+ // Role management first data rendered
+ }
+
+ // Handle role management column resized event
+ onRoleManagementColumnResized(params: any) {
+ // Role management column resized
+ }
+
+ // Handle role management model updated event
+ onRoleManagementModelUpdated() {
+ // Role management model updated
+ }
+
+ // Handle role management viewport changed event
+ onRoleManagementViewportChanged() {
+ // Role management viewport changed
+ }
+
+
+
+ // Refresh role management table
+ refreshRoleManagementTable() {
+ // Rebuild role management grid with current data
+ this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid(
+ 'indicatorAddRoleManagementTable',
+ this.roleManagementTableOptions,
+ this.kommonitorDataExchangeService.accessControl,
+ this.kommonitorDataExchangeService.getCurrentKomMonitorLoginRoleIds()
+ );
+
+ // Refresh the grid if API is available
+ if (this.roleManagementGridApi) {
+ this.roleManagementGridApi.refreshCells();
+ this.roleManagementGridApi.redrawRows();
+ }
+ }
+
+ // Set up event listeners for role management (like AngularJS component)
+ setupEventListeners() {
+ // Listen for role updates (like AngularJS "availableRolesUpdate" event)
+ // Note: We'll use a different approach since BroadcastService might not have the same API
+ // In a real implementation, you would need to check the BroadcastService API
+
+ // For now, we'll trigger refreshRoles() manually when needed
+ // This matches the AngularJS pattern where refreshRoles() is called when events are triggered
+ }
+
+ // Refresh roles (like other Angular components)
+ refreshRoles(orgUnitId?: string) {
+ // Check if access control data is available
+ if (!this.kommonitorDataExchangeService.accessControl || this.kommonitorDataExchangeService.accessControl.length === 0) {
+ this.createTestAccessControlData();
+ }
+
+ // Get permission IDs for the selected organization (like other Angular components)
+ let permissionIds: string[] = [];
+ if (orgUnitId) {
+ const accessControlItem = this.kommonitorDataExchangeService.getAccessControlById(orgUnitId);
+ if (accessControlItem && accessControlItem.permissions) {
+ permissionIds = accessControlItem.permissions
+ .filter((permission: any) => permission.permissionLevel === 'viewer' || permission.permissionLevel === 'editor')
+ .map((permission: any) => permission.permissionId);
+ }
+
+ // Set datasetOwner flag for the selected organization (like other Angular components)
+ this.kommonitorDataExchangeService.accessControl.forEach((item: any) => {
+ if (item.organizationalUnitId === orgUnitId) {
+ item.datasetOwner = true;
+ } else {
+ item.datasetOwner = false;
+ }
+ });
+ } else {
+ // Use current user roles if no organization is selected
+ permissionIds = this.kommonitorDataExchangeService.getCurrentKomMonitorLoginRoleIds();
+ }
+
+ // Build role management grid with filtered data
+ this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid(
+ 'indicatorAddRoleManagementTable',
+ this.roleManagementTableOptions,
+ this.kommonitorDataExchangeService.accessControl,
+ permissionIds
+ );
+
+ // Force change detection by updating the options
+ setTimeout(() => {
+ if (this.roleManagementGrid && this.roleManagementGrid.api) {
+ // Update the grid data directly using the API
+ this.roleManagementGrid.api.setRowData(this.roleManagementTableOptions.rowData);
+
+ // Refresh the grid to ensure it updates
+ this.roleManagementGrid.api.refreshCells();
+ this.roleManagementGrid.api.redrawRows();
+ }
+ }, 100);
+ }
+
+
+
+ // Create test access control data for development/testing
+ private createTestAccessControlData() {
+ this.accessControl = [
+ {
+ organizationalUnitId: 'org1',
+ name: 'Test Organisation 1',
+ organizationalUnitName: 'Test Organisation 1',
+ organizationDescription: 'Erste Testorganisation für Entwicklung',
+ viewerPermissionId: 'viewer_org1',
+ editorPermissionId: 'editor_org1',
+ creatorPermissionId: 'creator_org1',
+ permissions: [
+ {
+ permissionId: 'viewer_org1',
+ permissionLevel: 'viewer',
+ roleName: 'Viewer',
+ roleDescription: 'Nur Leserechte auf Daten'
+ },
+ {
+ permissionId: 'editor_org1',
+ permissionLevel: 'editor',
+ roleName: 'Editor',
+ roleDescription: 'Bearbeitung von Indikatoren und Daten'
+ },
+ {
+ permissionId: 'creator_org1',
+ permissionLevel: 'creator',
+ roleName: 'Creator',
+ roleDescription: 'Erstellen von neuen Datensätzen'
+ }
+ ]
+ },
+ {
+ organizationalUnitId: 'org2',
+ name: 'Test Organisation 2',
+ organizationalUnitName: 'Test Organisation 2',
+ organizationDescription: 'Zweite Testorganisation für Entwicklung',
+ viewerPermissionId: 'viewer_org2',
+ editorPermissionId: 'editor_org2',
+ creatorPermissionId: 'creator_org2',
+ permissions: [
+ {
+ permissionId: 'viewer_org2',
+ permissionLevel: 'viewer',
+ roleName: 'Viewer',
+ roleDescription: 'Nur Leserechte auf Daten'
+ },
+ {
+ permissionId: 'editor_org2',
+ permissionLevel: 'editor',
+ roleName: 'Editor',
+ roleDescription: 'Bearbeitung von Indikatoren und Daten'
+ },
+ {
+ permissionId: 'creator_org2',
+ permissionLevel: 'creator',
+ roleName: 'Creator',
+ roleDescription: 'Erstellen von neuen Datensätzen'
+ }
+ ]
+ },
+ {
+ organizationalUnitId: 'org3',
+ name: 'Test Organisation 3',
+ organizationalUnitName: 'Test Organisation 3',
+ organizationDescription: 'Dritte Testorganisation für Entwicklung',
+ viewerPermissionId: 'viewer_org3',
+ editorPermissionId: 'editor_org3',
+ creatorPermissionId: 'creator_org3',
+ permissions: [
+ {
+ permissionId: 'viewer_org3',
+ permissionLevel: 'viewer',
+ roleName: 'Viewer',
+ roleDescription: 'Nur Leserechte auf Daten'
+ },
+ {
+ permissionId: 'editor_org3',
+ permissionLevel: 'editor',
+ roleName: 'Editor',
+ roleDescription: 'Bearbeitung von Indikatoren und Daten'
+ },
+ {
+ permissionId: 'creator_org3',
+ permissionLevel: 'creator',
+ roleName: 'Creator',
+ roleDescription: 'Erstellen von neuen Datensätzen'
+ }
+ ]
+ }
+ ];
+
+ // Also update the service's access control data
+ this.kommonitorDataExchangeService.accessControl = this.accessControl;
+
+ this.filteredOrganizations = this.accessControl;
+ this.filteredRoles = this.accessControl;
+ }
+
+ private loadEnvironmentConfiguration() {
+ // Load default classification method from environment
+ this.defaultClassificationMethod = (window as any).__env?.defaultClassifyMethod || 'jenks';
+ this.classificationMethod = this.defaultClassificationMethod;
+
+ // Load color scheme names from environment
+ this.colorbreweSchemeName_dynamicIncrease = (window as any).__env?.defaultColorBrewerPaletteForBalanceIncreasingValues || 'Blues';
+ this.colorbreweSchemeName_dynamicDecrease = (window as any).__env?.defaultColorBrewerPaletteForBalanceDecreasingValues || 'Reds';
+
+ // Load classification method options
+ this.classificationMethodOptions = [
+ { id: 'jenks', name: 'Jenks Natural Breaks', description: 'Automatische Klassifizierung nach natürlichen Brüchen' },
+ { id: 'equal', name: 'Gleiche Intervalle', description: 'Gleichmäßige Aufteilung des Wertebereichs' },
+ { id: 'manual', name: 'Manuelle Klassifizierung', description: 'Benutzerdefinierte Klassengrenzen' },
+ { id: 'regional_default', name: 'Regionale Standard-Klassifizierung', description: 'Regionsspezifische Klassengrenzen' }
+ ];
+
+ // Check if manual classification is disabled
+ if ((window as any).__env?.disableManualClassification) {
+ this.classificationMethodOptions = this.classificationMethodOptions.filter(option => option.id !== 'manual');
+ }
+ }
+
+ 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' or first available
+ this.selectedColorBrewerPaletteEntry = this.colorbrewerPalettes.find(p => p.paletteName === 'Blues') ||
+ this.colorbrewerPalettes[0];
+ }
+
+ checkDatasetName() {
+ this.datasetNameInvalid = false;
+
+ if (this.datasetName && this.indicatorType && 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) {
+ 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();
+
+ 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 (error.data) {
+ this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error.data);
+ } else {
+ this.errorMessagePart = this.kommonitorDataExchangeService.syntaxHighlightJSON(error);
+ }
+
+ 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++;
+ this.updateProgressBar();
+ }
+ }
+
+ previousStep() {
+ if (this.currentStep > 1) {
+ this.currentStep--;
+ this.updateProgressBar();
+ }
+ }
+
+ 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) {
+
+ this.currentStep = step;
+ this.updateProgressBar();
+
+ // Initialize filtered lists when navigating to Step 4
+ if (step === 4) {
+ this.filterIndicators();
+ this.filterGeoresources();
+ // Expand the collapsible boxes by default for better UX
+ this.isIndicatorReferencesCollapsed = false;
+ this.isGeoresourceReferencesCollapsed = false;
+ }
+
+ // Initialize access control data when navigating to Step 7
+ if (step === 7) {
+ // Ensure access control data is loaded
+ if (this.filteredOrganizations.length === 0 || this.filteredRoles.length === 0) {
+
+ this.reloadAccessControlData();
+ }
+
+ // Initialize role management grid with delay to ensure DOM is ready (like AngularJS component)
+ setTimeout(() => {
+ this.refreshRoles(); // Call refreshRoles() like AngularJS component
+ }, 200);
+ }
+
+ // Show validation feedback if navigating to a step that requires validation
+ if (step > 1 && !this.isStepValid(step)) {
+
+ // You can add visual feedback here if needed
+ }
+ }
+ }
+
+ // 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.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.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.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.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
+ });
+ }
+ });
+ }
+
+ // Enhanced classification mapping parsing
+ if (this.metadataImportSettings.defaultClassificationMapping) {
+ const mapping = this.metadataImportSettings.defaultClassificationMapping;
+
+ // Parse basic classification settings
+ this.numClassesPerSpatialUnit = mapping.numClasses || 5;
+ this.classificationMethod = mapping.classificationMethod || 'jenks';
+
+ // Parse color brewer palette
+ if (mapping.colorBrewerSchemeName) {
+ this.selectedColorBrewerPaletteEntry = this.colorbrewerPalettes.find(palette =>
+ palette.paletteName === mapping.colorBrewerSchemeName
+ );
+ }
+
+ // Parse dynamic color assignment settings
+ if (mapping.dynamicColorAssignment) {
+ this.dynamicColorAssignmentEnabled = mapping.dynamicColorAssignment.enabled || false;
+ this.negativeValueColorScheme = mapping.dynamicColorAssignment.negativeColorScheme || 'Reds';
+ this.positiveValueColorScheme = mapping.dynamicColorAssignment.positiveColorScheme || 'Blues';
+ this.zeroValueColor = mapping.dynamicColorAssignment.zeroColor || '#bababa';
+ }
+
+ // Parse spatial unit classification with enhanced validation
+ 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 color assignment if available
+ if (item.colorAssignment) {
+ this.spatialUnitClassification[index].colorAssignment = item.colorAssignment;
+ }
+ }
+ });
+
+ // Update color assignment for all spatial units
+ this.updateColorAssignmentForAllSpatialUnits();
+ }
+
+ // Parse validation settings
+ if (mapping.validation) {
+ this.classificationBreakValidationEnabled = mapping.validation.enabled !== false;
+ this.enableColorValidation = mapping.validation.colorValidation || false;
+ }
+ }
+
+ // Parse role permissions
+ if (this.kommonitorDataExchangeService.accessControl && this.metadataImportSettings.allowedRoles) {
+ this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid(
+ 'indicatorAddRoleManagementTable',
+ this.roleManagementTableOptions,
+ this.kommonitorDataExchangeService.accessControl,
+ this.metadataImportSettings.allowedRoles
+ );
+ } else if (this.kommonitorDataExchangeService.accessControl) {
+ // Initialize with current user roles if no imported roles
+ this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid(
+ 'indicatorAddRoleManagementTable',
+ this.roleManagementTableOptions,
+ this.kommonitorDataExchangeService.accessControl,
+ this.kommonitorDataExchangeService.getCurrentKomMonitorLoginRoleIds()
+ );
+ }
+
+ // Classification settings imported
+ }
+
+ 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;
+
+ // Enhanced classification mapping export
+ metadataExport.defaultClassificationMapping = {
+ colorBrewerSchemeName: this.selectedColorBrewerPaletteEntry?.paletteName,
+ numClasses: this.numClassesPerSpatialUnit,
+ classificationMethod: this.classificationMethod,
+ dynamicColorAssignment: {
+ enabled: this.dynamicColorAssignmentEnabled,
+ negativeColorScheme: this.negativeValueColorScheme,
+ positiveColorScheme: this.positiveValueColorScheme,
+ zeroColor: this.zeroValueColor
+ },
+ validation: {
+ enabled: this.classificationBreakValidationEnabled,
+ colorValidation: this.enableColorValidation
+ },
+ items: this.spatialUnitClassification.map(classification => ({
+ spatialUnit: classification.spatialUnitId,
+ breaks: classification.breaks.filter(breakVal => breakVal !== null),
+ colorAssignment: classification.colorAssignment || null
+ }))
+ };
+
+ // Add role permissions
+ 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);
+ }
+ }
+ }
+
+ 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",
+ "dynamicColorAssignment": {
+ "enabled": false,
+ "negativeColorScheme": "Reds",
+ "positiveColorScheme": "Blues",
+ "zeroColor": "#bababa"
+ },
+ "validation": {
+ "enabled": true,
+ "colorValidation": false
+ },
+ "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 enhanced classification variables
+ this.classificationValidationErrors = [];
+ this.enableColorValidation = false;
+ this.dynamicColorAssignmentEnabled = false;
+ this.negativeValueColorScheme = 'Reds';
+ this.positiveValueColorScheme = 'Blues';
+ this.zeroValueColor = '#bababa';
+ this.classificationBreakValidationEnabled = true;
+
+ // 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 || [];
+
+ // Reset role management
+ this.roleManagementTableOptions = this.kommonitorDataGridHelperService.buildRoleManagementGrid(
+ 'indicatorAddRoleManagementTable',
+ this.roleManagementTableOptions,
+ this.kommonitorDataExchangeService.accessControl,
+ this.kommonitorDataExchangeService.getCurrentKomMonitorLoginRoleIds()
+ );
+
+ // 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;
+
+
+ // Refresh roles based on the selected owner organization
+ this.refreshRoles(this.ownerOrganization?.organizationalUnitId);
+ }
+
+ 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.indicatorName && indicator.indicatorName.toLowerCase().includes(filter)) ||
+ (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.georesourceName && georesource.georesourceName.toLowerCase().includes(filter)) ||
+ (georesource.datasetName && georesource.datasetName.toLowerCase().includes(filter))
+ );
+ }
+ }
+
+ // Step 4: Collapsible Box Properties
+ isIndicatorReferencesCollapsed = true;
+ isGeoresourceReferencesCollapsed = true;
+
+ // Step 4: Collapsible Box Methods
+ toggleIndicatorReferences() {
+ this.isIndicatorReferencesCollapsed = !this.isIndicatorReferencesCollapsed;
+ }
+
+ toggleGeoresourceReferences() {
+ this.isGeoresourceReferencesCollapsed = !this.isGeoresourceReferencesCollapsed;
+ }
+
+ // Step 4: Selection Methods
+ onIndicatorSelected() {
+ // Selection handled by ngModel binding
+ }
+
+ onGeoresourceSelected() {
+ // Selection handled by ngModel binding
+ }
+
+ // 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';
+ }
+
+ // Enhanced classification method selection
+ onClassificationMethodSelected(method: any) {
+ this.classificationMethod = method;
+
+ // Enable/disable specific features based on method
+ this.enableManualClassification = method === 'manual';
+ this.enableRegionalClassification = method === 'regional_default';
+
+ // Reset validation errors
+ this.classificationValidationErrors = [];
+ this.classBreaksInvalid = false;
+
+ // Reinitialize classification when method changes
+ this.onNumClassesChanged(this.numClassesPerSpatialUnit);
+
+ // Update dynamic color assignment based on method
+ this.updateDynamicColorAssignment();
+
+
+ }
+
+ // Update dynamic color assignment based on classification method
+ private updateDynamicColorAssignment() {
+ // Enable dynamic color assignment for certain methods
+ this.dynamicColorAssignmentEnabled = this.classificationMethod === 'regional_default' ||
+ this.classificationMethod === 'manual';
+
+ // Set color schemes based on method
+ if (this.classificationMethod === 'regional_default') {
+ this.negativeValueColorScheme = 'Reds';
+ this.positiveValueColorScheme = 'Blues';
+ } else if (this.classificationMethod === 'manual') {
+ this.negativeValueColorScheme = 'Reds';
+ this.positiveValueColorScheme = 'Blues';
+ } else {
+ // For automatic methods, use default schemes
+ this.negativeValueColorScheme = this.colorbreweSchemeName_dynamicDecrease;
+ this.positiveValueColorScheme = this.colorbreweSchemeName_dynamicIncrease;
+ }
+
+ // Update color assignment for all spatial units
+ this.updateColorAssignmentForAllSpatialUnits();
+ }
+
+ // Update color assignment for all spatial units
+ private updateColorAssignmentForAllSpatialUnits() {
+ if (!this.dynamicColorAssignmentEnabled) {
+ return;
+ }
+
+ for (let i = 0; i < this.spatialUnitClassification.length; i++) {
+ this.updateColorAssignmentForSpatialUnit(i);
+ }
+ }
+
+ // Update color assignment for a specific spatial unit
+ private updateColorAssignmentForSpatialUnit(spatialUnitIndex: number) {
+ if (!this.spatialUnitClassification[spatialUnitIndex]) {
+ return;
+ }
+
+ const classification = this.spatialUnitClassification[spatialUnitIndex];
+ const breaks = classification.breaks;
+
+ // Calculate color assignment based on break values
+ let hasNegativeValues = false;
+ let hasPositiveValues = false;
+ let hasZeroValue = false;
+
+ for (const breakValue of breaks) {
+ if (breakValue !== null && breakValue !== undefined) {
+ if (breakValue < 0) hasNegativeValues = true;
+ if (breakValue > 0) hasPositiveValues = true;
+ if (breakValue === 0) hasZeroValue = true;
+ }
+ }
+
+ // Store color assignment information
+ classification.colorAssignment = {
+ hasNegativeValues,
+ hasPositiveValues,
+ hasZeroValue,
+ negativeColorScheme: this.negativeValueColorScheme,
+ positiveColorScheme: this.positiveValueColorScheme,
+ zeroColor: this.zeroValueColor
+ };
+
+
+ }
+
+ // Get color for a specific class based on break value
+ getClassColorForBreak(breakValue: number, classIndex: number): string {
+ if (!this.dynamicColorAssignmentEnabled) {
+ // Use standard color brewer palette
+ if (this.selectedColorBrewerPaletteEntry && this.selectedColorBrewerPaletteEntry.paletteArrayObject) {
+ const colors = this.selectedColorBrewerPaletteEntry.paletteArrayObject[this.numClassesPerSpatialUnit.toString()];
+ if (colors && colors[classIndex]) {
+ return colors[classIndex];
+ }
+ }
+ return '#cccccc';
+ }
+
+ // Dynamic color assignment based on break value
+ if (breakValue < 0) {
+ // Negative values - use decreasing color scheme
+ const colors = this.colorbrewerSchemes[this.negativeValueColorScheme];
+ if (colors && colors[this.decreaseBreaksLength]) {
+ const colorIndex = Math.min(classIndex, this.decreaseBreaksLength - 1);
+ return colors[this.decreaseBreaksLength][colorIndex];
+ }
+ } else if (breakValue > 0) {
+ // Positive values - use increasing color scheme
+ const colors = this.colorbrewerSchemes[this.positiveValueColorScheme];
+ if (colors && colors[this.increaseBreaksLength]) {
+ const colorIndex = Math.min(classIndex, this.increaseBreaksLength - 1);
+ return colors[this.increaseBreaksLength][colorIndex];
+ }
+ } else if (breakValue === 0) {
+ // Zero value - use neutral color
+ return this.zeroValueColor;
+ }
+
+ return '#cccccc';
+ }
+
+ onClickColorBrewerEntry(colorPaletteEntry: any) {
+ this.selectedColorBrewerPaletteEntry = colorPaletteEntry;
+ }
+
+ onNumClassesChanged(numClasses: number) {
+ this.numClassesPerSpatialUnit = numClasses;
+
+ // Calculate break lengths for dynamic color assignment
+ this.decreaseBreaksLength = Math.floor(numClasses / 2);
+ this.increaseBreaksLength = numClasses - this.decreaseBreaksLength;
+
+ // 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 = 'tab-completed';
+ this.classBreaksInvalid = false;
+ this.classificationValidationErrors = [];
+
+ // Enhanced validation logic matching AngularJS implementation
+ if (this.classificationMethod === 'regional_default' || this.classificationMethod === 'manual') {
+ // Check if all breaks are filled
+ let allBreaksFilled = true;
+ for (const classBreak of breaks) {
+ if (classBreak === null || classBreak === undefined || classBreak === '') {
+ allBreaksFilled = false;
+ break;
+ }
+ }
+
+ if (allBreaksFilled) {
+ // Validate that breaks are in ascending order
+ for (let i = 0; i < breaks.length - 1; i++) {
+ if (breaks[i] >= breaks[i + 1]) {
+ cssClass = 'tab-error';
+ this.classBreaksInvalid = true;
+ this.classificationValidationErrors.push(
+ `Klassengrenze ${i + 1} (${breaks[i]}) muss kleiner sein als Klassengrenze ${i + 2} (${breaks[i + 1]})`
+ );
+ break;
+ }
+ }
+ } else {
+ // Check if any breaks are filled but not all
+ let hasAnyBreaks = false;
+ for (const classBreak of breaks) {
+ if (classBreak !== null && classBreak !== undefined && classBreak !== '') {
+ hasAnyBreaks = true;
+ break;
+ }
+ }
+
+ if (hasAnyBreaks) {
+ cssClass = 'tab-error';
+ this.classBreaksInvalid = true;
+ this.classificationValidationErrors.push('Alle Klassengrenzen müssen ausgefüllt werden');
+ } else {
+ cssClass = 'active';
+ }
+ }
+ } else {
+ // For automatic classification methods, check if any manual breaks are entered
+ let hasManualBreaks = false;
+ for (const classBreak of breaks) {
+ if (classBreak !== null && classBreak !== undefined && classBreak !== '') {
+ hasManualBreaks = true;
+ break;
+ }
+ }
+
+ if (hasManualBreaks) {
+ cssClass = 'tab-error';
+ this.classBreaksInvalid = true;
+ this.classificationValidationErrors.push('Manuelle Klassengrenzen sind für automatische Klassifizierungsmethoden nicht erlaubt');
+ }
+ }
+
+ this.tabClasses[tabIndex] = cssClass;
+
+ // Update decrease and increase breaks for dynamic color assignment
+ this.updateDecreaseAndIncreaseBreaks(tabIndex);
+ }
+
+ // Update decrease and increase breaks for dynamic color assignment
+ private updateDecreaseAndIncreaseBreaks(tabIndex: number) {
+ if (!this.spatialUnitClassification[tabIndex]) {
+ return;
+ }
+
+ const breaks = this.spatialUnitClassification[tabIndex].breaks;
+
+ // Count positive and negative breaks
+ this.increaseBreaksLength = breaks.filter(val => val !== null && val !== undefined && val > 0).length;
+ this.decreaseBreaksLength = breaks.filter(val => val !== null && val !== undefined && val < 0).length;
+
+ // Ensure minimum lengths for color schemes
+ if (this.increaseBreaksLength < 3) {
+ this.increaseBreaksLength = 3;
+ }
+ if (this.decreaseBreaksLength < 3) {
+ this.decreaseBreaksLength = 3;
+ }
+ // Updated break lengths
+ }
+
+ // Validate classification breaks across all spatial units
+ validateClassificationBreaks(): boolean {
+ this.classificationValidationErrors = [];
+ let isValid = true;
+
+ // Check if classification method is selected
+ if (!this.classificationMethod) {
+ this.classificationValidationErrors.push('Klassifizierungsmethode muss ausgewählt werden');
+ isValid = false;
+ }
+
+ // Check if number of classes is selected
+ if (!this.numClassesPerSpatialUnit || this.numClassesPerSpatialUnit < 3) {
+ this.classificationValidationErrors.push('Mindestens 3 Klassen müssen ausgewählt werden');
+ isValid = false;
+ }
+
+ // Validate breaks for each spatial unit
+ for (let i = 0; i < this.spatialUnitClassification.length; i++) {
+ const classification = this.spatialUnitClassification[i];
+ if (!classification) continue;
+
+ const breaks = classification.breaks;
+
+ // Check for null/undefined breaks
+ for (let j = 0; j < breaks.length; j++) {
+ if (breaks[j] === null || breaks[j] === undefined || breaks[j] === '') {
+ this.classificationValidationErrors.push(
+ `Klassengrenze ${j + 1} für Raumebene ${classification.spatialUnitLevel} ist nicht ausgefüllt`
+ );
+ isValid = false;
+ }
+ }
+
+ // Check for ascending order
+ for (let j = 0; j < breaks.length - 1; j++) {
+ if (breaks[j] >= breaks[j + 1]) {
+ this.classificationValidationErrors.push(
+ `Klassengrenzen für Raumebene ${classification.spatialUnitLevel} müssen in aufsteigender Reihenfolge sein`
+ );
+ isValid = false;
+ break;
+ }
+ }
+ }
+
+ this.classBreaksInvalid = !isValid;
+ return isValid;
+ }
+
+ // Get validation status for a specific spatial unit
+ getSpatialUnitValidationStatus(spatialUnitIndex: number): { isValid: boolean, errors: string[] } {
+ const errors: string[] = [];
+ let isValid = true;
+
+ if (!this.spatialUnitClassification[spatialUnitIndex]) {
+ return { isValid: false, errors: ['Raumeinheit nicht gefunden'] };
+ }
+
+ const classification = this.spatialUnitClassification[spatialUnitIndex];
+ const breaks = classification.breaks;
+
+ // Check for null/undefined breaks
+ for (let i = 0; i < breaks.length; i++) {
+ if (breaks[i] === null || breaks[i] === undefined || breaks[i] === '') {
+ errors.push(`Klassengrenze ${i + 1} ist nicht ausgefüllt`);
+ isValid = false;
+ }
+ }
+
+ // Check for ascending order
+ for (let i = 0; i < breaks.length - 1; i++) {
+ if (breaks[i] >= breaks[i + 1]) {
+ errors.push(`Klassengrenze ${i + 1} (${breaks[i]}) muss kleiner sein als Klassengrenze ${i + 2} (${breaks[i + 1]})`);
+ isValid = false;
+ }
+ }
+
+ return { isValid, errors };
+ }
+
+ // 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)
+ );
+ }
+ }
+
+ // Get filtered organizations based on admin permissions (like AngularJS component)
+ getFilteredOrganizations(): any[] {
+ if (this.checkAdminPermission()) {
+ return this.filteredOrganizations;
+ } else {
+ // For non-admin users, show only their creator rights
+ return this.resourcesCreatorRights || [];
+ }
+ }
+
+ 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);
+ }
+
+ // Step validation methods for progress bar
+ isStepValid(step: number): boolean {
+ // Validation for specific steps
+ switch (step) {
+ case 1:
+ return !!this.datasetName && !!this.indicatorType && !!this.indicatorUnit && !!this.indicatorInterpretation;
+ case 2:
+ return !!this.metadata.description && !!this.metadata.datasource && !!this.metadata.contact && !!this.metadata.updateInterval && !!this.metadata.lastUpdate;
+ case 3:
+ return !!this.indicatorTopic_mainTopic;
+ case 4:
+ // Step 4 is optional (references)
+ return true;
+ case 5:
+ // Enhanced Step 5 validation with classification breaks validation
+ if (this.indicatorType?.apiName?.includes('STATUS')) {
+ const basicValidation = !!this.selectedColorBrewerPaletteEntry && !!this.numClassesPerSpatialUnit;
+ if (this.classificationMethod === 'regional_default' || this.classificationMethod === 'manual') {
+ return basicValidation && this.validateClassificationBreaks();
+ }
+ return basicValidation;
+ }
+ return !!this.numClassesPerSpatialUnit;
+ case 6:
+ // Step 6 is informational
+ return true;
+ case 7:
+ // Step 7 validation for access control
+ return this.validateAccessControl();
+ default:
+ return true;
+ }
+ }
+
+ isCurrentStepValid(): boolean {
+ return this.isStepValid(this.currentStep);
+ }
+
+ updateProgressBar(): void {
+ // Update progress bar active states
+ const progressItems = document.querySelectorAll('#progressbar li');
+ progressItems.forEach((item, index) => {
+ if (index < this.currentStep) {
+ item.classList.add('active');
+ } else {
+ item.classList.remove('active');
+ }
+ });
+ }
+
+ isStepActive(step: number): boolean {
+ return this.currentStep === step;
+ }
+
+ isStepCompleted(step: number): boolean {
+ return this.currentStep > step;
+ }
+
+ cancel() {
+
+ this.activeModal.dismiss('cancel');
+ }
+
+ ngOnDestroy() {
+ // Clean up event subscriptions (like AngularJS component)
+ if (this.roleUpdateSubscription) {
+ this.roleUpdateSubscription.unsubscribe();
+ }
+ if (this.metadataLoadingSubscription) {
+ this.metadataLoadingSubscription.unsubscribe();
+ }
+ }
+}
\ 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..f7ea8645f
--- /dev/null
+++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorBatchUpdateModal/indicator-batch-update-modal.component.html
@@ -0,0 +1,531 @@
+
+
+
+
+
+
+
+
+
+
+
Diese Funktion ermöglicht es, die Mapping-Parameter mehrerer Indikatoren gleichzeitig zu aktualisieren.
+
* = Pflichtfeld
+
+
+
+
+
+
+
+
+
+
+ neue Zeile hinzufügen
+ ausgewählte Zeilen löschen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..eda1611b1
--- /dev/null
+++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorBatchUpdateModal/indicator-batch-update-modal.component.ts
@@ -0,0 +1,614 @@
+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';
+import { KommonitorImporterHelperService } from 'services/adminSpatialUnit/kommonitor-importer-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,
+ private kommonitorImporterHelperService: KommonitorImporterHelperService
+ ) {
+ this.keyDownHandler = this.handleKeyDown.bind(this);
+ }
+
+ ngOnInit(): void {
+ this.setupEventListeners();
+ this.initialize();
+
+ // Add keyboard event listener for Escape key
+ document.addEventListener('keydown', this.keyDownHandler);
+
+ // Initialize Bootstrap AdminLTE box widgets
+ setTimeout(() => {
+ if (typeof $ !== 'undefined' && $.fn && $.fn.boxWidget) {
+ $('.box').boxWidget();
+ }
+ }, 300);
+ }
+
+ 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();
+
+ // Initialize Bootstrap AdminLTE box widgets after modal is opened
+ setTimeout(() => {
+ if (typeof $ !== 'undefined' && $.fn && $.fn.boxWidget) {
+ $('.box').boxWidget();
+ }
+ }, 200);
+ }
+
+ private async initialize(): Promise {
+ if (this.isFirstStart) {
+ this.addNewRowToBatchList();
+ this.isFirstStart = false;
+ }
+
+ // Ensure importer helper service data is loaded
+ if (!this.kommonitorImporterHelperService.availableConverters?.length ||
+ !this.kommonitorImporterHelperService.availableDatasourceTypes?.length) {
+ try {
+ await this.kommonitorImporterHelperService.fetchResourcesFromImporter();
+ } catch (error) {
+ console.error('Error loading importer resources:', error);
+ }
+ }
+
+ // Set initial selected value if available
+ if (this.kommonitorDataExchangeService.availableIndicators &&
+ this.kommonitorDataExchangeService.availableIndicators.length > 0) {
+ this.selected.value = this.kommonitorDataExchangeService.availableIndicators[0];
+ }
+
+ // Initialize Bootstrap AdminLTE box widgets
+ setTimeout(() => {
+ if (typeof $ !== 'undefined' && $.fn && $.fn.boxWidget) {
+ $('.box').boxWidget();
+ console.log('Box widgets initialized');
+ } else {
+ console.log('jQuery or boxWidget not available');
+ }
+ }, 100);
+ }
+
+ 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 || null;
+
+ 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;
+ }
+
+ public getConverterObjectByName(name: string): any {
+ // Implementation to get converter object by name
+ if (this.kommonitorImporterHelperService.availableConverters) {
+ return this.kommonitorImporterHelperService.availableConverters.find((c: any) => c.name === name);
+ }
+ return null;
+ }
+
+ private getDatasourceTypeObjectByType(type: string): any {
+ // Implementation to get datasource type object by type
+ if (this.kommonitorImporterHelperService.availableDatasourceTypes) {
+ return this.kommonitorImporterHelperService.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[] {
+ if (!this.kommonitorImporterHelperService.availableConverters) {
+ return [];
+ }
+ // Filter converters for indicators (exclude georesource converters)
+ return this.kommonitorImporterHelperService.availableConverters.filter((converter: any) => {
+ // Remove converters that are not for indicators
+ if (converter.name.includes('Geokodierung') || converter.name.includes('Koordinate')) {
+ return false;
+ }
+ return true;
+ });
+ }
+
+ public getAvailableDatasourceTypes(): any[] {
+ return this.kommonitorImporterHelperService.availableDatasourceTypes || [];
+ }
+
+ // Default value function properties
+ public colDefaultFunctionSelectedColumn: string | null = null;
+ public colDefaultFunctionNewValue: any = undefined;
+ public colDefaultFunctionAllRowsChb: boolean = false;
+
+ public toggleAccordion(event: Event): void {
+ // Fallback method if Bootstrap AdminLTE JavaScript is not working
+ const button = event.target as HTMLElement;
+ const box = button.closest('.box');
+ const boxBody = box?.querySelector('.box-body') as HTMLElement;
+ const icon = button.querySelector('i');
+
+ if (box && boxBody && icon) {
+ const isCollapsed = box.classList.contains('collapsed-box');
+
+ if (isCollapsed) {
+ box.classList.remove('collapsed-box');
+ icon.classList.remove('fa-plus');
+ icon.classList.add('fa-minus');
+ boxBody.style.display = 'block';
+ } else {
+ box.classList.add('collapsed-box');
+ icon.classList.remove('fa-minus');
+ icon.classList.add('fa-plus');
+ boxBody.style.display = 'none';
+ }
+ }
+ }
+
+ public onClickSaveColDefaultValue(): void {
+ if (!this.colDefaultFunctionSelectedColumn || this.colDefaultFunctionNewValue === undefined) {
+ return;
+ }
+
+ // Apply the default value to all rows
+ this.batchList.forEach(row => {
+ if (!row.isSelected) return;
+
+ // Only update empty values unless colDefaultFunctionAllRowsChb is true
+ if (!this.colDefaultFunctionAllRowsChb) {
+ const currentValue = this.getNestedValue(row, this.colDefaultFunctionSelectedColumn);
+ if (currentValue !== null && currentValue !== undefined && currentValue !== '') {
+ return; // Skip if value already exists
+ }
+ }
+
+ // Set the new value
+ this.setNestedValue(row, this.colDefaultFunctionSelectedColumn, this.colDefaultFunctionNewValue);
+ });
+
+ // Reset the form
+ this.colDefaultFunctionSelectedColumn = null;
+ this.colDefaultFunctionNewValue = undefined;
+ }
+
+ private getNestedValue(obj: any, path: string | null): any {
+ if (!path) return undefined;
+ return path.split('.').reduce((current, key) => {
+ return current && current[key] !== undefined ? current[key] : undefined;
+ }, obj);
+ }
+
+ private setNestedValue(obj: any, path: string | null, value: any): void {
+ if (!path) return;
+ const keys = path.split('.');
+ const lastKey = keys.pop()!;
+ const target = keys.reduce((current, key) => {
+ if (!current[key]) {
+ current[key] = {};
+ }
+ return current[key];
+ }, obj);
+ target[lastKey] = value;
+ }
+
+ 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 @@
+
+
+
+
+
+
+
+
+
+
Indikatoren-Daten löschen
+ Hier können einzelne Zeitschnitte oder Raumebenen sowie ganze Indikatoren-Datensätze aus dem System entfernt werden.
+
+
+
+
+
+
+
+
+
+
+
+ -- Indikator wählen --
+
+ {{indicator.indicatorName}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Vollständigen Datensatz entfernen
+
Durch betätigen des Löschen-Buttons wird der gesamte Indikatoren-Datensatz aus dem System unwiderruflich entfernt (Metadaten sowie alle Indikatoren-Werte aller Raumebenen und Zeitschnitte)
+
+
ACHTUNG!
+
Dabei werden auch sämtliche Indikatoren-Referenzen und Georessourcen-Referenzen auf den betroffene Indikator dauerhaft aus dem System entfernt. Etwaige Skripte , in denen der betroffene Indikator als Berechnungsgrundlage verwendet werden, werden ebenfalls ungültig und daher aus dem System gelöscht
+
+
Betroffene Georessourcen-Referenzen
+
+
0">
+
+
+
+ referenzierte Georessource - ID
+ referenzierte Georessource - Name
+ referenzierte Georessource - Beschreibung
+
+
+
+
+ {{entry.georesourceReference.referencedGeoresourceId}}
+ {{entry.georesourceReference.referencedGeoresourceName}}
+ {{entry.georesourceReference.referencedGeoresourceDescription}}
+
+
+
+
+
+
Betroffene Indikator-Referenzen
+
+
0">
+
+
+
+ referenzierter Indikator - ID
+ referenzierter Indikator - Name
+ referenzierter Indikator - Beschreibung
+
+
+
+
+ {{entry.indicatorReference.referencedIndicatorId}}
+ {{entry.indicatorReference.referencedIndicatorName}}
+ {{entry.indicatorReference.referencedIndicatorDescription}}
+
+
+
+
+
+
Betroffene Skripte
+
+
0">
+
+
+
+ Skript-ID
+ Skript-Name
+ Skript-Beschreibung
+ ID des berechneten Indikators
+
+
+
+
+ {{script.scriptId}}
+ {{script.name}}
+ {{script.description}}
+ {{script.requiredIndicatorIds.join(', ')}}
+
+
+
+
+
+
+
+
+
Zeitschnitte entfernen
+
Hier können einzelne Zeitschnitte für alle verfügbaren Raumebenen entfernt werden
+
+
+
+
+ Alle Zeitschnitte {{selectIndicatorTimestampsInput ? 'ab' : ''}}wählen
+
+
+
+
+
+
+
+
+
Raumebenen entfernen
+
Hier können einzelne Raumebenen für alle verfügbaren Zeitschnitte entfernt werden
+
+
+
+
+ Alle Raumebenen {{selectIndicatorSpatialUnitsInput ? 'ab' : ''}}wählen
+
+
+
+
+
+
+
+
+
+
+
+
Folgende Indikatoren-Daten sowie assoziierte Indikatoren- und Georessourcen-Referenzen und Skripte, bei denen Indikatoren als Berechnungsgrundlage verwendet werden, wurden erfolgreich gelöscht
+
+
0">
+
Gesamter Indikatorendatensatz (Metadaten sowie alle Zeitreihen-Werte und Raumebenen)
+
+ {{dataset.indicatorName}}
+
+
+
+
+
0">
+
Referenzen zu Georessourcen
+
+ {{reference.georesourceReference.referencedGeoresourceName}}
+
+
+
+
0">
+
Referenzen zu anderen Indikatoren
+
+ {{reference.indicatorReference.referencedIndicatorName}}
+
+
+
+
+
0">
+
Zeitschnitte
+
+ {{timestamp.timestamp}}
+
+
+
+
0">
+
Raumebenen
+
+ {{spatialUnit.spatialUnitMetadata.spatialUnitLevel}}
+
+
+
+
+
+
+
+
Löschen gescheitert
+ Folgende Datensätze konnten nicht gelöscht werden.
+
+
+
0">
+
+
+
+ Name
+ Fehlermeldung
+
+
+
+
+ {{dataset[0].indicatorName}}
+
+
+
+
+
+
+
0">
+
+
+
+ Zeitschnitt
+ Fehlermeldung
+
+
+
+
+ {{dataset[0].timestamp}}
+
+
+
+
+
+
+
0">
+
+
+
+ Raumebene
+ Fehlermeldung
+
+
+
+
+ {{dataset[0].spatialUnitMetadata.spatialUnitLevel}}
+
+
+
+
+
+
+
+
+
\ 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..efcb2dd6b
--- /dev/null
+++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditFeaturesModal/indicator-edit-features-modal.component.css
@@ -0,0 +1,511 @@
+/* AG Grid Theme Import */
+@import '~ag-grid-community/styles/ag-theme-alpine.css';
+
+/* AG Grid Specific Styles */
+.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;
+}
+
+.ag-theme-alpine .ag-header-cell {
+ font-weight: 600;
+ border-bottom: 2px solid #dee2e6;
+}
+
+.ag-theme-alpine .ag-cell {
+ padding: 8px 12px;
+ border-right: 1px solid #dee2e6;
+}
+
+.ag-theme-alpine .ag-row {
+ border-bottom: 1px solid #dee2e6;
+}
+
+.ag-theme-alpine .ag-row:hover {
+ background-color: #e9ecef;
+}
+
+.ag-theme-alpine .ag-cell[style*="background-color: #9DC89F"] {
+ background-color: #9DC89F !important;
+}
+
+.ag-theme-alpine .ag-cell[style*="background-color: #E79595"] {
+ background-color: #E79595 !important;
+}
+
+/* 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;
+}
+
+/* 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;
+}
+
+/*progressbar*/
+#progressbar {
+ margin-bottom: 10px;
+ overflow: hidden;
+ /*CSS counters to number the steps*/
+ counter-reset: step;
+ /* z-index: 10000; */
+}
+
+#progressbar li {
+ list-style-type: none;
+ color: black;
+ text-transform: uppercase;
+ font-size: 9px;
+ /* width: 33.33%; */
+ float: left;
+ position: relative;
+ letter-spacing: 1px;
+ /* transform-style: preserve-3d; */
+ /* z-index: 1; */
+ 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);
+ /* z-index: +1; */
+}
+
+/*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;
+ /* transform: translateZ(-2px); */
+}
+
+#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;
+}
+
+/* Enhanced hover effects for better UX */
+#progressbar li:hover {
+ color: var(--kommonitor-primary);
+}
+
+#progressbar li:hover:before {
+ background: var(--kommonitor-primary);
+ transform: scale(1.1);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+}
+
+#progressbar li:active:before {
+ transform: scale(1.05);
+}
+
+/* Completed steps */
+#progressbar li.completed:before {
+ background: var(--kommonitor-primary);
+}
+
+#progressbar li.completed:after {
+ background: var(--kommonitor-primary);
+}
+
+/* Error states */
+#progressbar li.error:before {
+ background: #e74c3c;
+}
+
+#progressbar li.error:after {
+ background: #e74c3c;
+}
+
+#progressbar li.error {
+ color: #e74c3c;
+}
+
+/* Action buttons - Centered */
+.action-button {
+ width: 150px;
+ background: var(--kommonitor-primary);
+ color: white;
+ border: 0 none;
+ border-radius: 5px;
+ cursor: pointer;
+ padding: 10px 5px;
+ margin: 5px 5px 10px 10px;
+ text-decoration: none;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.3s ease;
+}
+
+.action-button:hover, .action-button:focus {
+ background: var(--kommonitor-primary);
+ color: white;
+ text-decoration: none;
+ box-shadow: 0 0 0 2px white, 0 0 0 3px var(--kommonitor-primary);
+}
+
+.action-button-previous {
+ width: 150px;
+ background: #95a5a6;
+ color: white;
+ border: 0 none;
+ border-radius: 5px;
+ cursor: pointer;
+ padding: 10px 5px;
+ margin: 5px 5px 10px 10px;
+ text-decoration: none;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.3s ease;
+}
+
+.action-button-previous:hover, .action-button-previous:focus {
+ background: #7f8c8d;
+ color: white;
+ text-decoration: none;
+ box-shadow: 0 0 0 2px white, 0 0 0 3px #7f8c8d;
+}
+
+.button-container {
+ text-align: center;
+ margin-top: 20px;
+}
+
+/* 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..806baa713
--- /dev/null
+++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditFeaturesModal/indicator-edit-features-modal.component.html
@@ -0,0 +1,428 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ = 1" (click)="goToStep(1)">Zeitreihen Übersicht
+ = 2" (click)="goToStep(2)">Räumlicher Datensatz
+
+
+
+
+
+ Zeitreihen Übersicht
+ Optionale Anzeige der Zeitreihen-Details
+
+
+
+
+
+ Letztes erfolgreiches Update eines Einzeleintrags
+ {{getFeatureTableSuccessTimestamp()}}
+
+
+ Letztes gescheitertes Update eines Einzeleintrags
+ {{getFeatureTableFailureTimestamp()}}
+
+
+
+
+
+
+
+
+
+
+
+
+ Mapping-Import
+
+
+
+ Mapping-Export
+
+
+
+ Zeitreihen-Import
+ Angaben über den Datensatz, aus dem die Indikatoren-Zeitreihen-Werte importiert werden
+ * = Pflichtfeld
+
+
+
+
+
+
+
Vergabe der Zugriffsrechte auf die Datensatz-Zeitreihe für Raumebene {{targetSpatialUnitMetadata?.spatialUnitLevel}}
+
lesender Zugriff auf die Indikator-Zeitreihe wird nur den Organisationseinheiten gewährt, die auch lesenden Zugriff auf Metadaten der assoziierten Raumebene besitzen.
+
+
+
+
Öffentliche Lesefreigabe*
+
+
+
+
+
+
Öffentlich freigegebene Datensätze können ohne Login abgerufen werden.
+
+
+
+
+
+ Als Eigentümer-Organisation des Datensatzes können Sie Lese- und Editier-Rechte an die eigene und weitere Organisationseinheiten vergeben.
+
+
+
+ Lese- und Editierrechte wurden anhand der selektierten Raumeinheit und für die von Eigentümer-Organisationseinheit vorab markiert.
+
+
+
+
+
+
+
+
+ Zeitreihen-Mapping
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
×
+
Zeitreihen erfolgreich fortgeführt
+
Fortführen der Zeitreihen des Indikators mit Namen {{successMessagePart}} war erfolgreich.
+
0">
+ {{importedFeatures.length}} Zeitreihen wurden dabei importiert.
+
+
+
+
+
+
×
+
Zeitreihen fortführen gescheitert
+ Beim Fortführen der Zeitreihen ist ein Fehler aufgetreten. Fehlermeldung:
+
+
+
+
+
+
Bei den {{importerErrors.length}} Zeitreihen mit folgenden IDs scheitert der Import:
+
+
+
+
Bitte beheben Sie die angezeigten Fehler im Datensatz und wiederholen den Prozess.
+
+
+
+
+
+
×
+
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/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..d53e97398
--- /dev/null
+++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditFeaturesModal/indicator-edit-features-modal.component.ts
@@ -0,0 +1,1432 @@
+import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
+import { NgbActiveModal } 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 { KommonitorIndicatorDataExchangeService } from 'services/adminIndicatorUnit/kommonitor-data-exchange.service';
+import { KommonitorIndicatorImporterHelperService } from 'services/adminIndicatorUnit/kommonitor-importer-helper.service';
+import { MultiStepHelperServiceService } from 'services/multi-step-helper-service/multi-step-helper-service.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;
+
+@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, OnDestroy {
+ @ViewChild('indicatorFeatureTable', { static: true }) indicatorFeatureTable!: AgGridAngular;
+
+ // 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;
+ public roleManagementColumnDefs: ColDef[] = [];
+ public roleManagementRowData: any[] = [];
+ public roleManagementGridOptions: GridOptions = {};
+
+ // 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;
+
+ // Mapping config import settings
+ mappingConfigImportSettings: any;
+ indicatorMappingConfigStructure_pretty: string = '';
+
+ // Grid options for feature table
+ featureTableGridOptions: GridOptions = {};
+ public columnDefs: ColDef[] = [];
+ public rowData: any[] = [];
+ public gridApi!: GridApi;
+ private columnApi!: ColumnApi;
+
+ // Subscriptions
+ private subscriptions: Subscription[] = [];
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ private broadcastService: BroadcastService,
+ public kommonitorIndicatorDataExchangeService: KommonitorIndicatorDataExchangeService,
+ public kommonitorIndicatorDataGridHelperService: KommonitorIndicatorDataGridHelperService,
+ public kommonitorIndicatorImporterHelperService: KommonitorIndicatorImporterHelperService,
+ private multiStepHelperService: MultiStepHelperServiceService,
+ private dataExchangeService: DataExchangeService,
+ private http: HttpClient
+ ) {}
+
+ ngOnInit(): void {
+
+ this.setupEventListeners();
+ this.initializeForm();
+ this.buildFeatureTable();
+
+ // Initialize mapping config structure
+ this.indicatorMappingConfigStructure_pretty = this.kommonitorIndicatorDataExchangeService.syntaxHighlightJSON(
+ this.kommonitorIndicatorImporterHelperService.mappingConfigStructure_indicator
+ );
+
+ // Fetch spatial units data and access control data
+ this.loadSpatialUnitsData();
+ this.loadAccessControlData();
+
+ // If currentIndicatorDataset is already set (from parent component), initialize form
+ if (this.currentIndicatorDataset) {
+ this.onEditIndicatorFeatures(this.currentIndicatorDataset);
+ }
+
+ // Ensure spatial unit is set after data is loaded
+ setTimeout(() => {
+ this.ensureSpatialUnitIsSet();
+ // Initialize role management table
+ this.refreshRoles();
+ }, 100);
+ }
+
+ ngOnDestroy(): void {
+ this.subscriptions.forEach(sub => sub.unsubscribe());
+ }
+
+ private setupEventListeners(): void {
+ // Listen for edit indicator features event
+ const broadcastSubscription = this.broadcastService.currentBroadcastMsg.subscribe((data: any) => {
+ if (data.msg === 'onEditIndicatorFeatures') {
+ this.onEditIndicatorFeatures(data.values);
+ } else if (data.msg === 'timeseriesMappingChanged') {
+ this.timeseriesMappingReference = data.mapping;
+ } else if (data.msg === 'refreshIndicatorOverviewTableCompleted') {
+ if (this.currentIndicatorDataset) {
+ this.currentIndicatorDataset = this.kommonitorIndicatorDataExchangeService.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') {
+ // Handle individual feature deletion
+ this.broadcastService.broadcast('refreshIndicatorOverviewTable', {
+ action: 'edit',
+ indicatorId: this.currentIndicatorDataset.indicatorId
+ });
+ this.refreshIndicatorEditFeaturesOverviewTable();
+ }
+ });
+
+ this.subscriptions.push(broadcastSubscription);
+
+ // Setup file input change listener
+ setTimeout(() => {
+ $(document).on("change", "#indicatorMappingConfigEditFeaturesImportFile", (event: any) => {
+ const file = (event.target as HTMLInputElement).files?.[0];
+ if (file) {
+ this.parseMappingConfigFromFile(file);
+ }
+ });
+ }, 100);
+ }
+
+ private initializeForm(): void {
+ // Initialize form components
+ }
+
+ private buildFeatureTable(): void {
+
+ // Build grid options using the helper service
+ this.featureTableGridOptions = this.kommonitorIndicatorDataGridHelperService.buildDataGrid_featureTable_indicatorResource(
+ "indicatorFeatureTable",
+ this.remainingFeatureHeaders || [],
+ this.indicatorFeaturesJSON || [],
+ this.currentIndicatorDataset?.indicatorId,
+ this.kommonitorIndicatorDataGridHelperService.resourceType_indicator,
+ this.enableDeleteFeatures
+ );
+
+
+
+ // Extract column definitions and row data for separate binding
+ this.columnDefs = this.featureTableGridOptions.columnDefs || [];
+ this.rowData = this.featureTableGridOptions.rowData || [];
+
+
+
+ // Ensure grid options are properly structured for AG Grid Angular
+ if (this.featureTableGridOptions) {
+ // Ensure required properties are present
+ if (!this.featureTableGridOptions.defaultColDef) {
+ this.featureTableGridOptions.defaultColDef = {
+ editable: true,
+ sortable: true,
+ flex: 1,
+ minWidth: 200,
+ filter: true,
+ floatingFilter: true,
+ resizable: true,
+ wrapText: true,
+ autoHeight: true
+ };
+ }
+
+ // Ensure pagination is enabled
+ if (this.featureTableGridOptions.pagination === undefined) {
+ this.featureTableGridOptions.pagination = true;
+ }
+
+ if (this.featureTableGridOptions.paginationPageSize === undefined) {
+ this.featureTableGridOptions.paginationPageSize = 10;
+ }
+
+
+ }
+ }
+
+ onEditIndicatorFeatures(indicatorDataset: any): void {
+
+ if (this.currentIndicatorDataset &&
+ this.currentIndicatorDataset.indicatorId === indicatorDataset.indicatorId) {
+ return;
+ }
+
+ this.currentIndicatorDataset = indicatorDataset;
+
+
+ // Ensure access control data is loaded before resetting form
+ this.loadAccessControlData().then(() => {
+ this.resetIndicatorEditFeaturesForm();
+ this.buildFeatureTable();
+
+ // Ensure spatial unit is set
+ this.ensureSpatialUnitIsSet();
+
+ // Fetch data for the indicator features after form reset
+ if (this.overviewTableTargetSpatialUnitMetadata) {
+ this.refreshIndicatorEditFeaturesOverviewTable();
+ }
+
+ // Force grid to refresh after a short delay to ensure it's ready
+ setTimeout(() => {
+ if (this.gridApi && this.indicatorFeaturesJSON) {
+ this.updateGridData();
+ }
+ }, 100);
+ });
+ }
+
+ closeModal(): void {
+ this.activeModal.dismiss();
+ }
+
+ resetIndicatorEditFeaturesForm(): void {
+ this.isPublic = false;
+ this.enableDeleteFeatures = false;
+
+ // Reset edit banners
+ this.kommonitorIndicatorDataGridHelperService.featureTable_indicator_lastUpdate_timestamp_success = undefined;
+ this.kommonitorIndicatorDataGridHelperService.featureTable_indicator_lastUpdate_timestamp_failure = undefined;
+
+ this.indicatorFeaturesJSON = [];
+ this.remainingFeatureHeaders = [];
+ this.overviewTableTargetSpatialUnitMetadata = undefined;
+
+ // Set default spatial unit (like in AngularJS version)
+ this.overviewTableTargetSpatialUnitMetadata = undefined;
+ for (const spatialUnitMetadataEntry of this.kommonitorIndicatorDataExchangeService.availableSpatialUnits) {
+ if (this.currentIndicatorDataset.applicableSpatialUnits.some((applicableUnit: any) =>
+ applicableUnit.spatialUnitName === spatialUnitMetadataEntry.spatialUnitLevel)) {
+ this.overviewTableTargetSpatialUnitMetadata = spatialUnitMetadataEntry;
+ break;
+ }
+ }
+
+ // Set targetApplicableSpatialUnit from currentIndicatorDataset.applicableSpatialUnits
+ this.targetApplicableSpatialUnit = undefined;
+ if (this.currentIndicatorDataset && this.currentIndicatorDataset.applicableSpatialUnits) {
+ // Set to the first applicable spatial unit by default
+ this.targetApplicableSpatialUnit = this.currentIndicatorDataset.applicableSpatialUnits[0];
+
+ }
+
+ // Initialize role management grid using the new Angular approach
+ this.refreshRoles();
+
+ this.spatialUnitRefKeyProperty = '';
+ this.targetSpatialUnitMetadata = 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();
+
+ // Rebuild the feature table with empty data
+ this.buildFeatureTable();
+
+ // If we have a target spatial unit selected, fetch the data
+ if (this.overviewTableTargetSpatialUnitMetadata) {
+ this.refreshIndicatorEditFeaturesOverviewTable();
+ }
+ }
+
+ refreshIndicatorEditFeaturesOverviewTable(): void {
+ if (!this.currentIndicatorDataset || !this.currentIndicatorDataset.indicatorId) {
+ return;
+ }
+
+ // Use the first applicable spatial unit from the indicator dataset
+ if (!this.overviewTableTargetSpatialUnitMetadata && this.currentIndicatorDataset.applicableSpatialUnits?.length > 0) {
+ this.overviewTableTargetSpatialUnitMetadata = this.currentIndicatorDataset.applicableSpatialUnits[0];
+ }
+
+ if (!this.overviewTableTargetSpatialUnitMetadata) {
+ return;
+ }
+
+ this.loadingData = true;
+ this.hideSuccessAlert();
+ this.hideErrorAlert();
+
+ const url = this.kommonitorIndicatorDataExchangeService.getBaseUrlToKomMonitorDataAPI_spatialResource() +
+ "/indicators/" + this.currentIndicatorDataset.indicatorId + "/" +
+ this.overviewTableTargetSpatialUnitMetadata.spatialUnitId + "/without-geometry";
+
+ this.http.get(url).subscribe({
+ next: (response: any) => {
+ // Handle both response.data and direct array response
+ let responseData = response;
+ if (response && response.data) {
+ responseData = response.data;
+ }
+
+ // Check if we have data
+ if (!responseData || !Array.isArray(responseData) || responseData.length === 0) {
+ this.indicatorFeaturesJSON = [];
+ this.remainingFeatureHeaders = [];
+
+ // Rebuild the grid with empty data
+ this.buildFeatureTable();
+
+ setTimeout(() => {
+ this.loadingData = false;
+ }, 500);
+ return;
+ }
+
+ this.indicatorFeaturesJSON = responseData;
+
+ const tmpRemainingHeaders: string[] = [];
+
+ // Extract headers from the first indicator feature
+ if (this.indicatorFeaturesJSON[0]) {
+ // Get indicator date prefix from environment or use default
+ const indicatorDatePrefix = (window.__env && window.__env.indicatorDatePrefix) || 'DATE_';
+
+ for (const property in this.indicatorFeaturesJSON[0]) {
+ // Only show indicator date columns as editable fields
+ if (property.includes(indicatorDatePrefix)) {
+ tmpRemainingHeaders.push(property);
+ }
+ }
+ }
+
+ // Sort date headers
+ tmpRemainingHeaders.sort((a, b) => a.localeCompare(b));
+
+ this.remainingFeatureHeaders = tmpRemainingHeaders;
+
+ // Rebuild the grid options with new data
+ this.buildFeatureTable();
+
+ // Force grid to refresh after a short delay to ensure it's ready
+ setTimeout(() => {
+ if (this.gridApi && this.indicatorFeaturesJSON) {
+ this.updateGridData();
+ }
+ }, 100);
+
+ setTimeout(() => {
+ this.loadingData = false;
+ }, 500);
+ },
+ error: (error: any) => {
+ this.handleError(error);
+
+ // Set empty data on error
+ this.indicatorFeaturesJSON = [];
+ this.remainingFeatureHeaders = [];
+
+ setTimeout(() => {
+ this.loadingData = false;
+ }, 500);
+ }
+ });
+ }
+
+ /**
+ * Update grid data with proper transformation
+ */
+ private updateGridData(): void {
+ if (!this.gridApi || !this.indicatorFeaturesJSON) {
+ return;
+ }
+
+ // Transform the data to match the expected format
+ const transformedData = this.indicatorFeaturesJSON.map((feature: any, index: number) => {
+ // Ensure each feature has the required properties
+ if (feature && typeof feature === 'object') {
+ // Add any missing required properties
+ if (!feature.hasOwnProperty('kommonitorRecordId')) {
+ feature.kommonitorRecordId = feature.fid || feature.ID || feature.id;
+ }
+ // Add required fields for delete functionality
+ feature.datasetId = this.currentIndicatorDataset?.indicatorId;
+ feature.spatialUnitId = this.overviewTableTargetSpatialUnitMetadata?.spatialUnitId;
+ feature.ID = feature.ID || feature.id || feature.fid;
+ feature.fid = feature.fid || feature.ID || feature.id;
+
+ return feature;
+ }
+ return feature;
+ });
+
+ // Update the rowData property
+ this.rowData = transformedData;
+
+ // Update the grid with new data
+ this.gridApi.setRowData(transformedData);
+
+ // Force refresh of the grid
+ this.gridApi.refreshCells({ force: true });
+ this.gridApi.redrawRows();
+
+ // Register click handlers after grid update
+ setTimeout(() => {
+ this.kommonitorIndicatorDataGridHelperService.registerFeatureTableClickHandlers(
+ this.currentIndicatorDataset?.indicatorId,
+ this.kommonitorIndicatorDataGridHelperService.resourceType_indicator,
+ this.enableDeleteFeatures
+ );
+ }, 200);
+ }
+
+ clearAllIndicatorFeatures(): void {
+ if (!this.overviewTableTargetSpatialUnitMetadata) {
+ return;
+ }
+
+ this.loadingData = true;
+ this.hideSuccessAlert();
+ this.hideErrorAlert();
+
+ const url = this.kommonitorIndicatorDataExchangeService.baseUrlToKomMonitorDataAPI +
+ "/indicators/" + this.currentIndicatorDataset.indicatorId + "/" +
+ this.overviewTableTargetSpatialUnitMetadata.spatialUnitId;
+
+ this.http.delete(url).subscribe({
+ next: (response: any) => {
+ this.indicatorFeaturesJSON = [];
+ this.remainingFeatureHeaders = [];
+
+ this.broadcastService.broadcast('refreshIndicatorOverviewTable', { action: 'edit', indicatorId: this.currentIndicatorDataset.indicatorId });
+
+ // Force empty feature overview table on successful deletion of entries
+ this.featureTableGridOptions = this.kommonitorIndicatorDataGridHelperService.buildDataGrid_featureTable_indicatorResource(
+ "indicatorFeatureTable",
+ [],
+ [],
+ this.currentIndicatorDataset.indicatorId,
+ this.kommonitorIndicatorDataGridHelperService.resourceType_indicator,
+ this.enableDeleteFeatures
+ );
+
+ this.successMessagePart = this.currentIndicatorDataset.indicatorName;
+ this.showSuccessAlert();
+
+ setTimeout(() => {
+ this.loadingData = false;
+ }, 500);
+ },
+ error: (error: any) => {
+ this.handleError(error);
+ setTimeout(() => {
+ this.loadingData = false;
+ }, 500);
+ }
+ });
+ }
+
+ 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 {
+ // Ensure access control data is loaded before proceeding
+ if (!this.kommonitorIndicatorDataExchangeService.accessControl || this.kommonitorIndicatorDataExchangeService.accessControl.length === 0) {
+ this.loadAccessControlData().then(() => {
+ this.refreshRoles();
+ });
+ return;
+ }
+
+ // Use current user's login role IDs as initial permissions (like in original AngularJS)
+ let permissions = this.kommonitorIndicatorDataExchangeService.getCurrentKomMonitorLoginRoleIds();
+
+ // If we have a target applicable spatial unit, use its allowedRoles instead
+ if (this.targetApplicableSpatialUnit && this.targetApplicableSpatialUnit.allowedRoles) {
+ permissions = this.targetApplicableSpatialUnit.allowedRoles;
+ }
+
+ if (this.currentIndicatorDataset) {
+ const accessControl = this.kommonitorIndicatorDataExchangeService.getAccessControlById(this.currentIndicatorDataset.ownerId);
+
+ if (accessControl && accessControl.permissions) {
+ const permissionIds_ownerUnit = accessControl.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.kommonitorIndicatorDataExchangeService.accessControl.forEach((item: any) => {
+ if (this.currentIndicatorDataset) {
+ if (item.organizationalUnitId == this.currentIndicatorDataset.ownerId) {
+ item.datasetOwner = true;
+ } else {
+ item.datasetOwner = false;
+ }
+ }
+ });
+
+ // Build role management grid using AG Grid Angular
+ this.buildRoleManagementGrid(permissions);
+ }
+
+ /**
+ * Build role management grid using AG Grid Angular
+ */
+ private buildRoleManagementGrid(permissions: string[]): void {
+ // Build grid options
+ this.roleManagementGridOptions = {
+ defaultColDef: {
+ sortable: true,
+ filter: true,
+ resizable: true
+ },
+ pagination: true,
+ paginationPageSize: 10
+ };
+
+ // Build column definitions
+ this.roleManagementColumnDefs = this.buildRoleManagementColumnDefs();
+
+ // Build row data
+ this.roleManagementRowData = this.buildRoleManagementRowData(permissions);
+ }
+
+ /**
+ * Build role management column definitions
+ */
+ private buildRoleManagementColumnDefs(): ColDef[] {
+ const columnDefs: ColDef[] = [
+ {
+ headerName: 'Organisationseinheit',
+ field: "organizationalUnitName",
+ pinned: 'left',
+ minWidth: 200
+ }
+ ];
+
+ // Add permission columns - using the correct German headers from AngularJS
+ columnDefs.push(
+ {
+ headerName: 'lesen',
+ field: 'viewer',
+ maxWidth: 100,
+ cellRenderer: this.kommonitorIndicatorDataGridHelperService.CheckboxRenderer_viewer
+ },
+ {
+ headerName: 'editieren',
+ field: 'editor',
+ maxWidth: 100,
+ cellRenderer: this.kommonitorIndicatorDataGridHelperService.CheckboxRenderer_editor
+ }
+ );
+
+ return columnDefs;
+ }
+
+ /**
+ * Build role management row data
+ */
+ private buildRoleManagementRowData(permissions: string[]): any[] {
+ if (!this.kommonitorIndicatorDataExchangeService.accessControl || this.kommonitorIndicatorDataExchangeService.accessControl.length === 0) {
+ return [];
+ }
+
+ // Create a deep copy of the data (like AngularJS)
+ let data = JSON.parse(JSON.stringify(this.kommonitorIndicatorDataExchangeService.accessControl));
+
+ // Process each item (like AngularJS)
+ for (let elem of data) {
+ // Handle 'public' name translation (like AngularJS)
+ if (elem.name === 'public') {
+ elem.name = 'Öffentlicher Zugriff';
+ }
+
+ // Process permissions
+ for (let permission of elem.permissions) {
+ permission.isChecked = false;
+ if (permissions && permissions.includes(permission.permissionId)) {
+ permission.isChecked = true;
+ }
+ }
+ }
+
+ // Apply special ordering logic (like AngularJS)
+ let array: any[] = [];
+
+ // Always put first 2 items at the top
+ if (data.length > 0) {
+ array.push(data[0]);
+ }
+ if (data.length > 1) {
+ array.push(data[1]);
+ }
+
+ // Remove first 2 items and sort the rest
+ data.splice(0, 2);
+ data.sort(function (a: any, b: any) {
+ if (a.name < b.name) {
+ return -1;
+ }
+ if (a.name > b.name) {
+ return 1;
+ }
+ return 0;
+ });
+
+ // Combine fixed first 2 + sorted rest
+ array = array.concat(data);
+
+ // Convert to the format expected by the grid
+ return array.map(item => {
+ // Extract permission IDs from the permissions array
+ const viewerPermission = item.permissions?.find((p: any) => p.permissionLevel === 'viewer');
+ const editorPermission = item.permissions?.find((p: any) => p.permissionLevel === 'editor');
+ const creatorPermission = item.permissions?.find((p: any) => p.permissionLevel === 'creator');
+
+ const viewerPermissionId = viewerPermission?.permissionId || '';
+ const editorPermissionId = editorPermission?.permissionId || '';
+ const creatorPermissionId = creatorPermission?.permissionId || '';
+
+ const result = {
+ organizationalUnitId: item.organizationalUnitId,
+ organizationalUnitName: item.name,
+ viewer: permissions.includes(viewerPermissionId),
+ editor: permissions.includes(editorPermissionId),
+ creator: permissions.includes(creatorPermissionId),
+ datasetOwner: item.datasetOwner || false,
+ // Store the permission IDs for later use
+ viewerPermissionId: viewerPermissionId,
+ editorPermissionId: editorPermissionId,
+ creatorPermissionId: creatorPermissionId
+ };
+
+ return result;
+ });
+ }
+
+ 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 {
+ // Rebuild the grid with updated delete settings
+ this.buildFeatureTable();
+
+ // Update grid column definitions and data if API is available
+ if (this.gridApi && this.columnDefs) {
+ // Update column definitions
+ this.gridApi.setColumnDefs(this.columnDefs);
+
+ // Update data if we have features
+ if (this.indicatorFeaturesJSON && this.indicatorFeaturesJSON.length > 0) {
+ this.updateGridData();
+ }
+
+ // Force refresh of the grid to show/hide delete buttons
+ this.gridApi.refreshCells({ force: true });
+
+ // Register click handlers after grid update
+ setTimeout(() => {
+ this.kommonitorIndicatorDataGridHelperService.registerFeatureTableClickHandlers(
+ this.currentIndicatorDataset?.indicatorId,
+ this.kommonitorIndicatorDataGridHelperService.resourceType_indicator,
+ this.enableDeleteFeatures
+ );
+ }, 100);
+ }
+ }
+
+ 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.getSelectedRoleIds();
+
+ // Create the put body manually since there's no buildPutBody_indicators method
+ this.putBody_indicators = {
+ "targetSpatialUnitMetadata": {
+ "spatialUnitLevel": this.targetSpatialUnitMetadata?.spatialUnitLevel,
+ },
+ "currentIndicatorDataset": {
+ "defaultClassificationMapping": this.currentIndicatorDataset?.defaultClassificationMapping
+ },
+ "permissions": roleIds || [],
+ "ownerId": this.currentIndicatorDataset?.ownerId,
+ "isPublic": this.isPublic
+ };
+
+ if (!this.converterDefinition || !this.datasourceTypeDefinition || !this.propertyMappingDefinition || !this.putBody_indicators) {
+ return false;
+ }
+
+ return true;
+ }
+
+ buildConverterDefinition(): any {
+ return this.kommonitorIndicatorImporterHelperService.buildConverterDefinition(
+ this.converter,
+ "converterParameter_indicatorEditFeatures_",
+ this.schema,
+ this.mimeType
+ );
+ }
+
+ async buildDatasourceTypeDefinition(): Promise {
+ try {
+ return await this.kommonitorIndicatorImporterHelperService.buildDatasourceTypeDefinition(
+ this.datasourceType,
+ 'datasourceTypeParameter_indicatorEditFeatures_',
+ 'indicatorDataSourceInput_editFeatures'
+ );
+ } catch (error: any) {
+ this.handleError(error);
+ return null;
+ }
+ }
+
+ buildPropertyMappingDefinition(): any {
+ let timeseriesMappingForImporter = this.timeseriesMappingReference || [];
+ return this.kommonitorIndicatorImporterHelperService.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.kommonitorIndicatorImporterHelperService.updateIndicator(
+ this.converterDefinition,
+ this.datasourceTypeDefinition,
+ this.propertyMappingDefinition,
+ this.currentIndicatorDataset.indicatorId,
+ this.putBody_indicators,
+ true
+ );
+
+ if (!this.kommonitorIndicatorImporterHelperService.importerResponseContainsErrors(updateIndicatorResponse_dryRun)) {
+ // All good, really execute the request to import data against data management API
+ const updateIndicatorResponse = await this.kommonitorIndicatorImporterHelperService.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.kommonitorIndicatorImporterHelperService.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.kommonitorIndicatorImporterHelperService.getErrorsFromImporterResponse(updateIndicatorResponse_dryRun) || [];
+
+ this.showErrorAlert();
+ this.loadingData = false;
+ }
+ } catch (error: any) {
+ this.handleError(error);
+ this.loadingData = false;
+ }
+ }
+
+ onImportIndicatorEditFeaturesMappingConfig(): void {
+ this.indicatorMappingConfigImportError = "";
+ $("#indicatorMappingConfigEditFeaturesImportFile").files = [];
+ $("#indicatorMappingConfigEditFeaturesImportFile").click();
+ }
+
+ parseMappingConfigFromFile(file: File): void {
+ const fileReader = new FileReader();
+
+ fileReader.onload = (event: any) => {
+ try {
+ this.parseFromMappingConfigFile(event);
+ } catch (error) {
+ this.indicatorMappingConfigImportError = "Uploaded MappingConfig File cannot be parsed correctly";
+ const element = document.getElementById("indicatorsEditFeaturesMappingConfigPre");
+ if (element) {
+ element.innerHTML = this.indicatorMappingConfigStructure_pretty;
+ }
+ this.showMappingConfigErrorAlert();
+ }
+ };
+
+ // Read in the file as text
+ fileReader.readAsText(file);
+ }
+
+ parseFromMappingConfigFile(event: any): void {
+ this.mappingConfigImportSettings = JSON.parse(event.target.result);
+
+ if (!this.mappingConfigImportSettings.converter ||
+ !this.mappingConfigImportSettings.dataSource ||
+ !this.mappingConfigImportSettings.propertyMapping) {
+ this.indicatorMappingConfigImportError = "Struktur der Datei stimmt nicht mit erwartetem Muster überein.";
+ const element = document.getElementById("indicatorsEditFeaturesMappingConfigPre");
+ if (element) {
+ element.innerHTML = this.indicatorMappingConfigStructure_pretty;
+ }
+ this.showMappingConfigErrorAlert();
+ return;
+ }
+
+ this.converter = undefined;
+ for (const converter of this.kommonitorIndicatorImporterHelperService.availableConverters) {
+ if (converter.name === this.mappingConfigImportSettings.converter.name) {
+ this.converter = converter;
+ break;
+ }
+ }
+
+ this.schema = undefined;
+ 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 = undefined;
+ 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.kommonitorIndicatorImporterHelperService.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_indicatorEditFeatures_" + 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_indicatorEditFeatures_" + dsParameter.name) as HTMLInputElement;
+ if (element) {
+ element.value = dsParameter.value;
+ }
+ }
+ }
+
+ // Property Mapping
+ this.spatialUnitRefKeyProperty = this.mappingConfigImportSettings.propertyMapping.spatialReferenceKeyProperty;
+
+ this.broadcastService.broadcast('loadTimeseriesMapping', { mapping: this.mappingConfigImportSettings.propertyMapping.timeseriesMappings });
+
+ if (this.mappingConfigImportSettings.targetSpatialUnitName) {
+ for (const spatialUnitMetadata of this.kommonitorIndicatorDataExchangeService.availableSpatialUnits) {
+ if (spatialUnitMetadata.spatialUnitLevel === this.mappingConfigImportSettings.targetSpatialUnitName) {
+ this.targetSpatialUnitMetadata = spatialUnitMetadata;
+ }
+ }
+ }
+
+ // Build role management grid with imported permissions
+ this.buildRoleManagementGrid(this.mappingConfigImportSettings.allowedRoles || []);
+
+ this.keepMissingValues = this.mappingConfigImportSettings.propertyMapping.keepMissingOrNullValueIndicator;
+ }
+
+ onExportIndicatorEditFeaturesMappingConfig(): void {
+ this.buildImporterObjects().then(() => {
+ const mappingConfigExport: any = {
+ "converter": this.converterDefinition,
+ "dataSource": this.datasourceTypeDefinition,
+ "propertyMapping": this.propertyMappingDefinition,
+ "targetSpatialUnitName": this.targetSpatialUnitMetadata.spatialUnitLevel,
+ "allowedRoles": []
+ };
+
+ const roleIds = this.getSelectedRoleIds();
+ mappingConfigExport.allowedRoles = 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;
+ }
+ }
+
+ // AG Grid event handlers
+ onGridReady(event: GridReadyEvent): void {
+ this.gridApi = event.api;
+ this.columnApi = event.columnApi;
+
+ // If we have data, set it to the grid
+ if (this.indicatorFeaturesJSON && this.indicatorFeaturesJSON.length > 0) {
+ this.updateGridData();
+ }
+
+ // Also set the column definitions if available
+ if (this.columnDefs && this.columnDefs.length > 0) {
+ this.gridApi.setColumnDefs(this.columnDefs);
+ }
+
+ // Register click handlers for delete functionality
+ setTimeout(() => {
+ this.kommonitorIndicatorDataGridHelperService.registerFeatureTableClickHandlers(
+ this.currentIndicatorDataset?.indicatorId,
+ this.kommonitorIndicatorDataGridHelperService.resourceType_indicator,
+ this.enableDeleteFeatures
+ );
+ }, 100);
+ }
+
+ 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
+ }
+
+ // Alert management
+ showSuccessAlert(): void {
+ $("#indicatorEditFeaturesSuccessAlert").show();
+ }
+
+ hideSuccessAlert(): void {
+ $("#indicatorEditFeaturesSuccessAlert").hide();
+ }
+
+ showErrorAlert(): void {
+ $("#indicatorEditFeaturesErrorAlert").show();
+ }
+
+ hideErrorAlert(): void {
+ $("#indicatorEditFeaturesErrorAlert").hide();
+ }
+
+ showMappingConfigErrorAlert(): void {
+ $("#indicatorEditFeaturesMappingConfigImportErrorAlert").show();
+ }
+
+ hideMappingConfigErrorAlert(): void {
+ $("#indicatorEditFeaturesMappingConfigImportErrorAlert").hide();
+ }
+
+ private handleError(error: any): void {
+ if (error.data) {
+ this.errorMessagePart = this.kommonitorIndicatorDataExchangeService.syntaxHighlightJSON(error.data);
+ } else {
+ this.errorMessagePart = this.kommonitorIndicatorDataExchangeService.syntaxHighlightJSON(error);
+ }
+ this.showErrorAlert();
+ }
+
+ /**
+ * Check if refresh button should be enabled
+ */
+ isRefreshButtonEnabled(): boolean {
+ // Enable button if we have a current indicator dataset with applicable spatial units
+ return !!this.currentIndicatorDataset &&
+ this.currentIndicatorDataset.applicableSpatialUnits &&
+ this.currentIndicatorDataset.applicableSpatialUnits.length > 0;
+ }
+
+ /**
+ * Check if clear button should be enabled
+ */
+ isClearButtonEnabled(): boolean {
+ return this.enableDeleteFeatures && !!this.overviewTableTargetSpatialUnitMetadata;
+ }
+
+ /**
+ * Get filtered converters for indicator resource type
+ */
+ getFilteredConvertersForIndicator(): any[] {
+ const converters = this.kommonitorIndicatorImporterHelperService.availableConverters;
+ const filterFn = this.kommonitorIndicatorImporterHelperService.filterConverters('indicator');
+ return converters.filter(filterFn);
+ }
+
+ /**
+ * Get available converters for indicators
+ */
+ getAvailableConvertersForIndicator(): any[] {
+ return this.kommonitorIndicatorImporterHelperService.availableConverters;
+ }
+
+ /**
+ * Get available datasource types
+ */
+ getAvailableDatasourceTypes(): any[] {
+ return this.kommonitorIndicatorImporterHelperService.availableDatasourceTypes;
+ }
+
+ /**
+ * Get available spatial units filtered for current indicator
+ */
+ getAvailableSpatialUnits(): any[] {
+ if (!this.currentIndicatorDataset || !this.currentIndicatorDataset.applicableSpatialUnits) {
+ return [];
+ }
+
+ // Filter spatial units to only show those applicable to the current indicator
+ return this.kommonitorIndicatorDataExchangeService.availableSpatialUnits.filter((spatialUnit: any) => {
+ return this.currentIndicatorDataset.applicableSpatialUnits.some((applicableUnit: any) =>
+ applicableUnit.spatialUnitId === spatialUnit.spatialUnitId ||
+ applicableUnit.spatialUnitName === spatialUnit.spatialUnitLevel ||
+ applicableUnit.spatialUnitName === spatialUnit.spatialUnitName
+ );
+ });
+ }
+
+ /**
+ * Check if keycloak security is enabled
+ */
+ isKeycloakSecurityEnabled(): boolean {
+ return this.kommonitorIndicatorDataExchangeService.enableKeycloakSecurity;
+ }
+
+ /**
+ * Check if user has admin permissions
+ */
+ hasAdminPermissions(): boolean {
+ return this.kommonitorIndicatorDataExchangeService.checkAdminPermission();
+ }
+
+ /**
+ * Check if user has create permissions
+ */
+ hasCreatePermissions(): boolean {
+ return this.kommonitorIndicatorDataExchangeService.checkCreatePermission();
+ }
+
+ /**
+ * Check if role management section should be visible
+ */
+ shouldShowRoleManagement(): boolean {
+ // Only show if Keycloak security is enabled
+ if (!this.isKeycloakSecurityEnabled()) {
+ return false;
+ }
+
+ // Only show if user has admin or create permissions
+ if (!this.hasAdminPermissions() && !this.hasCreatePermissions()) {
+ return false;
+ }
+
+ // Only show if we have a current indicator dataset
+ if (!this.currentIndicatorDataset) {
+ return false;
+ }
+
+ // Only show if we have access control data
+ if (!this.kommonitorIndicatorDataExchangeService.accessControl ||
+ this.kommonitorIndicatorDataExchangeService.accessControl.length === 0) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if current user is the owner of the indicator dataset
+ */
+ isCurrentUserOwner(): boolean {
+ if (!this.currentIndicatorDataset || !this.currentIndicatorDataset.ownerId) {
+ return false;
+ }
+
+ // Get current user's organizational unit ID
+ const currentUserOrgUnitId = this.kommonitorIndicatorDataExchangeService.currentKeycloakLoginRoles?.[0];
+
+ return currentUserOrgUnitId === this.currentIndicatorDataset.ownerId;
+ }
+
+ /**
+ * Get feature table success timestamp
+ */
+ getFeatureTableSuccessTimestamp(): string | undefined {
+ return this.kommonitorIndicatorDataGridHelperService.featureTable_indicator_lastUpdate_timestamp_success;
+ }
+
+ /**
+ * Get feature table failure timestamp
+ */
+ getFeatureTableFailureTimestamp(): string | undefined {
+ return this.kommonitorIndicatorDataGridHelperService.featureTable_indicator_lastUpdate_timestamp_failure;
+ }
+
+ /**
+ * Check if grid has data
+ */
+ hasGridData(): boolean {
+ return this.indicatorFeaturesJSON && this.indicatorFeaturesJSON.length > 0;
+ }
+
+ /**
+ * Get grid data count
+ */
+ getGridDataCount(): number {
+ return this.indicatorFeaturesJSON ? this.indicatorFeaturesJSON.length : 0;
+ }
+
+ /**
+ * Force grid refresh
+ */
+ forceGridRefresh(): void {
+ if (this.gridApi) {
+ this.updateGridData();
+ }
+ }
+
+ /**
+ * Check if grid API is available
+ */
+ isGridApiAvailable(): boolean {
+ return !!this.gridApi;
+ }
+
+ /**
+ * Check if role management grid has data
+ */
+ hasRoleManagementGridData(): boolean {
+ return this.roleManagementRowData && this.roleManagementRowData.length > 0;
+ }
+
+ /**
+ * Get role management grid data count
+ */
+ getRoleManagementGridDataCount(): number {
+ return this.roleManagementRowData ? this.roleManagementRowData.length : 0;
+ }
+
+ /**
+ * Role management grid ready event handler
+ */
+ onRoleManagementGridReady(event: GridReadyEvent): void {
+ // Grid is ready
+ }
+
+ /**
+ * Load spatial units data
+ */
+ private async loadSpatialUnitsData(): Promise {
+ try {
+ const currentRoles = this.kommonitorIndicatorDataExchangeService.currentKeycloakLoginRoles;
+ await this.kommonitorIndicatorDataExchangeService.fetchSpatialUnitsMetadata(currentRoles);
+ } catch (error) {
+ console.error('Error loading spatial units data:', error);
+ }
+ }
+
+ /**
+ * Load access control data
+ */
+ private async loadAccessControlData(): Promise {
+ try {
+ await this.kommonitorIndicatorDataExchangeService.fetchAccessControlMetadata();
+
+ // If no access control data is loaded, create some test data for development
+ if (!this.kommonitorIndicatorDataExchangeService.accessControl || this.kommonitorIndicatorDataExchangeService.accessControl.length === 0) {
+ this.createTestAccessControlData();
+ }
+ } catch (error) {
+ // Create test data as fallback
+ this.createTestAccessControlData();
+ }
+ }
+
+ /**
+ * Create test access control data for development
+ */
+ private createTestAccessControlData(): void {
+ const testData = [
+ {
+ organizationalUnitId: 'test-org-1',
+ name: 'Test Organization 1',
+ permissions: [
+ {
+ permissionId: 'viewer-perm-1',
+ permissionLevel: 'viewer',
+ isChecked: false
+ },
+ {
+ permissionId: 'editor-perm-1',
+ permissionLevel: 'editor',
+ isChecked: false
+ },
+ {
+ permissionId: 'creator-perm-1',
+ permissionLevel: 'creator',
+ isChecked: false
+ }
+ ],
+ datasetOwner: false
+ },
+ {
+ organizationalUnitId: 'test-org-2',
+ name: 'Test Organization 2',
+ permissions: [
+ {
+ permissionId: 'viewer-perm-2',
+ permissionLevel: 'viewer',
+ isChecked: false
+ },
+ {
+ permissionId: 'editor-perm-2',
+ permissionLevel: 'editor',
+ isChecked: false
+ },
+ {
+ permissionId: 'creator-perm-2',
+ permissionLevel: 'creator',
+ isChecked: false
+ }
+ ],
+ datasetOwner: false
+ }
+ ];
+
+ this.kommonitorIndicatorDataExchangeService.accessControl = testData;
+ }
+
+ /**
+ * Ensure spatial unit is set for the button to be enabled
+ */
+ private ensureSpatialUnitIsSet(): void {
+ // Use the first applicable spatial unit from the indicator dataset
+ if (!this.overviewTableTargetSpatialUnitMetadata &&
+ this.currentIndicatorDataset?.applicableSpatialUnits?.length > 0) {
+ this.overviewTableTargetSpatialUnitMetadata = this.currentIndicatorDataset.applicableSpatialUnits[0];
+ }
+ }
+
+ /**
+ * Get selected role IDs from the role management grid
+ */
+ getSelectedRoleIds(): string[] {
+ const selectedRoleIds: string[] = [];
+
+ if (this.roleManagementRowData) {
+ this.roleManagementRowData.forEach(row => {
+ if (row.viewer && row.viewerPermissionId) {
+ selectedRoleIds.push(row.viewerPermissionId);
+ }
+ if (row.editor && row.editorPermissionId) {
+ selectedRoleIds.push(row.editorPermissionId);
+ }
+ if (row.creator && row.creatorPermissionId) {
+ selectedRoleIds.push(row.creatorPermissionId);
+ }
+ });
+ }
+
+ return selectedRoleIds;
+ }
+}
\ 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..60a359c61
--- /dev/null
+++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditIndicatorSpatialUnitRolesModal/indicator-edit-indicator-spatial-unit-roles-modal.component.css
@@ -0,0 +1,537 @@
+/* CSS Variables */
+:root {
+ --kommonitor-primary: #007bff;
+}
+
+/* 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;
+}
+
+/* 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;
+}
+
+/* Progress Bar */
+#progressbar {
+ margin-bottom: 10px;
+ overflow: hidden;
+ /*CSS counters to number the steps*/
+ counter-reset: step;
+ /* z-index: 10000; */
+}
+
+#progressbar li {
+ list-style-type: none;
+ color: black;
+ text-transform: uppercase;
+ font-size: 9px;
+ /* width: 33.33%; */
+ float: left;
+ position: relative;
+ letter-spacing: 1px;
+ /* transform-style: preserve-3d; */
+ /* z-index: 1; */
+ 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);
+ /* z-index: +1; */
+}
+
+/*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;
+ /* transform: translateZ(-2px); */
+}
+
+#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;
+}
+
+/* Enhanced hover effects for better UX */
+#progressbar li:hover {
+ color: var(--kommonitor-primary);
+}
+
+#progressbar li:hover:before {
+ background: var(--kommonitor-primary);
+ transform: scale(1.1);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+}
+
+#progressbar li:active:before {
+ transform: scale(1.05);
+}
+
+/* Completed steps */
+#progressbar li.completed:before {
+ background: var(--kommonitor-primary);
+}
+
+#progressbar li.completed:after {
+ background: var(--kommonitor-primary);
+}
+
+/* Error states */
+#progressbar li.error:before {
+ background: #e74c3c;
+}
+
+#progressbar li.error:after {
+ background: #e74c3c;
+}
+
+#progressbar li.error {
+ color: #e74c3c;
+}
+
+/* Action buttons - Centered */
+.action-button {
+ width: 150px;
+ background: var(--kommonitor-primary);
+ color: white;
+ border: 0 none;
+ border-radius: 5px;
+ cursor: pointer;
+ padding: 10px 5px;
+ margin: 5px 5px 10px 10px;
+ text-decoration: none;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.3s ease;
+}
+
+.action-button:hover, .action-button:focus {
+ background: var(--kommonitor-primary);
+ color: white;
+ text-decoration: none;
+ box-shadow: 0 0 0 2px white, 0 0 0 3px var(--kommonitor-primary);
+}
+
+.action-button-previous {
+ width: 150px;
+ background: #95a5a6;
+ color: white;
+ border: 0 none;
+ border-radius: 5px;
+ cursor: pointer;
+ padding: 10px 5px;
+ margin: 5px 5px 10px 10px;
+ text-decoration: none;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.3s ease;
+}
+
+.action-button-previous:hover, .action-button-previous:focus {
+ background: #7f8c8d;
+ color: white;
+ text-decoration: none;
+ box-shadow: 0 0 0 2px white, 0 0 0 3px #7f8c8d;
+}
+
+.button-container {
+ text-align: center;
+ margin-top: 20px;
+}
+
+/* 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;
+}
+
+/* Modal footer button styles to match AngularJS */
+.modal-footer .pull-left {
+ float: left;
+}
+
+.modal-footer .btn {
+ margin-left: 5px;
+}
+
+.modal-footer .btn:first-child {
+ margin-left: 0;
+}
+
+/* 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..019ee02f4
--- /dev/null
+++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditIndicatorSpatialUnitRolesModal/indicator-edit-indicator-spatial-unit-roles-modal.component.html
@@ -0,0 +1,243 @@
+
+
+
+
+
+
+
+
+
+
+ = 1" (click)="goToStep(1)">Zugriffsschutz Indikator-Metadaten
+ = 2" (click)="goToStep(2)">Zugriffsschutz Indikator-Zeitreihe pro Raumeinheit
+ = 3" (click)="goToStep(3)">Eigentümerschaft
+
+
+
+
+
+ Zugriffsschutz Indikator-Metadaten
+ Vergabe der Zugriffsrechte auf Indikator-Metadaten pro Organisationseinheit. Hiermit legen Sie fest,
+ welche Organisationseinheiten den Indikator lesen und/oder editieren dürfen
+
+ Zugriffsrechte (lesen, editieren) müssen explizit vergeben werden
+
+
+
+
+ Zeige nur zugewiesene Rechte
+
+
+
+
+
+
+
+
Öffentliche Lesefreigabe*
+
+
+
+
+
+
Öffentlich freigegebene Datensätze können ohne Login abgerufen werden.
+
+
+
+
+
+ Als Eigentümer-Organisation des Datensatzes können Sie Lese- und Editier-Rechte an die eigene und weitere Organisationseinheiten vergeben.
+
+
+
+
+
+
+
+
+
+ Zeitreihen-Zugriffsschutz für verknüpfte Raumebenen
+ Vergabe der Zugriffsrechte auf die Indikator-Zeitreihen der einzelnen Raumebenen. Zusätzlich zu den Metadaten
+ muss der Zugriff auf die Zeitreihe einer verknüpften Raumeinheit explizit für authorisierte Organisationseinheiten freigegeben werden.
+
+
+
+
+ Zeige nur zugewiesene Rechte
+
+
+
+
+
+
+
+
+
+
+
+
Ziel-Raumebene*
+
+ -- Ziel-Raumebene wählen --
+
+ {{spatialUnit.spatialUnitName}}
+
+
+
+
Öffentliche Lesefreigabe*
+
+
+
+
+
+
Öffentlich freigegebene Datensätze können ohne Login abgerufen werden.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Eigentümerschaft eines Datensatzes
+ Übetragen der Eigentümerschaft von Datensätzen mit allen dazugehörigen Rechten
+
+ Bitte beachten Sie, dass Sie beim Übertragen einer Eigentümerschaft einer Resource unter Umständen jegliche Rechte eben dieser verlieren. Die Rechte werden unwiderruflich und sofort an den neuen Eigentümer übertragen.
+
+
+
+
+
Eigentümerschaft übertragen an
+
+
+
+
+
+ Eigentümerschaft nicht übertragen
+ {{org.name}}
+
+
+ Eigentümerschaft nicht übertragen
+ {{org.name}}
+
+
+
+
aktuelle Eigentümer-Organisationseinheit
+
{{kommonitorIndicatorDataExchangeService.getAccessControlById(currentIndicatorDataset?.ownerId)?.name}}
+
+
ACHTUNG: Sie sind dabei, die Eigentümerschaft an diesem Datensatz zu ändern.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ×
+
Zugriffsschutz und Eigentümerschaft aktualisiert
+ Erfolgreiche Aktualisierung des Zugriffsschutzes und der Eigentümerschaft für Indikator '{{currentIndicatorDataset?.indicatorName}}'.
+
+ sowie
+
+ Erfolgreiche Aktualisierung des Zugriffsschutzes für verknüpfte Raumebene '{{targetApplicableSpatialUnit?.spatialUnitName}}'
+
+
+
+
+
×
+
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/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..6cbf303f0
--- /dev/null
+++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditIndicatorSpatialUnitRolesModal/indicator-edit-indicator-spatial-unit-roles-modal.component.ts
@@ -0,0 +1,845 @@
+import { Component, OnInit, ViewChild } from '@angular/core';
+import { NgbActiveModal } 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 { KommonitorIndicatorDataExchangeService } from 'services/adminIndicatorUnit/kommonitor-data-exchange.service';
+import { MultiStepHelperServiceService } from 'services/multi-step-helper-service/multi-step-helper-service.service';
+import { HttpClient } from '@angular/common/http';
+import { AgGridAngular } from 'ag-grid-angular';
+import { ColDef, GridOptions, GridApi, ColumnApi, GridReadyEvent } from 'ag-grid-community';
+
+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('indicatorEditRoleManagementTable', { static: true }) indicatorEditRoleManagementTable!: AgGridAngular;
+ @ViewChild('indicatorEditIndicatorSpatialUnitsRoleManagementTable', { static: true }) indicatorEditIndicatorSpatialUnitsRoleManagementTable!: AgGridAngular;
+
+ // Form data
+ currentIndicatorDataset: any;
+ targetApplicableSpatialUnit: any;
+
+ // Role management tables - AG Grid Angular
+ public indicatorMetadataColumnDefs: ColDef[] = [];
+ public indicatorMetadataRowData: any[] = [];
+ public indicatorMetadataGridOptions: GridOptions = {};
+ public indicatorSpatialUnitColumnDefs: ColDef[] = [];
+ public indicatorSpatialUnitRowData: any[] = [];
+ public indicatorSpatialUnitGridOptions: GridOptions = {};
+
+ // Grid APIs
+ public indicatorMetadataGridApi!: GridApi;
+ public indicatorSpatialUnitGridApi!: GridApi;
+
+ // Messages
+ successMessagePart: string = '';
+ errorMessagePart: string = '';
+
+ // Form controls
+ ownerOrgFilter: string = '';
+ ownerOrganization: any;
+ activeRolesOnly: boolean = true;
+ activeConnectedRolesOnly: boolean = false;
+ permissions: any[] = [];
+ resourcesCreatorRights: any[] = [];
+
+ // Loading states
+ loadingData: boolean = false;
+
+ // Multi-step form
+ currentStep: number = 1;
+ totalSteps: number = 3;
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ private broadcastService: BroadcastService,
+ private http: HttpClient,
+ private dataExchangeService: DataExchangeService,
+ private dataGridHelperService: KommonitorIndicatorDataGridHelperService,
+ public kommonitorIndicatorDataExchangeService: KommonitorIndicatorDataExchangeService,
+ private multiStepHelperService: MultiStepHelperServiceService
+ ) {}
+
+ ngOnInit(): void {
+ this.setupEventListeners();
+ this.initializeForm();
+
+ // Load access control data if not already loaded
+ this.loadAccessControlData();
+
+ // If currentIndicatorDataset is already set (from parent component), initialize form
+ if (this.currentIndicatorDataset) {
+ this.onEditIndicatorSpatialUnitRoles(this.currentIndicatorDataset);
+ }
+ }
+
+ private async loadAccessControlData(): Promise {
+ try {
+ await this.kommonitorIndicatorDataExchangeService.fetchAccessControlMetadata();
+
+ // If no access control data is loaded, create some test data for development
+ if (!this.kommonitorIndicatorDataExchangeService.accessControl || this.kommonitorIndicatorDataExchangeService.accessControl.length === 0) {
+ this.createTestAccessControlData();
+ }
+ } catch (error) {
+ // Create test data as fallback
+ this.createTestAccessControlData();
+ }
+ }
+
+ /**
+ * Create test access control data for development
+ */
+ private createTestAccessControlData(): void {
+ const testData = [
+ {
+ organizationalUnitId: 'test-org-1',
+ name: 'Test Organization 1',
+ permissions: [
+ {
+ permissionId: 'viewer-perm-1',
+ permissionLevel: 'viewer',
+ isChecked: false
+ },
+ {
+ permissionId: 'editor-perm-1',
+ permissionLevel: 'editor',
+ isChecked: false
+ },
+ {
+ permissionId: 'creator-perm-1',
+ permissionLevel: 'creator',
+ isChecked: false
+ }
+ ],
+ datasetOwner: false
+ },
+ {
+ organizationalUnitId: 'test-org-2',
+ name: 'Test Organization 2',
+ permissions: [
+ {
+ permissionId: 'viewer-perm-2',
+ permissionLevel: 'viewer',
+ isChecked: false
+ },
+ {
+ permissionId: 'editor-perm-2',
+ permissionLevel: 'editor',
+ isChecked: false
+ },
+ {
+ permissionId: 'creator-perm-2',
+ permissionLevel: 'creator',
+ isChecked: false
+ }
+ ],
+ datasetOwner: false
+ }
+ ];
+
+ this.kommonitorIndicatorDataExchangeService.accessControl = testData;
+ }
+
+ private setupEventListeners(): void {
+ // Listen for available roles update event
+ this.broadcastService.currentBroadcastMsg.subscribe((data: any) => {
+ if (data.msg === 'availableRolesUpdate') {
+ this.refreshRoleManagementTable_indicatorMetadata();
+ this.refreshRoleManagementTable_indicatorSpatialUnitTimeseries();
+ }
+ });
+ }
+
+ private initializeForm(): void {
+ this.resetIndicatorEditIndicatorSpatialUnitRolesForm();
+ }
+
+ async onEditIndicatorSpatialUnitRoles(indicatorDataset: any): Promise {
+ this.currentIndicatorDataset = indicatorDataset;
+ this.prepareCreatorList();
+
+ // Ensure access control data is loaded
+ await this.loadAccessControlData();
+
+ // Fetch the indicator data with permissions if not already present
+ if (!this.currentIndicatorDataset.permissions) {
+ this.fetchIndicatorWithPermissions();
+ } else {
+ this.resetIndicatorEditIndicatorSpatialUnitRolesForm();
+ }
+
+ // Ensure spatial unit is set after form reset
+ setTimeout(() => {
+ this.ensureSpatialUnitIsSet();
+ }, 100);
+ }
+
+ private async fetchIndicatorWithPermissions(): Promise {
+ // Ensure access control data is loaded first
+ await this.loadAccessControlData();
+
+ this.http.get(
+ this.kommonitorIndicatorDataExchangeService.baseUrlToKomMonitorDataAPI + "/indicators/" + this.currentIndicatorDataset.indicatorId + "/without-geometry"
+ ).subscribe({
+ next: async (response: any) => {
+ this.currentIndicatorDataset = response;
+ this.resetIndicatorEditIndicatorSpatialUnitRolesForm();
+ },
+ error: (error: any) => {
+ console.error('Error fetching indicator with permissions:', error);
+ // Fallback to using the original data
+ this.resetIndicatorEditIndicatorSpatialUnitRolesForm();
+ }
+ });
+ }
+
+ closeModal(): void {
+ this.activeModal.dismiss();
+ }
+
+ prepareCreatorList(): void {
+ if (this.kommonitorIndicatorDataExchangeService.currentKeycloakLoginRoles.length > 0) {
+ let creatorRights: string[] = [];
+ let creatorRightsChildren: string[] = [];
+
+ this.kommonitorIndicatorDataExchangeService.currentKeycloakLoginRoles.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.kommonitorIndicatorDataExchangeService.accessControl.filter((elem: any) => creatorRights.includes(elem.name));
+ }
+ }
+
+ gatherCreatorRightsChildren(creatorRights: string[], creatorRightsChildren: string[]): void {
+ if (creatorRightsChildren.length > 0) {
+ this.kommonitorIndicatorDataExchangeService.accessControl
+ .filter((elem: any) => creatorRightsChildren.includes(elem.name))
+ .flatMap((res: any) => res.children)
+ .forEach((child: any) => {
+ this.kommonitorIndicatorDataExchangeService.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();
+ }
+
+ /**
+ * Ensure spatial unit is set for the grid to be enabled
+ */
+ private ensureSpatialUnitIsSet(): void {
+ // Use the first applicable spatial unit from the indicator dataset
+ if (!this.targetApplicableSpatialUnit &&
+ this.currentIndicatorDataset?.applicableSpatialUnits?.length > 0) {
+ this.targetApplicableSpatialUnit = this.currentIndicatorDataset.applicableSpatialUnits[0];
+ }
+ }
+
+ refreshRoleManagementTable_indicatorMetadata(): void {
+ // Ensure access control data is loaded before proceeding
+ if (!this.kommonitorIndicatorDataExchangeService.accessControl || this.kommonitorIndicatorDataExchangeService.accessControl.length === 0) {
+ this.loadAccessControlData().then(() => {
+ this.refreshRoleManagementTable_indicatorMetadata();
+ });
+ return;
+ }
+
+ // Use current user's login role IDs as initial permissions (like in features modal)
+ let permissions = this.kommonitorIndicatorDataExchangeService.getCurrentKomMonitorLoginRoleIds();
+
+ // If we have current indicator dataset permissions, use those instead
+ if (this.currentIndicatorDataset && this.currentIndicatorDataset.permissions) {
+ permissions = this.currentIndicatorDataset.permissions;
+ }
+
+ if (this.currentIndicatorDataset) {
+ const accessControl = this.kommonitorIndicatorDataExchangeService.getAccessControlById(this.currentIndicatorDataset.ownerId);
+
+ if (accessControl && accessControl.permissions) {
+ const permissionIds_ownerUnit = accessControl.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.kommonitorIndicatorDataExchangeService.accessControl.forEach((item: any) => {
+ if (this.currentIndicatorDataset) {
+ if (item.organizationalUnitId == this.currentIndicatorDataset.ownerId) {
+ item.datasetOwner = true;
+ } else {
+ item.datasetOwner = false;
+ }
+ }
+ });
+
+ // Build role management grid using AG Grid Angular
+ this.buildIndicatorMetadataGrid(this.kommonitorIndicatorDataExchangeService.accessControl, permissions);
+ }
+
+ refreshRoleManagementTable_indicatorSpatialUnitTimeseries(): void {
+ // Ensure access control data is loaded before proceeding
+ if (!this.kommonitorIndicatorDataExchangeService.accessControl || this.kommonitorIndicatorDataExchangeService.accessControl.length === 0) {
+ this.loadAccessControlData().then(() => {
+ this.refreshRoleManagementTable_indicatorSpatialUnitTimeseries();
+ });
+ return;
+ }
+
+ // Use current user's login role IDs as initial permissions (like in features modal)
+ let permissions = this.kommonitorIndicatorDataExchangeService.getCurrentKomMonitorLoginRoleIds();
+
+ // If we have target applicable spatial unit permissions, use those instead
+ if (this.targetApplicableSpatialUnit && this.targetApplicableSpatialUnit.permissions) {
+ permissions = this.targetApplicableSpatialUnit.permissions;
+ }
+
+ if (this.currentIndicatorDataset) {
+ const accessControl = this.kommonitorIndicatorDataExchangeService.getAccessControlById(this.currentIndicatorDataset.ownerId);
+
+ if (accessControl && accessControl.permissions) {
+ const permissionIds_ownerUnit = accessControl.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.kommonitorIndicatorDataExchangeService.accessControl.forEach((item: any) => {
+ if (this.currentIndicatorDataset) {
+ if (item.organizationalUnitId == this.currentIndicatorDataset.ownerId) {
+ item.datasetOwner = true;
+ } else {
+ item.datasetOwner = false;
+ }
+ }
+ });
+
+ // Handle active connected roles only filter - show all by default, filter only when explicitly requested
+ if (this.targetApplicableSpatialUnit && this.targetApplicableSpatialUnit.permissions) {
+ let connectedAccess = this.kommonitorIndicatorDataExchangeService.accessControl;
+
+ // Only apply filtering if explicitly enabled AND there are permissions to filter by
+ if (this.targetApplicableSpatialUnit.permissions.length > 0 && this.activeConnectedRolesOnly) {
+ connectedAccess = this.kommonitorIndicatorDataExchangeService.accessControl.filter((unit: any) => {
+ // Check if this unit has any permissions that match the spatial unit permissions
+ const matchingPermissions = unit.permissions.filter((unitPermission: any) =>
+ this.targetApplicableSpatialUnit.permissions.includes(unitPermission.permissionId)
+ );
+ return matchingPermissions.length > 0;
+ });
+ }
+
+ this.buildIndicatorSpatialUnitGrid(connectedAccess, permissions);
+ } else {
+ this.activeConnectedRolesOnly = false;
+ this.buildIndicatorSpatialUnitGrid(this.kommonitorIndicatorDataExchangeService.accessControl, permissions);
+ }
+ }
+
+ private buildIndicatorMetadataGrid(accessControl: any[], permissions: string[]): void {
+ // Build grid options
+ this.indicatorMetadataGridOptions = {
+ defaultColDef: {
+ sortable: true,
+ filter: true,
+ resizable: true
+ },
+ pagination: true,
+ paginationPageSize: 10
+ };
+
+ // Build column definitions
+ this.indicatorMetadataColumnDefs = this.buildRoleManagementColumnDefs();
+
+ // Build row data
+ this.indicatorMetadataRowData = this.buildRoleManagementRowData(accessControl, permissions);
+ }
+
+ private buildIndicatorSpatialUnitGrid(accessControl: any[], permissions: string[]): void {
+ // Build grid options
+ this.indicatorSpatialUnitGridOptions = {
+ defaultColDef: {
+ sortable: true,
+ filter: true,
+ resizable: true
+ },
+ pagination: true,
+ paginationPageSize: 10
+ };
+
+ // Build column definitions
+ this.indicatorSpatialUnitColumnDefs = this.buildRoleManagementColumnDefs();
+
+ // Build row data
+ this.indicatorSpatialUnitRowData = this.buildRoleManagementRowData(accessControl, permissions);
+
+ // Force refresh the grid if API is available
+ setTimeout(() => {
+ this.forceSpatialUnitGridRefresh();
+ }, 100);
+ }
+
+ private buildRoleManagementColumnDefs(): ColDef[] {
+ const columnDefs: ColDef[] = [
+ {
+ headerName: 'Organisationseinheit',
+ field: "organizationalUnitName",
+ pinned: 'left',
+ minWidth: 200
+ }
+ ];
+
+ // Add permission columns - using the correct German headers from AngularJS
+ columnDefs.push(
+ {
+ headerName: 'lesen',
+ field: 'viewer',
+ maxWidth: 100,
+ cellRenderer: this.dataGridHelperService.CheckboxRenderer_viewer
+ },
+ {
+ headerName: 'editieren',
+ field: 'editor',
+ maxWidth: 100,
+ cellRenderer: this.dataGridHelperService.CheckboxRenderer_editor
+ }
+ );
+
+ return columnDefs;
+ }
+
+ private buildRoleManagementRowData(accessControl: any[], permissions: string[]): any[] {
+ if (!accessControl || accessControl.length === 0) {
+ return [];
+ }
+
+ // Create a deep copy of the data (like AngularJS)
+ let data = JSON.parse(JSON.stringify(accessControl));
+
+ // Process each item (like AngularJS)
+ for (let elem of data) {
+ // Handle 'public' name translation (like AngularJS)
+ if (elem.name === 'public') {
+ elem.name = 'Öffentlicher Zugriff';
+ }
+
+ // Process permissions
+ for (let permission of elem.permissions) {
+ permission.isChecked = false;
+ if (permissions && permissions.includes(permission.permissionId)) {
+ permission.isChecked = true;
+ }
+ }
+ }
+
+ // Apply special ordering logic (like AngularJS)
+ let array: any[] = [];
+
+ // Always put first 2 items at the top
+ if (data.length > 0) {
+ array.push(data[0]);
+ }
+ if (data.length > 1) {
+ array.push(data[1]);
+ }
+
+ // Remove first 2 items and sort the rest
+ data.splice(0, 2);
+ data.sort(function (a: any, b: any) {
+ if (a.name < b.name) {
+ return -1;
+ }
+ if (a.name > b.name) {
+ return 1;
+ }
+ return 0;
+ });
+
+ // Combine fixed first 2 + sorted rest
+ array = array.concat(data);
+
+ // Convert to the format expected by the grid
+ return array.map(item => {
+ // Extract permission IDs from the permissions array
+ const viewerPermission = item.permissions?.find((p: any) => p.permissionLevel === 'viewer');
+ const editorPermission = item.permissions?.find((p: any) => p.permissionLevel === 'editor');
+ const creatorPermission = item.permissions?.find((p: any) => p.permissionLevel === 'creator');
+
+ const viewerPermissionId = viewerPermission?.permissionId || '';
+ const editorPermissionId = editorPermission?.permissionId || '';
+ const creatorPermissionId = creatorPermission?.permissionId || '';
+
+ const result = {
+ organizationalUnitId: item.organizationalUnitId,
+ organizationalUnitName: item.name,
+ viewer: permissions.includes(viewerPermissionId),
+ editor: permissions.includes(editorPermissionId),
+ creator: permissions.includes(creatorPermissionId),
+ datasetOwner: item.datasetOwner || false,
+ // Store the permission IDs for later use
+ viewerPermissionId: viewerPermissionId,
+ editorPermissionId: editorPermissionId,
+ creatorPermissionId: creatorPermissionId
+ };
+
+ return result;
+ });
+ }
+
+ onActiveConnectedRolesOnlyChange(): void {
+ this.refreshRoleManagementTable_indicatorSpatialUnitTimeseries();
+ }
+
+ onActiveRolesOnlyChange(): void {
+ this.refreshRoleManagementTable_indicatorMetadata();
+ }
+
+ onChangeOwner(ownerOrganization: any): void {
+ this.ownerOrganization = ownerOrganization;
+ this.refreshRoles(this.ownerOrganization);
+ }
+
+ refreshRoles(orgUnitId: string): void {
+ let permissionIds_ownerUnit = orgUnitId ?
+ this.kommonitorIndicatorDataExchangeService.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.kommonitorIndicatorDataExchangeService.accessControl.forEach((item: any) => {
+ if (item.organizationalUnitId == orgUnitId) {
+ item.datasetOwner = true;
+ } else {
+ item.datasetOwner = false;
+ }
+ });
+
+ this.buildIndicatorMetadataGrid(this.kommonitorIndicatorDataExchangeService.accessControl, permissionIds_ownerUnit);
+ this.buildIndicatorSpatialUnitGrid(this.kommonitorIndicatorDataExchangeService.accessControl, permissionIds_ownerUnit);
+ }
+
+ 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.getSelectedRoleIds_roleManagementGrid(this.indicatorMetadataGridApi),
+ "isPublic": this.currentIndicatorDataset.isPublic
+ };
+
+ this.http.put(
+ this.kommonitorIndicatorDataExchangeService.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.kommonitorIndicatorDataExchangeService.syntaxHighlightJSON(error.data);
+ } else {
+ this.errorMessagePart += this.kommonitorIndicatorDataExchangeService.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.kommonitorIndicatorDataExchangeService.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.kommonitorIndicatorDataExchangeService.syntaxHighlightJSON(error.data);
+ } else {
+ this.errorMessagePart += this.kommonitorIndicatorDataExchangeService.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.kommonitorIndicatorDataExchangeService.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.kommonitorIndicatorDataExchangeService.syntaxHighlightJSON(error.data);
+ } else {
+ this.errorMessagePart += this.kommonitorIndicatorDataExchangeService.syntaxHighlightJSON(error);
+ }
+ this.showErrorAlert();
+ this.loadingData = false;
+ }
+ });
+ });
+ }
+ }
+
+ executeRequest_indicatorSpatialUnitRoles(): void {
+ let putBody = {
+ "permissions": this.getSelectedRoleIds_roleManagementGrid(this.indicatorSpatialUnitGridApi),
+ "isPublic": this.targetApplicableSpatialUnit.isPublic
+ };
+
+ this.loadingData = true;
+
+ this.http.put(
+ this.kommonitorIndicatorDataExchangeService.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.kommonitorIndicatorDataExchangeService.syntaxHighlightJSON(error.data);
+ } else {
+ this.errorMessagePart += this.kommonitorIndicatorDataExchangeService.syntaxHighlightJSON(error);
+ }
+ this.showErrorAlert();
+ this.loadingData = false;
+ }
+ });
+ }
+
+ onChangeSelectedSpatialUnit(targetApplicableSpatialUnit: any): void {
+ console.log('onChangeSelectedSpatialUnit called with:', targetApplicableSpatialUnit);
+ this.targetApplicableSpatialUnit = targetApplicableSpatialUnit;
+
+ // Ensure access control data is loaded before refreshing
+ if (!this.kommonitorIndicatorDataExchangeService.accessControl || this.kommonitorIndicatorDataExchangeService.accessControl.length === 0) {
+ this.loadAccessControlData().then(() => {
+ this.refreshRoleManagementTable_indicatorSpatialUnitTimeseries();
+ });
+ } else {
+ 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;
+
+ // Ensure grids are refreshed when navigating to specific steps
+ if (step === 2) {
+ // Refresh step 2 grid when navigating to it
+ setTimeout(() => {
+ this.refreshRoleManagementTable_indicatorSpatialUnitTimeseries();
+ }, 100);
+ } else if (step === 1) {
+ // Refresh step 1 grid when navigating to it
+ setTimeout(() => {
+ this.refreshRoleManagementTable_indicatorMetadata();
+ }, 100);
+ }
+ }
+ }
+
+ // AG Grid event handlers
+ onIndicatorMetadataGridReady(event: GridReadyEvent): void {
+ this.indicatorMetadataGridApi = event.api;
+ }
+
+ onIndicatorSpatialUnitGridReady(event: GridReadyEvent): void {
+ console.log('onIndicatorSpatialUnitGridReady called');
+ this.indicatorSpatialUnitGridApi = event.api;
+ console.log('Spatial unit grid API set:', this.indicatorSpatialUnitGridApi);
+
+ // Force refresh of the grid data if we have row data
+ if (this.indicatorSpatialUnitRowData && this.indicatorSpatialUnitRowData.length > 0) {
+ console.log('Setting row data to spatial unit grid:', this.indicatorSpatialUnitRowData);
+ this.indicatorSpatialUnitGridApi.setRowData(this.indicatorSpatialUnitRowData);
+ }
+ }
+
+ // Helper method to get selected role IDs from AG Grid
+ private getSelectedRoleIds_roleManagementGrid(gridApi: GridApi): string[] {
+ const selectedRoleIds: string[] = [];
+
+ if (gridApi && gridApi.getRenderedNodes) {
+ const rowData = gridApi.getRenderedNodes().map(node => node.data);
+
+ rowData.forEach(row => {
+ if (row.viewer && row.viewerPermissionId) {
+ selectedRoleIds.push(row.viewerPermissionId);
+ }
+ if (row.editor && row.editorPermissionId) {
+ selectedRoleIds.push(row.editorPermissionId);
+ }
+ if (row.creator && row.creatorPermissionId) {
+ selectedRoleIds.push(row.creatorPermissionId);
+ }
+ });
+ }
+
+ return selectedRoleIds;
+ }
+
+ // Alert management
+ showSuccessAlert(): void {
+ $("#indicatorEditIndicatorSpatialUnitRolesSuccessAlert").show();
+ }
+
+ hideSuccessAlert(): void {
+ $("#indicatorEditIndicatorSpatialUnitRolesSuccessAlert").hide();
+ }
+
+ showErrorAlert(): void {
+ $("#indicatorEditIndicatorSpatialUnitRolesErrorAlert").show();
+ }
+
+ hideErrorAlert(): void {
+ $("#indicatorEditIndicatorSpatialUnitRolesErrorAlert").hide();
+ }
+
+ /**
+ * Check if spatial unit grid should be enabled
+ */
+ isSpatialUnitGridEnabled(): boolean {
+ return !!this.targetApplicableSpatialUnit &&
+ !!this.kommonitorIndicatorDataExchangeService.accessControl &&
+ this.kommonitorIndicatorDataExchangeService.accessControl.length > 0;
+ }
+
+ /**
+ * Check if spatial unit grid has data
+ */
+ hasSpatialUnitGridData(): boolean {
+ return this.indicatorSpatialUnitRowData && this.indicatorSpatialUnitRowData.length > 0;
+ }
+
+ /**
+ * Get spatial unit grid data count
+ */
+ getSpatialUnitGridDataCount(): number {
+ return this.indicatorSpatialUnitRowData ? this.indicatorSpatialUnitRowData.length : 0;
+ }
+
+ /**
+ * Force refresh spatial unit grid data
+ */
+ forceSpatialUnitGridRefresh(): void {
+ if (this.indicatorSpatialUnitGridApi && this.indicatorSpatialUnitRowData) {
+ console.log('Force refreshing spatial unit grid with data:', this.indicatorSpatialUnitRowData);
+ this.indicatorSpatialUnitGridApi.setRowData(this.indicatorSpatialUnitRowData);
+ this.indicatorSpatialUnitGridApi.refreshCells({ force: true });
+ }
+ }
+
+ /**
+ * Temporarily disable filtering to show all data
+ */
+ showAllData(): void {
+ console.log('Showing all data without filtering');
+ this.activeConnectedRolesOnly = false;
+ this.refreshRoleManagementTable_indicatorSpatialUnitTimeseries();
+ }
+}
\ 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..81018ac25
--- /dev/null
+++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditMetadataModal/indicator-edit-metadata-modal.component.css
@@ -0,0 +1,1399 @@
+/* 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;
+}
+
+/* Ensure select text is visible */
+select.form-control {
+ color: #333;
+ background-color: #fff;
+ line-height: 1.5;
+}
+
+select.form-control option {
+ color: #333;
+ background-color: #fff;
+}
+
+.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;
+}
+
+/* Multi-Step Form Styles */
+.multiStepForm {
+ margin-bottom: 0px;
+}
+
+/*progressbar*/
+#progressbar {
+ margin-bottom: 10px;
+ overflow: hidden;
+ /*CSS counters to number the steps*/
+ counter-reset: step;
+ /* z-index: 10000; */
+}
+
+#progressbar li {
+ list-style-type: none;
+ color: black;
+ text-transform: uppercase;
+ font-size: 9px;
+ /* width: 33.33%; */
+ float: left;
+ position: relative;
+ letter-spacing: 1px;
+ /* transform-style: preserve-3d; */
+ /* z-index: 1; */
+ 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);
+ /* z-index: +1; */
+}
+
+/*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;
+ /* transform: translateZ(-2px); */
+}
+
+#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;
+}
+
+/* Enhanced hover effects for better UX */
+#progressbar li:hover {
+ color: var(--kommonitor-primary);
+}
+
+#progressbar li:hover:before {
+ background: var(--kommonitor-primary);
+ transform: scale(1.1);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+}
+
+#progressbar li:active:before {
+ transform: scale(1.05);
+}
+
+/* Completed steps */
+#progressbar li.completed:before {
+ background: var(--kommonitor-primary);
+}
+
+#progressbar li.completed:after {
+ background: var(--kommonitor-primary);
+}
+
+/* Error states */
+#progressbar li.error:before {
+ background: #e74c3c;
+}
+
+#progressbar li.error:after {
+ background: #e74c3c;
+}
+
+#progressbar li.error {
+ color: #e74c3c;
+}
+
+/* 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);
+ color: white;
+ border: 0 none;
+ border-radius: 5px;
+ cursor: pointer;
+ padding: 10px 5px;
+ margin: 5px 5px 10px 10px;
+ text-decoration: none;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.3s ease;
+}
+
+.action-button:hover, .action-button:focus {
+ background: var(--kommonitor-primary);
+ color: white;
+ text-decoration: none;
+ box-shadow: 0 0 0 2px white, 0 0 0 3px var(--kommonitor-primary);
+}
+
+.action-button-previous {
+ width: 150px;
+ background: #95a5a6;
+ color: white;
+ border: 0 none;
+ border-radius: 5px;
+ cursor: pointer;
+ padding: 10px 5px;
+ margin: 5px 5px 10px 10px;
+ text-decoration: none;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.3s ease;
+}
+
+.action-button-previous:hover, .action-button-previous:focus {
+ background: #7f8c8d;
+ color: white;
+ text-decoration: none;
+ box-shadow: 0 0 0 2px white, 0 0 0 3px #7f8c8d;
+}
+
+.button-container {
+ text-align: center;
+ margin-top: 20px;
+}
+
+/* Modal footer button styles to match AngularJS */
+.modal-footer .pull-left {
+ float: left;
+}
+
+.modal-footer .btn {
+ margin-left: 5px;
+}
+
+.modal-footer .btn:first-child {
+ margin-left: 0;
+}
+
+/* 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%;
+}
+
+/* Help Block Styles */
+.help-block {
+ color: #737373;
+ font-size: 12px;
+ margin-top: 5px;
+}
+
+.help-block p {
+ margin-bottom: 5px;
+}
+
+.help-block.with-errors {
+ color: #a94442;
+}
+
+/* Loading Overlay */
+.loading-overlay-admin-panel {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(255, 255, 255, 0.8);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+}
+
+.loading-overlay-admin-panel .glyphicon {
+ font-size: 24px;
+ color: #007bff;
+}
+
+/* Vertical Align */
+.vertical-align {
+ display: flex;
+ align-items: center;
+}
+
+.vertical-align > div {
+ display: flex;
+ align-items: center;
+}
+
+/* Let form-controls stretch inside flex rows */
+.vertical-align .form-group {
+ width: 100%;
+ flex: 1 1 auto;
+}
+
+/* Margin Utilities */
+.margin-right {
+ margin-right: 10px;
+}
+
+/* Form Validation */
+.form-control.is-invalid {
+ border-color: #dc3545;
+ box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
+}
+
+.invalid-feedback {
+ display: block;
+ width: 100%;
+ margin-top: 0.25rem;
+ font-size: 80%;
+ color: #dc3545;
+}
+
+/* Alert Styles */
+.alert {
+ padding: 15px;
+ margin-bottom: 20px;
+ border: 1px solid transparent;
+ border-radius: 4px;
+}
+
+.alert-success {
+ color: #155724;
+ background-color: #d4edda;
+ border-color: #c3e6cb;
+}
+
+.alert-danger {
+ color: #721c24;
+ background-color: #f8d7da;
+ border-color: #f5c6cb;
+}
+
+.alert-warning {
+ color: #856404;
+ background-color: #fff3cd;
+ border-color: #ffeaa7;
+}
+
+.alert-info {
+ color: #0c5460;
+ background-color: #d1ecf1;
+ border-color: #bee5eb;
+}
+
+.alert-dismissible {
+ padding-right: 35px;
+}
+
+.alert-dismissible .close {
+ position: relative;
+ top: -2px;
+ right: -21px;
+ color: inherit;
+}
+
+/* Modal Styles */
+.modal-header {
+ padding: 15px;
+ border-bottom: 1px solid #e5e5e5;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.modal-title {
+ margin: 0;
+ line-height: 1.42857143;
+}
+
+.modal-body {
+ position: relative;
+ padding: 15px;
+}
+
+.modal-footer {
+ padding: 15px;
+ text-align: right;
+ border-top: 1px solid #e5e5e5;
+}
+
+/* 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-primary {
+ color: #fff;
+ background-color: #337ab7;
+ border-color: #2e6da4;
+}
+
+.btn-secondary {
+ color: #fff;
+ background-color: #6c757d;
+ border-color: #6c757d;
+}
+
+.btn-success {
+ color: #fff;
+ background-color: #5cb85c;
+ border-color: #4cae4c;
+}
+
+.btn-danger {
+ color: #fff;
+ background-color: #d9534f;
+ border-color: #d43f3a;
+}
+
+.btn:disabled {
+ opacity: 0.65;
+ cursor: not-allowed;
+}
+
+/* Spinner */
+.spinner-border {
+ display: inline-block;
+ width: 1rem;
+ height: 1rem;
+ vertical-align: text-bottom;
+ border: 0.125em solid currentColor;
+ border-right-color: transparent;
+ border-radius: 50%;
+ animation: spinner-border 0.75s linear infinite;
+}
+
+.spinner-border-sm {
+ width: 0.875rem;
+ height: 0.875rem;
+ border-width: 0.125em;
+}
+
+@keyframes spinner-border {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+/* Icon Spinning */
+.icon-spin {
+ animation: spin 2s linear infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+/* Enhanced Classification Method Selector */
+.classification-method-selector {
+ margin-bottom: 15px;
+}
+
+.method-options {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 15px;
+ margin-bottom: 15px;
+}
+
+.method-option {
+ display: flex;
+ align-items: center;
+ padding: 15px;
+ border: 2px solid #e0e0e0;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ background-color: #fafafa;
+}
+
+.method-option:hover {
+ border-color: #007bff;
+ background-color: #f8f9fa;
+ transform: translateY(-2px);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+.method-option.selected {
+ border-color: #007bff;
+ background-color: #e3f2fd;
+ box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2);
+}
+
+.method-icon {
+ margin-right: 15px;
+ flex-shrink: 0;
+}
+
+.method-icon-img {
+ width: 40px;
+ height: 40px;
+ object-fit: contain;
+}
+
+.method-info {
+ flex: 1;
+}
+
+.method-name {
+ font-weight: 600;
+ font-size: 14px;
+ color: #333;
+ margin-bottom: 5px;
+}
+
+.method-description {
+ font-size: 12px;
+ color: #666;
+ line-height: 1.4;
+}
+
+.fallback-select {
+ display: none;
+}
+
+/* Color Palette Dropdown Styles */
+.dropdown {
+ position: relative;
+}
+
+.dropdown-menu {
+ display: none;
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 1000;
+ float: left;
+ min-width: 10rem;
+ padding: 0.5rem 0;
+ margin: 0.125rem 0 0;
+ font-size: 1rem;
+ color: #212529;
+ text-align: left;
+ list-style: none;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ border-radius: 0.25rem;
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.175);
+}
+
+.dropdown-menu.show {
+ display: block;
+}
+
+.dropdown-item {
+ display: block;
+ width: 100%;
+ padding: 0.25rem 1rem;
+ clear: both;
+ font-weight: 400;
+ color: #212529;
+ text-align: inherit;
+ text-decoration: none;
+ white-space: nowrap;
+ background-color: transparent;
+ border: 0;
+}
+
+.dropdown-item:hover,
+.dropdown-item:focus {
+ color: #1e2125;
+ background-color: #e9ecef;
+}
+
+.dropdown-item:active {
+ color: #fff;
+ text-decoration: none;
+ background-color: #0d6efd;
+}
+
+.dropdown-menu-center {
+ right: auto;
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+/* Color palette specific styles */
+.color-palette-item {
+ padding: 8px 12px;
+ border-bottom: 1px solid #eee;
+}
+
+.color-palette-item:last-child {
+ border-bottom: none;
+}
+
+.color-palette-item:hover {
+ background-color: #f8f9fa;
+}
+
+.color-palette-svg {
+ display: inline-block;
+ vertical-align: middle;
+ margin-right: 10px;
+}
+
+.color-palette-name {
+ font-size: 12px;
+ color: #666;
+ margin-left: 10px;
+}
+
+/* Mobile responsive */
+@media (max-width: 768px) {
+ .method-options {
+ grid-template-columns: 1fr;
+ }
+
+ .method-option {
+ padding: 12px;
+ }
+
+ .method-icon-img {
+ width: 30px;
+ height: 30px;
+ }
+
+ .fallback-select {
+ display: block;
+ }
+
+ .method-options {
+ display: none;
+ }
+}
+
+/* Responsive adjustments for SVG previews */
+@media (max-width: 768px) {
+ .dropdown-menu-center {
+ columns: 2;
+ -webkit-columns: 2;
+ -moz-columns: 2;
+ }
+}
+
+/* Drop Zone Styles */
+.drop_zone {
+ border: 2px dashed #ccc;
+ border-radius: 8px;
+ padding: 40px 20px;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ background-color: #f9f9f9;
+ min-height: 200px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+.drop_zone:hover {
+ border-color: #007bff;
+ background-color: #f0f8ff;
+}
+
+.drop_zone.drag-over {
+ border-color: #28a745;
+ background-color: #d4edda;
+ transform: scale(1.02);
+}
+
+.drop_zone i {
+ color: #6c757d;
+ margin-bottom: 15px;
+}
+
+.drop_zone h4 {
+ margin: 0;
+ color: #495057;
+ font-size: 14px;
+}
+
+.drop_zone ul {
+ text-align: left;
+ margin-top: 15px;
+}
+
+.drop_zone li {
+ margin-bottom: 5px;
+ color: #6c757d;
+}
+
+/* File Upload Styles */
+.file-upload-info {
+ margin-top: 15px;
+ padding: 10px;
+ background-color: #e9ecef;
+ border-radius: 4px;
+ border: 1px solid #dee2e6;
+}
+
+.file-upload-info b {
+ color: #495057;
+}
+
+.file-upload-info i {
+ color: #6c757d;
+ font-style: italic;
+}
+
+/* Column Mapping Styles */
+.column-mapping-container {
+ margin-top: 20px;
+ padding: 15px;
+ background-color: #f8f9fa;
+ border-radius: 6px;
+ border: 1px solid #e9ecef;
+}
+
+.column-mapping-container label {
+ font-size: 11px;
+ font-weight: 600;
+ color: #495057;
+ margin-bottom: 5px;
+}
+
+.column-mapping-container select {
+ font-size: 11px;
+ padding: 4px 8px;
+ border: 1px solid #ced4da;
+ border-radius: 4px;
+}
+
+/* Load Button Styles */
+.load-button-container {
+ text-align: center;
+ margin-top: 20px;
+}
+
+.load-button-container .btn {
+ padding: 8px 20px;
+ font-size: 14px;
+}
+
+.load-button-container .btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+/* Data Grid Styles */
+#indicatorRegionalReferenceValuesManagementTable {
+ border: 1px solid #dee2e6;
+ border-radius: 6px;
+ overflow: hidden;
+}
+
+/* Validation and Feedback Styles */
+.validation-error {
+ color: #dc3545;
+ font-size: 12px;
+ margin-top: 5px;
+}
+
+.validation-success {
+ color: #28a745;
+ font-size: 12px;
+ margin-top: 5px;
+}
+
+.file-upload-status {
+ margin-top: 10px;
+ padding: 8px 12px;
+ border-radius: 4px;
+ font-size: 12px;
+}
+
+.file-upload-status.success {
+ background-color: #d4edda;
+ color: #155724;
+ border: 1px solid #c3e6cb;
+}
+
+.file-upload-status.error {
+ background-color: #f8d7da;
+ color: #721c24;
+ border: 1px solid #f5c6cb;
+}
+
+.file-upload-status.info {
+ background-color: #d1ecf1;
+ color: #0c5460;
+ border: 1px solid #bee5eb;
+}
+
+/* Column Mapping Container */
+.column-mapping-container {
+ margin-top: 20px;
+ padding: 15px;
+ background-color: #f8f9fa;
+ border-radius: 6px;
+ border: 1px solid #e9ecef;
+}
+
+.column-mapping-container label {
+ font-size: 11px;
+ font-weight: 600;
+ color: #495057;
+ margin-bottom: 5px;
+}
+
+.column-mapping-container select {
+ font-size: 11px;
+ padding: 4px 8px;
+ border: 1px solid #ced4da;
+ border-radius: 4px;
+}
+
+/* Load Button Styles */
+.load-button-container {
+ text-align: center;
+ margin-top: 20px;
+}
+
+.load-button-container .btn {
+ padding: 8px 20px;
+ font-size: 14px;
+}
+
+.load-button-container .btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+/* Classification Method Selector Styles */
+.classification-method-selector {
+ margin-bottom: 15px;
+}
+
+.method-options {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 15px;
+ margin-bottom: 15px;
+}
+
+.method-option {
+ display: flex;
+ align-items: center;
+ padding: 15px;
+ border: 2px solid #e0e0e0;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ background-color: #fafafa;
+}
+
+.method-option:hover {
+ border-color: #007bff;
+ background-color: #f8f9fa;
+ transform: translateY(-2px);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+.method-option.selected {
+ border-color: #007bff;
+ background-color: #e3f2fd;
+ box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2);
+}
+
+.method-icon {
+ margin-right: 15px;
+ flex-shrink: 0;
+}
+
+.method-icon-img {
+ width: 40px;
+ height: 40px;
+ object-fit: contain;
+}
+
+.method-info {
+ flex: 1;
+}
+
+.method-name {
+ font-weight: 600;
+ font-size: 14px;
+ color: #333;
+ margin-bottom: 5px;
+}
+
+.method-description {
+ font-size: 12px;
+ color: #666;
+ line-height: 1.4;
+}
+
+.fallback-select {
+ display: none;
+}
+
+/* Classification Tabs Styles */
+.nav-tabs-custom {
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ margin-top: 15px;
+}
+
+.nav-tabs-custom .nav-tabs {
+ border-bottom: 1px solid #ddd;
+ margin-bottom: 0;
+}
+
+.nav-tabs-custom .nav-tabs > li > a {
+ border: none;
+ border-radius: 0;
+ color: #555;
+ padding: 10px 15px;
+ text-decoration: none;
+}
+
+.nav-tabs-custom .nav-tabs > li > a:hover {
+ background-color: #f5f5f5;
+ border-color: transparent;
+}
+
+.nav-tabs-custom .nav-tabs > li.active > a {
+ background-color: #fff;
+ border: 1px solid #ddd;
+ border-bottom-color: transparent;
+ color: #333;
+ font-weight: 600;
+}
+
+.nav-tabs-custom .tab-content {
+ padding: 20px;
+ background-color: #fff;
+ border-top: none;
+}
+
+.tab-pane {
+ display: none;
+}
+
+.tab-pane.active {
+ display: block;
+}
+
+/* Tab Status Classes */
+.tab-completed {
+ background-color: #d4edda !important;
+ border-color: #c3e6cb !important;
+ color: #155724 !important;
+}
+
+.tab-error {
+ background-color: #f8d7da !important;
+ border-color: #f5c6cb !important;
+ color: #721c24 !important;
+}
+
+.active {
+ background-color: #007bff !important;
+ border-color: #007bff !important;
+ color: #fff !important;
+}
+
+/* Boxed Legend Styles */
+.boxedLegend {
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ padding: 15px;
+ background-color: #f9f9f9;
+}
+
+.boxedLegend i {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ border: 1px solid #ccc;
+ margin-right: 5px;
+}
+
+/* Mobile responsive */
+@media (max-width: 768px) {
+ .method-options {
+ grid-template-columns: 1fr;
+ }
+
+ .method-option {
+ padding: 12px;
+ }
+
+ .method-icon-img {
+ width: 30px;
+ height: 30px;
+ }
+
+ .fallback-select {
+ display: block;
+ }
+
+ .method-options {
+ display: none;
+ }
+}
\ 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..136862069
--- /dev/null
+++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditMetadataModal/indicator-edit-metadata-modal.component.html
@@ -0,0 +1,1168 @@
+
+
+
+
+
+
+
+
+
+
+ ×
+
+ Erfolg! {{successMessage}}
+
+
+
+
+
+
+
+
+
+
+ Debug: Keine Indikator-Daten verfügbar. currentIndicatorDataset ist null.
+
+
+
+
+
+
+
+ = 1"
+ [class.error]="currentStep === 1 && !isCurrentStepValid()"
+ (click)="goToStep(1)"
+ style="width: 16.66%;"
+ title="Klicken Sie hier, um zu Schritt 1 zu navigieren">Metadaten des Indikators
+ = 2"
+ [class.error]="currentStep === 2 && !isCurrentStepValid()"
+ (click)="goToStep(2)"
+ style="width: 16.66%;"
+ title="Klicken Sie hier, um zu Schritt 2 zu navigieren">Allgemeine Metadaten
+ = 3"
+ [class.error]="currentStep === 3 && !isCurrentStepValid()"
+ (click)="goToStep(3)"
+ style="width: 16.66%;"
+ title="Klicken Sie hier, um zu Schritt 3 zu navigieren">Themenhierarchie
+ = 4"
+ (click)="goToStep(4)"
+ style="width: 16.66%;"
+ title="Klicken Sie hier, um zu Schritt 4 zu navigieren">Referenzen zu Indikatoren/Georessourcen
+ = 5"
+ [class.error]="currentStep === 5 && !isCurrentStepValid()"
+ (click)="goToStep(5)"
+ style="width: 16.66%;"
+ title="Klicken Sie hier, um zu Schritt 5 zu navigieren">Klassifizierungsoptionen
+ = 6"
+ (click)="goToStep(6)"
+ style="width: 16.66%;"
+ title="Klicken Sie hier, um zu Schritt 6 zu navigieren">regionale Vergleichswerte
+
+
+
+
+
+ Metadaten des Indikators
+ Angaben über die Metadaten des Indikators
+ * = Pflichtfeld
+
+
+
+
+
+
+
+
+
+
+
+
+ Allgemeine Metadaten
+ Angaben über allgemeine Metadaten in KomMonitor
+ * = Pflichtfeld
+
+
+
+
+
+
+
+
+
+
+
+ Themenhierarchie
+ Angaben über die Themenhierarchie des Indikators
+
+ * = Pflichtfeld
+ Angabe der Themenhierarchie. Mindestens das Hauptthema muss gewählt werden. Vorhandene Unterthemen erscheinen nach Auswahl eines Hauptthemas. Bis zu vier Themen-Ebenen sind erlaubt.
+
+ Das Aufklappen der unteren Box ermöglicht die Verwaltung des Themenkatalogs.
+
+
+
+
+
0" class="form-group">
+
Unterthema - erste Ebene
+
+ -- Unterthema wählen --
+ {{subTopic.topicName}}
+
+
+
+
+
+
0" class="form-group">
+
Unterthema - zweite Ebene
+
+ -- Unterthema wählen --
+ {{subsubTopic.topicName}}
+
+
+
+
+
+
0" class="form-group">
+
Unterthema - dritte Ebene
+
+ -- Unterthema wählen --
+ {{subsubsubTopic.topicName}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Referenzen zu Indikatoren/Georessourcen (optional)
+ Angaben über Referenzen des Indikators auf andere Indikatoren oder Georessourcen
+
+ Hier werden Angaben über assoziierte Indikatoren und Georessourcen getätigt. Sie eignen sich dazu, im System Querverbindungen
+ zwischen Indikatoren und Georessourcen darzustellen und dem Nutzer so die Möglichkeit zu geben, einen Themenkomplex,
+ der durch mehrere Datensätze abgebildet wird, besser zu verstehen.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Übersicht der definierten Indikatoren-Referenzen
+
+
+
+ Editierfunktionen
+ ID des Indikators
+ Name des Indikators
+ Kürzel/Kennziffer
+ Beschreibung der Verknüpfung
+
+
+
+
+
+
+
+
+
+
+ {{indicatorReference.referencedIndicatorId}}
+ {{indicatorReference.referencedIndicatorName}}
+ {{indicatorReference.referencedIndicatorAbbreviation}}
+ {{indicatorReference.referencedIndicatorDescription}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Übersicht der definierten Georessourcen-Referenzen
+
+
+
+ Editierfunktionen
+ ID der Georessource
+ Name der Georessource
+ Beschreibung der Verknüpfung
+
+
+
+
+
+
+
+
+
+
+ {{georesourceReference.referencedGeoresourceId}}
+ {{georesourceReference.referencedGeoresourceName}}
+ {{georesourceReference.referencedGeoresourceDescription}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Klassifizierungsoptionen
+
+
+
+
Angaben über die Standard-Klassifizierung des Indikators
+
* = Pflichtfeld
+
+
+
+ Debug: Keine Farbpaletten geladen. Anzahl: {{colorbrewerPalettes.length}}
+
+
+
+
+
Sollte der Indikator negative und positive Werte enthalten, so wird in KomMonitor standardmäßig folgende zweifarbige Klassifizierung verwendet
+
+
+
+
+
+
+
Da ein dynamischer Indikatorentyp ('{{indicatorType?.displayName}}') konfiguriert wurde, ist hier keine Angabe erforderlich. Es wird folgende zweifarbige Standard-Klassifizierung verwendet
+
+
+
+
+
+
+
+
+
+
+ 0) && classificationMethod === 'regional_default'">
+
+
+
0) && classificationMethod === 'regional_default'">
+
Vollständig ausgefüllte Tabs werden grün hervorgehoben.
+
Fehlerhaft ausgefüllte Tabs werden rot hervorgehoben. Die Klassengrenzen müssen von niedrig nach hoch sortiert sein.
+
+
+
+
+
+
+
+
+
+ Vergleichswerte (optional)
+ Optionale Vergabe gesamtregionaler Vergleichswerte. Hierüber sind regionale Gesamtwerte als Summe oder Durchschnittswerte hinterlegbar.
+ Zur Wahrung der Datenkonsistenz können Vergleichswerte ausschließlich für bereits verfügbare Zeitschnitte hinterlegt werden.
+
+
+
+
+
+
+
+
+
+
+
+ Datei ausgewählt: {{file_regionalReferenceValuesImport.name}}
+
+
+
+ Angegebene Datei: -- keine --
+
+
+
+
+
+
+
+
+
+ Spaltenname Zeitstempel
+
+ {{property}}
+
+
+
+
+ Spaltenname Summenwert
+
+ {{property}}
+
+
+
+
+ Spaltenname Mittelwert
+
+ {{property}}
+
+
+
+
+ Spaltenname räumlich nicht zuordenbar
+
+ {{property}}
+
+
+
+
+
+
+
+
+
+
+
+ {{csvProcessingStatus.message}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Keine Daten verfügbar
+
Die Indikator-Daten konnten nicht geladen werden. Bitte versuchen Sie es erneut.
+
+ Schließen
+
+
+
+
+
+
\ 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..cc4449d30
--- /dev/null
+++ b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorEditMetadataModal/indicator-edit-metadata-modal.component.ts
@@ -0,0 +1,2313 @@
+import { Component, OnInit, OnDestroy, ViewChild, ElementRef, Input, HostListener } from '@angular/core';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { HttpClient } from '@angular/common/http';
+import { KommonitorIndicatorDataExchangeService } from 'services/adminIndicatorUnit/kommonitor-data-exchange.service';
+import { KommonitorIndicatorDataGridHelperService } from 'services/adminIndicatorUnit/kommonitor-data-grid-helper.service';
+import { BroadcastService } from 'services/broadcast-service/broadcast.service';
+import { AgGridAngular } from 'ag-grid-angular';
+import { ColDef, GridOptions } from 'ag-grid-community';
+import { Subscription } from 'rxjs';
+import { KommonitorIndicatorCacheHelperService } from 'services/adminIndicatorUnit/kommonitor-cache-helper.service';
+import { NgForm } from '@angular/forms';
+
+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;
+ @ViewChild('indicatorEditMetadataForm') indicatorEditMetadataForm!: NgForm;
+ @Input() currentIndicatorDataset: any = null;
+
+ private subscriptions: Subscription[] = [];
+
+ // Multi-step form properties
+ currentStep = 1;
+ totalSteps = 6;
+
+ // 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;
+ indicatorCharacteristicValue = '';
+
+ // 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[] = [];
+
+ // Step 4: Filtered lists for references (like add modal)
+ // Remove these - we'll use service properties directly like AngularJS
+ // filteredIndicators: any[] = [];
+ // filteredGeoresources: any[] = [];
+
+ // Step 4: Collapsible Box Properties (like add modal)
+ isIndicatorReferencesCollapsed = true;
+ isGeoresourceReferencesCollapsed = true;
+
+ // 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[] = [];
+
+ // Additional classification variables (missing from original)
+ classificationMethodOptions: any[] = [];
+ defaultClassificationMethod = (__env?.defaultClassifyMethod || 'jenks');
+ enableManualClassification = false;
+ enableRegionalClassification = false;
+
+
+
+ // Regional reference values
+ regionalReferenceValuesManagementTableOptions: any = undefined;
+ tmpIndicatorRegionalReferenceValuesObject: any = undefined;
+ noneColumnValue = '-- keine --';
+ file_regionalReferenceValuesImport: File | null = null;
+ isDragOver: boolean = false;
+ csvProcessingStatus: { type: 'success' | 'error' | 'info', message: string } | null = null;
+
+ // Messages - standardized to match spatial units pattern
+ successMessage = '';
+ errorMessage = '';
+ successMessagePart = '';
+ errorMessagePart = '';
+ indicatorMetadataImportError = '';
+ indicatorAddMetadataImportErrorAlert = false;
+
+ // Loading state - standardized to match spatial units pattern
+ loadingData = false;
+
+ // Color brewer
+ colorbrewerSchemes = colorbrewer || {};
+ colorbreweSchemeName_dynamicIncrease = __env?.defaultColorBrewerPaletteForBalanceIncreasingValues || 'Blues';
+ colorbreweSchemeName_dynamicDecrease = __env?.defaultColorBrewerPaletteForBalanceDecreasingValues || 'Reds';
+ colorbrewerPalettes: any[] = [];
+
+ // Classification properties
+ decreaseBreaksLength: number = 0;
+ increaseBreaksLength: number = 0;
+
+ // Enhanced classification properties (like add modal)
+ enableDynamicColorAssignment = false;
+ currentClassificationTab: number = 0;
+ classificationValidationErrors: string[] = [];
+ enableColorValidation: boolean = false;
+ dynamicColorAssignmentEnabled = false;
+ negativeValueColorScheme = 'Reds';
+ positiveValueColorScheme = 'Blues';
+ zeroValueColor = '#bababa';
+ classificationBreakValidationEnabled: boolean = true;
+
+ // Available options - these were missing!
+ availableSpatialUnits: any[] = [];
+ updateIntervalOptions: any[] = [];
+ indicatorTypeOptions: any[] = [];
+ indicatorCreationTypeOptions: any[] = [];
+ indicatorUnitOptions: any[] = [];
+ availableTopics: any[] = [];
+ availableIndicators: any[] = [];
+ availableGeoresources: 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 = '';
+
+ // Color palette management
+ isColorPaletteOpen: boolean = false;
+
+ // File handling properties
+ @ViewChild('fileInput') fileInput!: ElementRef;
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ private http: HttpClient,
+ public kommonitorDataExchangeService: KommonitorIndicatorDataExchangeService,
+ private kommonitorCacheHelperService: KommonitorIndicatorCacheHelperService,
+ private kommonitorDataGridHelperService: KommonitorIndicatorDataGridHelperService,
+ private broadcastService: BroadcastService
+ ) {}
+
+ async ngOnInit(): Promise {
+ console.log('IndicatorEditMetadataModalComponent ngOnInit started');
+ console.log('currentIndicatorDataset:', this.currentIndicatorDataset);
+
+ this.setupEventListeners();
+ this.loadEnvironmentConfiguration(); // Load environment config first
+ await this.loadInitialData();
+ this.instantiateColorBrewerPalettes();
+ this.indicatorMetadataStructure_pretty = this.kommonitorDataExchangeService.syntaxHighlightJSON(this.indicatorMetadataStructure);
+
+ // Initialize classification with default values
+ this.onNumClassesChanged(5);
+
+ // Set default classification method if not already set
+ if (!this.classificationMethod) {
+ this.classificationMethod = (__env?.defaultClassifyMethod || 'jenks');
+ }
+
+ // If currentIndicatorDataset is already set (from parent component), initialize form
+ if (this.currentIndicatorDataset) {
+ console.log('Initializing form with existing dataset');
+ this.resetIndicatorEditMetadataForm();
+ // Initialize regional reference values table
+ this.initializeRegionalReferenceValuesTable();
+ } else {
+ console.log('No currentIndicatorDataset available, form will be initialized later');
+ }
+ }
+
+ ngOnDestroy(): void {
+ this.subscriptions.forEach(sub => sub.unsubscribe());
+ }
+
+ // Multi-step form navigation methods
+ nextStep(): void {
+ // Set default classification method if not already set
+ if (!this.classificationMethod) {
+ this.classificationMethod = (__env?.defaultClassifyMethod || 'jenks');
+ }
+
+ if (this.currentStep < this.totalSteps && this.isCurrentStepValid()) {
+ this.currentStep++;
+ this.updateProgressBar();
+ }
+ }
+
+ previousStep(): void {
+ // Set default classification method if not already set
+ if (!this.classificationMethod) {
+ this.classificationMethod = (__env?.defaultClassifyMethod || 'jenks');
+ }
+
+ if (this.currentStep > 1) {
+ this.currentStep--;
+ this.updateProgressBar();
+ }
+ }
+
+ goToStep(step: number): void {
+ // Set default classification method if not already set
+ if (!this.classificationMethod) {
+ this.classificationMethod = (__env?.defaultClassifyMethod || 'jenks');
+ }
+
+ if (step >= 1 && step <= this.totalSteps) {
+ // Allow navigation to any step for better user experience
+ this.currentStep = step;
+ this.updateProgressBar();
+
+ // Initialize filtered lists when navigating to Step 4 (like add modal)
+ if (step === 4) {
+ console.log('=== Navigating to Step 4 ===');
+ console.log('Before - availableIndicators:', this.availableIndicators?.length || 0);
+ console.log('Before - availableGeoresources:', this.availableGeoresources?.length || 0);
+
+ // Always refresh local properties from service to ensure they're up-to-date
+ this.refreshLocalPropertiesFromService();
+
+ console.log('After refresh - availableIndicators:', this.availableIndicators?.length || 0);
+ console.log('After refresh - availableGeoresources:', this.availableGeoresources?.length || 0);
+
+ // Expand the collapsible boxes by default for better UX
+ this.isIndicatorReferencesCollapsed = false;
+ this.isGeoresourceReferencesCollapsed = false;
+ }
+
+ // Show validation feedback if navigating to a step that requires validation
+ if (step > 1 && !this.isStepValid(step)) {
+ // Step requires validation
+ }
+ }
+ }
+
+ isStepValid(step: number): boolean {
+ // Validation for specific steps
+ switch (step) {
+ case 1:
+ return !!this.datasetName && !!this.indicatorType && !!this.indicatorUnit && !!this.indicatorInterpretation;
+ case 2:
+ return !!this.metadata.description && !!this.metadata.datasource && !!this.metadata.contact && !!this.metadata.updateInterval && !!this.metadata.lastUpdate;
+ case 3:
+ return !!this.indicatorTopic_mainTopic;
+ case 4:
+ // Step 4 is optional (references)
+ return true;
+ case 5:
+ // Step 5 validation depends on indicator type
+ if (this.indicatorType?.apiName?.includes('STATUS')) {
+ return !!this.selectedColorBrewerPaletteEntry && !!this.numClassesPerSpatialUnit;
+ }
+ return !!this.numClassesPerSpatialUnit;
+ case 6:
+ // Step 6 is informational
+ return true;
+ default:
+ return true;
+ }
+ }
+
+ updateProgressBar(): void {
+ // Set default classification method if not already set
+ if (!this.classificationMethod) {
+ this.classificationMethod = (__env?.defaultClassifyMethod || 'jenks');
+ }
+
+ // Update progress bar active states
+ const progressItems = document.querySelectorAll('#progressbar li');
+ progressItems.forEach((item, index) => {
+ if (index < this.currentStep) {
+ item.classList.add('active');
+ } else {
+ item.classList.remove('active');
+ }
+ });
+ }
+
+ isStepActive(step: number): boolean {
+ // Set default classification method if not already set
+ if (!this.classificationMethod) {
+ this.classificationMethod = (__env?.defaultClassifyMethod || 'jenks');
+ }
+
+ return this.currentStep === step;
+ }
+
+ isStepCompleted(step: number): boolean {
+ // Set default classification method if not already set
+ if (!this.classificationMethod) {
+ this.classificationMethod = (__env?.defaultClassifyMethod || 'jenks');
+ }
+
+ return this.currentStep > step;
+ }
+
+ isCurrentStepValid(): boolean {
+ // Set default classification method if not already set
+ if (!this.classificationMethod) {
+ this.classificationMethod = (__env?.defaultClassifyMethod || 'jenks');
+ }
+
+ return this.isStepValid(this.currentStep);
+ }
+
+ private setupEventListeners(): void {
+ // Listen for broadcast messages if needed
+ const sub = this.broadcastService.currentBroadcastMsg.subscribe(data => {
+ if (data.msg === 'refreshIndicatorOverviewTableCompleted') {
+ if (this.currentIndicatorDataset) {
+ this.currentIndicatorDataset = this.kommonitorDataExchangeService.getIndicatorMetadataById(this.currentIndicatorDataset.indicatorId);
+ }
+ }
+ });
+ this.subscriptions.push(sub);
+
+ // Setup Bootstrap accordion functionality
+ setTimeout(() => {
+ ($('[data-widget="collapse"]') as any).on('click', function(this: any) {
+ const icon = $(this).find('i');
+ if (icon.hasClass('fa-plus')) {
+ icon.removeClass('fa-plus').addClass('fa-minus');
+ } else {
+ icon.removeClass('fa-minus').addClass('fa-plus');
+ }
+ });
+ }, 100);
+
+ // Setup Bootstrap dropdown functionality
+ setTimeout(() => {
+ ($('[data-toggle="dropdown"]') as any).on('click', function(this: any, e: any) {
+ e.preventDefault();
+ const dropdown = $(this).next('.dropdown-menu');
+ dropdown.toggleClass('show');
+ });
+
+ // Close dropdown when clicking outside
+ $(document).on('click', function(e: any) {
+ if (!$(e.target).closest('.dropdown').length) {
+ $('.dropdown-menu').removeClass('show');
+ }
+ });
+ }, 100);
+ }
+
+ private async loadInitialData(): Promise {
+ // 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 indicator type options
+ if (this.kommonitorDataExchangeService.indicatorTypeOptions) {
+ this.indicatorTypeOptions = this.kommonitorDataExchangeService.indicatorTypeOptions;
+ }
+
+ // Load indicator creation type options
+ if (this.kommonitorDataExchangeService.indicatorCreationTypeOptions) {
+ this.indicatorCreationTypeOptions = this.kommonitorDataExchangeService.indicatorCreationTypeOptions;
+ console.log('Available creation type options:', this.indicatorCreationTypeOptions);
+ }
+
+ // Load indicator unit options
+ if (this.kommonitorDataExchangeService.indicatorUnitOptions) {
+ this.indicatorUnitOptions = this.kommonitorDataExchangeService.indicatorUnitOptions;
+ }
+
+ // Load available topics
+ if (this.kommonitorDataExchangeService.availableTopics) {
+ this.availableTopics = this.kommonitorDataExchangeService.availableTopics;
+ }
+
+ // Load available indicators (like add modal)
+ this.availableIndicators = this.kommonitorDataExchangeService.availableIndicators || [];
+
+ // Load available georesources (like add modal)
+ this.availableGeoresources = this.kommonitorDataExchangeService.availableGeoresources || [];
+
+ // Initialize available lists for Step 4 (like AngularJS)
+
+ // Load indicators if not already loaded (do not gate by roles length)
+ if (!this.kommonitorDataExchangeService.availableIndicators ||
+ this.kommonitorDataExchangeService.availableIndicators.length === 0) {
+ try {
+ await this.kommonitorDataExchangeService.fetchIndicatorsMetadata(
+ this.kommonitorDataExchangeService.currentKeycloakLoginRoles || []
+ );
+ // Update local properties after fetching (like AngularJS)
+ this.availableIndicators = this.kommonitorDataExchangeService.availableIndicators || [];
+ } catch (error) {
+ console.error('Error loading indicators:', error);
+ }
+ }
+
+ // Load georesources if not already loaded (do not gate by roles length)
+ if (!this.kommonitorDataExchangeService.availableGeoresources ||
+ this.kommonitorDataExchangeService.availableGeoresources.length === 0) {
+ try {
+ await this.kommonitorDataExchangeService.fetchGeoresourcesMetadata(
+ this.kommonitorDataExchangeService.currentKeycloakLoginRoles || []
+ );
+ // Update local properties after fetching (like AngularJS)
+ this.availableGeoresources = this.kommonitorDataExchangeService.availableGeoresources || [];
+ } catch (error) {
+ console.error('Error loading georesources:', error);
+ }
+ }
+ }
+
+ // Remove openModal method - no longer needed
+
+ 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);
+ }
+
+ // Check if colorbrewer is available
+ if (!colorbrewer || typeof colorbrewer !== 'object') {
+ // Create fallback color palettes
+ this.createFallbackColorPalettes();
+ return;
+ }
+
+ 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'
+ if (this.colorbrewerPalettes.length > 13) {
+ this.selectedColorBrewerPaletteEntry = this.colorbrewerPalettes[13];
+ } else if (this.colorbrewerPalettes.length > 0) {
+ this.selectedColorBrewerPaletteEntry = this.colorbrewerPalettes[0];
+ }
+ }
+
+ // Create fallback color palettes when colorbrewer library is not available
+ private createFallbackColorPalettes(): void {
+ const fallbackPalettes = {
+ 'Blues': {
+ '3': ['#deebf7', '#9ecae1', '#3182bd'],
+ '4': ['#deebf7', '#9ecae1', '#3182bd', '#08519c'],
+ '5': ['#deebf7', '#9ecae1', '#3182bd', '#08519c', '#08306b'],
+ '6': ['#f7fbff', '#deebf7', '#9ecae1', '#3182bd', '#08519c', '#08306b'],
+ '7': ['#f7fbff', '#deebf7', '#c6dbef', '#9ecae1', '#3182bd', '#08519c', '#08306b'],
+ '8': ['#f7fbff', '#deebf7', '#c6dbef', '#9ecae1', '#6baed6', '#3182bd', '#08519c', '#08306b']
+ },
+ 'Reds': {
+ '3': ['#fee5d9', '#fcae91', '#de2d26'],
+ '4': ['#fee5d9', '#fcae91', '#de2d26', '#a50f15'],
+ '5': ['#fee5d9', '#fcae91', '#de2d26', '#a50f15', '#67000d'],
+ '6': ['#fff5f0', '#fee5d9', '#fcae91', '#de2d26', '#a50f15', '#67000d'],
+ '7': ['#fff5f0', '#fee5d9', '#fcbba1', '#fcae91', '#de2d26', '#a50f15', '#67000d'],
+ '8': ['#fff5f0', '#fee5d9', '#fcbba1', '#fcae91', '#fb6a4a', '#de2d26', '#a50f15', '#67000d']
+ },
+ 'Greens': {
+ '3': ['#e5f5e0', '#a1d99b', '#31a354'],
+ '4': ['#e5f5e0', '#a1d99b', '#31a354', '#006d2c'],
+ '5': ['#e5f5e0', '#a1d99b', '#31a354', '#006d2c', '#00441b'],
+ '6': ['#f7fcf5', '#e5f5e0', '#a1d99b', '#31a354', '#006d2c', '#00441b'],
+ '7': ['#f7fcf5', '#e5f5e0', '#c7e9c0', '#a1d99b', '#31a354', '#006d2c', '#00441b'],
+ '8': ['#f7fcf5', '#e5f5e0', '#c7e9c0', '#a1d99b', '#74c476', '#31a354', '#006d2c', '#00441b']
+ },
+ 'Oranges': {
+ '3': ['#fee6ce', '#fdd0a2', '#e6550d'],
+ '4': ['#fee6ce', '#fdd0a2', '#e6550d', '#a63603'],
+ '5': ['#fee6ce', '#fdd0a2', '#e6550d', '#a63603', '#7f2704'],
+ '6': ['#fff5eb', '#fee6ce', '#fdd0a2', '#e6550d', '#a63603', '#7f2704'],
+ '7': ['#fff5eb', '#fee6ce', '#fed98e', '#fdd0a2', '#e6550d', '#a63603', '#7f2704'],
+ '8': ['#fff5eb', '#fee6ce', '#fed98e', '#fdd0a2', '#fdbe85', '#e6550d', '#a63603', '#7f2704']
+ }
+ };
+
+ // Convert fallback palettes to the expected format
+ for (const key in fallbackPalettes) {
+ if (fallbackPalettes.hasOwnProperty(key)) {
+ const colorPalettes = fallbackPalettes[key];
+
+ const paletteEntry = {
+ "paletteName": key,
+ "paletteArrayObject": colorPalettes
+ };
+
+ this.colorbrewerPalettes.push(paletteEntry);
+ }
+ }
+
+ // Also update the colorbrewerSchemes for dynamic color assignment
+ this.colorbrewerSchemes = fallbackPalettes;
+
+ // Set default palette
+ if (this.colorbrewerPalettes.length > 0) {
+ this.selectedColorBrewerPaletteEntry = this.colorbrewerPalettes[0];
+ }
+ }
+
+ // Load environment configuration (like add modal)
+ private loadEnvironmentConfiguration() {
+ // Load default classification method from environment
+ this.defaultClassificationMethod = (__env?.defaultClassifyMethod || 'jenks');
+ this.classificationMethod = this.defaultClassificationMethod;
+
+ // Load color scheme names from environment
+ this.colorbreweSchemeName_dynamicIncrease = __env?.defaultColorBrewerPaletteForBalanceIncreasingValues || 'Blues';
+ this.colorbreweSchemeName_dynamicDecrease = __env?.defaultColorBrewerPaletteForBalanceDecreasingValues || 'Reds';
+
+ // Load classification method options
+ this.classificationMethodOptions = [
+ {
+ id: 'jenks',
+ name: 'Jenks Natural Breaks',
+ description: 'Automatische Klassifizierung nach natürlichen Brüchen',
+ imgPath: 'icons/classificationMethods/neu/jenks.svg'
+ },
+ {
+ id: 'equal',
+ name: 'Gleiche Intervalle',
+ description: 'Gleichmäßige Aufteilung des Wertebereichs',
+ imgPath: 'icons/classificationMethods/neu/gleichesIntervall.svg'
+ },
+ {
+ id: 'manual',
+ name: 'Manuelle Klassifizierung',
+ description: 'Benutzerdefinierte Klassengrenzen',
+ imgPath: 'icons/classificationMethods/neu/manual.svg'
+ },
+ {
+ id: 'regional_default',
+ name: 'Regionale Standard-Klassifizierung',
+ description: 'Regionsspezifische Klassengrenzen',
+ imgPath: 'icons/classificationMethods/neu/regional.svg'
+ }
+ ];
+
+ // Check if manual classification is disabled
+ if (__env?.disableManualClassification) {
+ this.classificationMethodOptions = this.classificationMethodOptions.filter(option => option.id !== 'manual');
+ }
+ }
+
+ // Enhanced classification method selection (like add modal)
+ onClassificationMethodSelected(method: any) {
+ // Handle both method objects and method IDs
+ const methodId = typeof method === 'string' ? method : method.id;
+ this.classificationMethod = methodId;
+
+ // Enable/disable specific features based on method
+ this.enableManualClassification = methodId === 'manual';
+ this.enableRegionalClassification = methodId === 'regional_default';
+
+ // Reset validation errors
+ this.classificationValidationErrors = [];
+ this.classBreaksInvalid = false;
+
+ // Reinitialize classification when method changes
+ this.onNumClassesChanged(this.numClassesPerSpatialUnit);
+
+ // Update dynamic color assignment based on method
+ this.updateDynamicColorAssignment();
+ }
+
+ onNumClassesChanged(numClasses: any): void {
+ // Handle both event parameters and direct values
+ let classes: number | null;
+
+ if (typeof numClasses === 'object' && numClasses !== null && numClasses.target) {
+ classes = parseInt(numClasses.target.value) || null;
+ } else {
+ classes = numClasses;
+ }
+
+ if (classes === null || classes === undefined || classes < 1) {
+ return; // Don't process if null, undefined, or less than 1
+ }
+
+ this.numClassesPerSpatialUnit = classes;
+ 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 < classes! - 1; i++) {
+ breaks.push(null);
+ }
+
+ this.spatialUnitClassification.push({
+ spatialUnitId: spatialUnit.spatialUnitId,
+ spatialUnitLevel: spatialUnit.spatialUnitLevel,
+ breaks: breaks
+ });
+
+ // Initialize tab class - first tab is active
+ this.tabClasses[index] = index === 0 ? 'active' : '';
+ });
+ }
+
+ // Reset validation
+ this.classBreaksInvalid = false;
+ this.classificationValidationErrors = [];
+
+ // Set first tab as active
+ this.currentClassificationTab = 0;
+
+ // Set default classification method if not already set
+ if (!this.classificationMethod) {
+ this.classificationMethod = (__env?.defaultClassifyMethod || 'jenks');
+ }
+ }
+
+ onBreaksChanged(tabIndex: number): void {
+ if (!this.spatialUnitClassification[tabIndex]) {
+ return;
+ }
+
+ // Set default classification method if not already set
+ if (!this.classificationMethod) {
+ this.classificationMethod = (__env?.defaultClassifyMethod || 'jenks');
+ }
+
+ const breaks = this.spatialUnitClassification[tabIndex].breaks;
+ let cssClass = 'tab-completed';
+ this.classBreaksInvalid = false;
+ this.classificationValidationErrors = [];
+
+ // Enhanced validation logic matching AngularJS implementation
+ if (this.classificationMethod === 'regional_default' || this.classificationMethod === 'manual') {
+ // Check if all breaks are filled
+ let allBreaksFilled = true;
+ for (const classBreak of breaks) {
+ if (classBreak === null || classBreak === undefined || classBreak === '') {
+ allBreaksFilled = false;
+ break;
+ }
+ }
+
+ if (allBreaksFilled) {
+ // Validate that breaks are in ascending order
+ for (let i = 0; i < breaks.length - 1; i++) {
+ if (breaks[i] >= breaks[i + 1]) {
+ cssClass = 'tab-error';
+ this.classBreaksInvalid = true;
+ this.classificationValidationErrors.push(
+ `Klassengrenze ${i + 1} (${breaks[i]}) muss kleiner sein als Klassengrenze ${i + 2} (${breaks[i + 1]})`
+ );
+ break;
+ }
+ }
+ } else {
+ // Check if any breaks are filled but not all
+ let hasAnyBreaks = false;
+ for (const classBreak of breaks) {
+ if (classBreak !== null && classBreak !== undefined && classBreak !== '') {
+ hasAnyBreaks = true;
+ break;
+ }
+ }
+
+ if (hasAnyBreaks) {
+ cssClass = 'tab-error';
+ this.classBreaksInvalid = true;
+ this.classificationValidationErrors.push('Alle Klassengrenzen müssen ausgefüllt werden');
+ } else {
+ cssClass = 'active';
+ }
+ }
+ } else {
+ // For automatic classification methods, check if any manual breaks are entered
+ let hasManualBreaks = false;
+ for (const classBreak of breaks) {
+ if (classBreak !== null && classBreak !== undefined && classBreak !== '') {
+ hasManualBreaks = true;
+ break;
+ }
+ }
+
+ if (hasManualBreaks) {
+ cssClass = 'tab-error';
+ this.classBreaksInvalid = true;
+ this.classificationValidationErrors.push('Manuelle Klassengrenzen sind für automatische Klassifizierungsmethoden nicht erlaubt');
+ }
+ }
+
+ this.tabClasses[tabIndex] = cssClass;
+
+ // Update decrease and increase breaks for dynamic color assignment
+ this.updateDecreaseAndIncreaseBreaks(tabIndex);
+ }
+
+ // Enhanced classification methods (like add modal)
+ goToClassificationTab(tabIndex: number) {
+ this.currentClassificationTab = tabIndex;
+
+ // Set default classification method if not already set
+ if (!this.classificationMethod) {
+ this.classificationMethod = (__env?.defaultClassifyMethod || 'jenks');
+ }
+
+ // 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';
+ }
+
+ // Update dynamic color assignment based on classification method
+ private updateDynamicColorAssignment() {
+ // Set default classification method if not already set
+ if (!this.classificationMethod) {
+ this.classificationMethod = (__env?.defaultClassifyMethod || 'jenks');
+ }
+
+ // Enable dynamic color assignment for certain methods
+ this.dynamicColorAssignmentEnabled = this.classificationMethod === 'regional_default' ||
+ this.classificationMethod === 'manual';
+
+ // Set color schemes based on method
+ if (this.classificationMethod === 'regional_default') {
+ this.negativeValueColorScheme = 'Reds';
+ this.positiveValueColorScheme = 'Blues';
+ } else if (this.classificationMethod === 'manual') {
+ this.negativeValueColorScheme = 'Reds';
+ this.positiveValueColorScheme = 'Blues';
+ } else {
+ // For automatic methods, use default schemes
+ this.negativeValueColorScheme = this.colorbreweSchemeName_dynamicDecrease;
+ this.positiveValueColorScheme = this.colorbreweSchemeName_dynamicIncrease;
+ }
+
+ // Update color assignment for all spatial units
+ this.updateColorAssignmentForAllSpatialUnits();
+ }
+
+ // Update color assignment for all spatial units
+ private updateColorAssignmentForAllSpatialUnits() {
+ if (!this.dynamicColorAssignmentEnabled) {
+ return;
+ }
+
+ for (let i = 0; i < this.spatialUnitClassification.length; i++) {
+ this.updateColorAssignmentForSpatialUnit(i);
+ }
+ }
+
+ // Update color assignment for a specific spatial unit
+ private updateColorAssignmentForSpatialUnit(spatialUnitIndex: number) {
+ if (!this.spatialUnitClassification[spatialUnitIndex]) {
+ return;
+ }
+
+ const classification = this.spatialUnitClassification[spatialUnitIndex];
+ const breaks = classification.breaks;
+
+ // Calculate color assignment based on break values
+ let hasNegativeValues = false;
+ let hasPositiveValues = false;
+ let hasZeroValue = false;
+
+ for (const breakValue of breaks) {
+ if (breakValue !== null && breakValue !== undefined) {
+ if (breakValue < 0) hasNegativeValues = true;
+ if (breakValue > 0) hasPositiveValues = true;
+ if (breakValue === 0) hasZeroValue = true;
+ }
+ }
+
+ // Store color assignment information
+ classification.colorAssignment = {
+ hasNegativeValues,
+ hasPositiveValues,
+ hasZeroValue,
+ negativeColorScheme: this.negativeValueColorScheme,
+ positiveColorScheme: this.positiveValueColorScheme,
+ zeroColor: this.zeroValueColor
+ };
+ }
+
+ // Get color for a specific class based on break value
+ getClassColorForBreak(breakValue: number, classIndex: number): string {
+ if (!this.dynamicColorAssignmentEnabled) {
+ // Use standard color brewer palette
+ if (this.selectedColorBrewerPaletteEntry?.paletteArrayObject) {
+ const colors = this.selectedColorBrewerPaletteEntry.paletteArrayObject[this.numClassesPerSpatialUnit?.toString() || '5'];
+ if (colors && colors[classIndex]) {
+ return colors[classIndex];
+ }
+ }
+ return '#cccccc';
+ }
+
+ // Dynamic color assignment based on break value
+ if (breakValue < 0) {
+ // Negative values - use decreasing color scheme
+ const colors = this.colorbrewerSchemes[this.negativeValueColorScheme];
+ if (colors && colors[this.decreaseBreaksLength]) {
+ const colorIndex = Math.min(classIndex, this.decreaseBreaksLength - 1);
+ return colors[this.decreaseBreaksLength][colorIndex];
+ }
+ } else if (breakValue > 0) {
+ // Positive values - use increasing color scheme
+ const colors = this.colorbrewerSchemes[this.positiveValueColorScheme];
+ if (colors && colors[this.increaseBreaksLength]) {
+ const colorIndex = Math.min(classIndex, this.increaseBreaksLength - 1);
+ return colors[this.increaseBreaksLength][colorIndex];
+ }
+ } else if (breakValue === 0) {
+ // Zero value - use neutral color
+ return this.zeroValueColor;
+ }
+
+ return '#cccccc';
+ }
+
+ // Helper method to get color palette colors safely
+ getColorPaletteColors(paletteEntry: any, numColors: number): string[] {
+ if (!paletteEntry?.paletteArrayObject) {
+ return ['#cccccc', '#cccccc', '#cccccc', '#cccccc', '#cccccc'];
+ }
+
+ const colors = paletteEntry.paletteArrayObject[numColors?.toString() || '5'];
+ if (!colors || !Array.isArray(colors)) {
+ return ['#cccccc', '#cccccc', '#cccccc', '#cccccc', '#cccccc'];
+ }
+
+ return colors;
+ }
+
+ // Helper method to get color scheme colors safely
+ getColorSchemeColors(schemeName: string, numColors: number): string[] {
+ if (!this.colorbrewerSchemes?.[schemeName]) {
+ return ['#cccccc', '#cccccc', '#cccccc', '#cccccc', '#cccccc'];
+ }
+
+ const colors = this.colorbrewerSchemes[schemeName][numColors?.toString() || '5'];
+ if (!colors || !Array.isArray(colors)) {
+ return ['#cccccc', '#cccccc', '#cccccc', '#cccccc', '#cccccc'];
+ }
+
+ return colors;
+ }
+
+ // Reload color palettes (like add modal)
+ reloadColorPalettes() {
+ this.colorbrewerPalettes = [];
+ this.instantiateColorBrewerPalettes();
+ }
+
+ // Get dynamic color for classification (like add modal)
+ getDynamicColor(schemeName: string, breakLength: number, index: number, type: 'increase' | 'decrease'): string {
+ if (!this.colorbrewerSchemes?.[schemeName]) {
+ return '#cccccc';
+ }
+
+ const colors = this.colorbrewerSchemes[schemeName];
+ if (!colors?.[breakLength + 1]) {
+ return '#cccccc';
+ }
+
+ const colorArray = colors[breakLength + 1];
+ if (type === 'decrease') {
+ const colorIndex = Math.max(0, breakLength - index - 1);
+ return colorArray[colorIndex] || '#cccccc';
+ } else {
+ const colorIndex = Math.max(0, breakLength - (this.spatialUnitClassification[this.currentClassificationTab]?.breaks?.length - index) - 1);
+ return colorArray[colorIndex] || '#cccccc';
+ }
+ }
+
+ updateDecreaseAndIncreaseBreaks(tabIndex: number): void {
+ if (!this.spatialUnitClassification[tabIndex]) {
+ return;
+ }
+
+ const breaks = this.spatialUnitClassification[tabIndex].breaks;
+
+ // Count positive and negative breaks
+ this.increaseBreaksLength = breaks.filter(val => val !== null && val !== undefined && val > 0).length;
+ this.decreaseBreaksLength = breaks.filter(val => val !== null && val !== undefined && val < 0).length;
+
+ // Ensure minimum lengths for color schemes
+ if (this.increaseBreaksLength < 3) {
+ this.increaseBreaksLength = 3;
+ }
+ if (this.decreaseBreaksLength < 3) {
+ this.decreaseBreaksLength = 3;
+ }
+ }
+
+ refreshReferenceValuesManagementTable(): void {
+ this.regionalReferenceValuesManagementTableOptions = this.kommonitorDataGridHelperService.buildReferenceValuesManagementGrid(
+ this.regionalReferenceValuesManagementTableOptions
+ );
+ }
+
+ resetIndicatorEditMetadataForm(): void {
+ if (!this.currentIndicatorDataset) {
+ return;
+ }
+
+ this.successMessage = '';
+ this.errorMessage = '';
+ this.successMessagePart = '';
+ this.errorMessagePart = '';
+
+ // Basic form data with null checks
+ this.datasetName = this.currentIndicatorDataset.indicatorName || '';
+ this.datasetNameInvalid = false;
+
+ this.indicatorReferenceDateNote = this.currentIndicatorDataset.referenceDateNote || '';
+ this.displayOrder = this.currentIndicatorDataset.displayOrder || 0;
+
+ // Reset metadata with null checks
+ const metadata = this.currentIndicatorDataset.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 || ''
+ };
+
+ // 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 || null;
+
+ if (this.currentIndicatorDataset.defaultPrecision === false) {
+ this.showCustomCommaValue = true;
+ } else {
+ this.showCustomCommaValue = false;
+ }
+
+ // Set indicator type
+ this.indicatorType = null;
+ console.log('Setting indicator type...');
+ console.log('Current dataset indicatorType:', this.currentIndicatorDataset.indicatorType);
+ console.log('Available indicator type options:', this.kommonitorDataExchangeService.indicatorTypeOptions);
+
+ if (this.currentIndicatorDataset.indicatorType) {
+ this.kommonitorDataExchangeService.indicatorTypeOptions.forEach((option: any) => {
+ if (option.apiName === this.currentIndicatorDataset.indicatorType) {
+ this.indicatorType = option;
+ console.log('Found matching indicator type:', this.indicatorType);
+ }
+ });
+ }
+
+ console.log('Final indicatorType:', this.indicatorType);
+ console.log('indicatorType.apiName:', this.indicatorType?.apiName);
+ console.log('includes STATUS:', this.indicatorType?.apiName?.includes('STATUS'));
+
+ this.isHeadlineIndicator = this.currentIndicatorDataset.isHeadlineIndicator || false;
+ 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.indicatorCreationType = null;
+ if (this.currentIndicatorDataset.creationType) {
+ this.kommonitorDataExchangeService.indicatorCreationTypeOptions.forEach((option: any) => {
+ if (option.apiName === this.currentIndicatorDataset.creationType) {
+ this.indicatorCreationType = option;
+ }
+ });
+ }
+
+ // Fallback: if no creation type is set, use INSERTION as default
+ if (!this.indicatorCreationType && this.kommonitorDataExchangeService.indicatorCreationTypeOptions && this.kommonitorDataExchangeService.indicatorCreationTypeOptions.length > 0) {
+ // Check what structure the service is returning
+ const firstOption = this.kommonitorDataExchangeService.indicatorCreationTypeOptions[0];
+ console.log('Service returned creation type option:', firstOption);
+
+ if (firstOption.apiName) {
+ // Service has correct structure, find INSERTION
+ const insertionOption = this.kommonitorDataExchangeService.indicatorCreationTypeOptions.find(option => option.apiName === 'INSERTION');
+ if (insertionOption) {
+ this.indicatorCreationType = insertionOption;
+ console.log('Fallback: Set default creation type to INSERTION:', this.indicatorCreationType);
+ } else {
+ this.indicatorCreationType = firstOption;
+ console.log('Fallback: Set default creation type to first option:', this.indicatorCreationType);
+ }
+ } else if (firstOption.value === 'manual') {
+ // Service has different structure, convert to expected format
+ this.indicatorCreationType = {
+ displayName: firstOption.label || 'Manuell',
+ apiName: 'INSERTION'
+ };
+ console.log('Fallback: Converted structure and set default creation type to INSERTION:', this.indicatorCreationType);
+ } else {
+ // Unknown structure, create safe fallback
+ this.indicatorCreationType = {
+ displayName: 'Manuell',
+ apiName: 'INSERTION'
+ };
+ console.log('Fallback: Created safe default creation type:', this.indicatorCreationType);
+ }
+ }
+
+ 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);
+ if (indicatorMetadata) {
+ 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);
+ if (georesourceMetadata) {
+ const geo_referenceEntry = {
+ "referencedGeoresourceName": georesourceMetadata.datasetName || georesourceMetadata.georesourceName,
+ "referencedGeoresourceId": georesourceMetadata.georesourceId,
+ "referencedGeoresourceDescription": georesourceReference.referencedGeoresourceDescription
+ };
+ this.georesourceReferences_adminView.push(geo_referenceEntry);
+ }
+ }
+ }
+
+ // Initialize available lists for Step 4 (like AngularJS)
+
+ // 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 && this.currentIndicatorDataset.defaultClassificationMapping.classificationMethod) {
+ this.classificationMethod = this.currentIndicatorDataset.defaultClassificationMapping.classificationMethod.toLowerCase();
+ }
+ if (this.currentIndicatorDataset.defaultClassificationMapping && this.currentIndicatorDataset.defaultClassificationMapping.numClasses) {
+ this.numClassesPerSpatialUnit = this.currentIndicatorDataset.defaultClassificationMapping.numClasses;
+ this.onNumClassesChanged(this.numClassesPerSpatialUnit || 5);
+
+ // apply breaks for spatial units:
+ if (this.currentIndicatorDataset.defaultClassificationMapping.items) {
+ 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);
+ }
+ }
+ }
+ }
+ } else {
+ // Fallback: initialize with default values if no classification mapping exists
+ this.numClassesPerSpatialUnit = 5;
+ this.onNumClassesChanged(5);
+ }
+
+ // instantiate with palette 'Blues'
+ this.selectedColorBrewerPaletteEntry = this.colorbrewerPalettes[13];
+
+ if (this.currentIndicatorDataset.defaultClassificationMapping && this.currentIndicatorDataset.defaultClassificationMapping.colorBrewerSchemeName) {
+ for (const colorbrewerPalette of this.colorbrewerPalettes) {
+ if (colorbrewerPalette.paletteName === this.currentIndicatorDataset.defaultClassificationMapping.colorBrewerSchemeName) {
+ this.selectedColorBrewerPaletteEntry = colorbrewerPalette;
+ break;
+ }
+ }
+ }
+
+ this.successMessage = '';
+ this.errorMessage = '';
+ this.successMessagePart = '';
+ this.errorMessagePart = '';
+ }
+
+ 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;
+ }
+ });
+ }
+
+ onClickColorBrewerEntry(colorPaletteEntry: any): void {
+ this.selectedColorBrewerPaletteEntry = colorPaletteEntry;
+ this.isColorPaletteOpen = false;
+
+ // Update dynamic color assignment
+ this.updateDynamicColorAssignment();
+ }
+
+ toggleColorPalette(): void {
+ this.isColorPaletteOpen = !this.isColorPaletteOpen;
+ }
+
+ @HostListener('document:click', ['$event'])
+ onDocumentClick(event: Event): void {
+ // Close color palette dropdown if clicking outside
+ if (this.isColorPaletteOpen) {
+ const target = event.target as HTMLElement;
+ if (!target.closest('.dropdown')) {
+ this.isColorPaletteOpen = false;
+ }
+ }
+ }
+
+ onAddOrUpdateIndicatorReference(): void {
+ if (!this.tmpIndicatorReference_selectedIndicatorMetadata) {
+ return;
+ }
+
+ 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 = '';
+
+ // Force change detection like AngularJS $scope.$digest()
+ this.checkButtonState();
+ }
+
+ onClickEditIndicatorReference(indicatorReference_adminView: any): void {
+ const indicatorMetadata = this.kommonitorDataExchangeService.getIndicatorMetadataById(indicatorReference_adminView.referencedIndicatorId);
+ if (indicatorMetadata) {
+ this.tmpIndicatorReference_selectedIndicatorMetadata = indicatorMetadata;
+ this.tmpIndicatorReference_referenceDescription = indicatorReference_adminView.referencedIndicatorDescription;
+
+ // Force change detection like AngularJS $scope.$digest()
+ this.checkButtonState();
+ }
+ }
+
+ 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;
+ }
+ }
+
+ // Force change detection like AngularJS $scope.$digest()
+ this.checkButtonState();
+ }
+
+ onAddOrUpdateGeoresourceReference(): void {
+ if (!this.tmpGeoresourceReference_selectedGeoresourceMetadata) {
+ return;
+ }
+
+ const tmpGeoresourceReference_adminView = {
+ "referencedGeoresourceName": this.tmpGeoresourceReference_selectedGeoresourceMetadata.datasetName || this.tmpGeoresourceReference_selectedGeoresourceMetadata.georesourceName,
+ "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 = '';
+
+ // Force change detection like AngularJS $scope.$digest()
+ this.checkButtonState();
+ }
+
+ onClickEditGeoresourceReference(georesourceReference_adminView: any): void {
+ const georesourceMetadata = this.kommonitorDataExchangeService.getGeoresourceMetadataById(georesourceReference_adminView.referencedGeoresourceId);
+ if (georesourceMetadata) {
+ this.tmpGeoresourceReference_selectedGeoresourceMetadata = georesourceMetadata;
+ this.tmpGeoresourceReference_referenceDescription = georesourceReference_adminView.referencedGeoresourceDescription;
+
+ // Force change detection like AngularJS $scope.$digest()
+ this.checkButtonState();
+ }
+ }
+
+ 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;
+ }
+ }
+
+ // Force change detection like AngularJS $scope.$digest()
+ this.checkButtonState();
+ }
+
+ 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;
+ }
+ }
+
+ // Topic hierarchy change methods
+ onMainTopicChanged(): void {
+ // Reset sub-topics when main topic changes
+ this.indicatorTopic_subTopic = null;
+ this.indicatorTopic_subsubTopic = null;
+ this.indicatorTopic_subsubsubTopic = null;
+ }
+
+ onSubTopicChanged(): void {
+ // Reset sub-sub-topics when sub topic changes
+ this.indicatorTopic_subsubTopic = null;
+ this.indicatorTopic_subsubsubTopic = null;
+ }
+
+ onSubSubTopicChanged(): void {
+ // Reset sub-sub-sub-topics when sub-sub topic changes
+ this.indicatorTopic_subsubsubTopic = null;
+ }
+
+ // Filter methods to replace AngularJS filters
+ getMainTopics(): any[] {
+ return this.availableTopics.filter((topic: any) =>
+ topic.topicType === 'main' && topic.topicResource === 'indicator'
+ );
+ }
+
+ // Reference filter methods (like AngularJS)
+ getFilteredIndicators(): any[] {
+ const indicators = this.availableIndicators || [];
+ if (!indicators || indicators.length === 0) {
+ return [];
+ }
+
+ if (!this.indicatorNameFilter || this.indicatorNameFilter.trim() === '') {
+ return indicators;
+ }
+
+ const filterLower = this.indicatorNameFilter.toLowerCase().trim();
+ return indicators.filter((indicator: any) => {
+ if (!indicator) return false;
+
+ const name = (indicator.indicatorName || indicator.datasetName || '').toLowerCase();
+ const abbr = (indicator.abbreviation || '').toLowerCase();
+
+ return name.includes(filterLower) || abbr.includes(filterLower);
+ });
+ }
+
+ getFilteredGeoresources(): any[] {
+ const georesources = this.availableGeoresources || [];
+ if (!georesources || georesources.length === 0) {
+ return [];
+ }
+
+ if (!this.georesourceNameFilter || this.georesourceNameFilter.trim() === '') {
+ return georesources;
+ }
+
+ const filterLower = this.georesourceNameFilter.toLowerCase().trim();
+ return georesources.filter((georesource: any) => {
+ if (!georesource) return false;
+
+ const name = (georesource.datasetName || georesource.georesourceName || '').toLowerCase();
+
+ return name.includes(filterLower);
+ });
+ }
+
+ // Step 4: Reference Filtering Methods (like AngularJS - no local filtering needed)
+ // The HTML will use the service properties directly with Angular pipes
+
+ // Step 4: Collapsible Box Methods (like add modal)
+ toggleIndicatorReferences() {
+ this.isIndicatorReferencesCollapsed = !this.isIndicatorReferencesCollapsed;
+ }
+
+ toggleGeoresourceReferences() {
+ this.isGeoresourceReferencesCollapsed = !this.isGeoresourceReferencesCollapsed;
+ }
+
+ // Step 4: Selection Methods (like add modal) - AngularJS style without ngModelChange
+ onIndicatorSelected() {
+ console.log('=== onIndicatorSelected called ===');
+ console.log('Indicator selected:', this.tmpIndicatorReference_selectedIndicatorMetadata);
+ console.log('Reference description:', this.tmpIndicatorReference_referenceDescription);
+ console.log('Reference description length:', this.tmpIndicatorReference_referenceDescription?.length || 0);
+ console.log('Button should be enabled:', !!(this.tmpIndicatorReference_selectedIndicatorMetadata && this.tmpIndicatorReference_referenceDescription && this.tmpIndicatorReference_referenceDescription.trim().length > 0));
+
+ // Force change detection like AngularJS $scope.$digest()
+ this.checkButtonState();
+ }
+
+ onGeoresourceSelected() {
+ console.log('=== onGeoresourceSelected called ===');
+ console.log('Georesource selected:', this.tmpGeoresourceReference_selectedGeoresourceMetadata);
+ console.log('Reference description:', this.tmpGeoresourceReference_referenceDescription);
+ console.log('Reference description length:', this.tmpGeoresourceReference_referenceDescription?.length || 0);
+ console.log('Button should be enabled:', !!(this.tmpGeoresourceReference_selectedGeoresourceMetadata && this.tmpGeoresourceReference_referenceDescription && this.tmpGeoresourceReference_referenceDescription.trim().length > 0));
+
+ // Force change detection like AngularJS $scope.$digest()
+ this.checkButtonState();
+ }
+
+ // Step 4: Description change handlers - AngularJS style
+ onIndicatorDescriptionChanged() {
+ console.log('=== onIndicatorDescriptionChanged called ===');
+ console.log('Reference description changed to:', this.tmpIndicatorReference_referenceDescription);
+ console.log('Reference description length:', this.tmpIndicatorReference_referenceDescription?.length || 0);
+ console.log('Current selected indicator:', this.tmpIndicatorReference_selectedIndicatorMetadata);
+
+ // Force change detection like AngularJS $scope.$digest()
+ this.checkButtonState();
+ }
+
+ onGeoresourceDescriptionChanged() {
+ console.log('=== onGeoresourceDescriptionChanged called ===');
+ console.log('Reference description changed to:', this.tmpGeoresourceReference_referenceDescription);
+ console.log('Reference description length:', this.tmpGeoresourceReference_referenceDescription?.length || 0);
+ console.log('Current selected georesource:', this.tmpGeoresourceReference_selectedGeoresourceMetadata);
+
+ // Force change detection like AngularJS $scope.$digest()
+ this.checkButtonState();
+ }
+
+ // Debug method for georesource data (simplified like AngularJS)
+ debugGeoresourceData() {
+ console.log('=== Debug Georesource Data ===');
+ console.log('Service availableGeoresources:', this.kommonitorDataExchangeService.availableGeoresources);
+ console.log('Local availableGeoresources:', this.availableGeoresources);
+ console.log('Current step:', this.currentStep);
+
+ // Try to reload data
+ if (this.kommonitorDataExchangeService.availableGeoresources && this.kommonitorDataExchangeService.availableGeoresources.length > 0) {
+ this.availableGeoresources = this.kommonitorDataExchangeService.availableGeoresources;
+ console.log('Reloaded georesources:', this.availableGeoresources);
+ } else {
+ // Try to fetch georesources manually
+ console.log('No georesources in service, trying to fetch manually...');
+ this.kommonitorDataExchangeService.fetchGeoresourcesMetadata(this.kommonitorDataExchangeService.currentKeycloakLoginRoles)
+ .then(georesources => {
+ console.log('Manually fetched georesources:', georesources);
+ this.availableGeoresources = georesources;
+ console.log('Updated georesources:', this.availableGeoresources);
+ })
+ .catch(error => {
+ console.error('Error fetching georesources:', error);
+ });
+ }
+ }
+
+ // Method to refresh local properties from service (like AngularJS)
+ private refreshLocalPropertiesFromService(): void {
+ // Sync indicators
+ if (this.kommonitorDataExchangeService.availableIndicators &&
+ this.kommonitorDataExchangeService.availableIndicators.length > 0) {
+ this.availableIndicators = this.kommonitorDataExchangeService.availableIndicators;
+ }
+
+ // Sync georesources
+ if (this.kommonitorDataExchangeService.availableGeoresources &&
+ this.kommonitorDataExchangeService.availableGeoresources.length > 0) {
+ this.availableGeoresources = this.kommonitorDataExchangeService.availableGeoresources;
+ }
+ }
+
+ // Method to manually refresh georesources (for debugging)
+ async refreshGeoresources(): Promise {
+ console.log('=== Manually refreshing georesources ===');
+ try {
+ await this.kommonitorDataExchangeService.fetchGeoresourcesMetadata(
+ this.kommonitorDataExchangeService.currentKeycloakLoginRoles || []
+ );
+ this.refreshLocalPropertiesFromService();
+ console.log('Georesources refreshed successfully:', this.availableGeoresources?.length || 0);
+ } catch (error) {
+ console.error('Error refreshing georesources:', error);
+ }
+ }
+
+ // Method to manually refresh indicators (for debugging)
+ async refreshIndicators(): Promise {
+ console.log('=== Manually refreshing indicators ===');
+ try {
+ await this.kommonitorDataExchangeService.fetchIndicatorsMetadata(
+ this.kommonitorDataExchangeService.currentKeycloakLoginRoles || []
+ );
+ this.refreshLocalPropertiesFromService();
+ console.log('Indicators refreshed successfully:', this.availableIndicators?.length || 0);
+ } catch (error) {
+ console.error('Error refreshing indicators:', error);
+ }
+ }
+
+ // Method to check button state (for debugging)
+ checkButtonState(): void {
+ console.log('=== Button State Check ===');
+
+ // Check indicator button state (AngularJS logic)
+ const indicatorSelected = !!this.tmpIndicatorReference_selectedIndicatorMetadata;
+ const indicatorDescriptionExists = !!(this.tmpIndicatorReference_referenceDescription && this.tmpIndicatorReference_referenceDescription.trim().length > 0);
+ const indicatorButtonEnabled = indicatorSelected || !indicatorDescriptionExists; // AngularJS logic: !selected && description
+
+ console.log('Indicator button state:');
+ console.log(' - Selected indicator:', this.tmpIndicatorReference_selectedIndicatorMetadata);
+ console.log(' - Reference description:', this.tmpIndicatorReference_referenceDescription);
+ console.log(' - Reference description length:', this.tmpIndicatorReference_referenceDescription?.length || 0);
+ console.log(' - Indicator selected:', indicatorSelected);
+ console.log(' - Description exists:', indicatorDescriptionExists);
+ console.log(' - Button should be enabled:', indicatorButtonEnabled);
+ console.log(' - Button disabled condition: !selected && description =', !indicatorSelected && indicatorDescriptionExists);
+
+ // Check georesource button state (AngularJS logic)
+ const georesourceSelected = !!this.tmpGeoresourceReference_selectedGeoresourceMetadata;
+ const georesourceDescriptionExists = !!(this.tmpGeoresourceReference_referenceDescription && this.tmpGeoresourceReference_referenceDescription.trim().length > 0);
+ const georesourceButtonEnabled = georesourceSelected || !georesourceDescriptionExists; // AngularJS logic: !selected && description
+
+ console.log('Georesource button state:');
+ console.log(' - Selected georesource:', this.tmpGeoresourceReference_selectedGeoresourceMetadata);
+ console.log(' - Reference description:', this.tmpGeoresourceReference_referenceDescription);
+ console.log(' - Reference description length:', this.tmpGeoresourceReference_referenceDescription?.length || 0);
+ console.log(' - Georesource selected:', georesourceSelected);
+ console.log(' - Description exists:', georesourceDescriptionExists);
+ console.log(' - Button should be enabled:', georesourceButtonEnabled);
+ console.log(' - Button disabled condition: !selected && description =', !georesourceSelected && georesourceDescriptionExists);
+ }
+
+ // Method to manually test button state (for debugging)
+ testButtonState(): void {
+ console.log('=== Testing Button State ===');
+
+ // Test with sample data
+ this.tmpIndicatorReference_selectedIndicatorMetadata = { indicatorId: 'test', indicatorName: 'Test Indicator' };
+ this.tmpIndicatorReference_referenceDescription = 'Test description';
+
+ console.log('Set test data:');
+ console.log(' - Selected indicator:', this.tmpIndicatorReference_selectedIndicatorMetadata);
+ console.log(' - Reference description:', this.tmpIndicatorReference_referenceDescription);
+
+ // Check button state again
+ this.checkButtonState();
+ }
+
+ // Method to manually check button state (for debugging)
+ manualCheckButtonState(): void {
+ console.log('=== Manual Button State Check ===');
+ console.log('This method was called manually');
+
+ // Check current state
+ this.checkButtonState();
+
+ // Try to manually trigger change detection
+ console.log('Attempting to manually trigger change detection...');
+
+ // Force a change detection cycle
+ setTimeout(() => {
+ console.log('=== After manual timeout ===');
+ this.checkButtonState();
+ }, 100);
+ }
+
+ // Method to manually simulate selection and description (for debugging)
+ simulateUserInput(): void {
+ console.log('=== Simulating User Input ===');
+
+ // Simulate selecting an indicator
+ console.log('Simulating indicator selection...');
+ this.tmpIndicatorReference_selectedIndicatorMetadata = {
+ indicatorId: 'simulated',
+ indicatorName: 'Simulated Indicator'
+ };
+
+ // Simulate typing a description
+ console.log('Simulating description input...');
+ this.tmpIndicatorReference_referenceDescription = 'Simulated description';
+
+ // Manually call the change handlers to see if they work
+ console.log('Manually calling change handlers...');
+ this.onIndicatorSelected();
+ this.onIndicatorDescriptionChanged();
+
+ // Check final state
+ console.log('=== Final state after simulation ===');
+ this.checkButtonState();
+ }
+
+ 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 || 'INSERTION',
+ "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.getRegionalReferenceValues();
+ 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 {
+ // Validate critical fields before sending
+ if (!this.indicatorCreationType?.apiName) {
+ console.error('Creation type is missing, attempting to set fallback...');
+ if (this.kommonitorDataExchangeService.indicatorCreationTypeOptions && this.kommonitorDataExchangeService.indicatorCreationTypeOptions.length > 0) {
+ // Check what structure the service is returning
+ const firstOption = this.kommonitorDataExchangeService.indicatorCreationTypeOptions[0];
+ console.log('Service returned creation type option:', firstOption);
+
+ if (firstOption.apiName) {
+ // Service has correct structure, find INSERTION
+ const insertionOption = this.kommonitorDataExchangeService.indicatorCreationTypeOptions.find(option => option.apiName === 'INSERTION');
+ if (insertionOption) {
+ this.indicatorCreationType = insertionOption;
+ console.log('Fallback creation type set to INSERTION:', this.indicatorCreationType);
+ } else {
+ this.indicatorCreationType = firstOption;
+ console.log('Fallback creation type set to first option:', this.indicatorCreationType);
+ }
+ } else if (firstOption.value === 'manual') {
+ // Service has different structure, convert to expected format
+ this.indicatorCreationType = {
+ displayName: firstOption.label || 'Manuell',
+ apiName: 'INSERTION'
+ };
+ console.log('Fallback: Converted structure and set default creation type to INSERTION:', this.indicatorCreationType);
+ } else {
+ // Unknown structure, create safe fallback
+ this.indicatorCreationType = {
+ displayName: 'Manuell',
+ apiName: 'INSERTION'
+ };
+ console.log('Fallback: Created safe default creation type:', this.indicatorCreationType);
+ }
+ } else {
+ this.errorMessage = 'Fehler: Keine gültigen Erstellungstypen verfügbar.';
+ this.errorMessagePart = 'Bitte stellen Sie sicher, dass die Erstellungstypen geladen wurden.';
+ return;
+ }
+ }
+
+ const patchBody = this.buildPatchBody_indicators();
+
+ // Debug: Log the patch body to see what's being sent
+ console.log('Patch body being sent:', patchBody);
+ console.log('Creation type in patch body:', patchBody.creationType);
+ console.log('Current indicatorCreationType:', this.indicatorCreationType);
+
+ this.loadingData = true;
+ this.errorMessage = '';
+ this.successMessage = '';
+ this.errorMessagePart = '';
+ this.successMessagePart = '';
+
+ this.http.patch(
+ this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI + "/indicators/" + this.currentIndicatorDataset.indicatorId,
+ patchBody
+ ).subscribe({
+ next: (response: any) => {
+ this.successMessagePart = this.datasetName;
+ this.successMessage = `Metadaten für Indikator "${this.successMessagePart}" erfolgreich aktualisiert.`;
+
+ // Broadcast refresh events with proper parameters (matching spatial units pattern)
+ this.broadcastService.broadcast('refreshIndicatorOverviewTable', {
+ crudType: 'edit',
+ targetIndicatorId: this.currentIndicatorDataset.indicatorId
+ });
+
+ this.loadingData = false;
+
+ // Auto-close after delay (matching spatial units pattern)
+ setTimeout(() => {
+ this.activeModal.close({ action: 'updated', indicatorId: this.currentIndicatorDataset.indicatorId });
+ }, 2000); // Close after 2 seconds
+ },
+ error: (error: any) => {
+ this.errorMessagePart = error.error ?
+ this.kommonitorDataExchangeService.syntaxHighlightJSON(error.error) :
+ this.kommonitorDataExchangeService.syntaxHighlightJSON(error);
+ this.errorMessage = 'Fehler beim Aktualisieren der Metadaten.';
+ this.loadingData = false;
+ }
+ });
+ }
+
+ hideSuccessAlert(): void {
+ this.successMessage = '';
+ }
+
+ hideErrorAlert(): void {
+ this.errorMessage = '';
+ }
+
+ hideMetadataErrorAlert(): void {
+ this.indicatorAddMetadataImportErrorAlert = false;
+ }
+
+ closeOnSuccess(): void {
+ this.activeModal.close({ action: 'updated', indicatorId: this.currentIndicatorDataset?.indicatorId });
+ }
+
+ cancel(): void {
+ this.activeModal.dismiss();
+ }
+
+ onSubmit(event?: Event): void {
+ // Set default classification method if not already set
+ if (!this.classificationMethod) {
+ this.classificationMethod = (__env?.defaultClassifyMethod || 'jenks');
+ }
+
+ if (event) {
+ event.preventDefault();
+ }
+
+ if (this.isFormValid()) {
+ this.editIndicatorMetadata();
+ }
+ }
+
+ isFormValid(): boolean {
+ return this.indicatorEditMetadataForm ? this.indicatorEditMetadataForm.valid || false : false;
+ }
+
+ // Import/Export methods for indicator metadata
+ onImportIndicatorEditMetadata(): void {
+ this.indicatorMetadataImportError = '';
+ const fileInput = document.getElementById('indicatorEditMetadataImportFile') as HTMLInputElement;
+ if (fileInput) {
+ fileInput.click();
+ }
+ }
+
+ onExportIndicatorEditMetadata(): void {
+ const metadataExport = {
+ metadata: {
+ note: this.metadata.note || '',
+ literature: this.metadata.literature || '',
+ updateInterval: this.metadata.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 || ''
+ },
+ datasetName: this.datasetName || '',
+ abbreviation: this.indicatorAbbreviation || '',
+ indicatorType: this.indicatorType ? this.indicatorType.apiName : '',
+ creationType: this.indicatorCreationType ? this.indicatorCreationType.apiName : '',
+ characteristicValue: this.indicatorCharacteristicValue || '',
+ isHeadlineIndicator: this.isHeadlineIndicator || false,
+ unit: this.indicatorUnit || '',
+ processDescription: this.indicatorProcessDescription || '',
+ tags: this.indicatorTagsString_withCommas ? this.indicatorTagsString_withCommas.split(',').map(tag => tag.trim()) : [],
+ interpretation: this.indicatorInterpretation || '',
+ lowestSpatialUnitForComputation: this.indicatorLowestSpatialUnitMetadataObjectForComputation ? this.indicatorLowestSpatialUnitMetadataObjectForComputation.spatialUnitLevel : '',
+ topicReference: this.getTopicReference(),
+ refrencesToOtherIndicators: this.getIndicatorReferences(),
+ refrencesToGeoresources: this.getGeoresourceReferences(),
+ defaultClassificationMapping: this.getDefaultClassificationMapping()
+ };
+
+ const metadataJSON = JSON.stringify(metadataExport, null, 2);
+ const fileName = `Indikator_Metadaten_Export${this.datasetName ? '-' + this.datasetName : ''}.json`;
+ this.downloadFile(metadataJSON, fileName);
+ }
+
+ private getTopicReference(): string {
+ if (this.indicatorTopic_subsubsubTopic) {
+ return this.indicatorTopic_subsubsubTopic.topicId;
+ } else if (this.indicatorTopic_subsubTopic) {
+ return this.indicatorTopic_subsubTopic.topicId;
+ } else if (this.indicatorTopic_subTopic) {
+ return this.indicatorTopic_subTopic.topicId;
+ } else if (this.indicatorTopic_mainTopic) {
+ return this.indicatorTopic_mainTopic.topicId;
+ }
+ return '';
+ }
+
+ private getIndicatorReferences(): any[] {
+ const references: any[] = [];
+ if (this.indicatorReferences_adminView && this.indicatorReferences_adminView.length > 0) {
+ for (const indicRef of this.indicatorReferences_adminView) {
+ references.push({
+ indicatorId: indicRef.referencedIndicatorId,
+ referenceDescription: indicRef.referencedIndicatorDescription
+ });
+ }
+ }
+ return references;
+ }
+
+ private getGeoresourceReferences(): any[] {
+ const references: any[] = [];
+ if (this.georesourceReferences_adminView && this.georesourceReferences_adminView.length > 0) {
+ for (const geoRef of this.georesourceReferences_adminView) {
+ references.push({
+ georesourceId: geoRef.referencedGeoresourceId,
+ referenceDescription: geoRef.referencedGeoresourceDescription
+ });
+ }
+ }
+ return references;
+ }
+
+ private downloadFile(content: string, fileName: string): void {
+ 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);
+ }
+
+ private getDefaultClassificationMapping(): any {
+ if (this.spatialUnitClassification && this.spatialUnitClassification.length > 0) {
+ return {
+ colorBrewerSchemeName: this.selectedColorBrewerPaletteEntry ? this.selectedColorBrewerPaletteEntry.paletteName : '',
+ items: this.spatialUnitClassification.map(classification => ({
+ defaultCustomRating: classification.customRating || '',
+ defaultColorAsHex: classification.colorAsHex || ''
+ }))
+ };
+ }
+ return null;
+ }
+
+ // Method to set the current indicator dataset (called by parent component)
+ setCurrentIndicatorDataset(indicatorDataset: any): void {
+ console.log('Setting current indicator dataset:', indicatorDataset);
+ this.currentIndicatorDataset = indicatorDataset;
+
+ if (this.currentIndicatorDataset) {
+ this.resetIndicatorEditMetadataForm();
+ // Initialize regional reference values table
+ this.initializeRegionalReferenceValuesTable();
+ }
+ }
+
+ // Debug method to set a test dataset
+ setTestDataset(): void {
+ console.log('Setting test dataset...');
+ const testDataset = {
+ indicatorId: 'test-123',
+ indicatorName: 'Test Indicator',
+ indicatorType: 'STATUS_ABSOLUTE',
+ abbreviation: 'TI',
+ unit: 'percent',
+ processDescription: 'Test process description',
+ tags: ['test', 'debug'],
+ interpretation: 'Test interpretation',
+ creationType: 'INSERTION',
+ metadata: {
+ note: 'Test note',
+ literature: 'Test literature',
+ updateInterval: 'MONTHLY',
+ datasource: 'Test source',
+ databasis: 'Test basis',
+ contact: 'test@example.com',
+ description: 'Test description',
+ lastUpdate: '2024-01-01'
+ }
+ };
+
+ this.setCurrentIndicatorDataset(testDataset);
+ }
+
+ // Step 6: Regional Reference Values Methods
+
+ // Initialize regional reference values management table
+ initializeRegionalReferenceValuesTable(): void {
+ if (this.currentIndicatorDataset?.applicableDates && this.currentIndicatorDataset?.regionalReferenceValues) {
+ // Create ag-Grid compatible structure
+ this.regionalReferenceValuesManagementTableOptions = {
+ columnDefs: [
+ { field: 'referenceDate', headerName: 'Referenzdatum', sortable: true, filter: true },
+ { field: 'regionalSum', headerName: 'Regionale Summe', sortable: true, filter: true },
+ { field: 'regionalAverage', headerName: 'Regionales Mittel', sortable: true, filter: true },
+ { field: 'spatiallyUnassignable', headerName: 'Räumlich nicht zuordenbar', sortable: true, filter: true }
+ ],
+ rowData: this.currentIndicatorDataset.regionalReferenceValues || [],
+ pagination: true,
+ paginationPageSize: 10,
+ domLayout: 'autoHeight',
+ defaultColDef: {
+ resizable: true,
+ sortable: true,
+ filter: true
+ }
+ };
+ }
+ }
+
+ // File dialog methods
+ openFileDialog(): void {
+ this.fileInput.nativeElement.click();
+ }
+
+ onFileSelected(event: any): void {
+ const file = event.target.files[0];
+ if (file && file.type === 'text/csv') {
+ this.file_regionalReferenceValuesImport = file;
+ this.csvProcessingStatus = { type: 'info', message: 'CSV-Datei wird verarbeitet...' };
+ this.processCSVFile(file);
+ } else {
+ this.csvProcessingStatus = { type: 'error', message: 'Ungültiger Dateityp. Bitte wählen Sie eine CSV-Datei aus.' };
+ console.error('Invalid file type. Please select a CSV file.');
+ }
+ }
+
+ // Drag and drop methods
+ onDragOver(event: DragEvent): void {
+ event.preventDefault();
+ this.isDragOver = true;
+ }
+
+ onDrop(event: DragEvent): void {
+ event.preventDefault();
+ this.isDragOver = false;
+
+ const files = event.dataTransfer?.files;
+ if (files && files.length > 0) {
+ const file = files[0];
+ if (file.type === 'text/csv') {
+ this.file_regionalReferenceValuesImport = file;
+ this.csvProcessingStatus = { type: 'info', message: 'CSV-Datei wird verarbeitet...' };
+ this.processCSVFile(file);
+ } else {
+ this.csvProcessingStatus = { type: 'error', message: 'Ungültiger Dateityp. Bitte legen Sie eine CSV-Datei ab.' };
+ console.error('Invalid file type. Please drop a CSV file.');
+ }
+ }
+ }
+
+ // Process CSV file and extract schema
+ private processCSVFile(file: File): void {
+ const reader = new FileReader();
+ reader.onload = (e: any) => {
+ const csvContent = e.target.result;
+ const lines = csvContent.split('\n');
+ if (lines.length > 0) {
+ const headers = lines[0].split(',').map((header: string) => header.trim());
+ this.tmpIndicatorRegionalReferenceValuesObject = {
+ featureSchema: headers,
+ TIMESTAMP_ATTRIBUTE: headers[0] || '',
+ REGIONAL_SUM_ATTRIBUTE: headers[1] || '',
+ REGIONAL_MEAN_ATTRIBUTE: headers[2] || '',
+ SPATIALLY_UNASSIGNABLE: headers[3] || ''
+ };
+ this.csvProcessingStatus = { type: 'success', message: `CSV-Schema erfolgreich geladen. ${headers.length} Spalten gefunden.` };
+ } else {
+ this.csvProcessingStatus = { type: 'error', message: 'CSV-Datei konnte nicht verarbeitet werden. Überprüfen Sie das Format.' };
+ }
+ };
+ reader.onerror = () => {
+ this.csvProcessingStatus = { type: 'error', message: 'Fehler beim Lesen der CSV-Datei.' };
+ };
+ reader.readAsText(file);
+ }
+
+ // Load CSV data into the system
+ loadCSV_indicatorRegionalReferenceValues(): void {
+ if (!this.tmpIndicatorRegionalReferenceValuesObject?.TIMESTAMP_ATTRIBUTE) {
+ this.csvProcessingStatus = { type: 'error', message: 'Zeitstempel-Spalte ist erforderlich.' };
+ console.error('Timestamp attribute is required');
+ return;
+ }
+
+ if (!this.file_regionalReferenceValuesImport) {
+ this.csvProcessingStatus = { type: 'error', message: 'Keine Datei ausgewählt.' };
+ console.error('No file selected');
+ return;
+ }
+
+ this.csvProcessingStatus = { type: 'info', message: 'CSV-Daten werden geladen...' };
+
+ // Process the CSV file and add to regional reference values
+ const reader = new FileReader();
+ reader.onload = (e: any) => {
+ const csvContent = e.target.result;
+ const lines = csvContent.split('\n');
+
+ if (lines.length < 2) {
+ this.csvProcessingStatus = { type: 'error', message: 'CSV-Datei muss mindestens eine Kopfzeile und eine Datenzeile haben.' };
+ console.error('CSV file must have at least a header and one data row');
+ return;
+ }
+
+ const headers = lines[0].split(',').map((header: string) => header.trim());
+ const timestampIndex = headers.indexOf(this.tmpIndicatorRegionalReferenceValuesObject.TIMESTAMP_ATTRIBUTE);
+ const sumIndex = headers.indexOf(this.tmpIndicatorRegionalReferenceValuesObject.REGIONAL_SUM_ATTRIBUTE);
+ const meanIndex = headers.indexOf(this.tmpIndicatorRegionalReferenceValuesObject.REGIONAL_MEAN_ATTRIBUTE);
+ const unassignableIndex = headers.indexOf(this.tmpIndicatorRegionalReferenceValuesObject.SPATIALLY_UNASSIGNABLE);
+
+ if (timestampIndex === -1) {
+ this.csvProcessingStatus = { type: 'error', message: 'Zeitstempel-Spalte nicht gefunden.' };
+ console.error('Timestamp column not found');
+ return;
+ }
+
+ // Process data rows
+ const newReferenceValues: any[] = [];
+ for (let i = 1; i < lines.length; i++) {
+ if (lines[i].trim()) {
+ const values = lines[i].split(',').map((value: string) => value.trim());
+ const referenceValue = {
+ referenceDate: values[timestampIndex],
+ regionalSum: sumIndex !== -1 ? parseFloat(values[sumIndex]) || 0 : 0,
+ regionalAverage: meanIndex !== -1 ? parseFloat(values[meanIndex]) || 0 : 0,
+ spatiallyUnassignable: unassignableIndex !== -1 ? parseFloat(values[unassignableIndex]) || 0 : 0
+ };
+ newReferenceValues.push(referenceValue);
+ }
+ }
+
+ // Add to existing regional reference values
+ if (!this.currentIndicatorDataset.regionalReferenceValues) {
+ this.currentIndicatorDataset.regionalReferenceValues = [];
+ }
+
+ this.currentIndicatorDataset.regionalReferenceValues.push(...newReferenceValues);
+
+ // Refresh the table
+ this.initializeRegionalReferenceValuesTable();
+
+ // Reset file selection
+ this.file_regionalReferenceValuesImport = null;
+ this.tmpIndicatorRegionalReferenceValuesObject = undefined;
+
+ this.csvProcessingStatus = { type: 'success', message: `${newReferenceValues.length} regionale Vergleichswerte erfolgreich geladen.` };
+ console.log(`Successfully loaded ${newReferenceValues.length} regional reference values`);
+ };
+ reader.onerror = () => {
+ this.csvProcessingStatus = { type: 'error', message: 'Fehler beim Lesen der CSV-Datei.' };
+ };
+ reader.readAsText(this.file_regionalReferenceValuesImport);
+ }
+
+ // Get regional reference values for form submission
+ private getRegionalReferenceValues(): any[] {
+ if (this.regionalReferenceValuesManagementTableOptions?.api) {
+ const referenceValues: any[] = [];
+ this.regionalReferenceValuesManagementTableOptions.api.forEachNode((node: any, index: number) => {
+ referenceValues.push(node.data);
+ });
+ return referenceValues;
+ }
+ return this.currentIndicatorDataset?.regionalReferenceValues || [];
+ }
+
+ // Initialize special fields for regional reference values
+ initSpecialFields(indicatorRegionalReferenceValuesObject: any): void {
+ if (indicatorRegionalReferenceValuesObject?.featureSchema) {
+ // Add none option to schema
+ indicatorRegionalReferenceValuesObject.featureSchema.splice(0, 0, this.noneColumnValue);
+
+ // Set default attributes
+ indicatorRegionalReferenceValuesObject.TIMESTAMP_ATTRIBUTE = indicatorRegionalReferenceValuesObject.featureSchema[0];
+ indicatorRegionalReferenceValuesObject.REGIONAL_SUM_ATTRIBUTE = indicatorRegionalReferenceValuesObject.featureSchema[1] || this.noneColumnValue;
+ indicatorRegionalReferenceValuesObject.REGIONAL_MEAN_ATTRIBUTE = indicatorRegionalReferenceValuesObject.featureSchema[2] || this.noneColumnValue;
+ indicatorRegionalReferenceValuesObject.SPATIALLY_UNASSIGNABLE = indicatorRegionalReferenceValuesObject.featureSchema[3] || this.noneColumnValue;
+ }
+ }
+
+}
\ No newline at end of file
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 @@
+
\ 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..60ac1ebc8
--- /dev/null
+++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/admin-spatial-units-management.component.ts
@@ -0,0 +1,748 @@
+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 {
+
+
+ // Subscribe to spatial units data
+ const spatialUnitsSub = this.kommonitorDataExchangeService.spatialUnits$.subscribe(spatialUnits => {
+ if (spatialUnits && spatialUnits.length > 0) {
+ this.loadingData = false;
+ this.initializationCompleted = true;
+ this.buildDataGrid_spatialUnits(spatialUnits);
+ } else {
+ }
+ });
+ this.subscriptions.push(spatialUnitsSub);
+
+ // Subscribe to loading state
+ const loadingSub = this.kommonitorDataExchangeService.loading$.subscribe(loading => {
+ this.loadingData = loading;
+ });
+ this.subscriptions.push(loadingSub);
+
+ // Subscribe to error state
+ const errorSub = this.kommonitorDataExchangeService.error$.subscribe(error => {
+ if (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) {
+ this.fetchSpatialUnitsData();
+
+ // If still no data after fallback, stop loading anyway
+ if (!this.kommonitorDataExchangeService.availableSpatialUnits || this.kommonitorDataExchangeService.availableSpatialUnits.length === 0) {
+ 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 {
+
+ // Get current roles or use empty array as fallback
+ const currentRoles = this.kommonitorDataExchangeService.currentKeycloakLoginRoles || [];
+
+ this.kommonitorDataExchangeService.fetchSpatialUnitsMetadata(currentRoles).subscribe({
+ next: (spatialUnits) => {
+ // The data will be handled by the subscription in ngOnInit
+ },
+ error: (error) => {
+ this.loadingData = false;
+ this.initializationCompleted = true;
+ }
+ });
+ }
+
+ public initializeOrRefreshOverviewTable(): void {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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 = '';
+ 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..7061f3260
--- /dev/null
+++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitAddModal/spatial-unit-add-modal.component.css
@@ -0,0 +1,954 @@
+/* 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;
+}
+
+/* Keep color picker stacked vertically at all sizes */
+.outline-color-group {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+}
+
+.outline-color-group label {
+ margin-bottom: 6px;
+ white-space: normal;
+}
+
+.outline-color-group km-color-picker {
+ display: block;
+}
\ 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..84cbb34ac
--- /dev/null
+++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitAddModal/spatial-unit-add-modal.component.html
@@ -0,0 +1,873 @@
+
+
+
+
+
+
+
+
+
+ = 1" [class.clickable]="true" (click)="goToStep(1)" style="width: 33.33%; cursor: pointer;">Metadaten der Raumebene
+ = 2" [class.clickable]="true" (click)="goToStep(2)" style="width: 33.33%; cursor: pointer;">Allgemeine Metadaten
+ = 3" [class.clickable]="true" (click)="goToStep(3)" style="width: 33.33%; cursor: pointer;">Räumlicher Datensatz
+
+
+ = 1" [class.clickable]="true" (click)="goToStep(1)" style="width: 25%; cursor: pointer;">Metadaten der Raumebene
+ = 2" [class.clickable]="true" (click)="goToStep(2)" style="width: 25%; cursor: pointer;">Allgemeine Metadaten
+ = 3" [class.clickable]="true" (click)="goToStep(3)" style="width: 25%; cursor: pointer;">Zugriffsschutz und Eigentümerschaft
+ = 4" [class.clickable]="true" (click)="goToStep(4)" style="width: 25%; cursor: pointer;">Räumlicher Datensatz
+
+
+
+
+ Metadaten der Raumebene
+ Angaben über die neue Raumebene
+ * = Pflichtfeld
+
+
+
+
+
+
+
+
+
+
+ Allgemeine Metadaten
+ Angaben über allgemeine Metadaten in KomMonitor
+ * = Pflichtfeld
+
+
+
+
+
+
+
+
+
+
+
+ Zugriffsschutz und Eigentümerschaft
+ Vergabe der Zugriffsrechte auf Datensatz-Metadaten und -Features pro Organisationseinheit
+
+
+
+
+ Bitte wählen Sie zunächst die Organisationseinheit aus, unter welcher Sie den Datensatz anlegen möchten
+
+
+
Eigentümer-Organisationseinheit*
+
+
+
+
+
+ -- Eigentümer-Organisationseinheit wählen --
+ {{org.name}}
+
+
+ -- Eigentümer-Organisationseinheit wählen --
+ {{org.name}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mapping-Import
+
+
+
+ Mapping-Export
+
+
+ Räumlicher Datensatz
+ Angaben über den räumlichen Datensatz, aus dem die Raumeinheiten importiert werden
+ * = Pflichtfeld
+
+
+
+
+
+
+
+
+
+
Datei*
+
+
+
Ausgewählt: {{selectedDataSourceFile.name}}
+
+
+
+
Räumlicher Filter*
+
+ -- Filter wählen --
+ Referenzraumebene
+ Manueller Begrenzungsrahmen
+
+
+
+
+
+ -- Raumebene wählen --
+
+ {{spatialUnit.spatialUnitLevel}}
+
+
+
+
Raumebene aus welchem ein umschließendes Rechteck extrahiert wird
+
+
+
+
Begrenzungsrahmen*
+
+
+
Minimale x-Koordinate
+
+
+
+
Minimale y-Koordinate
+
+
+
+
Maximale x-Koordinate
+
+
+
+
Maximale y-Koordinate
+
+
+
+
+
+
+
+
Eingabe ungültig. Grund: {{spatialUnitDataSourceInputInvalidReason}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Übersicht der definierten Attribut-Mappings
+
+
+
+ Editierfunktionen
+ Quell-Attributname im Datensatz
+ Ziel-Attributname nach Import
+ Datentyp
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{attributeMappingEntry.sourceName}}
+ {{attributeMappingEntry.destinationName}}
+ {{attributeMappingEntry.dataType.displayName}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
×
+
Raumebene registriert
+
Eine neue Raumebene mit Namen {{successMessagePart}} wurde in KomMonitor registriert und in die Übersichtstabelle eingetragen.
+
0">
+ {{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:
+
+
+
+
Bitte beheben Sie die angezeigten Fehler im Datensatz und wiederholen den Prozess.
+
+
+
+
+
+
+
+
+
×
+
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..11a056dfb
--- /dev/null
+++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitAddModal/spatial-unit-add-modal.component.ts
@@ -0,0 +1,1401 @@
+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 { KmLinePatternPickerComponent, LinePatternOption } from '../../../customElements/line-pattern-picker/km-line-pattern-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;
+
+ // 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;
+ selectedDataSourceFile: File | null = 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: LinePatternOption | null = null;
+ spatialUnitMetadataStructure_pretty: string = '';
+ spatialUnitMappingConfigStructure: any = {};
+
+ // Role form visibility
+ showRoleForm = false;
+
+ // Color picker handled by km-color-picker
+ // Line pattern picker handled by km-line-pattern-picker
+
+ // 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;
+ }
+
+ get availableLinePatternOptions(): LinePatternOption[] {
+ return (this.kommonitorDataExchangeService.availableLoiDashArrayObjects || []).map(option => ({
+ label: option.label,
+ dashArrayValue: option.dashArrayValue,
+ svgString: option.svgString
+ }));
+ }
+
+ 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 {
+ }
+
+ // 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) {
+
+ }
+
+ // 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) => {
+ // 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() {
+ const availableOptions = this.kommonitorDataExchangeService.availableLoiDashArrayObjects || [];
+ if (availableOptions.length > 0) {
+ this.selectedOutlineDashArrayObject = {
+ label: availableOptions[0].label,
+ dashArrayValue: availableOptions[0].dashArrayValue,
+ svgString: availableOptions[0].svgString
+ };
+ } else {
+ this.selectedOutlineDashArrayObject = null;
+ }
+ this.availableLoiDashArrayObjects = availableOptions;
+ }
+
+ 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) {
+
+ }
+ }
+
+ // 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.selectedDataSourceFile = null;
+ 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 = {};
+ }
+
+ onSpatialUnitFileSelected(event: any) {
+ const file = event?.target?.files?.[0] as File | undefined;
+ this.selectedDataSourceFile = file ?? null;
+ }
+
+ onChangeOutlineDashArray(outlineDashArrayObject: LinePatternOption | null) {
+
+ // Handle outline dash array change
+ this.selectedOutlineDashArrayObject = outlineDashArrayObject;
+ this.outlineDashArray = outlineDashArrayObject;
+
+ // No need to update dropdown display or close dropdown - handled by km-line-pattern-picker
+ }
+
+
+ // 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
+
+
+ // 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) {
+
+ }
+
+ 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') {
+ // Use persisted file across step changes
+ let file: File | undefined | null = this.selectedDataSourceFile;
+ if (!file) {
+ const inputEl = this.spatialUnitDataSourceInput?.nativeElement as HTMLInputElement | undefined;
+ 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) {
+
+ 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) {
+
+ // 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) {
+
+ 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) {
+ 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) {
+ 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) {
+ 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) {
+ 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 = {
+ label: option.label,
+ dashArrayValue: option.dashArrayValue,
+ svgString: option.svgString
+ };
+ this.onChangeOutlineDashArray(this.selectedOutlineDashArrayObject);
+ }
+ });
+
+ // Line pattern picker will handle the display automatically
+
+ 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) {
+ 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;
+
+ // Line pattern picker will handle the display automatically
+ }
+
+ 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;
+ const availableOptions = this.kommonitorDataExchangeService.availableLoiDashArrayObjects || [];
+ if (availableOptions.length > 0) {
+ this.selectedOutlineDashArrayObject = {
+ label: availableOptions[0].label,
+ dashArrayValue: availableOptions[0].dashArrayValue,
+ svgString: availableOptions[0].svgString
+ };
+ } else {
+ this.selectedOutlineDashArrayObject = null;
+ }
+ this.spatialUnitMappingConfigStructure = {};
+
+ // Line pattern picker will handle the display automatically
+
+ this.converter = null;
+ this.schema = '';
+ this.mimeType = '';
+ this.datasourceType = null;
+ this.selectedDataSourceFile = 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() {
+ 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 @@
+
+
+
+
+
+
Sollen die folgenden Raumebenen wirklich gelöscht werden?
+
+
Dabei werden auch sämtliche Indikatoren-Datensätze der angezeigten Raumebenen dauerhaft aus dem System entfernt.
+
+
0">
+
+ {{dataset.spatialUnitLevel}}
+
+
+
+
+
kein Datensatz zum Löschen markiert. Mindestens ein Datensatz muss markiert werden.
+
+
+
+
+
+
+
×
+
Folgende Raumebenen wurde erfolgreich gelöscht
+
+ {{dataset.spatialUnitLevel}}
+
+
+
+
+
×
+
Löschen gescheitert
+ Folgende Datensätze konnten nicht gelöscht werden.
+
+
+
+
+ Name
+ Fehlermeldung
+
+
+
+
+ {{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..604af3824
--- /dev/null
+++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitDeleteModal/spatial-unit-delete-modal.component.ts
@@ -0,0 +1,157 @@
+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
+ ) {
+ }
+
+ 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) {
+ 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..d8799f35d
--- /dev/null
+++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditFeaturesModal/spatial-unit-edit-features-modal.component.html
@@ -0,0 +1,537 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = 1" [class.clickable]="true" (click)="goToStep(1)" style="width: 50%; cursor: pointer;">Raumeinheit Übersicht
+ = 2" [class.clickable]="true" (click)="goToStep(2)" style="width: 50%; cursor: pointer;">Räumlicher Datensatz
+
+
+
+
+
+ Raumeinheit Übersicht
+ Optionale Anzeige der Raumeinheits-Details
+
+
+
+
+
+ Letztes erfolgreiches Update eines Einzeleintrags
+ {{ kommonitorDataGridHelperService.featureTable_spatialUnit_lastUpdate_timestamp_success | date:'medium' }}
+
+
+ Letztes gescheitertes Update eines Einzeleintrags
+ {{ kommonitorDataGridHelperService.featureTable_spatialUnit_lastUpdate_timestamp_failure | date:'medium' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mapping-Import
+
+
+
+ Mapping-Export
+
+
+
+ Räumlicher Datensatz
+ Angaben über den räumlichen Datensatz, aus dem die Raumeinheiten importiert werden
+
+ * = Pflichtfeld
+
+
+
+
+
+ Geodaten-Quellformat*
+
+ -- Quellformat wählen --
+
+ {{ conv.name }}
+
+
+
+
+
+ Schema*
+
+ -- Schema wählen --
+
+ {{ schemaOption }}
+
+
+
+
+
+ Quellformat*
+
+ -- Format wählen --
+
+ {{ mimeTypeOption }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Datenquelltyp*
+
+ -- Quelltyp wählen --
+
+ {{ dsType.type }}
+
+
+
+
+
+
Datei*
+
+
+
Ausgewählt: {{ selectedDataSourceFile.name }}
+
+
+
+
+
Räumlicher Filter*
+
+ -- Filter wählen --
+ Referenzraumebene
+ Manueller Begrenzungsrahmen
+
+
+
+
+
+
+
+
+
+
+
Eingabe ungültig. Grund: {{ spatialUnitEditFeaturesDataSourceInputInvalidReason }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Attributname im zu importierenden Datensatz*
+
+
+
+
+ Ziel-Attributname in Datenbank nach Import*
+
+
+
+
+ Datentyp*
+
+ -- Datentyp wählen --
+
+ {{ type.displayName }}
+
+
+
+
+
+
+ Hinzufügen/Editieren
+
+
+
+
+
+
Übersicht der definierten Attribut-Mappings
+
+
+
+ Editierfunktionen
+ Quell-Attributname im Datensatz
+ Ziel-Attributname nach Import
+ Datentyp
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ attributeMappingEntry.sourceName }}
+ {{ attributeMappingEntry.destinationName }}
+ {{ attributeMappingEntry.dataType?.displayName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
×
+
Erfolg!
+ {{ successMessage }}
+
+
Raumebene: {{successMessagePart}}
+
+
+
+
+
+
×
+
Fehler!
+ {{ errorMessage }}
+
+
+
+
0">
+
Bei den {{importerErrors.length}} Raumeinheiten mit folgenden IDs scheitert der Import:
+
+
+
+
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..ad409ec1e
--- /dev/null
+++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditFeaturesModal/spatial-unit-edit-features-modal.component.ts
@@ -0,0 +1,1269 @@
+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;
+ selectedDataSourceFile: File | null = null;
+ 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) {
+ }
+ }
+
+ // 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('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;
+
+ 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;
+ this.selectedDataSourceFile = null;
+ this.fileSelected = false;
+ }
+
+ refreshSpatialUnitEditFeaturesOverviewTable(): void {
+ if (!this.currentSpatialUnitDataset) {
+ 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) => {
+ 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) {
+
+ }
+ }
+
+ // 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') {
+ let file: File | undefined | null = this.selectedDataSourceFile;
+ if (!file) {
+ const inputEl = this.spatialUnitDataSourceInput?.nativeElement as HTMLInputElement | undefined;
+ 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.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 {
+ 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;
+ }
+ }
+ 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)');
+ }
+ }
+ // 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) {
+
+ 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.log('Missing importer objects', {
+ 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('Updating spatial unit', {
+ 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)) {
+ 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 {
+ 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) {
+ this.handleError(error);
+ this.loadingData = false;
+ }
+ }
+
+ onFileSelected(event: any): void {
+ const input = event?.target as HTMLInputElement;
+ if (input && input.files && input.files.length > 0) {
+ this.selectedDataSourceFile = input.files[0];
+ } else {
+ this.selectedDataSourceFile = null;
+ }
+ 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) {
+ 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 {
+ 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..ba7ce4bd8
--- /dev/null
+++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditMetadataModal/spatial-unit-edit-metadata-modal.component.html
@@ -0,0 +1,370 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = 1" [class.clickable]="true" (click)="goToStep(1)" style="width: 50%; cursor: pointer;">Metadaten der Raumebene
+ = 2" [class.clickable]="true" (click)="goToStep(2)" style="width: 50%; cursor: pointer;">Allgemeine Metadaten
+
+
+
+
+ Metadaten der Raumebene
+ Angaben über die Raumebene
+
+ * = Pflichtfeld
+
+
+
+
+
+
+
+
+
+
+
+ Allgemeine Metadaten
+ Angaben über allgemeine Metadaten in KomMonitor
+
+ * = Pflichtfeld
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ×
+
Raumebene aktualisiert
+ Die Metadaten der Raumebene mit Namen {{successMessagePart}} wurden in KomMonitor aktualisiert und in die Übersichtstabelle eingetragen.
+
+
+ Schließen
+
+
+
+
+
+
+
+
\ 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..73ff21d62
--- /dev/null
+++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditMetadataModal/spatial-unit-edit-metadata-modal.component.ts
@@ -0,0 +1,603 @@
+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 { KmLinePatternPickerComponent, LinePatternOption } from '../../../customElements/line-pattern-picker/km-line-pattern-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;
+
+ // 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: LinePatternOption | null = null;
+ selectedoutlineDashArrayObject: LinePatternOption | null = null; // Keep both for compatibility with original
+
+ // Color picker handled by km-color-picker component
+ // Line pattern picker handled by km-line-pattern-picker component
+
+ // 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;
+
+ get availableLinePatternOptions(): LinePatternOption[] {
+ return (this.kommonitorDataExchangeService.availableLoiDashArrayObjects || []).map(option => ({
+ label: option.label,
+ dashArrayValue: option.dashArrayValue,
+ svgString: option.svgString
+ }));
+ }
+
+ 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() {
+ }
+
+ // Remove the complex injection methods - not needed
+ private injectSvgContent() {
+ }
+
+ private checkElementsExist(): boolean {
+ return true;
+ }
+
+ private performSvgInjection() {
+ }
+
+ // 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 = {
+ label: option.label,
+ dashArrayValue: option.dashArrayValue,
+ svgString: option.svgString
+ };
+ this.selectedoutlineDashArrayObject = this.selectedOutlineDashArrayObject;
+ }
+ });
+ if (!this.selectedOutlineDashArrayObject) {
+ const firstOption = this.availableLoiDashArrayObjects[0];
+ this.selectedOutlineDashArrayObject = {
+ label: firstOption.label,
+ dashArrayValue: firstOption.dashArrayValue,
+ svgString: firstOption.svgString
+ };
+ this.selectedoutlineDashArrayObject = this.selectedOutlineDashArrayObject;
+ }
+
+ // Line pattern picker will handle the display automatically
+ }
+
+ // 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: LinePatternOption | null) {
+
+ this.selectedOutlineDashArrayObject = outlineDashArrayObject;
+ this.selectedoutlineDashArrayObject = outlineDashArrayObject; // Keep both for compatibility
+
+ // No need to update dropdown display or close dropdown - handled by km-line-pattern-picker
+ }
+
+
+
+ // 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) {
+
+ 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) {
+ 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) {
+ 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 = {
+ label: option.label,
+ dashArrayValue: option.dashArrayValue,
+ svgString: option.svgString
+ };
+ this.selectedoutlineDashArrayObject = this.selectedOutlineDashArrayObject;
+ }
+ });
+ }
+
+ // 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);
+ }
+
+
+ // km-date-picker handles validation and coercion itself; no blur handler needed
+
+}
\ 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..140fd6f68
--- /dev/null
+++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditUserRolesModal/spatial-unit-edit-user-roles-modal.component.html
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ = 1" [class.clickable]="true" (click)="goToStep(1)" style="width: 50%; cursor: pointer;">Zugriffsschutz
+ = 2" [class.clickable]="true" (click)="goToStep(2)" style="width: 50%; cursor: pointer;">Eigentümerschaft
+
+
+
+
+
+ Zugriffsschutz
+ Vergabe der Zugriffsrechte auf Datensatz-Metadaten und -Features pro Organisationseinheit
+
+ Zugriffsrechte (lesen, editieren) müssen explizit vergeben werden
+
+
+
+
+ Zeige nur zugewiesene Rechte
+
+
+
+
+
+
+
+
Öffentliche Lesefreigabe*
+
+
+
+
+
+
+ Öffentlich freigegebene Datensätze können ohne Login abgerufen werden.
+
+
+
+
+
+
+ Als Eigentümer-Organisation des Datensatzes können Sie Lese- und Editier-Rechte an die eigene und weitere Organisationseinheiten vergeben.
+
+
+
+
+
+
+
+
+ Eigentümerschaft eines Datensatzes
+ Übertragen der Eigentümerschaft von Datensätzen mit allen dazugehörigen Rechten
+
+ Bitte beachten Sie, dass Sie beim Übertragen einer Eigentümerschaft einer Resource unter Umständen jegliche Rechte eben dieser verlieren. Die Rechte werden unwiderruflich und sofort an den neuen Eigentümer übertragen.
+
+
+
+
+
Eigentümerschaft übertragen an
+
+
+
+
+
+ Eigentümerschaft nicht übertragen
+ {{org.name}}
+
+
+ Eigentümerschaft nicht übertragen
+ {{org.name}}
+
+
+
+
aktuelle Eigentümer-Organisationseinheit
+
{{getCurrentOwnerName()}}
+
+
+ ACHTUNG: Sie sind dabei, die Eigentümerschaft an diesem Datensatz zu ändern.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ×
+
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..4e4efd6b7
--- /dev/null
+++ b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditUserRolesModal/spatial-unit-edit-user-roles-modal.component.ts
@@ -0,0 +1,509 @@
+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[] = [];
+ filteredOrganizations: 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();
+ this.updateFilteredOrganizations();
+ }
+
+ 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)
+ );
+ this.updateFilteredOrganizations();
+ }
+ }
+
+ 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();
+ this.updateFilteredOrganizations();
+ }
+
+ 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;
+ // Update shared cache so reopening modal uses fresh data
+ this.kommonitorDataExchangeService.replaceSingleSpatialUnitMetadata(this.currentSpatialUnitDataset as any);
+ }
+ // 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]);
+ // Update local and shared dataset owner so reopening shows correct owner and disabled states
+ if (this.currentSpatialUnitDataset) {
+ this.currentSpatialUnitDataset.ownerId = putBody.ownerId;
+ this.kommonitorDataExchangeService.replaceSingleSpatialUnitMetadata(this.currentSpatialUnitDataset as any);
+ }
+
+ } 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[] {
+ // Deprecated: avoid calling methods from template repeatedly. Use filteredOrganizations instead.
+ return this.filteredOrganizations;
+ }
+
+ onOwnerOrgFilterChange(): void {
+ this.updateFilteredOrganizations();
+ }
+
+ private updateFilteredOrganizations(): void {
+ const base = this.kommonitorDataExchangeService.checkAdminPermission() ?
+ (this.kommonitorDataExchangeService.accessControl || []) :
+ (this.resourcesCreatorRights || []);
+ if (!this.ownerOrgFilter) {
+ this.filteredOrganizations = base.slice();
+ return;
+ }
+ const filter = this.ownerOrgFilter.toLowerCase();
+ this.filteredOrganizations = base.filter((org: any) => org.name?.toLowerCase().includes(filter));
+ }
+
+ 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 {
+ // Prefer the freshest copy from the shared service if available
+ const latest = spatialUnitDataset?.spatialUnitId ? this.kommonitorDataExchangeService.getSpatialUnitMetadataById(spatialUnitDataset.spatialUnitId) : null;
+ this.currentSpatialUnitDataset = latest || 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();
+ }
+ this.updateFilteredOrganizations();
+ } 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();
+ }
+ this.updateFilteredOrganizations();
+ },
+ error: (error) => {
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/components/ngComponents/admin/adminTopicsManagement/admin-topics-management.component.ts b/app/components/ngComponents/admin/adminTopicsManagement/admin-topics-management.component.ts
index c72a03509..808560591 100644
--- a/app/components/ngComponents/admin/adminTopicsManagement/admin-topics-management.component.ts
+++ b/app/components/ngComponents/admin/adminTopicsManagement/admin-topics-management.component.ts
@@ -1,4 +1,6 @@
import { Component, OnInit, OnDestroy, Inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
import { DataExchangeService } from 'services/data-exchange-service/data-exchange.service';
import { BroadcastService } from 'services/broadcast-service/broadcast.service';
import { HttpClient } from '@angular/common/http';
@@ -10,6 +12,8 @@ import { TopicDeleteModalComponent } from './topicDeleteModal/topic-delete-modal
@Component({
selector: 'admin-topics-management-new',
+ standalone: true,
+ imports: [CommonModule, FormsModule],
templateUrl: './admin-topics-management.component.html',
styleUrls: ['./admin-topics-management.component.css']
})
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 @@
+
+
+ {{ color }}
+
+
+
+
+
+
+
+
+
+
+
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/components/ngComponents/customElements/icon-picker/icon-picker.component.css b/app/components/ngComponents/customElements/icon-picker/icon-picker.component.css
new file mode 100644
index 000000000..3cce32de2
--- /dev/null
+++ b/app/components/ngComponents/customElements/icon-picker/icon-picker.component.css
@@ -0,0 +1,413 @@
+.icon-picker-container {
+ position: relative;
+ display: inline-block;
+ width: 100%;
+}
+
+.icon-picker-container.disabled {
+ opacity: 0.6;
+ pointer-events: none;
+}
+
+.icon-picker-button {
+ position: relative;
+ min-width: 150px;
+ text-align: left;
+ padding: 8px 12px;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.icon-picker-button .icon-text {
+ flex: 1;
+ margin-left: 5px;
+}
+
+.icon-picker-button .fa-caret-down {
+ margin-left: 5px;
+}
+
+.icon-picker-dropdown {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ z-index: 9999999;
+ background: white;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.2);
+ max-width: 400px;
+ min-width: 300px;
+ max-height: 400px;
+ overflow: hidden;
+ margin-top: 2px;
+}
+
+.icon-picker-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 10px 15px;
+ background-color: #f5f5f5;
+ border-bottom: 1px solid #ddd;
+}
+
+.icon-picker-title {
+ font-weight: bold;
+ font-size: 14px;
+}
+
+.icon-picker-header .close {
+ background: none;
+ border: none;
+ font-size: 18px;
+ cursor: pointer;
+ padding: 0;
+ margin: 0;
+ color: #999;
+}
+
+.icon-picker-header .close:hover {
+ color: #333;
+}
+
+.icon-picker-search {
+ padding: 10px 15px;
+ border-bottom: 1px solid #eee;
+}
+
+.icon-picker-search input {
+ width: 100%;
+ padding: 6px 12px;
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ font-size: 14px;
+}
+
+.icon-picker-content {
+ max-height: 250px;
+ overflow-y: auto;
+ padding: 10px;
+}
+
+.icon-grid {
+ display: grid;
+ gap: 5px;
+ grid-template-rows: repeat(var(--rows, 6), 1fr);
+}
+
+.icon-item {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ border: 1px solid #ddd;
+ background: white;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ padding: 0;
+ margin: 0;
+ font-size: 16px;
+ color: #333;
+}
+
+.icon-item:hover {
+ background-color: #f0f0f0;
+ border-color: #999;
+}
+
+.icon-item.selected {
+ background-color: #5cb85c;
+ border-color: #4cae4c;
+ color: white;
+}
+
+.icon-item i {
+ font-size: 16px;
+}
+
+.icon-picker-pagination {
+ padding: 10px 15px;
+ border-top: 1px solid #eee;
+ background-color: #f9f9f9;
+}
+
+.pagination-controls {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+}
+
+.page-info {
+ font-size: 12px;
+ color: #666;
+ min-width: 120px;
+ text-align: center;
+}
+
+.icon-picker-footer {
+ padding: 8px 15px;
+ background-color: #f5f5f5;
+ border-top: 1px solid #ddd;
+ font-size: 12px;
+ color: #666;
+ text-align: center;
+}
+
+/* Ensure glyphicons are visible */
+.glyphicon {
+ font-family: 'Glyphicons Halflings';
+ font-style: normal;
+ font-weight: normal;
+ line-height: 1;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+/* Common glyphicon icons */
+.glyphicon-home:before { content: "\e021"; }
+.glyphicon-star:before { content: "\e050"; }
+.glyphicon-heart:before { content: "\e005"; }
+.glyphicon-user:before { content: "\e008"; }
+.glyphicon-cog:before { content: "\e019"; }
+.glyphicon-search:before { content: "\e003"; }
+.glyphicon-plus:before { content: "\e081"; }
+.glyphicon-minus:before { content: "\e082"; }
+.glyphicon-check:before { content: "\e067"; }
+.glyphicon-remove:before { content: "\e014"; }
+.glyphicon-edit:before { content: "\e065"; }
+.glyphicon-eye:before { content: "\e105"; }
+.glyphicon-download:before { content: "\e026"; }
+.glyphicon-upload:before { content: "\e027"; }
+.glyphicon-folder:before { content: "\e117"; }
+.glyphicon-file:before { content: "\e022"; }
+.glyphicon-calendar:before { content: "\e109"; }
+.glyphicon-time:before { content: "\e023"; }
+.glyphicon-map-marker:before { content: "\e062"; }
+.glyphicon-phone:before { content: "\e145"; }
+.glyphicon-envelope:before { content: "\e270"; }
+.glyphicon-globe:before { content: "\e135"; }
+.glyphicon-lock:before { content: "\e033"; }
+.glyphicon-unlock:before { content: "\e034"; }
+.glyphicon-wrench:before { content: "\e136"; }
+.glyphicon-settings:before { content: "\e019"; }
+.glyphicon-info-sign:before { content: "\e086"; }
+.glyphicon-question-sign:before { content: "\e085"; }
+.glyphicon-exclamation-sign:before { content: "\e101"; }
+.glyphicon-warning-sign:before { content: "\e107"; }
+.glyphicon-ok:before { content: "\e013"; }
+.glyphicon-remove-circle:before { content: "\e088"; }
+.glyphicon-ok-circle:before { content: "\e089"; }
+.glyphicon-ban-circle:before { content: "\e090"; }
+.glyphicon-arrow-left:before { content: "\e091"; }
+.glyphicon-arrow-right:before { content: "\e092"; }
+.glyphicon-arrow-up:before { content: "\e093"; }
+.glyphicon-arrow-down:before { content: "\e094"; }
+.glyphicon-chevron-left:before { content: "\e079"; }
+.glyphicon-chevron-right:before { content: "\e080"; }
+.glyphicon-chevron-up:before { content: "\e113"; }
+.glyphicon-chevron-down:before { content: "\e114"; }
+.glyphicon-play:before { content: "\e072"; }
+.glyphicon-pause:before { content: "\e073"; }
+.glyphicon-stop:before { content: "\e074"; }
+.glyphicon-record:before { content: "\e075"; }
+.glyphicon-volume-up:before { content: "\e076"; }
+.glyphicon-volume-down:before { content: "\e077"; }
+.glyphicon-volume-off:before { content: "\e078"; }
+.glyphicon-headphones:before { content: "\e125"; }
+.glyphicon-music:before { content: "\e126"; }
+.glyphicon-film:before { content: "\e127"; }
+.glyphicon-camera:before { content: "\e128"; }
+.glyphicon-picture:before { content: "\e060"; }
+.glyphicon-thumbs-up:before { content: "\e129"; }
+.glyphicon-thumbs-down:before { content: "\e130"; }
+.glyphicon-hand-up:before { content: "\e131"; }
+.glyphicon-hand-down:before { content: "\e132"; }
+.glyphicon-hand-right:before { content: "\e133"; }
+.glyphicon-hand-left:before { content: "\e134"; }
+.glyphicon-resize-full:before { content: "\e096"; }
+.glyphicon-resize-small:before { content: "\e097"; }
+.glyphicon-fullscreen:before { content: "\e140"; }
+.glyphicon-resize-vertical:before { content: "\e120"; }
+.glyphicon-resize-horizontal:before { content: "\e119"; }
+.glyphicon-move:before { content: "\e118"; }
+.glyphicon-zoom-in:before { content: "\e116"; }
+.glyphicon-zoom-out:before { content: "\e115"; }
+.glyphicon-off:before { content: "\e017"; }
+.glyphicon-signal:before { content: "\e018"; }
+.glyphicon-trash:before { content: "\e020"; }
+.glyphicon-list:before { content: "\e012"; }
+.glyphicon-list-alt:before { content: "\e032"; }
+.glyphicon-indent-left:before { content: "\e110"; }
+.glyphicon-indent-right:before { content: "\e111"; }
+.glyphicon-text-width:before { content: "\e112"; }
+.glyphicon-text-height:before { content: "\e121"; }
+.glyphicon-align-left:before { content: "\e122"; }
+.glyphicon-align-center:before { content: "\e123"; }
+.glyphicon-align-right:before { content: "\e124"; }
+.glyphicon-align-justify:before { content: "\e125"; }
+.glyphicon-font:before { content: "\e126"; }
+.glyphicon-bold:before { content: "\e127"; }
+.glyphicon-italic:before { content: "\e128"; }
+.glyphicon-text-color:before { content: "\e129"; }
+.glyphicon-share:before { content: "\e130"; }
+.glyphicon-share-alt:before { content: "\e131"; }
+.glyphicon-plane:before { content: "\e132"; }
+.glyphicon-random:before { content: "\e133"; }
+.glyphicon-comment:before { content: "\e134"; }
+.glyphicon-magnet:before { content: "\e135"; }
+.glyphicon-retweet:before { content: "\e136"; }
+.glyphicon-shopping-cart:before { content: "\e137"; }
+.glyphicon-folder-close:before { content: "\e138"; }
+.glyphicon-folder-open:before { content: "\e139"; }
+.glyphicon-hdd:before { content: "\e140"; }
+.glyphicon-bullhorn:before { content: "\e141"; }
+.glyphicon-bell:before { content: "\e142"; }
+.glyphicon-certificate:before { content: "\e143"; }
+.glyphicon-circle-arrow-right:before { content: "\e144"; }
+.glyphicon-circle-arrow-left:before { content: "\e145"; }
+.glyphicon-circle-arrow-up:before { content: "\e146"; }
+.glyphicon-circle-arrow-down:before { content: "\e147"; }
+.glyphicon-tasks:before { content: "\e148"; }
+.glyphicon-filter:before { content: "\e149"; }
+.glyphicon-briefcase:before { content: "\e150"; }
+.glyphicon-dashboard:before { content: "\e151"; }
+.glyphicon-paperclip:before { content: "\e152"; }
+.glyphicon-heart-empty:before { content: "\e153"; }
+.glyphicon-link:before { content: "\e154"; }
+.glyphicon-pushpin:before { content: "\e155"; }
+.glyphicon-usd:before { content: "\e156"; }
+.glyphicon-gbp:before { content: "\e157"; }
+.glyphicon-sort:before { content: "\e158"; }
+.glyphicon-sort-by-alphabet:before { content: "\e159"; }
+.glyphicon-sort-by-alphabet-alt:before { content: "\e160"; }
+.glyphicon-sort-by-order:before { content: "\e161"; }
+.glyphicon-sort-by-order-alt:before { content: "\e162"; }
+.glyphicon-sort-by-attributes:before { content: "\e163"; }
+.glyphicon-sort-by-attributes-alt:before { content: "\e164"; }
+.glyphicon-unchecked:before { content: "\e165"; }
+.glyphicon-expand:before { content: "\e166"; }
+.glyphicon-collapse-down:before { content: "\e167"; }
+.glyphicon-collapse-up:before { content: "\e168"; }
+.glyphicon-log-in:before { content: "\e169"; }
+.glyphicon-flash:before { content: "\e170"; }
+.glyphicon-log-out:before { content: "\e171"; }
+.glyphicon-new-window:before { content: "\e172"; }
+.glyphicon-save:before { content: "\e173"; }
+.glyphicon-open:before { content: "\e174"; }
+.glyphicon-saved:before { content: "\e175"; }
+.glyphicon-import:before { content: "\e176"; }
+.glyphicon-export:before { content: "\e177"; }
+.glyphicon-send:before { content: "\e178"; }
+.glyphicon-floppy-disk:before { content: "\e179"; }
+.glyphicon-floppy-saved:before { content: "\e180"; }
+.glyphicon-floppy-remove:before { content: "\e181"; }
+.glyphicon-floppy-save:before { content: "\e182"; }
+.glyphicon-floppy-open:before { content: "\e183"; }
+.glyphicon-credit-card:before { content: "\e184"; }
+.glyphicon-transfer:before { content: "\e185"; }
+.glyphicon-cutlery:before { content: "\e186"; }
+.glyphicon-header:before { content: "\e187"; }
+.glyphicon-compressed:before { content: "\e188"; }
+.glyphicon-earphone:before { content: "\e189"; }
+.glyphicon-phone-alt:before { content: "\e190"; }
+.glyphicon-tower:before { content: "\e191"; }
+.glyphicon-stats:before { content: "\e192"; }
+.glyphicon-sd-video:before { content: "\e193"; }
+.glyphicon-hd-video:before { content: "\e194"; }
+.glyphicon-subtitles:before { content: "\e195"; }
+.glyphicon-sound-stereo:before { content: "\e196"; }
+.glyphicon-sound-dolby:before { content: "\e197"; }
+.glyphicon-sound-5-1:before { content: "\e198"; }
+.glyphicon-sound-6-1:before { content: "\e199"; }
+.glyphicon-sound-7-1:before { content: "\e200"; }
+.glyphicon-copyright-mark:before { content: "\e201"; }
+.glyphicon-registration-mark:before { content: "\e202"; }
+.glyphicon-cloud-download:before { content: "\e203"; }
+.glyphicon-cloud-upload:before { content: "\e204"; }
+.glyphicon-tree-conifer:before { content: "\e205"; }
+.glyphicon-tree-deciduous:before { content: "\e206"; }
+.glyphicon-cd:before { content: "\e207"; }
+.glyphicon-save-file:before { content: "\e208"; }
+.glyphicon-open-file:before { content: "\e209"; }
+.glyphicon-level-up:before { content: "\e210"; }
+.glyphicon-copy:before { content: "\e211"; }
+.glyphicon-paste:before { content: "\e212"; }
+.glyphicon-alert:before { content: "\e213"; }
+.glyphicon-equalizer:before { content: "\e214"; }
+.glyphicon-king:before { content: "\e215"; }
+.glyphicon-queen:before { content: "\e216"; }
+.glyphicon-pawn:before { content: "\e217"; }
+.glyphicon-bishop:before { content: "\e218"; }
+.glyphicon-knight:before { content: "\e219"; }
+.glyphicon-baby-formula:before { content: "\e220"; }
+.glyphicon-tent:before { content: "\e221"; }
+.glyphicon-blackboard:before { content: "\e222"; }
+.glyphicon-bed:before { content: "\e223"; }
+.glyphicon-apple:before { content: "\e224"; }
+.glyphicon-erase:before { content: "\e225"; }
+.glyphicon-hourglass:before { content: "\e226"; }
+.glyphicon-lamp:before { content: "\e227"; }
+.glyphicon-duplicate:before { content: "\e228"; }
+.glyphicon-piggy-bank:before { content: "\e229"; }
+.glyphicon-scissors:before { content: "\e230"; }
+.glyphicon-bitcoin:before { content: "\e231"; }
+.glyphicon-btc:before { content: "\e232"; }
+.glyphicon-xbt:before { content: "\e233"; }
+.glyphicon-yen:before { content: "\e234"; }
+.glyphicon-jpy:before { content: "\e235"; }
+.glyphicon-ruble:before { content: "\e236"; }
+.glyphicon-rub:before { content: "\e237"; }
+.glyphicon-scale:before { content: "\e238"; }
+.glyphicon-ice-lolly:before { content: "\e239"; }
+.glyphicon-ice-lolly-tasted:before { content: "\e240"; }
+.glyphicon-education:before { content: "\e241"; }
+.glyphicon-option-horizontal:before { content: "\e242"; }
+.glyphicon-option-vertical:before { content: "\e243"; }
+.glyphicon-menu-hamburger:before { content: "\e244"; }
+.glyphicon-modal-window:before { content: "\e245"; }
+.glyphicon-oil:before { content: "\e246"; }
+.glyphicon-grain:before { content: "\e247"; }
+.glyphicon-sunglasses:before { content: "\e248"; }
+.glyphicon-text-size:before { content: "\e249"; }
+.glyphicon-text-background:before { content: "\e250"; }
+.glyphicon-object-align-top:before { content: "\e251"; }
+.glyphicon-object-align-bottom:before { content: "\e252"; }
+.glyphicon-object-align-horizontal:before { content: "\e253"; }
+.glyphicon-object-align-left:before { content: "\e254"; }
+.glyphicon-object-align-vertical:before { content: "\e255"; }
+.glyphicon-object-align-right:before { content: "\e256"; }
+.glyphicon-triangle-right:before { content: "\e257"; }
+.glyphicon-triangle-left:before { content: "\e258"; }
+.glyphicon-triangle-bottom:before { content: "\e259"; }
+.glyphicon-triangle-top:before { content: "\e260"; }
+.glyphicon-console:before { content: "\e261"; }
+.glyphicon-superscript:before { content: "\e262"; }
+.glyphicon-subscript:before { content: "\e263"; }
+.glyphicon-menu-left:before { content: "\e264"; }
+.glyphicon-menu-right:before { content: "\e265"; }
+.glyphicon-menu-down:before { content: "\e266"; }
+.glyphicon-menu-up:before { content: "\e267"; }
+
+/* Responsive adjustments */
+@media (max-width: 768px) {
+ .icon-picker-dropdown {
+ max-width: 100%;
+ min-width: 100%;
+ }
+
+ .icon-grid {
+ grid-template-columns: repeat(6, 1fr) !important;
+ }
+
+ .icon-item {
+ width: 35px;
+ height: 35px;
+ font-size: 14px;
+ }
+}
diff --git a/app/components/ngComponents/customElements/icon-picker/icon-picker.component.html b/app/components/ngComponents/customElements/icon-picker/icon-picker.component.html
new file mode 100644
index 000000000..d58ef66b1
--- /dev/null
+++ b/app/components/ngComponents/customElements/icon-picker/icon-picker.component.html
@@ -0,0 +1,86 @@
+
+
+
+
+ {{ getDisplayText() }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/components/ngComponents/customElements/icon-picker/icon-picker.component.ts b/app/components/ngComponents/customElements/icon-picker/icon-picker.component.ts
new file mode 100644
index 000000000..8d7ca84b6
--- /dev/null
+++ b/app/components/ngComponents/customElements/icon-picker/icon-picker.component.ts
@@ -0,0 +1,183 @@
+import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+@Component({
+ selector: 'app-icon-picker',
+ standalone: true,
+ imports: [CommonModule, FormsModule],
+ templateUrl: './icon-picker.component.html',
+ styleUrls: ['./icon-picker.component.css']
+})
+export class IconPickerComponent implements OnInit, OnDestroy {
+ @Input() selectedIcon: string = 'home';
+ @Input() disabled: boolean = false;
+ @Input() placeholder: string = 'Select Icon';
+ @Input() buttonClass: string = 'btn btn-info';
+ @Input() showSearch: boolean = true;
+ @Input() showHeader: boolean = true;
+ @Input() showFooter: boolean = true;
+ @Input() cols: number = 10;
+ @Input() rows: number = 6;
+ @Input() searchText: string = 'Search icons...';
+ @Input() labelHeader: string = '{0} of {1} pages';
+ @Input() labelFooter: string = '{0} - {1} of {2} icons';
+
+ @Output() iconChange = new EventEmitter();
+ @Output() iconSelect = new EventEmitter();
+
+ isOpen = false;
+ searchTerm = '';
+
+ // Common glyphicon icons that match the original implementation
+ availableIcons = [
+ 'home', 'star', 'heart', 'user', 'cog', 'search', 'plus', 'minus',
+ 'check', 'remove', 'edit', 'eye', 'download', 'upload', 'folder', 'file',
+ 'calendar', 'time', 'map-marker', 'phone', 'envelope', 'globe', 'lock', 'unlock',
+ 'wrench', 'cog', 'settings', 'info-sign', 'question-sign', 'exclamation-sign',
+ 'warning-sign', 'ok', 'remove-circle', 'ok-circle', 'ban-circle', 'arrow-left',
+ 'arrow-right', 'arrow-up', 'arrow-down', 'chevron-left', 'chevron-right',
+ 'chevron-up', 'chevron-down', 'play', 'pause', 'stop', 'record', 'volume-up',
+ 'volume-down', 'volume-off', 'headphones', 'music', 'film', 'camera', 'picture',
+ 'thumbs-up', 'thumbs-down', 'hand-up', 'hand-down', 'hand-right', 'hand-left',
+ 'resize-full', 'resize-small', 'fullscreen', 'resize-vertical', 'resize-horizontal',
+ 'move', 'zoom-in', 'zoom-out', 'off', 'signal', 'cog', 'trash', 'list', 'list-alt',
+ 'indent-left', 'indent-right', 'text-width', 'text-height', 'align-left', 'align-center',
+ 'align-right', 'align-justify', 'font', 'bold', 'italic', 'text-color', 'list',
+ 'list-alt', 'ok', 'remove', 'ok-circle', 'remove-circle', 'question-sign',
+ 'info-sign', 'screenshot', 'remove-circle', 'ok-circle', 'ban-circle', 'arrow-left',
+ 'arrow-right', 'arrow-up', 'arrow-down', 'share', 'share-alt', 'resize-full',
+ 'resize-small', 'exclamation-sign', 'warning-sign', 'plane', 'calendar', 'random',
+ 'comment', 'magnet', 'chevron-up', 'chevron-down', 'retweet', 'shopping-cart',
+ 'folder-close', 'folder-open', 'resize-vertical', 'resize-horizontal', 'hdd',
+ 'bullhorn', 'bell', 'certificate', 'thumbs-up', 'thumbs-down', 'hand-right',
+ 'hand-left', 'hand-up', 'hand-down', 'circle-arrow-right', 'circle-arrow-left',
+ 'circle-arrow-up', 'circle-arrow-down', 'globe', 'wrench', 'tasks', 'filter',
+ 'briefcase', 'fullscreen', 'dashboard', 'paperclip', 'heart-empty', 'link',
+ 'phone', 'pushpin', 'usd', 'gbp', 'sort', 'sort-by-alphabet', 'sort-by-alphabet-alt',
+ 'sort-by-order', 'sort-by-order-alt', 'sort-by-attributes', 'sort-by-attributes-alt',
+ 'unchecked', 'expand', 'collapse-down', 'collapse-up', 'log-in', 'flash',
+ 'log-out', 'new-window', 'record', 'save', 'open', 'saved', 'import', 'export',
+ 'send', 'floppy-disk', 'floppy-saved', 'floppy-remove', 'floppy-save', 'floppy-open',
+ 'credit-card', 'transfer', 'cutlery', 'header', 'compressed', 'earphone', 'phone-alt',
+ 'tower', 'stats', 'sd-video', 'hd-video', 'subtitles', 'sound-stereo', 'sound-dolby',
+ 'sound-5-1', 'sound-6-1', 'sound-7-1', 'copyright-mark', 'registration-mark',
+ 'cloud-download', 'cloud-upload', 'tree-conifer', 'tree-deciduous', 'cd',
+ 'save-file', 'open-file', 'level-up', 'copy', 'paste', 'alert', 'equalizer',
+ 'king', 'queen', 'pawn', 'bishop', 'knight', 'baby-formula', 'tent', 'blackboard',
+ 'bed', 'apple', 'erase', 'hourglass', 'lamp', 'duplicate', 'piggy-bank', 'scissors',
+ 'bitcoin', 'btc', 'xbt', 'yen', 'jpy', 'ruble', 'rub', 'scale', 'ice-lolly',
+ 'ice-lolly-tasted', 'education', 'option-horizontal', 'option-vertical', 'menu-hamburger',
+ 'modal-window', 'oil', 'grain', 'sunglasses', 'text-size', 'text-color', 'text-background',
+ 'object-align-top', 'object-align-bottom', 'object-align-horizontal', 'object-align-left',
+ 'object-align-vertical', 'object-align-right', 'triangle-right', 'triangle-left',
+ 'triangle-bottom', 'triangle-top', 'console', 'superscript', 'subscript', 'menu-left',
+ 'menu-right', 'menu-down', 'menu-up'
+ ];
+
+ filteredIcons: string[] = [];
+ currentPage = 1;
+ totalPages = 1;
+ iconsPerPage = 60; // cols * rows
+
+ constructor(private cdr: ChangeDetectorRef) {}
+
+ ngOnInit(): void {
+ this.filteredIcons = [...this.availableIcons];
+ this.updatePagination();
+ }
+
+ ngOnDestroy(): void {
+ // Cleanup if needed
+ }
+
+ togglePicker(): void {
+ if (this.disabled) return;
+ this.isOpen = !this.isOpen;
+ if (this.isOpen) {
+ this.searchTerm = '';
+ this.filterIcons();
+ }
+ }
+
+ closePicker(): void {
+ this.isOpen = false;
+ this.searchTerm = '';
+ this.filterIcons();
+ }
+
+ selectIcon(icon: string): void {
+ this.selectedIcon = icon;
+ this.iconChange.emit(icon);
+ this.iconSelect.emit(icon);
+ this.closePicker();
+ }
+
+ filterIcons(): void {
+ if (!this.searchTerm.trim()) {
+ this.filteredIcons = [...this.availableIcons];
+ } else {
+ const term = this.searchTerm.toLowerCase();
+ this.filteredIcons = this.availableIcons.filter(icon =>
+ icon.toLowerCase().includes(term)
+ );
+ }
+ this.currentPage = 1;
+ this.updatePagination();
+ }
+
+ updatePagination(): void {
+ this.totalPages = Math.ceil(this.filteredIcons.length / this.iconsPerPage);
+ if (this.currentPage > this.totalPages) {
+ this.currentPage = 1;
+ }
+ }
+
+ getCurrentPageIcons(): string[] {
+ const startIndex = (this.currentPage - 1) * this.iconsPerPage;
+ const endIndex = startIndex + this.iconsPerPage;
+ return this.filteredIcons.slice(startIndex, endIndex);
+ }
+
+ goToPage(page: number): void {
+ if (page >= 1 && page <= this.totalPages) {
+ this.currentPage = page;
+ }
+ }
+
+ previousPage(): void {
+ if (this.currentPage > 1) {
+ this.currentPage--;
+ }
+ }
+
+ nextPage(): void {
+ if (this.currentPage < this.totalPages) {
+ this.currentPage++;
+ }
+ }
+
+ onDocumentClick(event: Event): void {
+ const target = event.target as HTMLElement;
+ if (!target.closest('.icon-picker-container')) {
+ this.closePicker();
+ }
+ }
+
+ getIconClass(icon: string): string {
+ return `glyphicon glyphicon-${icon}`;
+ }
+
+ getDisplayText(): string {
+ return this.selectedIcon || this.placeholder;
+ }
+
+ getFormattedLabel(template: string, ...args: any[]): string {
+ return template.replace(/\{(\d+)\}/g, (match, index) => {
+ return args[parseInt(index)] || match;
+ });
+ }
+
+ // Expose Math to template
+ Math = Math;
+}
diff --git a/app/components/ngComponents/customElements/line-pattern-picker/km-line-pattern-picker.component.css b/app/components/ngComponents/customElements/line-pattern-picker/km-line-pattern-picker.component.css
new file mode 100644
index 000000000..8e7677126
--- /dev/null
+++ b/app/components/ngComponents/customElements/line-pattern-picker/km-line-pattern-picker.component.css
@@ -0,0 +1,229 @@
+.km-line-pattern-picker {
+ position: relative;
+}
+
+.form-label {
+ display: block;
+ margin-bottom: 5px;
+ font-weight: bold;
+}
+
+.dropdown {
+ position: relative;
+ display: inline-block;
+}
+
+.km-pattern-button {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 12px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ background-color: #5bc0de;
+ color: white;
+ cursor: pointer;
+ transition: background-color 0.2s;
+ min-height: 38px;
+}
+
+.km-pattern-button:hover:not(:disabled) {
+ background-color: #46b8da;
+}
+
+.km-pattern-button:disabled {
+ background-color: #6c757d;
+ cursor: not-allowed;
+ opacity: 0.6;
+}
+
+.pattern-display {
+ display: flex;
+ align-items: center;
+ flex: 1;
+ min-height: 20px;
+}
+
+.selected-pattern {
+ display: flex;
+ align-items: center;
+ width: 100%;
+}
+
+.pattern-svg {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+}
+
+.pattern-svg svg {
+ max-width: 100%;
+ max-height: 20px;
+ height: auto;
+}
+
+.placeholder-text {
+ color: rgba(255, 255, 255, 0.8);
+ font-style: italic;
+ font-size: 14px;
+}
+
+.caret {
+ display: inline-block;
+ width: 0;
+ height: 0;
+ margin-left: 8px;
+ vertical-align: middle;
+ border-top: 4px solid;
+ border-right: 4px solid transparent;
+ border-left: 4px solid transparent;
+}
+
+.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 1000;
+ display: block;
+ float: left;
+ min-width: 200px;
+ padding: 5px 0;
+ margin: 2px 0 0;
+ list-style: none;
+ font-size: 14px;
+ text-align: left;
+ background-color: #fff;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+ background-clip: padding-box;
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.dropdown-menu-center {
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+.dropdown-menu li {
+ list-style: none;
+}
+
+.dropdown-item {
+ display: block;
+ width: 100%;
+ padding: 8px 16px;
+ clear: both;
+ font-weight: normal;
+ line-height: 1.42857143;
+ color: #333;
+ white-space: nowrap;
+ cursor: pointer;
+ border: none;
+ background: none;
+ text-align: left;
+}
+
+.dropdown-item:hover:not(.disabled) {
+ background-color: #f5f5f5;
+}
+
+.dropdown-item.selected {
+ background-color: #337ab7;
+ color: white;
+}
+
+.dropdown-item.disabled {
+ color: #777;
+ cursor: not-allowed;
+}
+
+.pattern-option .dropdown-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 12px 16px;
+}
+
+.option-label {
+ margin-bottom: 8px;
+ font-weight: 500;
+ text-align: center;
+}
+
+.option-svg {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ min-height: 20px;
+}
+
+.option-svg svg {
+ max-width: 100%;
+ max-height: 20px;
+ height: auto;
+}
+
+.clear-option {
+ border-bottom: 1px solid #eee;
+ margin-bottom: 5px;
+}
+
+.clear-option .dropdown-item {
+ text-align: center;
+ font-style: italic;
+ color: #d9534f;
+}
+
+.clear-option .dropdown-item:hover {
+ background-color: #f2dede;
+ color: #a94442;
+}
+
+.clear-text {
+ padding: 4px 0;
+}
+
+.no-options .dropdown-item {
+ text-align: center;
+ font-style: italic;
+ color: #777;
+}
+
+.no-options-text {
+ padding: 8px 0;
+}
+
+.help-block {
+ margin-top: 5px;
+ margin-bottom: 10px;
+ color: #737373;
+ font-size: 12px;
+}
+
+.with-errors {
+ color: #a94442;
+}
+
+/* Ensure dropdown stays within viewport */
+.dropdown-menu {
+ max-width: 300px;
+ word-wrap: break-word;
+}
+
+/* Mobile responsiveness */
+@media (max-width: 768px) {
+ .km-pattern-button {
+ width: 100% !important;
+ min-width: 200px;
+ }
+
+ .dropdown-menu {
+ width: 100%;
+ left: 0 !important;
+ transform: none !important;
+ }
+}
diff --git a/app/components/ngComponents/customElements/line-pattern-picker/km-line-pattern-picker.component.html b/app/components/ngComponents/customElements/line-pattern-picker/km-line-pattern-picker.component.html
new file mode 100644
index 000000000..6d3f95870
--- /dev/null
+++ b/app/components/ngComponents/customElements/line-pattern-picker/km-line-pattern-picker.component.html
@@ -0,0 +1,54 @@
+
+
{{ label }}
+
+
+
+
+
+
+
+ {{ placeholder }}
+
+
+
+
+
+
+
+
+
+
diff --git a/app/components/ngComponents/customElements/line-pattern-picker/km-line-pattern-picker.component.ts b/app/components/ngComponents/customElements/line-pattern-picker/km-line-pattern-picker.component.ts
new file mode 100644
index 000000000..4a3ae8474
--- /dev/null
+++ b/app/components/ngComponents/customElements/line-pattern-picker/km-line-pattern-picker.component.ts
@@ -0,0 +1,148 @@
+import { CommonModule } from '@angular/common';
+import { Component, ElementRef, EventEmitter, HostListener, Input, OnInit, Output, ViewChild } from '@angular/core';
+import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
+
+export interface LinePatternOption {
+ label: string;
+ dashArrayValue: string;
+ svgString: string;
+}
+
+@Component({
+ selector: 'km-line-pattern-picker',
+ standalone: true,
+ imports: [CommonModule],
+ templateUrl: './km-line-pattern-picker.component.html',
+ styleUrls: ['./km-line-pattern-picker.component.css']
+})
+export class KmLinePatternPickerComponent implements OnInit {
+ @Input() selectedPattern: LinePatternOption | null = null;
+ @Input() options: LinePatternOption[] = [];
+ @Input() label: string = 'Linienmuster wählen';
+ @Input() placeholder: string = 'Linienmuster wählen';
+ @Input() disabled: boolean = false;
+ @Input() closeOnOutsideClick: boolean = true;
+ @Input() width: string = '200px';
+
+ @Output() patternChange = new EventEmitter();
+ @Output() selectionChange = new EventEmitter();
+
+ @ViewChild('dropdownContainer', { static: true }) dropdownContainer!: ElementRef;
+
+ isOpen: boolean = false;
+ private svgSanitizeCache: Map = new Map();
+
+ constructor(private sanitizer: DomSanitizer) {}
+
+ ngOnInit(): void {
+ // If no pattern is selected and options are available, optionally select the first one
+ if (!this.selectedPattern && this.options.length > 0) {
+ // Don't auto-select - let the parent component decide
+ }
+ }
+
+ 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();
+ }
+
+ selectPattern(pattern: LinePatternOption, event?: Event): void {
+ if (event) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ if (this.disabled) {
+ return;
+ }
+
+ this.selectedPattern = pattern;
+ this.patternChange.emit(pattern);
+ this.selectionChange.emit(pattern);
+ this.close();
+ }
+
+ clearSelection(event?: Event): void {
+ if (event) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ if (this.disabled) {
+ return;
+ }
+
+ this.selectedPattern = null;
+ this.patternChange.emit(null);
+ this.selectionChange.emit(null);
+ this.close();
+ }
+
+ 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;
+ }
+
+ trackByOption(index: number, item: LinePatternOption): string {
+ return item?.dashArrayValue ?? index.toString();
+ }
+
+ @HostListener('document:click', ['$event'])
+ onDocumentClick(event: MouseEvent): void {
+ if (!this.isOpen || !this.closeOnOutsideClick) {
+ return;
+ }
+
+ const targetNode = event.target as Node | null;
+ const hostEl = this.dropdownContainer?.nativeElement;
+ if (!hostEl || !targetNode) {
+ this.isOpen = false;
+ return;
+ }
+
+ if (!hostEl.contains(targetNode)) {
+ this.isOpen = false;
+ }
+ }
+
+ get buttonWidth(): string {
+ return this.width;
+ }
+
+ get hasSelection(): boolean {
+ return !!this.selectedPattern;
+ }
+
+ get displayText(): string {
+ return this.selectedPattern?.label || this.placeholder;
+ }
+}
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-batch-update-helper.service.ts b/app/services/adminGeoresourceUnit/kommonitor-batch-update-helper.service.ts
new file mode 100644
index 000000000..e1eee8679
--- /dev/null
+++ b/app/services/adminGeoresourceUnit/kommonitor-batch-update-helper.service.ts
@@ -0,0 +1,549 @@
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { KommonitorGeoresourceDataExchangeService } from './kommonitor-data-exchange.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class KommonitorBatchUpdateHelperService {
+
+ constructor(
+ private http: HttpClient,
+ private kommonitorDataExchangeService: KommonitorGeoresourceDataExchangeService
+ ) {}
+
+ // Date picker options
+ datePickerOptions = {
+ format: 'yyyy-mm-dd',
+ autoclose: true,
+ todayHighlight: true,
+ clearBtn: true
+ };
+
+ /**
+ * Add new row to batch list
+ */
+ addNewRowToBatchList(resourceType: string, batchList: any[]): void {
+ const newRow = {
+ isSelected: true,
+ name: null,
+ mappingTableName: '',
+ mappingObj: {
+ converter: null,
+ dataSource: null
+ },
+ selectedConverter: null,
+ selectedDatasourceType: null
+ };
+
+ batchList.push(newRow);
+ }
+
+ /**
+ * Delete selected rows from batch list
+ */
+ deleteSelectedRowsFromBatchList(batchList: any[], allRowsSelected: boolean): void {
+ if (allRowsSelected) {
+ batchList.length = 0;
+ } else {
+ for (let i = batchList.length - 1; i >= 0; i--) {
+ if (batchList[i].isSelected) {
+ batchList.splice(i, 1);
+ }
+ }
+ }
+ }
+
+ /**
+ * Handle select all rows change
+ */
+ onChangeSelectAllRows(allRowsSelected: boolean, batchList: any[]): void {
+ batchList.forEach(row => {
+ row.isSelected = allRowsSelected;
+ });
+ }
+
+ /**
+ * Initialize georesource datepicker fields
+ */
+ initializeGeoresourceDatepickerFields(batchList: any[]): void {
+ setTimeout(() => {
+ batchList.forEach((row, index) => {
+ // Initialize start date picker
+ const startDatePicker = document.getElementById(`georesourceRow${index}StartDatePicker`);
+ if (startDatePicker && (window as any).$) {
+ (window as any).$(`#georesourceRow${index}StartDatePicker`).datepicker(this.datePickerOptions);
+ }
+
+ // Initialize end date picker
+ const endDatePicker = document.getElementById(`georesourceRow${index}EndDatePicker`);
+ if (endDatePicker && (window as any).$) {
+ (window as any).$(`#georesourceRow${index}EndDatePicker`).datepicker(this.datePickerOptions);
+ }
+ });
+ }, 100);
+ }
+
+ /**
+ * Resize name column dropdowns
+ */
+ resizeNameColumnDropdowns(georesource: any): void {
+ // Implementation for resizing dropdowns
+ setTimeout(() => {
+ const dropdowns = document.querySelectorAll('.georesource-name-dropdown');
+ dropdowns.forEach((dropdown: any) => {
+ if (dropdown.style) {
+ dropdown.style.width = 'auto';
+ dropdown.style.minWidth = '200px';
+ }
+ });
+ }, 100);
+ }
+
+ /**
+ * Handle mapping table selection
+ */
+ onMappingTableSelected(resourceType: string, event: any, index: number, file: File, batchList: any[]): void {
+ try {
+ const fileContent = event.target.result;
+ const mappingTable = this.parseMappingTable(fileContent, file.name);
+
+ if (mappingTable) {
+ batchList[index].mappingTableName = file.name;
+ batchList[index].mappingObj = mappingTable;
+
+ // Set selected converter and datasource type
+ if (mappingTable.converter) {
+ batchList[index].selectedConverter = this.getConverterObjectByName(mappingTable.converter.name);
+ }
+ if (mappingTable.dataSource) {
+ batchList[index].selectedDatasourceType = this.getDatasourceTypeObjectByType(mappingTable.dataSource.type);
+ }
+ }
+ } catch (error) {
+ console.error('Error processing mapping table:', error);
+ }
+ }
+
+ /**
+ * Handle data source file selection
+ */
+ onDataSourceFileSelected(file: File, index: number, batchList: any[]): void {
+ try {
+ const reader = new FileReader();
+ reader.onload = (event: any) => {
+ const fileContent = event.target.result;
+
+ // Update the mapping object with file information
+ if (!batchList[index].mappingObj) {
+ batchList[index].mappingObj = {};
+ }
+
+ batchList[index].mappingObj.dataSource = {
+ type: 'file',
+ file: file,
+ content: fileContent
+ };
+ };
+ reader.readAsText(file);
+ } catch (error) {
+ console.error('Error processing data source file:', error);
+ }
+ }
+
+ /**
+ * Parse batch list from file
+ */
+ parseBatchListFromFile(resourceType: string, file: File, batchList: any[]): void {
+ const reader = new FileReader();
+ reader.onload = (event: any) => {
+ try {
+ const fileContent = event.target.result as string;
+ const fileExtension = this.getFileExtension(file.name);
+
+ let parsedData: any[] = [];
+
+ if (fileExtension === 'json') {
+ parsedData = JSON.parse(fileContent);
+ } else if (fileExtension === 'csv') {
+ parsedData = this.parseCsvFile(fileContent);
+ }
+
+ // Broadcast the parsed data
+ // This would typically go through a broadcast service
+ console.log('Batch list parsed:', parsedData);
+
+ } catch (error) {
+ console.error('Error parsing batch list file:', error);
+ }
+ };
+ reader.readAsText(file);
+ }
+
+ /**
+ * Save column default value
+ */
+ onClickSaveColDefaultValue(
+ resourceType: string,
+ selectedColumn: string,
+ newValue: any,
+ allRowsChb: boolean,
+ batchList: any[]
+ ): void {
+ if (allRowsChb) {
+ batchList.forEach(row => {
+ if (row.mappingObj) {
+ row.mappingObj[selectedColumn] = newValue;
+ }
+ });
+ } else {
+ // Apply to selected rows only
+ batchList.forEach(row => {
+ if (row.isSelected && row.mappingObj) {
+ row.mappingObj[selectedColumn] = newValue;
+ }
+ });
+ }
+ }
+
+ /**
+ * Save batch list to file
+ */
+ saveBatchListToFile(resourceType: string, batchList: any[], keepMissingValues: boolean, includeMetadata: boolean = true): void {
+ try {
+ const exportData = batchList.map(row => {
+ const exportRow: any = {
+ isSelected: row.isSelected,
+ name: row.name?.georesourceId || row.name,
+ mappingTableName: row.mappingTableName,
+ mappingObj: { ...row.mappingObj }
+ };
+
+ if (includeMetadata && row.name) {
+ exportRow.metadata = {
+ datasetName: row.name.datasetName,
+ description: row.name.metadata?.description,
+ datasource: row.name.metadata?.datasource,
+ contact: row.name.metadata?.contact
+ };
+ }
+
+ return exportRow;
+ });
+
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `${resourceType}_batch_list_${new Date().toISOString().split('T')[0]}.json`;
+ a.click();
+ window.URL.revokeObjectURL(url);
+
+ } catch (error) {
+ console.error('Error saving batch list to file:', error);
+ }
+ }
+
+ /**
+ * Save mapping object to file
+ */
+ saveMappingObjectToFile(resourceType: string, event: any, batchList: any[]): void {
+ try {
+ const selectedRows = batchList.filter(row => row.isSelected);
+
+ if (selectedRows.length === 0) {
+ console.warn('No rows selected for export');
+ return;
+ }
+
+ const exportData = selectedRows.map(row => ({
+ georesourceId: row.name?.georesourceId || row.name,
+ mappingTableName: row.mappingTableName,
+ mappingObj: row.mappingObj
+ }));
+
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `${resourceType}_mapping_objects_${new Date().toISOString().split('T')[0]}.json`;
+ a.click();
+ window.URL.revokeObjectURL(url);
+
+ } catch (error) {
+ console.error('Error saving mapping objects to file:', error);
+ }
+ }
+
+ /**
+ * Execute batch update
+ */
+ batchUpdate(resourceType: string, batchList: any[]): void {
+ try {
+ const selectedRows = batchList.filter(row => row.isSelected);
+
+ if (selectedRows.length === 0) {
+ console.warn('No rows selected for batch update');
+ return;
+ }
+
+ // Validate all selected rows
+ const validationResults = selectedRows.map(row => this.validateBatchUpdateRow(row));
+ const invalidRows = validationResults.filter(result => !result.isValid);
+
+ if (invalidRows.length > 0) {
+ console.error('Validation failed for some rows:', invalidRows);
+ return;
+ }
+
+ // Execute batch update
+ console.log(`Executing batch update for ${selectedRows.length} ${resourceType}s`);
+
+ // This would typically make HTTP calls to update the backend
+ // For now, just log the operation
+ selectedRows.forEach((row, index) => {
+ console.log(`Updating ${resourceType} ${index + 1}:`, row);
+ });
+
+ // Broadcast completion
+ // this.broadcastService.broadcast('batchUpdateCompleted', { resourceType, count: selectedRows.length });
+
+ } catch (error) {
+ console.error('Error executing batch update:', error);
+ }
+ }
+
+ /**
+ * Check if name and files are chosen in each row
+ */
+ checkIfNameAndFilesChosenInEachRow(resourceType: string, batchList: any[]): boolean {
+ const selectedRows = batchList.filter(row => row.isSelected);
+
+ if (selectedRows.length === 0) {
+ return false;
+ }
+
+ return selectedRows.every(row => {
+ return row.name &&
+ row.mappingTableName &&
+ row.mappingObj &&
+ row.mappingObj.converter &&
+ row.mappingObj.dataSource;
+ });
+ }
+
+ /**
+ * Reset batch update form
+ */
+ resetBatchUpdateForm(resourceType: string, batchList: any[]): void {
+ batchList.length = 0;
+ this.addNewRowToBatchList(resourceType, batchList);
+ }
+
+ /**
+ * Refresh name column after updates
+ */
+ refreshNameColumn(resourceType: string, batchList: any[]): void {
+ batchList.forEach(row => {
+ if (row.name && typeof row.name === 'string') {
+ // If name is just an ID, try to get the full object
+ const georesourceObj = this.kommonitorDataExchangeService.getGeoresourceMetadataById(row.name);
+ if (georesourceObj) {
+ row.name = georesourceObj;
+ }
+ }
+ });
+ }
+
+ /**
+ * Check columns to show for selected converter
+ */
+ checkColumnsToShow_selectedConverter(batchList: any[]): string[] {
+ const selectedRows = batchList.filter(row => row.isSelected);
+ const converters = selectedRows.map(row => row.selectedConverter).filter(Boolean);
+
+ if (converters.length === 0) return [];
+
+ // Return common columns from all selected converters
+ const allColumns = new Set();
+ converters.forEach(converter => {
+ if (converter.columns) {
+ converter.columns.forEach((col: string) => allColumns.add(col));
+ }
+ });
+
+ return Array.from(allColumns);
+ }
+
+ /**
+ * Check if selected datasource type is file
+ */
+ checkIfSelectedDatasourceTypeIsFile(batchList: any[]): boolean {
+ const selectedRows = batchList.filter(row => row.isSelected);
+ return selectedRows.some(row => row.selectedDatasourceType?.type === 'file');
+ }
+
+ /**
+ * Check if selected datasource type is HTTP
+ */
+ checkIfSelectedDatasourceTypeIsHttp(batchList: any[]): boolean {
+ const selectedRows = batchList.filter(row => row.isSelected);
+ return selectedRows.some(row => row.selectedDatasourceType?.type === 'http');
+ }
+
+ /**
+ * Check if selected datasource type is inline
+ */
+ checkIfSelectedDatasourceTypeIsInline(batchList: any[]): boolean {
+ const selectedRows = batchList.filter(row => row.isSelected);
+ return selectedRows.some(row => row.selectedDatasourceType?.type === 'inline');
+ }
+
+ /**
+ * Get converter object by name
+ */
+ getConverterObjectByName(name: string): any {
+ // This would typically come from a service or configuration
+ const converters = [
+ { name: 'csv-converter', columns: ['id', 'name', 'description'] },
+ { name: 'json-converter', columns: ['id', 'name', 'description', 'metadata'] },
+ { name: 'xml-converter', columns: ['id', 'name', 'description', 'attributes'] }
+ ];
+
+ return converters.find(converter => converter.name === name) || null;
+ }
+
+ /**
+ * Get datasource type object by type
+ */
+ getDatasourceTypeObjectByType(type: string): any {
+ // This would typically come from a service or configuration
+ const datasourceTypes = [
+ { type: 'file', name: 'File Upload', description: 'Upload a file from your computer' },
+ { type: 'http', name: 'HTTP URL', description: 'Download from a web URL' },
+ { type: 'inline', name: 'Inline Data', description: 'Paste data directly' }
+ ];
+
+ return datasourceTypes.find(dsType => dsType.type === type) || null;
+ }
+
+ /**
+ * Convert converter parameters array to properties
+ */
+ converterParametersArrayToProperties(converterParams: any[]): any {
+ if (!Array.isArray(converterParams)) return converterParams;
+
+ const properties: any = {};
+ converterParams.forEach(param => {
+ if (param.name && param.value !== undefined) {
+ properties[param.name] = param.value;
+ }
+ });
+
+ return properties;
+ }
+
+ /**
+ * Convert datasource parameters array to properties
+ */
+ dataSourceParametersArrayToProperty(dataSourceParams: any[]): any {
+ if (!Array.isArray(dataSourceParams)) return dataSourceParams;
+
+ const properties: any = {};
+ dataSourceParams.forEach(param => {
+ if (param.name && param.value !== undefined) {
+ properties[param.name] = param.value;
+ }
+ });
+
+ return properties;
+ }
+
+ /**
+ * Parse mapping table from file content
+ */
+ private parseMappingTable(fileContent: string, fileName: string): any {
+ try {
+ const fileExtension = this.getFileExtension(fileName);
+
+ if (fileExtension === 'json') {
+ return JSON.parse(fileContent);
+ } else if (fileExtension === 'csv') {
+ return this.parseCsvFile(fileContent);
+ } else {
+ console.warn('Unsupported file format:', fileExtension);
+ return null;
+ }
+ } catch (error) {
+ console.error('Error parsing mapping table:', error);
+ return null;
+ }
+ }
+
+ /**
+ * Parse CSV file content
+ */
+ private parseCsvFile(fileContent: string): any[] {
+ try {
+ const lines = fileContent.split('\n');
+ const headers = lines[0].split(',').map((header: string) => header.trim());
+ const data: any[] = [];
+
+ for (let i = 1; i < lines.length; i++) {
+ if (lines[i].trim()) {
+ const values = lines[i].split(',').map((value: string) => value.trim());
+ const row: any = {};
+
+ headers.forEach((header: string, index: number) => {
+ row[header] = values[index] || '';
+ });
+
+ data.push(row);
+ }
+ }
+
+ return data;
+ } catch (error) {
+ console.error('Error parsing CSV file:', error);
+ return [];
+ }
+ }
+
+ /**
+ * Get file extension
+ */
+ private getFileExtension(fileName: string): string {
+ return fileName.split('.').pop()?.toLowerCase() || '';
+ }
+
+ /**
+ * Validate batch update row
+ */
+ private validateBatchUpdateRow(row: any): { isValid: boolean; errors: string[] } {
+ const errors: string[] = [];
+
+ if (!row.name) {
+ errors.push('Missing georesource name');
+ }
+
+ if (!row.mappingTableName) {
+ errors.push('Missing mapping table name');
+ }
+
+ if (!row.mappingObj) {
+ errors.push('Missing mapping object');
+ } else {
+ if (!row.mappingObj.converter) {
+ errors.push('Missing converter configuration');
+ }
+ if (!row.mappingObj.dataSource) {
+ errors.push('Missing data source configuration');
+ }
+ }
+
+ return {
+ isValid: errors.length === 0,
+ errors
+ };
+ }
+}
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..162da04e4
--- /dev/null
+++ b/app/services/adminGeoresourceUnit/kommonitor-cache-helper.service.ts
@@ -0,0 +1,650 @@
+import { Injectable, OnDestroy } from '@angular/core';
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { Observable, BehaviorSubject, Subject } from 'rxjs';
+import { map, catchError, tap } from 'rxjs/operators';
+import { AuthService } from '../auth-service/auth.service';
+
+// Define interfaces locally to avoid circular dependencies
+export interface GeoresourceMetadata {
+ georesourceId: string;
+ datasetName: string;
+ isPOI?: boolean;
+ isLOI?: boolean;
+ isAOI?: boolean;
+ poiSymbolColor?: string;
+ poiSymbolBootstrap3Name?: string;
+ poiMarkerColor?: string;
+ loiColor?: string;
+ loiWidth?: number;
+ loiDashArrayString?: string;
+ aoiColor?: string;
+ metadata?: {
+ description?: string;
+ datasource?: string;
+ contact?: string;
+ };
+ availablePeriodsOfValidity?: Array<{
+ startDate: string;
+ endDate?: string;
+ }>;
+ topicReference?: any;
+ permissions?: any;
+ isPublic?: boolean;
+ ownerId?: string;
+ userPermissions?: string[];
+}
+
+export interface DatabaseModificationInfo {
+ georesources: string;
+ spatialUnits: string;
+ indicators: string;
+ topics: string;
+ processScripts: string;
+ accessControl: string;
+}
+
+export interface CacheEntry {
+ data: T;
+ timestamp: number;
+ expiresAt: number;
+ lastModification: string;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class KommonitorGeoresourceCacheHelperService implements OnDestroy {
+ // Private subjects for reactive updates
+ private lastModificationSubject = new BehaviorSubject(null);
+ private loadingSubject = new BehaviorSubject(false);
+ private errorSubject = new BehaviorSubject(null);
+
+ // Destroy subject for cleanup
+ private destroy$ = new Subject();
+
+ // Public observables
+ public lastModification$ = this.lastModificationSubject.asObservable();
+ public loading$ = this.loadingSubject.asObservable();
+ public error$ = this.errorSubject.asObservable();
+
+ // Environment configuration
+ private readonly env: any;
+ private readonly baseUrl: string;
+ private readonly localStoragePrefix: string;
+
+ // Database modification info (like original AngularJS service)
+ private lastDatabaseModificationInfo: DatabaseModificationInfo | null = null;
+
+ // Endpoints (like original AngularJS service)
+ private readonly georesourcesPublicEndpoint = "/public/georesources";
+ private readonly georesourcesProtectedEndpoint = "/georesources";
+ private readonly spatialUnitsPublicEndpoint = "/public/spatial-units";
+ private readonly spatialUnitsProtectedEndpoint = "/spatial-units";
+ private readonly indicatorsPublicEndpoint = "/public/indicators";
+ private readonly indicatorsProtectedEndpoint = "/indicators";
+ private readonly scriptsPublicEndpoint = "/public/process-scripts";
+ private readonly scriptsProtectedEndpoint = "/process-scripts";
+ private readonly topicsPublicEndpoint = "/public/topics";
+ private readonly accessControlEndpoint = "/organizationalUnits";
+
+ // Current endpoints based on authentication
+ private georesourcesEndpoint = this.georesourcesProtectedEndpoint;
+ private spatialUnitsEndpoint = this.spatialUnitsProtectedEndpoint;
+ private indicatorsEndpoint = this.indicatorsProtectedEndpoint;
+ private scriptsEndpoint = this.scriptsProtectedEndpoint;
+ public spatialResourceGETUrlPath_forAuthentication = "";
+
+ // Local storage keys (like original AngularJS service)
+ private readonly localStorageKey_georesources: string;
+ private readonly localStorageKey_spatialUnits: string;
+ private readonly localStorageKey_indicators: string;
+ private readonly localStorageKey_topics: string;
+ private readonly localStorageKey_processScripts: string;
+ private readonly localStorageKey_accessControl: string;
+
+ // Cache duration in milliseconds (5 minutes)
+ private readonly CACHE_DURATION = 5 * 60 * 1000;
+
+ constructor(
+ private http: HttpClient,
+ private authService: AuthService
+ ) {
+ // Get environment configuration
+ this.env = (window as any).__env;
+ this.baseUrl = this.getBaseApiUrl();
+ this.localStoragePrefix = this.env?.localStoragePrefix || 'kommonitor';
+
+ // Initialize local storage keys
+ this.localStorageKey_georesources = this.localStoragePrefix + "_lastModification_georesources";
+ this.localStorageKey_spatialUnits = this.localStoragePrefix + "_lastModification_spatialUnits";
+ this.localStorageKey_indicators = this.localStoragePrefix + "_lastModification_indicators";
+ this.localStorageKey_topics = this.localStoragePrefix + "_lastModification_topics";
+ this.localStorageKey_processScripts = this.localStoragePrefix + "_lastModification_processScripts";
+ this.localStorageKey_accessControl = this.localStoragePrefix + "_lastModification_accessControl";
+
+ // Initialize the service
+ this.initializeService();
+ }
+
+ /**
+ * Initialize the service (like original AngularJS service)
+ */
+ private async initializeService(): Promise {
+ try {
+ this.checkAuthentication();
+ await this.fetchLastDatabaseModificationObject();
+ } catch (error) {
+ console.error('Error initializing cache helper service:', error);
+ this.handleError(error);
+ }
+ }
+
+ /**
+ * Check authentication and set endpoints accordingly (like original AngularJS service)
+ */
+ private checkAuthentication(): void {
+ try {
+ const keycloak = this.authService.Auth?.keycloak;
+ if (keycloak?.authenticated) {
+ this.georesourcesEndpoint = this.georesourcesProtectedEndpoint;
+ this.spatialUnitsEndpoint = this.spatialUnitsProtectedEndpoint;
+ this.indicatorsEndpoint = this.indicatorsProtectedEndpoint;
+ this.scriptsEndpoint = this.scriptsProtectedEndpoint;
+ this.spatialResourceGETUrlPath_forAuthentication = "";
+ } else {
+ this.georesourcesEndpoint = this.georesourcesPublicEndpoint;
+ this.spatialUnitsEndpoint = this.spatialUnitsPublicEndpoint;
+ this.indicatorsEndpoint = this.indicatorsPublicEndpoint;
+ this.scriptsEndpoint = this.scriptsPublicEndpoint;
+ this.spatialResourceGETUrlPath_forAuthentication = "/public";
+ }
+ } catch (error) {
+ console.error('Error checking authentication:', error);
+ // Default to public endpoints
+ this.georesourcesEndpoint = this.georesourcesPublicEndpoint;
+ this.spatialUnitsEndpoint = this.spatialUnitsPublicEndpoint;
+ this.indicatorsEndpoint = this.indicatorsPublicEndpoint;
+ this.scriptsEndpoint = this.scriptsPublicEndpoint;
+ this.spatialResourceGETUrlPath_forAuthentication = "/public";
+ }
+ }
+
+ /**
+ * Fetch last database modification object (like original AngularJS service)
+ */
+ private async fetchLastDatabaseModificationObject(): Promise {
+ try {
+ const url = `${this.baseUrl}/public/database/last-modification`;
+ const response = await this.http.get(url).toPromise();
+ console.log("fetchLastDatabaseModificationObject", response);
+
+ if (response) {
+ this.lastDatabaseModificationInfo = response;
+ this.lastModificationSubject.next(response);
+ }
+ } catch (error) {
+ // Error fetching last modification info
+ console.warn('Could not fetch last database modification info:', error);
+ }
+ }
+
+ /**
+ * Fetches topics metadata with caching (like original AngularJS service)
+ */
+ async fetchTopicsMetadata(keycloakRolesArray: string[]): Promise {
+ this.loadingSubject.next(true);
+ this.errorSubject.next(null);
+
+ console.log("Georesource Cache Helper - fetchTopicsMetadata called with roles:", keycloakRolesArray);
+
+ try {
+ // Check authentication
+ this.checkAuthentication();
+ console.log("Georesource Cache Helper - topics endpoint:", this.topicsPublicEndpoint);
+
+ // For now, use the public topics endpoint
+ // In the future, this could be enhanced to use protected endpoint when authenticated
+ const url = `${this.baseUrl}${this.topicsPublicEndpoint}`;
+ console.log("Georesource Cache Helper - fetching from URL:", url);
+
+ const response = await this.http.get(url).toPromise();
+ console.log("Georesource Cache Helper - topics response:", response);
+
+ if (!response || !Array.isArray(response)) {
+ console.warn("Georesource Cache Helper - No topics data received");
+ this.loadingSubject.next(false);
+ return [];
+ }
+
+ this.loadingSubject.next(false);
+ return response;
+ } catch (error) {
+ console.error("Georesource Cache Helper - Error fetching topics metadata:", error);
+ this.errorSubject.next('Error fetching topics metadata');
+ this.loadingSubject.next(false);
+ throw error;
+ }
+ }
+
+ /**
+ * Fetch single georesource metadata (like original AngularJS service)
+ */
+ async fetchSingleGeoresourceMetadata(georesourceId: string, keycloakRolesArray: string[]): Promise {
+ try {
+ this.loadingSubject.next(true);
+ this.errorSubject.next(null);
+
+ // Fetch from server
+ const url = `${this.baseUrl}${this.georesourcesEndpoint}/${georesourceId}`;
+ const headers = this.getAuthHeaders();
+
+ const response = await this.http.get(url, { headers }).toPromise();
+
+ if (!response) {
+ throw new Error('No response from georesource API');
+ }
+
+ // Cache the result
+ this.cacheGeoresource(georesourceId, response, keycloakRolesArray);
+
+ // Optionally refresh the full list in the background to keep list fresh
+ this.fetchGeoresourceMetadata(keycloakRolesArray).subscribe({
+ error: () => { /* ignore background errors */ }
+ });
+
+ return response;
+
+ } catch (error) {
+ console.error('Error fetching single georesource metadata:', error);
+ this.handleError(error);
+ throw error;
+ } finally {
+ this.loadingSubject.next(false);
+ }
+ }
+
+ /**
+ * Fetch georesource metadata (like original AngularJS service)
+ */
+ fetchGeoresourceMetadata(keycloakRolesArray: string[], filter?: any): Observable {
+ return this.fetchResource_fromCacheOrServer(
+ this.localStorageKey_georesources,
+ this.georesourcesEndpoint,
+ 'georesources',
+ keycloakRolesArray,
+ filter
+ );
+ }
+
+ /**
+ * Fetch spatial units metadata (like original AngularJS service)
+ */
+ fetchSpatialUnitsMetadata(keycloakRolesArray: string[]): Observable {
+ return this.fetchResource_fromCacheOrServer(
+ this.localStorageKey_spatialUnits,
+ this.spatialUnitsEndpoint,
+ 'spatialUnits',
+ keycloakRolesArray
+ );
+ }
+
+ /**
+ * Fetch indicators metadata (like original AngularJS service)
+ */
+ fetchIndicatorsMetadata(keycloakRolesArray: string[], filter?: any): Observable {
+ return this.fetchResource_fromCacheOrServer(
+ this.localStorageKey_indicators,
+ this.indicatorsEndpoint,
+ 'indicators',
+ keycloakRolesArray,
+ filter
+ );
+ }
+
+ /**
+ * Fetch single georesource schema (like original AngularJS service)
+ */
+ async fetchSingleGeoresourceSchema(targetGeoresourceId: string): Promise {
+ try {
+ const url = `${this.baseUrl}${this.georesourcesEndpoint}/${targetGeoresourceId}/schema`;
+ const headers = this.getAuthHeaders();
+
+ const response = await this.http.get(url, { headers }).toPromise();
+ return response;
+ } catch (error) {
+ console.error('Error fetching georesource schema:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Fetch single georesource without geometry (like original AngularJS service)
+ */
+ async fetchSingleGeoresourceWithoutGeometry(targetGeoresourceId: string): Promise {
+ try {
+ const url = `${this.baseUrl}${this.georesourcesEndpoint}/${targetGeoresourceId}/allFeatures/without-geometry`;
+ const headers = this.getAuthHeaders();
+
+ const response = await this.http.get(url, { headers }).toPromise();
+ return response;
+ } catch (error) {
+ console.error('Error fetching georesource without geometry:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Fetch resource from cache or server (like original AngularJS service)
+ */
+ private fetchResource_fromCacheOrServer(
+ localStorageKey: string,
+ resourceEndpoint: string,
+ lastModificationResourceName: string,
+ keycloakRolesArray: string[],
+ filter?: any
+ ): Observable {
+
+ return new Observable(observer => {
+ this.fetchResourceFromCacheOrServerAsync(
+ localStorageKey,
+ resourceEndpoint,
+ lastModificationResourceName,
+ keycloakRolesArray,
+ filter
+ ).then(data => {
+ observer.next(data as T[]);
+ observer.complete();
+ }).catch(error => {
+ observer.error(error);
+ });
+ });
+ }
+
+ /**
+ * Async implementation of fetchResource_fromCacheOrServer
+ */
+ private async fetchResourceFromCacheOrServerAsync(
+ localStorageKey: string,
+ resourceEndpoint: string,
+ lastModificationResourceName: string,
+ keycloakRolesArray: string[],
+ filter?: any
+ ): Promise {
+
+ // Fetch latest database modification info
+ await this.fetchLastDatabaseModificationObject();
+
+ // Build cache keys like original AngularJS service
+ let timestampKey = localStorageKey + "_timestamp";
+ let metadataKey = localStorageKey + "_metadata";
+
+ // Different cache keys based on roles (like original AngularJS service)
+ if (keycloakRolesArray && keycloakRolesArray.length > 0) {
+ if (keycloakRolesArray.includes(this.env?.keycloakKomMonitorAdminRoleName)) {
+ metadataKey += "_" + this.env?.keycloakKomMonitorAdminRoleName;
+ timestampKey += "_" + this.env?.keycloakKomMonitorAdminRoleName;
+ } else {
+ metadataKey += "_" + JSON.stringify(keycloakRolesArray);
+ timestampKey += "_" + JSON.stringify(keycloakRolesArray);
+ }
+ } else {
+ metadataKey += "_public";
+ timestampKey += "_public";
+ }
+
+ // Check cache timestamp (like original AngularJS service)
+ let lastModTimestamp_fromCache_string = localStorage.getItem(timestampKey);
+
+ if (lastModTimestamp_fromCache_string && !filter) {
+ const lastModTimestamp_fromCache = new Date(lastModTimestamp_fromCache_string);
+ const lastModTimestamp_fromServer = new Date(this.lastDatabaseModificationInfo?.[lastModificationResourceName as keyof DatabaseModificationInfo] || '');
+
+ if (lastModTimestamp_fromCache.getTime() === lastModTimestamp_fromServer.getTime()) {
+ // Cache is valid, return cached data
+ const cachedMetadata = localStorage.getItem(metadataKey);
+ if (cachedMetadata) {
+ try {
+ return JSON.parse(cachedMetadata);
+ } catch (error) {
+ console.error('Error parsing cached metadata:', error);
+ }
+ }
+ }
+ }
+
+ // Cache is invalid or doesn't exist, fetch from server
+ return this.fetchResourceFromServer(
+ localStorageKey,
+ resourceEndpoint,
+ lastModificationResourceName,
+ keycloakRolesArray,
+ filter
+ );
+ }
+
+ /**
+ * Fetch resource from server with optional filtering
+ */
+ private async fetchResourceFromServer(
+ localStorageKey: string,
+ resourceEndpoint: string,
+ lastModificationResourceName: string,
+ keycloakRolesArray: string[],
+ filter?: any
+ ): Promise {
+
+ const url = `${this.baseUrl}${resourceEndpoint}`;
+ const headers = this.getAuthHeaders();
+
+ let response: T[] | undefined;
+ if (filter) {
+ // POST request with filter
+ response = await this.http.post(`${url}/filter`, filter, { headers }).toPromise();
+ } else {
+ // Standard GET request
+ response = await this.http.get(url, { headers }).toPromise();
+ }
+
+ if (!response) {
+ throw new Error(`No response from ${resourceEndpoint} API`);
+ }
+
+ // Update cache
+ this.updateCache(localStorageKey, response, lastModificationResourceName, keycloakRolesArray);
+
+ return response;
+ }
+
+ /**
+ * Get cached georesource
+ */
+ private getCachedGeoresource(georesourceId: string, keycloakRolesArray: string[]): GeoresourceMetadata | null {
+ const cacheKey = this.buildCacheKey(this.localStorageKey_georesources, keycloakRolesArray);
+ const cachedData = localStorage.getItem(cacheKey + "_metadata");
+
+ if (cachedData) {
+ try {
+ const georesources: GeoresourceMetadata[] = JSON.parse(cachedData);
+ return georesources.find(geo => geo.georesourceId === georesourceId) || null;
+ } catch (error) {
+ console.error('Error parsing cached georesource data:', error);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Cache georesource
+ */
+ private cacheGeoresource(georesourceId: string, data: GeoresourceMetadata, keycloakRolesArray: string[]): void {
+ const cacheKey = this.buildCacheKey(this.localStorageKey_georesources, keycloakRolesArray);
+ const timestampKey = cacheKey + "_timestamp";
+ const metadataKey = cacheKey + "_metadata";
+
+ // Get existing cache
+ const existingData = localStorage.getItem(metadataKey);
+ let georesources: GeoresourceMetadata[] = [];
+
+ if (existingData) {
+ try {
+ georesources = JSON.parse(existingData);
+ } catch (error) {
+ console.error('Error parsing existing cache:', error);
+ }
+ }
+
+ // Update or add the georesource
+ const existingIndex = georesources.findIndex(geo => geo.georesourceId === georesourceId);
+ if (existingIndex >= 0) {
+ georesources[existingIndex] = data;
+ } else {
+ georesources.push(data);
+ }
+
+ // Save to cache
+ localStorage.setItem(metadataKey, JSON.stringify(georesources));
+ localStorage.setItem(timestampKey, new Date().toISOString());
+ }
+
+ /**
+ * Update cache
+ */
+ private updateCache(
+ localStorageKey: string,
+ data: T[],
+ lastModificationResourceName: string,
+ keycloakRolesArray: string[]
+ ): void {
+ const cacheKey = this.buildCacheKey(localStorageKey, keycloakRolesArray);
+ const timestampKey = cacheKey + "_timestamp";
+ const metadataKey = cacheKey + "_metadata";
+
+ localStorage.setItem(metadataKey, JSON.stringify(data));
+ localStorage.setItem(timestampKey, new Date().toISOString());
+ }
+
+ /**
+ * Build cache key based on roles
+ */
+ private buildCacheKey(localStorageKey: string, keycloakRolesArray: string[]): string {
+ if (keycloakRolesArray && keycloakRolesArray.length > 0) {
+ if (keycloakRolesArray.includes(this.env?.keycloakKomMonitorAdminRoleName)) {
+ return localStorageKey + "_" + this.env?.keycloakKomMonitorAdminRoleName;
+ } else {
+ return localStorageKey + "_" + JSON.stringify(keycloakRolesArray);
+ }
+ } else {
+ return localStorageKey + "_public";
+ }
+ }
+
+ /**
+ * Get 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 authentication headers
+ */
+ private getAuthHeaders(): HttpHeaders {
+ const headers = new HttpHeaders({
+ 'Content-Type': 'application/json'
+ });
+
+ // Add auth token if available
+ const token = this.getKeycloakToken();
+ if (token) {
+ headers.set('Authorization', `Bearer ${token}`);
+ }
+
+ return headers;
+ }
+
+ /**
+ * Get Keycloak token
+ */
+ private getKeycloakToken(): string | null {
+ try {
+ const keycloak = this.authService.Auth?.keycloak;
+ return keycloak?.token || null;
+ } catch (error) {
+ return null;
+ }
+ }
+
+ /**
+ * Handle errors
+ */
+ private handleError(error: any): void {
+ let errorMessage = 'An error occurred';
+
+ if (error.error && error.error.message) {
+ errorMessage = error.error.message;
+ } else if (error.message) {
+ errorMessage = error.message;
+ } else if (typeof error === 'string') {
+ errorMessage = error;
+ }
+
+ this.errorSubject.next(errorMessage);
+ console.error('Cache helper service error:', error);
+ }
+
+ /**
+ * Clear all caches
+ */
+ clearAllCaches(): void {
+ const keys = [
+ this.localStorageKey_georesources,
+ this.localStorageKey_spatialUnits,
+ this.localStorageKey_indicators,
+ this.localStorageKey_topics,
+ this.localStorageKey_processScripts,
+ this.localStorageKey_accessControl
+ ];
+
+ keys.forEach(key => {
+ this.clearCacheByPattern(key);
+ });
+ }
+
+ /**
+ * Clear cache by pattern
+ */
+ private clearCacheByPattern(pattern: string): void {
+ for (let i = 0; i < localStorage.length; i++) {
+ const key = localStorage.key(i);
+ if (key && key.startsWith(pattern)) {
+ localStorage.removeItem(key);
+ }
+ }
+ }
+
+ /**
+ * Refresh all data
+ */
+ async refreshAllData(keycloakRolesArray: string[]): Promise {
+ this.clearAllCaches();
+ await this.fetchLastDatabaseModificationObject();
+
+ // Refresh all metadata
+ this.fetchGeoresourceMetadata(keycloakRolesArray).subscribe();
+ this.fetchSpatialUnitsMetadata(keycloakRolesArray).subscribe();
+ this.fetchIndicatorsMetadata(keycloakRolesArray).subscribe();
+ }
+
+ /**
+ * Cleanup on destroy
+ */
+ ngOnDestroy(): void {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+}
\ 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..80d4059dd
--- /dev/null
+++ b/app/services/adminGeoresourceUnit/kommonitor-data-exchange.service.ts
@@ -0,0 +1,1075 @@
+import { Injectable, OnDestroy } from '@angular/core';
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { Observable, BehaviorSubject, Subject, timer, filter, takeUntil, map } from 'rxjs';
+import { map as rxMap, catchError, tap } from 'rxjs/operators';
+import { AuthService } from '../auth-service/auth.service';
+import { KommonitorGeoresourceCacheHelperService } from './kommonitor-cache-helper.service';
+
+// Interfaces for better typing
+export interface GeoresourceMetadata {
+ georesourceId: string;
+ datasetName: string;
+ isPOI?: boolean;
+ isLOI?: boolean;
+ isAOI?: boolean;
+ poiSymbolColor?: string;
+ poiSymbolBootstrap3Name?: string;
+ poiMarkerColor?: string;
+ loiColor?: string;
+ loiWidth?: number;
+ loiDashArrayString?: string;
+ aoiColor?: string;
+ metadata?: {
+ description?: string;
+ datasource?: string;
+ contact?: string;
+ };
+ availablePeriodsOfValidity?: Array<{
+ startDate: string;
+ endDate?: string;
+ }>;
+ topicReference?: any;
+ permissions?: any;
+ isPublic?: boolean;
+ ownerId?: string;
+ userPermissions?: string[];
+}
+
+export interface TopicHierarchy {
+ topicId: string;
+ name: string;
+ title?: string;
+ topicType?: string;
+ topicResource?: string;
+ topicName?: string;
+ subTopics?: TopicHierarchy[];
+}
+
+export interface RoleMetadata {
+ organizationalUnitId: string;
+ name: string;
+ title?: 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 KommonitorGeoresourceDataExchangeService implements OnDestroy {
+ // Private subjects for reactive updates
+ private georesourcesSubject = 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 georesources$ = this.georesourcesSubject.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 data with expiration
+ private georesourcesCache: {
+ data: GeoresourceMetadata[];
+ timestamp: number;
+ expiresAt: number;
+ } | null = null;
+
+ // Cache duration in milliseconds (5 minutes)
+ private readonly CACHE_DURATION = 5 * 60 * 1000;
+
+ // Environment configuration
+ private readonly env: any;
+ private readonly baseUrl: string;
+
+ // Date picker options
+ datePickerOptions = {
+ format: 'yyyy-mm-dd',
+ autoclose: true,
+ todayHighlight: true,
+ clearBtn: true
+ };
+
+ // Configuration options
+ enableKeycloakSecurity = true;
+ updateIntervalOptions = [
+ {
+ 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"
+ }
+ ];
+
+ // Available POI marker colors
+ availablePoiMarkerColors = [
+ {
+ "colorName": "red",
+ "colorValue": "rgb(205,59,40)"
+ },
+ {
+ "colorName": "white",
+ "colorValue": "rgb(255,255,255)"
+ },
+ {
+ "colorName": "orange",
+ "colorValue": "rgb(235,144,46)"
+ },
+ {
+ "colorName": "beige",
+ "colorValue": "rgb(255,198,138)"
+ },
+ {
+ "colorName": "green",
+ "colorValue": "rgb(108,166,36)"
+ },
+ {
+ "colorName": "blue",
+ "colorValue": "rgb(53,161,209)"
+ },
+ {
+ "colorName": "purple",
+ "colorValue": "rgb(198,77,175)"
+ },
+ {
+ "colorName": "pink",
+ "colorValue": "rgb(255,138,232)"
+ },
+ {
+ "colorName": "gray",
+ "colorValue": "rgb(163,163,163)"
+ },
+ {
+ "colorName": "black",
+ "colorValue": "rgb(47,47,47)"
+ }
+ ];
+
+ // Available LOI dash array objects
+ availableLoiDashArrayObjects = [
+ {
+ "svgString": ' ',
+ "dashArrayValue": ""
+ },
+ {
+ "svgString": ' ',
+ "dashArrayValue": "20"
+ },
+ {
+ "svgString": ' ',
+ "dashArrayValue": "20 10"
+ },
+ {
+ "svgString": ' ',
+ "dashArrayValue": "20 10 5 10"
+ },
+ {
+ "svgString": ' ',
+ "dashArrayValue": "5"
+ }
+ ];
+
+ // Available spatial units
+ availableSpatialUnits: any[] = [];
+
+ // Additional configuration options from AngularJS
+ indicatorTypeOptions: any[] = [];
+ indicatorUnitOptions: any[] = [];
+ indicatorCreationTypeOptions: any[] = [];
+ geodataSourceFormats: any[] = [];
+
+ // Current user state
+ private _currentKeycloakLoginRoles: string[] = [];
+ private _currentKomMonitorLoginRoleNames: string[] = [];
+ private currentKeycloakUser: any = null;
+
+ // Maps for quick access (like original AngularJS service)
+ private availableGeoresources_map = new Map();
+ private availableTopics_map = new Map();
+ private availableRoles_map = new Map();
+
+ // Available resources arrays (like original AngularJS service)
+ private _availableGeoresources: GeoresourceMetadata[] = [];
+ private _availableTopics: TopicHierarchy[] = [];
+ private _accessControl: RoleMetadata[] = [];
+
+ constructor(
+ private http: HttpClient,
+ private authService: AuthService,
+ private cacheHelperService: KommonitorGeoresourceCacheHelperService
+ ) {
+ // Get environment configuration
+ this.env = (window as any).__env || this.getDefaultEnvironment();
+ this.baseUrl = this.getBaseApiUrl();
+
+ // Initialize environment-based options
+ this.initializeEnvironmentOptions();
+
+ // Initialize the service
+ this.initializeService();
+ }
+
+ ngOnDestroy(): void {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+ /**
+ * Initialize environment-based configuration options
+ */
+ private initializeEnvironmentOptions(): void {
+ // Initialize options from environment configuration
+ if (this.env?.updateIntervalOptions) {
+ this.updateIntervalOptions = this.env.updateIntervalOptions;
+ }
+ if (this.env?.indicatorTypeOptions) {
+ this.indicatorTypeOptions = this.env.indicatorTypeOptions;
+ }
+ if (this.env?.indicatorUnitOptions) {
+ this.indicatorUnitOptions = this.env.indicatorUnitOptions.sort();
+ }
+ if (this.env?.indicatorCreationTypeOptions) {
+ this.indicatorCreationTypeOptions = this.env.indicatorCreationTypeOptions;
+ }
+ if (this.env?.geodataSourceFormats) {
+ this.geodataSourceFormats = this.env.geodataSourceFormats;
+ }
+ }
+
+ /**
+ * Get LOI dash SVG from string value (like AngularJS service)
+ */
+ getLoiDashSvgFromStringValue(loiDashArrayString: string): string | undefined {
+ for (const loiDashArrayObject of this.availableLoiDashArrayObjects) {
+ if (loiDashArrayObject.dashArrayValue === loiDashArrayString) {
+ return loiDashArrayObject.svgString;
+ }
+ }
+ return undefined;
+ }
+
+ /**
+ * 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._accessControl;
+ if (accessControl && accessControl.length > 0) {
+ accessControl.forEach(organizationalUnit => {
+ if (organizationalUnit.name) {
+ 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;
+ }
+ }
+
+ /**
+ * Set current Keycloak login roles and update subjects
+ */
+ private setCurrentKeycloakLoginRoles(roles: string[]): void {
+ this._currentKeycloakLoginRoles = roles;
+ this.currentRolesSubject.next(roles);
+
+ // Also filter and set KomMonitor-specific roles
+ const komMonitorRoles = this.filterKomMonitorRoles(roles);
+ this._currentKomMonitorLoginRoleNames = komMonitorRoles;
+ this.komMonitorRolesSubject.next(komMonitorRoles);
+ }
+
+ /**
+ * Clear all roles when user logs out
+ */
+ private clearRoles(): void {
+ this._currentKeycloakLoginRoles = [];
+ this._currentKomMonitorLoginRoleNames = [];
+ this.currentRolesSubject.next([]);
+ this.komMonitorRolesSubject.next([]);
+ }
+
+ /**
+ * Get current KomMonitor login role names
+ */
+ get currentKomMonitorLoginRoleNames(): string[] {
+ return this._currentKomMonitorLoginRoleNames;
+ }
+
+ /**
+ * Get current KomMonitor login role IDs
+ */
+ getCurrentKomMonitorLoginRoleIds(): string[] {
+ return this._currentKomMonitorLoginRoleNames;
+ }
+
+ /**
+ * Get default environment configuration
+ */
+ private getDefaultEnvironment(): any {
+ return {
+ enableKeycloakSecurity: true,
+ keycloakKomMonitorAdminRoleName: 'kommonitor-creator',
+ keycloakKomMonitorGroupsEditRoleNames: ['client-users-creator', 'unit-users-creator'],
+ keycloakKomMonitorThemesEditRoleNames: ['client-themes-creator', 'unit-themes-creator'],
+ keycloakKomMonitorGeodataEditRoleNames: ['client-resources-creator', 'unit-resources-creator'],
+ updateIntervalOptions: [
+ { displayName: 'jährlich', apiName: 'YEARLY' },
+ { displayName: 'halbjährlich', apiName: 'HALF_YEARLY' },
+ { displayName: 'vierteljährlich', apiName: 'QUARTERLY' },
+ { displayName: 'monatlich', apiName: 'MONTHLY' },
+ { displayName: 'beliebig', apiName: 'ARBITRARY' }
+ ],
+ availablePoiMarkerColors: [
+ { colorName: 'Weiß', colorValue: '#ffffff' },
+ { colorName: 'Rot', colorValue: '#ff0000' },
+ { colorName: 'Orange', colorValue: '#ffa500' },
+ { colorName: 'Beige', colorValue: '#f5f5dc' },
+ { colorName: 'Grün', colorValue: '#008000' },
+ { colorName: 'Blau', colorValue: '#0000ff' },
+ { colorName: 'Lila', colorValue: '#800080' },
+ { colorName: 'Pink', colorValue: '#ffc0cb' },
+ { colorName: 'Grau', colorValue: '#808080' },
+ { colorName: 'Schwarz', colorValue: '#000000' }
+ ],
+ availableLoiDashArrayObjects: [
+ { displayName: 'Durchgezogen', dashArrayValue: '0' },
+ { displayName: 'Gestrichelt', dashArrayValue: '20 20' },
+ { displayName: 'Gepunktet', dashArrayValue: '5 5' }
+ ]
+ };
+ }
+
+ /**
+ * Get base API URL from environment configuration
+ */
+ private getBaseApiUrl(): string {
+ if (this.env?.configStorageServerConfig?.targetUrlToConfigStorageServer) {
+ return this.env.configStorageServerConfig.targetUrlToConfigStorageServer;
+ }
+ 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 georesources
+ */
+ get availableGeoresources(): GeoresourceMetadata[] {
+ return this._availableGeoresources;
+ }
+
+ /**
+ * Get current Keycloak login roles
+ */
+ get currentKeycloakLoginRoles(): string[] {
+ return this._currentKeycloakLoginRoles;
+ }
+
+ /**
+ * Get available topics
+ */
+ get availableTopics(): TopicHierarchy[] {
+ return this._availableTopics;
+ }
+
+ /**
+ * Get access control
+ */
+ get accessControl(): RoleMetadata[] {
+ return this._accessControl;
+ }
+
+ /**
+ * Check create permission
+ */
+ 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;
+ }
+
+ /**
+ * Check editor permission
+ */
+ checkEditorPermission(): 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 editor or creator roles
+ const hasEditorRole = komMonitorRoles.some(role => role.endsWith('-editor') || role.endsWith('-creator'));
+
+ return hasEditorRole;
+ }
+
+ /**
+ * Check delete permission
+ */
+ checkDeletePermission(): 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;
+ }
+
+ /**
+ * Fetch georesources metadata
+ */
+ async fetchGeoresourcesMetadata(keycloakRolesArray: string[], filter?: any): Promise {
+ try {
+ this.loadingSubject.next(true);
+ this.errorSubject.next(null);
+
+ // Check cache first
+ if (this.georesourcesCache && Date.now() - this.georesourcesCache.timestamp < this.CACHE_DURATION) {
+ this.setGeoresources(this.georesourcesCache.data);
+ return this.georesourcesCache.data;
+ }
+
+ // Fetch from API
+ const url = `${this.baseUrl}/georesources`;
+ const headers = this.getAuthHeaders();
+
+ let response: GeoresourceMetadata[] | undefined;
+ if (filter) {
+ // POST request with filter
+ response = await this.http.post(`${url}/filter`, filter, { headers }).toPromise();
+ } else {
+ // Standard GET request
+ response = await this.http.get(url, { headers }).toPromise();
+ }
+
+ if (!response) {
+ throw new Error('No response from georesources API');
+ }
+
+ // Update cache
+ this.georesourcesCache = {
+ data: response,
+ timestamp: Date.now(),
+ expiresAt: Date.now() + this.CACHE_DURATION
+ };
+
+ // Set georesources
+ this.setGeoresources(response);
+
+ return response;
+
+ } catch (error) {
+ console.error('Error fetching georesources metadata:', error);
+ this.handleError(error);
+ throw error;
+ } finally {
+ this.loadingSubject.next(false);
+ }
+ }
+
+ /**
+ * Set georesources (like original AngularJS service)
+ */
+ private setGeoresources(georesourcesArray: GeoresourceMetadata[]): void {
+ this._availableGeoresources = georesourcesArray;
+ this.availableGeoresources_map.clear();
+
+ for (const georesourceMetadata of georesourcesArray) {
+ this.availableGeoresources_map.set(georesourceMetadata.georesourceId, georesourceMetadata);
+ }
+
+ // Update the subject
+ this.georesourcesSubject.next(georesourcesArray);
+ }
+
+ /**
+ * Add single georesource metadata (like original AngularJS service)
+ */
+ addSingleGeoresourceMetadata(georesourceMetadata: GeoresourceMetadata): void {
+ const tmpArray = [georesourceMetadata];
+ Array.prototype.push.apply(tmpArray, this._availableGeoresources);
+ this._availableGeoresources = tmpArray;
+ this.availableGeoresources_map.set(georesourceMetadata.georesourceId, georesourceMetadata);
+
+ // Update the subject
+ this.georesourcesSubject.next(this._availableGeoresources);
+ }
+
+ /**
+ * Replace single georesource metadata (like original AngularJS service)
+ */
+ replaceSingleGeoresourceMetadata(georesourceMetadata: GeoresourceMetadata): void {
+ for (let index = 0; index < this._availableGeoresources.length; index++) {
+ const georesource = this._availableGeoresources[index];
+ if (georesource.georesourceId === georesourceMetadata.georesourceId) {
+ this._availableGeoresources[index] = georesourceMetadata;
+ break;
+ }
+ }
+ this.availableGeoresources_map.set(georesourceMetadata.georesourceId, georesourceMetadata);
+ // Keep cache in sync so a subsequent cached fetch does not overwrite fresh data
+ if (this.georesourcesCache && Array.isArray(this.georesourcesCache.data)) {
+ const cacheIndex = this.georesourcesCache.data.findIndex(
+ (g) => g.georesourceId === georesourceMetadata.georesourceId
+ );
+ if (cacheIndex !== -1) {
+ this.georesourcesCache.data[cacheIndex] = georesourceMetadata;
+ } else {
+ // If it wasn't present, prepend to keep behavior consistent with add
+ this.georesourcesCache.data.unshift(georesourceMetadata);
+ }
+ // Refresh cache timestamp to avoid immediate refetch churn
+ this.georesourcesCache.timestamp = Date.now();
+ }
+
+ // Update the subject
+ this.georesourcesSubject.next(this._availableGeoresources);
+ }
+
+ /**
+ * Delete single georesource metadata (like original AngularJS service)
+ */
+ deleteSingleGeoresourceMetadata(georesourceId: string): void {
+ for (let index = 0; index < this._availableGeoresources.length; index++) {
+ const georesource = this._availableGeoresources[index];
+ if (georesource.georesourceId === georesourceId) {
+ this._availableGeoresources.splice(index, 1);
+ break;
+ }
+ }
+ this.availableGeoresources_map.delete(georesourceId);
+
+ // Update the subject
+ this.georesourcesSubject.next(this._availableGeoresources);
+ }
+
+ /**
+ * Get georesource metadata by ID (like original AngularJS service)
+ */
+ getGeoresourceMetadataById(georesourceId: string): GeoresourceMetadata | undefined {
+ return this.availableGeoresources_map.get(georesourceId);
+ }
+
+ /**
+ * Get base URL to KomMonitor Data API for spatial resources
+ */
+ getBaseUrlToKomMonitorDataAPI_spatialResource(): string {
+ return this.baseUrl;
+ }
+
+ /**
+ * Get base URL to KomMonitor Data API (getter for compatibility)
+ */
+ get baseUrlToKomMonitorDataAPI(): string {
+ return this.baseUrl;
+ }
+
+ /**
+ * Get role title (like original AngularJS service)
+ */
+ getRoleTitle(roleId: string): string {
+ const role = this.availableRoles_map.get(roleId);
+ if (role) {
+ return role.title || role.name || roleId;
+ }
+ return roleId;
+ }
+
+
+
+ /**
+ * Get topic hierarchy display string (like original AngularJS service)
+ */
+ getTopicHierarchyDisplayString(topicReference: any): string {
+ if (!topicReference) return '';
+
+ if (Array.isArray(topicReference)) {
+ return topicReference.map((topic: any) => topic.name || topic.title || topic.id).join(' > ');
+ }
+
+ if (typeof topicReference === 'object') {
+ return topicReference.name || topicReference.title || topicReference.id || '';
+ }
+
+ return String(topicReference);
+ }
+
+ /**
+ * Get all allowed roles string (like original AngularJS service)
+ */
+ getAllowedRolesString(permissions: any): string {
+ if (!permissions) return '';
+
+ if (Array.isArray(permissions)) {
+ return permissions.join(', ');
+ }
+
+ if (typeof permissions === 'object') {
+ return Object.keys(permissions).join(', ');
+ }
+
+ return String(permissions);
+ }
+
+
+
+ /**
+ * Syntax highlight JSON for display (matches original AngularJS implementation)
+ */
+ syntaxHighlightJSON(json: any): string {
+ if (typeof json !== 'string') {
+ json = JSON.stringify(json, undefined, 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 + ' ';
+ });
+ }
+
+
+
+ /**
+ * Get authentication headers
+ */
+ private getAuthHeaders(): HttpHeaders {
+ const headers = new HttpHeaders({
+ 'Content-Type': 'application/json'
+ });
+
+ // Add auth token if available
+ const token = this.getKeycloakToken();
+ if (token) {
+ headers.set('Authorization', `Bearer ${token}`);
+ }
+
+ return headers;
+ }
+
+ /**
+ * Get Keycloak token
+ */
+ private getKeycloakToken(): string | null {
+ try {
+ const keycloak = this.authService.Auth?.keycloak;
+ return keycloak?.token || null;
+ } catch (error) {
+ return null;
+ }
+ }
+
+ /**
+ * Handle errors
+ */
+ private handleError(error: any): void {
+ let errorMessage = 'An error occurred';
+
+ if (error.error && error.error.message) {
+ errorMessage = error.error.message;
+ } else if (error.message) {
+ errorMessage = error.message;
+ } else if (typeof error === 'string') {
+ errorMessage = error;
+ }
+
+ this.errorSubject.next(errorMessage);
+ console.error('Service error:', error);
+ }
+
+ /**
+ * Clear cache
+ */
+ clearCache(): void {
+ this.georesourcesCache = null;
+ }
+
+ /**
+ * Refresh data
+ */
+ async refreshData(): Promise {
+ this.clearCache();
+ await this.fetchGeoresourcesMetadata(this._currentKeycloakLoginRoles);
+ }
+
+ // Get access control by ID
+ getAccessControlById(organizationalUnitId: string): any | undefined {
+ if (!this.accessControl) return undefined;
+ return this.accessControl.find(item => item.organizationalUnitId === organizationalUnitId);
+ }
+
+ // Fetch access control metadata
+ async fetchAccessControlMetadata(): Promise {
+ try {
+ console.log('Fetching access control metadata from:', `${this.baseUrl}/organizationalUnits`);
+ const response = await this.http.get(`${this.baseUrl}/organizationalUnits`).toPromise();
+ console.log('Access control metadata response:', response);
+ if (response) {
+ this._accessControl = response;
+ console.log('Access control data set:', this._accessControl);
+ }
+ } catch (error) {
+ console.error('Error fetching access control metadata:', error);
+ console.error('Base URL:', this.baseUrl);
+ console.error('Full URL:', `${this.baseUrl}/organizationalUnits`);
+ throw error;
+ }
+ }
+
+ /**
+ * Fetches topics metadata
+ */
+ async fetchTopicsMetadata(keycloakRolesArray: string[]): Promise {
+ this.loadingSubject.next(true);
+ this.errorSubject.next(null);
+
+ // Set the current roles for permission checking
+ this.setCurrentKeycloakLoginRoles(keycloakRolesArray);
+
+ try {
+ // Check cache first
+ if (this._availableTopics && this._availableTopics.length > 0) {
+ console.log('Using cached topics data');
+ this.loadingSubject.next(false);
+ return this._availableTopics;
+ }
+
+ // Use cache helper service to fetch topics
+ if (!this.cacheHelperService) {
+ console.error('Cache helper service not available');
+ throw new Error('Cache helper service not available');
+ }
+
+ const topics = await this.cacheHelperService.fetchTopicsMetadata(keycloakRolesArray);
+
+ if (!topics || !Array.isArray(topics)) {
+ console.warn('No topics data received from cache helper');
+ this._availableTopics = [];
+ this.loadingSubject.next(false);
+ return [];
+ }
+
+ // Transform the response to match TopicHierarchy interface
+ const transformedTopics = this.transformTopicsResponse(topics);
+ this._availableTopics = transformedTopics;
+
+ // Update the map for quick access
+ this.availableTopics_map.clear();
+ transformedTopics.forEach(topic => {
+ this.availableTopics_map.set(topic.topicId, topic);
+ });
+
+ console.log('Topics data loaded:', this._availableTopics);
+ this.loadingSubject.next(false);
+
+ return this._availableTopics;
+ } catch (error) {
+ console.error('Error fetching topics metadata:', error);
+ this.handleError(error);
+ this.loadingSubject.next(false);
+ throw error;
+ }
+ }
+
+ /**
+ * Transform API response to TopicHierarchy format
+ */
+ private transformTopicsResponse(apiTopics: any[]): TopicHierarchy[] {
+ const transformed = apiTopics.map(topic => ({
+ topicId: topic.topicId || topic.id,
+ name: topic.name || topic.topicName,
+ title: topic.title || topic.name || topic.topicName,
+ topicType: topic.topicType,
+ topicResource: topic.topicResource,
+ topicName: topic.topicName || topic.name || topic.title,
+ subTopics: Array.isArray(topic.subTopics) ? this.transformTopicsResponse(topic.subTopics) : undefined
+ }));
+ try {
+ console.log('[DataExchangeService] transformTopicsResponse -> counts', {
+ inputCount: Array.isArray(apiTopics) ? apiTopics.length : 0,
+ outputCount: Array.isArray(transformed) ? transformed.length : 0
+ });
+ } catch {}
+ return transformed;
+ }
+
+ // Check if user has admin permission (matches original AngularJS implementation)
+ checkAdminPermission(): boolean {
+ if (!this.env?.keycloakKomMonitorAdminRoleName) {
+ return false;
+ }
+
+ return this.currentKeycloakLoginRoles.includes(this.env.keycloakKomMonitorAdminRoleName);
+ }
+
+ // Get topic hierarchy for topic ID (matches original AngularJS implementation)
+ getTopicHierarchyForTopicId(topicReferenceId: string): TopicHierarchy[] {
+ // create an array representing the topic hierarchy
+ // i.e. [mainTopic_firstTier, subTopic_secondTier, subTopic_thirdTier, ...]
+ const topicHierarchyArray: TopicHierarchy[] = [];
+
+ for (let i = 0; i < this.availableTopics.length; i++) {
+ const mainTopicCandidate = this.availableTopics[i];
+
+ if (mainTopicCandidate.topicId === topicReferenceId) {
+ topicHierarchyArray.push(mainTopicCandidate);
+ break;
+ } else if (mainTopicCandidate.subTopics && this.findIdInAnySubTopicHierarchy(topicReferenceId, mainTopicCandidate.subTopics)) {
+ topicHierarchyArray.push(mainTopicCandidate);
+ return this.addSubTopicHierarchy(topicHierarchyArray, topicReferenceId, mainTopicCandidate.subTopics);
+ }
+ }
+
+ return topicHierarchyArray;
+ }
+
+ private findIdInAnySubTopicHierarchy(topicReferenceId: string, subTopicsArray: TopicHierarchy[]): boolean {
+ for (let index = 0; index < subTopicsArray.length; index++) {
+ const subTopicCandidate = subTopicsArray[index];
+
+ if (subTopicCandidate.topicId === topicReferenceId) {
+ return true;
+ } else if (subTopicCandidate.subTopics && this.findIdInAnySubTopicHierarchy(topicReferenceId, subTopicCandidate.subTopics)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private addSubTopicHierarchy(topicHierarchyArray: TopicHierarchy[], topicReferenceId: string, subTopicsArray: TopicHierarchy[]): TopicHierarchy[] {
+ for (let index = 0; index < subTopicsArray.length; index++) {
+ const subTopicCandidate = subTopicsArray[index];
+
+ if (subTopicCandidate.topicId === topicReferenceId) {
+ topicHierarchyArray.push(subTopicCandidate);
+ break;
+ } else if (subTopicCandidate.subTopics && this.findIdInAnySubTopicHierarchy(topicReferenceId, subTopicCandidate.subTopics)) {
+ topicHierarchyArray.push(subTopicCandidate);
+ return this.addSubTopicHierarchy(topicHierarchyArray, topicReferenceId, subTopicCandidate.subTopics);
+ }
+ }
+
+ return topicHierarchyArray;
+ }
+}
\ 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..cd26acd41
--- /dev/null
+++ b/app/services/adminGeoresourceUnit/kommonitor-data-grid-helper.service.ts
@@ -0,0 +1,1749 @@
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+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';
+
+// Interfaces for better typing
+export interface GeoresourceMetadata {
+ georesourceId: string;
+ datasetName: string;
+ isPOI?: boolean;
+ isLOI?: boolean;
+ isAOI?: boolean;
+ poiSymbolColor?: string;
+ poiSymbolBootstrap3Name?: string;
+ poiMarkerColor?: string;
+ loiColor?: string;
+ loiWidth?: number;
+ loiDashArrayString?: string;
+ aoiColor?: string;
+ metadata?: {
+ description?: string;
+ datasource?: string;
+ contact?: string;
+ };
+ availablePeriodsOfValidity?: Array<{
+ startDate: string;
+ endDate?: string;
+ }>;
+ topicReference?: any;
+ permissions?: any;
+ isPublic?: boolean;
+ ownerId?: string;
+ userPermissions?: string[];
+}
+
+export interface GridState {
+ columnDefs: any[];
+ timestamp: Date;
+}
+
+@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;
+
+ // Grid state storage
+ private gridStates = new Map();
+
+ // Current georesources data for lookup
+ private currentGeoresources: GeoresourceMetadata[] = [];
+
+ // Timestamp properties for feature table updates (like original AngularJS service)
+ 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;
+
+ // Resource type constants (like original AngularJS service)
+ readonly resourceType_georesource = "georesource";
+ readonly resourceType_spatialUnit = "spatialUnit";
+ readonly resourceType_indicator = "indicator";
+
+ constructor(
+ private broadcastService: BroadcastService,
+ private http: HttpClient,
+ 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: GeoresourceMetadata[]): 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;
+ }
+
+ // Store current georesources for lookup
+ this.currentGeoresources = [...georesourcesArray];
+
+ // Update timestamps like original AngularJS service
+ this.featureTable_georesource_lastUpdate_timestamp_success = new Date();
+
+ this.buildPoiGrid(georesourcesArray);
+ this.buildLoiGrid(georesourcesArray);
+ this.buildAoiGrid(georesourcesArray);
+ }
+
+ /**
+ * Build POI grid
+ */
+ private buildPoiGrid(georesourcesArray: GeoresourceMetadata[]): 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);
+ }, 500);
+ } catch (error) {
+ console.error('Error updating POI grid:', error);
+ this.featureTable_georesource_lastUpdate_timestamp_failure = new Date();
+ }
+ }
+
+ /**
+ * Build LOI grid
+ */
+ private buildLoiGrid(georesourcesArray: GeoresourceMetadata[]): 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);
+ }, 500);
+ } catch (error) {
+ console.error('Error updating LOI grid:', error);
+ this.featureTable_georesource_lastUpdate_timestamp_failure = new Date();
+ }
+ }
+
+ /**
+ * Build AOI grid
+ */
+ private buildAoiGrid(georesourcesArray: GeoresourceMetadata[]): 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);
+ }, 500);
+ } catch (error) {
+ console.error('Error updating AOI grid:', error);
+ this.featureTable_georesource_lastUpdate_timestamp_failure = new Date();
+ }
+ }
+
+ /**
+ * Register click handlers for georesource buttons
+ */
+ private registerClickHandler_georesources(georesourceMetadataArray: GeoresourceMetadata[]): 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);
+ });
+
+ // Also try to find buttons by their specific IDs
+ if (georesourceMetadataArray && georesourceMetadataArray.length > 0) {
+ georesourceMetadataArray.forEach(geo => {
+ const editMetadataBtn = document.getElementById(`btn_georesource_editMetadata_${geo.georesourceId}`);
+ const editFeaturesBtn = document.getElementById(`btn_georesource_editFeatures_${geo.georesourceId}`);
+ const editUserRolesBtn = document.getElementById(`btn_georesource_editUserRoles_${geo.georesourceId}`);
+ const deleteBtn = document.getElementById(`btn_georesource_deleteGeoresource_${geo.georesourceId}`);
+
+ if (editMetadataBtn) {
+ editMetadataBtn.removeEventListener('click', this.handleEditMetadataClick);
+ editMetadataBtn.addEventListener('click', this.handleEditMetadataClick);
+ }
+ if (editFeaturesBtn) {
+ editFeaturesBtn.removeEventListener('click', this.handleEditFeaturesClick);
+ editFeaturesBtn.addEventListener('click', this.handleEditFeaturesClick);
+ }
+ if (editUserRolesBtn) {
+ editUserRolesBtn.removeEventListener('click', this.handleEditUserRolesClick);
+ editUserRolesBtn.addEventListener('click', this.handleEditUserRolesClick);
+ }
+ if (deleteBtn) {
+ deleteBtn.removeEventListener('click', this.handleDeleteClick);
+ deleteBtn.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.findGeoresourceMetadataById(georesourceId);
+
+ if (this.componentRef && georesourceMetadata) {
+ 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.findGeoresourceMetadataById(georesourceId);
+
+ if (this.componentRef && georesourceMetadata) {
+ 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.findGeoresourceMetadataById(georesourceId);
+
+ if (this.componentRef && georesourceMetadata) {
+ 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.findGeoresourceMetadataById(georesourceId);
+
+ if (this.componentRef && georesourceMetadata) {
+ this.componentRef.onClickDeleteGeoresource(georesourceMetadata);
+ }
+ }
+
+ /**
+ * Find georesource metadata by ID from current data
+ */
+ private findGeoresourceMetadataById(georesourceId: string): GeoresourceMetadata | null {
+ return this.currentGeoresources.find(geo => geo.georesourceId === georesourceId) || null;
+ }
+
+ /**
+ * 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 = '';
+ return html;
+ }
+ },
+ {
+ headerName: 'Themenhierarchie',
+ minWidth: 400,
+ cellRenderer: (params: any) => {
+ return this.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.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.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.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 = '';
+ return html;
+ }
+ },
+ {
+ headerName: 'Themenhierarchie',
+ minWidth: 400,
+ cellRenderer: (params: any) => {
+ return this.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.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.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 = '';
+ return html;
+ }
+ },
+ {
+ headerName: 'Themenhierarchie',
+ minWidth: 400,
+ cellRenderer: (params: any) => {
+ return this.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.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.getRoleTitle(params.data.ownerId);
+ }
+ }
+ ];
+ }
+
+ /**
+ * Get selected georesources metadata from all grids
+ */
+ getSelectedGeoresourcesMetadata(): GeoresourceMetadata[] {
+ const selectedRows: GeoresourceMetadata[] = [];
+
+ 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 {
+ const date = new Date();
+ const hours = date.getHours().toString().padStart(2, '0');
+ const minutes = date.getMinutes().toString().padStart(2, '0');
+ const seconds = date.getSeconds().toString().padStart(2, '0');
+ return `${hours}:${minutes}:${seconds}`;
+ }
+
+ /**
+ * 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`
+ });
+ }
+ }
+
+ /**
+ * Save grid state for a specific grid
+ */
+ saveGridState(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) {
+ const state: GridState = {
+ columnDefs: gridApi.getColumnDefs() || [],
+ timestamp: new Date()
+ };
+
+ this.gridStates.set(gridType, state);
+ }
+ }
+
+ /**
+ * Restore grid state for a specific grid
+ */
+ restoreGridState(gridType: 'poi' | 'loi' | 'aoi'): void {
+ const state = this.gridStates.get(gridType);
+ if (!state) return;
+
+ 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) {
+ // Restore column definitions
+ gridApi.setColumnDefs(state.columnDefs);
+ }
+ }
+
+ /**
+ * 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);
+ }
+ };
+ }
+
+ // Utility methods that were in the original AngularJS service
+
+ /**
+ * Get topic hierarchy display string
+ */
+ private getTopicHierarchyDisplayString(topicReference: any): string {
+ if (!topicReference) return '';
+
+ // Simple implementation - can be enhanced based on actual topic structure
+ if (Array.isArray(topicReference)) {
+ return topicReference.map((topic: any) => topic.name || topic.title || topic.id).join(' > ');
+ }
+
+ if (typeof topicReference === 'object') {
+ return topicReference.name || topicReference.title || topicReference.id || '';
+ }
+
+ return String(topicReference);
+ }
+
+ /**
+ * Get allowed roles string
+ */
+ private getAllowedRolesString(permissions: any): string {
+ if (!permissions) return '';
+
+ if (Array.isArray(permissions)) {
+ return permissions.join(', ');
+ }
+
+ if (typeof permissions === 'object') {
+ return Object.keys(permissions).join(', ');
+ }
+
+ return String(permissions);
+ }
+
+ /**
+ * Get role title
+ */
+ private getRoleTitle(ownerId: any): string {
+ if (!ownerId) return '';
+
+ // Simple implementation - can be enhanced based on actual role structure
+ if (typeof ownerId === 'object') {
+ return ownerId.name || ownerId.title || ownerId.id || '';
+ }
+
+ return String(ownerId);
+ }
+
+ /**
+ * Get LOI dash SVG from string value
+ */
+ private getLoiDashSvgFromStringValue(dashArrayString: string): string {
+ if (!dashArrayString) return '';
+
+ // Simple implementation - can be enhanced to generate actual SVG
+ return `
`;
+ }
+
+ /**
+ * Manually re-register click handlers for all grids
+ */
+ reRegisterClickHandlers(): void {
+ if (this.currentGeoresources && this.currentGeoresources.length > 0) {
+ this.registerClickHandler_georesources(this.currentGeoresources);
+ }
+ }
+
+ /**
+ * Build role management grid
+ */
+ buildRoleManagementGrid(
+ gridId: string,
+ existingOptions: any,
+ accessControl: any[],
+ selectedRoleIds: string[]
+ ): any {
+ if (!accessControl || accessControl.length === 0) {
+ return null;
+ }
+
+ // Build row data from access control (like spatial unit service)
+ const rowData = this.buildRoleManagementGridRowData(accessControl, selectedRoleIds);
+
+ return {
+ gridId: gridId,
+ rowData: rowData,
+ columnDefs: this.buildRoleManagementGridColumnConfig(true), // Use reducedRoleManagement = true
+ components: this.getRoleManagementComponents(),
+ defaultColDef: {
+ editable: false,
+ sortable: true,
+ filter: true,
+ resizable: true,
+ wrapText: true,
+ autoHeight: true,
+ cellStyle: {
+ 'font-size': '12px',
+ 'white-space': 'normal',
+ 'line-height': '20px',
+ 'word-break': 'break-word',
+ 'padding-top': '12px',
+ 'padding-bottom': '12px'
+ }
+ },
+ suppressRowClickSelection: true,
+ rowSelection: 'multiple',
+ enableCellTextSelection: true,
+ ensureDomOrder: true,
+ pagination: true,
+ paginationPageSize: 10,
+ suppressColumnVirtualisation: true,
+ headerHeight: 40,
+ rowHeight: 35,
+ onFirstDataRendered: (params: any) => {
+ try { params.api.resetRowHeights(); } catch {}
+ },
+ onColumnResized: (params: any) => {
+ try { params.api.resetRowHeights(); } catch {}
+ },
+ onRowDataChanged: (params: any) => {
+ try { params.api.resetRowHeights(); } catch {}
+ }
+ };
+ }
+
+ /**
+ * Build role management grid row data (like spatial unit service)
+ */
+ private buildRoleManagementGridRowData(accessControl: any[], permissionIds: string[]): any[] {
+ if (!accessControl || accessControl.length === 0) {
+ return [];
+ }
+
+ // Clone and annotate permissions with isChecked flags based on provided permissionIds
+ const data = JSON.parse(JSON.stringify(accessControl));
+
+ for (const elem of data) {
+ if (elem.name === 'public') {
+ elem.name = 'Öffentlicher Zugriff';
+ }
+ // Ensure helper flags exist for disable cascading
+ elem._viewerDisabledBecauseOfEditor = false;
+ elem._viewerDisabledBecauseOfCreator = false;
+ elem._editorDisabledBecauseOfCreator = false;
+ if (elem.permissions && Array.isArray(elem.permissions)) {
+ for (const permission of elem.permissions) {
+ permission.isChecked = permissionIds && permissionIds.includes(permission.permissionId);
+ }
+ }
+ }
+
+ // Sort data properly - put 'public' and first organization first, then sort the rest
+ const sortedData: any[] = [];
+ const publicItem = data.find(item => item.name === 'Öffentlicher Zugriff');
+ const firstOrg = data.find(item => item.name !== 'Öffentlicher Zugriff');
+
+ if (publicItem) {
+ sortedData.push(publicItem);
+ }
+ if (firstOrg) {
+ sortedData.push(firstOrg);
+ }
+
+ // Add remaining items sorted alphabetically
+ const remainingItems = data.filter(item =>
+ item.name !== 'Öffentlicher Zugriff' && item !== firstOrg
+ ).sort((a, b) => {
+ if (a.name < b.name) return -1;
+ if (a.name > b.name) return 1;
+ return 0;
+ });
+
+ return sortedData.concat(remainingItems);
+ }
+
+ /**
+ * Build role management grid column configuration (like spatial unit service)
+ */
+ 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;
+ }
+
+ /**
+ * Get selected role IDs from role management grid
+ */
+ getSelectedRoleIds_roleManagementGrid(gridOptions: any): string[] {
+ if (!gridOptions || !gridOptions.rowData) {
+ return [];
+ }
+
+ 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);
+ }
+ }
+ };
+
+ for (const row of gridOptions.rowData) {
+ collectFromRow(row);
+ }
+
+ return Array.from(selectedIds);
+ }
+
+ /**
+ * 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
+ };
+ }
+
+ /**
+ * Checkbox renderer for viewer permissions (georesource)
+ */
+ 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: string | undefined;
+ if (params && params.data && Array.isArray(params.data.permissions)) {
+ 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 enforced by editor/creator
+ 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 {
+ this.eGui = document.createElement('span');
+ }
+ }
+
+ checkedHandler(e: any) {
+ const checked = e.target.checked;
+ if (this.params && this.params.data && Array.isArray(this.params.data.permissions)) {
+ 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 (georesource)
+ */
+ 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: string | undefined;
+ if (params && params.data && Array.isArray(params.data.permissions)) {
+ 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 for dataset owner or creator enforced
+ 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 {
+ this.eGui = document.createElement('span');
+ }
+ }
+
+ checkedHandler(e: any) {
+ const checked = e.target.checked;
+ if (this.params && this.params.data && Array.isArray(this.params.data.permissions)) {
+ for (const permission of this.params.data.permissions) {
+ if (permission.permissionLevel === 'viewer') {
+ permission.isChecked = !!checked || !!permission.isChecked;
+ } else if (permission.permissionLevel === 'editor') {
+ permission.isChecked = checked;
+ }
+ }
+ }
+ // Enforce viewer checked+disabled when editor is checked
+ 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;
+ }
+ 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 (georesource)
+ */
+ 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: string | undefined;
+ if (params && params.data && Array.isArray(params.data.permissions)) {
+ 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 for dataset owner
+ 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 {
+ this.eGui = document.createElement('span');
+ }
+ }
+
+ checkedHandler(e: any) {
+ const checked = e.target.checked;
+ if (this.params && this.params.data && Array.isArray(this.params.data.permissions)) {
+ for (const permission of this.params.data.permissions) {
+ if (permission.permissionLevel === 'creator' || permission.permissionLevel === 'editor' || permission.permissionLevel === 'viewer') {
+ permission.isChecked = checked;
+ }
+ }
+ }
+ // Enforce cascading disable flags
+ if (checked) {
+ this.params.data._editorDisabledBecauseOfCreator = true;
+ this.params.data._viewerDisabledBecauseOfCreator = true;
+ } else {
+ this.params.data._editorDisabledBecauseOfCreator = false;
+ this.params.data._viewerDisabledBecauseOfCreator = false;
+ }
+ 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 data grid for feature table of spatial resource (like spatial unit service)
+ */
+ buildDataGrid_featureTable_spatialResource(
+ tableId: string,
+ headers: string[],
+ features: any[] = [],
+ resourceId?: string,
+ resourceType?: string,
+ enableDelete: boolean = false
+ ): any {
+ console.log(`Building feature table grid for ${tableId} with ${features.length} features`);
+
+ 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'
+ },
+ },
+ columnDefs: columnDefs,
+ rowData: rowData,
+ pagination: true,
+ paginationPageSize: 25,
+ domLayout: 'autoHeight',
+ suppressRowClickSelection: true,
+ enableCellTextSelection: true,
+ suppressCellFocus: true
+ };
+
+ return gridOptions;
+ }
+
+ /**
+ * Build column configuration for feature table
+ */
+ private buildFeatureTableColumnConfig(headers: string[], enableDelete: boolean, resourceType?: string): any[] {
+ const columnDefs: any[] = [];
+
+ // Add standard columns
+ columnDefs.push(
+ {
+ headerName: 'ID',
+ field: 'ID',
+ minWidth: 100,
+ editable: false,
+ cellStyle: { 'font-weight': 'bold' }
+ },
+ {
+ headerName: 'Name',
+ field: 'NAME',
+ minWidth: 200,
+ editable: true
+ },
+ {
+ headerName: 'Valid Start Date',
+ field: 'validStartDate',
+ minWidth: 150,
+ editable: true
+ },
+ {
+ headerName: 'Valid End Date',
+ field: 'validEndDate',
+ minWidth: 150,
+ editable: true
+ }
+ );
+
+ // Add dynamic headers
+ headers.forEach(header => {
+ columnDefs.push({
+ headerName: header,
+ field: header,
+ minWidth: 150,
+ editable: true
+ });
+ });
+
+ // Add delete button column if enabled
+ if (enableDelete) {
+ columnDefs.push({
+ headerName: 'Actions',
+ field: 'actions',
+ minWidth: 100,
+ editable: false,
+ cellRenderer: 'deleteButtonRenderer',
+ cellRendererParams: {
+ resourceType: resourceType || 'georesource'
+ }
+ });
+ }
+
+ return columnDefs;
+ }
+
+ /**
+ * Build row data for feature table
+ */
+ private buildFeatureTableRowData(features: any[]): any[] {
+ if (!features || features.length === 0) {
+ return [];
+ }
+
+ return features.map(feature => {
+ if (feature.properties) {
+ return {
+ ...feature.properties,
+ kommonitorGeometry: feature.geometry,
+ kommonitorRecordId: feature.id
+ };
+ }
+ return feature;
+ });
+ }
+
+ /**
+ * Register click handlers for feature table
+ */
+ registerFeatureTableClickHandlers(resourceId: string, resourceType: string, enableDelete: boolean): void {
+ if (!enableDelete) return;
+
+ // This would typically register delete button click handlers
+
+ }
+
+ /**
+ * Delete button renderer for feature table
+ */
+ deleteButtonRenderer(params: any): string {
+ const resourceType = params.colDef?.cellRendererParams?.resourceType || 'georesource';
+ const recordId = params.data?.kommonitorRecordId || params.data?.ID;
+
+ if (!recordId) {
+ return 'No ID
';
+ }
+
+ const deleteButtonId = `btn_${resourceType}_deleteFeature_${recordId}`;
+
+ return `
+
+
+
+
+
+ `;
+ }
+
+ /**
+ * Handle cell value changes for feature table
+ */
+ handleCellValueChanged(newValueParams: any, resourceId?: string, resourceType?: string): void {
+ console.log('handleCellValueChanged called with:', {
+ resourceId,
+ resourceType,
+ componentRef: !!this.componentRef,
+ column: newValueParams.colDef?.field,
+ oldValue: newValueParams.oldValue,
+ newValue: newValueParams.newValue
+ });
+
+ // Get the resourceId from the component context if not provided
+ if (!resourceId && this.componentRef && this.componentRef.currentGeoresourceDataset) {
+ resourceId = this.componentRef.currentGeoresourceDataset.georesourceId;
+ console.log('Got resourceId from component:', resourceId);
+ }
+
+ // If we still don't have a resourceId, log error and return
+ if (!resourceId) {
+ console.error('ResourceId is undefined. Cannot update feature.');
+ console.error('Available data:', newValueParams.data);
+ console.error('Component ref:', this.componentRef);
+ return;
+ }
+
+ // 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}`;
+
+ console.log('Making PUT request to:', url);
+
+ // Make HTTP PUT request
+ this.http.put(url, geoJSON, {
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }).subscribe({
+ next: (response: any) => {
+ console.log('Feature update successful:', response);
+ // 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('Feature update failed:', 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();
+ }
+ }
+ });
+ }
+
+ /**
+ * Get current timestamp
+ */
+ private getCurrentTimestamp(): Date {
+ return new Date();
+ }
+}
\ No newline at end of file
diff --git a/app/services/adminGeoresourceUnit/kommonitor-importer-helper.service.ts b/app/services/adminGeoresourceUnit/kommonitor-importer-helper.service.ts
new file mode 100644
index 000000000..801689a60
--- /dev/null
+++ b/app/services/adminGeoresourceUnit/kommonitor-importer-helper.service.ts
@@ -0,0 +1,610 @@
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class KommonitorImporterHelperService {
+
+ constructor(private http: HttpClient) {
+ this.targetUrlToImporterService = (window as any).__env?.targetUrlToImporterService || '/api/importer/';
+ }
+
+ private targetUrlToImporterService: string = '/api/importer/';
+
+ // Single feature import definitions
+ converterDefinition_singleFeatureImport = {
+ name: 'single-feature-converter',
+ schema: 'geojson',
+ mimeType: 'application/geo+json',
+ parameters: []
+ };
+
+ datasourceDefinition_singleFeatureImport = {
+ type: 'inline',
+ parameters: [
+ { name: 'geoJsonData', value: '' }
+ ]
+ };
+
+ propertyMappingDefinition_singleFeatureImport = {
+ nameProperty: 'NAME',
+ identifierProperty: 'ID',
+ validStartDateProperty: 'validStartDate',
+ validEndDateProperty: 'validEndDate',
+ keepAttributes: true,
+ keepMissingOrNullValueAttributes: true,
+ attributes: []
+ };
+
+ // Available converters for the template
+ availableConverters = [
+ {
+ name: 'csv-converter',
+ displayName: 'CSV Converter',
+ description: 'Convert CSV files to georesource format',
+ schemas: ['csv-schema'],
+ mimeTypes: ['text/csv'],
+ datasources: ['FILE'],
+ parameters: []
+ },
+ {
+ name: 'json-converter',
+ displayName: 'JSON Converter',
+ description: 'Convert JSON files to georesource format',
+ schemas: ['json-schema'],
+ mimeTypes: ['application/json'],
+ datasources: ['FILE', 'HTTP'],
+ parameters: []
+ },
+ {
+ name: 'xml-converter',
+ displayName: 'XML Converter',
+ description: 'Convert XML files to georesource format',
+ schemas: ['xml-schema'],
+ mimeTypes: ['application/xml', 'text/xml'],
+ datasources: ['FILE', 'HTTP'],
+ parameters: []
+ },
+ {
+ name: 'geojson-converter',
+ displayName: 'GeoJSON Converter',
+ description: 'Convert GeoJSON files to georesource format',
+ schemas: ['geojson-schema'],
+ mimeTypes: ['application/geo+json'],
+ datasources: ['FILE', 'HTTP', 'OGCAPI_FEATURES'],
+ parameters: []
+ }
+ ];
+
+ // Available datasource types for the template
+ availableDatasourceTypes = [
+ {
+ type: 'FILE',
+ name: 'File Upload',
+ description: 'Upload a file from your computer',
+ parameters: [
+ { name: 'file', type: 'file', required: true }
+ ]
+ },
+ {
+ type: 'HTTP',
+ name: 'HTTP URL',
+ description: 'Download from a web URL',
+ parameters: [
+ { name: 'url', type: 'string', required: true }
+ ]
+ },
+ {
+ type: 'OGCAPI_FEATURES',
+ name: 'OGC API Features',
+ description: 'Fetch features from OGC API',
+ parameters: [
+ { name: 'endpoint', type: 'string', required: true },
+ { name: 'collection', type: 'string', required: true },
+ { name: 'bbox', type: 'string', required: false }
+ ]
+ },
+ {
+ type: 'inline',
+ name: 'Inline Data',
+ description: 'Paste data directly',
+ parameters: [
+ { name: 'data', type: 'textarea', required: true }
+ ]
+ }
+ ];
+
+ // Mapping config structure for export/import
+ mappingConfigStructure = {
+ converter: {
+ name: 'converter-name',
+ schema: 'schema-name',
+ mimeType: 'mime-type',
+ parameters: [
+ { name: 'param1', value: 'value1' },
+ { name: 'param2', value: 'value2' }
+ ]
+ },
+ dataSource: {
+ type: 'file|http|inline',
+ parameters: [
+ { name: 'param1', value: 'value1' },
+ { name: 'param2', value: 'value2' }
+ ]
+ },
+ propertyMapping: {
+ nameProperty: 'property-name',
+ identifierProperty: 'id-property',
+ validStartDateProperty: 'start-date-property',
+ validEndDateProperty: 'end-date-property',
+ keepAttributes: true,
+ keepMissingOrNullValueAttributes: true,
+ attributes: [
+ { name: 'attr1', mappingName: 'mapped-attr1', type: 'string' },
+ { name: 'attr2', mappingName: 'mapped-attr2', type: 'number' }
+ ]
+ }
+ };
+
+ // Attribute mapping types
+ attributeMapping_attributeTypes = [
+ { apiName: 'string', displayName: 'String', description: 'Text data' },
+ { apiName: 'number', displayName: 'Number', description: 'Numeric data' },
+ { apiName: 'boolean', displayName: 'Boolean', description: 'True/False data' },
+ { apiName: 'date', displayName: 'Date', description: 'Date data' },
+ { apiName: 'geometry', displayName: 'Geometry', description: 'Spatial data' }
+ ];
+
+ // Get available converters
+ getAvailableConverters(): any[] {
+ return this.availableConverters;
+ }
+
+ // Get available datasource types
+ getAvailableDatasourceTypes(): any[] {
+ return this.availableDatasourceTypes;
+ }
+
+ // Filter converters by resource type
+ filterConverters(resourceType: string): (converter: any) => boolean {
+ return (converter: any) => {
+ // Add filtering logic based on resource type
+ return true; // For now, return all converters
+ };
+ }
+
+ // Fetch resources from importer
+ async fetchResourcesFromImporter(): Promise {
+ // This would typically fetch from the importer service
+ // For now, we'll use the static data
+ console.log('Fetching importer resources...');
+ }
+
+ // Update georesource method
+ async updateGeoresource(
+ converterDefinition: any,
+ datasourceTypeDefinition: any,
+ propertyMappingDefinition: any,
+ georesourceId: string,
+ putBody: any,
+ isDryRun: boolean = false
+ ): Promise {
+ try {
+ const url = `/api/georesources/${georesourceId}/features`;
+ const method = isDryRun ? 'POST' : 'PUT';
+ const dryRunParam = isDryRun ? '?dryRun=true' : '';
+
+ const requestBody = {
+ converterDefinition,
+ datasourceTypeDefinition,
+ propertyMappingDefinition,
+ ...putBody
+ };
+
+ // For now, return a mock response
+ // In real implementation, this would make an HTTP request
+ return {
+ success: true,
+ georesourceId,
+ isDryRun,
+ message: isDryRun ? 'Dry run completed successfully' : 'Georesource updated successfully',
+ importedFeatures: isDryRun ? [] : [{ id: 'mock-feature-1', name: 'Mock Feature' }]
+ };
+ } catch (error) {
+ console.error('Error updating georesource:', error);
+ throw error;
+ }
+ }
+
+ // Build put body for georesources
+ buildPutBody_georesources(scopeProperties: any): any {
+ return {
+ geoJsonString: "",
+ periodOfValidity: scopeProperties.periodOfValidity || {},
+ isPartialUpdate: scopeProperties.isPartialUpdate || false
+ };
+ }
+
+ // Check if importer response contains errors
+ importerResponseContainsErrors(response: any): boolean {
+ return !response || !response.success || response.errors || response.errors?.length > 0;
+ }
+
+ // Get ID from importer response
+ getIdFromImporterResponse(response: any): string {
+ return response?.georesourceId || 'unknown';
+ }
+
+ // Get imported features from importer response
+ getImportedFeaturesFromImporterResponse(response: any): any[] {
+ return response?.importedFeatures || [];
+ }
+
+ // Get errors from importer response
+ getErrorsFromImporterResponse(response: any): any[] {
+ return response?.errors || [];
+ }
+
+ // Build converter definition
+ buildConverterDefinition(
+ converter: any,
+ parameterPrefix: string,
+ schema: string,
+ mimeType: string,
+ formValues?: { [key: string]: string }
+ ): any {
+ if (!converter) return null;
+
+ const parameters: any[] = [];
+
+ // Collect parameters from declared converter parameters (if any)
+ const declaredParams: string[] = Array.isArray(converter.parameters)
+ ? converter.parameters.map((p: any) => p.name)
+ : [];
+
+ if (Array.isArray(converter.parameters) && converter.parameters.length > 0) {
+ converter.parameters.forEach((param: any) => {
+ const key = param.name;
+ const fromForm = formValues ? formValues[key] : undefined;
+ const element = document.getElementById(parameterPrefix + key) as HTMLInputElement;
+ const value = fromForm !== undefined ? fromForm : (element ? element.value : undefined);
+ if (value !== undefined && value !== null && value !== '') {
+ parameters.push({ name: key, value });
+ }
+ });
+ }
+
+ // Also merge any additional formValues not declared on converter (e.g., CRS)
+ if (formValues) {
+ Object.keys(formValues).forEach(key => {
+ if (!declaredParams.includes(key)) {
+ const value = formValues[key];
+ if (value !== undefined && value !== null && value !== '') {
+ parameters.push({ name: key, value });
+ }
+ }
+ });
+ }
+
+ return {
+ name: converter.name,
+ schema: schema,
+ mimeType: mimeType,
+ parameters: parameters
+ };
+ }
+
+ // Build datasource type definition
+ async buildDatasourceTypeDefinition(
+ datasourceType: any,
+ parameterPrefix: string,
+ inputElementId: string
+ ): Promise {
+ if (!datasourceType) return null;
+
+ const parameters: any[] = [];
+
+ // FILE datasource: upload the file to importer and pass returned NAME like legacy flow
+ if (datasourceType.type === 'FILE') {
+ const fileInput = document.getElementById(inputElementId) as HTMLInputElement;
+ const file = fileInput?.files?.[0];
+ if (!file) {
+ return null;
+ }
+
+ let uploadedName: string;
+ try {
+ uploadedName = await this.uploadNewFile(file, file.name);
+ } catch (error) {
+ console.error('Error while uploading file to importer.', error);
+ throw error;
+ }
+
+ parameters.push({
+ name: 'NAME',
+ value: uploadedName
+ });
+ } else {
+ // Non-FILE datasource: collect parameters from DOM, handle bbox specially when present
+ if (datasourceType.parameters && datasourceType.parameters.length > 0) {
+ for (const param of datasourceType.parameters) {
+ if (param.name === 'bbox') {
+ const bboxTypeEl = document.getElementById(parameterPrefix + 'bboxType') as HTMLInputElement;
+ const bboxType = bboxTypeEl?.value;
+ if (bboxType) {
+ parameters.push({ name: 'bboxType', value: bboxType });
+ }
+
+ let bboxValue: string | undefined;
+ if (bboxType === 'ref') {
+ const bboxRefEl = document.getElementById(parameterPrefix + 'bboxRef') as HTMLInputElement;
+ bboxValue = bboxRefEl?.value;
+ } else if (bboxType === 'literal') {
+ const minx = (document.getElementById(parameterPrefix + 'bbox_minx') as HTMLInputElement)?.value;
+ const miny = (document.getElementById(parameterPrefix + 'bbox_miny') as HTMLInputElement)?.value;
+ const maxx = (document.getElementById(parameterPrefix + 'bbox_maxx') as HTMLInputElement)?.value;
+ const maxy = (document.getElementById(parameterPrefix + 'bbox_maxy') as HTMLInputElement)?.value;
+ bboxValue = `${minx},${miny},${maxx},${maxy}`;
+ }
+
+ parameters.push({ name: 'bbox', value: bboxValue || '' });
+ } else {
+ const el = document.getElementById(parameterPrefix + param.name) as HTMLInputElement;
+ const value = el?.value ?? '';
+ parameters.push({ name: param.name, value });
+ }
+ }
+ }
+ }
+
+ return {
+ type: datasourceType.type,
+ parameters
+ };
+ }
+
+ /**
+ * Upload a new file to importer service (legacy-compatible)
+ */
+ async uploadNewFile(fileData: File, fileName: string): Promise {
+ 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 as string || '')
+ .catch(error => {
+ console.error('Error uploading file to importer service.', error);
+ throw error;
+ });
+ }
+
+ // Build property mapping for spatial resource
+ buildPropertyMapping_spatialResource(
+ nameProperty: string,
+ identifierProperty: string,
+ validStartDateProperty: string,
+ validEndDateProperty: string,
+ additionalProperties: any,
+ keepAttributes: boolean,
+ keepMissingValues: boolean,
+ attributeMappings: any[]
+ ): any {
+ return {
+ nameProperty: nameProperty,
+ identifierProperty: identifierProperty,
+ validStartDateProperty: validStartDateProperty,
+ validEndDateProperty: validEndDateProperty,
+ additionalProperties: additionalProperties,
+ keepAttributes: keepAttributes,
+ keepMissingOrNullValueAttributes: keepMissingValues,
+ attributes: attributeMappings.map(mapping => ({
+ name: mapping.sourceName,
+ mappingName: mapping.destinationName,
+ type: mapping.dataType?.apiName || 'string'
+ }))
+ };
+ }
+
+ /**
+ * Parse CSV file content
+ */
+ parseCsvFile(fileContent: string): any[] {
+ try {
+ const lines = fileContent.split('\n');
+ const headers = lines[0].split(',').map((header: string) => header.trim());
+ const data: any[] = [];
+
+ for (let i = 1; i < lines.length; i++) {
+ if (lines[i].trim()) {
+ const values = lines[i].split(',').map((value: string) => value.trim());
+ const row: any = {};
+
+ headers.forEach((header: string, index: number) => {
+ row[header] = values[index] || '';
+ });
+
+ data.push(row);
+ }
+ }
+
+ return data;
+ } catch (error) {
+ console.error('Error parsing CSV file:', error);
+ return [];
+ }
+ }
+
+ /**
+ * Parse JSON file content
+ */
+ parseJsonFile(fileContent: string): any {
+ try {
+ return JSON.parse(fileContent);
+ } catch (error) {
+ console.error('Error parsing JSON file:', error);
+ return null;
+ }
+ }
+
+ /**
+ * Validate file format
+ */
+ validateFileFormat(file: File, allowedTypes: string[]): boolean {
+ return allowedTypes.includes(file.type) ||
+ allowedTypes.some(type => file.name.endsWith(type));
+ }
+
+ /**
+ * Get file extension
+ */
+ getFileExtension(fileName: string): string {
+ return fileName.split('.').pop()?.toLowerCase() || '';
+ }
+
+ /**
+ * Convert file to base64
+ */
+ fileToBase64(file: File): Promise {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.readAsDataURL(file);
+ reader.onload = () => {
+ const result = reader.result as string;
+ resolve(result.split(',')[1]); // Remove data:application/...;base64, prefix
+ };
+ reader.onerror = error => reject(error);
+ });
+ }
+
+ /**
+ * Download file from URL
+ */
+ async downloadFileFromUrl(url: string): Promise {
+ try {
+ const response = await this.http.get(url, { responseType: 'blob' }).toPromise();
+ return response as Blob;
+ } catch (error) {
+ console.error('Error downloading file from URL:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Validate georesource data structure
+ */
+ validateGeoresourceData(data: any): { isValid: boolean; errors: string[] } {
+ const errors: string[] = [];
+
+ if (!data.georesourceId) {
+ errors.push('Missing georesourceId');
+ }
+
+ if (!data.datasetName) {
+ errors.push('Missing datasetName');
+ }
+
+ if (!data.metadata) {
+ errors.push('Missing metadata');
+ } else {
+ if (!data.metadata.description) {
+ errors.push('Missing metadata.description');
+ }
+ if (!data.metadata.datasource) {
+ errors.push('Missing metadata.datasource');
+ }
+ if (!data.metadata.contact) {
+ errors.push('Missing metadata.contact');
+ }
+ }
+
+ return {
+ isValid: errors.length === 0,
+ errors
+ };
+ }
+
+ /**
+ * Transform data to georesource format
+ */
+ transformToGeoresourceFormat(rawData: any): any {
+ return {
+ georesourceId: rawData.georesourceId || rawData.id,
+ datasetName: rawData.datasetName || rawData.name,
+ isPOI: rawData.isPOI || false,
+ isLOI: rawData.isLOI || false,
+ isAOI: rawData.isAOI || false,
+ poiSymbolColor: rawData.poiSymbolColor || '#000000',
+ poiSymbolBootstrap3Name: rawData.poiSymbolBootstrap3Name || 'default',
+ poiMarkerColor: rawData.poiMarkerColor || '#000000',
+ loiColor: rawData.loiColor || '#000000',
+ loiWidth: rawData.loiWidth || 2,
+ loiDashArrayString: rawData.loiDashArrayString || '5,5',
+ aoiColor: rawData.aoiColor || '#000000',
+ metadata: {
+ description: rawData.description || rawData.metadata?.description || '',
+ datasource: rawData.datasource || rawData.metadata?.datasource || '',
+ contact: rawData.contact || rawData.metadata?.contact || ''
+ },
+ availablePeriodsOfValidity: rawData.availablePeriodsOfValidity || [],
+ topicReference: rawData.topicReference || null,
+ permissions: rawData.permissions || [],
+ isPublic: rawData.isPublic || false,
+ ownerId: rawData.ownerId || ''
+ };
+ }
+
+ /**
+ * Generate sample georesource template
+ */
+ generateSampleTemplate(): any {
+ return {
+ georesourceId: 'sample-id',
+ datasetName: 'Sample Dataset',
+ isPOI: true,
+ isLOI: false,
+ isAOI: false,
+ poiSymbolColor: '#FF0000',
+ poiSymbolBootstrap3Name: 'map-marker',
+ poiMarkerColor: '#FF0000',
+ metadata: {
+ description: 'Sample description',
+ datasource: 'Sample datasource',
+ contact: 'sample@example.com'
+ },
+ availablePeriodsOfValidity: [
+ {
+ startDate: '2024-01-01',
+ endDate: '2024-12-31'
+ }
+ ],
+ topicReference: null,
+ permissions: [],
+ isPublic: true,
+ ownerId: 'sample-owner'
+ };
+ }
+
+ // Methods for georesource registration
+ async registerNewGeoresource(
+ converterDefinition: any,
+ datasourceTypeDefinition: any,
+ propertyMappingDefinition: any,
+ postBody: any,
+ isDryRun: boolean = false
+ ): Promise {
+ const payload = {
+ converter: converterDefinition,
+ dataSource: datasourceTypeDefinition,
+ propertyMapping: propertyMappingDefinition,
+ georesourcePostBody: postBody,
+ dryRun: isDryRun
+ };
+ return this.http.post(`${this.targetUrlToImporterService}georesources`, payload, {
+ headers: { 'Content-Type': 'application/json' }
+ }).toPromise();
+ }
+}
diff --git a/app/services/adminGeoresourceUnit/kommonitor-multi-step-form-helper.service.ts b/app/services/adminGeoresourceUnit/kommonitor-multi-step-form-helper.service.ts
new file mode 100644
index 000000000..91c119b6e
--- /dev/null
+++ b/app/services/adminGeoresourceUnit/kommonitor-multi-step-form-helper.service.ts
@@ -0,0 +1,146 @@
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class KommonitorMultiStepFormHelperService {
+
+ constructor() { }
+
+ /**
+ * Register click handlers for multi-step forms
+ * @param formId Optional form identifier
+ */
+ registerClickHandler(formId?: string): void {
+ // This method can be extended to handle specific form interactions
+ // For now, it's a placeholder that maintains compatibility with the existing code
+ console.log('Multi-step form click handler registered', formId ? `for form: ${formId}` : '');
+ }
+
+ /**
+ * Initialize multi-step form with default settings
+ * @param totalSteps Total number of steps in the form
+ * @returns Initial form configuration
+ */
+ initializeForm(totalSteps: number = 2): any {
+ return {
+ currentStep: 1,
+ totalSteps: totalSteps,
+ steps: Array.from({ length: totalSteps }, (_, i) => i + 1)
+ };
+ }
+
+ /**
+ * Validate if a step can be accessed
+ * @param currentStep Current step number
+ * @param targetStep Target step number
+ * @param validationRules Optional validation rules for step transitions
+ * @returns Whether the step transition is valid
+ */
+ canAccessStep(currentStep: number, targetStep: number, validationRules?: any): boolean {
+ if (targetStep < 1 || targetStep > this.getTotalSteps()) {
+ return false;
+ }
+
+ // Add custom validation logic here if needed
+ if (validationRules && validationRules[targetStep]) {
+ return validationRules[targetStep]();
+ }
+
+ return true;
+ }
+
+ /**
+ * Get total number of steps
+ * @returns Total steps count
+ */
+ getTotalSteps(): number {
+ return 2; // Default for most forms
+ }
+
+ /**
+ * Check if form is on the last step
+ * @param currentStep Current step number
+ * @returns Whether current step is the last step
+ */
+ isLastStep(currentStep: number): boolean {
+ return currentStep === this.getTotalSteps();
+ }
+
+ /**
+ * Check if form is on the first step
+ * @param currentStep Current step number
+ * @returns Whether current step is the first step
+ */
+ isFirstStep(currentStep: number): boolean {
+ return currentStep === 1;
+ }
+
+ /**
+ * Get step progress percentage
+ * @param currentStep Current step number
+ * @returns Progress percentage (0-100)
+ */
+ getStepProgress(currentStep: number): number {
+ return (currentStep / this.getTotalSteps()) * 100;
+ }
+
+ /**
+ * Validate form data for a specific step
+ * @param stepData Form data for the step
+ * @param stepNumber Step number to validate
+ * @returns Validation result object
+ */
+ validateStep(stepData: any, stepNumber: number): { isValid: boolean; errors: string[] } {
+ const errors: string[] = [];
+
+ // Add step-specific validation logic here
+ switch (stepNumber) {
+ case 1:
+ // Validate step 1 data
+ if (!stepData || Object.keys(stepData).length === 0) {
+ errors.push('Step 1 data is required');
+ }
+ break;
+ case 2:
+ // Validate step 2 data
+ if (!stepData || Object.keys(stepData).length === 0) {
+ errors.push('Step 2 data is required');
+ }
+ break;
+ default:
+ errors.push(`Unknown step ${stepNumber}`);
+ }
+
+ return {
+ isValid: errors.length === 0,
+ errors: errors
+ };
+ }
+
+ /**
+ * Reset form to initial state
+ * @param formData Form data object to reset
+ * @returns Reset form data
+ */
+ resetForm(formData: any): any {
+ // Reset form data to initial state
+ if (formData) {
+ Object.keys(formData).forEach(key => {
+ if (Array.isArray(formData[key])) {
+ formData[key] = [];
+ } else if (typeof formData[key] === 'boolean') {
+ formData[key] = false;
+ } else if (typeof formData[key] === 'string') {
+ formData[key] = '';
+ } else if (typeof formData[key] === 'number') {
+ formData[key] = 0;
+ } else {
+ formData[key] = null;
+ }
+ });
+ }
+
+ return formData;
+ }
+}
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..5e211ab4a
--- /dev/null
+++ b/app/services/adminIndicatorUnit/kommonitor-cache-helper.service.ts
@@ -0,0 +1,316 @@
+import { Injectable } from '@angular/core';
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { Observable, BehaviorSubject, firstValueFrom } from 'rxjs';
+import { AuthService } from 'services/auth-service/auth.service';
+
+// Interfaces for type safety
+export interface DatabaseModificationInfo {
+ lastModification: string;
+ spatialUnits: string;
+ georesources: string;
+ indicators: string;
+ topics: string;
+ processScripts: string;
+ accessControl: string;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class KommonitorIndicatorCacheHelperService {
+ // Private subjects for reactive updates
+ private lastModificationSubject = new BehaviorSubject(null);
+ private loadingSubject = new BehaviorSubject(false);
+ private errorSubject = new BehaviorSubject(null);
+
+ // Public observables
+ public lastModification$ = this.lastModificationSubject.asObservable();
+ public loading$ = this.loadingSubject.asObservable();
+ public error$ = this.errorSubject.asObservable();
+
+ // Environment configuration
+ private readonly env: any;
+ private readonly baseUrl: string;
+ private readonly localStoragePrefix: string;
+
+ // Database modification info (like original AngularJS service)
+ private lastDatabaseModificationInfo: DatabaseModificationInfo | null = null;
+
+ // Endpoints (like original AngularJS service)
+ private readonly indicatorsPublicEndpoint = "/public/indicators";
+ private readonly indicatorsProtectedEndpoint = "/indicators";
+ private indicatorsEndpoint = this.indicatorsProtectedEndpoint;
+
+ private readonly topicsPublicEndpoint = "/public/topics";
+ private readonly topicsProtectedEndpoint = "/topics";
+ private topicsEndpoint = this.topicsProtectedEndpoint;
+
+ // Local storage keys (like original AngularJS service)
+ private readonly localStorageKey_indicators: string;
+ private readonly localStorageKey_topics: string;
+
+ constructor(
+ private http: HttpClient,
+ private authService: AuthService
+ ) {
+ // Get environment configuration
+ this.env = (window as any).__env;
+ this.baseUrl = this.getBaseApiUrl();
+ this.localStoragePrefix = this.env?.localStoragePrefix || 'kommonitor';
+ this.localStorageKey_indicators = this.localStoragePrefix + "_lastModification_indicators";
+ this.localStorageKey_topics = this.localStoragePrefix + "_lastModification_topics";
+
+ // Initialize like original AngularJS service
+ this.init();
+ }
+
+ /**
+ * Initialize the service (like original AngularJS service)
+ */
+ private async init(): Promise {
+ this.checkAuthentication();
+ await this.fetchLastDatabaseModificationObject();
+ }
+
+ /**
+ * Check authentication and set appropriate endpoints (like original AngularJS service)
+ */
+ private checkAuthentication(): void {
+ if (this.authService.Auth && this.authService.Auth.keycloak && this.authService.Auth.keycloak.authenticated) {
+ this.indicatorsEndpoint = this.indicatorsProtectedEndpoint;
+ } else {
+ this.indicatorsEndpoint = this.indicatorsPublicEndpoint;
+ }
+ }
+
+ /**
+ * Fetch last database modification object (like original AngularJS service)
+ */
+ private async fetchLastDatabaseModificationObject(): Promise {
+ try {
+ const url = `${this.baseUrl}/public/database/last-modification`;
+ const response = await firstValueFrom(this.http.get(url));
+ console.log("fetchLastDatabaseModificationObject", response);
+ this.lastDatabaseModificationInfo = response;
+ this.lastModificationSubject.next(response);
+ } catch (error) {
+ // Error fetching last modification info
+ }
+ }
+
+ /**
+ * Fetches topics metadata with caching (like original AngularJS service)
+ */
+ async fetchTopicsMetadata(keycloakRolesArray: string[]): Promise {
+ this.loadingSubject.next(true);
+ this.errorSubject.next(null);
+
+ console.log("Cache Helper - fetchTopicsMetadata called with roles:", keycloakRolesArray);
+
+ try {
+ // Check authentication
+ this.checkAuthentication();
+ console.log("Cache Helper - topicsEndpoint:", this.topicsEndpoint);
+ // Use the same logic as original AngularJS service
+ return await this.fetchResource_fromCacheOrServer(
+ this.localStorageKey_topics,
+ this.topicsEndpoint,
+ "topics",
+ keycloakRolesArray
+ );
+ } catch (error) {
+ this.errorSubject.next('Error fetching topics metadata');
+ this.loadingSubject.next(false);
+ throw error;
+ }
+ }
+
+ /**
+ * Fetches indicators metadata with caching (like original AngularJS service)
+ */
+ async fetchIndicatorsMetadata(keycloakRolesArray: string[], filter?: any): Promise {
+ this.loadingSubject.next(true);
+ this.errorSubject.next(null);
+
+ console.log("Cache Helper - fetchIndicatorsMetadata called with roles:", keycloakRolesArray);
+ console.log("Cache Helper - filter:", filter);
+
+ try {
+ // Check authentication
+ this.checkAuthentication();
+ console.log("Cache Helper - indicatorsEndpoint:", this.indicatorsEndpoint);
+ // Use the same logic as original AngularJS service
+ if (filter) {
+ const filterBody = {
+ topicIds: filter.indicatorTopics,
+ ids: filter.indicators
+ };
+ return await this.fetchResource_fromCacheOrServer(
+ this.localStorageKey_indicators,
+ this.indicatorsEndpoint,
+ "indicators",
+ keycloakRolesArray,
+ filterBody
+ );
+ } else {
+ return await this.fetchResource_fromCacheOrServer(
+ this.localStorageKey_indicators,
+ this.indicatorsEndpoint,
+ "indicators",
+ keycloakRolesArray
+ );
+ }
+ } catch (error) {
+ this.errorSubject.next('Error fetching indicators metadata');
+ this.loadingSubject.next(false);
+ throw error;
+ }
+ }
+
+ /**
+ * Fetches single indicator metadata (like original AngularJS service)
+ */
+ async fetchSingleIndicatorMetadata(indicatorId: string, keycloakRolesArray: string[]): Promise {
+ try {
+ const url = `${this.baseUrl}${this.indicatorsEndpoint}/${indicatorId}`;
+ const headers = this.getAuthHeaders();
+ const response = await firstValueFrom(this.http.get(url, { headers }));
+
+ // Refresh the full indicators cache in the background (like original AngularJS service)
+ this.fetchIndicatorsMetadata(keycloakRolesArray);
+
+ return response;
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ /**
+ * Fetch resource from cache or server (like original AngularJS service)
+ */
+ private async fetchResource_fromCacheOrServer(
+ localStorageKey: string,
+ resourceEndpoint: string,
+ lastModificationResourceName: string,
+ keycloakRolesArray: string[],
+ filter?: any
+ ): Promise {
+ console.log("Cache Helper - fetchResource_fromCacheOrServer called with roles:", keycloakRolesArray);
+
+ // Fetch latest database modification info
+ await this.fetchLastDatabaseModificationObject();
+
+ // Build cache keys like original AngularJS service
+ let timestampKey = localStorageKey + "_timestamp";
+ let metadataKey = localStorageKey + "_metadata";
+
+ // Different cache keys based on roles (like original AngularJS service)
+ if (keycloakRolesArray && keycloakRolesArray.length > 0) {
+ if (keycloakRolesArray.includes(this.env?.keycloakKomMonitorAdminRoleName)) {
+ metadataKey += "_" + this.env?.keycloakKomMonitorAdminRoleName;
+ timestampKey += "_" + this.env?.keycloakKomMonitorAdminRoleName;
+ } else {
+ metadataKey += "_" + JSON.stringify(keycloakRolesArray);
+ timestampKey += "_" + JSON.stringify(keycloakRolesArray);
+ }
+ } else {
+ metadataKey += "_public";
+ timestampKey += "_public";
+ }
+
+ console.log("Cache Helper - Generated cache keys:");
+ console.log("Cache Helper - metadataKey:", metadataKey);
+ console.log("Cache Helper - timestampKey:", timestampKey);
+
+ // Check cache timestamp (like original AngularJS service)
+ let lastModTimestamp_fromCache_string = localStorage.getItem(timestampKey);
+
+ if (lastModTimestamp_fromCache_string && !filter) {
+ let lastModTimestamp_fromCache = JSON.parse(lastModTimestamp_fromCache_string);
+
+ if (lastModTimestamp_fromCache && this.lastDatabaseModificationInfo) {
+ let lastModTimestamp_fromServer = this.lastDatabaseModificationInfo[lastModificationResourceName];
+
+ if (lastModTimestamp_fromCache == lastModTimestamp_fromServer) {
+ let storageObject_string = localStorage.getItem(metadataKey);
+
+ if (storageObject_string) {
+ let storageObject = JSON.parse(storageObject_string);
+ this.loadingSubject.next(false);
+ return storageObject;
+ }
+ }
+ }
+ }
+
+ // Fetch from server (like original AngularJS service)
+ if (filter) {
+ const url = `${this.baseUrl}${resourceEndpoint}/filter`;
+ const headers = this.getAuthHeaders();
+ console.log("Cache Helper - Making POST request to:", url);
+ console.log("Cache Helper - Headers:", headers);
+ const response = await firstValueFrom(this.http.post(url, filter, { headers }));
+ console.log("Cache Helper - POST response:", response);
+ this.loadingSubject.next(false);
+ return response;
+ } else {
+ // Persist timestamp when fetching from server (like original AngularJS service)
+ if (this.lastDatabaseModificationInfo) {
+ localStorage.setItem(timestampKey, JSON.stringify(this.lastDatabaseModificationInfo[lastModificationResourceName]));
+ }
+
+ const url = `${this.baseUrl}${resourceEndpoint}`;
+ const headers = this.getAuthHeaders();
+ console.log("Cache Helper - Making GET request to:", url);
+ console.log("Cache Helper - Headers:", headers);
+ const response = await firstValueFrom(this.http.get(url, { headers }));
+ console.log("Cache Helper - GET response:", response);
+
+ // Cache the response (like original AngularJS service)
+ if (response && response.length > 0) {
+ localStorage.setItem(metadataKey, JSON.stringify(response));
+ }
+
+ this.loadingSubject.next(false);
+ return response;
+ }
+ }
+
+ /**
+ * Get base API URL (like original AngularJS service)
+ */
+ private getBaseApiUrl(): string {
+ const apiUrl = this.env?.apiUrl || '';
+ const basePath = this.env?.basePath || '';
+ return apiUrl + basePath;
+ }
+
+ /**
+ * Get authentication headers (like original AngularJS service)
+ */
+ private getAuthHeaders(): HttpHeaders {
+ const headers = new HttpHeaders({
+ 'Content-Type': 'application/json'
+ });
+
+ // Add authentication headers if needed
+ if (this.env?.enableKeycloakSecurity) {
+ const token = this.getKeycloakToken();
+ if (token) {
+ return headers.set('Authorization', `Bearer ${token}`);
+ }
+ }
+
+ return headers;
+ }
+
+ /**
+ * Get Keycloak token (like original AngularJS service)
+ */
+ private getKeycloakToken(): string | null {
+ if (this.authService.Auth && this.authService.Auth.keycloak && this.authService.Auth.keycloak.token) {
+ return this.authService.Auth.keycloak.token;
+ }
+ return null;
+ }
+}
\ 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..62d7298ac
--- /dev/null
+++ b/app/services/adminIndicatorUnit/kommonitor-data-exchange.service.ts
@@ -0,0 +1,927 @@
+import { Injectable, Inject } from '@angular/core';
+import { Observable, BehaviorSubject, Subject } from 'rxjs';
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { KommonitorIndicatorCacheHelperService } from './kommonitor-cache-helper.service';
+import { AuthService } from 'services/auth-service/auth.service';
+
+// Interfaces for type safety
+export interface IndicatorMetadata {
+ indicatorId: string;
+ indicatorName: string;
+ unit: string;
+ metadata: {
+ description: string;
+ databasis: string;
+ datasource: string;
+ contact: string;
+ updateInterval: string;
+ lastUpdate: string;
+ literature: string;
+ note: string;
+ sridEPSG: number;
+ };
+ processDescription: string;
+ applicableSpatialUnits: any[];
+ applicableDates: string[];
+ abbreviation: string;
+ isHeadlineIndicator: boolean;
+ indicatorType: any;
+ characteristicValue: string;
+ creationType: string;
+ tags: string;
+ topicReference: any;
+ permissions: string[];
+ isPublic: boolean;
+ ownerId: string;
+ precision: number;
+ userPermissions: string[];
+}
+
+export interface SpatialUnitMetadata {
+ spatialUnitId: string;
+ spatialUnitName: string;
+ spatialUnitLevel: string;
+ userPermissions: string[];
+}
+
+export interface GeoresourceMetadata {
+ georesourceId: string;
+ georesourceName: string;
+ datasetName?: string; // Optional for backward compatibility
+ userPermissions: string[];
+}
+
+export interface TopicMetadata {
+ topicId: string;
+ topicName: string;
+ subTopics: TopicMetadata[];
+}
+
+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 KommonitorIndicatorDataExchangeService {
+ // Private subjects for reactive updates
+ private indicatorsSubject = new BehaviorSubject([]);
+ private spatialUnitsSubject = new BehaviorSubject([]);
+ private georesourcesSubject = new BehaviorSubject([]);
+ private topicsSubject = new BehaviorSubject([]);
+ private loadingSubject = new BehaviorSubject(false);
+ private errorSubject = new BehaviorSubject(null);
+
+ // Public observables
+ public indicators$ = this.indicatorsSubject.asObservable();
+ public spatialUnits$ = this.spatialUnitsSubject.asObservable();
+ public georesources$ = this.georesourcesSubject.asObservable();
+ public topics$ = this.topicsSubject.asObservable();
+ public loading$ = this.loadingSubject.asObservable();
+ public error$ = this.errorSubject.asObservable();
+
+ // Cache for data with expiration
+ private indicatorsCache: {
+ data: IndicatorMetadata[];
+ timestamp: number;
+ expiresAt: number;
+ } | null = null;
+
+ private georesourcesCache: {
+ data: GeoresourceMetadata[];
+ timestamp: number;
+ expiresAt: number;
+ } | null = null;
+
+ // Cache duration in milliseconds (5 minutes)
+ private readonly CACHE_DURATION = 5 * 60 * 1000;
+
+ // Environment configuration
+ private readonly env: any;
+ private readonly baseUrl: string;
+
+ // Current user state
+ private _currentKeycloakLoginRoles: string[] = [];
+ private currentKeycloakUser: any = null;
+
+ // Maps for quick access
+ private availableIndicators_map = new Map();
+ private availableSpatialUnits_map = new Map();
+ private availableGeoresources_map = new Map();
+
+ // Cache for topic hierarchy
+ private topicHierarchyCache: any[] | null = null;
+ private topicHierarchyCacheTimestamp: number = 0;
+ private readonly TOPIC_HIERARCHY_CACHE_DURATION = 5000; // 5 seconds
+
+ constructor(
+ private http: HttpClient,
+ private cacheHelperService: KommonitorIndicatorCacheHelperService,
+ private authService: AuthService
+ ) {
+ // Get environment configuration
+ this.env = (window as any).__env;
+ this.baseUrl = this.getBaseApiUrl();
+ }
+
+ /**
+ * Get available indicators
+ */
+ get availableIndicators(): IndicatorMetadata[] {
+ return this.indicatorsSubject.value;
+ }
+
+ /**
+ * Get available spatial units
+ */
+ get availableSpatialUnits(): SpatialUnitMetadata[] {
+ return this.spatialUnitsSubject.value;
+ }
+
+ /**
+ * Fetches spatial units metadata
+ */
+ async fetchSpatialUnitsMetadata(keycloakRolesArray: string[]): Promise {
+ try {
+ const url = `${this.baseUrl}/spatial-units`;
+ const response = await this.http.get(url).toPromise();
+
+ if (response) {
+ this.spatialUnitsSubject.next(response);
+ // Update the map for quick access
+ this.availableSpatialUnits_map.clear();
+ response.forEach(spatialUnit => {
+ this.availableSpatialUnits_map.set(spatialUnit.spatialUnitId, spatialUnit);
+ });
+ }
+
+ return response || [];
+ } catch (error) {
+ console.error('Error fetching spatial units:', error);
+ return [];
+ }
+ }
+
+ /**
+ * Get available georesources
+ */
+ get availableGeoresources(): GeoresourceMetadata[] {
+ return this.georesourcesSubject.value;
+ }
+
+ /**
+ * Get available topics
+ */
+ get availableTopics(): TopicMetadata[] {
+ return this.topicsSubject.value;
+ }
+
+ /**
+ * Get topic indicator hierarchy for order view
+ */
+ get topicIndicatorHierarchy_forOrderView(): any[] {
+ const now = Date.now();
+
+ // Check if cache is still valid
+ if (this.topicHierarchyCache &&
+ (now - this.topicHierarchyCacheTimestamp) < this.TOPIC_HIERARCHY_CACHE_DURATION) {
+ return this.topicHierarchyCache;
+ }
+
+ // Rebuild cache
+ this.topicHierarchyCache = this.buildTopicIndicatorHierarchy();
+ this.topicHierarchyCacheTimestamp = now;
+
+ return this.topicHierarchyCache;
+ }
+
+ /**
+ * Get access control
+ */
+ get accessControl(): AccessControlMetadata[] {
+ return this._accessControl || [];
+ }
+
+ /**
+ * Set access control
+ */
+ set accessControl(value: AccessControlMetadata[]) {
+ this._accessControl = value;
+ }
+
+ private _accessControl: AccessControlMetadata[] = [];
+
+ /**
+ * Get update interval options
+ */
+ get updateIntervalOptions(): any[] {
+ return [
+ { value: 'ARBITRARY', label: 'beliebig' },
+ { value: 'YEARLY', label: 'jährlich' },
+ { value: 'HALF_YEARLY', label: 'halbjährig' },
+ { value: 'MONTHLY', label: 'monatlich' },
+ { value: 'QUARTERLY', label: 'vierteljährlich' }
+ ];
+ }
+
+ /**
+ * Get indicator type options
+ */
+ get indicatorTypeOptions(): any[] {
+ return [
+ { value: 'headline', label: 'Leitindikator' },
+ { value: 'base', label: 'Basisindikator' },
+ { value: 'computed', label: 'Berechneter Indikator' }
+ ];
+ }
+
+ /**
+ * Get indicator unit options
+ */
+ get indicatorUnitOptions(): any[] {
+ return [
+ { value: 'percent', label: 'Prozent' },
+ { value: 'number', label: 'Anzahl' },
+ { value: 'ratio', label: 'Verhältnis' },
+ { value: 'custom', label: 'Benutzerdefiniert' }
+ ];
+ }
+
+ /**
+ * Get indicator creation type options
+ */
+ get indicatorCreationTypeOptions(): any[] {
+ return [
+ { value: 'manual', label: 'Manuell' },
+ { value: 'automatic', label: 'Automatisch' },
+ { value: 'import', label: 'Import' }
+ ];
+ }
+
+ /**
+ * Get enable Keycloak security flag
+ */
+ get enableKeycloakSecurity(): boolean {
+ return this.env?.enableKeycloakSecurity || false;
+ }
+
+ /**
+ * Get current Keycloak login roles
+ */
+ get currentKeycloakLoginRoles(): string[] {
+ return this._currentKeycloakLoginRoles;
+ }
+
+ /**
+ * Get current KomMonitor login role IDs
+ */
+ getCurrentKomMonitorLoginRoleIds(): string[] {
+ return this.currentKeycloakLoginRoles;
+ }
+
+ /**
+ * Get base URL to KomMonitor Data API
+ */
+ get baseUrlToKomMonitorDataAPI(): string {
+ return this.baseUrl;
+ }
+
+ /**
+ * Get base URL to KomMonitor Data API for spatial resources
+ */
+ getBaseUrlToKomMonitorDataAPI_spatialResource(): string {
+ return this.getBaseApiUrl();
+ }
+
+ /**
+ * Get access control by ID
+ */
+ getAccessControlById(ownerId: string): any {
+ return this.accessControl.find((item: any) => item.organizationalUnitId === ownerId);
+ }
+
+ /**
+ * Fetch access control metadata
+ */
+ async fetchAccessControlMetadata(): Promise {
+ try {
+ const url = `${this.getBaseApiUrl()}/organizationalUnits`;
+
+ const headers = this.getAuthHeaders();
+ const response = await this.http.get(url, { headers }).toPromise();
+
+ if (response && Array.isArray(response)) {
+ this.accessControl = response;
+ return response;
+ } else {
+ return [];
+ }
+ } catch (error) {
+ console.error('Error fetching access control metadata:', error);
+ this.handleError(error);
+ return [];
+ }
+ }
+
+ /**
+ * Fetches topics metadata
+ */
+ async fetchTopicsMetadata(keycloakRolesArray: string[]): Promise {
+ this.loadingSubject.next(true);
+ this.errorSubject.next(null);
+
+ // Set the current roles for permission checking
+ this.setCurrentKeycloakLoginRoles(keycloakRolesArray);
+ try {
+ // Use the cache helper service to fetch topics
+ const topics = await this.cacheHelperService.fetchTopicsMetadata(keycloakRolesArray);
+
+ if (!topics || !Array.isArray(topics)) {
+ this.topicsSubject.next([]);
+ this.loadingSubject.next(false);
+ return [];
+ }
+
+ this.topicsSubject.next(topics);
+
+ // Invalidate topic hierarchy cache since topics changed
+ this.invalidateTopicHierarchyCache();
+
+ this.loadingSubject.next(false);
+
+ return topics;
+ } catch (error) {
+ this.handleError(error);
+ this.loadingSubject.next(false);
+ throw error;
+ }
+ }
+
+ /**
+ * Fetches indicators metadata
+ */
+ async fetchIndicatorsMetadata(keycloakRolesArray: string[]): Promise {
+ try {
+ this.loadingSubject.next(true);
+ this.errorSubject.next(null);
+
+ // Set the current roles for permission checking
+ this.setCurrentKeycloakLoginRoles(keycloakRolesArray);
+
+ // Check cache first
+ if (this.indicatorsCache && Date.now() - this.indicatorsCache.timestamp < this.CACHE_DURATION) {
+ this.indicatorsSubject.next(this.indicatorsCache.data);
+ return this.indicatorsCache.data;
+ }
+
+ const url = `${this.getBaseApiUrl()}/indicators`;
+ const headers = this.getAuthHeaders();
+ const response = await this.http.get(url, { headers }).toPromise();
+
+ if (!response) {
+ throw new Error('No response from indicators API');
+ }
+
+ // Process and filter indicators
+ const modifiedIndicators = this.modifyIndicators(response);
+ const displayableIndicators = this.filterDisplayableIndicators(modifiedIndicators);
+
+ // Update cache
+ this.indicatorsCache = {
+ data: displayableIndicators,
+ timestamp: Date.now(),
+ expiresAt: Date.now() + this.CACHE_DURATION
+ };
+
+ // Update maps and subjects
+ this.availableIndicators_map.clear();
+ displayableIndicators.forEach(indicator => {
+ this.availableIndicators_map.set(indicator.indicatorId, indicator);
+ });
+
+ this.indicatorsSubject.next(displayableIndicators);
+ this.invalidateTopicHierarchyCache();
+
+ // Also fetch topics since they're needed for the topic hierarchy
+ try {
+ await this.fetchTopicsMetadata(keycloakRolesArray);
+ } catch (topicsError) {
+ // Don't fail the entire operation if topics fail to load
+ console.warn('Failed to load topics:', topicsError);
+ }
+
+ return displayableIndicators;
+
+ } catch (error) {
+ console.error('Error fetching indicators metadata:', error);
+ this.handleError(error);
+ throw error;
+ } finally {
+ this.loadingSubject.next(false);
+ }
+ }
+
+ async fetchGeoresourcesMetadata(keycloakRolesArray: string[]): Promise {
+ try {
+ this.loadingSubject.next(true);
+ this.errorSubject.next(null);
+
+ // Check cache first
+ if (this.georesourcesCache && Date.now() - this.georesourcesCache.timestamp < this.CACHE_DURATION) {
+ this.georesourcesSubject.next(this.georesourcesCache.data);
+ return this.georesourcesCache.data;
+ }
+
+ // Fetch from API
+ const url = `${this.getBaseApiUrl()}/georesources`;
+ const headers = this.getAuthHeaders();
+
+ const response = await this.http.get(url, { headers }).toPromise();
+
+ if (!response) {
+ throw new Error('No response from georesources API');
+ }
+
+ // Process georesources
+ const georesources: GeoresourceMetadata[] = response.map((item: any) => ({
+ georesourceId: item.georesourceId,
+ georesourceName: item.georesourceName || item.datasetName,
+ datasetName: item.datasetName,
+ userPermissions: item.userPermissions || []
+ }));
+
+ // Update cache
+ this.georesourcesCache = {
+ data: georesources,
+ timestamp: Date.now(),
+ expiresAt: Date.now() + this.CACHE_DURATION
+ };
+
+ // Update maps and subjects
+ this.availableGeoresources_map.clear();
+ georesources.forEach(georesource => {
+ this.availableGeoresources_map.set(georesource.georesourceId, georesource);
+ });
+
+ this.georesourcesSubject.next(georesources);
+
+ return georesources;
+
+ } catch (error) {
+ console.error('Error fetching georesources metadata:', error);
+ this.handleError(error);
+ throw error;
+ } finally {
+ this.loadingSubject.next(false);
+ }
+ }
+
+ /**
+ * Adds a single indicator metadata
+ */
+ addSingleIndicatorMetadata(indicatorMetadata: IndicatorMetadata): void {
+ const modifiedIndicator = this.modifySingleIndicator(indicatorMetadata);
+ const currentIndicators = this.indicatorsSubject.value;
+ const updatedIndicators = [modifiedIndicator, ...currentIndicators];
+
+ this.availableIndicators_map.set(indicatorMetadata.indicatorId, indicatorMetadata);
+ this.indicatorsSubject.next(updatedIndicators);
+
+ // Invalidate topic hierarchy cache since indicators changed
+ this.invalidateTopicHierarchyCache();
+ }
+
+ /**
+ * Replaces a single indicator metadata
+ */
+ replaceSingleIndicatorMetadata(indicatorMetadata: IndicatorMetadata): void {
+ const currentIndicators = this.indicatorsSubject.value;
+ const modifiedIndicator = this.modifySingleIndicator(indicatorMetadata);
+
+ const updatedIndicators = currentIndicators.map(indicator =>
+ indicator.indicatorId === indicatorMetadata.indicatorId ? modifiedIndicator : indicator
+ );
+
+ this.availableIndicators_map.set(indicatorMetadata.indicatorId, indicatorMetadata);
+ this.indicatorsSubject.next(updatedIndicators);
+
+ // Invalidate topic hierarchy cache since indicators changed
+ this.invalidateTopicHierarchyCache();
+ }
+
+ /**
+ * Deletes a single indicator metadata
+ */
+ deleteSingleIndicatorMetadata(indicatorId: string): void {
+ const currentIndicators = this.indicatorsSubject.value;
+ const updatedIndicators = currentIndicators.filter(
+ indicator => indicator.indicatorId !== indicatorId
+ );
+
+ this.availableIndicators_map.delete(indicatorId);
+ this.indicatorsSubject.next(updatedIndicators);
+
+ // Invalidate topic hierarchy cache since indicators changed
+ this.invalidateTopicHierarchyCache();
+ }
+
+ /**
+ * Gets indicator metadata by ID
+ */
+ getIndicatorMetadataById(indicatorId: string): IndicatorMetadata | undefined {
+ return this.availableIndicators_map.get(indicatorId);
+ }
+
+ /**
+ * Gets georesource metadata by ID
+ */
+ getGeoresourceMetadataById(georesourceId: string): GeoresourceMetadata | undefined {
+ return this.availableGeoresources_map.get(georesourceId);
+ }
+
+ /**
+ * Gets topic hierarchy for topic ID
+ */
+ getTopicHierarchyForTopicId(topicId: string): any {
+ // Implementation for topic hierarchy lookup
+ return null;
+ }
+
+ /**
+ * Gets spatial unit metadata by ID
+ */
+ getSpatialUnitMetadataById(spatialUnitId: string): SpatialUnitMetadata | undefined {
+ return this.availableSpatialUnits_map.get(spatialUnitId);
+ }
+
+ /**
+ * Checks if the current user has create permissions
+ */
+ checkCreatePermission(): boolean {
+ if (this.checkAdminPermission()) {
+ return true;
+ }
+
+ for (const role of this._currentKeycloakLoginRoles) {
+ const roleNameParts = role.split(".");
+ const permissionLevel = roleNameParts[roleNameParts.length - 1];
+ if (permissionLevel === "client-resources-creator" || permissionLevel === "unit-resources-creator") {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks if the current user has editor permissions
+ */
+ checkEditorPermission(): boolean {
+ if (this.checkAdminPermission()) {
+ return true;
+ }
+
+ for (const role of this._currentKeycloakLoginRoles) {
+ const roleNameParts = role.split(".");
+ const permissionLevel = roleNameParts[roleNameParts.length - 1];
+ if (permissionLevel === "client-resources-creator" || permissionLevel === "unit-resources-creator") {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks if the current user has delete permissions
+ */
+ checkDeletePermission(): boolean {
+ if (this.checkAdminPermission()) {
+ return true;
+ }
+
+ for (const role of this._currentKeycloakLoginRoles) {
+ const roleNameParts = role.split(".");
+ const permissionLevel = roleNameParts[roleNameParts.length - 1];
+ if (permissionLevel === "client-resources-creator" || permissionLevel === "unit-resources-creator") {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Sets the current Keycloak login roles
+ */
+ setCurrentKeycloakLoginRoles(roles: string[]): void {
+ this._currentKeycloakLoginRoles = roles;
+ }
+
+ /**
+ * Display map application error
+ */
+ displayMapApplicationError(error: any): void {
+ let errorMessage = '';
+
+ if (error.data) {
+ errorMessage = this.syntaxHighlightJSON(error.data);
+ } else if (error.message) {
+ errorMessage = this.syntaxHighlightJSON(error.message);
+ } else {
+ errorMessage = this.syntaxHighlightJSON(error);
+ }
+
+ this.errorSubject.next(errorMessage);
+
+ // Show error alert in UI
+ setTimeout(() => {
+ const errorAlert = document.querySelector('.mapApplicationErrorAlert') as HTMLElement;
+ if (errorAlert) {
+ errorAlert.style.display = 'block';
+ }
+ }, 1000);
+ }
+
+ /**
+ * Get all allowed roles string
+ */
+ getAllowedRolesString(permissions: any): string {
+ if (!permissions || !Array.isArray(permissions)) return '';
+
+ const roleMap: { [key: string]: string } = {
+ 'viewer': 'Betrachter',
+ 'editor': 'Bearbeiter',
+ 'creator': 'Ersteller'
+ };
+
+ return permissions.map((permission: string) => roleMap[permission] || permission).join(', ');
+ }
+
+ /**
+ * Get role title
+ */
+ getRoleTitle(roleId: string): string {
+ if (!roleId) return '';
+
+ const roleMap: { [key: string]: string } = {
+ 'admin': 'Administrator',
+ 'user': 'Benutzer',
+ 'guest': 'Gast'
+ };
+
+ return roleMap[roleId] || roleId;
+ }
+
+ /**
+ * Get indicator string from indicator type
+ */
+ getIndicatorStringFromIndicatorType(indicatorType: any): string {
+ if (!indicatorType) return '';
+
+ const typeMap: { [key: string]: string } = {
+ 'headline': 'Leitindikator',
+ 'base': 'Basisindikator',
+ 'computed': 'Berechneter Indikator'
+ };
+
+ return typeMap[indicatorType] || indicatorType;
+ }
+
+ /**
+ * Get topic hierarchy display string
+ */
+ getTopicHierarchyDisplayString(topicReference: any): string {
+ if (!topicReference) return '';
+
+ let hierarchy = '';
+ if (topicReference.mainTopic) {
+ hierarchy += topicReference.mainTopic;
+ }
+ if (topicReference.subTopic) {
+ hierarchy += ' > ' + topicReference.subTopic;
+ }
+ if (topicReference.subsubTopic) {
+ hierarchy += ' > ' + topicReference.subsubTopic;
+ }
+ if (topicReference.subsubsubTopic) {
+ hierarchy += ' > ' + topicReference.subsubsubTopic;
+ }
+
+ return hierarchy;
+ }
+
+ /**
+ * Syntax highlight JSON
+ */
+ syntaxHighlightJSON(json: any): string {
+ if (typeof json === 'string') {
+ try {
+ json = JSON.parse(json);
+ } catch (e) {
+ return json;
+ }
+ }
+
+ return JSON.stringify(json, null, 2)
+ .replace(/&/g, '&')
+ .replace(//g, '>');
+ }
+
+ /**
+ * Private helper methods
+ */
+ private getBaseApiUrl(): string {
+ // Use the same pattern as the original AngularJS service
+ const apiUrl = this.env?.apiUrl || '';
+ const basePath = this.env?.basePath || '';
+ const baseUrl = apiUrl + basePath;
+
+ return baseUrl || 'http://localhost:8080/api';
+ }
+
+ private getAuthHeaders(): HttpHeaders {
+ const headers = new HttpHeaders({
+ 'Content-Type': 'application/json'
+ });
+
+ // Add authentication headers if needed
+ if (this.env?.enableKeycloakSecurity) {
+ // Add Keycloak token if available
+ const token = this.getKeycloakToken();
+ if (token) {
+ return headers.set('Authorization', `Bearer ${token}`);
+ }
+ }
+
+ return headers;
+ }
+
+ private getKeycloakToken(): string | null {
+ // Get token from AuthService (like other Angular components)
+ if (this.authService?.Auth && this.authService.Auth.keycloak && this.authService.Auth.keycloak.token) {
+ return this.authService.Auth.keycloak.token;
+ }
+ return null;
+ }
+
+ private modifyIndicators(indicators: IndicatorMetadata[]): IndicatorMetadata[] {
+ const decimalDefault = this.env?.numberOfDecimals || 2;
+
+ // First, modify precision values
+ const modifiedIndicators = indicators.map(indicator => {
+ if (indicator.precision === null || indicator.precision === undefined) {
+ indicator.precision = decimalDefault;
+ (indicator as any).defaultPrecision = true;
+ } else {
+ (indicator as any).defaultPrecision = false;
+ }
+ return indicator;
+ });
+
+ // Then apply the same filtering logic as the original AngularJS service
+ return this.filterDisplayableIndicators(modifiedIndicators);
+ }
+
+ private filterDisplayableIndicators(indicators: IndicatorMetadata[]): IndicatorMetadata[] {
+ const arrayOfNameSubstringsForHidingIndicators = this.env?.arrayOfNameSubstringsForHidingIndicators || [];
+
+ const filteredIndicators = indicators.filter(indicator => {
+ // Check if indicator has applicable dates
+ if (!indicator.applicableDates || indicator.applicableDates.length === 0) {
+ return false;
+ }
+
+ // Check if indicator has applicable spatial units
+ if (!indicator.applicableSpatialUnits || indicator.applicableSpatialUnits.length === 0) {
+ return false;
+ }
+
+ // Check if indicator name contains hidden substrings
+ const isIndicatorThatShallNotBeDisplayed = arrayOfNameSubstringsForHidingIndicators.some(
+ substring => String(indicator.indicatorName).includes(substring)
+ );
+
+ if (isIndicatorThatShallNotBeDisplayed) {
+ return false;
+ }
+
+ return true;
+ });
+
+ return filteredIndicators;
+ }
+
+ private modifySingleIndicator(indicator: IndicatorMetadata): IndicatorMetadata {
+ const modified = this.modifyIndicators([indicator]);
+ return modified[0];
+ }
+
+ private buildTopicIndicatorHierarchy(): any[] {
+ // Filter topics that are for indicators
+ const indicatorTopics = this.availableTopics.filter(topic => (topic as any).topicResource === "indicator");
+
+ const topicsMap = this.buildTopicsMap_indicators(indicatorTopics);
+
+ // Get filtered indicators
+ const filteredIndicators = this.availableIndicators;
+
+ // Map indicators to their topics
+ for (const indicatorMetadata of filteredIndicators) {
+ if (topicsMap.has(indicatorMetadata.topicReference)) {
+ const indicatorArray = topicsMap.get(indicatorMetadata.topicReference);
+ if (indicatorArray) {
+ indicatorArray.push(indicatorMetadata);
+ topicsMap.set(indicatorMetadata.topicReference, indicatorArray);
+ }
+ }
+ }
+
+ const result = this.addIndicatorDataToTopicHierarchy(indicatorTopics, topicsMap);
+ return result;
+ }
+
+ private buildTopicsMap_indicators(indicatorTopics: TopicMetadata[]): Map {
+ const topicsMap = new Map();
+
+ for (const topic of indicatorTopics) {
+ topicsMap.set(topic.topicId, []);
+ if (topic.subTopics.length > 0) {
+ this.addSubTopicsToMap_indicators(topic.subTopics, topicsMap);
+ }
+ }
+
+ return topicsMap;
+ }
+
+ private addSubTopicsToMap_indicators(subTopicsArray: TopicMetadata[], topicsMap: Map): Map {
+ for (const subTopic of subTopicsArray) {
+ topicsMap.set(subTopic.topicId, []);
+ if (subTopic.subTopics.length > 0) {
+ this.addSubTopicsToMap_indicators(subTopic.subTopics, topicsMap);
+ }
+ }
+
+ return topicsMap;
+ }
+
+ private addIndicatorDataToTopicHierarchy(topicsArray: TopicMetadata[], topicsMap: Map): any[] {
+ for (const topic of topicsArray) {
+ (topic as any).indicatorData = topicsMap.get(topic.topicId) || [];
+
+ // Sort by display order
+ (topic as any).indicatorData.sort((a: any, b: any) => (a.displayOrder > b.displayOrder) ? 1 : ((b.displayOrder > a.displayOrder) ? -1 : 0));
+
+ (topic as any).indicatorCount = (topic as any).indicatorData.length;
+
+ if (topic.subTopics.length > 0) {
+ this.addIndicatorDataToSubTopics(topic, topicsMap);
+ }
+ }
+
+ return topicsArray as any[];
+ }
+
+ private addIndicatorDataToSubTopics(topic: TopicMetadata, topicsMap: Map): TopicMetadata {
+ for (const subTopic of topic.subTopics) {
+ (subTopic as any).indicatorData = topicsMap.get(subTopic.topicId) || [];
+ (subTopic as any).indicatorData.sort((a: any, b: any) => (a.displayOrder > b.displayOrder) ? 1 : ((b.displayOrder > a.displayOrder) ? -1 : 0));
+ (subTopic as any).indicatorCount = (subTopic as any).indicatorData.length;
+
+ if (subTopic.subTopics.length > 0) {
+ this.addIndicatorDataToSubTopics(subTopic, topicsMap);
+ }
+ (topic as any).indicatorCount = (topic as any).indicatorCount + (subTopic as any).indicatorCount;
+ }
+
+ return topic;
+ }
+
+ public checkAdminPermission(): boolean {
+ return this._currentKeycloakLoginRoles.includes(this.env?.keycloakKomMonitorAdminRoleName);
+ }
+
+ private handleError(error: any): void {
+ this.errorSubject.next('An error occurred while fetching data');
+ }
+
+ /**
+ * Invalidates the topic hierarchy cache
+ */
+ private invalidateTopicHierarchyCache(): void {
+ this.topicHierarchyCache = null;
+ this.topicHierarchyCacheTimestamp = 0;
+ }
+}
\ 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..3619107b6
--- /dev/null
+++ b/app/services/adminIndicatorUnit/kommonitor-data-grid-helper.service.ts
@@ -0,0 +1,1160 @@
+import { Injectable, Inject } from '@angular/core';
+import { ColDef, GridOptions, ICellRendererParams, GridApi, ColumnApi, GridReadyEvent } from 'ag-grid-community';
+import { KommonitorIndicatorDataExchangeService } from './kommonitor-data-exchange.service';
+import { BroadcastService } from '../broadcast-service/broadcast.service';
+import { HttpClient } from '@angular/common/http';
+import { Subject } from 'rxjs';
+
+declare const MathJax: any;
+
+@Injectable({
+ providedIn: 'root'
+})
+export class KommonitorIndicatorDataGridHelperService {
+
+ // Grid references
+ private gridApi: GridApi | null = null;
+ private columnApi: ColumnApi | null = null;
+
+ // Observable for grid events
+ private gridReadySubject = new Subject();
+ public gridReady$ = this.gridReadySubject.asObservable();
+
+ // Resource type constants
+ readonly resourceType_georesource = "georesource";
+ readonly resourceType_spatialUnit = "spatialUnit";
+ readonly resourceType_indicator = "indicator";
+
+ // Timestamp properties for feature table updates
+ featureTable_spatialUnit_lastUpdate_timestamp_success: string | undefined = undefined;
+ featureTable_spatialUnit_lastUpdate_timestamp_failure: string | undefined = undefined;
+ featureTable_georesource_lastUpdate_timestamp_success: string | undefined = undefined;
+ featureTable_georesource_lastUpdate_timestamp_failure: string | undefined = undefined;
+ featureTable_indicator_lastUpdate_timestamp_success: string | undefined = undefined;
+ featureTable_indicator_lastUpdate_timestamp_failure: string | undefined = undefined;
+
+ constructor(
+ private kommonitorDataExchangeService: KommonitorIndicatorDataExchangeService,
+ private broadcastService: BroadcastService,
+ private http: HttpClient
+ ) {}
+
+ /**
+ * Builds data grid for indicators - returns column definitions and row data for AG Grid Angular
+ */
+ buildDataGrid_indicators(indicatorMetadataArray: any[]): GridOptions {
+ return this.buildDataGridOptions_indicators(indicatorMetadataArray);
+ }
+
+ /**
+ * Builds complete grid options for indicators (matches AngularJS implementation)
+ */
+ buildDataGridOptions_indicators(indicatorMetadataArray: any[]): GridOptions {
+ const columnDefs = this.buildDataGridColumnConfig_indicators(indicatorMetadataArray);
+ const rowData = this.buildDataGridRowData_indicators(indicatorMetadataArray);
+
+ return {
+ 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:
+ '' +
+ ' ' +
+ ' ' +
+ '
',
+ },
+ },
+ columnDefs: columnDefs,
+ rowData: rowData,
+ enableCellTextSelection: true,
+ ensureDomOrder: true,
+ pagination: true,
+ paginationPageSize: 10,
+ suppressColumnVirtualisation: true,
+ onGridReady: () => {
+ // Grid ready logic - equivalent to AngularJS onGridReady
+ },
+ onFirstDataRendered: () => {
+ // Header height setter logic - equivalent to AngularJS onFirstDataRendered
+ if (this.gridApi) {
+ this.headerHeightSetter({ api: this.gridApi } as any);
+ }
+ },
+ onColumnResized: () => {
+ // Header height setter logic - equivalent to AngularJS onColumnResized
+ if (this.gridApi) {
+ this.headerHeightSetter({ api: this.gridApi } as any);
+ }
+ },
+ onModelUpdated: () => {
+ // Register click handlers - equivalent to AngularJS onModelUpdated
+ this.registerClickHandler_indicators(indicatorMetadataArray);
+ },
+ onViewportChanged: () => {
+ // Register click handlers and MathJax typesetting - equivalent to AngularJS onViewportChanged
+ this.registerClickHandler_indicators(indicatorMetadataArray);
+
+ // MathJax typesetting (equivalent to AngularJS implementation)
+ setTimeout(() => {
+ if (typeof MathJax !== 'undefined') {
+ MathJax.typesetPromise().then(() => {
+ // MathJax rendering complete
+ });
+ }
+ }, 250);
+ },
+ };
+ }
+
+ /**
+ * Builds column configuration for indicators
+ */
+ buildDataGridColumnConfig_indicators(indicatorMetadataArray: any[]): ColDef[] {
+ const columnDefs: ColDef[] = [
+ {
+ headerName: 'Editierfunktionen',
+ pinned: 'left',
+ maxWidth: 150,
+ checkboxSelection: false,
+ filter: false,
+ sortable: false,
+ cellRenderer: (params: ICellRendererParams) => 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: ICellRendererParams) => {
+ return params.data?.metadata?.description || '';
+ },
+ filter: 'agTextColumnFilter',
+ filterValueGetter: (params: any) => {
+ return params.data?.metadata?.description || '';
+ }
+ },
+ {
+ headerName: 'Methodik',
+ minWidth: 400,
+ cellRenderer: (params: ICellRendererParams) => {
+ 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: ICellRendererParams) => {
+ if (!params.data?.applicableSpatialUnits) return '';
+
+ 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: ICellRendererParams) => {
+ if (!params.data?.applicableDates) return '';
+
+ 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: ICellRendererParams) => {
+ return this.getIndicatorStringFromIndicatorType(params.data?.indicatorType);
+ },
+ filter: 'agTextColumnFilter',
+ filterValueGetter: (params: any) => {
+ return this.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: ICellRendererParams) => {
+ return this.getTopicHierarchyDisplayString(params.data?.topicReference);
+ },
+ filter: 'agTextColumnFilter',
+ filterValueGetter: (params: any) => {
+ return this.getTopicHierarchyDisplayString(params.data?.topicReference);
+ }
+ },
+ {
+ headerName: 'Datenquelle',
+ minWidth: 400,
+ cellRenderer: (params: ICellRendererParams) => {
+ return params.data?.metadata?.datasource || '';
+ },
+ filter: 'agTextColumnFilter',
+ filterValueGetter: (params: any) => {
+ return params.data?.metadata?.datasource || '';
+ }
+ },
+ {
+ headerName: 'Datenhalter und Kontakt',
+ minWidth: 400,
+ cellRenderer: (params: ICellRendererParams) => {
+ return params.data?.metadata?.contact || '';
+ },
+ filter: 'agTextColumnFilter',
+ filterValueGetter: (params: any) => {
+ return params.data?.metadata?.contact || '';
+ }
+ },
+ {
+ headerName: 'Rollen',
+ minWidth: 400,
+ cellRenderer: (params: ICellRendererParams) => {
+ return this.getAllowedRolesString(params.data?.permissions);
+ },
+ filter: 'agTextColumnFilter',
+ filterValueGetter: (params: any) => {
+ return this.getAllowedRolesString(params.data?.permissions);
+ }
+ },
+ {
+ headerName: 'Öffentlich sichtbar',
+ minWidth: 400,
+ cellRenderer: (params: ICellRendererParams) => {
+ return params.data?.isPublic ? 'ja' : 'nein';
+ },
+ filter: 'agTextColumnFilter',
+ filterValueGetter: (params: any) => {
+ return params.data?.isPublic ? 'ja' : 'nein';
+ }
+ },
+ {
+ headerName: 'Eigentümer',
+ minWidth: 400,
+ cellRenderer: (params: ICellRendererParams) => {
+ return this.getRoleTitle(params.data?.ownerId);
+ },
+ filter: 'agTextColumnFilter',
+ filterValueGetter: (params: any) => {
+ return this.getRoleTitle(params.data?.ownerId);
+ }
+ },
+ {
+ headerName: 'Nachkommastellen',
+ minWidth: 200,
+ cellRenderer: (params: ICellRendererParams) => {
+ 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: ICellRendererParams): string => {
+ // 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.kommonitorDataExchangeService.enableKeycloakSecurity) {
+ let disabled = !(params.data.userPermissions && Array.isArray(params.data.userPermissions) && params.data.userPermissions.includes("creator"));
+ html += ' ';
+ }
+ html += '
';
+
+ return html;
+ };
+
+ /**
+ * Registers click handlers for indicator buttons (complete implementation)
+ * This method handles all indicator button click events
+ */
+ registerClickHandler_indicators(indicatorMetadataArray: any[]): void {
+ // Register edit metadata button click handlers
+ setTimeout(() => {
+ const editMetadataButtons = document.querySelectorAll('.indicatorEditMetadataBtn');
+ editMetadataButtons.forEach(button => {
+ button.addEventListener('click', (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const buttonId = (button as HTMLElement).id;
+ const indicatorId = buttonId.split('_')[3];
+
+ const indicatorMetadata = this.kommonitorDataExchangeService.getIndicatorMetadataById(indicatorId);
+
+ // Broadcast edit metadata event
+ this.broadcastService.broadcast('onEditIndicatorMetadata', indicatorMetadata);
+ });
+ });
+
+ // Register edit features button click handlers
+ const editFeaturesButtons = document.querySelectorAll('.indicatorEditFeaturesBtn');
+ editFeaturesButtons.forEach(button => {
+ button.addEventListener('click', (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const buttonId = (button as HTMLElement).id;
+ const indicatorId = buttonId.split('_')[3];
+
+ const indicatorMetadata = this.kommonitorDataExchangeService.getIndicatorMetadataById(indicatorId);
+
+ // Broadcast edit features event
+ this.broadcastService.broadcast('onEditIndicatorFeatures', indicatorMetadata);
+ });
+ });
+
+ // Register edit role based access button click handlers
+ const editRoleButtons = document.querySelectorAll('.indicatorEditRoleBasedAccessBtn');
+ editRoleButtons.forEach(button => {
+ button.addEventListener('click', (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const buttonId = (button as HTMLElement).id;
+ const indicatorId = buttonId.split('_')[3];
+
+ const indicatorMetadata = this.kommonitorDataExchangeService.getIndicatorMetadataById(indicatorId);
+
+ // Broadcast edit role based access event
+ this.broadcastService.broadcast('onEditIndicatorSpatialUnitRoles', indicatorMetadata);
+ });
+ });
+ }, 100);
+ }
+
+ /**
+ * Set grid API references
+ */
+ setGridApi(gridApi: GridApi, columnApi: ColumnApi): void {
+ this.gridApi = gridApi;
+ this.columnApi = columnApi;
+ }
+
+ /**
+ * Get grid API
+ */
+ getGridApi(): GridApi | null {
+ return this.gridApi;
+ }
+
+ /**
+ * Get column API
+ */
+ getColumnApi(): ColumnApi | null {
+ return this.columnApi;
+ }
+
+ /**
+ * Get current timestamp string (matches AngularJS implementation)
+ */
+ private getCurrentTimestampString(): string {
+ const date = new Date();
+ let hours = date.getHours();
+ if (hours < 10) {
+ hours = 0 + hours;
+ }
+ let minutes = date.getMinutes();
+ if (minutes < 10) {
+ minutes = 0 + minutes;
+ }
+ let seconds = date.getSeconds();
+ if (seconds < 10) {
+ seconds = 0 + seconds;
+ }
+ return `${hours}:${minutes}:${seconds}`;
+ }
+
+ /**
+ * Header height getter utility function
+ */
+ private headerHeightGetter(): number {
+ const columnHeaderTexts = Array.from(document.querySelectorAll('.ag-header-cell-text'));
+ const clientHeights = columnHeaderTexts.map(
+ (headerText: any) => headerText.clientHeight
+ );
+ const tallestHeaderTextHeight = Math.max(...clientHeights);
+
+ return tallestHeaderTextHeight;
+ }
+
+ /**
+ * Header height setter utility function
+ */
+ private headerHeightSetter(gridOptions: GridOptions): void {
+ if (gridOptions.api) {
+ const padding = 20;
+ const height = this.headerHeightGetter() + padding;
+ (gridOptions.api as any).setHeaderHeight(height);
+ }
+ }
+
+ /**
+ * Save grid store (filters, sorting, etc.)
+ */
+ private saveGridStore(gridOptions: GridOptions): void {
+ if (gridOptions.columnApi && gridOptions.api) {
+ (window as any).colState = (gridOptions.columnApi as any).getColumnState();
+ (window as any).filterState = (gridOptions.api as any).getFilterModel();
+ }
+ }
+
+ /**
+ * Restore grid store (filters, sorting, etc.)
+ */
+ private restoreGridStore(gridOptions: GridOptions): void {
+ if (gridOptions.columnApi && gridOptions.api) {
+ if ((window as any).colState) {
+ (gridOptions.columnApi as any).applyColumnState({
+ state: (window as any).colState,
+ applyOrder: true
+ });
+ }
+
+ if ((window as any).filterState) {
+ (gridOptions.api as any).setFilterModel((window as any).filterState);
+ }
+ }
+ }
+
+ /**
+ * Broadcast event for Angular component communication
+ */
+ // Removed broadcastEvent method as it's no longer needed with direct approach
+
+ /**
+ * Get indicator string from indicator type
+ */
+ private getIndicatorStringFromIndicatorType(indicatorType: any): string {
+ if (!indicatorType) return '';
+
+ // Map indicator types to display strings
+ const typeMap: { [key: string]: string } = {
+ 'headline': 'Leitindikator',
+ 'base': 'Basisindikator',
+ 'computed': 'Berechneter Indikator'
+ };
+
+ return typeMap[indicatorType] || indicatorType;
+ }
+
+ /**
+ * Get topic hierarchy display string
+ */
+ private getTopicHierarchyDisplayString(topicReference: any): string {
+ if (!topicReference) return '';
+
+ // Build topic hierarchy string
+ let hierarchy = '';
+ if (topicReference.mainTopic) {
+ hierarchy += topicReference.mainTopic;
+ }
+ if (topicReference.subTopic) {
+ hierarchy += ' > ' + topicReference.subTopic;
+ }
+ if (topicReference.subsubTopic) {
+ hierarchy += ' > ' + topicReference.subsubTopic;
+ }
+ if (topicReference.subsubsubTopic) {
+ hierarchy += ' > ' + topicReference.subsubsubTopic;
+ }
+
+ return hierarchy;
+ }
+
+ /**
+ * Get allowed roles string
+ */
+ private getAllowedRolesString(permissions: any): string {
+ if (!permissions || !Array.isArray(permissions)) return '';
+
+ const roleMap: { [key: string]: string } = {
+ 'viewer': 'Betrachter',
+ 'editor': 'Bearbeiter',
+ 'creator': 'Ersteller'
+ };
+
+ return permissions.map((permission: string) => roleMap[permission] || permission).join(', ');
+ }
+
+ /**
+ * Get role title
+ */
+ private getRoleTitle(roleId: string): string {
+ if (!roleId) return '';
+
+ // Map role IDs to display titles
+ const roleMap: { [key: string]: string } = {
+ 'admin': 'Administrator',
+ 'user': 'Benutzer',
+ 'guest': 'Gast'
+ };
+
+ return roleMap[roleId] || roleId;
+ }
+
+ /**
+ * Get indicator metadata by ID
+ */
+ private getIndicatorMetadataById(indicatorId: string): any {
+ const indicators = this.kommonitorDataExchangeService.availableIndicators;
+ return indicators.find((indicator: any) => indicator.indicatorId === indicatorId);
+ }
+
+ /**
+ * Builds data grid for indicator feature table
+ */
+ buildDataGrid_featureTable_indicatorResource(
+ tableId: string,
+ headers: string[],
+ features: any[] = [],
+ resourceId?: string,
+ resourceType?: string,
+ enableDelete: boolean = false
+ ): GridOptions {
+ return this.buildDataGridOptions_featureTable_indicatorResource(headers, features, resourceId, resourceType, enableDelete);
+ }
+
+ /**
+ * Builds complete grid options for indicator feature table (matches AngularJS implementation)
+ */
+ buildDataGridOptions_featureTable_indicatorResource(
+ headers: string[],
+ dataArray: any[],
+ datasetId?: string,
+ resourceType?: string,
+ deleteButtonEnabled: boolean = false
+ ): GridOptions {
+ const columnDefs = this.buildFeatureTableColumnConfig(headers, deleteButtonEnabled, resourceType);
+ const rowData = this.buildDataGridRowData_featureTable_indicatorResource(dataArray);
+
+ return {
+ defaultColDef: {
+ editable: true,
+ cellEditor: 'agLargeTextCellEditor',
+ onCellValueChanged: (newValueParams: any) => {
+ // Handle cell value changes for indicator data
+ this.handleIndicatorCellValueChanged(newValueParams, datasetId, resourceType);
+ },
+ 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:
+ '' +
+ ' ' +
+ ' ' +
+ '
',
+ },
+ },
+ columnDefs: columnDefs,
+ rowData: rowData,
+ undoRedoCellEditing: true,
+ undoRedoCellEditingLimit: 10,
+ enableCellChangeFlash: true,
+ suppressRowClickSelection: true,
+ enableCellTextSelection: true,
+ ensureDomOrder: true,
+ pagination: true,
+ paginationPageSize: 10,
+ suppressColumnVirtualisation: true,
+ onViewportChanged: () => {
+ this.registerFeatureTableClickHandlers(datasetId, resourceType, deleteButtonEnabled);
+ },
+ };
+ }
+
+ /**
+ * Builds row data for indicator feature table
+ */
+ private buildDataGridRowData_featureTable_indicatorResource(dataArray: any[]): any[] {
+ const result = dataArray.map(dataItem => {
+ // Remove arisenFrom property as this is currently never used (matches AngularJS)
+ delete dataItem.arisenFrom;
+
+ // Ensure required fields are present for delete functionality
+ if (dataItem && typeof dataItem === 'object') {
+ // Add any missing required properties
+ if (!dataItem.hasOwnProperty('kommonitorRecordId')) {
+ dataItem.kommonitorRecordId = dataItem.fid || dataItem.ID || dataItem.id;
+ }
+ // Ensure ID and fid fields are present
+ dataItem.ID = dataItem.ID || dataItem.id || dataItem.fid;
+ dataItem.fid = dataItem.fid || dataItem.ID || dataItem.id;
+ }
+
+ return dataItem;
+ });
+
+ return result;
+ }
+
+ /**
+ * Build feature table column configuration (matches AngularJS implementation)
+ */
+ private buildFeatureTableColumnConfig(headers: string[], enableDelete: boolean, resourceType?: string): ColDef[] {
+ const columnDefs: ColDef[] = [];
+
+ // Get environment variables with fallbacks
+ const featureIdProperty = (window as any).__env?.FEATURE_ID_PROPERTY_NAME || 'ID';
+ const featureNameProperty = (window as any).__env?.FEATURE_NAME_PROPERTY_NAME || 'NAME';
+ const validStartDateProperty = (window as any).__env?.VALID_START_DATE_PROPERTY_NAME || 'VALID_START_DATE';
+ const validEndDateProperty = (window as any).__env?.VALID_END_DATE_PROPERTY_NAME || 'VALID_END_DATE';
+
+ console.log('Using environment variables:', {
+ featureIdProperty,
+ featureNameProperty,
+ validStartDateProperty,
+ validEndDateProperty
+ });
+
+ // Add DB-Record-Id column (matches AngularJS implementation)
+ columnDefs.push({
+ headerName: 'DB-Record-Id',
+ field: 'fid',
+ pinned: 'left',
+ editable: false,
+ maxWidth: 125,
+ cellRenderer: (params: any) => {
+ let html = '';
+
+ if (enableDelete) {
+ html += ` `;
+ }
+
+ html += ' ';
+ html += params.data?.fid || '';
+
+ return html;
+ }
+ });
+
+ // Add Feature-Id column (matches AngularJS implementation)
+ columnDefs.push({
+ headerName: 'Feature-Id',
+ field: featureIdProperty,
+ pinned: 'left',
+ editable: false,
+ maxWidth: 125
+ });
+
+ // Add Name column (matches AngularJS implementation)
+ columnDefs.push({
+ headerName: 'Name',
+ field: featureNameProperty,
+ pinned: 'left',
+ minWidth: 200,
+ editable: false
+ });
+
+ // Add Lebenszeitbeginn column (matches AngularJS implementation)
+ columnDefs.push({
+ headerName: 'Lebenszeitbeginn',
+ field: validStartDateProperty,
+ minWidth: 125,
+ editable: false
+ });
+
+ // Add Lebenszeitende column (matches AngularJS implementation)
+ columnDefs.push({
+ headerName: 'Lebenszeitende',
+ field: validEndDateProperty,
+ minWidth: 125,
+ editable: false
+ });
+
+ // Add dynamic headers for indicator date columns (matches AngularJS implementation)
+ headers.forEach(header => {
+ columnDefs.push({
+ headerName: header,
+ field: header,
+ minWidth: 125,
+ editable: true,
+ cellEditor: 'agTextCellEditor'
+ });
+ });
+
+ return columnDefs;
+ }
+
+ /**
+ * Register feature table click handlers (matches AngularJS implementation)
+ */
+ registerFeatureTableClickHandlers(resourceId?: string, resourceType?: string, enableDelete?: boolean): void {
+ if (!enableDelete) return;
+
+ // Register delete button click handlers (matches AngularJS implementation)
+ setTimeout(() => {
+ const deleteButtons = document.querySelectorAll('.indicatorDeleteFeatureRecordBtn');
+ deleteButtons.forEach(button => {
+ button.addEventListener('click', (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ event.stopImmediatePropagation();
+
+ // Broadcast loading icon event
+ this.broadcastService.broadcast(`showLoadingIcon_${resourceType}`, {});
+
+ // Get the button ID and parse it
+ const buttonId = (button as HTMLElement).id;
+ this.handleIndicatorFeatureDelete(buttonId, resourceId, resourceType);
+ });
+ });
+ }, 100);
+ }
+
+ /**
+ * Handle indicator feature delete (matches AngularJS implementation)
+ */
+ private handleIndicatorFeatureDelete(featureId: string, resourceId?: string, resourceType?: string): void {
+ if (!resourceId || !resourceType) return;
+
+ // Parse the button ID to extract parameters (matches AngularJS implementation)
+ const buttonId = featureId; // featureId parameter contains the full button ID
+ const idArray = buttonId.split('__');
+
+ if (idArray.length < 7) return;
+
+ const datasetId = idArray[3];
+ const spatialUnitId = idArray[4];
+ const actualFeatureId = idArray[5];
+ const recordId = idArray[6];
+
+ // Build URL for the DELETE request
+ const url = `${this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI}/indicators/${datasetId}/${spatialUnitId}/singleFeature/${actualFeatureId}/singleFeatureRecord/${recordId}`;
+
+ // Send DELETE request
+ this.http.delete(url).subscribe({
+ next: (response: any) => {
+ // Broadcast delete event
+ this.broadcastService.broadcast(`onDeleteFeatureEntry_${resourceType}`, {});
+
+ // Update timestamp for successful deletion
+ this.featureTable_indicator_lastUpdate_timestamp_success = this.getCurrentTimestampString();
+ },
+ error: (error: any) => {
+ // Update timestamp for failure
+ this.featureTable_indicator_lastUpdate_timestamp_failure = this.getCurrentTimestampString();
+ }
+ });
+ }
+
+ /**
+ * Handle indicator cell value changed (matches AngularJS implementation)
+ */
+ private handleIndicatorCellValueChanged(newValueParams: any, resourceId?: string, resourceType?: string): void {
+ const { data, field, newValue, oldValue, column, node, api } = newValueParams;
+
+ if (newValue === oldValue) return;
+
+ // Take the modified data from newValueParams.data
+ let json = JSON.parse(JSON.stringify(data));
+
+ // Get environment variables with fallbacks
+ const featureIdProperty = (window as any).__env?.FEATURE_ID_PROPERTY_NAME || 'ID';
+ const featureNameProperty = (window as any).__env?.FEATURE_NAME_PROPERTY_NAME || 'NAME';
+ const validStartDateProperty = (window as any).__env?.VALID_START_DATE_PROPERTY_NAME || 'VALID_START_DATE';
+ const validEndDateProperty = (window as any).__env?.VALID_END_DATE_PROPERTY_NAME || 'VALID_END_DATE';
+
+ // Delete information - only ID, fid as datatable recordId and all timestamp attributes starting with prefix 'DATE_' shall remain for indicator record update
+ const allowedProperties = [featureIdProperty, 'fid'];
+ for (const key in json) {
+ if (Object.hasOwnProperty.call(json, key)) {
+ if (!key.includes('DATE_') && !allowedProperties.includes(key)) {
+ delete json[key];
+ }
+ }
+ }
+
+ // Remove specific properties
+ delete json[validStartDateProperty];
+ delete json[validEndDateProperty];
+ delete json[featureNameProperty];
+
+ // For indicators we should check if an empty/null/undefined value has been set by user and transmit it as null value
+ for (const key in json) {
+ if (Object.hasOwnProperty.call(json, key)) {
+ const element = json[key];
+ if (key.includes('DATE_')) {
+ if (element === '') {
+ json[key] = null;
+ }
+ }
+ }
+ }
+
+ // Build URL for the PUT request
+ const url = `${this.kommonitorDataExchangeService.baseUrlToKomMonitorDataAPI}/indicators/${resourceId}/${data.spatialUnitId}/singleFeature/${data.ID}/singleFeatureRecord/${data.fid}`;
+
+ // Send PUT request
+ this.http.put(url, json, {
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }).subscribe({
+ next: (response: any) => {
+ // On success mark grid cell with green background
+ column.colDef.cellStyle = (p: any) =>
+ p.rowIndex.toString() === node.id ? { 'background-color': '#9DC89F' } : '';
+
+ api.refreshCells({
+ force: true,
+ columns: [column.getId()],
+ rowNodes: [node]
+ });
+
+ // Update timestamp for successful edit
+ this.featureTable_indicator_lastUpdate_timestamp_success = this.getCurrentTimestampString();
+ },
+ error: (error: any) => {
+ // Reset cell value as an error occurred
+ data[column.colId] = oldValue;
+
+ // On failure mark grid cell with red background
+ column.colDef.cellStyle = (p: any) =>
+ p.rowIndex.toString() === node.id ? { 'background-color': '#E79595' } : '';
+
+ api.refreshCells({
+ force: true,
+ columns: [column.getId()],
+ rowNodes: [node]
+ });
+
+ // Update timestamp for failure
+ this.featureTable_indicator_lastUpdate_timestamp_failure = this.getCurrentTimestampString();
+ }
+ });
+ }
+
+ /**
+ * Build role management grid
+ */
+ buildRoleManagementGrid(tableDOMId: string, currentTableOptionsObject: any, accessControlMetadata: any[], selectedPermissionIds: string[], reducedRoleManagement: boolean = false): any {
+ const gridOptions: GridOptions = {
+ defaultColDef: {
+ sortable: true,
+ filter: true,
+ resizable: true
+ },
+ columnDefs: this.buildRoleManagementGridColumnConfig(reducedRoleManagement),
+ rowData: this.buildRoleManagementGridRowData(accessControlMetadata, selectedPermissionIds),
+ pagination: true,
+ paginationPageSize: 10
+ };
+
+ return gridOptions;
+ }
+
+ /**
+ * Build role management grid column configuration
+ */
+ private buildRoleManagementGridColumnConfig(reducedRoleManagement: boolean = false): ColDef[] {
+ const columnDefs: ColDef[] = [
+ { headerName: 'Organisationseinheit', field: 'organizationalUnitName', pinned: 'left', minWidth: 200 }
+ ];
+
+ if (!reducedRoleManagement) {
+ columnDefs.push(
+ {
+ headerName: 'Betrachter',
+ field: 'viewer',
+ maxWidth: 100,
+ cellRenderer: this.CheckboxRenderer_viewer
+ },
+ {
+ headerName: 'Bearbeiter',
+ field: 'editor',
+ maxWidth: 100,
+ cellRenderer: this.CheckboxRenderer_editor
+ },
+ {
+ headerName: 'Ersteller',
+ field: 'creator',
+ maxWidth: 100,
+ cellRenderer: this.CheckboxRenderer_creator
+ }
+ );
+ }
+
+ return columnDefs;
+ }
+
+ /**
+ * Build role management grid row data
+ */
+ private buildRoleManagementGridRowData(accessControlMetadata: any[], permissionIds: string[]): any[] {
+ return accessControlMetadata.map(item => ({
+ organizationalUnitId: item.organizationalUnitId,
+ organizationalUnitName: item.organizationalUnitName || item.name, // Handle both field names
+ viewer: permissionIds.includes(item.viewerPermissionId),
+ editor: permissionIds.includes(item.editorPermissionId),
+ creator: permissionIds.includes(item.creatorPermissionId),
+ datasetOwner: item.datasetOwner || false
+ }));
+ }
+
+ /**
+ * Get selected role IDs from role management grid
+ */
+ getSelectedRoleIds_roleManagementGrid(roleManagementTableOptions: any): string[] {
+ if (!roleManagementTableOptions || !roleManagementTableOptions.rowData) return [];
+
+ const selectedRoleIds: string[] = [];
+
+ roleManagementTableOptions.rowData.forEach((row: any) => {
+ if (row.viewer) {
+ selectedRoleIds.push(row.viewerPermissionId);
+ }
+ if (row.editor) {
+ selectedRoleIds.push(row.editorPermissionId);
+ }
+ if (row.creator) {
+ selectedRoleIds.push(row.creatorPermissionId);
+ }
+ });
+
+ return selectedRoleIds;
+ }
+
+ /**
+ * Checkbox renderer for viewer permissions
+ */
+ public CheckboxRenderer_viewer = class {
+ private params: any;
+ private eGui: HTMLInputElement | null = null;
+ private boundCheckedHandler: any;
+
+ init(params: any) {
+ this.params = params;
+ this.eGui = document.createElement('input');
+ this.eGui.type = 'checkbox';
+ this.eGui.checked = params.value;
+ this.eGui.disabled = params.data.datasetOwner;
+
+ this.boundCheckedHandler = this.checkedHandler.bind(this);
+ this.eGui.addEventListener('click', this.boundCheckedHandler);
+ }
+
+ checkedHandler(e: any) {
+ if (this.params.node) {
+ this.params.node.setDataValue('viewer', e.target.checked);
+ }
+ }
+
+ getGui() {
+ return this.eGui;
+ }
+
+ destroy() {
+ if (this.eGui) {
+ this.eGui.removeEventListener('click', this.boundCheckedHandler);
+ }
+ }
+ };
+
+ /**
+ * Checkbox renderer for editor permissions
+ */
+ public CheckboxRenderer_editor = class {
+ private params: any;
+ private eGui: HTMLInputElement | null = null;
+ private boundCheckedHandler: any;
+
+ init(params: any) {
+ this.params = params;
+ this.eGui = document.createElement('input');
+ this.eGui.type = 'checkbox';
+ this.eGui.checked = params.value;
+ this.eGui.disabled = params.data.datasetOwner;
+
+ this.boundCheckedHandler = this.checkedHandler.bind(this);
+ this.eGui.addEventListener('click', this.boundCheckedHandler);
+ }
+
+ checkedHandler(e: any) {
+ if (this.params.node) {
+ this.params.node.setDataValue('editor', e.target.checked);
+ }
+ }
+
+ getGui() {
+ return this.eGui;
+ }
+
+ destroy() {
+ if (this.eGui) {
+ this.eGui.removeEventListener('click', this.boundCheckedHandler);
+ }
+ }
+ };
+
+ public CheckboxRenderer_creator = class {
+ private params: any;
+ private eGui: HTMLInputElement | null = null;
+ private boundCheckedHandler: any;
+
+ init(params: any) {
+ this.params = params;
+ this.eGui = document.createElement('input');
+ this.eGui.type = 'checkbox';
+ this.eGui.checked = params.value;
+ this.eGui.disabled = params.data.datasetOwner;
+
+ this.boundCheckedHandler = this.checkedHandler.bind(this);
+ this.eGui.addEventListener('click', this.boundCheckedHandler);
+ }
+
+ checkedHandler(e: any) {
+ if (this.params.node) {
+ this.params.node.setDataValue('creator', e.target.checked);
+ }
+ }
+
+ getGui() {
+ return this.eGui;
+ }
+
+ destroy() {
+ if (this.eGui) {
+ this.eGui.removeEventListener('click', this.boundCheckedHandler);
+ }
+ }
+ };
+
+ /**
+ * Gets reference values from regional reference values management grid
+ */
+ getReferenceValues_regionalReferenceValuesManagementGrid(gridOptions: any): any[] {
+ // Implementation for reference values management
+ return [];
+ }
+
+ /**
+ * Builds reference values management grid
+ */
+ buildReferenceValuesManagementGrid(gridOptions: any): any {
+ // Implementation for reference values management grid
+ return gridOptions;
+ }
+}
\ No newline at end of file
diff --git a/app/services/adminIndicatorUnit/kommonitor-importer-helper.service.ts b/app/services/adminIndicatorUnit/kommonitor-importer-helper.service.ts
new file mode 100644
index 000000000..cab17c4da
--- /dev/null
+++ b/app/services/adminIndicatorUnit/kommonitor-importer-helper.service.ts
@@ -0,0 +1,480 @@
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+
+// Interfaces for 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 KommonitorIndicatorImporterHelperService {
+ private targetUrlToImporterService: string;
+ public availableConverters: Converter[] = [];
+ public availableDatasourceTypes: DatasourceType[] = [];
+
+ public readonly attributeMapping_attributeTypes: AttributeMappingType[] = [
+ { displayName: "String", apiName: "string" },
+ { displayName: "Integer", apiName: "integer" },
+ { displayName: "Double", apiName: "double" },
+ { displayName: "Boolean", apiName: "boolean" },
+ { displayName: "Date", apiName: "date" },
+ { displayName: "DateTime", apiName: "datetime" }
+ ];
+
+ public readonly mappingConfigStructure: MappingConfigStructure = {
+ converter: {
+ encoding: "UTF-8",
+ mimeType: "application/vnd.geo+json",
+ name: "GeoJSON",
+ parameters: [
+ { name: "CRS", value: "EPSG:4326" }
+ ],
+ schema: "http://schemas.opengis.net/gml/3.2.1/feature.xsd"
+ },
+ dataSource: {
+ parameters: [
+ { name: "url", value: "https://example.com/data.geojson" }
+ ],
+ type: "URL"
+ },
+ propertyMapping: {
+ arisenFromProperty: "arisenFrom",
+ attributes: [
+ { mappingName: "name", name: "NAME", type: "string" },
+ { mappingName: "id", name: "ID", type: "string" }
+ ],
+ identifierProperty: "ID",
+ keepAttributes: true,
+ nameProperty: "NAME",
+ validEndDateProperty: "validEndDate",
+ validStartDateProperty: "validStartDate"
+ },
+ periodOfValidity: {
+ startDate: "2023-01-01",
+ endDate: "2023-12-31"
+ }
+ };
+
+ public readonly mappingConfigStructure_indicator = {
+ converter: {
+ encoding: "UTF-8",
+ mimeType: "application/vnd.geo+json",
+ name: "GeoJSON",
+ parameters: [
+ { name: "CRS", value: "EPSG:4326" }
+ ],
+ schema: "http://schemas.opengis.net/gml/3.2.1/feature.xsd"
+ },
+ dataSource: {
+ parameters: [
+ { name: "url", value: "https://example.com/indicator-data.geojson" }
+ ],
+ type: "URL"
+ },
+ propertyMapping: {
+ spatialReferenceKeyProperty: "SPATIAL_UNIT_ID",
+ timeseriesMappings: [
+ { sourceProperty: "VALUE_2023", targetProperty: "indicator_value_2023", dataType: "double" }
+ ],
+ keepMissingOrNullValueIndicator: true
+ }
+ };
+
+ constructor(private http: HttpClient) {
+ this.targetUrlToImporterService = window.__env.targetUrlToImporterService || 'http://localhost:8080/importer/';
+ }
+
+ /**
+ * Fetch resources from importer service
+ */
+ async fetchResourcesFromImporter(): Promise {
+ try {
+ await Promise.all([
+ this.fetchConverters(),
+ this.fetchDatasourceTypes()
+ ]);
+ } catch (error) {
+ console.error('Error fetching resources from importer service:', error);
+ }
+ }
+
+ /**
+ * Filter converters by resource type
+ */
+ filterConverters(resourceType: string): (converter: Converter) => boolean {
+ return (converter: Converter) => {
+ if (resourceType === 'indicator') {
+ return converter.type === 'indicator' || converter.type === 'general';
+ }
+ return converter.type === resourceType || converter.type === 'general';
+ };
+ }
+
+ /**
+ * Fetch converters from importer service
+ */
+ async fetchConverters(): Promise {
+ try {
+ const response = await this.http.get(`${this.targetUrlToImporterService}converters`).toPromise();
+ this.availableConverters = response || [];
+ return this.availableConverters;
+ } catch (error) {
+ console.error('Error fetching converters:', error);
+ return [];
+ }
+ }
+
+ /**
+ * Fetch converter details
+ */
+ async fetchConverterDetails(converter: Converter): Promise {
+ try {
+ const response = await this.http.get(`${this.targetUrlToImporterService}converters/${converter.name}`).toPromise();
+ return response || converter;
+ } catch (error) {
+ console.error('Error fetching converter details:', error);
+ return converter;
+ }
+ }
+
+ /**
+ * Fetch datasource types from importer service
+ */
+ async fetchDatasourceTypes(): Promise {
+ try {
+ const response = await this.http.get(`${this.targetUrlToImporterService}datasource-types`).toPromise();
+ this.availableDatasourceTypes = response || [];
+ return this.availableDatasourceTypes;
+ } catch (error) {
+ console.error('Error fetching datasource types:', error);
+ return [];
+ }
+ }
+
+ /**
+ * Fetch datasource type details
+ */
+ async fetchDatasourceTypeDetails(datasourceType: DatasourceType): Promise {
+ try {
+ const response = await this.http.get(`${this.targetUrlToImporterService}datasource-types/${datasourceType.type}`).toPromise();
+ return response || datasourceType;
+ } catch (error) {
+ console.error('Error fetching datasource type details:', error);
+ return datasourceType;
+ }
+ }
+
+ /**
+ * Upload new file to importer service
+ */
+ async uploadNewFile(fileData: File, fileName: string): Promise {
+ try {
+ const formData = new FormData();
+ formData.append('file', fileData, fileName);
+
+ const response = await this.http.post<{ fileId: string }>(`${this.targetUrlToImporterService}files/upload`, formData).toPromise();
+ return response?.fileId || '';
+ } catch (error) {
+ console.error('Error uploading file:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Build converter definition
+ */
+ buildConverterDefinition(
+ selectedConverter: Converter,
+ converterParameterPrefix: string,
+ schema: string,
+ mimeType: string,
+ formValues?: { [key: string]: string }
+ ): ConverterDefinition | null {
+ if (!selectedConverter) return null;
+
+ const parameters: Array<{ name: string; value: string }> = [];
+
+ if (selectedConverter.parameters) {
+ for (const parameter of selectedConverter.parameters) {
+ const elementId = converterParameterPrefix + parameter.name;
+ const element = document.getElementById(elementId) as HTMLInputElement;
+
+ if (element) {
+ parameters.push({
+ name: parameter.name,
+ value: formValues?.[parameter.name] || element.value || ''
+ });
+ }
+ }
+ }
+
+ return {
+ encoding: "UTF-8",
+ mimeType: mimeType,
+ name: selectedConverter.name,
+ parameters: parameters,
+ schema: schema
+ };
+ }
+
+ /**
+ * Build datasource type definition
+ */
+ async buildDatasourceTypeDefinition(
+ selectedDatasourceType: DatasourceType,
+ datasourceTypeParameterPrefix: string,
+ datasourceFileInputId: string,
+ formValues?: { [key: string]: string }
+ ): Promise {
+ if (!selectedDatasourceType) return null;
+
+ const parameters: Array<{ name: string; value: string }> = [];
+
+ if (selectedDatasourceType.parameters) {
+ for (const parameter of selectedDatasourceType.parameters) {
+ const elementId = datasourceTypeParameterPrefix + parameter.name;
+ const element = document.getElementById(elementId) as HTMLInputElement;
+
+ if (element) {
+ parameters.push({
+ name: parameter.name,
+ value: formValues?.[parameter.name] || element.value || ''
+ });
+ }
+ }
+ }
+
+ // Handle file upload for FILE type
+ if (selectedDatasourceType.type === 'FILE') {
+ const fileInput = document.getElementById(datasourceFileInputId) as HTMLInputElement;
+ if (fileInput && fileInput.files && fileInput.files.length > 0) {
+ const file = fileInput.files[0];
+ const fileId = await this.uploadNewFile(file, file.name);
+ parameters.push({
+ name: 'fileId',
+ value: fileId
+ });
+ }
+ }
+
+ return {
+ parameters: parameters,
+ type: selectedDatasourceType.type
+ };
+ }
+
+ /**
+ * Build property mapping for indicator resource
+ */
+ buildPropertyMapping_indicatorResource(
+ spatialReferenceKeyProperty: string,
+ timeseriesMappings: any[],
+ keepMissingOrNullValueIndicator: boolean
+ ): any {
+ return {
+ spatialReferenceKeyProperty: spatialReferenceKeyProperty,
+ timeseriesMappings: timeseriesMappings,
+ keepMissingOrNullValueIndicator: keepMissingOrNullValueIndicator
+ };
+ }
+
+ /**
+ * 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/adminSpatialUnit/kommonitor-cache-helper.service.ts b/app/services/adminSpatialUnit/kommonitor-cache-helper.service.ts
new file mode 100644
index 000000000..44f56b51f
--- /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';
+
+
+
+ // 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;
+ }
+
+
+ }
+
+ /**
+ * 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);
+
+ }),
+ catchError(this.handleError)
+ );
+ }
+
+ /**
+ * Fetch spatial units metadata with caching
+ */
+ fetchSpatialUnitsMetadata(keycloakRolesArray: string[]): Observable {
+
+
+ // Check cache first
+ const cachedData = this.getCachedSpatialUnits(keycloakRolesArray);
+ if (cachedData) {
+
+ this.spatialUnitsSubject.next(cachedData);
+ return of(cachedData);
+ }
+
+ // Fetch 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);
+
+ }),
+ 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) {
+
+ return null;
+ }
+
+ const cachedData = localStorage.getItem(metadataKey);
+ if (!cachedData) {
+ return null;
+ }
+
+ try {
+ const parsedData = JSON.parse(cachedData);
+
+ return parsedData;
+ } catch (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));
+
+
+ }
+
+ /**
+ * 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);
+
+ }
+
+ /**
+ * 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));
+
+ }
+
+ /**
+ * 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}`;
+ }
+
+
+ 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..5d24c2e3d
--- /dev/null
+++ b/app/services/adminSpatialUnit/kommonitor-data-exchange.service.ts
@@ -0,0 +1,1171 @@
+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) {
+ 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..c4b52f079
--- /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 = '';
+ 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);
+
+ }
+ 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) {
+
+ 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
+
+ }
+
+ 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) {
+
+ 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 {
+
+ return;
+ }
+
+ // Make DELETE request
+ this.http.delete(url).subscribe({
+ next: (response: any) => {
+
+
+ // 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) => {
+
+
+ // 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) => {
+
+
+ // 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) => {
+
+
+ // 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/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 60c1376fa..263bec3a1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,15 +10,20 @@
"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",
"@angular/forms": "^16.1.3",
+ "@angular/localize": "^20.2.4",
"@angular/platform-browser": "^16.1.3",
"@angular/platform-browser-dynamic": "^16.1.3",
"@angular/router": "^16.1.3",
"@angular/upgrade": "^16.1.4",
+ "@fortawesome/angular-fontawesome": "^0.13.0",
"@fortawesome/fontawesome-free": "^6.1.1",
+ "@fortawesome/fontawesome-svg-core": "^1.2.36",
+ "@fortawesome/free-solid-svg-icons": "^6.7.2",
"@googlemaps/js-api-loader": "^1.16.8",
"@ng-bootstrap/ng-bootstrap": "^15.1.2",
"@ngx-translate/core": "^16.0.4",
@@ -26,6 +31,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",
@@ -80,6 +86,8 @@
"mathjax": "^3.2.2",
"ng2-ion-range-slider": "^2.0.0",
"ng2-nouislider": "^2.0.0",
+ "ngx-color": "^8.0.3",
+ "ngx-icon-picker": "^1.11.2",
"nouislider": "^15.8.1",
"papaparse": "^5.4.1",
"rangeslide.js": "^0.13.0",
@@ -90,7 +98,8 @@
"sortablejs": "^1.15.3",
"tableexport": "^5.2.0",
"toastr": "^2.1.4",
- "ui-select": "^0.19.8"
+ "ui-select": "^0.19.8",
+ "zone.js": "^0.15.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "^16.1.3",
@@ -139,6 +148,7 @@
"http-server": "^14.1.0",
"license-checker": "^25.0.1",
"release-it": "^15.10.1",
+ "typescript": "~5.0.4",
"webpack": "^5.61.0",
"webpack-cli": "^4.5.0"
}
@@ -580,6 +590,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",
@@ -652,6 +705,7 @@
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.2.12.tgz",
"integrity": "sha512-pWSrr152562ujh6lsFZR8NfNc5Ljj+zSTQO44DsuB0tZjwEpnRcjJEgzuhGXr+CoiBf+jTSPZKemtSktDk5aaA==",
+ "dev": true,
"dependencies": {
"@babel/core": "7.23.2",
"@jridgewell/sourcemap-codec": "^1.4.14",
@@ -679,6 +733,7 @@
"version": "7.23.2",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz",
"integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==",
+ "dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.22.13",
@@ -707,12 +762,14 @@
"node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true
},
"node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
"bin": {
"semver": "bin/semver.js"
}
@@ -721,6 +778,7 @@
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz",
"integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==",
+ "dev": true,
"dependencies": {
"@babel/parser": "^7.26.5",
"@babel/types": "^7.26.5",
@@ -736,6 +794,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
+ "dev": true,
"dependencies": {
"@babel/code-frame": "^7.25.9",
"@babel/parser": "^7.25.9",
@@ -749,6 +808,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
"bin": {
"jsesc": "bin/jsesc"
},
@@ -789,14 +849,15 @@
}
},
"node_modules/@angular/localize": {
- "version": "16.2.12",
- "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-16.2.12.tgz",
- "integrity": "sha512-sNIHDlZKENPQqx64qGF99g2sOCy9i9O4VOmjKD/FZbeE8O5qBbaQlkwOlFoQIt35/cnvtAtf7oQF6tqmiVtS2w==",
- "peer": true,
+ "version": "20.2.4",
+ "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-20.2.4.tgz",
+ "integrity": "sha512-8OimXwR/hzUHJdegLD4+Zhg1h3qaAVLwLLK3G6Ba4EU9W9HJCyqvxIXooXossLBp/toFKyjU/RxmH+dwy4ztCQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/core": "7.23.2",
- "fast-glob": "3.3.0",
- "yargs": "^17.2.1"
+ "@babel/core": "7.28.3",
+ "@types/babel__core": "7.20.5",
+ "tinyglobby": "^0.2.12",
+ "yargs": "^18.0.0"
},
"bin": {
"localize-extract": "tools/bundles/src/extract/cli.js",
@@ -804,114 +865,130 @@
"localize-translate": "tools/bundles/src/translate/cli.js"
},
"engines": {
- "node": "^16.14.0 || >=18.10.0"
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
- "@angular/compiler": "16.2.12",
- "@angular/compiler-cli": "16.2.12"
+ "@angular/compiler": "20.2.4",
+ "@angular/compiler-cli": "20.2.4"
}
},
- "node_modules/@angular/localize/node_modules/@babel/core": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz",
- "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==",
- "peer": true,
- "dependencies": {
- "@ampproject/remapping": "^2.2.0",
- "@babel/code-frame": "^7.22.13",
- "@babel/generator": "^7.23.0",
- "@babel/helper-compilation-targets": "^7.22.15",
- "@babel/helper-module-transforms": "^7.23.0",
- "@babel/helpers": "^7.23.2",
- "@babel/parser": "^7.23.0",
- "@babel/template": "^7.22.15",
- "@babel/traverse": "^7.23.2",
- "@babel/types": "^7.23.0",
- "convert-source-map": "^2.0.0",
- "debug": "^4.1.0",
- "gensync": "^1.0.0-beta.2",
- "json5": "^2.2.3",
- "semver": "^6.3.1"
+ "node_modules/@angular/localize/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
},
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@angular/localize/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "license": "MIT",
"engines": {
- "node": ">=6.9.0"
+ "node": ">=12"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/babel"
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/@angular/localize/node_modules/@babel/generator": {
- "version": "7.26.5",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz",
- "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==",
- "peer": true,
+ "node_modules/@angular/localize/node_modules/cliui": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
+ "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
+ "license": "ISC",
"dependencies": {
- "@babel/parser": "^7.26.5",
- "@babel/types": "^7.26.5",
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.25",
- "jsesc": "^3.0.2"
+ "string-width": "^7.2.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
},
"engines": {
- "node": ">=6.9.0"
+ "node": ">=20"
}
},
- "node_modules/@angular/localize/node_modules/@babel/template": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
- "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
- "peer": true,
+ "node_modules/@angular/localize/node_modules/emoji-regex": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz",
+ "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==",
+ "license": "MIT"
+ },
+ "node_modules/@angular/localize/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.25.9",
- "@babel/parser": "^7.25.9",
- "@babel/types": "^7.25.9"
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
},
"engines": {
- "node": ">=6.9.0"
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/@angular/localize/node_modules/convert-source-map": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
- "peer": true
+ "node_modules/@angular/localize/node_modules/strip-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
},
- "node_modules/@angular/localize/node_modules/fast-glob": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz",
- "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==",
- "peer": true,
+ "node_modules/@angular/localize/node_modules/wrap-ansi": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+ "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
+ "license": "MIT",
"dependencies": {
- "@nodelib/fs.stat": "^2.0.2",
- "@nodelib/fs.walk": "^1.2.3",
- "glob-parent": "^5.1.2",
- "merge2": "^1.3.0",
- "micromatch": "^4.0.4"
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
},
"engines": {
- "node": ">=8.6.0"
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
- "node_modules/@angular/localize/node_modules/jsesc": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
- "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
- "peer": true,
- "bin": {
- "jsesc": "bin/jsesc"
+ "node_modules/@angular/localize/node_modules/yargs": {
+ "version": "18.0.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
+ "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^9.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "string-width": "^7.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^22.0.0"
},
"engines": {
- "node": ">=6"
+ "node": "^20.19.0 || ^22.12.0 || >=23"
}
},
- "node_modules/@angular/localize/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "peer": true,
- "bin": {
- "semver": "bin/semver.js"
+ "node_modules/@angular/localize/node_modules/yargs-parser": {
+ "version": "22.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
+ "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/@angular/platform-browser": {
@@ -1053,41 +1130,44 @@
"dev": true
},
"node_modules/@babel/code-frame": {
- "version": "7.26.2",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
- "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-validator-identifier": "^7.25.9",
+ "@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
- "picocolors": "^1.0.0"
+ "picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/compat-data": {
- "version": "7.26.5",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz",
- "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz",
+ "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==",
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
- "version": "7.26.7",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.7.tgz",
- "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz",
+ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
+ "license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
- "@babel/code-frame": "^7.26.2",
- "@babel/generator": "^7.26.5",
- "@babel/helper-compilation-targets": "^7.26.5",
- "@babel/helper-module-transforms": "^7.26.0",
- "@babel/helpers": "^7.26.7",
- "@babel/parser": "^7.26.7",
- "@babel/template": "^7.25.9",
- "@babel/traverse": "^7.26.7",
- "@babel/types": "^7.26.7",
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.3",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.3",
+ "@babel/parser": "^7.28.3",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.3",
+ "@babel/types": "^7.28.2",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -1103,14 +1183,15 @@
}
},
"node_modules/@babel/core/node_modules/@babel/generator": {
- "version": "7.26.5",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz",
- "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
+ "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
+ "license": "MIT",
"dependencies": {
- "@babel/parser": "^7.26.5",
- "@babel/types": "^7.26.5",
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.25",
+ "@babel/parser": "^7.28.3",
+ "@babel/types": "^7.28.2",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
},
"engines": {
@@ -1118,13 +1199,14 @@
}
},
"node_modules/@babel/core/node_modules/@babel/template": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
- "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.25.9",
- "@babel/parser": "^7.25.9",
- "@babel/types": "^7.25.9"
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1139,6 +1221,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
},
@@ -1182,12 +1265,13 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
- "version": "7.26.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz",
- "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==",
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/compat-data": "^7.26.5",
- "@babel/helper-validator-option": "^7.25.9",
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
@@ -1312,6 +1396,15 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/helper-member-expression-to-functions": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz",
@@ -1326,25 +1419,27 @@
}
},
"node_modules/@babel/helper-module-imports": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
- "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "license": "MIT",
"dependencies": {
- "@babel/traverse": "^7.25.9",
- "@babel/types": "^7.25.9"
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz",
- "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-module-imports": "^7.25.9",
- "@babel/helper-validator-identifier": "^7.25.9",
- "@babel/traverse": "^7.25.9"
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
},
"engines": {
"node": ">=6.9.0"
@@ -1446,25 +1541,28 @@
}
},
"node_modules/@babel/helper-string-parser": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
- "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
- "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-option": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
- "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
@@ -1498,36 +1596,39 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.26.7",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz",
- "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
+ "license": "MIT",
"dependencies": {
- "@babel/template": "^7.25.9",
- "@babel/types": "^7.26.7"
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers/node_modules/@babel/template": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
- "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.25.9",
- "@babel/parser": "^7.25.9",
- "@babel/types": "^7.25.9"
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
- "version": "7.26.7",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
- "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
+ "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
+ "license": "MIT",
"dependencies": {
- "@babel/types": "^7.26.7"
+ "@babel/types": "^7.28.4"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -2819,31 +2920,33 @@
}
},
"node_modules/@babel/traverse": {
- "version": "7.26.7",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz",
- "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
+ "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.26.2",
- "@babel/generator": "^7.26.5",
- "@babel/parser": "^7.26.7",
- "@babel/template": "^7.25.9",
- "@babel/types": "^7.26.7",
- "debug": "^4.3.1",
- "globals": "^11.1.0"
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.3",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.4",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4",
+ "debug": "^4.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse/node_modules/@babel/generator": {
- "version": "7.26.5",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz",
- "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
+ "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
+ "license": "MIT",
"dependencies": {
- "@babel/parser": "^7.26.5",
- "@babel/types": "^7.26.5",
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.25",
+ "@babel/parser": "^7.28.3",
+ "@babel/types": "^7.28.2",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
},
"engines": {
@@ -2851,13 +2954,14 @@
}
},
"node_modules/@babel/traverse/node_modules/@babel/template": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
- "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.25.9",
- "@babel/parser": "^7.25.9",
- "@babel/types": "^7.25.9"
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -2867,6 +2971,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
},
@@ -2875,17 +2980,27 @@
}
},
"node_modules/@babel/types": {
- "version": "7.26.7",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz",
- "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
+ "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^7.25.9",
- "@babel/helper-validator-identifier": "^7.25.9"
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1"
},
"engines": {
"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",
@@ -3247,6 +3362,29 @@
"node": ">=12"
}
},
+ "node_modules/@fortawesome/angular-fontawesome": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.13.0.tgz",
+ "integrity": "sha512-gzSPRdveOXNO7NIiMgTyB46aiHG0i98KinnAEqHXi8qzraM/kCcHn/0y3f4MhemX6kftwsFli0IU8RyHmtXlSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.4.1"
+ },
+ "peerDependencies": {
+ "@angular/core": "^16.0.0",
+ "@fortawesome/fontawesome-svg-core": "~1.2.27 || ~1.3.0-beta2 || ^6.1.0"
+ }
+ },
+ "node_modules/@fortawesome/fontawesome-common-types": {
+ "version": "0.2.36",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
+ "integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/@fortawesome/fontawesome-free": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz",
@@ -3255,6 +3393,40 @@
"node": ">=6"
}
},
+ "node_modules/@fortawesome/fontawesome-svg-core": {
+ "version": "1.2.36",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz",
+ "integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "^0.2.36"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/free-solid-svg-icons": {
+ "version": "6.7.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
+ "integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
+ "license": "(CC-BY-4.0 AND MIT)",
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.7.2"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/free-solid-svg-icons/node_modules/@fortawesome/fontawesome-common-types": {
+ "version": "6.7.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
+ "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/@gar/promisify": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
@@ -3394,16 +3566,13 @@
}
},
"node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.8",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
- "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
"dependencies": {
- "@jridgewell/set-array": "^1.2.1",
- "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
@@ -3414,14 +3583,6 @@
"node": ">=6.0.0"
}
},
- "node_modules/@jridgewell/set-array": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
- "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
- "engines": {
- "node": ">=6.0.0"
- }
- },
"node_modules/@jridgewell/source-map": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
@@ -3438,9 +3599,10 @@
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
},
"node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.25",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
- "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "version": "0.3.30",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
+ "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
+ "license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@@ -3521,6 +3683,7 @@
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
@@ -3533,6 +3696,7 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
"engines": {
"node": ">= 8"
}
@@ -3541,6 +3705,7 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
@@ -6764,6 +6929,47 @@
"@types/angular": "*"
}
},
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
"node_modules/@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@@ -7507,6 +7713,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",
@@ -7744,6 +7964,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
@@ -9302,6 +9523,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
"engines": {
"node": ">=8"
},
@@ -9691,6 +9913,7 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
"dependencies": {
"fill-range": "^7.1.1"
},
@@ -10498,6 +10721,7 @@
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "dev": true,
"funding": [
{
"type": "individual",
@@ -10703,6 +10927,7 @@
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
@@ -10716,6 +10941,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
"engines": {
"node": ">=8"
}
@@ -10724,6 +10950,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -10784,6 +11011,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -10794,7 +11022,8 @@
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
},
"node_modules/color-support": {
"version": "1.1.3",
@@ -11134,7 +11363,8 @@
"node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
- "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+ "dev": true
},
"node_modules/cookie": {
"version": "0.7.1",
@@ -12644,7 +12874,8 @@
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
},
"node_modules/emojis-list": {
"version": "3.0.0",
@@ -13491,6 +13722,7 @@
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
"integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==",
+ "dev": true,
"dependencies": {
"reusify": "^1.0.4"
}
@@ -13516,6 +13748,23 @@
"pend": "~1.2.0"
}
},
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
@@ -13579,6 +13828,7 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -13941,6 +14191,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
@@ -14105,6 +14356,18 @@
"node": "6.* || 8.* || >= 10.*"
}
},
+ "node_modules/get-east-asian-width": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
+ "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
@@ -14274,6 +14537,7 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
@@ -14363,6 +14627,7 @@
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
"engines": {
"node": ">=4"
}
@@ -16686,6 +16951,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
@@ -16803,6 +17069,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -16838,6 +17105,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
"engines": {
"node": ">=8"
}
@@ -16863,6 +17131,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
@@ -16962,6 +17231,7 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
"engines": {
"node": ">=0.12.0"
}
@@ -19363,6 +19633,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",
@@ -19553,6 +19829,7 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
"engines": {
"node": ">= 8"
}
@@ -19575,6 +19852,7 @@
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
@@ -20190,6 +20468,37 @@
"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/ngx-icon-picker": {
+ "version": "1.11.2",
+ "resolved": "https://registry.npmjs.org/ngx-icon-picker/-/ngx-icon-picker-1.11.2.tgz",
+ "integrity": "sha512-Hxirc46MkTP1cK00exyudNYIhthe8Pw6KX39FD5n67lVNfcOSfqNkhU8UBQ41XMzCHTaLC8oBZXc60oBoweWKg==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@angular/common": "^16",
+ "@angular/core": "^16",
+ "@fortawesome/angular-fontawesome": ">=0.10.2",
+ "@fortawesome/fontawesome-svg-core": "^6.1.1",
+ "@fortawesome/free-solid-svg-icons": "^6.1.1",
+ "primeicons": "^5.0.0"
+ }
+ },
"node_modules/nice-napi": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",
@@ -20417,6 +20726,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -22093,6 +22403,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
"engines": {
"node": ">=8.6"
},
@@ -22856,6 +23167,7 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
"funding": [
{
"type": "github",
@@ -23415,6 +23727,7 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
@@ -23462,7 +23775,8 @@
"node_modules/reflect-metadata": {
"version": "0.1.14",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz",
- "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A=="
+ "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==",
+ "dev": true
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
@@ -24255,6 +24569,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -24440,6 +24755,7 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true,
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
@@ -24626,6 +24942,7 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
"funding": [
{
"type": "github",
@@ -24844,6 +25161,7 @@
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -24873,6 +25191,7 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
@@ -24883,7 +25202,8 @@
"node_modules/semver/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
},
"node_modules/send": {
"version": "0.19.0",
@@ -26097,6 +26417,7 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -26146,6 +26467,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
"engines": {
"node": ">=8"
}
@@ -26154,6 +26476,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -26895,6 +27218,34 @@
"ms": "^2.1.1"
}
},
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/tinyqueue": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz",
@@ -26943,6 +27294,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
"dependencies": {
"is-number": "^7.0.0"
},
@@ -27389,16 +27741,17 @@
}
},
"node_modules/typescript": {
- "version": "5.1.6",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
- "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
- "peer": true,
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
+ "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
+ "dev": true,
+ "license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
- "node": ">=14.17"
+ "node": ">=12.20"
}
},
"node_modules/uglify-es": {
@@ -28877,6 +29230,7 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@@ -28947,6 +29301,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
"engines": {
"node": ">=8"
}
@@ -28955,6 +29310,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -28969,6 +29325,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -29166,6 +29523,7 @@
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
@@ -29183,6 +29541,7 @@
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
"engines": {
"node": ">=12"
}
@@ -29242,13 +29601,10 @@
}
},
"node_modules/zone.js": {
- "version": "0.13.3",
- "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.13.3.tgz",
- "integrity": "sha512-MKPbmZie6fASC/ps4dkmIhaT5eonHkEt6eAy80K42tAm0G2W+AahLJjbfi6X9NPdciOE9GRFTTM8u2IiF6O3ww==",
- "peer": true,
- "dependencies": {
- "tslib": "^2.3.0"
- }
+ "version": "0.15.1",
+ "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
+ "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
+ "license": "MIT"
},
"node_modules/zrender": {
"version": "5.6.1",
diff --git a/package.json b/package.json
index fef7963f9..69f54a46c 100644
--- a/package.json
+++ b/package.json
@@ -55,6 +55,7 @@
"http-server": "^14.1.0",
"license-checker": "^25.0.1",
"release-it": "^15.10.1",
+ "typescript": "~5.0.4",
"webpack": "^5.61.0",
"webpack-cli": "^4.5.0"
},
@@ -69,15 +70,20 @@
},
"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",
"@angular/forms": "^16.1.3",
+ "@angular/localize": "^20.2.4",
"@angular/platform-browser": "^16.1.3",
"@angular/platform-browser-dynamic": "^16.1.3",
"@angular/router": "^16.1.3",
"@angular/upgrade": "^16.1.4",
+ "@fortawesome/angular-fontawesome": "^0.13.0",
"@fortawesome/fontawesome-free": "^6.1.1",
+ "@fortawesome/fontawesome-svg-core": "^1.2.36",
+ "@fortawesome/free-solid-svg-icons": "^6.7.2",
"@googlemaps/js-api-loader": "^1.16.8",
"@ng-bootstrap/ng-bootstrap": "^15.1.2",
"@ngx-translate/core": "^16.0.4",
@@ -85,6 +91,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",
@@ -139,6 +146,8 @@
"mathjax": "^3.2.2",
"ng2-ion-range-slider": "^2.0.0",
"ng2-nouislider": "^2.0.0",
+ "ngx-color": "^8.0.3",
+ "ngx-icon-picker": "^1.11.2",
"nouislider": "^15.8.1",
"papaparse": "^5.4.1",
"rangeslide.js": "^0.13.0",
@@ -149,7 +158,8 @@
"sortablejs": "^1.15.3",
"tableexport": "^5.2.0",
"toastr": "^2.1.4",
- "ui-select": "^0.19.8"
+ "ui-select": "^0.19.8",
+ "zone.js": "^0.15.1"
},
"jshintConfig": {
"undef": true,
diff --git a/test-icon-picker.html b/test-icon-picker.html
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/test-icon-picker.html
@@ -0,0 +1 @@
+