| @@ -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; | |||||