Ver código fonte

update dashboard & report

add_swagger
MSI\derek 11 meses atrás
pai
commit
c0d092a7b4
5 arquivos alterados com 417 adições e 8 exclusões
  1. +4
    -0
      src/main/java/com/ffii/tsms/modules/data/entity/GradeLogRepository.java
  2. +12
    -2
      src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt
  3. +397
    -4
      src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt
  4. +4
    -2
      src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt
  5. BIN
      src/main/resources/templates/report/Cross Team Charge Report.xlsx

+ 4
- 0
src/main/java/com/ffii/tsms/modules/data/entity/GradeLogRepository.java Ver arquivo

@@ -3,9 +3,13 @@ package com.ffii.tsms.modules.data.entity;
import com.ffii.core.support.AbstractRepository;
import com.ffii.tsms.modules.data.entity.projections.GradeLogInfo;

import java.time.LocalDate;
import java.util.List;

public interface GradeLogRepository extends AbstractRepository<GradeLog, Long> {
List<GradeLogInfo> findGradeLogInfoByStaffIdAndDeletedFalseOrderByCreatedDesc(Long staffId);
GradeLog findFirstByStaffIdAndDeletedFalseOrderByCreatedDesc(Long staffId);

List<GradeLog> findByDeletedFalseAndFromBeforeAndToIsNullOrToAfter(LocalDate before, LocalDate after);
// fun findByDeletedFalseAndRecordDateBetweenOrderByRecordDate(start: LocalDate, end: LocalDate): List<Timesheet>
}

+ 12
- 2
src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt Ver arquivo

@@ -2217,11 +2217,19 @@ open class DashboardService(
// + " ) as expenditure on 1 = 1"
// + " where p.id = :projectId"
// + " group by expenditure.expenditure"
" select"
"with pe_cte as ("
+ " select "
+ " pe.projectId "
+ " ,sum(pe.amount) as expense "
+ " from project_expense pe "
+ " where pe.deleted = false "
+ " group by pe.projectId "
+ " ) "
+ " select"
+ " concat(p.code,'-',p.name) as projectCodeAndName,"
+ " p.expectedTotalFee as totalFee,"
+ " (p.expectedTotalFee - ifnull(p.subContractFee, 0)) * 0.8 as totalBudget,"
+ " coalesce (expenditure.expenditure,0) as expenditure,"
+ " coalesce (expenditure.expenditure,0) + coalesce(pc.expense, 0) as expenditure, "
+ " ((p.expectedTotalFee - ifnull(p.subContractFee, 0)) * 0.8) - coalesce (expenditure.expenditure,0) as remainingBudget,"
+ " case"
+ " when p.expectedTotalFee - coalesce (expenditure.expenditure,0) >= 0 then 'Within Budget'"
@@ -2230,7 +2238,9 @@ open class DashboardService(
+ " p.totalManhour as plannedResources,"
+ " sum(ifnull(t.normalConsumed, 0) + ifnull(t.otConsumed, 0)) as resourcesSpent,"
+ " p.totalManhour - sum(ifnull(t.normalConsumed, 0) + ifnull(t.otConsumed, 0)) as remainingResources"
+ " , pc.expense"
+ " from project p"
+ " left join pe_cte pc on pc.projectId = p.id "
+ " left join project_task pt on p.id = pt.project_id"
+ " left join timesheet t on pt.id = t.projectTaskId"
+ " left join("


+ 397
- 4
src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt Ver arquivo

@@ -33,17 +33,23 @@ import java.util.*
import java.awt.Color
import java.math.RoundingMode
import java.time.Year
import javax.swing.plaf.synth.Region
import kotlin.collections.ArrayList


data class DayInfo(val date: String?, val weekday: String?)

data class TsData(var manhour: Double, var cost: Double)
data class InOut(var In: TsData, var Out: TsData)

@Service
open class ReportService(
private val jdbcDao: JdbcDao,
private val projectRepository: ProjectRepository,
private val salaryEffectiveService: SalaryEffectiveService, private val salaryRepository: SalaryRepository, private val timesheetRepository: TimesheetRepository
private val salaryEffectiveService: SalaryEffectiveService,
private val salaryEffectiveRepository: SalaryEffectiveRepository,
private val salaryRepository: SalaryRepository,
private val timesheetRepository: TimesheetRepository,
private val teamLogRepository: TeamLogRepository
) {
private val logger: Log = LogFactory.getLog(javaClass)
private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd")
@@ -63,6 +69,45 @@ open class ReportService(
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 fun cellBorderArgs(top: Int, bottom: Int, left: Int, right: Int): MutableMap<String, Any> {
var cellBorderArgs = mutableMapOf<String, Any>()
when (top) {
1 -> cellBorderArgs.put(CellUtil.BORDER_TOP, BorderStyle.THIN)
2 -> cellBorderArgs.put(CellUtil.BORDER_TOP, BorderStyle.THICK)
}
when (bottom) {
1 -> cellBorderArgs.put(CellUtil.BORDER_BOTTOM, BorderStyle.THIN)
2 -> cellBorderArgs.put(CellUtil.BORDER_BOTTOM, BorderStyle.THICK)
}
when (left) {
1 -> cellBorderArgs.put(CellUtil.BORDER_LEFT, BorderStyle.THIN)
2 -> cellBorderArgs.put(CellUtil.BORDER_LEFT, BorderStyle.THICK)
}
when (right) {
1 -> cellBorderArgs.put(CellUtil.BORDER_RIGHT, BorderStyle.THIN)
2 -> cellBorderArgs.put(CellUtil.BORDER_RIGHT, BorderStyle.THICK)
}
return cellBorderArgs
}
private fun setRegionBorders(top: Int, bottom: Int, left: Int, right: Int, region: CellRangeAddress, sheet: Sheet) {
when (top) {
1 -> RegionUtil.setBorderTop(BorderStyle.THIN, region, sheet)
2 -> RegionUtil.setBorderTop(BorderStyle.THICK, region, sheet)
}
when (bottom) {
1 -> RegionUtil.setBorderBottom(BorderStyle.THIN, region, sheet)
2 -> RegionUtil.setBorderBottom(BorderStyle.THICK, region, sheet)
}
when (left) {
1 -> RegionUtil.setBorderLeft(BorderStyle.THIN, region, sheet)
2 -> RegionUtil.setBorderLeft(BorderStyle.THICK, region, sheet)
}
when (right) {
1 -> RegionUtil.setBorderRight(BorderStyle.THIN, region, sheet)
2 -> RegionUtil.setBorderRight(BorderStyle.THICK, region, sheet)
}
}

private val chargeFee = 1.15
private fun conditionalFormattingNegative(sheet: Sheet) {
// Create a conditional formatting rule
@@ -330,6 +375,7 @@ open class ReportService(
grades: List<Grade>,
monthlyStaffSalaryEffective: List<MonthlyStaffSalaryData>,
teamId: String,
gradeLog: List<GradeLog>,
): ByteArray {
// Generate the Excel report with query results
val workbook: Workbook =
@@ -340,6 +386,7 @@ open class ReportService(
grades,
monthlyStaffSalaryEffective,
teamId,
gradeLog,
CROSS_TEAM_CHARGE_REPORT
)

@@ -3547,6 +3594,342 @@ open class ReportService(
return jdbcDao.queryForList(sql.toString(), args)
}

private fun generateTeamBlock(
teams: List<Team>,
sortedTeams: MutableList<Team>,
grades: List<Grade>,
timesheets: List<Timesheet>,
gradeLog: List<GradeLog>,
salaryEffective: List<SalaryEffective>,
): MutableMap<String, MutableMap<String, InOut>> {
// val teamlog = teamLogRepository.findAll().filter { it.deleted == false }
val gradeMap: MutableMap<String, InOut> = mutableMapOf()
var teamsMap: MutableMap<String, MutableMap<String, InOut>> = mutableMapOf()
grades.forEach {
val key = it.code
if (key !in gradeMap) {
gradeMap[key] = InOut(
TsData(0.0, 0.0),
TsData(0.0, 0.0)
)
}
}
teams.forEach { team ->
val key = team.code
if (key !in teamsMap) {
// Create a new map for each team with copies of the entries from gradeMap
teamsMap[key] = gradeMap.mapValues { (_, value) ->
InOut(
TsData(value.In.manhour, value.In.cost),
TsData(value.Out.manhour, value.Out.cost)
)
}.toMutableMap()
}
}
println("teamsMap")
println(gradeMap)
// teamsMap["TW"]!!["1"]!!.In.manhour += 1000
println(teamsMap)
sortedTeams.forEach { team ->
val currentTeam = team.code
val _timesheets = timesheets.filter { ts ->
// for team log
// val thisTeam = teamlog.filter {
// it.from.isBefore(ts.recordDate)
// && it.to != null
// && it.staff.id == ts.staff!!.id
// }.maxByOrNull { it.from }
ts.project!!.teamLead!!.team.code == currentTeam // check isChargingTeam
&&
ts.project!!.teamLead!!.team.code != ts.staff!!.team.code // check isCrossTeam
// ts.project!!.teamLead!!.team.id != thisTeam!!.team.id // for team log
}
_timesheets.forEach {ts ->
// this team charging others
// get the grade and salary data of the record
val _grade = gradeLog.find { it.staff.id == ts.staff!!.id }
val gradeCode = _grade!!.grade.code
val otMultiplier = 1
val thisSE = salaryEffective.filter {
it.staff.id == ts.staff!!.id
&&
it.date.isBefore(ts.recordDate)
}.maxByOrNull { it.date }
val normalHour = ts.normalConsumed ?: 0.0
val otHour = ts.otConsumed ?: 0.0
val normalCost = normalHour.times(thisSE!!.salary.hourlyRate.toDouble())
val otCost = otHour.times(thisSE.salary.hourlyRate.toDouble()).times(otMultiplier)
//assigning data
val projectTeam = ts.project!!.teamLead!!.team.code
val staffTeam = ts.staff!!.team.code
// write in
println("putting in")
var tsInData = teamsMap[projectTeam]!![gradeCode]!!.In
println(tsInData)
tsInData.manhour += normalHour + otHour
tsInData.cost += normalCost + otCost
// write out
println("putting out")
val tsOutData = teamsMap[staffTeam]!!.get(gradeCode)!!.Out
tsOutData.manhour += normalHour + otHour
tsOutData.cost += normalCost + otCost
}
}
println("all team - gradeMap")
println(teamsMap)
return teamsMap
}


private fun alignTopCenter(tempCell: Cell) {
CellUtil.setVerticalAlignment(tempCell, VerticalAlignment.TOP)
CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER)
}
private fun createCrossTeamForm(
workbook: Workbook,
sheet: Sheet,
_rowIndex: Int,
_columnIndex: Int,
teamsMap: MutableMap<String, MutableMap<String, InOut>>
) {
var rowIndex = _rowIndex
var columnIndex = _columnIndex
var tempRow: Row
var tempCell: Cell

val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)")

fun dataFormatArgs(accountingStyle: Short): MutableMap<String, Any> {
val dataFormatArgs = mutableMapOf<String, Any>(
CellUtil.DATA_FORMAT to accountingStyle
)
return dataFormatArgs
}

fun fontArgs(isBold: Boolean): MutableMap<String, Any>{
val font = sheet.workbook.createFont()
font.bold = isBold
font.fontName = "Times New Roman"
val fontArgs = mutableMapOf<String, Any>(
CellUtil.FONT to font.index,
CellUtil.WRAP_TEXT to true,
)
return fontArgs
}

val InHourLetter = CellReference.convertNumToColString(3) // col D
val InCostLetter = CellReference.convertNumToColString(4) // col E
val OutHourLetter = CellReference.convertNumToColString(5) // col F
val OutCostLetter = CellReference.convertNumToColString(6) // col G
val NetHourLetter = CellReference.convertNumToColString(7) // col G
val NetCostLetter = CellReference.convertNumToColString(8) // col G
val subTotalRowIndexList = mutableListOf<Int>()

teamsMap.forEach{ (teamCode, gradeMap) ->
columnIndex = 0
tempRow = getOrCreateRow(sheet, rowIndex)
tempCell = getOrCreateCell(tempRow, columnIndex)
tempCell.setCellValue(teamCode)
alignTopCenter(tempCell)
val teamCodeRegion = CellRangeAddress(rowIndex, rowIndex + 5,0,0)
sheet.addMergedRegion(teamCodeRegion)
setRegionBorders(2,2,1,1, teamCodeRegion, sheet)
CellUtil.setCellStyleProperties(tempCell, fontArgs(false))
columnIndex++

val gradeRegion = CellRangeAddress(rowIndex, rowIndex + 4,1,1)
tempCell = getOrCreateCell(tempRow, columnIndex)
tempCell.setCellValue("Grade")
setRegionBorders(1,1,1,1, gradeRegion, sheet)
CellUtil.setCellStyleProperties(tempCell, fontArgs(true))
alignTopCenter(tempCell)
sheet.addMergedRegion(gradeRegion)
columnIndex++

gradeMap.forEach{ (gradeCode, inOut) ->
tempRow = getOrCreateRow(sheet, rowIndex)
tempCell = getOrCreateCell(tempRow, columnIndex).apply {
setCellValue(gradeCode)
alignTopCenter(this)
}
CellUtil.setCellStyleProperties(tempCell, cellBorderArgs(1,1,1,1)+fontArgs(false))
for (i in 1..6) { // horizontal loop: payment In & payment Out & Net Amount
tempCell = getOrCreateCell(tempRow, columnIndex + i).apply {
when (i) {
1 -> setCellValue(inOut.In.manhour)
2 -> setCellValue(inOut.In.cost)
3 -> setCellValue(inOut.Out.manhour)
4 -> setCellValue(inOut.Out.cost)
5 -> cellFormula = "${InHourLetter}${rowIndex+1}-${OutHourLetter}${rowIndex+1}"
6 -> cellFormula = "${InCostLetter}${rowIndex+1}-${OutCostLetter}${rowIndex+1}"
}
CellUtil.setCellStyleProperties(this,
cellBorderArgs(1,1,1,1)
+ fontArgs(false)
+ dataFormatArgs(accountingStyle)
)
}
} // end loop: payment In & payment Out & Net Amount
rowIndex++
}
// write subtotal
columnIndex = 1
tempRow = getOrCreateRow(sheet, rowIndex)
tempCell = getOrCreateCell(tempRow, columnIndex).apply {
setCellValue("Sub-total:")
CellUtil.setCellStyleProperties(this,
cellBorderArgs(1,1,1,1)
+ fontArgs(false))
alignTopCenter(this)
}
subTotalRowIndexList.add(rowIndex)
// InHour subtotal
columnIndex = 3
tempCell = getOrCreateCell(tempRow, columnIndex).apply {
cellFormula = "SUM(${InHourLetter}${rowIndex-4}:${InHourLetter}${rowIndex})"
CellUtil.setCellStyleProperties(this,
cellBorderArgs(1,1,1,1)
+ fontArgs(false)
+ dataFormatArgs(accountingStyle)
)
}
// InCost subtotal
columnIndex = 4
tempCell = getOrCreateCell(tempRow, columnIndex).apply {
cellFormula = "SUM(${InCostLetter}${rowIndex-4}:${InCostLetter}${rowIndex})"
CellUtil.setCellStyleProperties(this,
cellBorderArgs(1,1,1,1)
+ fontArgs(false)
+ dataFormatArgs(accountingStyle)
)
}
// OutHour subtotal
columnIndex = 5
tempCell = getOrCreateCell(tempRow, columnIndex).apply {
cellFormula = "SUM(${OutHourLetter}${rowIndex-4}:${OutHourLetter}${rowIndex})"
CellUtil.setCellStyleProperties(this,
cellBorderArgs(1,1,1,1)
+ fontArgs(false)
+ dataFormatArgs(accountingStyle)
)
}
// OutCost subtotal
columnIndex = 6
tempCell = getOrCreateCell(tempRow, columnIndex).apply {
cellFormula = "SUM(${OutCostLetter}${rowIndex-4}:${OutCostLetter}${rowIndex})"
CellUtil.setCellStyleProperties(this,
cellBorderArgs(1,1,1,1)
+ fontArgs(false)
+ dataFormatArgs(accountingStyle)
)
}
// NetHour subtotal
columnIndex = 7
tempCell = getOrCreateCell(tempRow, columnIndex).apply {
cellFormula = "SUM(${NetHourLetter}${rowIndex-4}:${NetHourLetter}${rowIndex})"
CellUtil.setCellStyleProperties(this, cellBorderArgs(1,1,1,1)
+ fontArgs(false)
+ dataFormatArgs(accountingStyle)
)
}
// NetCost subtotal
columnIndex = 8
tempCell = getOrCreateCell(tempRow, columnIndex).apply {
cellFormula = "SUM(${NetCostLetter}${rowIndex-4}:${NetCostLetter}${rowIndex})"
CellUtil.setCellStyleProperties(this, cellBorderArgs(1,1,1,1)
+ fontArgs(false)
+ dataFormatArgs(accountingStyle)
)
}
// merge at last
val subtotalRegion = CellRangeAddress(rowIndex, rowIndex,1,2)
sheet.addMergedRegion(subtotalRegion)
val subtotalRowRegion = CellRangeAddress(rowIndex, rowIndex,0,8)
setRegionBorders(1,2,1,1, subtotalRowRegion, sheet)
rowIndex++
}
// rowIndex == 47
columnIndex = 1
tempRow = getOrCreateRow(sheet, rowIndex)
tempCell = getOrCreateCell(tempRow, columnIndex).apply {
setCellValue("Total:")
CellUtil.setCellStyleProperties(this,
cellBorderArgs(1,1,1,1)
+ fontArgs(false)
)
alignTopCenter(this)
}
val totalRegion = CellRangeAddress(rowIndex, rowIndex,1,2)
sheet.addMergedRegion(totalRegion)
for (i in 3..8) {
columnIndex = i
val subtotalList = mutableListOf<String>()
val currColumnLetter = CellReference.convertNumToColString(i)
subTotalRowIndexList.forEach{
subtotalList.add("${currColumnLetter}${it+1}")
}
val subtotalFormula = subtotalList.joinToString("+")
tempCell = getOrCreateCell(tempRow, columnIndex).apply {
cellFormula = subtotalFormula
CellUtil.setCellStyleProperties(this, cellBorderArgs(1,1,1,1)
+ fontArgs(false)
+ dataFormatArgs(accountingStyle)
)
}
}
val subtotalRowRegion = CellRangeAddress(rowIndex, rowIndex,0,8)
setRegionBorders(1,2,1,1, subtotalRowRegion, sheet)
}

private fun createThirdSheetTeamChargeReport(
month: String,
workbook: Workbook,
timesheets: List<Timesheet>,
teams: List<Team>,
grades: List<Grade>,
teamId: String,
gradeLog: List<GradeLog>,
isTeamLead: Boolean,
) {
val salaryEffective = salaryEffectiveRepository.findAll()
var sheet: Sheet = workbook.getSheetAt(2)
val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)")
val rowIndex = 1 // Assuming the location is in (1,2), which is the report date field
val columnIndex = 2
val monthFormat = DateTimeFormatter.ofPattern("MMMM yyyy", Locale.ENGLISH)
val reportMonth = YearMonth.parse(month, DateTimeFormatter.ofPattern("yyyy-MM"))
val convertReportMonth = YearMonth.of(reportMonth.year, reportMonth.month).format(monthFormat)
sheet.getRow(rowIndex).getCell(columnIndex).apply {
setCellValue(convertReportMonth)
}
var sortedTeams = teams.sortedBy { it.id }.toMutableList()
if (teamId.lowercase() != "all") {
// sortedTeams = teams.sortedWith(compareBy { if (it.id == teamId.toLong()) 0 else 1 }).toMutableList()
sortedTeams = mutableListOf(sortedTeams.find { teamId.toLong() == it.id }!!)
}
//// generate info map /////
val teamsInOutMap: MutableMap<String, MutableMap<String, InOut>> = generateTeamBlock(
teams,
sortedTeams,
grades,
timesheets,
gradeLog,
salaryEffective,
)
///// end of function ////
///// create the form ////
createCrossTeamForm(
workbook,
sheet,
5,
0,
teamsInOutMap
)
// autosize
for (i in 3..8) {
sheet.setColumnWidth(i, 20 * 256)
}
}
@Throws(IOException::class)
private fun createCrossTeamChargeReport(
month: String,
@@ -3555,6 +3938,7 @@ open class ReportService(
grades: List<Grade>,
monthlyStaffSalaryEffective: List<MonthlyStaffSalaryData>,
teamId: String,
gradeLog: List<GradeLog>,
templatePath: String,
): Workbook {
// please create a new function for each report template
@@ -4119,10 +4503,19 @@ open class ReportService(
}
}
}
val isTeamLead = false
conditionalFormattingNegative(sheet)
conditionalFormattingPositive(sheet)

createThirdSheetTeamChargeReport(
month,
workbook,
timesheets,
teams,
grades,
teamId,
gradeLog,
isTeamLead,
)
return workbook
}
// Use to Calculate cummunlative expenditure


+ 4
- 2
src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt Ver arquivo

@@ -63,6 +63,7 @@ class ReportController(
private val invoiceService: InvoiceService, private val gradeAllocationRepository: GradeAllocationRepository,
private val subsidiaryRepository: SubsidiaryRepository, private val staffAllocationRepository: StaffAllocationRepository,
private val gradeRepository: GradeRepository,
private val gradeLogRepository: GradeLogRepository,
private val salaryEffectiveService: SalaryEffectiveService,
private val projectExpenseRepository: ProjectExpenseRepository,
) {
@@ -342,8 +343,9 @@ class ReportController(
val teams = teamRepository.findAll().filter { it.deleted == false }
val grades = gradeRepository.findAll().filter { it.deleted == false }
val monthlyStaffSalaryEffective = salaryEffectiveService.getMonthlyStaffSalaryData(startDate, endDate)

val reportResult: ByteArray = excelReportService.generateCrossTeamChargeReport(request.month, timesheets, teams, grades, monthlyStaffSalaryEffective, request.teamId)
val gradeLog = gradeLogRepository.findByDeletedFalseAndFromBeforeAndToIsNullOrToAfter(startDate, endDate)
print(gradeLog)
val reportResult: ByteArray = excelReportService.generateCrossTeamChargeReport(request.month, timesheets, teams, grades, monthlyStaffSalaryEffective, request.teamId, gradeLog)
return ResponseEntity.ok()
.header("filename", "Cross Team Charge Report - " + LocalDate.now() + ".xlsx")
.body(ByteArrayResource(reportResult))


BIN
src/main/resources/templates/report/Cross Team Charge Report.xlsx Ver arquivo


Carregando…
Cancelar
Salvar