@@ -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) | ||||
} | } | ||||
@@ -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>> { | ||||
@@ -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() | |||||
} | |||||
} | } |
@@ -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") | ||||
@@ -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> | |||||
) | ) |
@@ -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> | |||||
} | } |