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 @@ +
+ +
+
+ +
+
+ + +
+

+ Verwalten der Geodaten + Info +

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

Points of Interest

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

Lines of Interest

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

Areas of Interest

+ +
+ +
+
+ +
+ +
+ + + + +
+
+ +
+ + +
+ + +
\ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/admin-georesources-management.component.ts b/app/components/ngComponents/admin/adminGeoresourcesManagement/admin-georesources-management.component.ts new file mode 100644 index 000000000..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 @@ + + + + + + + +
+ +

Georessource registriert

+

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

+
+ + +
+ +

Registrierung gescheitert

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

+  
+
+ +
+

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

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

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

+
+
+ + +
+ +

Metadata Import gescheitert

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

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

+

+
+ + +
+ +

Mapping-Konfiguration Import gescheitert

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

+  
+
+

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

+

+
\ No newline at end of file diff --git a/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceAddModal/georesource-add-modal.component.ts b/app/components/ngComponents/admin/adminGeoresourcesManagement/georesourceAddModal/georesource-add-modal.component.ts new file mode 100644 index 000000000..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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + + + + + + + \ 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 @@ + + + + + + + +
+ +

Georessource aktualisiert

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

Aktualisierung gescheitert

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

+
+ + +
+ +

Metadata Import gescheitert

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

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

+

+
\ 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 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 @@ +
+ +
+
+ +
+
+ + +
+

+ Verwalten der Indikatoren + Info +

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

Indikatoren-Metadaten

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

Indikatoren-Reihenfolge in Themenkatalog

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

+    + Standardindikator (numerische Wertverteilung) [Einheitsbezeichnung] +

+
+ +
+

+    + Leitindikator (bewertende Aussage) [Einheitsbezeichnung] +

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

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

+
+
+

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

+
+
+
+
+ + +
+
+
+

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

+
+
+

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

+
+
+
+
+
+ + +
+
+
+

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

+
+
+

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

+
+
+
+
+
+ + +
+
+
+

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

+
+
+

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

+
+
+
+
+
+ +
+ +
+ +
+ +
+ + +
+ + +
\ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/admin-indicators-management.component.ts b/app/components/ngComponents/admin/adminIndicatorsManagement/admin-indicators-management.component.ts new file mode 100644 index 000000000..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 @@ + + + + + + + +
+ +

Indikator-Metadaten registriert

+

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

+
+ + +
+ +

Registrierung gescheitert

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

+  
+
+
+ + +
+ +

Metadata Import gescheitert

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

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

+
{{indicatorMetadataStructure_pretty}}
+
\ No newline at end of file diff --git a/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorAddModal/indicator-add-modal.component.ts b/app/components/ngComponents/admin/adminIndicatorsManagement/indicatorAddModal/indicator-add-modal.component.ts new file mode 100644 index 000000000..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 @@ + +
+
+ +
+
+ + + + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + + + +
+ +

Zeitreihen erfolgreich fortgeführt

+

Fortführen der Zeitreihen des Indikators mit Namen {{successMessagePart}} war erfolgreich.

+
+ {{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:

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

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 @@ + + + + + + + +
+ +

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 @@ + + + + + + \ 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 @@ +
+ +
+
+ +
+
+ + + +
+

+ Verwalten der Raumebenen + Info +

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

Raumebenen

+
+ +
+ +
+ + + + +
+
+ +
+ + +
+
+ +
+ +
\ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/admin-spatial-units-management.component.ts b/app/components/ngComponents/admin/adminSpatialUnitsManagement/admin-spatial-units-management.component.ts new file mode 100644 index 000000000..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 = '
    '; + for (const periodOfValidity of params.data.availablePeriodsOfValidity) { + html += '
  • '; + if (periodOfValidity.endDate) { + html += '

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

    '; + } else { + html += '

    ' + periodOfValidity.startDate + ' ‐ heute

    '; + } + html += '
  • '; + } + html += '
'; + return html; + }, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => { + if (params.data.availablePeriodsOfValidity && params.data.availablePeriodsOfValidity.length > 1) { + return '' + JSON.stringify(params.data.availablePeriodsOfValidity); + } + return params.data.availablePeriodsOfValidity; + } + }, + { + headerName: 'Datenquelle', + minWidth: 400, + cellRenderer: (params: any) => params.data.metadata.datasource, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + params.data.metadata.datasource + }, + { + headerName: 'Datenhalter und Kontakt', + minWidth: 400, + cellRenderer: (params: any) => params.data.metadata.contact, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + params.data.metadata.contact + }, + { + headerName: 'Rollen', + minWidth: 400, + cellRenderer: (params: any) => this.kommonitorDataExchangeService.getAllowedRolesString(params.data.permissions), + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + this.kommonitorDataExchangeService.getAllowedRolesString(params.data.permissions) + }, + { + headerName: 'Öffentlich sichtbar', + minWidth: 400, + cellRenderer: (params: any) => params.data.isPublic ? 'ja' : 'nein', + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + (params.data.isPublic ? 'ja' : 'nein') + }, + { + headerName: 'Eigentümer', + minWidth: 400, + cellRenderer: (params: any) => this.kommonitorDataExchangeService.getRoleTitle(params.data.ownerId), + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + this.kommonitorDataExchangeService.getRoleTitle(params.data.ownerId) + } + ]; + } + + private buildDataGridRowData_spatialUnits(spatialUnitMetadataArray: any[]): any[] { + return spatialUnitMetadataArray.map(metadata => ({ + ...metadata, + spatialUnitId: metadata.spatialUnitId, + spatialUnitLevel: metadata.spatialUnitLevel + })); + } + + private displayEditButtons_spatialUnits(params: any): string { + const data = params.data; + let html = '
'; + + // Edit Metadata Button + html += ''; + + // Edit Features Button + html += ''; + + // Edit User Roles Button + html += ''; + + // Delete Button + html += ''; + + html += '
'; + return html; + } + + // Grid event handlers + onFirstDataRendered(event: FirstDataRenderedEvent): void { + this.headerHeightSetter(); + this.registerClickHandler_spatialUnits(); + } + + onColumnResized(event: ColumnResizedEvent): void { + this.headerHeightSetter(); + } + + onRowDataChanged(): void { + // Click handler registration is now handled by the service + } + + onModelUpdated(): void { + // Click handler registration is now handled by the service + } + + onViewportChanged(): void { + // Click handler registration is now handled by the service + } + + private registerClickHandler_spatialUnits(): void { + // Use event delegation on the grid container instead of individual buttons + // This ensures handlers work even for dynamically rendered buttons + const $ = (window as any).$; + + // Remove any existing handlers first to avoid duplicates + $('#spatialUnitOverviewTable').off('click', '.spatialUnitEditMetadataBtn'); + $('#spatialUnitOverviewTable').off('click', '.spatialUnitEditFeaturesBtn'); + $('#spatialUnitOverviewTable').off('click', '.spatialUnitEditUserRolesBtn'); + $('#spatialUnitOverviewTable').off('click', '.spatialUnitDeleteBtn'); + + // Edit Metadata Button - use event delegation + $('#spatialUnitOverviewTable').on('click', '.spatialUnitEditMetadataBtn', (event: any) => { + event.stopPropagation(); + event.preventDefault(); + + // Get the button element (could be the icon inside) + const button = $(event.target).closest('.spatialUnitEditMetadataBtn')[0]; + const spatialUnitId = button.id.split('_')[3]; + const spatialUnitMetadata = this.kommonitorDataExchangeService.getSpatialUnitMetadataById(spatialUnitId); + + if (spatialUnitMetadata) { + this.zone.run(() => { + this.onClickEditMetadata(spatialUnitMetadata); + }); + } + }); + + // Edit Features Button - use event delegation + $('#spatialUnitOverviewTable').on('click', '.spatialUnitEditFeaturesBtn', (event: any) => { + event.stopPropagation(); + event.preventDefault(); + + // Get the button element (could be the icon inside) + const button = $(event.target).closest('.spatialUnitEditFeaturesBtn')[0]; + const spatialUnitId = button.id.split('_')[3]; + const spatialUnitMetadata = this.kommonitorDataExchangeService.getSpatialUnitMetadataById(spatialUnitId); + + if (spatialUnitMetadata) { + this.zone.run(() => { + this.onClickEditFeatures(spatialUnitMetadata); + }); + } + }); + + // Edit User Roles Button - use event delegation + $('#spatialUnitOverviewTable').on('click', '.spatialUnitEditUserRolesBtn', (event: any) => { + event.stopPropagation(); + event.preventDefault(); + + // Get the button element (could be the icon inside) + const button = $(event.target).closest('.spatialUnitEditUserRolesBtn')[0]; + const spatialUnitId = button.id.split('_')[3]; + const spatialUnitMetadata = this.kommonitorDataExchangeService.getSpatialUnitMetadataById(spatialUnitId); + + if (spatialUnitMetadata) { + this.zone.run(() => { + this.onClickEditUserRoles(spatialUnitMetadata); + }); + } + }); + + // Delete Button - use event delegation + $('#spatialUnitOverviewTable').on('click', '.spatialUnitDeleteBtn', (event: any) => { + event.stopPropagation(); + event.preventDefault(); + + // Get the button element (could be the icon inside) + const button = $(event.target).closest('.spatialUnitDeleteBtn')[0]; + const spatialUnitId = button.id.split('_')[3]; + const spatialUnitMetadata = this.kommonitorDataExchangeService.getSpatialUnitMetadataById(spatialUnitId); + + if (spatialUnitMetadata) { + this.zone.run(() => { + this.onClickDeleteSpatialUnits([spatialUnitMetadata]); + }); + } + }); + } + + private headerHeightSetter(): void { + if (this.gridApi) { + const headerHeight = this.headerHeightGetter(); + this.gridApi.setHeaderHeight(headerHeight); + } + } + + private headerHeightGetter(): number { + const headerElement = document.querySelector('.ag-header'); + if (headerElement) { + const headerTextElements = headerElement.querySelectorAll('.ag-header-cell-text'); + let maxHeight = 0; + headerTextElements.forEach(element => { + const height = element.scrollHeight; + if (height > maxHeight) { + maxHeight = height; + } + }); + return Math.max(maxHeight + 20, 50); // Add padding and minimum height + } + return 50; + } + + getSelectedSpatialUnitsMetadata(): any[] { + if (this.gridApi) { + const selectedNodes = this.gridApi.getSelectedNodes(); + return selectedNodes.map(node => node.data); + } + return []; + } +} \ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitAddModal/spatial-unit-add-modal.component.css b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitAddModal/spatial-unit-add-modal.component.css new file mode 100644 index 000000000..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 @@ + + + + + + + +
+ +

Raumebene registriert

+

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

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

Registrierung gescheitert

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

+  
+
+
+

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

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

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

+
+
+ + +
+ +

Metadata Import gescheitert

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

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

+

+
+ + +
+ +

Mapping-Konfiguration Import gescheitert

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

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

+

+
\ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitAddModal/spatial-unit-add-modal.component.ts b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitAddModal/spatial-unit-add-modal.component.ts new file mode 100644 index 000000000..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 @@ + + + + + + +
+ +

Folgende Raumebenen wurde erfolgreich gelöscht

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

Löschen gescheitert

+ Folgende Datensätze konnten nicht gelöscht werden. +
+ + + + + + + + + + + + + +
NameFehlermeldung
{{dataset[0].spatialUnitLevel}}
+
\ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitDeleteModal/spatial-unit-delete-modal.component.ts b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitDeleteModal/spatial-unit-delete-modal.component.ts new file mode 100644 index 000000000..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 @@ + + + + + + + +
+ +

Erfolg!

+ {{ successMessage }} +
+

Raumebene: {{successMessagePart}}

+
+
+ + +
+ +

Fehler!

+ {{ errorMessage }} +
+

+  
+
+

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

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

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

+
+
+ + +
+ +

Mapping-Konfiguration Fehler!

+

+
\ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditFeaturesModal/spatial-unit-edit-features-modal.component.ts b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditFeaturesModal/spatial-unit-edit-features-modal.component.ts new file mode 100644 index 000000000..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 @@ + + + + + + + +
+ +

Raumebene aktualisiert

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

Aktualisierung gescheitert

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

+
+ + +
+ +

Metadata Import gescheitert

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

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

+
{{spatialUnitMetadataStructure | json}}
+
\ No newline at end of file diff --git a/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditMetadataModal/spatial-unit-edit-metadata-modal.component.ts b/app/components/ngComponents/admin/adminSpatialUnitsManagement/spatialUnitEditMetadataModal/spatial-unit-edit-metadata-modal.component.ts new file mode 100644 index 000000000..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 @@ +
+
+ +
+
+ + + + + + +
+ +

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 @@ +
+ + +
+ + +
+ +
+
+ + 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 @@ +
+ + + + +
+ +
+
Select Icon
+ +
+ + + + + +
+
+ +
+
+ + +
+
+ + + + {{ getFormattedLabel(labelHeader, currentPage, totalPages) }} + + + +
+
+ + + +
+
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 @@ +
+ + + + +
+
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 = '
    '; + for (const periodOfValidity of params.data.availablePeriodsOfValidity || []) { + html += '
  • '; + if(periodOfValidity.endDate){ + html += "

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

    "; + } else { + html += "

    " + periodOfValidity.startDate + " ‐ heute

    "; + } + html += '
  • '; + } + html += '
'; + return html; + } + }, + { + headerName: 'Themenhierarchie', + minWidth: 400, + cellRenderer: (params: any) => { + return this.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 = '
    '; + for (const periodOfValidity of params.data.availablePeriodsOfValidity || []) { + html += '
  • '; + if(periodOfValidity.endDate){ + html += "

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

    "; + } else { + html += "

    " + periodOfValidity.startDate + " ‐ heute

    "; + } + html += '
  • '; + } + html += '
'; + return html; + } + }, + { + headerName: 'Themenhierarchie', + minWidth: 400, + cellRenderer: (params: any) => { + return this.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 = '
    '; + for (const periodOfValidity of params.data.availablePeriodsOfValidity || []) { + html += '
  • '; + if(periodOfValidity.endDate){ + html += "

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

    "; + } else { + html += "

    " + periodOfValidity.startDate + " ‐ heute

    "; + } + html += '
  • '; + } + html += '
'; + return html; + } + }, + { + headerName: 'Themenhierarchie', + minWidth: 400, + cellRenderer: (params: any) => { + return this.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 += '  '; + 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 = '
    '; + for (const periodOfValidity of params.data.availablePeriodsOfValidity) { + html += '
  • '; + if (periodOfValidity.endDate) { + html += '

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

    '; + } else { + html += '

    ' + periodOfValidity.startDate + ' ‐ heute

    '; + } + html += '
  • '; + } + html += '
'; + return html; + }, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => { + if (params.data.availablePeriodsOfValidity && params.data.availablePeriodsOfValidity.length > 1) { + return '' + JSON.stringify(params.data.availablePeriodsOfValidity); + } + return params.data.availablePeriodsOfValidity; + } + }, + { + headerName: 'Datenquelle', + minWidth: 400, + cellRenderer: (params: ICellRendererParams) => params.data.metadata.datasource, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + params.data.metadata.datasource + }, + { + headerName: 'Datenhalter und Kontakt', + minWidth: 400, + cellRenderer: (params: ICellRendererParams) => params.data.metadata.contact, + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + params.data.metadata.contact + }, + { + headerName: 'Rollen', + minWidth: 400, + cellRenderer: (params: ICellRendererParams) => this.kommonitorDataExchangeService.getAllowedRolesString(params.data.permissions), + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + this.kommonitorDataExchangeService.getAllowedRolesString(params.data.permissions) + }, + { + headerName: 'Öffentlich sichtbar', + minWidth: 400, + cellRenderer: (params: ICellRendererParams) => params.data.isPublic ? 'ja' : 'nein', + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + (params.data.isPublic ? 'ja' : 'nein') + }, + { + headerName: 'Eigentümer', + minWidth: 400, + cellRenderer: (params: ICellRendererParams) => this.kommonitorDataExchangeService.getRoleTitle(params.data.ownerId), + filter: 'agTextColumnFilter', + filterValueGetter: (params: any) => '' + this.kommonitorDataExchangeService.getRoleTitle(params.data.ownerId) + } + ]; + + return columnDefs; + } + + /** + * Build row data for spatial units (just return the input array) + */ + buildDataGridRowData_spatialUnits(spatialUnitMetadataArray: any[]): any[] { + return spatialUnitMetadataArray; + } + + /** + * Build grid options for spatial units + */ + buildGridOptions(): GridOptions { + return { + suppressRowClickSelection: true, + rowSelection: 'multiple', + enableCellTextSelection: true, + ensureDomOrder: true, + pagination: true, + paginationPageSize: 10, + suppressColumnVirtualisation: true + }; + } + + /** + * Build default column definition for role management grids + */ + buildRoleManagementDefaultColDef(): any { + return { + editable: false, + sortable: true, + flex: 1, + minWidth: 100, + filter: false, + floatingFilter: false, + resizable: true, + wrapText: true, + autoHeight: true, + cellStyle: { + 'font-size': '12px', + 'white-space': 'normal !important', + 'line-height': '20px !important', + 'word-break': 'break-word !important', + 'padding-top': '17px', + 'padding-bottom': '17px' + }, + headerComponentParams: { + template: + '', + }, + }; + } + + /** + * Build grid options for role management grids (public method for components) + */ + buildRoleManagementGridOptionsPublic(components?: any): GridOptions { + return { + components: components || {}, + suppressRowClickSelection: true, + rowSelection: 'multiple', + enableCellTextSelection: true, + ensureDomOrder: true, + pagination: true, + paginationPageSize: 10, + suppressColumnVirtualisation: true, + headerHeight: 40, + rowHeight: 35 + }; + } + + + + /** + * Cell renderer for edit buttons + */ + displayEditButtons_spatialUnits(params: any): string { + const data = params.data; + let html = '
'; + + // Edit Metadata Button + html += ''; + + // Edit Features Button + html += ''; + + // Edit User Roles Button + html += ''; + + // Delete Button + html += ''; + + html += '
'; + return html; + } + + /** + * Register click handlers for buttons + */ + private registerClickHandler_spatialUnits(): void { + // Use native DOM methods instead of jQuery + setTimeout(() => { + // Edit Metadata Button + const editMetadataButtons = document.querySelectorAll('.spatialUnitEditMetadataBtn'); + editMetadataButtons.forEach((button: any) => { + button.removeEventListener('click', this.handleEditMetadataClick); + button.addEventListener('click', this.handleEditMetadataClick); + }); + + // Edit Features Button + const editFeaturesButtons = document.querySelectorAll('.spatialUnitEditFeaturesBtn'); + editFeaturesButtons.forEach((button: any) => { + button.removeEventListener('click', this.handleEditFeaturesClick); + button.addEventListener('click', this.handleEditFeaturesClick); + }); + + // Edit User Roles Button + const editUserRolesButtons = document.querySelectorAll('.spatialUnitEditUserRolesBtn'); + editUserRolesButtons.forEach((button: any) => { + button.removeEventListener('click', this.handleEditUserRolesClick); + button.addEventListener('click', this.handleEditUserRolesClick); + }); + + // Delete Button + const deleteButtons = document.querySelectorAll('.spatialUnitDeleteBtn'); + deleteButtons.forEach((button: any) => { + button.removeEventListener('click', this.handleDeleteClick); + button.addEventListener('click', this.handleDeleteClick); + }); + }, 100); + } + + /** + * Handle edit metadata button click + */ + private handleEditMetadataClick = (event: any): void => { + event.stopPropagation(); + + const spatialUnitId = event.target.id.split('_')[3]; + const spatialUnitMetadata = this.kommonitorDataExchangeService.getSpatialUnitMetadataById(spatialUnitId); + + this.broadcastService.broadcast('onEditSpatialUnitMetadata', spatialUnitMetadata); + } + + /** + * Handle edit features button click + */ + private handleEditFeaturesClick = (event: any): void => { + event.stopPropagation(); + + const spatialUnitId = event.target.id.split('_')[3]; + const spatialUnitMetadata = this.kommonitorDataExchangeService.getSpatialUnitMetadataById(spatialUnitId); + + this.broadcastService.broadcast('onEditSpatialUnitFeatures', spatialUnitMetadata); + } + + /** + * Handle edit user roles button click + */ + private handleEditUserRolesClick = (event: any): void => { + event.stopPropagation(); + + const spatialUnitId = event.target.id.split('_')[3]; + const spatialUnitMetadata = this.kommonitorDataExchangeService.getSpatialUnitMetadataById(spatialUnitId); + + this.broadcastService.broadcast('onEditSpatialUnitUserRoles', spatialUnitMetadata); + } + + /** + * Handle delete button click + */ + private handleDeleteClick = (event: any): void => { + event.stopPropagation(); + + const spatialUnitId = event.target.id.split('_')[3]; + const spatialUnitMetadata = this.kommonitorDataExchangeService.getSpatialUnitMetadataById(spatialUnitId); + + this.broadcastService.broadcast('onDeleteSpatialUnits', [spatialUnitMetadata]); + } + + /** + * Get selected spatial units metadata + */ + getSelectedSpatialUnitsMetadata(): any[] { + const spatialUnitsMetadataArray: any[] = []; + + if (this.dataGridOptions_spatialUnits && this.gridApi_spatialUnits) { + const selectedNodes = this.gridApi_spatialUnits.getSelectedNodes(); + for (const selectedNode of selectedNodes) { + spatialUnitsMetadataArray.push(selectedNode.data); + } + } + + return spatialUnitsMetadataArray; + } + + /** + * Save grid state (for preserving selection/filters when updating data) + */ + private saveGridStore(gridOptions: any): void { + if (gridOptions && this.gridApi_spatialUnits) { + // Store selection state + const selectedNodes = this.gridApi_spatialUnits.getSelectedNodes(); + gridOptions._savedState = { + selectedIds: selectedNodes.map((node: any) => node.data.spatialUnitId) + }; + } + } + + /** + * Restore grid state (for preserving selection/filters when updating data) + */ + private restoreGridStore(gridOptions: any): void { + if (gridOptions && this.gridApi_spatialUnits && gridOptions._savedState) { + setTimeout(() => { + // Restore selection + this.gridApi_spatialUnits?.forEachNode((node: any) => { + if (gridOptions._savedState.selectedIds.includes(node.data.spatialUnitId)) { + node.setSelected(true); + } + }); + }, 100); + } + } + + /** + * Set header height for proper display + */ + private headerHeightSetter(): void { + if (this.gridApi_spatialUnits) { + const headerHeight = this.headerHeightGetter(); + this.gridApi_spatialUnits.setHeaderHeight(headerHeight); + } + } + + /** + * Calculate header height based on content + */ + private headerHeightGetter(): number { + const columnHeaderTexts = document.querySelectorAll('.ag-header-cell-text'); + let maxHeight = 0; + + columnHeaderTexts.forEach((element: any) => { + const height = element.offsetHeight; + if (height > maxHeight) { + maxHeight = height; + } + }); + + return Math.max(maxHeight + 20, 50); // Add padding, minimum 50px + } + + /** + * Build role management grid for spatial units + */ + buildRoleManagementGrid(tableDOMId: string, currentTableOptionsObject: any, accessControlMetadata: any[], selectedPermissionIds: string[], reducedRoleManagement: boolean = false): any { + if (currentTableOptionsObject && this.gridApi_spatialUnits) { + // Grid already exists, just update the data + const newRowData = this.buildRoleManagementGridRowData(accessControlMetadata, selectedPermissionIds); + // update underlying options so callers get the latest data + currentTableOptionsObject.rowData = newRowData; + this.gridApi_spatialUnits.setRowData(newRowData); + // ensure cells re-render to apply disabled state and checks + setTimeout(() => { + try { + this.gridApi_spatialUnits?.refreshCells({ force: true }); + this.gridApi_spatialUnits?.redrawRows(); + } catch (e) {} + }, 0); + } else { + // Create new grid options + currentTableOptionsObject = this.buildRoleManagementGridOptions(accessControlMetadata, selectedPermissionIds, reducedRoleManagement); + + } + 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 @@ +