Several fixes

- added organizations
- added industries
- added logo in 2 colors for light and dark theme
- improved authorization to allow multi tenancy
This commit is contained in:
Murat Özkorkmaz
2025-11-13 19:56:50 +01:00
parent 5d029221db
commit e901aefbf5
28 changed files with 997 additions and 134 deletions

View File

@@ -0,0 +1,156 @@
<div class="flex">
<div class="flex-1 text-left">
<div class="mb-5">
<h1>Organisationen</h1>
<p class="my-4 text-lg text-gray-500">Übersicht über Organisationen</p>
</div>
</div>
<div class="flex-1 text-right">
@if (['dev', 'admin', 'can-create-organizations'] | isRoleAllowed: 'any') {
<p-button (click)="showDialog()" label="Neue Organisation" class="p-button-outlined ml-4">
<span class="flex items-center gap-2">
<span class="material-icons !text-base">add</span>
</span>
</p-button>
}
</div>
</div>
<p-dialog header="Neue Organisation"
[modal]="true"
[(visible)]="addNewOrganizationDialogVisible" [style]="{ width: '50rem' }">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div class="flex flex-col">
<label for="organization_name" class="mb-2 font-semibold">Organisations Name</label>
<input [(ngModel)]="newOrganizationName"
[required]="true"
pInputText
id="organization_name"
class="w-full"
autocomplete="on" />
</div>
<div class="flex flex-col">
<label for="organization_owner" class="mb-2 font-semibold">Organisations Owner</label>
<input [(ngModel)]="newOrganizationOwner"
[required]="true"
pInputText
id="organization_owner"
class="w-full"
autocomplete="on" />
</div>
<div class="col-span-1 md:col-span-2 flex flex-col">
<label for="organization_industry" class="mb-2 font-semibold">Organisations-Branche</label>
<p-select [options]="organizationIndustryOptionsForNewOrganization"
[(ngModel)]="newOrganizationIndustry"
[required]="true"
appendTo="body"
optionLabel="name"
id="organization_industry" class="w-full"></p-select>
</div>
</div>
<div class="flex justify-end gap-2">
<p-button label="Cancel" severity="secondary" (click)="addNewOrganizationDialogVisible = false" />
<p-button label="Save" (click)="createNewOrganization()" />
</div>
</p-dialog>
<!-- Main content -->
<div class="grid grid-cols-12 gap-8 mb-8">
<div class="col-span-12 ">
<div class="card">
<p-table #dt1
[value]="organizations"
dataKey="id"
[rows]="10"
[rowsPerPageOptions]="[10, 25, 50, 100]"
[loading]="loading"
[paginator]="true"
[globalFilterFields]="['name', 'country.name', 'representative.name', 'status']"
>
<ng-template #caption>
<div class="flex">
<p-button label="Clear" [outlined]="true" icon="pi pi-filter-slash" (click)="clear(dt1)" />
<p-iconfield iconPosition="left" class="ml-auto">
<p-inputicon>
<i class="pi pi-search"></i>
</p-inputicon>
<input pInputText type="text" [(ngModel)]="searchValue" (input)="filterGlobal(dt1, $event.target)" placeholder="Suche nach..." />
</p-iconfield>
</div>
</ng-template>
<ng-template #header>
<tr>
<th pSortableColumn="name" style="width:50%;">
<div class="flex items-center justify-between w-full">
<!-- Linker Bereich: Text + SortIcon -->
<div class="flex items-center gap-1">
<span>Name</span>
<p-sortIcon field="name"></p-sortIcon>
</div>
<!-- Rechter Bereich: Filter -->
<p-columnFilter type="text" field="name" display="menu"></p-columnFilter>
</div>
</th>
<th pSortableColumn="industry.name" style="width:35%;">
<div class="flex items-center justify-between w-full">
<!-- Linker Bereich: Text + SortIcon -->
<div class="flex items-center gap-1">
<span>Branche</span>
<p-sortIcon field="industry.name"></p-sortIcon>
</div>
<!-- Rechter Bereich: Filter -->
<p-columnFilter type="text" field="industry.name" display="menu"></p-columnFilter>
</div>
</th>
<th pSortableColumn="owner" style="width:15%;">
<div class="flex items-center justify-between w-full">
<!-- Linker Bereich: Text + SortIcon -->
<div class="flex items-center gap-1">
<span>Owner</span>
<p-sortIcon field="owner"></p-sortIcon>
</div>
<!-- Rechter Bereich: Filter -->
<p-columnFilter type="text" field="owner" display="menu"></p-columnFilter>
</div>
</th>
</tr>
</ng-template>
<ng-template #body let-organization>
<tr>
<td>
{{ organization?.name }}
</td>
<td>
{{ organization?.industry?.name }}
</td>
<td>
{{ organization?.owner }}
</td>
</tr>
</ng-template>
<ng-template #emptymessage>
<tr>
<td colspan="7">Keine Einträge gefunden.</td>
</tr>
</ng-template>
</p-table>
</div>
</div>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Organizations } from './organizations';
describe('Organizations', () => {
let component: Organizations;
let fixture: ComponentFixture<Organizations>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Organizations]
})
.compileComponents();
fixture = TestBed.createComponent(Organizations);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -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);
}
});
}
}