From e901aefbf5f50c0f2157e4d430c119e03106917e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Murat=20O=CC=88zkorkmaz?= Date: Thu, 13 Nov 2025 19:56:50 +0100 Subject: [PATCH] Several fixes - added organizations - added industries - added logo in 2 colors for light and dark theme - improved authorization to allow multi tenancy --- README.md | 9 + public/logo-dark.png | Bin 0 -> 8319 bytes public/logo-light.png | Bin 0 -> 6028 bytes src/app.routes.ts | 10 + src/app/layout/component/app.footer.ts | 2 +- src/app/layout/component/app.menu.ts | 13 +- src/app/layout/component/app.menuitem.ts | 6 +- src/app/layout/component/app.topbar.ts | 193 ++++++------- .../industry-manager/industry-manager.html | 91 ++++++ .../industry-manager/industry-manager.scss | 0 .../industry-manager/industry-manager.spec.ts | 23 ++ .../industry-manager/industry-manager.ts | 272 ++++++++++++++++++ .../pages/organizations/organizations.html | 156 ++++++++++ .../pages/organizations/organizations.scss | 0 .../pages/organizations/organizations.spec.ts | 23 ++ src/app/pages/organizations/organizations.ts | 120 ++++++++ .../project-details/project-details.html | 22 +- .../pages/project-details/project-details.ts | 22 +- src/app/pages/projects/projects.ts | 3 - .../property-manager/property-manager.html | 2 +- .../pages/service/industry.service.spec.ts | 16 ++ src/app/pages/service/industry.service.ts | 63 ++++ .../service/organization.service.spec.ts | 16 ++ src/app/pages/service/organization.service.ts | 42 +++ src/app/pages/service/property.service.ts | 4 +- src/app/pipes/has-role-pipe.spec.ts | 8 - ...s-role-pipe.ts => is-role-allowed-pipe.ts} | 4 +- src/app/pipes/safe-url.pipe.ts | 11 + 28 files changed, 997 insertions(+), 134 deletions(-) create mode 100644 public/logo-dark.png create mode 100644 public/logo-light.png create mode 100644 src/app/pages/industry-manager/industry-manager.html create mode 100644 src/app/pages/industry-manager/industry-manager.scss create mode 100644 src/app/pages/industry-manager/industry-manager.spec.ts create mode 100644 src/app/pages/industry-manager/industry-manager.ts create mode 100644 src/app/pages/organizations/organizations.html create mode 100644 src/app/pages/organizations/organizations.scss create mode 100644 src/app/pages/organizations/organizations.spec.ts create mode 100644 src/app/pages/organizations/organizations.ts create mode 100644 src/app/pages/service/industry.service.spec.ts create mode 100644 src/app/pages/service/industry.service.ts create mode 100644 src/app/pages/service/organization.service.spec.ts create mode 100644 src/app/pages/service/organization.service.ts delete mode 100644 src/app/pipes/has-role-pipe.spec.ts rename src/app/pipes/{has-role-pipe.ts => is-role-allowed-pipe.ts} (87%) create mode 100644 src/app/pipes/safe-url.pipe.ts diff --git a/README.md b/README.md index 5e0bf7f..57a9b89 100644 --- a/README.md +++ b/README.md @@ -71,3 +71,12 @@ CREATE src/app/pages/immo-manager/billing/billing.spec.ts (535 bytes) CREATE src/app/pages/immo-manager/billing/billing.ts (190 bytes) CREATE src/app/pages/immo-manager/billing/billing.html (22 bytes) ``` + +### Create new service + +```bash +ng generate s pages/service/organization.service + +CREATE src/app/pages/service/organization.service.spec.ts (387 bytes) +CREATE src/app/pages/service/organization.service.ts (123 bytes) +``` diff --git a/public/logo-dark.png b/public/logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..9ae5e93f629cedc8fc04fb7c089387e8348236aa GIT binary patch literal 8319 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 H(D(iiBVb-I literal 0 HcmV?d00001 diff --git a/src/app.routes.ts b/src/app.routes.ts index fd038f9..ba55cce 100644 --- a/src/app.routes.ts +++ b/src/app.routes.ts @@ -11,6 +11,8 @@ import { canActivateAuthRole } from '@/guards/auth.guard'; import { Projects } from '@/pages/projects/projects'; import { ProjectDetails } from '@/pages/project-details/project-details'; import { Contacts } from '@/pages/contacts/contacts'; +import { Organizations } from '@/pages/organizations/organizations'; +import { IndustryManager } from '@/pages/industry-manager/industry-manager'; export const appRoutes: Routes = [ { @@ -26,6 +28,10 @@ export const appRoutes: Routes = [ path: 'projects', component: Projects, data: { role: ['dev', 'admin', 'can-view-projects'] }, canActivate: [canActivateAuthRole] }, + { + path: 'organizations', component: Organizations, + data: { role: ['dev', 'admin', 'can-view-organizations'] }, canActivate: [canActivateAuthRole] + }, { path: 'projects/:id', component: ProjectDetails, data: { role: ['dev', 'admin', 'can-view-projects'] }, canActivate: [canActivateAuthRole] @@ -37,6 +43,10 @@ export const appRoutes: Routes = [ }, // admin pages + { + path: 'admin/industries', component: IndustryManager, + data: { role: ['dev', 'admin', 'can-manage-industries'] }, canActivate: [canActivateAuthRole] + }, { path: 'properties', component: Properties, data: { role: ['dev', 'admin', 'can-view-properties'] }, canActivate: [canActivateAuthRole] diff --git a/src/app/layout/component/app.footer.ts b/src/app/layout/component/app.footer.ts index 1ab5367..6ccb051 100644 --- a/src/app/layout/component/app.footer.ts +++ b/src/app/layout/component/app.footer.ts @@ -4,7 +4,7 @@ import { Component } from '@angular/core'; standalone: true, selector: 'app-footer', template: `` }) diff --git a/src/app/layout/component/app.menu.ts b/src/app/layout/component/app.menu.ts index 9a10131..923b463 100644 --- a/src/app/layout/component/app.menu.ts +++ b/src/app/layout/component/app.menu.ts @@ -4,19 +4,20 @@ import { RouterModule } from '@angular/router'; import { MenuItem } from 'primeng/api'; import { AppMenuitem } from './app.menuitem'; import Keycloak from 'keycloak-js'; -import { HasRolePipe } from '@/pipes/has-role-pipe'; +import { IsRoleAllowedPipe } from '@/pipes/is-role-allowed-pipe'; + @Component({ selector: 'app-menu', standalone: true, - imports: [CommonModule, AppMenuitem, RouterModule, HasRolePipe], + imports: [CommonModule, AppMenuitem, RouterModule, IsRoleAllowedPipe, IsRoleAllowedPipe], template: `
    @for (rootMenuItem of model; track $index) { @if (!rootMenuItem.separator) { @if (rootMenuItem['roles']) { - @if (rootMenuItem['roles'] | hasRole: 'any') { + @if (rootMenuItem['roles'] | isRoleAllowed: 'any') {
  • } } @@ -72,6 +73,12 @@ export class AppMenu { label: 'Gebäude Verwalten', icon: 'pi pi-fw pi-home', routerLink: ['/admin/properties'] + }, + { + roles: ['dev', 'admin', 'can-manage-industries'], + label: 'Branchen Verwalten', + icon: 'pi pi-fw pi-home', + routerLink: ['/admin/industries'] } ] }, diff --git a/src/app/layout/component/app.menuitem.ts b/src/app/layout/component/app.menuitem.ts index 92d9498..9a936d5 100644 --- a/src/app/layout/component/app.menuitem.ts +++ b/src/app/layout/component/app.menuitem.ts @@ -7,12 +7,12 @@ import { CommonModule } from '@angular/common'; import { RippleModule } from 'primeng/ripple'; import { MenuItem } from 'primeng/api'; import { LayoutService } from '../service/layout.service'; -import { HasRolePipe } from '@/pipes/has-role-pipe'; +import { IsRoleAllowedPipe } from '@/pipes/is-role-allowed-pipe'; @Component({ // eslint-disable-next-line @angular-eslint/component-selector selector: '[app-menuitem]', - imports: [CommonModule, RouterModule, RippleModule, HasRolePipe], + imports: [CommonModule, RouterModule, RippleModule, IsRoleAllowedPipe], template: `
    {{ item.label }}
    @@ -47,7 +47,7 @@ import { HasRolePipe } from '@/pipes/has-role-pipe';
      @if (child['roles']) { - @if (child['roles'] | hasRole: 'any') { + @if (child['roles'] | isRoleAllowed: 'any') {
    • } } diff --git a/src/app/layout/component/app.topbar.ts b/src/app/layout/component/app.topbar.ts index 00234af..4ab1253 100644 --- a/src/app/layout/component/app.topbar.ts +++ b/src/app/layout/component/app.topbar.ts @@ -1,4 +1,4 @@ -import { Component, inject } from '@angular/core'; +import { Component, inject, OnInit } from '@angular/core'; import { MenuItem } from 'primeng/api'; import { RouterModule } from '@angular/router'; import { CommonModule } from '@angular/common'; @@ -15,113 +15,88 @@ import { Tag } from 'primeng/tag'; selector: 'app-topbar', standalone: true, imports: [RouterModule, CommonModule, StyleClassModule, AppConfigurator, Popover, Button, Tag], - template: ` -
      -
      - + + + @if (keycloak.authenticated) { + + @if (keycloak) { + + } + + @for (item of keycloak.realmAccess?.roles; track $index) { + + } + } + @if (!keycloak.authenticated) { + + } +
      + +
      +
      + - - - @if (keycloak.authenticated) { - - @if(keycloak) { - - } - - @for (item of keycloak.realmAccess?.roles; track $index) { - - } - } - @if (!keycloak.authenticated) { - - } -
      - -
      -
      - -
      - - -
      -
      - - - -
      -
      ` + + + + +
      + ` }) -export class AppTopbar { +export class AppTopbar implements OnInit { items!: MenuItem[]; protected keycloak = inject(Keycloak); @@ -130,7 +105,7 @@ export class AppTopbar { public firstName?: string = 'unknown'; public lastName?: string = 'unknown'; public email?: string = 'unknown'; - + protected logo: string = ''; constructor(public layoutService: LayoutService) { if (this.keycloak.authenticated) { @@ -145,6 +120,16 @@ export class AppTopbar { } } + ngOnInit(): void { + this.layoutService.configUpdate$.subscribe(() => { + this.setLogo(); + }); + } + + setLogo() { + const isDarkTheme = this.layoutService._config.darkTheme; + this.logo = isDarkTheme? '/logo-dark.png' : '/logo-light.png'; + } logout() { this.keycloak.logout(); } diff --git a/src/app/pages/industry-manager/industry-manager.html b/src/app/pages/industry-manager/industry-manager.html new file mode 100644 index 0000000..5caeaf0 --- /dev/null +++ b/src/app/pages/industry-manager/industry-manager.html @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + +
      +
      Verwalte Branchen
      + + + + +
      +
      + + + + + + + + ID + + + Name + + + + + + + + + + + {{ industry.id }} + {{ industry.name }} + + + + + + +
      + + + + +
      + +
      + + + @if (submitted && !industry().name) { + Name is required. + } +
      + +
      +
      + + + + + +
      + + diff --git a/src/app/pages/industry-manager/industry-manager.scss b/src/app/pages/industry-manager/industry-manager.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/industry-manager/industry-manager.spec.ts b/src/app/pages/industry-manager/industry-manager.spec.ts new file mode 100644 index 0000000..e4451e8 --- /dev/null +++ b/src/app/pages/industry-manager/industry-manager.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { IndustryManager } from './industry-manager'; + +describe('IndustryManager', () => { + let component: IndustryManager; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [IndustryManager] + }) + .compileComponents(); + + fixture = TestBed.createComponent(IndustryManager); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/industry-manager/industry-manager.ts b/src/app/pages/industry-manager/industry-manager.ts new file mode 100644 index 0000000..66f2a40 --- /dev/null +++ b/src/app/pages/industry-manager/industry-manager.ts @@ -0,0 +1,272 @@ +import { Component, signal, ViewChild } from '@angular/core'; +import { Button } from 'primeng/button'; +import { ConfirmDialog } from 'primeng/confirmdialog'; +import { Dialog } from 'primeng/dialog'; +import { FormsModule } from '@angular/forms'; +import { IconField } from 'primeng/iconfield'; +import { InputIcon } from 'primeng/inputicon'; +import { InputText } from 'primeng/inputtext'; +import { Table, TableModule } from 'primeng/table'; +import { Toolbar } from 'primeng/toolbar'; +import { Country, PropertyService } from '@/pages/service/property.service'; +import { ConfirmationService, MessageService } from 'primeng/api'; +import { CountryService } from '@/pages/service/country.service'; +import { AttachmentService } from '@/pages/service/attachment.service'; +import { environment } from '../../../environments/environments'; +import { Industry, IndustryService } from '@/pages/service/industry.service'; +import { Toast } from 'primeng/toast'; +import { Ripple } from 'primeng/ripple'; + +interface Column { + field: string; + header: string; + customExportHeader?: string; +} + +interface ExportColumn { + title: string; + dataKey: string; +} + +@Component({ + selector: 'app-industry-manager', + imports: [ + Button, + ConfirmDialog, + Dialog, + FormsModule, + IconField, + InputIcon, + InputText, + TableModule, + Toolbar, + Toast + ], + templateUrl: './industry-manager.html', + styleUrl: './industry-manager.scss', + providers: [MessageService, ConfirmationService, PropertyService, CountryService, AttachmentService], +}) +export class IndustryManager { + industryDialog: boolean = false; + + industries = signal([]); + industry = signal({}); + + selectedIndustries!: Industry[] | null; + + submitted: boolean = false; + statuses!: any[]; + + @ViewChild('dt') dt!: Table; + + exportColumns!: ExportColumn[]; + + cols!: Column[]; + countries: Country[] = []; + + constructor( + private messageService: MessageService, + private confirmationService: ConfirmationService, + private industryService: IndustryService, + private countryService: CountryService, + private attachmentService: AttachmentService, + ) {} + + exportCSV() { + // this.dt.exportCSV(); + console.debug(this, ' -- ', this.dt); + } + + ngOnInit() { + this.selectedIndustries = []; + + this.industryService.getIndustries().subscribe((industries) => { + console.log('Industries', industries); + this.industries.set(industries); + }); + + this.countryService.getCountries().then((countries) => { + this.countries = countries; + }); + } + + loadDemoData() { + this.statuses = [ + { label: 'INSTOCK', value: 'instock' }, + { label: 'LOWSTOCK', value: 'lowstock' }, + { label: 'OUTOFSTOCK', value: 'outofstock' } + ]; + + this.cols = [ + { field: 'id', header: 'ID', customExportHeader: 'Branchen ID' }, + { field: 'name', header: 'Name' } + ]; + + this.exportColumns = this.cols.map((col) => ({ title: col.header, dataKey: col.field })); + } + + onGlobalFilter(table: Table, event: Event) { + table.filterGlobal((event.target as HTMLInputElement).value, 'contains'); + } + + openNew() { + this.industry.set({}); + this.submitted = false; + this.industryDialog = true; + } + + editIndustry(industry: Industry) { + this.industry.set({ ...industry }); + this.industryDialog = true; + } + + deleteSelected() { + this.confirmationService.confirm({ + message: 'Sind Sie sicher, dass sie die angewählten Branchen endgültig löschen möchten?', + header: 'Bestätigung', + icon: 'pi pi-exclamation-triangle', + accept: () => { + console.log('properties to delete', this.selectedIndustries); + + if (this.selectedIndustries === null) { + return; + } + + this.industryService.deleteIndustries(this.selectedIndustries).subscribe({ + next: () => { + this.industries.set(this.industries().filter((val) => !this.selectedIndustries?.includes(val))); + this.selectedIndustries = null; + this.messageService.add({ + severity: 'success', + summary: 'Successful', + detail: 'Branche wurde erfolgreich gelöscht.', + life: 3000 + }); + }, + error: (err) => { + console.log('Error while deleting industries -- Error: ' + err + ' -- Industries: ', this.selectedIndustries); + this.messageService.add({ + severity: 'error', + summary: 'Fehler', + detail: 'Beim Löschen der Branche ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.', + life: 3000 + }); + } + }); + } + }); + } + + hideDialog() { + this.industryDialog = false; + this.submitted = false; + } + + deleteIndustry(industry: Industry) { + this.confirmationService.confirm({ + message: 'Sind Sie sicher, dass sie "' + industry.name + '" löschen möchten?', + acceptLabel: 'Ja', + acceptButtonStyleClass: 'p-button-danger', + rejectLabel: 'Nein', + header: 'Bitte bestätigen Sie', + icon: 'pi pi-exclamation-triangle', + accept: () => { + this.industryService.deleteIndustry(industry).subscribe({ + next: () => { + this.industries.set(this.industries().filter((val) => val.id !== industry.id)); + this.industry.set({}); + this.messageService.add({ + severity: 'success', + summary: 'Successful', + detail: 'Branche erfolgreich gelöscht', + life: 3000 + }); + }, + error: (err) => { + console.log('Error while deleting industry -- Error: ' + err + ' -- Industry: ', industry); + + this.messageService.add({ + severity: 'error', + summary: 'Fehler', + detail: 'Beim Löschen der Branche ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.', + life: 3000 + }); + } + }); + }, + reject: () => { + this.messageService.add({ + severity: 'info', + summary: 'Info', + detail: 'Der Löschvorgang wurde abgebrochen.', + life: 3000 + }) + } + }); + } + + findIndexById(id?: string): number { + let index = -1; + // console.log("Looking for id " + id); + for (let i = 0; i < this.industries().length; i++) { + // console.log("current item id: " + this.properties()[i].id) + if (this.industries()[i].id === id) { + // console.log("Index", this.properties()[i].id, i); + index = i; + break; + } + } + + return index; + } + + saveIndustry() { + this.submitted = true; + let _industry = this.industry(); + let _industries = this.industries(); + + console.log('Saving Industry', _industry); + + if (this.industry.name?.trim()) { + if (_industry.id) { + // update + this.industryService.updateIndustry(_industry).subscribe({ + next: (arg) => { + this.messageService.add({ + severity: 'success', + summary: 'Successful', + detail: 'Branche aktualisiert', + life: 3000 + }); + _industries[this.findIndexById(_industry.id)] = _industry; + this.industries.set([..._industries]); + }, + error: (err) => { + console.log('Error while updating industry -- Error: ' + err + ' -- Industry: ', _industry); + } + }); + } else { + this.industryService.createIndustry(_industry).subscribe({ + next: (arg) => { + this.messageService.add({ + severity: 'success', + summary: 'Successful', + detail: 'Branche angelegt', + life: 3000 + }); + this.industries.set([..._industries, arg]); + }, + error: (err) => { + console.log('Error while creating industry -- Error: ' + err + ' -- Industry: ', _industry); + } + }); + } + + this.industryDialog = false; + this.industry.set({}); + } + } + + + protected readonly environment = environment; +} diff --git a/src/app/pages/organizations/organizations.html b/src/app/pages/organizations/organizations.html new file mode 100644 index 0000000..311c8bd --- /dev/null +++ b/src/app/pages/organizations/organizations.html @@ -0,0 +1,156 @@ +
      +
      +
      +

      Organisationen

      +

      Übersicht über Organisationen

      +
      +
      + +
      + + @if (['dev', 'admin', 'can-create-organizations'] | isRoleAllowed: 'any') { + + + add + + + } +
      +
      + + + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + +
      + + +
      +
      + + +
      +
      + +
      + + +
      + + + + + + + +
      +
      + + + +
      + +
      + Name + +
      + + + +
      + + + +
      + +
      + Branche + +
      + + + +
      + + + +
      + +
      + Owner + +
      + + + +
      + + +
      + + + + {{ organization?.name }} + + + {{ organization?.industry?.name }} + + + {{ organization?.owner }} + + + + + + Keine Einträge gefunden. + + +
      +
      + +
      +
      + diff --git a/src/app/pages/organizations/organizations.scss b/src/app/pages/organizations/organizations.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/organizations/organizations.spec.ts b/src/app/pages/organizations/organizations.spec.ts new file mode 100644 index 0000000..620c570 --- /dev/null +++ b/src/app/pages/organizations/organizations.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Organizations } from './organizations'; + +describe('Organizations', () => { + let component: Organizations; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Organizations] + }) + .compileComponents(); + + fixture = TestBed.createComponent(Organizations); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/organizations/organizations.ts b/src/app/pages/organizations/organizations.ts new file mode 100644 index 0000000..dbf2292 --- /dev/null +++ b/src/app/pages/organizations/organizations.ts @@ -0,0 +1,120 @@ +import { Component } from '@angular/core'; +import { Button } from 'primeng/button'; +import { Dialog } from 'primeng/dialog'; +import { IconField } from 'primeng/iconfield'; +import { InputIcon } from 'primeng/inputicon'; +import { InputText } from 'primeng/inputtext'; +import { MessageService } from 'primeng/api'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { Select } from 'primeng/select'; +import { ProjectStatus } from '@/pages/service/project-status.service'; +import { Router } from '@angular/router'; +import { Table, TableModule } from 'primeng/table'; +import { Organization, OrganizationService } from '@/pages/service/organization.service'; +import { Industry, IndustryService } from '@/pages/service/industry.service'; +import { IsRoleAllowedPipe } from '@/pipes/is-role-allowed-pipe'; + +@Component({ + selector: 'app-organizations', + imports: [Button, Dialog, IconField, InputIcon, InputText, ReactiveFormsModule, Select, FormsModule, TableModule, IsRoleAllowedPipe], + templateUrl: './organizations.html', + styleUrl: './organizations.scss', + providers: [MessageService] +}) +export class Organizations { + organizations: Organization[] = []; + organizationIndustries: Industry[] = []; + projectStatuses: ProjectStatus[] = []; + addNewOrganizationDialogVisible = false; + + statuses: any; + measures: any; + + // for new organization dialog + organizationIndustryOptionsForNewOrganization: any = []; + newOrganizationName: string | undefined; + newOrganizationOwner: string | undefined; + newOrganizationIndustry: Industry | undefined; + protected loading: boolean = true; + protected searchValue: string | undefined; + + constructor( + private router: Router, + private organizationService: OrganizationService, + private industryService: IndustryService, + private messageService: MessageService + ) {} + + ngOnInit(): void { + this.statuses = [{ label: 'Alle', value: 'all' }, ...this.projectStatuses]; + + this.measures = [{ label: 'Alle', value: 'all' }, ...this.organizationIndustries]; + + this.organizationService.getOrganizations().subscribe((organizations) => { + console.debug('Organizations', organizations); + this.organizations = organizations; + this.loading = false; + }); + + this.industryService.getIndustries().subscribe((industries) => { + console.debug('Industries', industries); + this.organizationIndustries = industries; + }); + } + + navigateToDetails(id: string | undefined) { + this.router.navigate(['/projects', id]); + } + + clear(table: Table) { + table.clear(); + this.searchValue = ''; + } + + filterGlobal(table: Table, eventTarget: EventTarget | null) { + table.filterGlobal((eventTarget as HTMLInputElement).value, 'contains'); + } + + showDialog() { + this.industryService.getIndustries().subscribe((industries) => { + this.organizationIndustryOptionsForNewOrganization = []; + this.organizationIndustries.forEach((projectType) => { + this.organizationIndustryOptionsForNewOrganization.push({ + id: projectType.id, + name: projectType.name + }); + }); + }); + + this.organizationIndustryOptionsForNewOrganization = this.organizationIndustries; + this.addNewOrganizationDialogVisible = true; + } + + createNewOrganization() { + let newOrganization = this.organizationService.getOrganizationInstance(this.newOrganizationName, this.newOrganizationIndustry, this.newOrganizationOwner); + + this.organizationService.create(newOrganization).subscribe({ + next: (arg) => { + this.messageService.add({ + severity: 'success', + summary: 'Erfolgreich', + detail: 'Organisation erfolgreich angelegt', + life: 3000 + }); + + this.organizations = [...this.organizations, newOrganization]; + + this.addNewOrganizationDialogVisible = false; + }, + error: (err) => { + this.messageService.add({ + severity: 'danger', + summary: 'Fehler', + detail: 'Beim Anlegen der Organisation ist ein Fehler aufgetreten.', + life: 3000 + }); + console.log('Error while creating organization -- Error: ' + err + ' -- Organization: ', newOrganization); + } + }); + } +} diff --git a/src/app/pages/project-details/project-details.html b/src/app/pages/project-details/project-details.html index 85a43f5..0ce786e 100644 --- a/src/app/pages/project-details/project-details.html +++ b/src/app/pages/project-details/project-details.html @@ -2,8 +2,8 @@

      Projekt Details

      -

      {{ projectDetailsDTO?.name }}

      -

      Erstellt: {{ projectDetailsDTO?.createdAt | date: 'dd.MM.yyyy HH:mm' }}

      +

      {{ projectDetailsDTO.name }}

      +

      Erstellt: {{ projectDetailsDTO.createdAt | date: 'dd.MM.yyyy HH:mm' }}

      @@ -16,6 +16,7 @@
      +
      @@ -80,7 +81,8 @@ - + +
      apartment @@ -92,9 +94,23 @@ Hier können Projektnotizen und Dokumente verwaltet werden. + + + + +
      +
      diff --git a/src/app/pages/project-details/project-details.ts b/src/app/pages/project-details/project-details.ts index 7973891..f06844b 100644 --- a/src/app/pages/project-details/project-details.ts +++ b/src/app/pages/project-details/project-details.ts @@ -9,14 +9,13 @@ import { ActivatedRoute } from '@angular/router'; import { CurrencyPipe, DatePipe } from '@angular/common'; import { SafeHtmlPipe } from '@/pipes/safe-html-pipe'; import { ProjectType } from '@/pages/service/project-type.service'; +import { SafeUrlPipe } from '@/pipes/safe-url.pipe'; export interface ProjectTimelineEventDTO { id?: string; // UUID date?: Date; description?: string; documents?: string[]; - - projectEventType?: string; // impact on icon and color title?: string; } @@ -38,13 +37,17 @@ export interface ProjectDetailsDTO { @Component({ selector: 'app-project-details', - imports: [Button, Panel, Timeline, Card, Divider, CurrencyPipe, DatePipe, SafeHtmlPipe, SafeHtmlPipe], + imports: [Button, Panel, Timeline, Card, Divider, CurrencyPipe, DatePipe, SafeHtmlPipe, SafeUrlPipe], templateUrl: './project-details.html', styleUrl: './project-details.scss' }) export class ProjectDetails implements OnInit { events: any[] = []; - projectDetailsDTO: ProjectDetailsDTO | undefined; + projectDetailsDTO: ProjectDetailsDTO = { + address: "unknown", + amountRequested: -1, + timelineEvents: [], + }; constructor( private projectService: ProjectService, @@ -69,6 +72,17 @@ export class ProjectDetails implements OnInit { } protected readonly event = event; + + getMapUrl(address: string | undefined): string { + if (address === undefined || address.trim().length === 0) { + return ''; + } + + let result = `https://www.google.com/maps?q=${encodeURIComponent(address)}&output=embed`; + console.debug('getMapUrl', result); + + return result; + } } diff --git a/src/app/pages/projects/projects.ts b/src/app/pages/projects/projects.ts index a1e07b0..f643cd8 100644 --- a/src/app/pages/projects/projects.ts +++ b/src/app/pages/projects/projects.ts @@ -46,9 +46,6 @@ import { ProjectStatus, ProjectStatusService } from '@/pages/service/project-sta InputNumber, DatePipe, SafeHtmlPipe, - SafeHtmlPipe, - SafeHtmlPipe, - SafeHtmlPipe, CurrencyPipe ], templateUrl: './projects.html', diff --git a/src/app/pages/property-manager/property-manager.html b/src/app/pages/property-manager/property-manager.html index 1efa29c..dc2994a 100644 --- a/src/app/pages/property-manager/property-manager.html +++ b/src/app/pages/property-manager/property-manager.html @@ -29,7 +29,7 @@
      Verwalte Liegenschaften
      - +
      diff --git a/src/app/pages/service/industry.service.spec.ts b/src/app/pages/service/industry.service.spec.ts new file mode 100644 index 0000000..5b5dfe1 --- /dev/null +++ b/src/app/pages/service/industry.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { IndustryService } from './industry.service'; + +describe('IndustryService', () => { + let service: IndustryService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(IndustryService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/pages/service/industry.service.ts b/src/app/pages/service/industry.service.ts new file mode 100644 index 0000000..b44d485 --- /dev/null +++ b/src/app/pages/service/industry.service.ts @@ -0,0 +1,63 @@ +import { inject, Injectable } from '@angular/core'; +import { environment } from '../../../environments/environments'; +import { HttpClient } from '@angular/common/http'; +import { EMPTY, Observable } from 'rxjs'; +import { Organization } from '@/pages/service/organization.service'; +import { BulkDeleteIds, Property } from '@/pages/service/property.service'; + +export interface Industry { + id?: string; // UUID + name?: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class IndustryService { + apiEndpoint: string = environment.apiBaseUrl + '/industries'; + private http = inject(HttpClient); + + constructor() {} + + getIndustries(params?: any): Observable { + return this.http.get(this.apiEndpoint); + } + + deleteIndustry(industry: Industry): Observable { + console.debug(this + " -- args: ", arguments); + + return this.http.delete(this.apiEndpoint + "/" + industry.id) + } + + createIndustry(industry: Industry): Observable { + console.debug(this + " -- args: ", arguments); + + return this.http.post(this.apiEndpoint, industry) + } + + updateIndustry(industry: Industry): Observable { + console.debug(this + " -- args: ", arguments); + + return this.http.patch(this.apiEndpoint + "/" + industry.id, industry) + } + + deleteIndustries(industries: Industry[]): Observable { + console.debug(this + " -- args: ", arguments); + + if (!industries || industries.length === 0) { + return EMPTY; // Observable, which is "completed" immediately + } + + let ids : BulkDeleteIds = { + "ids": [] + }; + + industries.forEach(industry => { + if (industry.id !== undefined) { + ids.ids.push(industry.id); + } + }); + + return this.http.post(this.apiEndpoint + "/bulk-delete", ids) + } +} diff --git a/src/app/pages/service/organization.service.spec.ts b/src/app/pages/service/organization.service.spec.ts new file mode 100644 index 0000000..c6070f6 --- /dev/null +++ b/src/app/pages/service/organization.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { OrganizationService } from './organization.service'; + +describe('OrganizationService', () => { + let service: OrganizationService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(OrganizationService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/pages/service/organization.service.ts b/src/app/pages/service/organization.service.ts new file mode 100644 index 0000000..78b9b40 --- /dev/null +++ b/src/app/pages/service/organization.service.ts @@ -0,0 +1,42 @@ +import { inject, Injectable } from '@angular/core'; +import { environment } from '../../../environments/environments'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { Industry } from '@/pages/service/industry.service'; + +export interface Organization { + id?: string; // UUID + name?: string; + industry?: Industry; + owner?: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class OrganizationService { + apiEndpoint: string = environment.apiBaseUrl + '/organizations'; + private http = inject(HttpClient); + + constructor() {} + + getOrganizations(params?: any): Observable { + return this.http.get(this.apiEndpoint); + } + + create(organization: Organization): Observable { + return this.http.post(this.apiEndpoint, organization); + } + + getOrganizationInstance( + newOrganizationName?: string, + newOrganizationIndustry?: Industry, + newOrganizationOwner?: string + ) { + return { + name: newOrganizationName, + industry: newOrganizationIndustry, + owner: newOrganizationOwner + }; + } +} diff --git a/src/app/pages/service/property.service.ts b/src/app/pages/service/property.service.ts index 693decd..6797fea 100644 --- a/src/app/pages/service/property.service.ts +++ b/src/app/pages/service/property.service.ts @@ -4,7 +4,7 @@ import { EMPTY, Observable } from 'rxjs'; import { Attachment } from '@/pages/service/attachment.service'; import { environment } from '../../../environments/environments'; -export interface BulkDeletePropertyIds { +export interface BulkDeleteIds { ids: String[]; } @@ -237,7 +237,7 @@ export class PropertyService { return EMPTY; // Observable, which is "completed" immediately } - let ids : BulkDeletePropertyIds = { + let ids : BulkDeleteIds = { "ids": [] }; diff --git a/src/app/pipes/has-role-pipe.spec.ts b/src/app/pipes/has-role-pipe.spec.ts deleted file mode 100644 index a7660db..0000000 --- a/src/app/pipes/has-role-pipe.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { HasRolePipe } from './has-role-pipe'; - -describe('HasRolePipe', () => { - it('create an instance', () => { - const pipe = new HasRolePipe(); - expect(pipe).toBeTruthy(); - }); -}); diff --git a/src/app/pipes/has-role-pipe.ts b/src/app/pipes/is-role-allowed-pipe.ts similarity index 87% rename from src/app/pipes/has-role-pipe.ts rename to src/app/pipes/is-role-allowed-pipe.ts index 2ae7c6a..1af3319 100644 --- a/src/app/pipes/has-role-pipe.ts +++ b/src/app/pipes/is-role-allowed-pipe.ts @@ -2,10 +2,10 @@ import { Pipe, PipeTransform } from '@angular/core'; import Keycloak from 'keycloak-js'; @Pipe({ - name: 'hasRole', + name: 'isRoleAllowed', standalone: true }) -export class HasRolePipe implements PipeTransform { +export class IsRoleAllowedPipe implements PipeTransform { constructor(private keycloak: Keycloak) {} transform(allowed: string | string[], mode: 'any' | 'all' = 'any'): boolean { diff --git a/src/app/pipes/safe-url.pipe.ts b/src/app/pipes/safe-url.pipe.ts new file mode 100644 index 0000000..1690c47 --- /dev/null +++ b/src/app/pipes/safe-url.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; + +@Pipe({ name: 'safeUrl' }) +export class SafeUrlPipe implements PipeTransform { + constructor(private sanitizer: DomSanitizer) {} + + transform(url: string): SafeResourceUrl { + return this.sanitizer.bypassSecurityTrustResourceUrl(url); + } +} -- 2.49.1