#!/bin/bash # ============================================================================ # LDPv2 - Story 2: Applications Implementation + Dashboard # ============================================================================ echo "🚀 Starting deployment of Story 2 (Applications) and Dashboard..." BASE_BACKEND="backend/src/main/java/com/ldpv2" BASE_FRONTEND="frontend/src/app" BASE_RESOURCES="backend/src/main/resources" # ============================================================================ # BACKEND - Database Migration # ============================================================================ echo "📦 Creating database migration for Application entity..." mkdir -p "$BASE_RESOURCES/db/changelog/v1.0" cat > "$BASE_RESOURCES/db/changelog/v1.0/004-create-application-table.xml" << 'XML' XML # Update master changelog cat > "$BASE_RESOURCES/db/changelog/db.changelog-master.xml" << 'XML' XML # ============================================================================ # BACKEND - Enum # ============================================================================ echo "📦 Creating ApplicationStatus enum..." mkdir -p "$BASE_BACKEND/domain/enums" cat > "$BASE_BACKEND/domain/enums/ApplicationStatus.java" << 'JAVA' package com.ldpv2.domain.enums; public enum ApplicationStatus { IDEA("Idea"), IN_DEVELOPMENT("In Development"), IN_SERVICE("In Service"), MAINTENANCE("Maintenance"), DECOMMISSIONED("Decommissioned"); private final String displayName; ApplicationStatus(String displayName) { this.displayName = displayName; } public String getDisplayName() { return displayName; } } JAVA # ============================================================================ # BACKEND - Entity # ============================================================================ echo "📦 Creating Application entity..." cat > "$BASE_BACKEND/domain/entity/Application.java" << 'JAVA' package com.ldpv2.domain.entity; import com.ldpv2.domain.enums.ApplicationStatus; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import java.time.LocalDate; /** * Application entity representing software systems */ @Data @Entity @Table(name = "application") @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(callSuper = true) public class Application extends BaseEntity { @Column(nullable = false, length = 255) private String name; @Column(columnDefinition = "TEXT") private String description; @Enumerated(EnumType.STRING) @Column(nullable = false, length = 50) private ApplicationStatus status; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "business_unit_id", nullable = false) private BusinessUnit businessUnit; @Column(name = "end_of_life_date") private LocalDate endOfLifeDate; @Column(name = "end_of_support_date") private LocalDate endOfSupportDate; } JAVA # ============================================================================ # BACKEND - Repository # ============================================================================ echo "📦 Creating Application repository..." mkdir -p "$BASE_BACKEND/repository" cat > "$BASE_BACKEND/repository/ApplicationRepository.java" << 'JAVA' package com.ldpv2.repository; import com.ldpv2.domain.entity.Application; import com.ldpv2.domain.enums.ApplicationStatus; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.UUID; @Repository public interface ApplicationRepository extends JpaRepository { Page findByStatus(ApplicationStatus status, Pageable pageable); Page findByBusinessUnitId(UUID businessUnitId, Pageable pageable); Page findByNameContainingIgnoreCase(String name, Pageable pageable); Page findByStatusAndBusinessUnitId(ApplicationStatus status, UUID businessUnitId, Pageable pageable); @Query("SELECT a FROM Application a WHERE " + "(:status IS NULL OR a.status = :status) AND " + "(:businessUnitId IS NULL OR a.businessUnit.id = :businessUnitId) AND " + "(:name IS NULL OR LOWER(a.name) LIKE LOWER(CONCAT('%', :name, '%')))") Page search( @Param("status") ApplicationStatus status, @Param("businessUnitId") UUID businessUnitId, @Param("name") String name, Pageable pageable ); } JAVA # ============================================================================ # BACKEND - DTOs # ============================================================================ echo "📦 Creating Application DTOs..." mkdir -p "$BASE_BACKEND/dto/request" mkdir -p "$BASE_BACKEND/dto/response" cat > "$BASE_BACKEND/dto/request/CreateApplicationRequest.java" << 'JAVA' package com.ldpv2.dto.request; import com.ldpv2.domain.enums.ApplicationStatus; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDate; import java.util.UUID; @Data @NoArgsConstructor @AllArgsConstructor public class CreateApplicationRequest { @NotBlank(message = "Name is required") @Size(max = 255, message = "Name must not exceed 255 characters") private String name; private String description; @NotNull(message = "Status is required") private ApplicationStatus status; @NotNull(message = "Business unit is required") private UUID businessUnitId; private LocalDate endOfLifeDate; private LocalDate endOfSupportDate; } JAVA cat > "$BASE_BACKEND/dto/request/UpdateApplicationRequest.java" << 'JAVA' package com.ldpv2.dto.request; import com.ldpv2.domain.enums.ApplicationStatus; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDate; import java.util.UUID; @Data @NoArgsConstructor @AllArgsConstructor public class UpdateApplicationRequest { @Size(max = 255, message = "Name must not exceed 255 characters") private String name; private String description; private ApplicationStatus status; private UUID businessUnitId; private LocalDate endOfLifeDate; private LocalDate endOfSupportDate; } JAVA cat > "$BASE_BACKEND/dto/response/ApplicationResponse.java" << 'JAVA' package com.ldpv2.dto.response; import com.ldpv2.domain.enums.ApplicationStatus; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.UUID; @Data @NoArgsConstructor @AllArgsConstructor public class ApplicationResponse { private UUID id; private String name; private String description; private ApplicationStatus status; private BusinessUnitSummaryResponse businessUnit; private LocalDate endOfLifeDate; private LocalDate endOfSupportDate; private LocalDateTime createdAt; private LocalDateTime updatedAt; } JAVA cat > "$BASE_BACKEND/dto/response/ApplicationSummaryResponse.java" << 'JAVA' package com.ldpv2.dto.response; import com.ldpv2.domain.enums.ApplicationStatus; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.UUID; @Data @NoArgsConstructor @AllArgsConstructor public class ApplicationSummaryResponse { private UUID id; private String name; private ApplicationStatus status; private String businessUnitName; } JAVA # ============================================================================ # BACKEND - Service # ============================================================================ echo "📦 Creating Application service..." cat > "$BASE_BACKEND/service/ApplicationService.java" << 'JAVA' package com.ldpv2.service; import com.ldpv2.domain.entity.Application; import com.ldpv2.domain.entity.BusinessUnit; import com.ldpv2.domain.enums.ApplicationStatus; import com.ldpv2.dto.request.CreateApplicationRequest; import com.ldpv2.dto.request.UpdateApplicationRequest; import com.ldpv2.dto.response.ApplicationResponse; import com.ldpv2.dto.response.BusinessUnitSummaryResponse; import com.ldpv2.exception.BadRequestException; import com.ldpv2.exception.ResourceNotFoundException; import com.ldpv2.repository.ApplicationRepository; 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 ApplicationService { @Autowired private ApplicationRepository applicationRepository; @Autowired private BusinessUnitRepository businessUnitRepository; @Transactional public ApplicationResponse create(CreateApplicationRequest request) { // Validate business unit exists BusinessUnit businessUnit = businessUnitRepository.findById(request.getBusinessUnitId()) .orElseThrow(() -> new ResourceNotFoundException( "Business unit not found with id: " + request.getBusinessUnitId())); // Validate dates if both are provided if (request.getEndOfSupportDate() != null && request.getEndOfLifeDate() != null) { if (request.getEndOfSupportDate().isAfter(request.getEndOfLifeDate())) { throw new BadRequestException( "End of support date must be before end of life date"); } } Application application = new Application(); application.setName(request.getName()); application.setDescription(request.getDescription()); application.setStatus(request.getStatus()); application.setBusinessUnit(businessUnit); application.setEndOfLifeDate(request.getEndOfLifeDate()); application.setEndOfSupportDate(request.getEndOfSupportDate()); application = applicationRepository.save(application); return mapToResponse(application); } @Transactional public ApplicationResponse update(UUID id, UpdateApplicationRequest request) { Application application = applicationRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException( "Application not found with id: " + id)); if (request.getName() != null) { application.setName(request.getName()); } if (request.getDescription() != null) { application.setDescription(request.getDescription()); } if (request.getStatus() != null) { application.setStatus(request.getStatus()); } if (request.getBusinessUnitId() != null) { BusinessUnit businessUnit = businessUnitRepository.findById(request.getBusinessUnitId()) .orElseThrow(() -> new ResourceNotFoundException( "Business unit not found with id: " + request.getBusinessUnitId())); application.setBusinessUnit(businessUnit); } if (request.getEndOfLifeDate() != null) { application.setEndOfLifeDate(request.getEndOfLifeDate()); } if (request.getEndOfSupportDate() != null) { application.setEndOfSupportDate(request.getEndOfSupportDate()); } // Validate dates if both are set if (application.getEndOfSupportDate() != null && application.getEndOfLifeDate() != null) { if (application.getEndOfSupportDate().isAfter(application.getEndOfLifeDate())) { throw new BadRequestException( "End of support date must be before end of life date"); } } application = applicationRepository.save(application); return mapToResponse(application); } @Transactional public ApplicationResponse updateStatus(UUID id, ApplicationStatus newStatus) { Application application = applicationRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException( "Application not found with id: " + id)); application.setStatus(newStatus); application = applicationRepository.save(application); return mapToResponse(application); } public ApplicationResponse findById(UUID id) { Application application = applicationRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException( "Application not found with id: " + id)); return mapToResponse(application); } public Page findAll(Pageable pageable) { return applicationRepository.findAll(pageable).map(this::mapToResponse); } public Page findByStatus(ApplicationStatus status, Pageable pageable) { return applicationRepository.findByStatus(status, pageable).map(this::mapToResponse); } public Page findByBusinessUnit(UUID businessUnitId, Pageable pageable) { return applicationRepository.findByBusinessUnitId(businessUnitId, pageable) .map(this::mapToResponse); } public Page search(ApplicationStatus status, UUID businessUnitId, String name, Pageable pageable) { return applicationRepository.search(status, businessUnitId, name, pageable) .map(this::mapToResponse); } @Transactional public void delete(UUID id) { if (!applicationRepository.existsById(id)) { throw new ResourceNotFoundException("Application not found with id: " + id); } applicationRepository.deleteById(id); } private ApplicationResponse mapToResponse(Application application) { BusinessUnitSummaryResponse buSummary = new BusinessUnitSummaryResponse( application.getBusinessUnit().getId(), application.getBusinessUnit().getName() ); return new ApplicationResponse( application.getId(), application.getName(), application.getDescription(), application.getStatus(), buSummary, application.getEndOfLifeDate(), application.getEndOfSupportDate(), application.getCreatedAt(), application.getUpdatedAt() ); } } JAVA # ============================================================================ # BACKEND - Controller # ============================================================================ echo "📦 Creating Application controller..." cat > "$BASE_BACKEND/controller/ApplicationController.java" << 'JAVA' package com.ldpv2.controller; import com.ldpv2.domain.enums.ApplicationStatus; import com.ldpv2.dto.request.CreateApplicationRequest; import com.ldpv2.dto.request.UpdateApplicationRequest; import com.ldpv2.dto.response.ApplicationResponse; import com.ldpv2.service.ApplicationService; 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("/applications") @Tag(name = "Applications", description = "Application management endpoints") @SecurityRequirement(name = "bearerAuth") public class ApplicationController { @Autowired private ApplicationService applicationService; @PostMapping @Operation(summary = "Create application", description = "Create a new application") public ResponseEntity create(@Valid @RequestBody CreateApplicationRequest request) { ApplicationResponse response = applicationService.create(request); return new ResponseEntity<>(response, HttpStatus.CREATED); } @PutMapping("/{id}") @Operation(summary = "Update application", description = "Update an existing application") public ResponseEntity update( @PathVariable UUID id, @Valid @RequestBody UpdateApplicationRequest request) { ApplicationResponse response = applicationService.update(id, request); return ResponseEntity.ok(response); } @PatchMapping("/{id}/status") @Operation(summary = "Update status", description = "Update application status only") public ResponseEntity updateStatus( @PathVariable UUID id, @RequestParam ApplicationStatus status) { ApplicationResponse response = applicationService.updateStatus(id, status); return ResponseEntity.ok(response); } @GetMapping("/{id}") @Operation(summary = "Get application", description = "Get application by ID") public ResponseEntity getById(@PathVariable UUID id) { ApplicationResponse response = applicationService.findById(id); return ResponseEntity.ok(response); } @GetMapping @Operation(summary = "List applications", description = "Get paginated list of applications") public ResponseEntity> getAll( @RequestParam(required = false) ApplicationStatus status, @RequestParam(required = false) UUID businessUnitId, @RequestParam(required = false) String name, @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 response; if (status != null || businessUnitId != null || name != null) { response = applicationService.search(status, businessUnitId, name, pageable); } else { response = applicationService.findAll(pageable); } return ResponseEntity.ok(response); } @GetMapping("/by-status/{status}") @Operation(summary = "Filter by status", description = "Get applications by status") public ResponseEntity> getByStatus( @PathVariable ApplicationStatus status, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { Pageable pageable = PageRequest.of(page, size); Page response = applicationService.findByStatus(status, pageable); return ResponseEntity.ok(response); } @GetMapping("/by-business-unit/{businessUnitId}") @Operation(summary = "Filter by business unit", description = "Get applications by business unit") public ResponseEntity> getByBusinessUnit( @PathVariable UUID businessUnitId, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { Pageable pageable = PageRequest.of(page, size); Page response = applicationService.findByBusinessUnit(businessUnitId, pageable); return ResponseEntity.ok(response); } @DeleteMapping("/{id}") @Operation(summary = "Delete application", description = "Delete an application") public ResponseEntity delete(@PathVariable UUID id) { applicationService.delete(id); return ResponseEntity.noContent().build(); } } JAVA # ============================================================================ # FRONTEND - Models # ============================================================================ echo "📦 Creating Application models..." mkdir -p "$BASE_FRONTEND/shared/models" cat > "$BASE_FRONTEND/shared/models/application.model.ts" << 'TS' export enum ApplicationStatus { IDEA = 'IDEA', IN_DEVELOPMENT = 'IN_DEVELOPMENT', IN_SERVICE = 'IN_SERVICE', MAINTENANCE = 'MAINTENANCE', DECOMMISSIONED = 'DECOMMISSIONED' } export interface Application { id: string; name: string; description?: string; status: ApplicationStatus; businessUnit: { id: string; name: string }; endOfLifeDate?: Date; endOfSupportDate?: Date; createdAt: Date; updatedAt: Date; } export interface CreateApplicationRequest { name: string; description?: string; status: ApplicationStatus; businessUnitId: string; endOfLifeDate?: Date; endOfSupportDate?: Date; } export interface UpdateApplicationRequest { name?: string; description?: string; status?: ApplicationStatus; businessUnitId?: string; endOfLifeDate?: Date; endOfSupportDate?: Date; } TS # ============================================================================ # FRONTEND - Service # ============================================================================ echo "📦 Creating Application service..." mkdir -p "$BASE_FRONTEND/features/applications" cat > "$BASE_FRONTEND/features/applications/application.service.ts" << 'TS' import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import { Application, ApplicationStatus, CreateApplicationRequest, UpdateApplicationRequest } from '../../shared/models/application.model'; import { Page } from '../../shared/models/environment.model'; @Injectable({ providedIn: 'root' }) export class ApplicationService { private readonly API_URL = '/api/applications'; constructor(private http: HttpClient) {} getApplications( filters?: { status?: ApplicationStatus; businessUnitId?: string; name?: string; }, page: number = 0, size: number = 20, sortBy: string = 'name', sortDirection: string = 'asc' ): Observable> { let params = new HttpParams() .set('page', page.toString()) .set('size', size.toString()) .set('sortBy', sortBy) .set('sortDirection', sortDirection); if (filters?.status) { params = params.set('status', filters.status); } if (filters?.businessUnitId) { params = params.set('businessUnitId', filters.businessUnitId); } if (filters?.name) { params = params.set('name', filters.name); } return this.http.get>(this.API_URL, { params }); } getApplication(id: string): Observable { return this.http.get(`${this.API_URL}/${id}`); } createApplication(data: CreateApplicationRequest): Observable { return this.http.post(this.API_URL, data); } updateApplication(id: string, data: UpdateApplicationRequest): Observable { return this.http.put(`${this.API_URL}/${id}`, data); } updateStatus(id: string, status: ApplicationStatus): Observable { return this.http.patch(`${this.API_URL}/${id}/status`, null, { params: { status } }); } deleteApplication(id: string): Observable { return this.http.delete(`${this.API_URL}/${id}`); } } TS # ============================================================================ # FRONTEND - Components (Application List) # ============================================================================ echo "📦 Creating Application List component..." mkdir -p "$BASE_FRONTEND/features/applications/application-list" cat > "$BASE_FRONTEND/features/applications/application-list/application-list.component.ts" << 'TS' 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 { ApplicationService } from '../application.service'; import { BusinessUnitService } from '../../business-units/business-unit.service'; import { Application, ApplicationStatus } from '../../../shared/models/application.model'; import { BusinessUnit } from '../../../shared/models/business-unit.model'; import { Page } from '../../../shared/models/environment.model'; @Component({ selector: 'app-application-list', standalone: true, imports: [CommonModule, FormsModule], templateUrl: './application-list.component.html', styleUrls: ['./application-list.component.scss'] }) export class ApplicationListComponent implements OnInit { applications: Application[] = []; businessUnits: BusinessUnit[] = []; loading = false; error = ''; page = 0; size = 20; totalElements = 0; totalPages = 0; // Filters searchQuery = ''; selectedStatus: ApplicationStatus | '' = ''; selectedBusinessUnitId = ''; // Status options statusOptions = Object.values(ApplicationStatus); ApplicationStatus = ApplicationStatus; private searchSubject = new Subject(); constructor( private applicationService: ApplicationService, private businessUnitService: BusinessUnitService, private router: Router ) { this.searchSubject.pipe( debounceTime(300), distinctUntilChanged() ).subscribe(() => { this.page = 0; this.loadApplications(); }); } ngOnInit(): void { this.loadBusinessUnits(); this.loadApplications(); } loadBusinessUnits(): void { this.businessUnitService.getBusinessUnits(0, 100).subscribe({ next: (data) => { this.businessUnits = data.content; }, error: () => { // Silent fail for filters } }); } loadApplications(): void { this.loading = true; const filters = { status: this.selectedStatus || undefined, businessUnitId: this.selectedBusinessUnitId || undefined, name: this.searchQuery || undefined }; this.applicationService.getApplications(filters, this.page, this.size).subscribe({ next: (data: Page) => { this.applications = data.content; this.totalElements = data.totalElements; this.totalPages = data.totalPages; this.loading = false; }, error: (err) => { this.error = 'Failed to load applications'; this.loading = false; } }); } onSearchChange(query: string): void { this.searchQuery = query; this.searchSubject.next(query); } onFilterChange(): void { this.page = 0; this.loadApplications(); } createNew(): void { this.router.navigate(['/applications/new']); } viewDetails(id: string): void { this.router.navigate(['/applications', id]); } edit(id: string): void { this.router.navigate(['/applications', id, 'edit']); } changeStatus(id: string, newStatus: ApplicationStatus): void { this.applicationService.updateStatus(id, newStatus).subscribe({ next: () => { this.loadApplications(); }, error: (err) => { this.error = 'Failed to update status'; } }); } delete(id: string): void { if (confirm('Are you sure you want to delete this application?')) { this.applicationService.deleteApplication(id).subscribe({ next: () => { this.loadApplications(); }, error: (err) => { this.error = 'Failed to delete application'; } }); } } getStatusClass(status: ApplicationStatus): string { const classes: { [key in ApplicationStatus]: string } = { [ApplicationStatus.IDEA]: 'status-idea', [ApplicationStatus.IN_DEVELOPMENT]: 'status-in-development', [ApplicationStatus.IN_SERVICE]: 'status-in-service', [ApplicationStatus.MAINTENANCE]: 'status-maintenance', [ApplicationStatus.DECOMMISSIONED]: 'status-decommissioned' }; return classes[status]; } getStatusDisplay(status: ApplicationStatus): string { const displays: { [key in ApplicationStatus]: string } = { [ApplicationStatus.IDEA]: 'Idea', [ApplicationStatus.IN_DEVELOPMENT]: 'In Development', [ApplicationStatus.IN_SERVICE]: 'In Service', [ApplicationStatus.MAINTENANCE]: 'Maintenance', [ApplicationStatus.DECOMMISSIONED]: 'Decommissioned' }; return displays[status]; } nextPage(): void { if (this.page < this.totalPages - 1) { this.page++; this.loadApplications(); } } previousPage(): void { if (this.page > 0) { this.page--; this.loadApplications(); } } } TS cat > "$BASE_FRONTEND/features/applications/application-list/application-list.component.html" << 'HTML'

Applications

Loading...
{{ error }}
Name Status Business Unit End of Life Actions
{{ app.name }} {{ getStatusDisplay(app.status) }} {{ app.businessUnit.name }} {{ app.endOfLifeDate ? (app.endOfLifeDate | date:'mediumDate') : '-' }}
No applications found. Click "Create New Application" to get started.
HTML cat > "$BASE_FRONTEND/features/applications/application-list/application-list.component.scss" << 'SCSS' .container { max-width: 1400px; margin: 0 auto; padding: 2rem; } .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; h1 { margin: 0; } } .filters { background: white; padding: 1.5rem; border-radius: 8px; margin-bottom: 1.5rem; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .filter-row { display: flex; gap: 1rem; flex-wrap: wrap; } .filter-group { flex: 1; min-width: 200px; label { display: block; margin-bottom: 0.5rem; font-weight: 500; color: #555; } .search-input, .filter-select { width: 100%; 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; } } .status-badge { padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.875rem; font-weight: 500; &.status-idea { background-color: #e3f2fd; color: #1976d2; } &.status-in-development { background-color: #fff3e0; color: #f57c00; } &.status-in-service { background-color: #e8f5e9; color: #388e3c; } &.status-maintenance { background-color: #fff9c4; color: #f57f17; } &.status-decommissioned { background-color: #f5f5f5; color: #616161; } } .actions { display: flex; gap: 0.5rem; flex-wrap: wrap; } .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; } } &.status-select { background-color: #9c27b0; &:hover { background-color: #7b1fa2; } } } .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; } } SCSS # ============================================================================ # FRONTEND - Application Detail Component # ============================================================================ echo "📦 Creating Application Detail component..." mkdir -p "$BASE_FRONTEND/features/applications/application-detail" cat > "$BASE_FRONTEND/features/applications/application-detail/application-detail.component.ts" << 'TS' import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Router, ActivatedRoute } from '@angular/router'; import { ApplicationService } from '../application.service'; import { Application, ApplicationStatus } from '../../../shared/models/application.model'; @Component({ selector: 'app-application-detail', standalone: true, imports: [CommonModule], templateUrl: './application-detail.component.html', styleUrls: ['./application-detail.component.scss'] }) export class ApplicationDetailComponent implements OnInit { application?: Application; loading = false; error = ''; ApplicationStatus = ApplicationStatus; constructor( private applicationService: ApplicationService, private router: Router, private route: ActivatedRoute ) {} ngOnInit(): void { const id = this.route.snapshot.paramMap.get('id'); if (id) { this.loadApplication(id); } } loadApplication(id: string): void { this.loading = true; this.applicationService.getApplication(id).subscribe({ next: (app) => { this.application = app; this.loading = false; }, error: (err) => { this.error = 'Failed to load application'; this.loading = false; } }); } edit(): void { if (this.application) { this.router.navigate(['/applications', this.application.id, 'edit']); } } delete(): void { if (this.application && confirm('Are you sure you want to delete this application?')) { this.applicationService.deleteApplication(this.application.id).subscribe({ next: () => { this.router.navigate(['/applications']); }, error: (err) => { this.error = 'Failed to delete application'; } }); } } back(): void { this.router.navigate(['/applications']); } getStatusClass(status: ApplicationStatus): string { const classes: { [key in ApplicationStatus]: string } = { [ApplicationStatus.IDEA]: 'status-idea', [ApplicationStatus.IN_DEVELOPMENT]: 'status-in-development', [ApplicationStatus.IN_SERVICE]: 'status-in-service', [ApplicationStatus.MAINTENANCE]: 'status-maintenance', [ApplicationStatus.DECOMMISSIONED]: 'status-decommissioned' }; return classes[status]; } getStatusDisplay(status: ApplicationStatus): string { const displays: { [key in ApplicationStatus]: string } = { [ApplicationStatus.IDEA]: 'Idea', [ApplicationStatus.IN_DEVELOPMENT]: 'In Development', [ApplicationStatus.IN_SERVICE]: 'In Service', [ApplicationStatus.MAINTENANCE]: 'Maintenance', [ApplicationStatus.DECOMMISSIONED]: 'Decommissioned' }; return displays[status]; } } TS cat > "$BASE_FRONTEND/features/applications/application-detail/application-detail.component.html" << 'HTML'
Loading...
{{ error }}

{{ application.name }}

{{ getStatusDisplay(application.status) }}
{{ application.description || '-' }}
{{ application.businessUnit.name }}
{{ application.endOfSupportDate ? (application.endOfSupportDate | date:'mediumDate') : '-' }}
{{ application.endOfLifeDate ? (application.endOfLifeDate | date:'mediumDate') : '-' }}
{{ application.createdAt | date:'medium' }}
{{ application.updatedAt | date:'medium' }}
HTML cat > "$BASE_FRONTEND/features/applications/application-detail/application-detail.component.scss" << 'SCSS' .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: 250px; color: #555; } span { flex: 1; color: #333; } } .status-badge { padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.875rem; font-weight: 500; &.status-idea { background-color: #e3f2fd; color: #1976d2; } &.status-in-development { background-color: #fff3e0; color: #f57c00; } &.status-in-service { background-color: #e8f5e9; color: #388e3c; } &.status-maintenance { background-color: #fff9c4; color: #f57f17; } &.status-decommissioned { background-color: #f5f5f5; color: #616161; } } .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; } } SCSS # ============================================================================ # FRONTEND - Application Form Component # ============================================================================ echo "📦 Creating Application Form component..." mkdir -p "$BASE_FRONTEND/features/applications/application-form" cat > "$BASE_FRONTEND/features/applications/application-form/application-form.component.ts" << 'TS' 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 { ApplicationService } from '../application.service'; import { BusinessUnitService } from '../../business-units/business-unit.service'; import { ApplicationStatus } from '../../../shared/models/application.model'; import { BusinessUnit } from '../../../shared/models/business-unit.model'; @Component({ selector: 'app-application-form', standalone: true, imports: [CommonModule, ReactiveFormsModule], templateUrl: './application-form.component.html', styleUrls: ['./application-form.component.scss'] }) export class ApplicationFormComponent implements OnInit { form: FormGroup; loading = false; error = ''; isEditMode = false; applicationId?: string; businessUnits: BusinessUnit[] = []; statusOptions = Object.values(ApplicationStatus); ApplicationStatus = ApplicationStatus; constructor( private fb: FormBuilder, private applicationService: ApplicationService, private businessUnitService: BusinessUnitService, private router: Router, private route: ActivatedRoute ) { this.form = this.fb.group({ name: ['', [Validators.required, Validators.maxLength(255)]], description: [''], status: [ApplicationStatus.IDEA, [Validators.required]], businessUnitId: ['', [Validators.required]], endOfSupportDate: [''], endOfLifeDate: [''] }); } ngOnInit(): void { this.loadBusinessUnits(); this.applicationId = this.route.snapshot.paramMap.get('id') || undefined; this.isEditMode = !!this.applicationId; if (this.isEditMode && this.applicationId) { this.loadApplication(this.applicationId); } } loadBusinessUnits(): void { this.businessUnitService.getBusinessUnits(0, 100).subscribe({ next: (data) => { this.businessUnits = data.content; }, error: (err) => { this.error = 'Failed to load business units'; } }); } loadApplication(id: string): void { this.loading = true; this.applicationService.getApplication(id).subscribe({ next: (app) => { this.form.patchValue({ name: app.name, description: app.description, status: app.status, businessUnitId: app.businessUnit.id, endOfSupportDate: app.endOfSupportDate, endOfLifeDate: app.endOfLifeDate }); this.loading = false; }, error: (err) => { this.error = 'Failed to load application'; this.loading = false; } }); } onSubmit(): void { if (this.form.valid) { // Validate dates const endOfSupport = this.form.value.endOfSupportDate; const endOfLife = this.form.value.endOfLifeDate; if (endOfSupport && endOfLife) { const supportDate = new Date(endOfSupport); const lifeDate = new Date(endOfLife); if (supportDate > lifeDate) { this.error = 'End of support date must be before end of life date'; return; } } this.loading = true; this.error = ''; const request$ = this.isEditMode && this.applicationId ? this.applicationService.updateApplication(this.applicationId, this.form.value) : this.applicationService.createApplication(this.form.value); request$.subscribe({ next: () => { this.router.navigate(['/applications']); }, error: (err) => { this.error = err.error?.message || 'Failed to save application'; this.loading = false; } }); } } cancel(): void { this.router.navigate(['/applications']); } getStatusDisplay(status: ApplicationStatus): string { const displays: { [key in ApplicationStatus]: string } = { [ApplicationStatus.IDEA]: 'Idea', [ApplicationStatus.IN_DEVELOPMENT]: 'In Development', [ApplicationStatus.IN_SERVICE]: 'In Service', [ApplicationStatus.MAINTENANCE]: 'Maintenance', [ApplicationStatus.DECOMMISSIONED]: 'Decommissioned' }; return displays[status]; } } TS cat > "$BASE_FRONTEND/features/applications/application-form/application-form.component.html" << 'HTML'

{{ isEditMode ? 'Edit Application' : 'Create New Application' }}

Name is required Name must not exceed 255 characters
Status is required
Business unit is required
{{ error }}
HTML cat > "$BASE_FRONTEND/features/applications/application-form/application-form.component.scss" << 'SCSS' .container { max-width: 800px; 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"], input[type="date"], select, 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-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; } .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; } SCSS # ============================================================================ # FRONTEND - Dashboard Component # ============================================================================ echo "📦 Creating Dashboard component..." mkdir -p "$BASE_FRONTEND/features/dashboard" cat > "$BASE_FRONTEND/features/dashboard/dashboard.component.ts" << 'TS' import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Router } from '@angular/router'; @Component({ selector: 'app-dashboard', standalone: true, imports: [CommonModule], templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.scss'] }) export class DashboardComponent { cards = [ { title: 'Business Units', description: 'Manage organizational units', icon: '🏢', route: '/business-units', color: '#3f51b5' }, { title: 'Applications', description: 'Manage applications and their lifecycle', icon: '📱', route: '/applications', color: '#f57c00' }, { title: 'Environments', description: 'Manage deployment environments', icon: '🌍', route: '/environments', color: '#388e3c' } ]; constructor(private router: Router) {} navigate(route: string): void { this.router.navigate([route]); } } TS cat > "$BASE_FRONTEND/features/dashboard/dashboard.component.html" << 'HTML'

LDPv2 Dashboard

Lifecycle Data Platform - Application Management

{{ card.icon }}

{{ card.title }}

{{ card.description }}

HTML cat > "$BASE_FRONTEND/features/dashboard/dashboard.component.scss" << 'SCSS' .dashboard-container { max-width: 1200px; margin: 0 auto; padding: 3rem 2rem; } .header { margin-bottom: 3rem; text-align: center; h1 { font-size: 2.5rem; color: #333; margin-bottom: 0.5rem; } .subtitle { font-size: 1.1rem; color: #666; } } .card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem; } .dashboard-card { background: white; border-radius: 8px; padding: 2rem; box-shadow: 0 2px 8px rgba(0,0,0,0.1); border-left: 4px solid; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; gap: 1.5rem; &:hover { transform: translateY(-4px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); } .card-icon { font-size: 3rem; line-height: 1; } .card-content { flex: 1; h2 { font-size: 1.5rem; margin: 0 0 0.5rem 0; color: #333; } p { margin: 0; color: #666; font-size: 0.95rem; } } .card-arrow { font-size: 1.5rem; color: #999; transition: transform 0.3s ease; } &:hover .card-arrow { transform: translateX(4px); } } @media (max-width: 768px) { .card-grid { grid-template-columns: 1fr; } .header h1 { font-size: 2rem; } } SCSS # ============================================================================ # FRONTEND - Update Routes # ============================================================================ echo "📦 Updating Angular routes..." cat > "$BASE_FRONTEND/app.routes.ts" << 'TS' import { Routes } from '@angular/router'; import { authGuard } from './core/guards/auth.guard'; export const routes: Routes = [ { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, { path: 'login', loadComponent: () => import('./core/auth/login/login.component').then(m => m.LoginComponent) }, { path: 'dashboard', canActivate: [authGuard], loadComponent: () => import('./features/dashboard/dashboard.component').then(m => m.DashboardComponent) }, { 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: 'applications', canActivate: [authGuard], children: [ { path: '', loadComponent: () => import('./features/applications/application-list/application-list.component') .then(m => m.ApplicationListComponent) }, { path: 'new', loadComponent: () => import('./features/applications/application-form/application-form.component') .then(m => m.ApplicationFormComponent) }, { path: ':id', loadComponent: () => import('./features/applications/application-detail/application-detail.component') .then(m => m.ApplicationDetailComponent) }, { path: ':id/edit', loadComponent: () => import('./features/applications/application-form/application-form.component') .then(m => m.ApplicationFormComponent) } ] }, { path: 'environments', canActivate: [authGuard], children: [ { path: '', loadComponent: () => import('./features/environments/environment-list/environment-list.component') .then(m => m.EnvironmentListComponent) }, { path: 'new', loadComponent: () => import('./features/environments/environment-form/environment-form.component') .then(m => m.EnvironmentFormComponent) }, { path: ':id', loadComponent: () => import('./features/environments/environment-detail/environment-detail.component') .then(m => m.EnvironmentDetailComponent) }, { path: ':id/edit', loadComponent: () => import('./features/environments/environment-form/environment-form.component') .then(m => m.EnvironmentFormComponent) } ] } ]; TS echo "" echo "✅ Story 2 (Applications) deployment complete!" echo "" echo "📋 Summary:" echo " - Database migration created (004-create-application-table.xml)" echo " - Backend: ApplicationStatus enum, Application entity, repository, service, controller" echo " - Frontend: Application models, service, list/detail/form components" echo " - Dashboard component with navigation cards" echo " - Routes updated to include dashboard and applications" echo "" echo "🚀 Next steps:" echo " 1. Run from project root: bash deploy-story-2-applications.sh" echo " 2. Rebuild backend: cd backend && mvn clean package" echo " 3. Rebuild containers: docker-compose down && docker-compose up --build" echo " 4. Access at: http://localhost" echo " 5. Login with admin/admin123" echo " 6. Navigate to Dashboard to access all features" echo ""