From e3d9ea1050a9312daae19316bd389d1442c76e55 Mon Sep 17 00:00:00 2001 From: "laurent.deleers@gmail.com" Date: Sun, 8 Feb 2026 15:52:23 +0100 Subject: [PATCH] autocomit --- .../ldpv2/controller/ContactController.java | 82 +++++++++ .../controller/ContactRoleController.java | 41 +++++ .../ldpv2/controller/PersonController.java | 82 +++++++++ .../java/com/ldpv2/domain/entity/Contact.java | 41 +++++ .../ldpv2/domain/entity/ContactPerson.java | 47 +++++ .../com/ldpv2/domain/entity/ContactRole.java | 25 +++ .../java/com/ldpv2/domain/entity/Person.java | 31 ++++ .../dto/request/CreateContactRequest.java | 25 +++ .../dto/request/CreateContactRoleRequest.java | 19 ++ .../dto/request/CreatePersonRequest.java | 29 +++ .../dto/request/UpdatePersonRequest.java | 25 +++ .../ldpv2/dto/response/ContactResponse.java | 20 ++ .../dto/response/ContactRoleResponse.java | 19 ++ .../dto/response/PersonInContactResponse.java | 13 ++ .../ldpv2/dto/response/PersonResponse.java | 21 +++ .../ldpv2/repository/ContactRepository.java | 19 ++ .../repository/ContactRoleRepository.java | 14 ++ .../ldpv2/repository/PersonRepository.java | 23 +++ .../com/ldpv2/service/ContactRoleService.java | 50 +++++ .../com/ldpv2/service/ContactService.java | 166 +++++++++++++++++ .../java/com/ldpv2/service/PersonService.java | 102 +++++++++++ .../db/changelog/db.changelog-master.xml | 3 + .../v1.0/005-create-contact-tables.xml | 172 ++++++++++++++++++ .../app/features/contacts/contact.service.ts | 53 ++++++ .../app/features/persons/person.service.ts | 50 +++++ .../src/app/shared/models/contact.model.ts | 55 ++++++ 26 files changed, 1227 insertions(+) create mode 100644 backend/src/main/java/com/ldpv2/controller/ContactController.java create mode 100644 backend/src/main/java/com/ldpv2/controller/ContactRoleController.java create mode 100644 backend/src/main/java/com/ldpv2/controller/PersonController.java create mode 100644 backend/src/main/java/com/ldpv2/domain/entity/Contact.java create mode 100644 backend/src/main/java/com/ldpv2/domain/entity/ContactPerson.java create mode 100644 backend/src/main/java/com/ldpv2/domain/entity/ContactRole.java create mode 100644 backend/src/main/java/com/ldpv2/domain/entity/Person.java create mode 100644 backend/src/main/java/com/ldpv2/dto/request/CreateContactRequest.java create mode 100644 backend/src/main/java/com/ldpv2/dto/request/CreateContactRoleRequest.java create mode 100644 backend/src/main/java/com/ldpv2/dto/request/CreatePersonRequest.java create mode 100644 backend/src/main/java/com/ldpv2/dto/request/UpdatePersonRequest.java create mode 100644 backend/src/main/java/com/ldpv2/dto/response/ContactResponse.java create mode 100644 backend/src/main/java/com/ldpv2/dto/response/ContactRoleResponse.java create mode 100644 backend/src/main/java/com/ldpv2/dto/response/PersonInContactResponse.java create mode 100644 backend/src/main/java/com/ldpv2/dto/response/PersonResponse.java create mode 100644 backend/src/main/java/com/ldpv2/repository/ContactRepository.java create mode 100644 backend/src/main/java/com/ldpv2/repository/ContactRoleRepository.java create mode 100644 backend/src/main/java/com/ldpv2/repository/PersonRepository.java create mode 100644 backend/src/main/java/com/ldpv2/service/ContactRoleService.java create mode 100644 backend/src/main/java/com/ldpv2/service/ContactService.java create mode 100644 backend/src/main/java/com/ldpv2/service/PersonService.java create mode 100644 backend/src/main/resources/db/changelog/v1.0/005-create-contact-tables.xml create mode 100644 frontend/src/app/features/contacts/contact.service.ts create mode 100644 frontend/src/app/features/persons/person.service.ts create mode 100644 frontend/src/app/shared/models/contact.model.ts diff --git a/backend/src/main/java/com/ldpv2/controller/ContactController.java b/backend/src/main/java/com/ldpv2/controller/ContactController.java new file mode 100644 index 0000000..4bfff9d --- /dev/null +++ b/backend/src/main/java/com/ldpv2/controller/ContactController.java @@ -0,0 +1,82 @@ +package com.ldpv2.controller; + +import com.ldpv2.dto.request.CreateContactRequest; +import com.ldpv2.dto.response.ContactResponse; +import com.ldpv2.service.ContactService; +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.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/contacts") +@Tag(name = "Contacts", description = "Contact management endpoints") +@SecurityRequirement(name = "bearerAuth") +public class ContactController { + + @Autowired + private ContactService contactService; + + @PostMapping + @Operation(summary = "Create contact", description = "Create a new contact") + public ResponseEntity create(@Valid @RequestBody CreateContactRequest request) { + ContactResponse response = contactService.create(request); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } + + @GetMapping("/{id}") + @Operation(summary = "Get contact", description = "Get contact by ID with persons") + public ResponseEntity getById(@PathVariable UUID id) { + ContactResponse response = contactService.findById(id); + return ResponseEntity.ok(response); + } + + @GetMapping + @Operation(summary = "List contacts", description = "Get all contacts") + public ResponseEntity> getAll() { + List response = contactService.findAll(); + return ResponseEntity.ok(response); + } + + @PostMapping("/{contactId}/persons/{personId}") + @Operation(summary = "Add person to contact", description = "Add a person to a contact") + public ResponseEntity addPerson( + @PathVariable UUID contactId, + @PathVariable UUID personId, + @RequestParam(defaultValue = "false") boolean isPrimary) { + ContactResponse response = contactService.addPerson(contactId, personId, isPrimary); + return ResponseEntity.ok(response); + } + + @DeleteMapping("/{contactId}/persons/{personId}") + @Operation(summary = "Remove person from contact", description = "Remove a person from a contact") + public ResponseEntity removePerson( + @PathVariable UUID contactId, + @PathVariable UUID personId) { + ContactResponse response = contactService.removePerson(contactId, personId); + return ResponseEntity.ok(response); + } + + @PatchMapping("/{contactId}/persons/{personId}/primary") + @Operation(summary = "Set primary person", description = "Set a person as primary contact") + public ResponseEntity setPrimary( + @PathVariable UUID contactId, + @PathVariable UUID personId) { + ContactResponse response = contactService.setPrimary(contactId, personId); + return ResponseEntity.ok(response); + } + + @DeleteMapping("/{id}") + @Operation(summary = "Delete contact", description = "Delete a contact") + public ResponseEntity delete(@PathVariable UUID id) { + contactService.delete(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/com/ldpv2/controller/ContactRoleController.java b/backend/src/main/java/com/ldpv2/controller/ContactRoleController.java new file mode 100644 index 0000000..c0e0f88 --- /dev/null +++ b/backend/src/main/java/com/ldpv2/controller/ContactRoleController.java @@ -0,0 +1,41 @@ +package com.ldpv2.controller; + +import com.ldpv2.dto.request.CreateContactRoleRequest; +import com.ldpv2.dto.response.ContactRoleResponse; +import com.ldpv2.service.ContactRoleService; +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.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/contact-roles") +@Tag(name = "Contact Roles", description = "Contact role management endpoints") +@SecurityRequirement(name = "bearerAuth") +public class ContactRoleController { + + @Autowired + private ContactRoleService contactRoleService; + + @PostMapping + @PreAuthorize("hasRole('ADMIN')") + @Operation(summary = "Create contact role", description = "Create a new contact role (Admin only)") + public ResponseEntity create(@Valid @RequestBody CreateContactRoleRequest request) { + ContactRoleResponse response = contactRoleService.create(request); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } + + @GetMapping + @Operation(summary = "List contact roles", description = "Get all contact roles") + public ResponseEntity> getAll() { + List response = contactRoleService.findAll(); + return ResponseEntity.ok(response); + } +} diff --git a/backend/src/main/java/com/ldpv2/controller/PersonController.java b/backend/src/main/java/com/ldpv2/controller/PersonController.java new file mode 100644 index 0000000..18f6106 --- /dev/null +++ b/backend/src/main/java/com/ldpv2/controller/PersonController.java @@ -0,0 +1,82 @@ +package com.ldpv2.controller; + +import com.ldpv2.dto.request.CreatePersonRequest; +import com.ldpv2.dto.request.UpdatePersonRequest; +import com.ldpv2.dto.response.PersonResponse; +import com.ldpv2.service.PersonService; +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("/persons") +@Tag(name = "Persons", description = "Person management endpoints") +@SecurityRequirement(name = "bearerAuth") +public class PersonController { + + @Autowired + private PersonService personService; + + @PostMapping + @Operation(summary = "Create person", description = "Create a new person") + public ResponseEntity create(@Valid @RequestBody CreatePersonRequest request) { + PersonResponse response = personService.create(request); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } + + @PutMapping("/{id}") + @Operation(summary = "Update person", description = "Update an existing person") + public ResponseEntity update( + @PathVariable UUID id, + @Valid @RequestBody UpdatePersonRequest request) { + PersonResponse response = personService.update(id, request); + return ResponseEntity.ok(response); + } + + @GetMapping("/{id}") + @Operation(summary = "Get person", description = "Get person by ID") + public ResponseEntity getById(@PathVariable UUID id) { + PersonResponse response = personService.findById(id); + return ResponseEntity.ok(response); + } + + @GetMapping + @Operation(summary = "List persons", description = "Get paginated list of persons") + public ResponseEntity> getAll( + @RequestParam(required = false) String name, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(defaultValue = "lastName") 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 = (name != null && !name.trim().isEmpty()) + ? personService.search(name, pageable) + : personService.findAll(pageable); + + return ResponseEntity.ok(response); + } + + @DeleteMapping("/{id}") + @Operation(summary = "Delete person", description = "Delete a person") + public ResponseEntity delete(@PathVariable UUID id) { + personService.delete(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/com/ldpv2/domain/entity/Contact.java b/backend/src/main/java/com/ldpv2/domain/entity/Contact.java new file mode 100644 index 0000000..571dfa4 --- /dev/null +++ b/backend/src/main/java/com/ldpv2/domain/entity/Contact.java @@ -0,0 +1,41 @@ +package com.ldpv2.domain.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.HashSet; +import java.util.Set; + +/** + * Contact entity representing functional roles with associated persons + */ +@Data +@Entity +@Table(name = "contact") +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true, exclude = {"contactPersons"}) +public class Contact extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "contact_role_id", nullable = false) + private ContactRole contactRole; + + @OneToMany(mappedBy = "contact", cascade = CascadeType.ALL, orphanRemoval = true) + private Set contactPersons = new HashSet<>(); + + public void addPerson(Person person, boolean isPrimary) { + ContactPerson contactPerson = new ContactPerson(); + contactPerson.setContact(this); + contactPerson.setPerson(person); + contactPerson.setPrimary(isPrimary); + contactPersons.add(contactPerson); + } + + public void removePerson(Person person) { + contactPersons.removeIf(cp -> cp.getPerson().equals(person)); + } +} diff --git a/backend/src/main/java/com/ldpv2/domain/entity/ContactPerson.java b/backend/src/main/java/com/ldpv2/domain/entity/ContactPerson.java new file mode 100644 index 0000000..7bb1ecc --- /dev/null +++ b/backend/src/main/java/com/ldpv2/domain/entity/ContactPerson.java @@ -0,0 +1,47 @@ +package com.ldpv2.domain.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * Junction entity for Contact-Person many-to-many relationship + */ +@Data +@Entity +@Table(name = "contact_person") +@NoArgsConstructor +@AllArgsConstructor +public class ContactPerson implements Serializable { + + @EmbeddedId + private ContactPersonId id = new ContactPersonId(); + + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("contactId") + @JoinColumn(name = "contact_id") + private Contact contact; + + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("personId") + @JoinColumn(name = "person_id") + private Person person; + + @Column(name = "is_primary", nullable = false) + private boolean isPrimary = false; + + @Embeddable + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class ContactPersonId implements Serializable { + @Column(name = "contact_id") + private java.util.UUID contactId; + + @Column(name = "person_id") + private java.util.UUID personId; + } +} diff --git a/backend/src/main/java/com/ldpv2/domain/entity/ContactRole.java b/backend/src/main/java/com/ldpv2/domain/entity/ContactRole.java new file mode 100644 index 0000000..091fa4b --- /dev/null +++ b/backend/src/main/java/com/ldpv2/domain/entity/ContactRole.java @@ -0,0 +1,25 @@ +package com.ldpv2.domain.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Contact Role entity representing functional roles + */ +@Data +@Entity +@Table(name = "contact_role") +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class ContactRole extends BaseEntity { + + @Column(name = "role_name", nullable = false, unique = true, length = 100) + private String roleName; + + @Column(columnDefinition = "TEXT") + private String description; +} diff --git a/backend/src/main/java/com/ldpv2/domain/entity/Person.java b/backend/src/main/java/com/ldpv2/domain/entity/Person.java new file mode 100644 index 0000000..f442119 --- /dev/null +++ b/backend/src/main/java/com/ldpv2/domain/entity/Person.java @@ -0,0 +1,31 @@ +package com.ldpv2.domain.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Person entity representing individuals + */ +@Data +@Entity +@Table(name = "person") +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Person extends BaseEntity { + + @Column(name = "first_name", nullable = false, length = 100) + private String firstName; + + @Column(name = "last_name", nullable = false, length = 100) + private String lastName; + + @Column(nullable = false, unique = true, length = 255) + private String email; + + @Column(length = 50) + private String phone; +} diff --git a/backend/src/main/java/com/ldpv2/dto/request/CreateContactRequest.java b/backend/src/main/java/com/ldpv2/dto/request/CreateContactRequest.java new file mode 100644 index 0000000..7c07293 --- /dev/null +++ b/backend/src/main/java/com/ldpv2/dto/request/CreateContactRequest.java @@ -0,0 +1,25 @@ +package com.ldpv2.dto.request; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.UUID; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CreateContactRequest { + + @NotNull(message = "Contact role is required") + private UUID contactRoleId; + + @NotEmpty(message = "At least one person is required") + private List personIds; + + @NotNull(message = "Primary person must be specified") + private UUID primaryPersonId; +} diff --git a/backend/src/main/java/com/ldpv2/dto/request/CreateContactRoleRequest.java b/backend/src/main/java/com/ldpv2/dto/request/CreateContactRoleRequest.java new file mode 100644 index 0000000..0e55f9b --- /dev/null +++ b/backend/src/main/java/com/ldpv2/dto/request/CreateContactRoleRequest.java @@ -0,0 +1,19 @@ +package com.ldpv2.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CreateContactRoleRequest { + + @NotBlank(message = "Role name is required") + @Size(max = 100, message = "Role name must not exceed 100 characters") + private String roleName; + + private String description; +} diff --git a/backend/src/main/java/com/ldpv2/dto/request/CreatePersonRequest.java b/backend/src/main/java/com/ldpv2/dto/request/CreatePersonRequest.java new file mode 100644 index 0000000..7c7cbd8 --- /dev/null +++ b/backend/src/main/java/com/ldpv2/dto/request/CreatePersonRequest.java @@ -0,0 +1,29 @@ +package com.ldpv2.dto.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CreatePersonRequest { + + @NotBlank(message = "First name is required") + @Size(max = 100, message = "First name must not exceed 100 characters") + private String firstName; + + @NotBlank(message = "Last name is required") + @Size(max = 100, message = "Last name must not exceed 100 characters") + private String lastName; + + @NotBlank(message = "Email is required") + @Email(message = "Email must be valid") + private String email; + + @Size(max = 50, message = "Phone must not exceed 50 characters") + private String phone; +} diff --git a/backend/src/main/java/com/ldpv2/dto/request/UpdatePersonRequest.java b/backend/src/main/java/com/ldpv2/dto/request/UpdatePersonRequest.java new file mode 100644 index 0000000..342fe55 --- /dev/null +++ b/backend/src/main/java/com/ldpv2/dto/request/UpdatePersonRequest.java @@ -0,0 +1,25 @@ +package com.ldpv2.dto.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UpdatePersonRequest { + + @Size(max = 100, message = "First name must not exceed 100 characters") + private String firstName; + + @Size(max = 100, message = "Last name must not exceed 100 characters") + private String lastName; + + @Email(message = "Email must be valid") + private String email; + + @Size(max = 50, message = "Phone must not exceed 50 characters") + private String phone; +} diff --git a/backend/src/main/java/com/ldpv2/dto/response/ContactResponse.java b/backend/src/main/java/com/ldpv2/dto/response/ContactResponse.java new file mode 100644 index 0000000..c449a33 --- /dev/null +++ b/backend/src/main/java/com/ldpv2/dto/response/ContactResponse.java @@ -0,0 +1,20 @@ +package com.ldpv2.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ContactResponse { + private UUID id; + private ContactRoleResponse contactRole; + private List persons; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/backend/src/main/java/com/ldpv2/dto/response/ContactRoleResponse.java b/backend/src/main/java/com/ldpv2/dto/response/ContactRoleResponse.java new file mode 100644 index 0000000..cea2e47 --- /dev/null +++ b/backend/src/main/java/com/ldpv2/dto/response/ContactRoleResponse.java @@ -0,0 +1,19 @@ +package com.ldpv2.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ContactRoleResponse { + private UUID id; + private String roleName; + private String description; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/backend/src/main/java/com/ldpv2/dto/response/PersonInContactResponse.java b/backend/src/main/java/com/ldpv2/dto/response/PersonInContactResponse.java new file mode 100644 index 0000000..861fd8d --- /dev/null +++ b/backend/src/main/java/com/ldpv2/dto/response/PersonInContactResponse.java @@ -0,0 +1,13 @@ +package com.ldpv2.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PersonInContactResponse { + private PersonResponse person; + private boolean isPrimary; +} diff --git a/backend/src/main/java/com/ldpv2/dto/response/PersonResponse.java b/backend/src/main/java/com/ldpv2/dto/response/PersonResponse.java new file mode 100644 index 0000000..695dea0 --- /dev/null +++ b/backend/src/main/java/com/ldpv2/dto/response/PersonResponse.java @@ -0,0 +1,21 @@ +package com.ldpv2.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PersonResponse { + private UUID id; + private String firstName; + private String lastName; + private String email; + private String phone; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/backend/src/main/java/com/ldpv2/repository/ContactRepository.java b/backend/src/main/java/com/ldpv2/repository/ContactRepository.java new file mode 100644 index 0000000..56a6735 --- /dev/null +++ b/backend/src/main/java/com/ldpv2/repository/ContactRepository.java @@ -0,0 +1,19 @@ +package com.ldpv2.repository; + +import com.ldpv2.domain.entity.Contact; +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 ContactRepository extends JpaRepository { + + @Query("SELECT c FROM Contact c JOIN FETCH c.contactRole LEFT JOIN FETCH c.contactPersons cp LEFT JOIN FETCH cp.person") + List findAllWithDetails(); + + @Query("SELECT c FROM Contact c JOIN FETCH c.contactRole LEFT JOIN FETCH c.contactPersons cp LEFT JOIN FETCH cp.person WHERE c.id = :id") + Contact findByIdWithDetails(UUID id); +} diff --git a/backend/src/main/java/com/ldpv2/repository/ContactRoleRepository.java b/backend/src/main/java/com/ldpv2/repository/ContactRoleRepository.java new file mode 100644 index 0000000..1b82243 --- /dev/null +++ b/backend/src/main/java/com/ldpv2/repository/ContactRoleRepository.java @@ -0,0 +1,14 @@ +package com.ldpv2.repository; + +import com.ldpv2.domain.entity.ContactRole; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface ContactRoleRepository extends JpaRepository { + Optional findByRoleName(String roleName); + boolean existsByRoleName(String roleName); +} diff --git a/backend/src/main/java/com/ldpv2/repository/PersonRepository.java b/backend/src/main/java/com/ldpv2/repository/PersonRepository.java new file mode 100644 index 0000000..d6dcc73 --- /dev/null +++ b/backend/src/main/java/com/ldpv2/repository/PersonRepository.java @@ -0,0 +1,23 @@ +package com.ldpv2.repository; + +import com.ldpv2.domain.entity.Person; +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.Optional; +import java.util.UUID; + +@Repository +public interface PersonRepository extends JpaRepository { + Optional findByEmail(String email); + boolean existsByEmail(String email); + + @Query("SELECT p FROM Person p WHERE " + + "LOWER(p.firstName) LIKE LOWER(CONCAT('%', :name, '%')) OR " + + "LOWER(p.lastName) LIKE LOWER(CONCAT('%', :name, '%'))") + Page findByName(@Param("name") String name, Pageable pageable); +} diff --git a/backend/src/main/java/com/ldpv2/service/ContactRoleService.java b/backend/src/main/java/com/ldpv2/service/ContactRoleService.java new file mode 100644 index 0000000..07ce64c --- /dev/null +++ b/backend/src/main/java/com/ldpv2/service/ContactRoleService.java @@ -0,0 +1,50 @@ +package com.ldpv2.service; + +import com.ldpv2.domain.entity.ContactRole; +import com.ldpv2.dto.request.CreateContactRoleRequest; +import com.ldpv2.dto.response.ContactRoleResponse; +import com.ldpv2.exception.BadRequestException; +import com.ldpv2.repository.ContactRoleRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class ContactRoleService { + + @Autowired + private ContactRoleRepository contactRoleRepository; + + @Transactional + public ContactRoleResponse create(CreateContactRoleRequest request) { + if (contactRoleRepository.existsByRoleName(request.getRoleName())) { + throw new BadRequestException("Contact role with name '" + request.getRoleName() + "' already exists"); + } + + ContactRole role = new ContactRole(); + role.setRoleName(request.getRoleName()); + role.setDescription(request.getDescription()); + + role = contactRoleRepository.save(role); + return mapToResponse(role); + } + + public List findAll() { + return contactRoleRepository.findAll().stream() + .map(this::mapToResponse) + .collect(Collectors.toList()); + } + + private ContactRoleResponse mapToResponse(ContactRole role) { + return new ContactRoleResponse( + role.getId(), + role.getRoleName(), + role.getDescription(), + role.getCreatedAt(), + role.getUpdatedAt() + ); + } +} diff --git a/backend/src/main/java/com/ldpv2/service/ContactService.java b/backend/src/main/java/com/ldpv2/service/ContactService.java new file mode 100644 index 0000000..d0af160 --- /dev/null +++ b/backend/src/main/java/com/ldpv2/service/ContactService.java @@ -0,0 +1,166 @@ +package com.ldpv2.service; + +import com.ldpv2.domain.entity.Contact; +import com.ldpv2.domain.entity.ContactPerson; +import com.ldpv2.domain.entity.ContactRole; +import com.ldpv2.domain.entity.Person; +import com.ldpv2.dto.request.CreateContactRequest; +import com.ldpv2.dto.response.ContactResponse; +import com.ldpv2.dto.response.ContactRoleResponse; +import com.ldpv2.dto.response.PersonInContactResponse; +import com.ldpv2.dto.response.PersonResponse; +import com.ldpv2.exception.BadRequestException; +import com.ldpv2.exception.ResourceNotFoundException; +import com.ldpv2.repository.ContactRepository; +import com.ldpv2.repository.ContactRoleRepository; +import com.ldpv2.repository.PersonRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +public class ContactService { + + @Autowired + private ContactRepository contactRepository; + + @Autowired + private ContactRoleRepository contactRoleRepository; + + @Autowired + private PersonRepository personRepository; + + @Transactional + public ContactResponse create(CreateContactRequest request) { + ContactRole role = contactRoleRepository.findById(request.getContactRoleId()) + .orElseThrow(() -> new ResourceNotFoundException( + "Contact role not found with id: " + request.getContactRoleId())); + + if (!request.getPersonIds().contains(request.getPrimaryPersonId())) { + throw new BadRequestException("Primary person must be in the list of persons"); + } + + Contact contact = new Contact(); + contact.setContactRole(role); + + for (UUID personId : request.getPersonIds()) { + Person person = personRepository.findById(personId) + .orElseThrow(() -> new ResourceNotFoundException("Person not found with id: " + personId)); + boolean isPrimary = personId.equals(request.getPrimaryPersonId()); + contact.addPerson(person, isPrimary); + } + + contact = contactRepository.save(contact); + return mapToResponse(contact); + } + + public ContactResponse findById(UUID id) { + Contact contact = contactRepository.findByIdWithDetails(id); + if (contact == null) { + throw new ResourceNotFoundException("Contact not found with id: " + id); + } + return mapToResponse(contact); + } + + public List findAll() { + return contactRepository.findAllWithDetails().stream() + .map(this::mapToResponse) + .collect(Collectors.toList()); + } + + @Transactional + public ContactResponse addPerson(UUID contactId, UUID personId, boolean isPrimary) { + Contact contact = contactRepository.findById(contactId) + .orElseThrow(() -> new ResourceNotFoundException("Contact not found with id: " + contactId)); + + Person person = personRepository.findById(personId) + .orElseThrow(() -> new ResourceNotFoundException("Person not found with id: " + personId)); + + contact.addPerson(person, isPrimary); + contact = contactRepository.save(contact); + return mapToResponse(contact); + } + + @Transactional + public ContactResponse removePerson(UUID contactId, UUID personId) { + Contact contact = contactRepository.findById(contactId) + .orElseThrow(() -> new ResourceNotFoundException("Contact not found with id: " + contactId)); + + Person person = personRepository.findById(personId) + .orElseThrow(() -> new ResourceNotFoundException("Person not found with id: " + personId)); + + contact.removePerson(person); + contact = contactRepository.save(contact); + return mapToResponse(contact); + } + + @Transactional + public ContactResponse setPrimary(UUID contactId, UUID personId) { + Contact contact = contactRepository.findByIdWithDetails(contactId); + if (contact == null) { + throw new ResourceNotFoundException("Contact not found with id: " + contactId); + } + + boolean personFound = false; + for (ContactPerson cp : contact.getContactPersons()) { + if (cp.getPerson().getId().equals(personId)) { + cp.setPrimary(true); + personFound = true; + } else { + cp.setPrimary(false); + } + } + + if (!personFound) { + throw new ResourceNotFoundException("Person not found in contact"); + } + + contact = contactRepository.save(contact); + return mapToResponse(contact); + } + + @Transactional + public void delete(UUID id) { + if (!contactRepository.existsById(id)) { + throw new ResourceNotFoundException("Contact not found with id: " + id); + } + contactRepository.deleteById(id); + } + + private ContactResponse mapToResponse(Contact contact) { + ContactRoleResponse roleResponse = new ContactRoleResponse( + contact.getContactRole().getId(), + contact.getContactRole().getRoleName(), + contact.getContactRole().getDescription(), + contact.getContactRole().getCreatedAt(), + contact.getContactRole().getUpdatedAt() + ); + + List 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() + ); + } +} diff --git a/backend/src/main/java/com/ldpv2/service/PersonService.java b/backend/src/main/java/com/ldpv2/service/PersonService.java new file mode 100644 index 0000000..25fbe9a --- /dev/null +++ b/backend/src/main/java/com/ldpv2/service/PersonService.java @@ -0,0 +1,102 @@ +package com.ldpv2.service; + +import com.ldpv2.domain.entity.Person; +import com.ldpv2.dto.request.CreatePersonRequest; +import com.ldpv2.dto.request.UpdatePersonRequest; +import com.ldpv2.dto.response.PersonResponse; +import com.ldpv2.exception.BadRequestException; +import com.ldpv2.exception.ResourceNotFoundException; +import com.ldpv2.repository.PersonRepository; +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 PersonService { + + @Autowired + private PersonRepository personRepository; + + @Transactional + public PersonResponse create(CreatePersonRequest request) { + if (personRepository.existsByEmail(request.getEmail())) { + throw new BadRequestException("Person with email '" + request.getEmail() + "' already exists"); + } + + Person person = new Person(); + person.setFirstName(request.getFirstName()); + person.setLastName(request.getLastName()); + person.setEmail(request.getEmail()); + person.setPhone(request.getPhone()); + + person = personRepository.save(person); + return mapToResponse(person); + } + + @Transactional + public PersonResponse update(UUID id, UpdatePersonRequest request) { + Person person = personRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Person not found with id: " + id)); + + if (request.getFirstName() != null) { + person.setFirstName(request.getFirstName()); + } + + if (request.getLastName() != null) { + person.setLastName(request.getLastName()); + } + + if (request.getEmail() != null) { + if (!request.getEmail().equals(person.getEmail()) && + personRepository.existsByEmail(request.getEmail())) { + throw new BadRequestException("Person with email '" + request.getEmail() + "' already exists"); + } + person.setEmail(request.getEmail()); + } + + if (request.getPhone() != null) { + person.setPhone(request.getPhone()); + } + + person = personRepository.save(person); + return mapToResponse(person); + } + + public PersonResponse findById(UUID id) { + Person person = personRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Person not found with id: " + id)); + return mapToResponse(person); + } + + public Page findAll(Pageable pageable) { + return personRepository.findAll(pageable).map(this::mapToResponse); + } + + public Page search(String name, Pageable pageable) { + return personRepository.findByName(name, pageable).map(this::mapToResponse); + } + + @Transactional + public void delete(UUID id) { + if (!personRepository.existsById(id)) { + throw new ResourceNotFoundException("Person not found with id: " + id); + } + personRepository.deleteById(id); + } + + private PersonResponse mapToResponse(Person person) { + return new PersonResponse( + person.getId(), + person.getFirstName(), + person.getLastName(), + person.getEmail(), + person.getPhone(), + person.getCreatedAt(), + person.getUpdatedAt() + ); + } +} diff --git a/backend/src/main/resources/db/changelog/db.changelog-master.xml b/backend/src/main/resources/db/changelog/db.changelog-master.xml index 2bd2ea3..68c0dd1 100644 --- a/backend/src/main/resources/db/changelog/db.changelog-master.xml +++ b/backend/src/main/resources/db/changelog/db.changelog-master.xml @@ -16,4 +16,7 @@ + + + diff --git a/backend/src/main/resources/db/changelog/v1.0/005-create-contact-tables.xml b/backend/src/main/resources/db/changelog/v1.0/005-create-contact-tables.xml new file mode 100644 index 0000000..868bc0f --- /dev/null +++ b/backend/src/main/resources/db/changelog/v1.0/005-create-contact-tables.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/app/features/contacts/contact.service.ts b/frontend/src/app/features/contacts/contact.service.ts new file mode 100644 index 0000000..b110389 --- /dev/null +++ b/frontend/src/app/features/contacts/contact.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { Contact, ContactRole, CreateContactRequest, CreateContactRoleRequest } from '../../shared/models/contact.model'; + +@Injectable({ + providedIn: 'root' +}) +export class ContactService { + private readonly CONTACT_URL = '/api/contacts'; + private readonly ROLE_URL = '/api/contact-roles'; + + constructor(private http: HttpClient) {} + + // Contact Roles + getContactRoles(): Observable { + return this.http.get(this.ROLE_URL); + } + + createContactRole(data: CreateContactRoleRequest): Observable { + return this.http.post(this.ROLE_URL, data); + } + + // Contacts + getContacts(): Observable { + return this.http.get(this.CONTACT_URL); + } + + getContact(id: string): Observable { + return this.http.get(`${this.CONTACT_URL}/${id}`); + } + + createContact(data: CreateContactRequest): Observable { + return this.http.post(this.CONTACT_URL, data); + } + + addPersonToContact(contactId: string, personId: string, isPrimary: boolean = false): Observable { + const params = new HttpParams().set('isPrimary', isPrimary.toString()); + return this.http.post(`${this.CONTACT_URL}/${contactId}/persons/${personId}`, null, { params }); + } + + removePersonFromContact(contactId: string, personId: string): Observable { + return this.http.delete(`${this.CONTACT_URL}/${contactId}/persons/${personId}`); + } + + setPrimaryPerson(contactId: string, personId: string): Observable { + return this.http.patch(`${this.CONTACT_URL}/${contactId}/persons/${personId}/primary`, null); + } + + deleteContact(id: string): Observable { + return this.http.delete(`${this.CONTACT_URL}/${id}`); + } +} diff --git a/frontend/src/app/features/persons/person.service.ts b/frontend/src/app/features/persons/person.service.ts new file mode 100644 index 0000000..453ee39 --- /dev/null +++ b/frontend/src/app/features/persons/person.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { Person, CreatePersonRequest, UpdatePersonRequest } from '../../shared/models/contact.model'; +import { Page } from '../../shared/models/environment.model'; + +@Injectable({ + providedIn: 'root' +}) +export class PersonService { + private readonly API_URL = '/api/persons'; + + constructor(private http: HttpClient) {} + + getPersons( + name?: string, + page: number = 0, + size: number = 20, + sortBy: string = 'lastName', + sortDirection: string = 'asc' + ): Observable> { + let params = new HttpParams() + .set('page', page.toString()) + .set('size', size.toString()) + .set('sortBy', sortBy) + .set('sortDirection', sortDirection); + + if (name && name.trim()) { + params = params.set('name', name.trim()); + } + + return this.http.get>(this.API_URL, { params }); + } + + getPerson(id: string): Observable { + return this.http.get(`${this.API_URL}/${id}`); + } + + createPerson(data: CreatePersonRequest): Observable { + return this.http.post(this.API_URL, data); + } + + updatePerson(id: string, data: UpdatePersonRequest): Observable { + return this.http.put(`${this.API_URL}/${id}`, data); + } + + deletePerson(id: string): Observable { + return this.http.delete(`${this.API_URL}/${id}`); + } +} diff --git a/frontend/src/app/shared/models/contact.model.ts b/frontend/src/app/shared/models/contact.model.ts new file mode 100644 index 0000000..addbab8 --- /dev/null +++ b/frontend/src/app/shared/models/contact.model.ts @@ -0,0 +1,55 @@ +export interface ContactRole { + id: string; + roleName: string; + description?: string; + createdAt: Date; + updatedAt: Date; +} + +export interface Person { + id: string; + firstName: string; + lastName: string; + email: string; + phone?: string; + createdAt: Date; + updatedAt: Date; +} + +export interface PersonInContact { + person: Person; + isPrimary: boolean; +} + +export interface Contact { + id: string; + contactRole: ContactRole; + persons: PersonInContact[]; + createdAt: Date; + updatedAt: Date; +} + +export interface CreatePersonRequest { + firstName: string; + lastName: string; + email: string; + phone?: string; +} + +export interface UpdatePersonRequest { + firstName?: string; + lastName?: string; + email?: string; + phone?: string; +} + +export interface CreateContactRequest { + contactRoleId: string; + personIds: string[]; + primaryPersonId: string; +} + +export interface CreateContactRoleRequest { + roleName: string; + description?: string; +}