@@ -4,6 +4,7 @@ import com.ffii.core.entity.BaseEntity | |||||
import com.ffii.tsms.modules.data.entity.Staff | import com.ffii.tsms.modules.data.entity.Staff | ||||
import jakarta.persistence.* | import jakarta.persistence.* | ||||
import jakarta.validation.constraints.NotNull | import jakarta.validation.constraints.NotNull | ||||
import java.time.LocalDate | |||||
@Entity | @Entity | ||||
@Table(name = "leave") | @Table(name = "leave") | ||||
@@ -20,4 +21,11 @@ open class Leave : BaseEntity<Long>() { | |||||
@ManyToOne | @ManyToOne | ||||
@JoinColumn(name = "leaveTypeId") | @JoinColumn(name = "leaveTypeId") | ||||
open var leaveType: LeaveType? = null | open var leaveType: LeaveType? = null | ||||
@NotNull | |||||
@Column(name = "recordDate") | |||||
open var recordDate: LocalDate? = null | |||||
@Column(name = "remark") | |||||
open var remark: String? = null | |||||
} | } |
@@ -1,6 +1,11 @@ | |||||
package com.ffii.tsms.modules.timesheet.entity; | package com.ffii.tsms.modules.timesheet.entity; | ||||
import com.ffii.core.support.AbstractRepository | import com.ffii.core.support.AbstractRepository | ||||
import com.ffii.tsms.modules.data.entity.Staff | |||||
import java.time.LocalDate | |||||
interface LeaveRepository : AbstractRepository<Leave, Long> { | interface LeaveRepository : AbstractRepository<Leave, Long> { | ||||
fun findAllByStaff(staff: Staff): List<Leave> | |||||
fun deleteAllByStaffAndRecordDate(staff: Staff, recordDate: LocalDate) | |||||
} | } |
@@ -0,0 +1,6 @@ | |||||
package com.ffii.tsms.modules.timesheet.entity; | |||||
import com.ffii.core.support.AbstractRepository | |||||
interface LeaveTypeRepository : AbstractRepository<LeaveType, Long> { | |||||
} |
@@ -28,4 +28,7 @@ open class Timesheet : BaseEntity<Long>() { | |||||
@ManyToOne | @ManyToOne | ||||
@JoinColumn(name = "projectTaskId") | @JoinColumn(name = "projectTaskId") | ||||
open var projectTask: ProjectTask? = null | open var projectTask: ProjectTask? = null | ||||
@Column(name = "remark") | |||||
open var remark: String? = null | |||||
} | } |
@@ -0,0 +1,72 @@ | |||||
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.timesheet.entity.* | |||||
import com.ffii.tsms.modules.timesheet.web.models.LeaveEntry | |||||
import org.springframework.stereotype.Service | |||||
import org.springframework.transaction.annotation.Transactional | |||||
import java.time.LocalDate | |||||
import java.time.format.DateTimeFormatter | |||||
@Service | |||||
open class LeaveService( | |||||
private val leaveRepository: LeaveRepository, | |||||
private val leaveTypeRepository: LeaveTypeRepository, | |||||
private val staffsService: StaffsService | |||||
) { | |||||
open fun getLeaveTypes(): List<LeaveType> { | |||||
return leaveTypeRepository.findAll() | |||||
} | |||||
open fun getLeaves(): Map<String, List<LeaveEntry>> { | |||||
val currentStaff = staffsService.currentStaff() ?: return emptyMap() | |||||
return transformToLeaveEntryMap(leaveRepository.findAllByStaff(currentStaff)) | |||||
} | |||||
@Transactional | |||||
open fun saveLeave(recordLeaveEntry: Map<LocalDate, List<LeaveEntry>>): Map<String, List<LeaveEntry>> { | |||||
// Need to be associated with a staff | |||||
val currentStaff = staffsService.currentStaff() ?: throw BadRequestException() | |||||
val leaveTypesMap = getLeaveTypes().associateBy { it.id } | |||||
val leaves = recordLeaveEntry.entries.flatMap { (entryDate, leaveEntries) -> | |||||
// Replace db leave entries by deleting and then adding back | |||||
leaveRepository.deleteAllByStaffAndRecordDate(currentStaff, entryDate) | |||||
mergeLeaveEntriesByType(leaveEntries).map { leaveEntry -> | |||||
Leave().apply { | |||||
this.staff = currentStaff | |||||
this.recordDate = entryDate | |||||
this.leaveType = leaveTypesMap[leaveEntry.leaveTypeId] | |||||
this.leaveHours = leaveEntry.inputHours | |||||
} | |||||
} | |||||
} | |||||
val savedLeaves = leaveRepository.saveAll(leaves) | |||||
return transformToLeaveEntryMap(savedLeaves) | |||||
} | |||||
private fun transformToLeaveEntryMap(leaves: List<Leave>): Map<String, List<LeaveEntry>> { | |||||
return leaves | |||||
.groupBy { leave -> leave.recordDate!!.format(DateTimeFormatter.ISO_LOCAL_DATE) } | |||||
.mapValues { (_, leaveEntries) -> leaveEntries.map { leave -> | |||||
LeaveEntry( | |||||
id = leave.id!!, | |||||
inputHours = leave.leaveHours ?: 0.0, | |||||
leaveTypeId = leave.leaveType!!.id | |||||
) | |||||
} } | |||||
} | |||||
private fun mergeLeaveEntriesByType(entries: List<LeaveEntry>): List<LeaveEntry> { | |||||
return entries | |||||
.groupBy { leaveEntry -> leaveEntry.leaveTypeId } | |||||
.values.map { leaveEntires -> | |||||
leaveEntires.reduce { acc, leaveEntry -> acc.copy(inputHours = acc.inputHours + leaveEntry.inputHours) } | |||||
} | |||||
} | |||||
} |
@@ -16,7 +16,6 @@ import kotlin.jvm.optionals.getOrDefault | |||||
import kotlin.jvm.optionals.getOrNull | import kotlin.jvm.optionals.getOrNull | ||||
@Service | @Service | ||||
open class TimesheetsService( | open class TimesheetsService( | ||||
private val timesheetRepository: TimesheetRepository, | private val timesheetRepository: TimesheetRepository, | ||||
private val projectTaskRepository: ProjectTaskRepository, | private val projectTaskRepository: ProjectTaskRepository, | ||||
@@ -54,7 +53,7 @@ open class TimesheetsService( | |||||
open fun getTimesheet(): Map<String, List<TimeEntry>> { | open fun getTimesheet(): Map<String, List<TimeEntry>> { | ||||
// Need to be associated with a staff | // Need to be associated with a staff | ||||
val currentStaff = staffsService.currentStaff() ?: throw BadRequestException() | |||||
val currentStaff = staffsService.currentStaff() ?: return emptyMap() | |||||
return transformToTimeEntryMap(timesheetRepository.findAllByStaff(currentStaff)) | return transformToTimeEntryMap(timesheetRepository.findAllByStaff(currentStaff)) | ||||
} | } | ||||
@@ -1,7 +1,10 @@ | |||||
package com.ffii.tsms.modules.timesheet.web | package com.ffii.tsms.modules.timesheet.web | ||||
import com.ffii.core.exception.BadRequestException | import com.ffii.core.exception.BadRequestException | ||||
import com.ffii.tsms.modules.timesheet.entity.LeaveType | |||||
import com.ffii.tsms.modules.timesheet.service.LeaveService | |||||
import com.ffii.tsms.modules.timesheet.service.TimesheetsService | import com.ffii.tsms.modules.timesheet.service.TimesheetsService | ||||
import com.ffii.tsms.modules.timesheet.web.models.LeaveEntry | |||||
import com.ffii.tsms.modules.timesheet.web.models.TimeEntry | import com.ffii.tsms.modules.timesheet.web.models.TimeEntry | ||||
import jakarta.validation.Valid | import jakarta.validation.Valid | ||||
import org.springframework.web.bind.annotation.GetMapping | import org.springframework.web.bind.annotation.GetMapping | ||||
@@ -14,21 +17,39 @@ import java.time.format.DateTimeFormatter | |||||
@RestController | @RestController | ||||
@RequestMapping("/timesheets") | @RequestMapping("/timesheets") | ||||
class TimesheetsController(private val timesheetsService: TimesheetsService) { | |||||
class TimesheetsController(private val timesheetsService: TimesheetsService, private val leaveService: LeaveService) { | |||||
@PostMapping("/save") | @PostMapping("/save") | ||||
fun newTimesheetEntry(@Valid @RequestBody recordTimesheet: Map<String, List<TimeEntry>>): Map<String, List<TimeEntry>> { | fun newTimesheetEntry(@Valid @RequestBody recordTimesheet: Map<String, List<TimeEntry>>): Map<String, List<TimeEntry>> { | ||||
val parseDateTimeResult = kotlin.runCatching { | |||||
val parsedEntries = kotlin.runCatching { | |||||
recordTimesheet.mapKeys { (dateString) -> | recordTimesheet.mapKeys { (dateString) -> | ||||
LocalDate.parse(dateString, DateTimeFormatter.ISO_LOCAL_DATE) | LocalDate.parse(dateString, DateTimeFormatter.ISO_LOCAL_DATE) | ||||
} | } | ||||
} | |||||
val parsedEntries = parseDateTimeResult.getOrElse { throw BadRequestException() } | |||||
}.getOrElse { throw BadRequestException() } | |||||
return timesheetsService.saveTimesheet(parsedEntries) | return timesheetsService.saveTimesheet(parsedEntries) | ||||
} | } | ||||
@PostMapping("/saveLeave") | |||||
fun newLeaveEntry(@Valid @RequestBody recordLeave: Map<String, List<LeaveEntry>>): Map<String, List<LeaveEntry>> { | |||||
val parsedEntries = kotlin.runCatching { | |||||
recordLeave.mapKeys { (dateString) -> LocalDate.parse(dateString, DateTimeFormatter.ISO_LOCAL_DATE) } | |||||
}.getOrElse { throw BadRequestException() } | |||||
return leaveService.saveLeave(parsedEntries) | |||||
} | |||||
@GetMapping | @GetMapping | ||||
fun getTimesheetEntry(): Map<String, List<TimeEntry>> { | fun getTimesheetEntry(): Map<String, List<TimeEntry>> { | ||||
return timesheetsService.getTimesheet() | return timesheetsService.getTimesheet() | ||||
} | } | ||||
@GetMapping("/leaves") | |||||
fun getLeaveEntry(): Map<String, List<LeaveEntry>> { | |||||
return leaveService.getLeaves() | |||||
} | |||||
@GetMapping("/leaveTypes") | |||||
fun leaveTypes(): List<LeaveType> { | |||||
return leaveService.getLeaveTypes() | |||||
} | |||||
} | } |
@@ -0,0 +1,7 @@ | |||||
package com.ffii.tsms.modules.timesheet.web.models | |||||
data class LeaveEntry( | |||||
val id: Long, | |||||
val leaveTypeId: Long?, | |||||
val inputHours: Double | |||||
) |
@@ -21,6 +21,7 @@ spring: | |||||
database-platform: org.hibernate.dialect.MySQL8Dialect | database-platform: org.hibernate.dialect.MySQL8Dialect | ||||
properties: | properties: | ||||
hibernate: | hibernate: | ||||
globally_quoted_identifiers: true | |||||
dialect: | dialect: | ||||
storage_engine: innodb | storage_engine: innodb | ||||
@@ -0,0 +1,11 @@ | |||||
-- liquibase formatted sql | |||||
-- changeset wayne:leave_type | |||||
INSERT | |||||
INTO | |||||
leave_type | |||||
(name) | |||||
VALUES | |||||
('Annual Leave'), | |||||
('Sick Leave'), | |||||
('Special Leave'); |
@@ -0,0 +1,5 @@ | |||||
-- liquibase formatted sql | |||||
-- changeset wayne:timesheet_remark | |||||
ALTER TABLE timesheet ADD remark VARCHAR(255) NULL; | |||||
@@ -0,0 +1,4 @@ | |||||
-- liquibase formatted sql | |||||
-- changeset wayne:leave_update | |||||
ALTER TABLE `leave` ADD recordDate date NOT NULL, ADD remark VARCHAR(255) NULL; |