Project management related changes

This commit is contained in:
2026-08-24 22:57:06 +05:30
parent 71b2389221
commit 2a106a8716
67 changed files with 3565 additions and 80 deletions

View File

@@ -0,0 +1,138 @@
package com.kifi.api.controller;
import com.kifi.api.entity.project.Project;
import com.kifi.api.entity.project.ProjectTask;
import com.kifi.api.entity.project.ProjectTaskComment;
import com.kifi.api.service.ProjectService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/kifi-v2/projects")
@RequiredArgsConstructor
public class ProjectController {
private final ProjectService projectService;
@GetMapping
public Flux<Project> getProjects(Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return projectService.getProjectsByUser(userId);
}
@PostMapping
public Mono<Project> createProject(@RequestBody CreateProjectRequest request, Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return projectService.createProject(
userId,
request.getName(),
request.getDescription(),
request.getCustomerId(),
request.getBudget()
);
}
@PutMapping("/{projectId}")
public Mono<Project> updateProject(@PathVariable Long projectId, @RequestBody CreateProjectRequest request, Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return projectService.updateProject(
projectId,
userId,
request.getName(),
request.getDescription(),
request.getCustomerId(),
request.getBudget(),
request.getStatus()
);
}
@DeleteMapping("/{projectId}")
public Mono<Void> deleteProject(@PathVariable Long projectId, Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return projectService.deleteProject(projectId, userId);
}
@GetMapping("/{projectId}/tasks")
public Flux<ProjectTask> getTasks(@PathVariable Long projectId) {
return projectService.getTasksByProject(projectId);
}
@PostMapping("/{projectId}/tasks")
public Mono<ProjectTask> createTask(@PathVariable Long projectId, @RequestBody CreateTaskRequest request) {
return projectService.createTask(
projectId,
request.getTitle(),
request.getDescription(),
request.getAssigneeName(),
request.getAssigneeUserId(),
request.getIsBillable(),
request.getHourlyRate()
);
}
@PutMapping("/tasks/{taskId}")
public Mono<ProjectTask> updateTask(@PathVariable Long taskId, @RequestBody CreateTaskRequest request) {
return projectService.updateTask(
taskId,
request.getTitle(),
request.getDescription(),
request.getAssigneeName(),
request.getAssigneeUserId(),
request.getStatus(),
request.getIsBillable(),
request.getHourlyRate(),
request.getHoursLogged()
);
}
@DeleteMapping("/tasks/{taskId}")
public Mono<Void> deleteTask(@PathVariable Long taskId) {
return projectService.deleteTask(taskId);
}
@GetMapping("/tasks/{taskId}/comments")
public Flux<ProjectTaskComment> getComments(@PathVariable Long taskId) {
return projectService.getCommentsForTask(taskId);
}
@PostMapping("/tasks/{taskId}/comments")
public Mono<ProjectTaskComment> addComment(
@PathVariable Long taskId,
@RequestBody CreateCommentRequest request,
Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return projectService.addComment(taskId, userId, request.getContent(), request.getImagesJson());
}
@Data
static class CreateProjectRequest {
private String name;
private String description;
private Long customerId;
private java.math.BigDecimal budget;
private String status;
}
@Data
static class CreateTaskRequest {
private String title;
private String description;
private String assigneeName;
private Long assigneeUserId;
private String status;
private Boolean isBillable;
private java.math.BigDecimal hourlyRate;
private java.math.BigDecimal hoursLogged;
}
@Data
static class CreateCommentRequest {
private String content;
private String imagesJson;
}
}

View File

@@ -0,0 +1,25 @@
package com.kifi.api.controller;
import com.kifi.api.entity.User;
import com.kifi.api.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
import java.util.List;
@RestController
@RequestMapping("/api/kifi-v2/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/search")
public Mono<ResponseEntity<List<User>>> searchUsers(@RequestParam String q) {
return userService.searchUsers(q)
.collectList()
.map(ResponseEntity::ok);
}
}

View File

@@ -0,0 +1,20 @@
package com.kifi.api.entity.project;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
import java.time.LocalDateTime;
@Data
@Table("projects")
public class Project {
@Id
private Long id;
private String name;
private String description;
private Long customerId;
private Long userId;
private java.math.BigDecimal budget;
private String status;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,23 @@
package com.kifi.api.entity.project;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
import java.time.LocalDateTime;
@Data
@Table("project_tasks")
public class ProjectTask {
@Id
private Long id;
private Long projectId;
private String title;
private String description;
private String assigneeName;
private Long assigneeUserId;
private String status;
private Boolean isBillable;
private java.math.BigDecimal hourlyRate;
private java.math.BigDecimal hoursLogged;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,18 @@
package com.kifi.api.entity.project;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
import java.time.LocalDateTime;
@Data
@Table("project_task_comments")
public class ProjectTaskComment {
@Id
private Long id;
private Long taskId;
private Long userId;
private String content;
private String images;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,9 @@
package com.kifi.api.repository;
import com.kifi.api.entity.project.Project;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
public interface ProjectRepository extends ReactiveCrudRepository<Project, Long> {
Flux<Project> findByUserId(Long userId);
}

View File

@@ -0,0 +1,9 @@
package com.kifi.api.repository;
import com.kifi.api.entity.project.ProjectTaskComment;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
public interface ProjectTaskCommentRepository extends ReactiveCrudRepository<ProjectTaskComment, Long> {
Flux<ProjectTaskComment> findByTaskIdOrderByCreatedAtAsc(Long taskId);
}

View File

@@ -0,0 +1,9 @@
package com.kifi.api.repository;
import com.kifi.api.entity.project.ProjectTask;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
public interface ProjectTaskRepository extends ReactiveCrudRepository<ProjectTask, Long> {
Flux<ProjectTask> findByProjectId(Long projectId);
}

View File

@@ -10,4 +10,5 @@ import reactor.core.publisher.Mono;
@Repository
public interface UserRepository extends R2dbcRepository<User, Long> {
Mono<User> findByEmail(String email);
Flux<User> findByEmailContainingIgnoreCase(String email);
}

View File

@@ -0,0 +1,117 @@
package com.kifi.api.service;
import com.kifi.api.entity.project.Project;
import com.kifi.api.entity.project.ProjectTask;
import com.kifi.api.entity.project.ProjectTaskComment;
import com.kifi.api.repository.ProjectRepository;
import com.kifi.api.repository.ProjectTaskRepository;
import com.kifi.api.repository.ProjectTaskCommentRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
@Service
@RequiredArgsConstructor
@Slf4j
public class ProjectService {
private final ProjectRepository projectRepository;
private final ProjectTaskRepository projectTaskRepository;
private final ProjectTaskCommentRepository projectTaskCommentRepository;
public Flux<Project> getProjectsByUser(Long userId) {
return projectRepository.findByUserId(userId);
}
public Mono<Project> createProject(Long userId, String name, String description, Long customerId, java.math.BigDecimal budget) {
Project p = new Project();
p.setUserId(userId);
p.setName(name);
p.setDescription(description);
p.setCustomerId(customerId);
p.setBudget(budget != null ? budget : java.math.BigDecimal.ZERO);
p.setStatus("ACTIVE");
p.setCreatedAt(LocalDateTime.now());
return projectRepository.save(p);
}
public Mono<Project> updateProject(Long projectId, Long userId, String name, String description, Long customerId, java.math.BigDecimal budget, String status) {
return projectRepository.findById(projectId)
.filter(p -> p.getUserId().equals(userId))
.flatMap(p -> {
if (name != null) p.setName(name);
if (description != null) p.setDescription(description);
if (customerId != null) p.setCustomerId(customerId);
if (budget != null) p.setBudget(budget);
if (status != null) p.setStatus(status);
return projectRepository.save(p);
});
}
public Mono<Void> deleteProject(Long projectId, Long userId) {
return projectRepository.findById(projectId)
.filter(p -> p.getUserId().equals(userId))
.flatMap(p -> projectTaskRepository.findByProjectId(projectId)
.flatMap(task -> projectTaskRepository.delete(task))
.then(projectRepository.delete(p))
);
}
public Flux<ProjectTask> getTasksByProject(Long projectId) {
return projectTaskRepository.findByProjectId(projectId);
}
public Mono<ProjectTask> createTask(Long projectId, String title, String description, String assigneeName, Long assigneeUserId, Boolean isBillable, java.math.BigDecimal hourlyRate) {
ProjectTask t = new ProjectTask();
t.setProjectId(projectId);
t.setTitle(title);
t.setDescription(description);
t.setAssigneeName(assigneeName);
t.setAssigneeUserId(assigneeUserId);
t.setStatus("TODO");
t.setIsBillable(isBillable != null ? isBillable : false);
t.setHourlyRate(hourlyRate != null ? hourlyRate : java.math.BigDecimal.ZERO);
t.setHoursLogged(java.math.BigDecimal.ZERO);
t.setCreatedAt(LocalDateTime.now());
return projectTaskRepository.save(t);
}
public Mono<ProjectTask> updateTask(Long taskId, String title, String description, String assigneeName, Long assigneeUserId, String status, Boolean isBillable, java.math.BigDecimal hourlyRate, java.math.BigDecimal hoursLogged) {
return projectTaskRepository.findById(taskId)
.flatMap(t -> {
if (title != null) t.setTitle(title);
if (description != null) t.setDescription(description);
if (assigneeName != null) t.setAssigneeName(assigneeName);
if (assigneeUserId != null) t.setAssigneeUserId(assigneeUserId);
if (status != null) t.setStatus(status);
if (isBillable != null) t.setIsBillable(isBillable);
if (hourlyRate != null) t.setHourlyRate(hourlyRate);
if (hoursLogged != null) t.setHoursLogged(hoursLogged);
return projectTaskRepository.save(t);
});
}
public Mono<Void> deleteTask(Long taskId) {
return projectTaskRepository.deleteById(taskId);
}
public Flux<ProjectTaskComment> getCommentsForTask(Long taskId) {
return projectTaskCommentRepository.findByTaskIdOrderByCreatedAtAsc(taskId);
}
public Mono<ProjectTaskComment> addComment(Long taskId, Long userId, String content, String imagesJson) {
ProjectTaskComment comment = new ProjectTaskComment();
comment.setTaskId(taskId);
comment.setUserId(userId);
comment.setContent(content);
if (imagesJson != null && !imagesJson.isEmpty()) {
comment.setImages(imagesJson);
}
comment.setCreatedAt(LocalDateTime.now());
return projectTaskCommentRepository.save(comment);
}
}

View File

@@ -0,0 +1,25 @@
package com.kifi.api.service;
import com.kifi.api.entity.User;
import com.kifi.api.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public Flux<User> searchUsers(String query) {
if (query == null || query.isBlank()) {
return Flux.empty();
}
return userRepository.findByEmailContainingIgnoreCase(query)
.map(user -> {
// Ensure password is not returned in search results
user.setPassword(null);
return user;
});
}
}

View File

@@ -1,6 +1,9 @@
spring:
application:
name: kifi-api
codec:
max-in-memory-size: 10MB
r2dbc:
url: r2dbc:postgresql://103.125.129.116:5333/kifi-v2

View File

@@ -364,3 +364,37 @@ CREATE TABLE IF NOT EXISTS invoice_payments (
emi_installment_number INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS projects (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
customer_id INTEGER REFERENCES customers(id),
user_id INTEGER REFERENCES users(id),
budget DECIMAL(15, 2) DEFAULT 0.00,
status VARCHAR(50) DEFAULT 'ACTIVE',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS project_tasks (
id SERIAL PRIMARY KEY,
project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
description TEXT,
assignee_name VARCHAR(255),
assignee_user_id INTEGER REFERENCES users(id),
status VARCHAR(50) DEFAULT 'TODO', -- TODO, IN_PROGRESS, REVIEW, DONE
is_billable BOOLEAN DEFAULT false,
hourly_rate DECIMAL(15, 2) DEFAULT 0.00,
hours_logged DECIMAL(10, 2) DEFAULT 0.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS project_task_comments (
id SERIAL PRIMARY KEY,
task_id INTEGER REFERENCES project_tasks(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id),
content TEXT NOT NULL,
images TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);