@@ -2,6 +2,7 @@ package com.ffii.tsms.modules.data.service | |||||
import com.ffii.core.support.AbstractBaseEntityService | import com.ffii.core.support.AbstractBaseEntityService | ||||
import com.ffii.core.support.JdbcDao | import com.ffii.core.support.JdbcDao | ||||
import com.ffii.tsms.modules.common.SecurityUtils | |||||
import com.ffii.tsms.modules.data.entity.* | import com.ffii.tsms.modules.data.entity.* | ||||
import com.ffii.tsms.modules.data.entity.projections.StaffSearchInfo | import com.ffii.tsms.modules.data.entity.projections.StaffSearchInfo | ||||
import com.ffii.tsms.modules.data.web.models.NewStaffRequest | import com.ffii.tsms.modules.data.web.models.NewStaffRequest | ||||
@@ -13,6 +14,7 @@ import org.springframework.stereotype.Service | |||||
import org.springframework.transaction.annotation.Transactional | import org.springframework.transaction.annotation.Transactional | ||||
import java.util.* | import java.util.* | ||||
import java.util.stream.Collectors | import java.util.stream.Collectors | ||||
import kotlin.jvm.optionals.getOrNull | |||||
@Service | @Service | ||||
@@ -225,4 +227,10 @@ open class StaffsService( | |||||
) | ) | ||||
return jdbcDao.queryForList(sql.toString(), args) | return jdbcDao.queryForList(sql.toString(), args) | ||||
} | } | ||||
open fun currentStaff(): Staff? { | |||||
return SecurityUtils.getUser().getOrNull()?.let { user -> | |||||
staffRepository.findByUserId(user.id).getOrNull() | |||||
} | |||||
} | |||||
} | } |
@@ -4,4 +4,6 @@ import com.ffii.core.support.AbstractRepository | |||||
interface ProjectTaskRepository : AbstractRepository<ProjectTask, Long> { | interface ProjectTaskRepository : AbstractRepository<ProjectTask, Long> { | ||||
fun findAllByProject(project: Project): List<ProjectTask> | fun findAllByProject(project: Project): List<ProjectTask> | ||||
fun findByProjectAndTask(project: Project, task: Task): ProjectTask | |||||
} | } |
@@ -0,0 +1,23 @@ | |||||
package com.ffii.tsms.modules.timesheet.entity | |||||
import com.ffii.core.entity.BaseEntity | |||||
import com.ffii.tsms.modules.data.entity.Staff | |||||
import jakarta.persistence.* | |||||
import jakarta.validation.constraints.NotNull | |||||
@Entity | |||||
@Table(name = "leave") | |||||
open class Leave : BaseEntity<Long>() { | |||||
@Column(name = "leaveHours") | |||||
open var leaveHours: Double? = null | |||||
@NotNull | |||||
@ManyToOne | |||||
@JoinColumn(name = "staffId") | |||||
open var staff: Staff? = null | |||||
@NotNull | |||||
@ManyToOne | |||||
@JoinColumn(name = "leaveTypeId") | |||||
open var leaveType: LeaveType? = null | |||||
} |
@@ -0,0 +1,6 @@ | |||||
package com.ffii.tsms.modules.timesheet.entity; | |||||
import com.ffii.core.support.AbstractRepository | |||||
interface LeaveRepository : AbstractRepository<Leave, Long> { | |||||
} |
@@ -0,0 +1,15 @@ | |||||
package com.ffii.tsms.modules.timesheet.entity | |||||
import com.ffii.core.entity.IdEntity | |||||
import jakarta.persistence.Column | |||||
import jakarta.persistence.Entity | |||||
import jakarta.persistence.Table | |||||
import jakarta.validation.constraints.NotNull | |||||
@Entity | |||||
@Table(name = "leave_type") | |||||
open class LeaveType : IdEntity<Long>() { | |||||
@NotNull | |||||
@Column(name = "name") | |||||
open var name: String? = null | |||||
} |
@@ -0,0 +1,31 @@ | |||||
package com.ffii.tsms.modules.timesheet.entity | |||||
import com.ffii.core.entity.BaseEntity | |||||
import com.ffii.tsms.modules.data.entity.Staff | |||||
import com.ffii.tsms.modules.project.entity.ProjectTask | |||||
import jakarta.persistence.* | |||||
import jakarta.validation.constraints.NotNull | |||||
import java.time.LocalDate | |||||
@Entity | |||||
@Table(name = "timesheet") | |||||
open class Timesheet : BaseEntity<Long>() { | |||||
@Column(name = "normalConsumed") | |||||
open var normalConsumed: Double? = null | |||||
@Column(name = "otConsumed") | |||||
open var otConsumed: Double? = null | |||||
@NotNull | |||||
@Column(name = "recordDate") | |||||
open var recordDate: LocalDate? = null | |||||
@NotNull | |||||
@ManyToOne | |||||
@JoinColumn(name = "staffId") | |||||
open var staff: Staff? = null | |||||
@ManyToOne | |||||
@JoinColumn(name = "projectTaskId") | |||||
open var projectTask: ProjectTask? = null | |||||
} |
@@ -0,0 +1,12 @@ | |||||
package com.ffii.tsms.modules.timesheet.entity; | |||||
import com.ffii.core.support.AbstractRepository | |||||
import com.ffii.tsms.modules.data.entity.Staff | |||||
import java.time.LocalDate | |||||
interface TimesheetRepository : AbstractRepository<Timesheet, Long> { | |||||
fun findAllByStaff(staff: Staff): List<Timesheet> | |||||
fun deleteAllByStaffAndRecordDate(staff: Staff, recordDate: LocalDate) | |||||
} |
@@ -0,0 +1,82 @@ | |||||
package com.ffii.tsms.modules.timesheet.service | |||||
import com.ffii.core.exception.BadRequestException | |||||
import com.ffii.tsms.modules.data.service.StaffsService | |||||
import com.ffii.tsms.modules.project.entity.ProjectRepository | |||||
import com.ffii.tsms.modules.project.entity.ProjectTaskRepository | |||||
import com.ffii.tsms.modules.project.entity.TaskRepository | |||||
import com.ffii.tsms.modules.timesheet.entity.Timesheet | |||||
import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository | |||||
import com.ffii.tsms.modules.timesheet.web.models.TimeEntry | |||||
import org.springframework.stereotype.Service | |||||
import org.springframework.transaction.annotation.Transactional | |||||
import java.time.LocalDate | |||||
import java.time.format.DateTimeFormatter | |||||
import kotlin.jvm.optionals.getOrDefault | |||||
import kotlin.jvm.optionals.getOrNull | |||||
@Service | |||||
open class TimesheetsService( | |||||
private val timesheetRepository: TimesheetRepository, | |||||
private val projectTaskRepository: ProjectTaskRepository, | |||||
private val projectRepository: ProjectRepository, | |||||
private val taskRepository: TaskRepository, | |||||
private val staffsService: StaffsService | |||||
) { | |||||
@Transactional | |||||
open fun saveTimesheet(recordTimeEntry: Map<LocalDate, List<TimeEntry>>): Map<String, List<TimeEntry>> { | |||||
// Need to be associated with a staff | |||||
val currentStaff = staffsService.currentStaff() ?: throw BadRequestException() | |||||
val timesheetEntries = recordTimeEntry.entries.flatMap { (entryDate, timeEntries) -> | |||||
// Replace db timesheet entries by deleting and then adding back | |||||
timesheetRepository.deleteAllByStaffAndRecordDate(currentStaff, entryDate) | |||||
mergeTimeEntriesByProjectAndTask(timeEntries).map { timeEntry -> | |||||
val task = timeEntry.taskId?.let { taskRepository.findById(it).getOrNull() } | |||||
val project = timeEntry.projectId?.let { projectRepository.findById(it).getOrNull() } | |||||
val projectTask = project?.let { p -> task?.let { t -> projectTaskRepository.findByProjectAndTask(p, t) } } | |||||
Timesheet().apply { | |||||
this.staff = currentStaff | |||||
this.recordDate = entryDate | |||||
this.normalConsumed = timeEntry.inputHours | |||||
this.projectTask = projectTask | |||||
} | |||||
} | |||||
} | |||||
val savedTimesheets = timesheetRepository.saveAll(timesheetEntries) | |||||
return transformToTimeEntryMap(savedTimesheets) | |||||
} | |||||
open fun getTimesheet(): Map<String, List<TimeEntry>> { | |||||
// Need to be associated with a staff | |||||
val currentStaff = staffsService.currentStaff() ?: throw BadRequestException() | |||||
return transformToTimeEntryMap(timesheetRepository.findAllByStaff(currentStaff)) | |||||
} | |||||
private fun transformToTimeEntryMap(timesheets: List<Timesheet>): Map<String, List<TimeEntry>> { | |||||
return timesheets | |||||
.groupBy { timesheet -> timesheet.recordDate!!.format(DateTimeFormatter.ISO_LOCAL_DATE) } | |||||
.mapValues { (_, timesheets) -> timesheets.map { timesheet -> | |||||
TimeEntry( | |||||
id = timesheet.id!!, | |||||
projectId = timesheet.projectTask?.project?.id, | |||||
taskId = timesheet.projectTask?.task?.id, | |||||
taskGroupId = timesheet.projectTask?.task?.taskGroup?.id, | |||||
inputHours = timesheet.normalConsumed ?: 0.0 | |||||
) | |||||
} } | |||||
} | |||||
private fun mergeTimeEntriesByProjectAndTask(entries: List<TimeEntry>): List<TimeEntry> { | |||||
return entries | |||||
.groupBy { timeEntry -> Pair(timeEntry.projectId, timeEntry.taskId) } | |||||
.values.map { timeEntries -> | |||||
timeEntries.reduce { acc, timeEntry -> acc.copy(inputHours = acc.inputHours + timeEntry.inputHours) } | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,34 @@ | |||||
package com.ffii.tsms.modules.timesheet.web | |||||
import com.ffii.core.exception.BadRequestException | |||||
import com.ffii.tsms.modules.timesheet.service.TimesheetsService | |||||
import com.ffii.tsms.modules.timesheet.web.models.TimeEntry | |||||
import jakarta.validation.Valid | |||||
import org.springframework.web.bind.annotation.GetMapping | |||||
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.LocalDate | |||||
import java.time.format.DateTimeFormatter | |||||
@RestController | |||||
@RequestMapping("/timesheets") | |||||
class TimesheetsController(private val timesheetsService: TimesheetsService) { | |||||
@PostMapping("/save") | |||||
fun newTimesheetEntry(@Valid @RequestBody recordTimesheet: Map<String, List<TimeEntry>>): Map<String, List<TimeEntry>> { | |||||
val parseDateTimeResult = kotlin.runCatching { | |||||
recordTimesheet.mapKeys { (dateString) -> | |||||
LocalDate.parse(dateString, DateTimeFormatter.ISO_LOCAL_DATE) | |||||
} | |||||
} | |||||
val parsedEntries = parseDateTimeResult.getOrElse { throw BadRequestException() } | |||||
return timesheetsService.saveTimesheet(parsedEntries) | |||||
} | |||||
@GetMapping | |||||
fun getTimesheetEntry(): Map<String, List<TimeEntry>> { | |||||
return timesheetsService.getTimesheet() | |||||
} | |||||
} |
@@ -0,0 +1,10 @@ | |||||
package com.ffii.tsms.modules.timesheet.web.models | |||||
data class TimeEntry( | |||||
val id: Long, | |||||
val projectId: Long?, | |||||
val taskGroupId: Long?, | |||||
val taskId: Long?, | |||||
val inputHours: Double | |||||
) |
@@ -0,0 +1,24 @@ | |||||
-- liquibase formatted sql | |||||
-- changeset wayne:timesheet | |||||
DROP TABLE timesheet; | |||||
CREATE TABLE timesheet ( | |||||
id INT NOT NULL AUTO_INCREMENT, | |||||
version INT NOT NULL DEFAULT '0', | |||||
created datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, | |||||
createdBy VARCHAR(30) NULL, | |||||
modified datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, | |||||
modifiedBy VARCHAR(30) NULL, | |||||
deleted TINYINT(1) NOT NULL DEFAULT '0', | |||||
normalConsumed DOUBLE NULL, | |||||
otConsumed DOUBLE NULL, | |||||
recordDate date NOT NULL, | |||||
staffId INT NOT NULL, | |||||
projectTaskId INT NULL, | |||||
CONSTRAINT pk_timesheet PRIMARY KEY (id) | |||||
); | |||||
ALTER TABLE timesheet ADD CONSTRAINT FK_TIMESHEET_ON_PROJECTTASKID FOREIGN KEY (projectTaskId) REFERENCES project_task (id); | |||||
ALTER TABLE timesheet ADD CONSTRAINT FK_TIMESHEET_ON_STAFFID FOREIGN KEY (staffId) REFERENCES staff (id); |
@@ -0,0 +1,8 @@ | |||||
-- liquibase formatted sql | |||||
-- changeset wayne:leave_type | |||||
CREATE TABLE leave_type ( | |||||
id INT NOT NULL AUTO_INCREMENT, | |||||
name VARCHAR(255) NOT NULL, | |||||
CONSTRAINT pk_leave_type PRIMARY KEY (id) | |||||
); |
@@ -0,0 +1,22 @@ | |||||
-- liquibase formatted sql | |||||
-- changeset wayne:leave | |||||
DROP TABLE `leave`; | |||||
CREATE TABLE `leave` ( | |||||
id INT NOT NULL AUTO_INCREMENT, | |||||
version INT NOT NULL DEFAULT '0', | |||||
created datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, | |||||
createdBy VARCHAR(30) NULL, | |||||
modified datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, | |||||
modifiedBy VARCHAR(30) NULL, | |||||
deleted TINYINT(1) NOT NULL DEFAULT '0', | |||||
leaveHours DOUBLE NULL, | |||||
staffId INT NOT NULL, | |||||
leaveTypeId INT NOT NULL, | |||||
CONSTRAINT pk_leave PRIMARY KEY (id) | |||||
); | |||||
ALTER TABLE `leave` ADD CONSTRAINT FK_LEAVE_ON_LEAVETYPEID FOREIGN KEY (leaveTypeId) REFERENCES leave_type (id); | |||||
ALTER TABLE `leave` ADD CONSTRAINT FK_LEAVE_ON_STAFFID FOREIGN KEY (staffId) REFERENCES staff (id); |