diff --git a/src/main/java/com/ffii/tsms/modules/common/mail/service/MailReminderService.kt b/src/main/java/com/ffii/tsms/modules/common/mail/service/MailReminderService.kt index ff38e1a..d425551 100644 --- a/src/main/java/com/ffii/tsms/modules/common/mail/service/MailReminderService.kt +++ b/src/main/java/com/ffii/tsms/modules/common/mail/service/MailReminderService.kt @@ -4,6 +4,7 @@ import com.ffii.tsms.modules.common.SettingNames import com.ffii.tsms.modules.common.holiday.service.HolidayService import com.ffii.tsms.modules.common.mail.pojo.MailRequest import com.ffii.tsms.modules.common.mail.web.models.StaffRecords +import com.ffii.tsms.modules.common.mail.web.models.StaffWithMissingDates import com.ffii.tsms.modules.common.mail.web.models.WorkHourRecords import com.ffii.tsms.modules.common.mail.web.models.WorkHourRecordsWithJoinDate import com.ffii.tsms.modules.data.entity.Staff @@ -18,6 +19,7 @@ import com.ffii.tsms.modules.user.service.UserService import jakarta.mail.internet.InternetAddress import org.apache.commons.logging.Log import org.apache.commons.logging.LogFactory +import org.springframework.cglib.core.Local import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Service import java.time.DayOfWeek @@ -58,7 +60,7 @@ open class MailReminderService( return false } } - private fun createHTMLTable(naughtyList: MutableList): String { + private fun createHTMLTable(naughtyList: MutableList, dateForRed: LocalDate? = LocalDate.of(2024,4,1)): String { val tableStarter = StringBuilder(" ") val header = StringBuilder( " " @@ -69,11 +71,21 @@ open class MailReminderService( ) tableStarter.append(header) for (per in naughtyList) { + val missDatesHtml = per.missingDates.joinToString(", "){ + localDate -> + val color = if(localDate.isBefore(LocalDate.now().minusMonths(1))){ + "red" + }else{ + "black" + } + "$localDate" + } + tableStarter.append( " " + " " + " " - + " " + + " " + " " ) } @@ -103,7 +115,7 @@ open class MailReminderService( mailService.send(mailRequestList) } - @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() { logger.info("-----------------------") logger.info("Scheduled Start: 7th email reminder") @@ -153,6 +165,7 @@ open class MailReminderService( key.third // joinDate ) } +// println("**** timesheetByIdAndRecord: ${timesheetByIdAndRecord.filter { it.staffId == 1L }}") val goodStaffsList = filteredLastMonthDays.map { date -> val matchedStaffIds = timesheetByIdAndRecord .filter { @@ -196,6 +209,107 @@ open class MailReminderService( } } + // Function to generate a sequence of workdays, excluding weekends and public holidays + private fun generateWorkdays(startDate: LocalDate, endDate: LocalDate, publicHolidays: Set): Sequence { + return generateSequence(startDate) { date -> + date.plusDays(1).takeIf { it <= endDate } + }.filter { date -> + date.dayOfWeek != DayOfWeek.SATURDAY && + date.dayOfWeek != DayOfWeek.SUNDAY && + !publicHolidays.contains(date) + } + } + + // Function to find missing workdays for staff + private fun findStaffWithMissingDates(staffRecords: List, workdays: Sequence, staffList: List): List { + val staffMap = staffList.associateBy { it.id } + return staffRecords + .groupBy { it.staffId } + .map { (staffId, records) -> + val joinDate = records.firstOrNull()?.joinDate ?: LocalDate.of(1970, 1, 1) + val recordedDates = records.map { it.recordDate }.toSet() + val staffWorkdays = workdays.filter { it >= joinDate }.toList() + + val missingDates = staffWorkdays.filterNot { recordedDates.contains(it) } + + val staffInfo = staffMap[staffId] + + StaffWithMissingDates( + staffId = staffId, + staffIdinCode = staffInfo?.staffId ?: "", + staffName = staffInfo?.name ?: "", + missingDates = missingDates + ) + } + .filter { it.missingDates.isNotEmpty() } + } + + @Scheduled(cron = "0 0 6 7 * ?") // (SS/MM/HH/DD/MM/YY) + open fun sendMissingTimesheetDateToTeamLead7th(){ + logger.info("-----------------------") + logger.info("Scheduled Start: 7th email reminder V2") + + val firstDate = LocalDate.of(2024,10,1) + val today = LocalDate.now() + + val publicHolidays = holidayService.commonHolidayList().map { it.date } + val companyHolidays = companyHolidayService.allCompanyHolidays().map { it.date } + val allHolidays: Set = (publicHolidays + companyHolidays).toSet() + + val workdays = generateWorkdays(firstDate, today, allHolidays) + println("workdays: ${workdays.joinToString { ", " }}") + //Get all missing timesheet date and group by staff + val args = mutableMapOf( + "from" to firstDate, + "to" to today, + ) + + val ts = timesheetsService.workHourRecordsWithinRange(args) + val timesheet: List = ts.map { + WorkHourRecordsWithJoinDate( + staffId = it["staffId"].toString().toLong(), + recordDate = LocalDate.parse(it["recordDate"].toString()), + hours = it["hours"].toString().toDouble(), + joinDate = it["joinDate"]?.toString()?.let { date -> LocalDate.parse(date) } ?: LocalDate.of(1970, 1, 1) + ) + }.groupBy { Triple(it.staffId, it.recordDate, it.joinDate) } + .mapNotNull { (key, records) -> + StaffRecords( + key.second, // recordDate + key.first, // staffId + records.sumOf { it.hours }, + key.third // joinDate + ) + } + + val staffs = staffRepository.findAllByEmployTypeAndDeletedFalseAndDepartDateIsNull(FULLTIME).filter { it.staffId != "A003" && it.staffId != "A004" && it.staffId != "B011" }.filter{ it.team?.code != "HO"} + val teams = teamRepository.findAll().filter { team -> team.deleted == false } + + for (team in teams) { + val teamLead = team.staff + val teamMembers: List = staffs.filter { it.team != null && it.team.id == team.id } + if (teamMembers.isEmpty()) continue + val teamMembersIds: List = teamMembers.map { it.id!! }.sorted() + val filteredTimesheet = timesheet.filter { teamMembersIds.contains(it.staffId) } + val staffWithMissingDates = findStaffWithMissingDates(filteredTimesheet, workdays, teamMembers) +// println("staffWithMissingDates----: $staffWithMissingDates") + val intro = StringBuilder("${teamLead.name}, Staffs Missing Timesheet in the Table Below: \n") + val tableData = mutableListOf() + staffWithMissingDates.forEach{ + val rowData = TableRow(it.staffIdinCode, it.staffName, (it.missingDates).toMutableList()) + tableData.add(rowData) + } + val table = createHTMLTable(tableData) + val emailContent = intro + .append(table) + + val receiver = listOf(teamLead.email) + + createEmailRequest(emailContent.toString(), receiver) + } + + } + // Testing function to print the good staff list open fun test7thStaffList(){ val today = LocalDate.now() @@ -259,7 +373,7 @@ open class MailReminderService( Pair(date, matchedStaffIds) }.sortedBy { it.first } - println(goodStaffsList) +// println(goodStaffsList) } } diff --git a/src/main/java/com/ffii/tsms/modules/common/mail/web/MailController.kt b/src/main/java/com/ffii/tsms/modules/common/mail/web/MailController.kt index 406b493..385b3eb 100644 --- a/src/main/java/com/ffii/tsms/modules/common/mail/web/MailController.kt +++ b/src/main/java/com/ffii/tsms/modules/common/mail/web/MailController.kt @@ -37,7 +37,7 @@ class MailController( } @GetMapping("/test7th") fun test7th() { - mailReminderService.sendTimesheetToTeamLead7TH() + mailReminderService.sendMissingTimesheetDateToTeamLead7th() } @GetMapping("/test15th") fun test15th() { @@ -46,7 +46,7 @@ class MailController( @GetMapping("/test7th-staff-list") fun test7thStaffList(){ - mailReminderService.test7thStaffList() + mailReminderService.sendMissingTimesheetDateToTeamLead7th() } @GetMapping("/test15th-staff-list") diff --git a/src/main/java/com/ffii/tsms/modules/common/mail/web/models/WorkHourRecords.kt b/src/main/java/com/ffii/tsms/modules/common/mail/web/models/WorkHourRecords.kt index 10691ac..b8ab393 100644 --- a/src/main/java/com/ffii/tsms/modules/common/mail/web/models/WorkHourRecords.kt +++ b/src/main/java/com/ffii/tsms/modules/common/mail/web/models/WorkHourRecords.kt @@ -21,3 +21,10 @@ data class StaffRecords( val sumOfHours: Double, val joinDate: LocalDate? = LocalDate.of(1970,1,1) ) + +data class StaffWithMissingDates( + val staffId: Long, + val staffIdinCode: String, + val staffName: String, + val missingDates: List +) diff --git a/src/main/java/com/ffii/tsms/modules/data/service/CustomerService.kt b/src/main/java/com/ffii/tsms/modules/data/service/CustomerService.kt index a0df3dc..39da26f 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/CustomerService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/CustomerService.kt @@ -103,4 +103,22 @@ open class CustomerService( return SaveCustomerResponse(customer = customer, message = "Success"); } + + //Auto generate the latest client code + open fun findMaxCustomerCode(): String{ + val sql = StringBuilder("select MAX(code) as maxCode from customer where code regexp 'CT-'") + val maxCode = jdbcDao.queryForString(sql.toString()) + val regex = "(\\D+)-(\\d+)".toRegex() + val match = regex.matchEntire(maxCode) + + val next = if (match != null) { + val (prefix, numberStr) = match.destructured + val nextNumber = numberStr.toInt() + 1 + val padded = nextNumber.toString().padStart(numberStr.length, '0') + "$prefix-$padded" + } else { + maxCode + } + return next + } } diff --git a/src/main/java/com/ffii/tsms/modules/data/web/CustomerController.kt b/src/main/java/com/ffii/tsms/modules/data/web/CustomerController.kt index 19c0001..df1cd86 100644 --- a/src/main/java/com/ffii/tsms/modules/data/web/CustomerController.kt +++ b/src/main/java/com/ffii/tsms/modules/data/web/CustomerController.kt @@ -8,6 +8,7 @@ import com.ffii.tsms.modules.data.service.CustomerSubsidiaryService import com.ffii.tsms.modules.data.web.models.CustomerResponse import com.ffii.tsms.modules.data.web.models.SaveCustomerResponse import com.ffii.tsms.modules.project.web.models.SaveCustomerRequest +import jakarta.servlet.http.HttpServletResponse import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @@ -56,4 +57,9 @@ class CustomerController( fun saveCustomer(@Valid @RequestBody saveCustomer: SaveCustomerRequest): SaveCustomerResponse { return customerService.saveCustomer(saveCustomer) } + + @GetMapping("/getMaxCode") + fun getMaxCustomerCode(): String{ + return customerService.findMaxCustomerCode(); + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/service/InvoiceService.kt b/src/main/java/com/ffii/tsms/modules/project/service/InvoiceService.kt index ea4754a..d1bffcf 100644 --- a/src/main/java/com/ffii/tsms/modules/project/service/InvoiceService.kt +++ b/src/main/java/com/ffii/tsms/modules/project/service/InvoiceService.kt @@ -609,6 +609,8 @@ open class InvoiceService( if (workbook == null) { return InvoiceResponse(false, "No Excel import", ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList()) // if workbook is null } + + // find the imported Invoice Records val invoiceRecords = repository.findImportInvoiceInfoByAndDeletedFalse() val importInvoices: MutableList> = mutableListOf(); diff --git a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt index 3206391..1ea7ad9 100644 --- a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt @@ -1,7 +1,12 @@ package com.ffii.tsms.modules.report.service import com.ffii.core.support.JdbcDao +import com.ffii.tsms.modules.common.holiday.service.HolidayService +import com.ffii.tsms.modules.common.mail.web.models.StaffRecords +import com.ffii.tsms.modules.common.mail.web.models.StaffWithMissingDates +import com.ffii.tsms.modules.common.mail.web.models.WorkHourRecordsWithJoinDate import com.ffii.tsms.modules.data.entity.* +import com.ffii.tsms.modules.data.service.CompanyHolidayService import com.ffii.tsms.modules.data.service.SalaryEffectiveService import com.ffii.tsms.modules.data.service.SalaryEffectiveService.MonthlyStaffSalaryData import com.ffii.tsms.modules.data.service.StaffsService @@ -10,6 +15,7 @@ import com.ffii.tsms.modules.report.web.model.ExportCurrentStaffInfoRequest import com.ffii.tsms.modules.report.web.model.costAndExpenseRequest import com.ffii.tsms.modules.timesheet.entity.Timesheet import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository +import com.ffii.tsms.modules.timesheet.service.TimesheetsService import org.apache.commons.logging.Log import org.apache.commons.logging.LogFactory import org.apache.poi.ss.usermodel.* @@ -58,6 +64,10 @@ open class ReportService( private val teamLogRepository: TeamLogRepository, private val teamRepository: TeamRepository, private val staffService: StaffsService, + private val holidayService: HolidayService, + private val companyHolidayService: CompanyHolidayService, + private val timesheetsService: TimesheetsService, + private val staffRepository: StaffRepository ) { private val logger: Log = LogFactory.getLog(javaClass) private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd") @@ -3478,6 +3488,7 @@ open class ReportService( if (teamId != null) { sql.append( " and p.teamLead = :teamLeadId " + + " and p.deleted = false " ) } @@ -5729,6 +5740,16 @@ open class ReportService( val team: String, val lastRecordDate: String, ) + + data class StaffLastRecordDataV2( + val staffId: Long, + val staffIdinCode: String, + val staffName: String, + val email: String, + val team: String, + val missingDates: List, + val lastRecordDate: LocalDate + ) private fun getStaffLastRecordDateByDate(date: LocalDate): List{ val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") val sql = StringBuilder( @@ -5760,7 +5781,7 @@ open class ReportService( val currentDate = Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant()) departDate.after(currentDate) } else { - false // Exclude entries with null departDate + true // Include entries with null departDate, null = not departed } } .map { @@ -5777,16 +5798,100 @@ open class ReportService( return results } + // Function to generate a sequence of workdays, excluding weekends and public holidays + private fun generateWorkdays(startDate: LocalDate, endDate: LocalDate, publicHolidays: Set): Sequence { + return generateSequence(startDate) { date -> + date.plusDays(1).takeIf { it <= endDate } + }.filter { date -> + date.dayOfWeek != DayOfWeek.SATURDAY && + date.dayOfWeek != DayOfWeek.SUNDAY && + !publicHolidays.contains(date) + } + } + + // Function to find missing workdays for staff + private fun findStaffWithMissingDates(staffRecords: List, workdays: Sequence, staffList: List): List { + val staffMap = staffList.associateBy { it.id } + return staffRecords + .groupBy { it.staffId } + .map { (staffId, records) -> + val joinDate = records.firstOrNull()?.joinDate ?: LocalDate.of(1970, 1, 1) + val recordedDates = records.map { it.recordDate }.toSet() + val staffWorkdays = workdays.filter { it >= joinDate }.toList() + + val missingDates = staffWorkdays.filterNot { recordedDates.contains(it) } + val workDates = staffWorkdays.filter { recordedDates.contains(it) } + + val staffInfo = staffMap[staffId] + + StaffLastRecordDataV2( + staffId = staffId, + staffIdinCode = staffInfo?.staffId ?: "", + staffName = staffInfo?.name ?: "", + email = staffInfo?.email ?: "", + team = staffInfo?.team?.code ?: "", + missingDates = missingDates, + lastRecordDate = workDates.last() + ) + } + .filter { it.missingDates.isNotEmpty() } + } + private fun getStaffLastRecordDateByDateWithDayRange(date: LocalDate, dayRange: Long): List{ + logger.info("-----------------------") + logger.info("Report Start: Staff Last Record Date By Date With DayRange") + + val today = date + val firstDate = today.minusDays(dayRange) + + val publicHolidays = holidayService.commonHolidayList().map { it.date } + val companyHolidays = companyHolidayService.allCompanyHolidays().map { it.date } + val allHolidays: Set = (publicHolidays + companyHolidays).toSet() + + val workdays = generateWorkdays(firstDate, today, allHolidays) + //Get all missing timesheet date and group by staff + val args = mutableMapOf( + "from" to firstDate, + "to" to today, + ) + logger.info(args) + val ts = timesheetsService.workHourRecordsWithinRange(args) + val timesheet: List = ts.map { + WorkHourRecordsWithJoinDate( + staffId = it["staffId"].toString().toLong(), + recordDate = LocalDate.parse(it["recordDate"].toString()), + hours = it["hours"].toString().toDouble(), + joinDate = it["joinDate"]?.toString()?.let { date -> LocalDate.parse(date) } ?: LocalDate.of(1970, 1, 1) + ) + }.groupBy { Triple(it.staffId, it.recordDate, it.joinDate) } + .mapNotNull { (key, records) -> + StaffRecords( + key.second, // recordDate + key.first, // staffId + records.sumOf { it.hours }, + key.third // joinDate + ) + } + + val staffs = staffRepository.findAllByEmployTypeAndDeletedFalseAndDepartDateIsNull("FT").filter { it.staffId != "A003" && it.staffId != "A004" && it.staffId != "B011" }.filter{ it.team?.code != "HO"} +// val teams = teamRepository.findAll().filter { team -> team.deleted == false } + val filteredTimesheet = timesheet.filter { staffs.map { it.id!! }.sorted().contains(it.staffId) } + val staffWithMissingDates = findStaffWithMissingDates(filteredTimesheet, workdays, staffs) +// logger.info(staffWithMissingDates.sortedBy { it.team }) + return staffWithMissingDates.sortedWith(compareBy({ it.team }, { it.staffId })) + } + @Throws(IOException::class) private fun createLastRecordReportWorkbook( date: LocalDate, + dayRange: Long, templatePath: String, ): Workbook{ val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) - val result = getStaffLastRecordDateByDate(date) +// val result = getStaffLastRecordDateByDate(date) + val result = getStaffLastRecordDateByDateWithDayRange(date, dayRange) println(result) var sheet: Sheet = workbook.getSheetAt(0) @@ -5812,7 +5917,9 @@ open class ReportService( rowIndex = 2 sheet.getRow(rowIndex).getCell(columnIndex+1).apply { - setCellValue(date.format(DATE_FORMATTER)) + val startDate = date.minusDays(dayRange).format(DATE_FORMATTER) + val endDate = date.format(DATE_FORMATTER) + setCellValue("$startDate - $endDate ($dayRange)") } rowIndex = 3 @@ -5830,9 +5937,10 @@ open class ReportService( val emailCell = row.createCell(2) val teamCell = row.createCell(3) val lastRecordDateCell = row.createCell(4) + val numOfMissingDateCell = row.createCell(5) staffIdCell.apply { - setCellValue(it.staffId) + setCellValue(it.staffIdinCode) cellStyle =blackFontStyle } @@ -5852,7 +5960,12 @@ open class ReportService( } lastRecordDateCell.apply { - setCellValue(it.lastRecordDate) + setCellValue(it.lastRecordDate.toString()) + cellStyle =blackFontStyle + } + + numOfMissingDateCell.apply { + setCellValue(it.missingDates.size.toDouble()) // count total number of missing date in list cellStyle =blackFontStyle } @@ -5864,8 +5977,9 @@ open class ReportService( fun genLastRecordReport( date: LocalDate, + dayRange: Long ): ByteArray{ - val workbook = createLastRecordReportWorkbook(date, LAST_RECORD_REPORT) + val workbook = createLastRecordReportWorkbook(date, dayRange, LAST_RECORD_REPORT) val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() workbook.write(outputStream) diff --git a/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt b/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt index 2ea4633..3d4477a 100644 --- a/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt +++ b/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt @@ -387,16 +387,17 @@ class ReportController( val reportResult: ByteArray = excelReportService.genProjectMonthlyReport(request.yearMonth, request.projectId, request.holidays) return ResponseEntity.ok() - .header("filename", "Project Monthly Report - " + LocalDate.now() + ".xlsx") + .header("filename", "Project Monthly daily Summary Report - " + LocalDate.now() + ".xlsx") .body(ByteArrayResource(reportResult)) } @PostMapping("/gen-staff-last-record-report") fun genLastRecordReport(@RequestBody @Valid request: LastRecordReportRequest): ResponseEntity{ println("================= ${request.dateString}") + println("================= ${request.dayRange}") val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") val date = LocalDate.parse(request.dateString, formatter) - val reportResult: ByteArray = excelReportService.genLastRecordReport(date) + val reportResult: ByteArray = excelReportService.genLastRecordReport(date, request.dayRange) return ResponseEntity.ok() .header("filename", "Staff Last Record Report - " + LocalDate.now() + ".xlsx") diff --git a/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt b/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt index f7a687e..29bd339 100644 --- a/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt +++ b/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt @@ -79,7 +79,8 @@ data class ProjectMonthlyRequest ( ) data class LastRecordReportRequest ( - val dateString: String + val dateString: String, + val dayRange: Long ) data class ExportCurrentStaffInfoRequest ( diff --git a/src/main/resources/templates/report/AR10_Staff Last Record Report.xlsx b/src/main/resources/templates/report/AR10_Staff Last Record Report.xlsx index 37601e1..f2b8141 100644 Binary files a/src/main/resources/templates/report/AR10_Staff Last Record Report.xlsx and b/src/main/resources/templates/report/AR10_Staff Last Record Report.xlsx differ
${per.staffId}${per.name}${per.missingDates.joinToString(", ")}${missDatesHtml}