Add project management support and integrate customer-project functionality

This commit is contained in:
2025-07-15 18:23:53 +00:00
parent 2707aa48bc
commit 03f633ae52
26 changed files with 1135 additions and 43 deletions

View File

@@ -0,0 +1,63 @@
package dev.rheinsw.server.project.controller;
import dev.rheinsw.server.common.controller.AbstractController;
import dev.rheinsw.server.project.model.CreateCustomerProjectDto;
import dev.rheinsw.server.project.model.Project;
import dev.rheinsw.server.project.model.records.ProjectNote;
import dev.rheinsw.server.project.usecase.ProjectUseCaseImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
/**
* @author Thatsaphorn Atchariyaphap
* @since 12.07.25
*/
@RestController
@RequestMapping("/projects")
@RequiredArgsConstructor
public class ProjectController extends AbstractController {
private final ProjectUseCaseImpl useCase;
@PostMapping
public ResponseEntity<UUID> create(@RequestBody CreateCustomerProjectDto request) {
var currentUser = getUserFromCurrentSession();
var now = Instant.now();
var notes = request.notes().stream().map(n -> new ProjectNote(n.text(), currentUser.getId(), currentUser.getId(), now, now)).toList();
var result = useCase.createProject(
currentUser,
request.customerId(),
request.name(),
request.description(),
request.status(),
notes
);
return ResponseEntity.ok(result);
}
@GetMapping
public ResponseEntity<Project> findProjectById(@PathVariable("id") UUID id) {
var result = useCase.getProjectById(id);
return ResponseEntity.ok(result);
}
@GetMapping("/customer/{customerId}")
public ResponseEntity<List<Project>> findAllCustomerProjects(@PathVariable("customerId") UUID customerId) {
var result = useCase.getProjectsByCustomerId(customerId);
return ResponseEntity.ok(result);
}
}

View File

@@ -0,0 +1,26 @@
package dev.rheinsw.server.project.model;
import dev.rheinsw.server.project.model.enums.ProjectStatus;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
/**
* @author Thatsaphorn Atchariyaphap
* @since 13.07.25
*/
public record CreateCustomerProjectDto(
UUID customerId, // Reference to the related customer
String name, // Project name
String description, // Optional project description
ProjectStatus status, // Enum for project status
List<ProjectNoteDto> notes, // Optional list of project notes
LocalDate startDate // Project start date
) {
public record ProjectNoteDto(
String text // Note text
) {
}
}

View File

@@ -0,0 +1,51 @@
package dev.rheinsw.server.project.model;
import com.vladmihalcea.hibernate.type.json.JsonType;
import dev.rheinsw.server.common.entity.BaseEntity;
import dev.rheinsw.server.project.model.enums.ProjectStatus;
import dev.rheinsw.server.project.model.records.ProjectNote;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.Type;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
/**
* @author Thatsaphorn Atchariyaphap
* @since 12.07.25
*/
@Entity
@Table(name = "project")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Project extends BaseEntity {
@Id
private UUID id;
private UUID customerId;
private String name;
private String description;
@Enumerated(EnumType.STRING)
private ProjectStatus status;
@Column(name = "notes", columnDefinition = "jsonb")
@Type(JsonType.class)
private List<ProjectNote> notes;
}

View File

@@ -0,0 +1,13 @@
package dev.rheinsw.server.project.model.enums;
/**
* @author Thatsaphorn Atchariyaphap
* @since 12.07.25
*/
public enum ProjectStatus {
PLANNED,
IN_PROGRESS,
COMPLETED,
ON_HOLD,
CANCELLED
}

View File

@@ -0,0 +1,14 @@
package dev.rheinsw.server.project.model.records;
import java.time.Instant;
/**
* @author Thatsaphorn Atchariyaphap
* @since 12.07.25
*/
public record ProjectNote(String text,
Long createdBy,
Long updatedBy,
Instant createdAt,
Instant updatedAt) {
}

View File

@@ -0,0 +1,16 @@
package dev.rheinsw.server.project.repository;
import dev.rheinsw.server.customer.model.Customer;
import dev.rheinsw.server.project.model.Project;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.UUID;
/**
* @author Thatsaphorn Atchariyaphap
* @since 12.07.25
*/
public interface ProjectRepository extends JpaRepository<Project, UUID> {
List<Project> findByCustomerId(UUID customerId);
}

View File

@@ -0,0 +1,23 @@
package dev.rheinsw.server.project.usecase;
import dev.rheinsw.server.common.entity.User;
import dev.rheinsw.server.project.model.enums.ProjectStatus;
import dev.rheinsw.server.project.model.records.ProjectNote;
import java.util.List;
import java.util.UUID;
/**
* @author Thatsaphorn Atchariyaphap
* @since 13.07.25
*/
public interface CreateProjectUseCase {
UUID createProject(
User creator,
UUID customerId,
String name,
String description,
ProjectStatus status,
List<ProjectNote> notes
);
}

View File

@@ -0,0 +1,16 @@
package dev.rheinsw.server.project.usecase;
import dev.rheinsw.server.project.model.Project;
import java.util.List;
import java.util.UUID;
/**
* @author Thatsaphorn Atchariyaphap
* @since 12.07.25
*/
public interface LoadProjectUseCase {
Project getProjectById(UUID id);
List<Project> getProjectsByCustomerId(UUID customerId);
}

View File

@@ -0,0 +1,61 @@
package dev.rheinsw.server.project.usecase;
import dev.rheinsw.server.common.entity.User;
import dev.rheinsw.server.project.model.Project;
import dev.rheinsw.server.project.model.enums.ProjectStatus;
import dev.rheinsw.server.project.model.records.ProjectNote;
import dev.rheinsw.server.project.repository.ProjectRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
/**
* @author Thatsaphorn Atchariyaphap
* @since 12.07.25
*/
@Service
@RequiredArgsConstructor
public class ProjectUseCaseImpl implements LoadProjectUseCase, CreateProjectUseCase {
private final ProjectRepository repository;
@Override
public UUID createProject(
User creator,
UUID customerId,
String name,
String description,
ProjectStatus status,
List<ProjectNote> notes
) {
final var now = Instant.now();
var enrichedNotes = notes.stream()
.map(n -> new ProjectNote(n.text(), creator.getId(), creator.getId(), now, now))
.toList();
Project project = Project.builder()
.id(UUID.randomUUID())
.customerId(customerId)
.name(name)
.description(description)
.status(status)
.notes(enrichedNotes)
.build();
var savedProject = repository.save(project);
return savedProject.getId();
}
@Override
public Project getProjectById(UUID id) {
return repository.findById(id).orElse(null);
}
@Override
public List<Project> getProjectsByCustomerId(UUID customerId) {
return repository.findByCustomerId(customerId);
}
}

View File

@@ -0,0 +1,22 @@
-- Migration script for Project table and related components
CREATE TABLE project
(
id UUID PRIMARY KEY,
customer_id UUID NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
status VARCHAR(50) NOT NULL, -- ProjectStatus enum
notes JSONB, -- JSONB for storing ProjectNotes list
start_date VARCHAR(50),
end_date VARCHAR(50),
created_at TIMESTAMP NOT NULL, -- From BaseEntity
updated_at TIMESTAMP, -- From BaseEntity
created_by BIGINT, -- From BaseEntity
updated_by BIGINT, -- From BaseEntity
version BIGINT -- From BaseEntity
);
-- Adding a CHECK constraint to enforce valid ProjectStatus values
ALTER TABLE project
ADD CONSTRAINT chk_project_status
CHECK (status IN ('PLANNED', 'IN_PROGRESS', 'COMPLETED', 'ON_HOLD', 'CANCELLED'));