Browse Source

7th, 15th mail reminder

Dashboard filter by team
Man hour daily report
develop
MSI\2Fi 3 months ago
parent
commit
83c634d0b6
7 changed files with 290 additions and 9 deletions
  1. +8
    -1
      src/main/java/com/ffii/tsms/modules/common/mail/service/MailReminderService.kt
  2. +26
    -5
      src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt
  3. +230
    -3
      src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt
  4. +17
    -0
      src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt
  5. +6
    -0
      src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt
  6. +3
    -0
      src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt
  7. BIN
      src/main/resources/templates/report/AR09_Project Daily Work Hours Analysis Report.xlsx

+ 8
- 1
src/main/java/com/ffii/tsms/modules/common/mail/service/MailReminderService.kt View File

@@ -105,6 +105,8 @@ open class MailReminderService(


@Scheduled(cron = "0 0 6 7 * ?") // (SS/MM/HH/DD/MM/YY) @Scheduled(cron = "0 0 6 7 * ?") // (SS/MM/HH/DD/MM/YY)
open fun sendTimesheetToTeamLead7TH() { open fun sendTimesheetToTeamLead7TH() {
logger.info("-----------------------")
logger.info("Scheduled Start: 7th email reminder")
if (!isSettingsConfigValid()) return if (!isSettingsConfigValid()) return
val today = LocalDate.now() val today = LocalDate.now()
val holidayList = holidayService.commonHolidayList().map { it.date } val holidayList = holidayService.commonHolidayList().map { it.date }
@@ -189,6 +191,7 @@ open class MailReminderService(
.append(table) .append(table)


val receiver = listOf(teamLead.email) val receiver = listOf(teamLead.email)

createEmailRequest(emailContent.toString(), receiver) createEmailRequest(emailContent.toString(), receiver)
} }
} }
@@ -263,6 +266,8 @@ open class MailReminderService(
// @Scheduled(cron = "0 51 14 * * ?") // (SS/MM/HH/DD/MM/YY) // @Scheduled(cron = "0 51 14 * * ?") // (SS/MM/HH/DD/MM/YY)
@Scheduled(cron = "0 0 6 15 * ?") // (SS/MM/HH/DD/MM/YY) @Scheduled(cron = "0 0 6 15 * ?") // (SS/MM/HH/DD/MM/YY)
open fun sendTimesheetToTeamLead15TH() { open fun sendTimesheetToTeamLead15TH() {
logger.info("-----------------------")
logger.info("Scheduled Start: 15th email reminder")
if (!isSettingsConfigValid()) return if (!isSettingsConfigValid()) return
val today = LocalDate.now().minusDays(1) // should always be 14, exclude 15th because the email is sent at 0600, suppose no one input timesheet in advance val today = LocalDate.now().minusDays(1) // should always be 14, exclude 15th because the email is sent at 0600, suppose no one input timesheet in advance
val firstDay = today.withDayOfMonth(1) val firstDay = today.withDayOfMonth(1)
@@ -508,9 +513,11 @@ open class MailReminderService(
message.append("\n [$key] ") message.append("\n [$key] ")
} }
} }
println(message)
// println(message)
if (!isNaughty) return@forEach if (!isNaughty) return@forEach
val emailAddress = staffRepository.findById(id).get().email val emailAddress = staffRepository.findById(id).get().email
// val staffId = staffRepository.findById(id).get().staffId
println(emailAddress)
val receiver = listOf(emailAddress) val receiver = listOf(emailAddress)
createEmailRequest(message.toString(), receiver) createEmailRequest(message.toString(), receiver)
} }


+ 26
- 5
src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt View File

@@ -466,9 +466,18 @@ open class DashboardService(
+ " LEFT JOIN milestone_payment mp ON m.id = mp.milestoneId" + " LEFT JOIN milestone_payment mp ON m.id = mp.milestoneId"
) )


if (args["teamLeadId"] != null && args["teamLeadId"].toString().toInt() > 0) {
sql.append(" WHERE p.teamLead = :teamLeadId")
if (viewDashboardAuthority() == "self") {
val teamId = staffsService.currentStaff()?.team?.id
if (teamId != null) {
sql.append(" WHERE p.teamLead = $teamId")
}
}else{
if (args["teamLeadId"] != null && args["teamLeadId"].toString().toInt() > 0) {
// println("if 1 ${args["teamLeadId"]}")
sql.append(" WHERE p.teamLead = :teamLeadId")
}
} }

sql.append(" AND p.status NOT IN ('Pending to Start', 'Completed', 'Deleted')" sql.append(" AND p.status NOT IN ('Pending to Start', 'Completed', 'Deleted')"
+ " AND mp.date >= CURDATE()" + " AND mp.date >= CURDATE()"
+ " ) AS subquery" + " ) AS subquery"
@@ -478,9 +487,20 @@ open class DashboardService(
+ " WHERE p.deleted = false " + " WHERE p.deleted = false "


) )
if (args["teamLeadId"] != null && args["teamLeadId"].toString().toInt() > 0) {
sql.append(" and p.teamLead = :teamLeadId")

if (viewDashboardAuthority() == "self") {
val teamId = staffsService.currentStaff()?.team?.id
if (teamId != null) {
sql.append(" and p.teamLead = $teamId")
}
}else{
if (args["teamLeadId"] != null && args["teamLeadId"].toString().toInt() > 0) {
// println("if 2 ${args["teamLeadId"]}")
sql.append(" and p.teamLead = :teamLeadId")
}
} }


sql.append(" and p.status not in (\"Pending to Start\",\"Completed\",\"Deleted\")" sql.append(" and p.status not in (\"Pending to Start\",\"Completed\",\"Deleted\")"
// + " and (tg.name != '5. Miscellaneous' or tg.name is null)" // + " and (tg.name != '5. Miscellaneous' or tg.name is null)"
+ " group by p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone, taskGroup.expectedStage" + " group by p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone, taskGroup.expectedStage"
@@ -1410,7 +1430,6 @@ open class DashboardService(
+ " left join subsidiary s2 on p.customerSubsidiaryId = s2.id" + " left join subsidiary s2 on p.customerSubsidiaryId = s2.id"
+ " where p.deleted = 0" + " where p.deleted = 0"
+ " and p.status = 'On-going'" + " and p.status = 'On-going'"
+ " order by p.code"
) )


if (viewDashboardAuthority() == "self") { if (viewDashboardAuthority() == "self") {
@@ -1420,6 +1439,8 @@ open class DashboardService(
} }
} }


sql.append(" order by p.code")

return jdbcDao.queryForList(sql.toString(), args) return jdbcDao.queryForList(sql.toString(), args)
} }
fun CashFlowMonthlyIncomeByMonth(args: Map<String, Any>): List<Map<String, Any>> { fun CashFlowMonthlyIncomeByMonth(args: Map<String, Any>): List<Map<String, Any>> {


+ 230
- 3
src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt View File

@@ -33,10 +33,8 @@ import java.util.*
import java.awt.Color import java.awt.Color
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
import java.math.RoundingMode import java.math.RoundingMode
import java.time.Year
import javax.swing.plaf.synth.Region
import java.time.DayOfWeek
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.collections.HashMap




data class DayInfo(val date: String?, val weekday: String?) data class DayInfo(val date: String?, val weekday: String?)
@@ -76,6 +74,7 @@ open class ReportService(
private val COMPLETION_PROJECT = "templates/report/AR05_Project Completion Report.xlsx" private val COMPLETION_PROJECT = "templates/report/AR05_Project Completion Report.xlsx"
private val CROSS_TEAM_CHARGE_REPORT = "templates/report/Cross Team Charge Report.xlsx" private val CROSS_TEAM_CHARGE_REPORT = "templates/report/Cross Team Charge Report.xlsx"
private val PROJECT_MANHOUR_SUMMARY = "templates/report/Project Manhour Summary.xlsx" private val PROJECT_MANHOUR_SUMMARY = "templates/report/Project Manhour Summary.xlsx"
private val PROJECT_MONTHLY_REPORT = "templates/report/AR09_Project Daily Work Hours Analysis Report.xlsx"


private fun cellBorderArgs(top: Int, bottom: Int, left: Int, right: Int): MutableMap<String, Any> { private fun cellBorderArgs(top: Int, bottom: Int, left: Int, right: Int): MutableMap<String, Any> {
var cellBorderArgs = mutableMapOf<String, Any>() var cellBorderArgs = mutableMapOf<String, Any>()
@@ -5425,4 +5424,232 @@ open class ReportService(
ProjectSummary(staffData, projectCumulativeExpenditure) ProjectSummary(staffData, projectCumulativeExpenditure)
} }
} }

data class DateInfo(
val date: LocalDate?,
val isHoliday: Boolean
)

fun convertStringToLocalDate(dateStrings: List<String>, pattern: String = "dd/MM/yyyy"): List<LocalDate> {
// Define the formatter for the date string (dd/MM/yyyy format)
val formatter = DateTimeFormatter.ofPattern(pattern)

// Convert each string in the list to LocalDate
return dateStrings.mapNotNull { dateString ->
try {
LocalDate.parse(dateString, formatter)
} catch (e: DateTimeParseException) {
println("Invalid date format: $dateString") // Log the invalid date
null // Exclude invalid dates from the result
}
}
}
// Function to generate all dates in a month
fun generateDatesForMonthWithHolidays(yearMonth: YearMonth, holidays: List<LocalDate>): List<DateInfo> {

return (1..yearMonth.lengthOfMonth()).map { day ->
val date = yearMonth.atDay(day)
val isHoliday = date in holidays || date.dayOfWeek == DayOfWeek.SATURDAY || date.dayOfWeek == DayOfWeek.SUNDAY
DateInfo(date, isHoliday)
}
}

fun columnIndexToLetter(columnIndex: Int): String {
val letters = StringBuilder()
var index = columnIndex

// Convert to Excel column letters (1-based index)
while (index > 0) {
val letterIndex = (index - 1) % 26
letters.insert(0, ('A' + letterIndex).toChar())
index = (index - letterIndex) / 26
}

return letters.toString()
}

@Throws(IOException::class)
private fun createProjectMonthlyWorkbook(
yearMonth: YearMonth,
project: Project,
timesheets: List<Timesheet>,
monthList: List<DateInfo>,
templatePath: String,
): Workbook {
val resource = ClassPathResource(templatePath)
val templateInputStream = resource.inputStream
val workbook: Workbook = XSSFWorkbook(templateInputStream)

val monthStyle = workbook.createDataFormat().getFormat("MMM YYYY")
val formatter = DateTimeFormatter.ofPattern("MMM yyyy")

// Style Related
val redFont = workbook.createFont().apply {
color = IndexedColors.RED.index
fontHeightInPoints = 12
}

val blackFont = workbook.createFont().apply {
color = IndexedColors.BLACK.index
fontHeightInPoints = 12
}

val redFontStyle = workbook.createCellStyle().apply {
setFont(redFont)
borderTop = BorderStyle.THIN
borderBottom = BorderStyle.THIN
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
}

val blackFontStyle = workbook.createCellStyle().apply {
setFont(blackFont)
borderTop = BorderStyle.THIN
borderBottom = BorderStyle.THIN
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
}

val yellowBackgroundStyle = workbook.createCellStyle().apply {
setFont(blackFont)
borderTop = BorderStyle.THIN
borderBottom = BorderStyle.THIN
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN

fillForegroundColor = IndexedColors.YELLOW.index
fillPattern = FillPatternType.SOLID_FOREGROUND
}

var sheet: Sheet = workbook.getSheetAt(0)

val staffs : List<Staff> = timesheets.mapNotNull { it.staff }.distinct()
val timesheetRecordsByStaff = timesheets.groupBy ({it.staff}, {it})
.mapValues { (_, records) ->
records.groupBy( {it.recordDate}, {(it.normalConsumed ?: 0.0) + (it.otConsumed ?: 0.0)})
.mapValues { it.value.sum() }
}


var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field
var columnIndex = 1

sheet.getRow(rowIndex).createCell(columnIndex+1).apply {
setCellValue(FORMATTED_TODAY)
}

val startRowIndex = 6
val startColumnIndex = 2

rowIndex = 2
sheet.getRow(rowIndex).getCell(columnIndex+1).apply {
setCellValue(yearMonth.format(formatter))
cellStyle.dataFormat = monthStyle
}

rowIndex = 3
sheet.getRow(rowIndex).getCell(columnIndex+1).apply {
setCellValue( "${project.code} - ${project.name}" )
}

// date header row
rowIndex = 5
columnIndex = 2
for (date in monthList){
val row = sheet.getRow(rowIndex)
val dateCell = row.createCell(columnIndex)
dateCell.apply {
setCellValue(date.date?.format(DateTimeFormatter.ofPattern("dd")))
cellStyle = blackFontStyle
if(date.isHoliday){
cellStyle.apply { cellStyle = redFontStyle }
}
}
sheet.autoSizeColumn(columnIndex)
columnIndex++
}

//
rowIndex = 6
timesheetRecordsByStaff.forEach{(staff, dates) ->
val row = sheet.getRow(rowIndex)
val staffIdCell = row.createCell(0)
val staffNameCell = row.createCell(1)
staffIdCell.apply {
setCellValue(staff?.staffId ?: "")
cellStyle =blackFontStyle
}
staffNameCell.apply {
setCellValue(staff?.name ?: "")
cellStyle =blackFontStyle
}


var recordDateCol = 2
monthList.forEachIndexed { index, dateInfo ->
val recordDateByStaffCell = row.createCell(2+index)
recordDateByStaffCell.cellStyle = blackFontStyle
if(dates.keys.contains(dateInfo.date)){
recordDateByStaffCell.setCellValue(dates.getValue(dateInfo.date))
}else{
recordDateByStaffCell.setCellValue("-")
}
recordDateCol++
}
rowIndex++
}

val lastRowIndex = rowIndex
val totalRow = sheet.getRow(lastRowIndex)

// calculate the sum of work hours spent per day
val numberOfColumnns = monthList.size
val lastColumnIndex = startColumnIndex + numberOfColumnns
for (colIndex in startColumnIndex until lastColumnIndex){
val columnLetter = columnIndexToLetter(colIndex + 1)
val totalSumFormula = "SUM($columnLetter${6 + 1}:$columnLetter${lastRowIndex})" //
val totalSumCell = totalRow.createCell(colIndex)
totalSumCell.apply {
cellFormula = totalSumFormula
cellStyle = yellowBackgroundStyle
}
}

// calculate the sum of work hour per staff
val startColumnLetter = columnIndexToLetter(startColumnIndex)
val lastColumnLetter = columnIndexToLetter(lastColumnIndex)
for (index in startRowIndex .. lastRowIndex){
val staffTotalSumFormula = "SUM($startColumnLetter${index+1}:$lastColumnLetter${index+1})" // excel start from 1, index start from 0
val staffTotalRow = sheet.getRow(index)
val staffTotalRowCell = staffTotalRow.createCell(lastColumnIndex)
staffTotalRowCell.apply {
cellFormula = staffTotalSumFormula
cellStyle = yellowBackgroundStyle
}
}

return workbook
}

fun genProjectMonthlyReport(
yearMonth: YearMonth,
projectId: Long,
holidays: List<String>,
): ByteArray{

val project = projectRepository.findById(projectId).get();

val year = yearMonth.year
val month = yearMonth.monthValue
val timesheetRecords : List<Timesheet> = timesheetRepository.findByYearAndMonthAndProjectId(year, month, projectId)
val monthList = generateDatesForMonthWithHolidays(yearMonth, convertStringToLocalDate(holidays))

val workbook: Workbook = createProjectMonthlyWorkbook(yearMonth, project, timesheetRecords, monthList, PROJECT_MONTHLY_REPORT)

val outputStream: ByteArrayOutputStream = ByteArrayOutputStream()
workbook.write(outputStream)
workbook.close()

return outputStream.toByteArray()
}
} }

+ 17
- 0
src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt View File

@@ -35,6 +35,7 @@ import java.time.format.DateTimeFormatter
import com.ffii.tsms.modules.project.entity.Project import com.ffii.tsms.modules.project.entity.Project
import com.ffii.tsms.modules.project.service.SubsidiaryService import com.ffii.tsms.modules.project.service.SubsidiaryService
import com.ffii.tsms.modules.report.web.model.* import com.ffii.tsms.modules.report.web.model.*
import com.ffii.tsms.modules.timesheet.entity.Timesheet
import org.apache.commons.logging.Log import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory import org.apache.commons.logging.LogFactory
import org.springframework.data.domain.Example import org.springframework.data.domain.Example
@@ -370,6 +371,22 @@ class ReportController(
// return ResponseEntity.noContent().build() // return ResponseEntity.noContent().build()
// } // }
} }
@PostMapping("/project-monthly-report")
fun getMonthDateList(@RequestBody @Valid request: ProjectMonthlyRequest){
println("-------getMonthDateList----------")
println(request)

// println(excelReportService.generateDatesForMonthWithHolidays(request.yearMonth, excelReportService.convertStringToLocalDate(request.holidays)))
}

@PostMapping("/gen-project-monthly-report")
fun genProjectMonthlyReport(@RequestBody @Valid request: ProjectMonthlyRequest): ResponseEntity<Resource>{
val reportResult: ByteArray = excelReportService.genProjectMonthlyReport(request.yearMonth, request.projectId, request.holidays)

return ResponseEntity.ok()
.header("filename", "Project Monthly Report - " + LocalDate.now() + ".xlsx")
.body(ByteArrayResource(reportResult))
}


// API for testing data of total cumulative expenditure // API for testing data of total cumulative expenditure
// @GetMapping("/test") // @GetMapping("/test")


+ 6
- 0
src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt View File

@@ -68,4 +68,10 @@ data class ProjectManhourSummaryRequest (
val startDate: LocalDate, val startDate: LocalDate,
val endDate: LocalDate, val endDate: LocalDate,
val teamId: Any val teamId: Any
)

data class ProjectMonthlyRequest (
val projectId: Long,
val yearMonth: YearMonth,
val holidays: List<String>
) )

+ 3
- 0
src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt View File

@@ -35,4 +35,7 @@ interface TimesheetRepository : AbstractRepository<Timesheet, Long> {
fun findMinRecordDateByProjectId(projectId: Long): LocalDate? fun findMinRecordDateByProjectId(projectId: Long): LocalDate?


fun findAllByDeletedFalseAndRecordDate(recordDate: LocalDate): List<Timesheet> fun findAllByDeletedFalseAndRecordDate(recordDate: LocalDate): List<Timesheet>

@Query("SELECT t FROM Timesheet t WHERE YEAR(t.recordDate) = ?1 AND MONTH(t.recordDate) = ?2 and t.project.id = ?3 order by t.recordDate, t.staff.id")
fun findByYearAndMonthAndProjectId(year: Int, month: Int, projectId: Long): List<Timesheet>
} }

BIN
src/main/resources/templates/report/AR09_Project Daily Work Hours Analysis Report.xlsx View File


Loading…
Cancel
Save