Skip to content

Commit

Permalink
Merge pull request #33 from seyedali-dev/feature/time-entry-report
Browse files Browse the repository at this point in the history
feat: find project by project done.
  • Loading branch information
seyedali-dev committed Jun 15, 2024
2 parents e2eddd0 + e3c06b1 commit 5b9b95f
Show file tree
Hide file tree
Showing 15 changed files with 268 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.seyed.ali.timeentryservice.client;

import com.seyed.ali.timeentryservice.keycloak.util.KeycloakSecurityUtil;
import com.seyed.ali.timeentryservice.model.payload.ProjectDTO;
import com.seyed.ali.timeentryservice.model.payload.response.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.ParameterizedTypeReference;
Expand Down Expand Up @@ -37,4 +38,10 @@ public boolean isProjectValid(String projectId) {
return (boolean) booleanResult.getData();
}

public ProjectDTO getProjectByNameOrId(String projectInfo) {
String url = this.projectServiceBaseURL + "/projects?identifier=" + projectInfo;
return this.sendRequest(url, HttpMethod.GET, new ParameterizedTypeReference<>() {
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

Expand Down Expand Up @@ -117,4 +118,22 @@ public ResponseEntity<Result> deleteTimeEntry(@PathVariable String timeEntryId)
));
}

// ###################################################################################
@GetMapping("/project/{projectCriteria}")
@Operation(summary = "Get all time entries by project(ID or Name)", description = "Fetches all time entries from the database based on name or ID", responses = {
@ApiResponse(
responseCode = "200",
description = "Successful operation",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TimeEntry.class)))
)
})
public ResponseEntity<Result> getTimeEntriesByProject(@PathVariable String projectCriteria) {
return ResponseEntity.ok(new Result(
true,
OK,
"TimeEntries - Project",
this.timeEntryService.getTimeEntriesByProjectCriteria(projectCriteria)
));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.seyed.ali.timeentryservice.exceptions.OperationNotSupportedException;
import com.seyed.ali.timeentryservice.exceptions.ResourceNotFoundException;
import com.seyed.ali.timeentryservice.model.payload.response.Result;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
Expand All @@ -14,6 +15,7 @@
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.net.ConnectException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -53,6 +55,16 @@ public ResponseEntity<Result> handleOperationNotSupportedException(OperationNotS
));
}

@ExceptionHandler({ConnectException.class})
public ResponseEntity<Result> handleConnectException(ConnectException e) {
return ResponseEntity.status(SERVICE_UNAVAILABLE).body(new Result(
false,
SERVICE_UNAVAILABLE,
"The service is not available 👎🏻",
"ServerMessage 🚫 - " + e.getMessage()
));
}

@ExceptionHandler({MethodArgumentNotValidException.class})
public ResponseEntity<Result> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.seyed.ali.timeentryservice.model.domain;

import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
Expand Down Expand Up @@ -30,6 +31,7 @@ public class TimeEntry implements Serializable {
@OneToMany(mappedBy = "timeEntry", cascade = CascadeType.ALL)
@ToString.Exclude
@Builder.Default
@JsonManagedReference // this is the forward part of the relationship – the one that gets serialized normally
private List<TimeSegment> timeSegmentList = new ArrayList<>();

private String userId;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.seyed.ali.timeentryservice.model.domain;

import com.fasterxml.jackson.annotation.JsonBackReference;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
Expand All @@ -26,6 +27,7 @@ public class TimeSegment implements Serializable {
private Duration duration;

@ManyToOne(cascade = CascadeType.ALL)
@JsonBackReference // this is the back part of the relationship – it will be omitted from serialization to avoid the infinite loop
private TimeEntry timeEntry;

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

Expand All @@ -13,6 +14,7 @@
* DTO for {@link com.seyed.ali.projectservice.model.domain.Project}
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ProjectDTO implements Serializable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,20 @@
@AllArgsConstructor
public class TimeEntryDTO {

// #######
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Unique identifier for the time entry", example = "12345")
@Size(max = 36, message = "timeEntryId must be maximum 36 characters")
private String timeEntryId;

@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Unique identifier for the associated time entry", example = "12345")
@NotBlank(message = "projectId is mandatory and cannot be blank")
@NotNull(message = "projectId is mandatory and cannot be null")
private String projectId;

@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Unique identifier for the task to assign the time entry with", example = "12345")
private String taskId;

// #######
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Start time of the time entry in the format yyyy-MM-dd HH:mm", example = "2024-05-12 08:00:00")
@NotBlank(message = "startTime is mandatory") @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$", message = "startTime must be in the format yyyy-MM-dd HH:mm:ss")
private String startTime;
Expand All @@ -45,12 +55,4 @@ public class TimeEntryDTO {
@Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$", message = "duration must be in the format HH:mm:ss")
private String duration;

@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Unique identifier for the associated time entry", example = "12345")
@NotBlank(message = "projectId is mandatory and cannot be blank")
@NotNull(message = "projectId is mandatory and cannot be null")
private String projectId;

@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Unique identifier for the task to assign the time entry with", example = "12345")
private String taskId;

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@
@NoArgsConstructor
public class TimeEntryResponse {

// #######
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Unique identifier for the time entry", example = "12345")
private String timeEntryId;

@ArraySchema(schema = @Schema(implementation = TimeSegmentDTO.class))
private List<TimeSegmentDTO> timeSegmentDTOList;
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Unique identifier for associated project with the time entry", example = "12345")
private String projectId;

@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Unique identifier for the task to associate the task with time entry", example = "12345")
private String taskId;

// #######
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "A flag determining this time entry is billable", example = "true")
private boolean billable;

Expand All @@ -31,10 +36,8 @@ public class TimeEntryResponse {
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Total time recorded", example = "00:00:18")
private String totalDuration;

@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Unique identifier for associated project with the time entry", example = "12345")
private String projectId;

@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Unique identifier for the task to associate the task with time entry", example = "12345")
private String taskId;
// #######
@ArraySchema(schema = @Schema(implementation = TimeSegmentDTO.class))
private List<TimeSegmentDTO> timeSegmentDTOList;

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.seyed.ali.timeentryservice.service;

import com.seyed.ali.timeentryservice.client.ProjectServiceClient;
import com.seyed.ali.timeentryservice.exceptions.ResourceNotFoundException;
import com.seyed.ali.timeentryservice.model.domain.TimeEntry;
import com.seyed.ali.timeentryservice.model.domain.TimeSegment;
import com.seyed.ali.timeentryservice.model.payload.ProjectDTO;
import com.seyed.ali.timeentryservice.model.payload.TimeEntryDTO;
import com.seyed.ali.timeentryservice.repository.TimeEntryRepository;
import com.seyed.ali.timeentryservice.service.cache.TimeEntryCacheManager;
Expand All @@ -27,6 +29,7 @@ public class TimeEntryServiceImpl implements TimeEntryService {
private final TimeParser timeParser;
private final TimeEntryUtility timeEntryUtility;
private final TimeEntryCacheManager timeEntryCacheManager;
private final ProjectServiceClient projectServiceClient;

/**
* {@inheritDoc}
Expand Down Expand Up @@ -128,4 +131,14 @@ public void deleteTimeEntry(TimeEntry timeEntry) {
this.timeEntryRepository.delete(foundTimeEntry);
}

/**
* {@inheritDoc}
*/
@Override
@Transactional
public List<TimeEntry> getTimeEntriesByProjectCriteria(String projectCriteria) throws ResourceNotFoundException {
ProjectDTO projectDTO = this.projectServiceClient.getProjectByNameOrId(projectCriteria);
return this.timeEntryRepository.findByProjectId(projectDTO.getProjectId());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,16 @@ public TimeEntryDTO stopTrackingTimeEntry(String timeEntryId) {
String endTimeStr = this.timeParser.parseLocalDateTimeToString(endTime);
String durationStr = this.timeParser.parseDurationToString(totalDuration);

return new TimeEntryDTO(null, startTimeStr, endTimeStr, timeEntry.isBillable(), timeEntry.getHourlyRate().toString(), durationStr, timeEntry.getProjectId(), timeEntry.getTaskId());
return TimeEntryDTO.builder()
.timeEntryId(null)
.projectId(timeEntry.getProjectId())
.taskId(timeEntry.getTaskId())
.startTime(startTimeStr)
.endTime(endTimeStr)
.duration(durationStr)
.billable(timeEntry.isBillable())
.hourlyRate(timeEntry.getHourlyRate().toString())
.build();
}

/**
Expand All @@ -98,7 +107,16 @@ public TimeEntryDTO continueTrackingTimeEntry(String timeEntryId) {

String hourlyRate = timeEntry.getHourlyRate() != null ? timeEntry.getHourlyRate().toString() : null;
String startTimeStr = this.timeParser.parseLocalDateTimeToString(timeEntry.getTimeSegmentList().getLast().getStartTime());
return new TimeEntryDTO(timeEntryId, startTimeStr, null, timeEntry.isBillable(), hourlyRate, null, timeEntry.getProjectId(), timeEntry.getTaskId());
return TimeEntryDTO.builder()
.timeEntryId(timeEntryId)
.projectId(timeEntry.getProjectId())
.taskId(timeEntry.getTaskId())
.startTime(startTimeStr)
.endTime(null)
.duration(null)
.billable(timeEntry.isBillable())
.hourlyRate(hourlyRate)
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,13 @@ public interface TimeEntryService {
*/
void deleteTimeEntry(TimeEntry timeEntry);

/**
* Fetches the time-entries by project(either it's ID or Name).
*
* @param projectCriteria either the ID or the Name of the project.
* @return List of Found TimeEntries.
* @throws ResourceNotFoundException If the project is not found.
*/
List<TimeEntry> getTimeEntriesByProjectCriteria(String projectCriteria) throws ResourceNotFoundException;

}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,15 @@ public TimeEntryResponse convertToTimeEntryResponse(TimeEntry timeEntry) {
}

String totalDurationStr = this.timeParser.parseDurationToString(totalDuration);
return new TimeEntryResponse(timeEntry.getTimeEntryId(), timeSegmentDTOList, timeEntry.isBillable(), hourlyRate, totalDurationStr, timeEntry.getProjectId(), timeEntry.getTaskId());
return TimeEntryResponse.builder()
.timeEntryId(timeEntry.getTimeEntryId())
.projectId(timeEntry.getProjectId())
.taskId(timeEntry.getTaskId())
.totalDuration(totalDurationStr)
.billable(timeEntry.isBillable())
.hourlyRate(hourlyRate)
.timeSegmentDTOList(timeSegmentDTOList)
.build();
}

/**
Expand All @@ -90,7 +98,16 @@ public TimeEntryDTO createTimeEntryDTO(TimeEntry timeEntry, TimeSegment lastTime
: null;
String endTimeStr = this.timeParser.parseLocalDateTimeToString(lastTimeSegment.getEndTime());
String durationStr = this.timeParser.parseDurationToString(lastTimeSegment.getDuration());
return new TimeEntryDTO(timeEntry.getTimeEntryId(), startTimeString, endTimeStr, timeEntry.isBillable(), hourlyRate, durationStr, timeEntry.getProjectId(), timeEntry.getTaskId());
return TimeEntryDTO.builder()
.timeEntryId(timeEntry.getTimeEntryId())
.projectId(timeEntry.getProjectId())
.taskId(timeEntry.getTaskId())
.startTime(startTimeString)
.endTime(endTimeStr)
.billable(timeEntry.isBillable())
.hourlyRate(hourlyRate)
.duration(durationStr)
.build();
}

/**
Expand Down
Loading

0 comments on commit 5b9b95f

Please sign in to comment.