autocomit
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package com.ldpv2.controller;
|
||||
|
||||
import com.ldpv2.dto.request.CreateBusinessUnitRequest;
|
||||
import com.ldpv2.dto.request.UpdateBusinessUnitRequest;
|
||||
import com.ldpv2.dto.response.BusinessUnitResponse;
|
||||
import com.ldpv2.service.BusinessUnitService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/business-units")
|
||||
@Tag(name = "Business Units", description = "Business unit management endpoints")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
public class BusinessUnitController {
|
||||
|
||||
@Autowired
|
||||
private BusinessUnitService businessUnitService;
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "Create business unit", description = "Create a new business unit")
|
||||
public ResponseEntity<BusinessUnitResponse> create(@Valid @RequestBody CreateBusinessUnitRequest request) {
|
||||
BusinessUnitResponse response = businessUnitService.create(request);
|
||||
return new ResponseEntity<>(response, HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@Operation(summary = "Update business unit", description = "Update an existing business unit")
|
||||
public ResponseEntity<BusinessUnitResponse> update(
|
||||
@PathVariable UUID id,
|
||||
@Valid @RequestBody UpdateBusinessUnitRequest request) {
|
||||
BusinessUnitResponse response = businessUnitService.update(id, request);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "Get business unit", description = "Get business unit by ID")
|
||||
public ResponseEntity<BusinessUnitResponse> getById(@PathVariable UUID id) {
|
||||
BusinessUnitResponse response = businessUnitService.findById(id);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "List business units", description = "Get paginated list of business units")
|
||||
public ResponseEntity<Page<BusinessUnitResponse>> getAll(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size,
|
||||
@RequestParam(defaultValue = "name") String sortBy,
|
||||
@RequestParam(defaultValue = "asc") String sortDirection) {
|
||||
|
||||
Sort sort = sortDirection.equalsIgnoreCase("desc")
|
||||
? Sort.by(sortBy).descending()
|
||||
: Sort.by(sortBy).ascending();
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
Page<BusinessUnitResponse> response = businessUnitService.findAll(pageable);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/search")
|
||||
@Operation(summary = "Search business units", description = "Search business units by name")
|
||||
public ResponseEntity<Page<BusinessUnitResponse>> search(
|
||||
@RequestParam String q,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size);
|
||||
Page<BusinessUnitResponse> response = businessUnitService.search(q, pageable);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Operation(summary = "Delete business unit", description = "Delete a business unit")
|
||||
public ResponseEntity<Void> delete(@PathVariable UUID id) {
|
||||
businessUnitService.delete(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.ldpv2.domain.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Business Unit entity representing organizational units
|
||||
*/
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "business_unit")
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class BusinessUnit extends BaseEntity {
|
||||
|
||||
@Column(nullable = false, unique = true, length = 255)
|
||||
private String name;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String description;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ldpv2.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CreateBusinessUnitRequest {
|
||||
|
||||
@NotBlank(message = "Name is required")
|
||||
@Size(max = 255, message = "Name must not exceed 255 characters")
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.ldpv2.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UpdateBusinessUnitRequest {
|
||||
|
||||
@Size(max = 255, message = "Name must not exceed 255 characters")
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ldpv2.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BusinessUnitResponse {
|
||||
private UUID id;
|
||||
private String name;
|
||||
private String description;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ldpv2.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BusinessUnitSummaryResponse {
|
||||
private UUID id;
|
||||
private String name;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.ldpv2.repository;
|
||||
|
||||
import com.ldpv2.domain.entity.BusinessUnit;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface BusinessUnitRepository extends JpaRepository<BusinessUnit, UUID> {
|
||||
Optional<BusinessUnit> findByName(String name);
|
||||
boolean existsByName(String name);
|
||||
Page<BusinessUnit> findByNameContainingIgnoreCase(String name, Pageable pageable);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.ldpv2.service;
|
||||
|
||||
import com.ldpv2.domain.entity.BusinessUnit;
|
||||
import com.ldpv2.dto.request.CreateBusinessUnitRequest;
|
||||
import com.ldpv2.dto.request.UpdateBusinessUnitRequest;
|
||||
import com.ldpv2.dto.response.BusinessUnitResponse;
|
||||
import com.ldpv2.exception.BadRequestException;
|
||||
import com.ldpv2.exception.ResourceNotFoundException;
|
||||
import com.ldpv2.repository.BusinessUnitRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class BusinessUnitService {
|
||||
|
||||
@Autowired
|
||||
private BusinessUnitRepository businessUnitRepository;
|
||||
|
||||
@Transactional
|
||||
public BusinessUnitResponse create(CreateBusinessUnitRequest request) {
|
||||
// Check if name already exists
|
||||
if (businessUnitRepository.existsByName(request.getName())) {
|
||||
throw new BadRequestException("Business unit with name '" + request.getName() + "' already exists");
|
||||
}
|
||||
|
||||
BusinessUnit businessUnit = new BusinessUnit();
|
||||
businessUnit.setName(request.getName());
|
||||
businessUnit.setDescription(request.getDescription());
|
||||
|
||||
businessUnit = businessUnitRepository.save(businessUnit);
|
||||
return mapToResponse(businessUnit);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public BusinessUnitResponse update(UUID id, UpdateBusinessUnitRequest request) {
|
||||
BusinessUnit businessUnit = businessUnitRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Business unit not found with id: " + id));
|
||||
|
||||
// Check if new name already exists (excluding current business unit)
|
||||
if (request.getName() != null && !request.getName().equals(businessUnit.getName())) {
|
||||
if (businessUnitRepository.existsByName(request.getName())) {
|
||||
throw new BadRequestException("Business unit with name '" + request.getName() + "' already exists");
|
||||
}
|
||||
businessUnit.setName(request.getName());
|
||||
}
|
||||
|
||||
if (request.getDescription() != null) {
|
||||
businessUnit.setDescription(request.getDescription());
|
||||
}
|
||||
|
||||
businessUnit = businessUnitRepository.save(businessUnit);
|
||||
return mapToResponse(businessUnit);
|
||||
}
|
||||
|
||||
public BusinessUnitResponse findById(UUID id) {
|
||||
BusinessUnit businessUnit = businessUnitRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Business unit not found with id: " + id));
|
||||
return mapToResponse(businessUnit);
|
||||
}
|
||||
|
||||
public Page<BusinessUnitResponse> findAll(Pageable pageable) {
|
||||
return businessUnitRepository.findAll(pageable).map(this::mapToResponse);
|
||||
}
|
||||
|
||||
public Page<BusinessUnitResponse> search(String query, Pageable pageable) {
|
||||
return businessUnitRepository.findByNameContainingIgnoreCase(query, pageable)
|
||||
.map(this::mapToResponse);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(UUID id) {
|
||||
if (!businessUnitRepository.existsById(id)) {
|
||||
throw new ResourceNotFoundException("Business unit not found with id: " + id);
|
||||
}
|
||||
businessUnitRepository.deleteById(id);
|
||||
}
|
||||
|
||||
private BusinessUnitResponse mapToResponse(BusinessUnit businessUnit) {
|
||||
return new BusinessUnitResponse(
|
||||
businessUnit.getId(),
|
||||
businessUnit.getName(),
|
||||
businessUnit.getDescription(),
|
||||
businessUnit.getCreatedAt(),
|
||||
businessUnit.getUpdatedAt()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
<insert tableName="users">
|
||||
<column name="username" value="admin"/>
|
||||
<!-- <column name="password" value="$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"/>-->
|
||||
<column name="password" value="$2a$12$ZDflpzoNCyt/hDu9PyMqE.jinzhLtQ8Cjc/NgvXU7BIxni.SH2PnW"/>
|
||||
<column name="password" value="$2a$12$mCGWGeNM3r11.yFhPFi22e./YQl2pRTIpJBVUwydScZioE3y6xm3m"/>
|
||||
<column name="email" value="admin@ldpv2.com"/>
|
||||
<column name="role" value="ADMIN"/>
|
||||
</insert>
|
||||
|
||||
@@ -10,4 +10,7 @@
|
||||
<include file="db/changelog/v1.0/002-create-environment-table.xml"/>
|
||||
<include file="db/changelog/data/initial-data.xml"/>
|
||||
|
||||
<!-- Story 1: Business Units -->
|
||||
<include file="db/changelog/v1.0/003-create-business-unit-table.xml"/>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||
|
||||
<changeSet id="003-create-business-unit-table" author="ldpv2-team">
|
||||
|
||||
<!-- Create business_unit table -->
|
||||
<createTable tableName="business_unit">
|
||||
<column name="id" type="UUID" defaultValueComputed="uuid_generate_v4()">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<column name="name" type="VARCHAR(255)">
|
||||
<constraints nullable="false" unique="true"/>
|
||||
</column>
|
||||
<column name="description" type="TEXT"/>
|
||||
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="updated_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
|
||||
<!-- Create index on name for search performance -->
|
||||
<createIndex tableName="business_unit" indexName="idx_business_unit_name">
|
||||
<column name="name"/>
|
||||
</createIndex>
|
||||
|
||||
<!-- Insert sample data for testing -->
|
||||
<insert tableName="business_unit">
|
||||
<column name="name" value="Digital Services"/>
|
||||
<column name="description" value="Digital transformation initiatives and online services"/>
|
||||
</insert>
|
||||
|
||||
<insert tableName="business_unit">
|
||||
<column name="name" value="Operations"/>
|
||||
<column name="description" value="Core business operations and support"/>
|
||||
</insert>
|
||||
|
||||
<insert tableName="business_unit">
|
||||
<column name="name" value="Finance"/>
|
||||
<column name="description" value="Financial services and accounting systems"/>
|
||||
</insert>
|
||||
|
||||
<insert tableName="business_unit">
|
||||
<column name="name" value="Human Resources"/>
|
||||
<column name="description" value="HR management and employee services"/>
|
||||
</insert>
|
||||
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -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],
|
||||
|
||||
+38
@@ -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>
|
||||
+95
@@ -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;
|
||||
}
|
||||
}
|
||||
+68
@@ -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']);
|
||||
}
|
||||
}
|
||||
+39
@@ -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>
|
||||
+90
@@ -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;
|
||||
}
|
||||
+80
@@ -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']);
|
||||
}
|
||||
}
|
||||
+58
@@ -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>
|
||||
+140
@@ -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;
|
||||
}
|
||||
}
|
||||
+136
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user