autocomit

This commit is contained in:
2026-02-08 10:06:41 +01:00
parent c6bab3bd68
commit b69ff3965b
23 changed files with 1203 additions and 2 deletions
+27 -1
View File
@@ -4,13 +4,39 @@ import { authGuard } from './core/guards/auth.guard';
export const routes: Routes = [
{
path: '',
redirectTo: '/environments',
redirectTo: '/business-units',
pathMatch: 'full'
},
{
path: 'login',
loadComponent: () => import('./core/auth/login/login.component').then(m => m.LoginComponent)
},
{
path: 'business-units',
canActivate: [authGuard],
children: [
{
path: '',
loadComponent: () => import('./features/business-units/business-unit-list/business-unit-list.component')
.then(m => m.BusinessUnitListComponent)
},
{
path: 'new',
loadComponent: () => import('./features/business-units/business-unit-form/business-unit-form.component')
.then(m => m.BusinessUnitFormComponent)
},
{
path: ':id',
loadComponent: () => import('./features/business-units/business-unit-detail/business-unit-detail.component')
.then(m => m.BusinessUnitDetailComponent)
},
{
path: ':id/edit',
loadComponent: () => import('./features/business-units/business-unit-form/business-unit-form.component')
.then(m => m.BusinessUnitFormComponent)
}
]
},
{
path: 'environments',
canActivate: [authGuard],
@@ -0,0 +1,38 @@
<div class="container">
<div *ngIf="loading" class="loading">Loading...</div>
<div *ngIf="error" class="error">{{ error }}</div>
<div *ngIf="businessUnit && !loading" class="detail-card">
<div class="header">
<h1>{{ businessUnit.name }}</h1>
<div class="actions">
<button (click)="edit()" class="btn-primary">Edit</button>
<button (click)="delete()" class="btn-danger">Delete</button>
</div>
</div>
<div class="details">
<div class="detail-row">
<label>Name:</label>
<span>{{ businessUnit.name }}</span>
</div>
<div class="detail-row">
<label>Description:</label>
<span>{{ businessUnit.description || '-' }}</span>
</div>
<div class="detail-row">
<label>Created:</label>
<span>{{ businessUnit.createdAt | date:'medium' }}</span>
</div>
<div class="detail-row">
<label>Last Updated:</label>
<span>{{ businessUnit.updatedAt | date:'medium' }}</span>
</div>
</div>
<button (click)="back()" class="btn-secondary">Back to List</button>
</div>
</div>
@@ -0,0 +1,95 @@
.container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.loading, .error {
text-align: center;
padding: 2rem;
}
.error {
color: #f44336;
}
.detail-card {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #f5f5f5;
h1 {
margin: 0;
}
.actions {
display: flex;
gap: 0.5rem;
}
}
.details {
margin-bottom: 2rem;
}
.detail-row {
display: flex;
padding: 1rem 0;
border-bottom: 1px solid #f5f5f5;
label {
font-weight: 600;
width: 200px;
color: #555;
}
span {
flex: 1;
color: #333;
}
}
.btn-primary, .btn-secondary, .btn-danger {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.btn-primary {
background-color: #3f51b5;
color: white;
&:hover {
background-color: #303f9f;
}
}
.btn-secondary {
background-color: #f5f5f5;
color: #333;
&:hover {
background-color: #e0e0e0;
}
}
.btn-danger {
background-color: #f44336;
color: white;
&:hover {
background-color: #d32f2f;
}
}
@@ -0,0 +1,68 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router, ActivatedRoute } from '@angular/router';
import { BusinessUnitService } from '../business-unit.service';
import { BusinessUnit } from '../../../shared/models/business-unit.model';
@Component({
selector: 'app-business-unit-detail',
standalone: true,
imports: [CommonModule],
templateUrl: './business-unit-detail.component.html',
styleUrls: ['./business-unit-detail.component.scss']
})
export class BusinessUnitDetailComponent implements OnInit {
businessUnit?: BusinessUnit;
loading = false;
error = '';
constructor(
private businessUnitService: BusinessUnitService,
private router: Router,
private route: ActivatedRoute
) {}
ngOnInit(): void {
const id = this.route.snapshot.paramMap.get('id');
if (id) {
this.loadBusinessUnit(id);
}
}
loadBusinessUnit(id: string): void {
this.loading = true;
this.businessUnitService.getBusinessUnit(id).subscribe({
next: (bu) => {
this.businessUnit = bu;
this.loading = false;
},
error: (err) => {
this.error = 'Failed to load business unit';
this.loading = false;
}
});
}
edit(): void {
if (this.businessUnit) {
this.router.navigate(['/business-units', this.businessUnit.id, 'edit']);
}
}
delete(): void {
if (this.businessUnit && confirm('Are you sure you want to delete this business unit?')) {
this.businessUnitService.deleteBusinessUnit(this.businessUnit.id).subscribe({
next: () => {
this.router.navigate(['/business-units']);
},
error: (err) => {
this.error = 'Failed to delete business unit';
}
});
}
}
back(): void {
this.router.navigate(['/business-units']);
}
}
@@ -0,0 +1,39 @@
<div class="container">
<h1>{{ isEditMode ? 'Edit Business Unit' : 'Create New Business Unit' }}</h1>
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="name">Name *</label>
<input
id="name"
type="text"
formControlName="name"
[class.error]="form.get('name')?.invalid && form.get('name')?.touched"
/>
<div class="error-message" *ngIf="form.get('name')?.invalid && form.get('name')?.touched">
<span *ngIf="form.get('name')?.errors?.['required']">Name is required</span>
<span *ngIf="form.get('name')?.errors?.['maxlength']">Name must not exceed 255 characters</span>
</div>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea
id="description"
formControlName="description"
rows="4"
></textarea>
</div>
<div class="error-message" *ngIf="error">
{{ error }}
</div>
<div class="form-actions">
<button type="button" (click)="cancel()" class="btn-secondary">Cancel</button>
<button type="submit" [disabled]="form.invalid || loading" class="btn-primary">
{{ loading ? 'Saving...' : 'Save' }}
</button>
</div>
</form>
</div>
@@ -0,0 +1,90 @@
.container {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
h1 {
margin-bottom: 2rem;
}
}
form {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 1.5rem;
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #555;
}
input[type="text"],
textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
font-family: inherit;
&:focus {
outline: none;
border-color: #3f51b5;
}
&.error {
border-color: #f44336;
}
}
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
}
.btn-primary, .btn-secondary {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.btn-primary {
background-color: #3f51b5;
color: white;
&:hover:not(:disabled) {
background-color: #303f9f;
}
&:disabled {
background-color: #ccc;
cursor: not-allowed;
}
}
.btn-secondary {
background-color: #f5f5f5;
color: #333;
&:hover {
background-color: #e0e0e0;
}
}
.error-message {
color: #f44336;
font-size: 0.875rem;
margin-top: 0.25rem;
}
@@ -0,0 +1,80 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router';
import { BusinessUnitService } from '../business-unit.service';
@Component({
selector: 'app-business-unit-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
templateUrl: './business-unit-form.component.html',
styleUrls: ['./business-unit-form.component.scss']
})
export class BusinessUnitFormComponent implements OnInit {
form: FormGroup;
loading = false;
error = '';
isEditMode = false;
businessUnitId?: string;
constructor(
private fb: FormBuilder,
private businessUnitService: BusinessUnitService,
private router: Router,
private route: ActivatedRoute
) {
this.form = this.fb.group({
name: ['', [Validators.required, Validators.maxLength(255)]],
description: ['']
});
}
ngOnInit(): void {
this.businessUnitId = this.route.snapshot.paramMap.get('id') || undefined;
this.isEditMode = !!this.businessUnitId;
if (this.isEditMode && this.businessUnitId) {
this.loadBusinessUnit(this.businessUnitId);
}
}
loadBusinessUnit(id: string): void {
this.loading = true;
this.businessUnitService.getBusinessUnit(id).subscribe({
next: (bu) => {
this.form.patchValue(bu);
this.loading = false;
},
error: (err) => {
this.error = 'Failed to load business unit';
this.loading = false;
}
});
}
onSubmit(): void {
if (this.form.valid) {
this.loading = true;
this.error = '';
const request$ = this.isEditMode && this.businessUnitId
? this.businessUnitService.updateBusinessUnit(this.businessUnitId, this.form.value)
: this.businessUnitService.createBusinessUnit(this.form.value);
request$.subscribe({
next: () => {
this.router.navigate(['/business-units']);
},
error: (err) => {
this.error = err.error?.message || 'Failed to save business unit';
this.loading = false;
}
});
}
}
cancel(): void {
this.router.navigate(['/business-units']);
}
}
@@ -0,0 +1,58 @@
<div class="container">
<div class="header">
<h1>Business Units</h1>
<button (click)="createNew()" class="btn-primary">Create New Business Unit</button>
</div>
<div class="search-bar">
<input
type="text"
[(ngModel)]="searchQuery"
(ngModelChange)="onSearchChange($event)"
placeholder="Search business units..."
class="search-input"
/>
</div>
<div *ngIf="loading" class="loading">Loading...</div>
<div *ngIf="error" class="error">{{ error }}</div>
<div *ngIf="!loading && businessUnits.length > 0" class="table-container">
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let bu of businessUnits">
<td><strong>{{ bu.name }}</strong></td>
<td>{{ bu.description || '-' }}</td>
<td>{{ bu.createdAt | date:'short' }}</td>
<td class="actions">
<button (click)="viewDetails(bu.id)" class="btn-sm">View</button>
<button (click)="edit(bu.id)" class="btn-sm">Edit</button>
<button (click)="delete(bu.id)" class="btn-sm btn-danger">Delete</button>
</td>
</tr>
</tbody>
</table>
</div>
<div *ngIf="!loading && businessUnits.length === 0 && !searchQuery" class="empty">
No business units found. Click "Create New Business Unit" to get started.
</div>
<div *ngIf="!loading && businessUnits.length === 0 && searchQuery" class="empty">
No business units match your search "{{ searchQuery }}".
</div>
<div *ngIf="totalPages > 1" class="pagination">
<button (click)="previousPage()" [disabled]="page === 0">Previous</button>
<span>Page {{ page + 1 }} of {{ totalPages }} ({{ totalElements }} total)</span>
<button (click)="nextPage()" [disabled]="page >= totalPages - 1">Next</button>
</div>
</div>
@@ -0,0 +1,140 @@
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
h1 {
margin: 0;
}
}
.search-bar {
margin-bottom: 1.5rem;
.search-input {
width: 100%;
max-width: 400px;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
&:focus {
outline: none;
border-color: #3f51b5;
}
}
}
.btn-primary {
background-color: #3f51b5;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: #303f9f;
}
}
.loading, .error, .empty {
text-align: center;
padding: 2rem;
color: #666;
}
.error {
color: #f44336;
}
.table-container {
overflow-x: auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
table {
width: 100%;
border-collapse: collapse;
th, td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f5f5f5;
font-weight: 600;
}
tbody tr:hover {
background-color: #f9f9f9;
}
}
.actions {
display: flex;
gap: 0.5rem;
}
.btn-sm {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
background-color: #2196f3;
color: white;
font-size: 0.875rem;
&:hover {
background-color: #1976d2;
}
&.btn-danger {
background-color: #f44336;
&:hover {
background-color: #d32f2f;
}
}
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin-top: 2rem;
button {
padding: 0.5rem 1rem;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
&:hover:not(:disabled) {
background-color: #f5f5f5;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
span {
color: #666;
}
}
@@ -0,0 +1,136 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { debounceTime, distinctUntilChanged, Subject } from 'rxjs';
import { BusinessUnitService } from '../business-unit.service';
import { BusinessUnit } from '../../../shared/models/business-unit.model';
import { Page } from '../../../shared/models/environment.model';
@Component({
selector: 'app-business-unit-list',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './business-unit-list.component.html',
styleUrls: ['./business-unit-list.component.scss']
})
export class BusinessUnitListComponent implements OnInit {
businessUnits: BusinessUnit[] = [];
loading = false;
error = '';
page = 0;
size = 20;
totalElements = 0;
totalPages = 0;
searchQuery = '';
private searchSubject = new Subject<string>();
constructor(
private businessUnitService: BusinessUnitService,
private router: Router
) {
// Setup search debounce
this.searchSubject.pipe(
debounceTime(300),
distinctUntilChanged()
).subscribe(query => {
this.performSearch(query);
});
}
ngOnInit(): void {
this.loadBusinessUnits();
}
loadBusinessUnits(): void {
this.loading = true;
this.businessUnitService.getBusinessUnits(this.page, this.size).subscribe({
next: (data: Page<BusinessUnit>) => {
this.businessUnits = data.content;
this.totalElements = data.totalElements;
this.totalPages = data.totalPages;
this.loading = false;
},
error: (err) => {
this.error = 'Failed to load business units';
this.loading = false;
}
});
}
onSearchChange(query: string): void {
this.searchQuery = query;
this.searchSubject.next(query);
}
performSearch(query: string): void {
if (!query || query.trim() === '') {
this.page = 0;
this.loadBusinessUnits();
return;
}
this.loading = true;
this.businessUnitService.searchBusinessUnits(query, this.page, this.size).subscribe({
next: (data: Page<BusinessUnit>) => {
this.businessUnits = data.content;
this.totalElements = data.totalElements;
this.totalPages = data.totalPages;
this.loading = false;
},
error: (err) => {
this.error = 'Search failed';
this.loading = false;
}
});
}
createNew(): void {
this.router.navigate(['/business-units/new']);
}
viewDetails(id: string): void {
this.router.navigate(['/business-units', id]);
}
edit(id: string): void {
this.router.navigate(['/business-units', id, 'edit']);
}
delete(id: string): void {
if (confirm('Are you sure you want to delete this business unit?')) {
this.businessUnitService.deleteBusinessUnit(id).subscribe({
next: () => {
this.loadBusinessUnits();
},
error: (err) => {
this.error = 'Failed to delete business unit';
}
});
}
}
nextPage(): void {
if (this.page < this.totalPages - 1) {
this.page++;
if (this.searchQuery) {
this.performSearch(this.searchQuery);
} else {
this.loadBusinessUnits();
}
}
}
previousPage(): void {
if (this.page > 0) {
this.page--;
if (this.searchQuery) {
this.performSearch(this.searchQuery);
} else {
this.loadBusinessUnits();
}
}
}
}
@@ -0,0 +1,58 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import {
BusinessUnit,
CreateBusinessUnitRequest,
UpdateBusinessUnitRequest
} from '../../shared/models/business-unit.model';
import { Page } from '../../shared/models/environment.model';
@Injectable({
providedIn: 'root'
})
export class BusinessUnitService {
private readonly API_URL = '/api/business-units';
constructor(private http: HttpClient) {}
getBusinessUnits(
page: number = 0,
size: number = 20,
sortBy: string = 'name',
sortDirection: string = 'asc'
): Observable<Page<BusinessUnit>> {
const params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString())
.set('sortBy', sortBy)
.set('sortDirection', sortDirection);
return this.http.get<Page<BusinessUnit>>(this.API_URL, { params });
}
searchBusinessUnits(query: string, page: number = 0, size: number = 20): Observable<Page<BusinessUnit>> {
const params = new HttpParams()
.set('q', query)
.set('page', page.toString())
.set('size', size.toString());
return this.http.get<Page<BusinessUnit>>(`${this.API_URL}/search`, { params });
}
getBusinessUnit(id: string): Observable<BusinessUnit> {
return this.http.get<BusinessUnit>(`${this.API_URL}/${id}`);
}
createBusinessUnit(data: CreateBusinessUnitRequest): Observable<BusinessUnit> {
return this.http.post<BusinessUnit>(this.API_URL, data);
}
updateBusinessUnit(id: string, data: UpdateBusinessUnitRequest): Observable<BusinessUnit> {
return this.http.put<BusinessUnit>(`${this.API_URL}/${id}`, data);
}
deleteBusinessUnit(id: string): Observable<void> {
return this.http.delete<void>(`${this.API_URL}/${id}`);
}
}
@@ -0,0 +1,22 @@
export interface BusinessUnit {
id: string;
name: string;
description?: string;
createdAt: Date;
updatedAt: Date;
}
export interface BusinessUnitSummary {
id: string;
name: string;
}
export interface CreateBusinessUnitRequest {
name: string;
description?: string;
}
export interface UpdateBusinessUnitRequest {
name?: string;
description?: string;
}