@@ -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<TableRow>): String { | |||
private fun createHTMLTable(naughtyList: MutableList<TableRow>, dateForRed: LocalDate? = LocalDate.of(2024,4,1)): String { | |||
val tableStarter = StringBuilder(" <table width='100%' border='1' align='center'> ") | |||
val header = StringBuilder( | |||
" <tr> " | |||
@@ -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" | |||
} | |||
"<span style='color: $color;'>$localDate</span>" | |||
} | |||
tableStarter.append( | |||
" <tr> " | |||
+ " <td>${per.staffId}</td>" | |||
+ " <td>${per.name}</td>" | |||
+ " <td>${per.missingDates.joinToString(", ")}</td>" | |||
+ " <td>${missDatesHtml}</td>" | |||
+ " </tr> " | |||
) | |||
} | |||
@@ -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<LocalDate>): Sequence<LocalDate> { | |||
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<StaffRecords>, workdays: Sequence<LocalDate>, staffList: List<Staff>): List<StaffWithMissingDates> { | |||
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<LocalDate> = (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<StaffRecords> = 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<Staff> = staffs.filter { it.team != null && it.team.id == team.id } | |||
if (teamMembers.isEmpty()) continue | |||
val teamMembersIds: List<Long> = 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<TableRow>() | |||
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) | |||
} | |||
} | |||
@@ -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") | |||
@@ -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<LocalDate> | |||
) |
@@ -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 | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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<Map<String, Any>> = mutableListOf(); | |||
@@ -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<LocalDate>, | |||
val lastRecordDate: LocalDate | |||
) | |||
private fun getStaffLastRecordDateByDate(date: LocalDate): List<StaffLastRecordData>{ | |||
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<LocalDate>): Sequence<LocalDate> { | |||
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<StaffRecords>, workdays: Sequence<LocalDate>, staffList: List<Staff>): List<StaffLastRecordDataV2> { | |||
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<StaffLastRecordDataV2>{ | |||
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<LocalDate> = (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<StaffRecords> = 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) | |||
@@ -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<Resource>{ | |||
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") | |||
@@ -79,7 +79,8 @@ data class ProjectMonthlyRequest ( | |||
) | |||
data class LastRecordReportRequest ( | |||
val dateString: String | |||
val dateString: String, | |||
val dayRange: Long | |||
) | |||
data class ExportCurrentStaffInfoRequest ( | |||