autocomit
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
package com.ldpv2.controller;
|
package com.ldpv2.controller;
|
||||||
|
|
||||||
import com.ldpv2.domain.enums.ApplicationStatus;
|
import com.ldpv2.domain.enums.ApplicationStatus;
|
||||||
|
import com.ldpv2.dto.request.AddContactToApplicationRequest;
|
||||||
import com.ldpv2.dto.request.CreateApplicationRequest;
|
import com.ldpv2.dto.request.CreateApplicationRequest;
|
||||||
import com.ldpv2.dto.request.UpdateApplicationRequest;
|
import com.ldpv2.dto.request.UpdateApplicationRequest;
|
||||||
|
import com.ldpv2.dto.response.ApplicationContactResponse;
|
||||||
import com.ldpv2.dto.response.ApplicationResponse;
|
import com.ldpv2.dto.response.ApplicationResponse;
|
||||||
import com.ldpv2.service.ApplicationService;
|
import com.ldpv2.service.ApplicationService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
@@ -18,6 +20,7 @@ import org.springframework.http.HttpStatus;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -118,4 +121,31 @@ public class ApplicationController {
|
|||||||
applicationService.delete(id);
|
applicationService.delete(id);
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== CONTACTS MANAGEMENT ==========
|
||||||
|
|
||||||
|
@GetMapping("/{applicationId}/contacts")
|
||||||
|
@Operation(summary = "Get application contacts", description = "Get all contacts for an application")
|
||||||
|
public ResponseEntity<List<ApplicationContactResponse>> getContacts(@PathVariable UUID applicationId) {
|
||||||
|
List<ApplicationContactResponse> response = applicationService.getApplicationContacts(applicationId);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{applicationId}/contacts")
|
||||||
|
@Operation(summary = "Add contact to application", description = "Associate a contact with an application")
|
||||||
|
public ResponseEntity<ApplicationContactResponse> addContact(
|
||||||
|
@PathVariable UUID applicationId,
|
||||||
|
@Valid @RequestBody AddContactToApplicationRequest request) {
|
||||||
|
ApplicationContactResponse response = applicationService.addContact(applicationId, request.getContactId());
|
||||||
|
return new ResponseEntity<>(response, HttpStatus.CREATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{applicationId}/contacts/{contactId}")
|
||||||
|
@Operation(summary = "Remove contact from application", description = "Remove a contact from an application")
|
||||||
|
public ResponseEntity<Void> removeContact(
|
||||||
|
@PathVariable UUID applicationId,
|
||||||
|
@PathVariable UUID contactId) {
|
||||||
|
applicationService.removeContact(applicationId, contactId);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import lombok.EqualsAndHashCode;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Application entity representing software systems
|
* Application entity representing software systems
|
||||||
@@ -17,7 +19,7 @@ import java.time.LocalDate;
|
|||||||
@Table(name = "application")
|
@Table(name = "application")
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true, exclude = {"applicationContacts"})
|
||||||
public class Application extends BaseEntity {
|
public class Application extends BaseEntity {
|
||||||
|
|
||||||
@Column(nullable = false, length = 255)
|
@Column(nullable = false, length = 255)
|
||||||
@@ -39,4 +41,18 @@ public class Application extends BaseEntity {
|
|||||||
|
|
||||||
@Column(name = "end_of_support_date")
|
@Column(name = "end_of_support_date")
|
||||||
private LocalDate endOfSupportDate;
|
private LocalDate endOfSupportDate;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "application", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
|
private Set<ApplicationContact> applicationContacts = new HashSet<>();
|
||||||
|
|
||||||
|
public void addContact(Contact contact) {
|
||||||
|
ApplicationContact appContact = new ApplicationContact();
|
||||||
|
appContact.setApplication(this);
|
||||||
|
appContact.setContact(contact);
|
||||||
|
applicationContacts.add(appContact);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeContact(Contact contact) {
|
||||||
|
applicationContacts.removeIf(ac -> ac.getContact().equals(contact));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.ldpv2.domain.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Junction entity linking Applications to Contacts
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "application_contact")
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ApplicationContact implements Serializable {
|
||||||
|
|
||||||
|
@EmbeddedId
|
||||||
|
private ApplicationContactId id = new ApplicationContactId();
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@MapsId("applicationId")
|
||||||
|
@JoinColumn(name = "application_id")
|
||||||
|
private Application application;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@MapsId("contactId")
|
||||||
|
@JoinColumn(name = "contact_id")
|
||||||
|
private Contact contact;
|
||||||
|
|
||||||
|
@Embeddable
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class ApplicationContactId implements Serializable {
|
||||||
|
@Column(name = "application_id")
|
||||||
|
private java.util.UUID applicationId;
|
||||||
|
|
||||||
|
@Column(name = "contact_id")
|
||||||
|
private java.util.UUID contactId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ldpv2.dto.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class AddContactToApplicationRequest {
|
||||||
|
@NotNull(message = "Contact ID is required")
|
||||||
|
private UUID contactId;
|
||||||
|
}
|
||||||
@@ -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 ApplicationContactResponse {
|
||||||
|
private UUID applicationId;
|
||||||
|
private ContactResponse contact;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ldpv2.repository;
|
||||||
|
|
||||||
|
import com.ldpv2.domain.entity.ApplicationContact;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface ApplicationContactRepository extends JpaRepository<ApplicationContact, ApplicationContact.ApplicationContactId> {
|
||||||
|
|
||||||
|
@Query("SELECT ac FROM ApplicationContact ac " +
|
||||||
|
"JOIN FETCH ac.contact c " +
|
||||||
|
"JOIN FETCH c.contactRole " +
|
||||||
|
"LEFT JOIN FETCH c.contactPersons cp " +
|
||||||
|
"LEFT JOIN FETCH cp.person " +
|
||||||
|
"WHERE ac.application.id = :applicationId")
|
||||||
|
List<ApplicationContact> findByApplicationIdWithDetails(UUID applicationId);
|
||||||
|
}
|
||||||
@@ -1,23 +1,28 @@
|
|||||||
package com.ldpv2.service;
|
package com.ldpv2.service;
|
||||||
|
|
||||||
import com.ldpv2.domain.entity.Application;
|
import com.ldpv2.domain.entity.Application;
|
||||||
|
import com.ldpv2.domain.entity.ApplicationContact;
|
||||||
import com.ldpv2.domain.entity.BusinessUnit;
|
import com.ldpv2.domain.entity.BusinessUnit;
|
||||||
|
import com.ldpv2.domain.entity.Contact;
|
||||||
import com.ldpv2.domain.enums.ApplicationStatus;
|
import com.ldpv2.domain.enums.ApplicationStatus;
|
||||||
import com.ldpv2.dto.request.CreateApplicationRequest;
|
import com.ldpv2.dto.request.CreateApplicationRequest;
|
||||||
import com.ldpv2.dto.request.UpdateApplicationRequest;
|
import com.ldpv2.dto.request.UpdateApplicationRequest;
|
||||||
import com.ldpv2.dto.response.ApplicationResponse;
|
import com.ldpv2.dto.response.*;
|
||||||
import com.ldpv2.dto.response.BusinessUnitSummaryResponse;
|
|
||||||
import com.ldpv2.exception.BadRequestException;
|
import com.ldpv2.exception.BadRequestException;
|
||||||
import com.ldpv2.exception.ResourceNotFoundException;
|
import com.ldpv2.exception.ResourceNotFoundException;
|
||||||
|
import com.ldpv2.repository.ApplicationContactRepository;
|
||||||
import com.ldpv2.repository.ApplicationRepository;
|
import com.ldpv2.repository.ApplicationRepository;
|
||||||
import com.ldpv2.repository.BusinessUnitRepository;
|
import com.ldpv2.repository.BusinessUnitRepository;
|
||||||
|
import com.ldpv2.repository.ContactRepository;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class ApplicationService {
|
public class ApplicationService {
|
||||||
@@ -28,14 +33,18 @@ public class ApplicationService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private BusinessUnitRepository businessUnitRepository;
|
private BusinessUnitRepository businessUnitRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ContactRepository contactRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ApplicationContactRepository applicationContactRepository;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public ApplicationResponse create(CreateApplicationRequest request) {
|
public ApplicationResponse create(CreateApplicationRequest request) {
|
||||||
// Validate business unit exists
|
|
||||||
BusinessUnit businessUnit = businessUnitRepository.findById(request.getBusinessUnitId())
|
BusinessUnit businessUnit = businessUnitRepository.findById(request.getBusinessUnitId())
|
||||||
.orElseThrow(() -> new ResourceNotFoundException(
|
.orElseThrow(() -> new ResourceNotFoundException(
|
||||||
"Business unit not found with id: " + request.getBusinessUnitId()));
|
"Business unit not found with id: " + request.getBusinessUnitId()));
|
||||||
|
|
||||||
// Validate dates if both are provided
|
|
||||||
if (request.getEndOfSupportDate() != null && request.getEndOfLifeDate() != null) {
|
if (request.getEndOfSupportDate() != null && request.getEndOfLifeDate() != null) {
|
||||||
if (request.getEndOfSupportDate().isAfter(request.getEndOfLifeDate())) {
|
if (request.getEndOfSupportDate().isAfter(request.getEndOfLifeDate())) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
@@ -88,7 +97,6 @@ public class ApplicationService {
|
|||||||
application.setEndOfSupportDate(request.getEndOfSupportDate());
|
application.setEndOfSupportDate(request.getEndOfSupportDate());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate dates if both are set
|
|
||||||
if (application.getEndOfSupportDate() != null && application.getEndOfLifeDate() != null) {
|
if (application.getEndOfSupportDate() != null && application.getEndOfLifeDate() != null) {
|
||||||
if (application.getEndOfSupportDate().isAfter(application.getEndOfLifeDate())) {
|
if (application.getEndOfSupportDate().isAfter(application.getEndOfLifeDate())) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
@@ -145,6 +153,52 @@ public class ApplicationService {
|
|||||||
applicationRepository.deleteById(id);
|
applicationRepository.deleteById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ApplicationContactResponse addContact(UUID applicationId, UUID contactId) {
|
||||||
|
Application application = applicationRepository.findById(applicationId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException(
|
||||||
|
"Application not found with id: " + applicationId));
|
||||||
|
|
||||||
|
Contact contact = contactRepository.findById(contactId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException(
|
||||||
|
"Contact not found with id: " + contactId));
|
||||||
|
|
||||||
|
application.addContact(contact);
|
||||||
|
applicationRepository.save(application);
|
||||||
|
|
||||||
|
return new ApplicationContactResponse(applicationId, mapContactToResponse(contact));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void removeContact(UUID applicationId, UUID contactId) {
|
||||||
|
Application application = applicationRepository.findById(applicationId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException(
|
||||||
|
"Application not found with id: " + applicationId));
|
||||||
|
|
||||||
|
Contact contact = contactRepository.findById(contactId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException(
|
||||||
|
"Contact not found with id: " + contactId));
|
||||||
|
|
||||||
|
application.removeContact(contact);
|
||||||
|
applicationRepository.save(application);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ApplicationContactResponse> getApplicationContacts(UUID applicationId) {
|
||||||
|
if (!applicationRepository.existsById(applicationId)) {
|
||||||
|
throw new ResourceNotFoundException("Application not found with id: " + applicationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ApplicationContact> appContacts = applicationContactRepository
|
||||||
|
.findByApplicationIdWithDetails(applicationId);
|
||||||
|
|
||||||
|
return appContacts.stream()
|
||||||
|
.map(ac -> new ApplicationContactResponse(
|
||||||
|
applicationId,
|
||||||
|
mapContactToResponse(ac.getContact())
|
||||||
|
))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
private ApplicationResponse mapToResponse(Application application) {
|
private ApplicationResponse mapToResponse(Application application) {
|
||||||
BusinessUnitSummaryResponse buSummary = new BusinessUnitSummaryResponse(
|
BusinessUnitSummaryResponse buSummary = new BusinessUnitSummaryResponse(
|
||||||
application.getBusinessUnit().getId(),
|
application.getBusinessUnit().getId(),
|
||||||
@@ -163,4 +217,37 @@ public class ApplicationService {
|
|||||||
application.getUpdatedAt()
|
application.getUpdatedAt()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ContactResponse mapContactToResponse(Contact contact) {
|
||||||
|
ContactRoleResponse roleResponse = new ContactRoleResponse(
|
||||||
|
contact.getContactRole().getId(),
|
||||||
|
contact.getContactRole().getRoleName(),
|
||||||
|
contact.getContactRole().getDescription(),
|
||||||
|
contact.getContactRole().getCreatedAt(),
|
||||||
|
contact.getContactRole().getUpdatedAt()
|
||||||
|
);
|
||||||
|
|
||||||
|
List<PersonInContactResponse> personsResponse = contact.getContactPersons().stream()
|
||||||
|
.map(cp -> {
|
||||||
|
PersonResponse personResponse = new PersonResponse(
|
||||||
|
cp.getPerson().getId(),
|
||||||
|
cp.getPerson().getFirstName(),
|
||||||
|
cp.getPerson().getLastName(),
|
||||||
|
cp.getPerson().getEmail(),
|
||||||
|
cp.getPerson().getPhone(),
|
||||||
|
cp.getPerson().getCreatedAt(),
|
||||||
|
cp.getPerson().getUpdatedAt()
|
||||||
|
);
|
||||||
|
return new PersonInContactResponse(personResponse, cp.isPrimary());
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return new ContactResponse(
|
||||||
|
contact.getId(),
|
||||||
|
roleResponse,
|
||||||
|
personsResponse,
|
||||||
|
contact.getCreatedAt(),
|
||||||
|
contact.getUpdatedAt()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,6 @@
|
|||||||
<include file="db/changelog/v1.0/005-create-contact-tables.xml"/>
|
<include file="db/changelog/v1.0/005-create-contact-tables.xml"/>
|
||||||
<include file="db/changelog/v1.0/006-create-version-table.xml"/>
|
<include file="db/changelog/v1.0/006-create-version-table.xml"/>
|
||||||
<include file="db/changelog/v1.0/007-create-deployment-table.xml"/>
|
<include file="db/changelog/v1.0/007-create-deployment-table.xml"/>
|
||||||
|
<include file="db/changelog/v1.0/008-create-application-contact-table.xml"/>
|
||||||
|
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?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="008-create-application-contact-table" author="ldpv2-team">
|
||||||
|
|
||||||
|
<createTable tableName="application_contact">
|
||||||
|
<column name="application_id" type="UUID">
|
||||||
|
<constraints nullable="false"
|
||||||
|
foreignKeyName="fk_application_contact_app"
|
||||||
|
references="application(id)"
|
||||||
|
deleteCascade="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="contact_id" type="UUID">
|
||||||
|
<constraints nullable="false"
|
||||||
|
foreignKeyName="fk_application_contact_contact"
|
||||||
|
references="contact(id)"
|
||||||
|
deleteCascade="true"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey tableName="application_contact"
|
||||||
|
columnNames="application_id, contact_id"
|
||||||
|
constraintName="pk_application_contact"/>
|
||||||
|
|
||||||
|
<createIndex tableName="application_contact" indexName="idx_app_contact_application">
|
||||||
|
<column name="application_id"/>
|
||||||
|
</createIndex>
|
||||||
|
|
||||||
|
<createIndex tableName="application_contact" indexName="idx_app_contact_contact">
|
||||||
|
<column name="contact_id"/>
|
||||||
|
</createIndex>
|
||||||
|
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
<div class="contacts-container">
|
||||||
|
<div class="header">
|
||||||
|
<h3>Application Contacts</h3>
|
||||||
|
<button (click)="openAddDialog()" class="btn-primary">Add Contact</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="loading" class="loading">Loading contacts...</div>
|
||||||
|
<div *ngIf="error" class="error">{{ error }}</div>
|
||||||
|
|
||||||
|
<div *ngIf="!loading && contacts.length > 0" class="contacts-grid">
|
||||||
|
<div class="contact-card" *ngFor="let appContact of contacts">
|
||||||
|
<div class="contact-header">
|
||||||
|
<h4>{{ appContact.contact.contactRole.roleName }}</h4>
|
||||||
|
<button (click)="removeContact(appContact.contact.id)" class="btn-remove">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="contact-body">
|
||||||
|
<div class="primary-person">
|
||||||
|
<strong>Primary:</strong> {{ getPrimaryPerson(appContact.contact) }}
|
||||||
|
</div>
|
||||||
|
<div class="person-count">
|
||||||
|
{{ appContact.contact.persons.length }} person(s)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="!loading && contacts.length === 0" class="empty">
|
||||||
|
No contacts assigned to this application yet.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Contact Dialog -->
|
||||||
|
<div *ngIf="showAddDialog" class="dialog-overlay" (click)="closeAddDialog()">
|
||||||
|
<div class="dialog" (click)="$event.stopPropagation()">
|
||||||
|
<div class="dialog-header">
|
||||||
|
<h3>Add Contact</h3>
|
||||||
|
<button (click)="closeAddDialog()" class="btn-close">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dialog-body">
|
||||||
|
<div class="filter-section">
|
||||||
|
<label>Filter by role:</label>
|
||||||
|
<select [(ngModel)]="selectedContactRole" class="filter-select">
|
||||||
|
<option value="">All Roles</option>
|
||||||
|
<option *ngFor="let role of availableRoles" [value]="role.id">
|
||||||
|
{{ role.roleName }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="contacts-list">
|
||||||
|
<div *ngIf="getFilteredContacts().length === 0" class="no-contacts">
|
||||||
|
No available contacts to add.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="contact-item"
|
||||||
|
*ngFor="let contact of getFilteredContacts()"
|
||||||
|
(click)="addContact(contact.id)">
|
||||||
|
<div class="contact-info">
|
||||||
|
<strong>{{ contact.contactRole.roleName }}</strong>
|
||||||
|
<div class="contact-primary">{{ getPrimaryPerson(contact) }}</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-add">Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
+216
@@ -0,0 +1,216 @@
|
|||||||
|
.contacts-container {
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-card {
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.contact-header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-body {
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
.primary-person {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-count {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dialog styles
|
||||||
|
.dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
.no-contacts {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-info {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-primary {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add {
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { ApplicationService } from '../application.service';
|
||||||
|
import { ContactService } from '../../contacts/contact.service';
|
||||||
|
import { ApplicationContactResponse } from '../../../shared/models/application.model';
|
||||||
|
import { ContactRole } from '../../../shared/models/contact.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-application-contacts',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, FormsModule],
|
||||||
|
templateUrl: './application-contacts.component.html',
|
||||||
|
styleUrls: ['./application-contacts.component.scss']
|
||||||
|
})
|
||||||
|
export class ApplicationContactsComponent implements OnInit {
|
||||||
|
@Input() applicationId!: string;
|
||||||
|
@Input() applicationName!: string;
|
||||||
|
|
||||||
|
contacts: ApplicationContactResponse[] = [];
|
||||||
|
availableRoles: ContactRole[] = [];
|
||||||
|
loading = false;
|
||||||
|
error = '';
|
||||||
|
|
||||||
|
showAddDialog = false;
|
||||||
|
selectedContactRole = '';
|
||||||
|
availableContacts: any[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private applicationService: ApplicationService,
|
||||||
|
private contactService: ContactService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (this.applicationId) {
|
||||||
|
this.loadContacts();
|
||||||
|
this.loadContactRoles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadContacts(): void {
|
||||||
|
this.loading = true;
|
||||||
|
this.applicationService.getApplicationContacts(this.applicationId).subscribe({
|
||||||
|
next: (contacts) => {
|
||||||
|
this.contacts = contacts;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.error = 'Failed to load contacts';
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadContactRoles(): void {
|
||||||
|
this.contactService.getContactRoles().subscribe({
|
||||||
|
next: (roles) => {
|
||||||
|
this.availableRoles = roles;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error('Failed to load contact roles', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openAddDialog(): void {
|
||||||
|
this.showAddDialog = true;
|
||||||
|
this.loadAllContacts();
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAddDialog(): void {
|
||||||
|
this.showAddDialog = false;
|
||||||
|
this.selectedContactRole = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAllContacts(): void {
|
||||||
|
this.contactService.getContacts().subscribe({
|
||||||
|
next: (contacts) => {
|
||||||
|
this.availableContacts = contacts.filter(c =>
|
||||||
|
!this.contacts.some(ac => ac.contact.id === c.id)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error('Failed to load contacts', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addContact(contactId: string): void {
|
||||||
|
this.applicationService.addContactToApplication(this.applicationId, contactId).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.loadContacts();
|
||||||
|
this.closeAddDialog();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.error = 'Failed to add contact';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeContact(contactId: string): void {
|
||||||
|
if (confirm('Remove this contact from the application?')) {
|
||||||
|
this.applicationService.removeContactFromApplication(this.applicationId, contactId).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.loadContacts();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.error = 'Failed to remove contact';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPrimaryPerson(contact: any): string {
|
||||||
|
const primary = contact.persons.find((p: any) => p.isPrimary);
|
||||||
|
if (primary) {
|
||||||
|
return `${primary.person.firstName} ${primary.person.lastName}`;
|
||||||
|
}
|
||||||
|
return 'No primary contact';
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilteredContacts(): any[] {
|
||||||
|
if (!this.selectedContactRole) {
|
||||||
|
return this.availableContacts;
|
||||||
|
}
|
||||||
|
return this.availableContacts.filter(c =>
|
||||||
|
c.contactRole.id === this.selectedContactRole
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
<div class="deployments-container">
|
||||||
|
<div class="header">
|
||||||
|
<h3>Deployment History</h3>
|
||||||
|
<button (click)="recordDeployment()" class="btn-primary">Record New Deployment</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="loading" class="loading">Loading deployments...</div>
|
||||||
|
<div *ngIf="error" class="error">{{ error }}</div>
|
||||||
|
|
||||||
|
<div *ngIf="!loading && deployments.length > 0" class="deployments-list">
|
||||||
|
<div class="deployment-item" *ngFor="let deployment of deployments">
|
||||||
|
<div class="deployment-main">
|
||||||
|
<div class="deployment-version">
|
||||||
|
<strong>Version {{ deployment.version.versionIdentifier }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="deployment-environment">
|
||||||
|
<span
|
||||||
|
class="env-badge"
|
||||||
|
[class.env-prod]="deployment.environment.isProduction"
|
||||||
|
[class.env-non-prod]="!deployment.environment.isProduction">
|
||||||
|
{{ deployment.environment.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="deployment-meta">
|
||||||
|
<div>{{ deployment.deploymentDate | date:'medium' }}</div>
|
||||||
|
<div class="time-ago">{{ getDaysAgo(deployment.deploymentDate) }} days ago</div>
|
||||||
|
<div *ngIf="deployment.deployedBy">by {{ deployment.deployedBy }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="deployment-actions">
|
||||||
|
<button (click)="viewDetails(deployment.id)" class="btn-sm">Details</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="!loading && deployments.length === 0" class="empty">
|
||||||
|
No deployments recorded for this application yet.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="totalPages > 1" class="pagination">
|
||||||
|
<button (click)="previousPage()" [disabled]="page === 0">Previous</button>
|
||||||
|
<span>Page {{ page + 1 }} of {{ totalPages }}</span>
|
||||||
|
<button (click)="nextPage()" [disabled]="page >= totalPages - 1">Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
.deployments-container {
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployments-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployment-item {
|
||||||
|
background: #f9f9f9;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto auto;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.deployment-main {
|
||||||
|
.deployment-version {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.env-badge {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.env-prod {
|
||||||
|
background-color: #ffebee;
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.env-non-prod {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
color: #1565c0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployment-meta {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
.time-ago {
|
||||||
|
color: #999;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { DeploymentService } from '../../deployments/deployment.service';
|
||||||
|
import { Deployment } from '../../../shared/models/deployment.model';
|
||||||
|
import { Page } from '../../../shared/models/environment.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-application-deployments',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule],
|
||||||
|
templateUrl: './application-deployments.component.html',
|
||||||
|
styleUrls: ['./application-deployments.component.scss']
|
||||||
|
})
|
||||||
|
export class ApplicationDeploymentsComponent implements OnInit {
|
||||||
|
@Input() applicationId!: string;
|
||||||
|
@Input() applicationName!: string;
|
||||||
|
|
||||||
|
deployments: Deployment[] = [];
|
||||||
|
loading = false;
|
||||||
|
error = '';
|
||||||
|
|
||||||
|
page = 0;
|
||||||
|
size = 10;
|
||||||
|
totalPages = 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private deploymentService: DeploymentService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (this.applicationId) {
|
||||||
|
this.loadDeployments();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDeployments(): void {
|
||||||
|
this.loading = true;
|
||||||
|
this.deploymentService.getDeploymentsByApplication(this.applicationId, this.page, this.size).subscribe({
|
||||||
|
next: (data: Page<Deployment>) => {
|
||||||
|
this.deployments = data.content;
|
||||||
|
this.totalPages = data.totalPages;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.error = 'Failed to load deployments';
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
recordDeployment(): void {
|
||||||
|
this.router.navigate(['/deployments/new'], {
|
||||||
|
queryParams: { applicationId: this.applicationId }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
viewDetails(id: string): void {
|
||||||
|
this.router.navigate(['/deployments', id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDaysAgo(date: Date): number {
|
||||||
|
const now = new Date();
|
||||||
|
const deployDate = new Date(date);
|
||||||
|
const diff = now.getTime() - deployDate.getTime();
|
||||||
|
return Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPage(): void {
|
||||||
|
if (this.page < this.totalPages - 1) {
|
||||||
|
this.page++;
|
||||||
|
this.loadDeployments();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previousPage(): void {
|
||||||
|
if (this.page > 0) {
|
||||||
|
this.page--;
|
||||||
|
this.loadDeployments();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+74
-11
@@ -2,23 +2,55 @@
|
|||||||
<div *ngIf="loading" class="loading">Loading...</div>
|
<div *ngIf="loading" class="loading">Loading...</div>
|
||||||
<div *ngIf="error" class="error">{{ error }}</div>
|
<div *ngIf="error" class="error">{{ error }}</div>
|
||||||
|
|
||||||
<div *ngIf="application && !loading" class="detail-card">
|
<div *ngIf="application && !loading" class="detail-page">
|
||||||
|
<!-- Header -->
|
||||||
<div class="header">
|
<div class="header">
|
||||||
|
<div class="header-left">
|
||||||
<h1>{{ application.name }}</h1>
|
<h1>{{ application.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>Status:</label>
|
|
||||||
<span class="status-badge" [ngClass]="getStatusClass(application.status)">
|
<span class="status-badge" [ngClass]="getStatusClass(application.status)">
|
||||||
{{ getStatusDisplay(application.status) }}
|
{{ getStatusDisplay(application.status) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button (click)="edit()" class="btn-primary">Edit</button>
|
||||||
|
<button (click)="delete()" class="btn-danger">Delete</button>
|
||||||
|
<button (click)="back()" class="btn-secondary">Back</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Navigation -->
|
||||||
|
<div class="tabs">
|
||||||
|
<button
|
||||||
|
class="tab-button"
|
||||||
|
[class.active]="activeTab === 'overview'"
|
||||||
|
(click)="setActiveTab('overview')">
|
||||||
|
📋 Overview
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="tab-button"
|
||||||
|
[class.active]="activeTab === 'versions'"
|
||||||
|
(click)="setActiveTab('versions')">
|
||||||
|
📦 Versions
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="tab-button"
|
||||||
|
[class.active]="activeTab === 'deployments'"
|
||||||
|
(click)="setActiveTab('deployments')">
|
||||||
|
🚀 Deployments
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="tab-button"
|
||||||
|
[class.active]="activeTab === 'contacts'"
|
||||||
|
(click)="setActiveTab('contacts')">
|
||||||
|
👥 Contacts
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Content -->
|
||||||
|
<div class="tab-content">
|
||||||
|
<!-- Overview Tab -->
|
||||||
|
<div *ngIf="activeTab === 'overview'" class="overview-tab">
|
||||||
|
<div class="detail-card">
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<label>Description:</label>
|
<label>Description:</label>
|
||||||
<span>{{ application.description || '-' }}</span>
|
<span>{{ application.description || '-' }}</span>
|
||||||
@@ -29,6 +61,13 @@
|
|||||||
<span>{{ application.businessUnit.name }}</span>
|
<span>{{ application.businessUnit.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-row">
|
||||||
|
<label>Status:</label>
|
||||||
|
<span class="status-badge" [ngClass]="getStatusClass(application.status)">
|
||||||
|
{{ getStatusDisplay(application.status) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<label>End of Support Date:</label>
|
<label>End of Support Date:</label>
|
||||||
<span>{{ application.endOfSupportDate ? (application.endOfSupportDate | date:'mediumDate') : '-' }}</span>
|
<span>{{ application.endOfSupportDate ? (application.endOfSupportDate | date:'mediumDate') : '-' }}</span>
|
||||||
@@ -49,7 +88,31 @@
|
|||||||
<span>{{ application.updatedAt | date:'medium' }}</span>
|
<span>{{ application.updatedAt | date:'medium' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button (click)="back()" class="btn-secondary">Back to List</button>
|
<!-- Versions Tab -->
|
||||||
|
<div *ngIf="activeTab === 'versions'">
|
||||||
|
<app-version-list
|
||||||
|
[applicationId]="application.id"
|
||||||
|
[applicationName]="application.name">
|
||||||
|
</app-version-list>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Deployments Tab -->
|
||||||
|
<div *ngIf="activeTab === 'deployments'">
|
||||||
|
<app-application-deployments
|
||||||
|
[applicationId]="application.id"
|
||||||
|
[applicationName]="application.name">
|
||||||
|
</app-application-deployments>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contacts Tab -->
|
||||||
|
<div *ngIf="activeTab === 'contacts'">
|
||||||
|
<app-application-contacts
|
||||||
|
[applicationId]="application.id"
|
||||||
|
[applicationName]="application.name">
|
||||||
|
</app-application-contacts>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+45
-9
@@ -1,5 +1,5 @@
|
|||||||
.container {
|
.container {
|
||||||
max-width: 800px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
@@ -13,9 +13,8 @@
|
|||||||
color: #f44336;
|
color: #f44336;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-card {
|
.detail-page {
|
||||||
background: white;
|
background: white;
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
@@ -24,24 +23,60 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 2rem;
|
padding: 2rem;
|
||||||
padding-bottom: 1rem;
|
border-bottom: 1px solid #f5f5f5;
|
||||||
border-bottom: 2px solid #f5f5f5;
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.actions {
|
.header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.details {
|
.tabs {
|
||||||
margin-bottom: 2rem;
|
display: flex;
|
||||||
|
border-bottom: 2px solid #f5f5f5;
|
||||||
|
background: #fafafa;
|
||||||
|
|
||||||
|
.tab-button {
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #3f51b5;
|
||||||
|
border-bottom-color: #3f51b5;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
padding: 2rem;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card {
|
||||||
.detail-row {
|
.detail-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
@@ -58,6 +93,7 @@
|
|||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.status-badge {
|
.status-badge {
|
||||||
padding: 0.25rem 0.75rem;
|
padding: 0.25rem 0.75rem;
|
||||||
|
|||||||
+24
-1
@@ -3,11 +3,19 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
import { ApplicationService } from '../application.service';
|
import { ApplicationService } from '../application.service';
|
||||||
import { Application, ApplicationStatus } from '../../../shared/models/application.model';
|
import { Application, ApplicationStatus } from '../../../shared/models/application.model';
|
||||||
|
import { VersionListComponent } from '../../versions/version-list/version-list.component';
|
||||||
|
import { ApplicationDeploymentsComponent } from '../application-deployments/application-deployments.component';
|
||||||
|
import { ApplicationContactsComponent } from '../application-contacts/application-contacts.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-application-detail',
|
selector: 'app-application-detail',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
VersionListComponent,
|
||||||
|
ApplicationDeploymentsComponent,
|
||||||
|
ApplicationContactsComponent
|
||||||
|
],
|
||||||
templateUrl: './application-detail.component.html',
|
templateUrl: './application-detail.component.html',
|
||||||
styleUrls: ['./application-detail.component.scss']
|
styleUrls: ['./application-detail.component.scss']
|
||||||
})
|
})
|
||||||
@@ -15,6 +23,7 @@ export class ApplicationDetailComponent implements OnInit {
|
|||||||
application?: Application;
|
application?: Application;
|
||||||
loading = false;
|
loading = false;
|
||||||
error = '';
|
error = '';
|
||||||
|
activeTab: 'overview' | 'versions' | 'deployments' | 'contacts' = 'overview';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private applicationService: ApplicationService,
|
private applicationService: ApplicationService,
|
||||||
@@ -24,6 +33,11 @@ export class ApplicationDetailComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
const id = this.route.snapshot.paramMap.get('id');
|
const id = this.route.snapshot.paramMap.get('id');
|
||||||
|
const tab = this.route.snapshot.queryParamMap.get('tab');
|
||||||
|
if (tab && ['overview', 'versions', 'deployments', 'contacts'].includes(tab)) {
|
||||||
|
this.activeTab = tab as any;
|
||||||
|
}
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
this.loadApplication(id);
|
this.loadApplication(id);
|
||||||
}
|
}
|
||||||
@@ -43,6 +57,15 @@ export class ApplicationDetailComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setActiveTab(tab: 'overview' | 'versions' | 'deployments' | 'contacts'): void {
|
||||||
|
this.activeTab = tab;
|
||||||
|
this.router.navigate([], {
|
||||||
|
relativeTo: this.route,
|
||||||
|
queryParams: { tab },
|
||||||
|
queryParamsHandling: 'merge'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
edit(): void {
|
edit(): void {
|
||||||
if (this.application) {
|
if (this.application) {
|
||||||
this.router.navigate(['/applications', this.application.id, 'edit']);
|
this.router.navigate(['/applications', this.application.id, 'edit']);
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import {
|
|||||||
Application,
|
Application,
|
||||||
ApplicationStatus,
|
ApplicationStatus,
|
||||||
CreateApplicationRequest,
|
CreateApplicationRequest,
|
||||||
UpdateApplicationRequest
|
UpdateApplicationRequest,
|
||||||
|
ApplicationContactResponse,
|
||||||
|
AddContactToApplicationRequest
|
||||||
} from '../../shared/models/application.model';
|
} from '../../shared/models/application.model';
|
||||||
import { Page } from '../../shared/models/environment.model';
|
import { Page } from '../../shared/models/environment.model';
|
||||||
|
|
||||||
@@ -68,4 +70,25 @@ export class ApplicationService {
|
|||||||
deleteApplication(id: string): Observable<void> {
|
deleteApplication(id: string): Observable<void> {
|
||||||
return this.http.delete<void>(`${this.API_URL}/${id}`);
|
return this.http.delete<void>(`${this.API_URL}/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Contact management
|
||||||
|
getApplicationContacts(applicationId: string): Observable<ApplicationContactResponse[]> {
|
||||||
|
return this.http.get<ApplicationContactResponse[]>(
|
||||||
|
`${this.API_URL}/${applicationId}/contacts`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
addContactToApplication(applicationId: string, contactId: string): Observable<ApplicationContactResponse> {
|
||||||
|
const request: AddContactToApplicationRequest = { contactId };
|
||||||
|
return this.http.post<ApplicationContactResponse>(
|
||||||
|
`${this.API_URL}/${applicationId}/contacts`,
|
||||||
|
request
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeContactFromApplication(applicationId: string, contactId: string): Observable<void> {
|
||||||
|
return this.http.delete<void>(
|
||||||
|
`${this.API_URL}/${applicationId}/contacts/${contactId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ContactResponse } from './contact.model';
|
||||||
|
|
||||||
export enum ApplicationStatus {
|
export enum ApplicationStatus {
|
||||||
IDEA = 'IDEA',
|
IDEA = 'IDEA',
|
||||||
IN_DEVELOPMENT = 'IN_DEVELOPMENT',
|
IN_DEVELOPMENT = 'IN_DEVELOPMENT',
|
||||||
@@ -35,3 +37,12 @@ export interface UpdateApplicationRequest {
|
|||||||
endOfLifeDate?: Date;
|
endOfLifeDate?: Date;
|
||||||
endOfSupportDate?: Date;
|
endOfSupportDate?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApplicationContactResponse {
|
||||||
|
applicationId: string;
|
||||||
|
contact: ContactResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddContactToApplicationRequest {
|
||||||
|
contactId: string;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user