Bläddra i källkod

Merge branch 'master' of https://git.2fi-solutions.com/davidhui/TSMS-backend

tags/Baseline_30082024_BACKEND_UAT
MSI\2Fi 1 år sedan
förälder
incheckning
7982282383
39 ändrade filer med 1645 tillägg och 317 borttagningar
  1. +1
    -0
      src/main/java/com/ffii/tsms/modules/data/entity/BuildingTypeRepository.kt
  2. +4
    -0
      src/main/java/com/ffii/tsms/modules/data/entity/CustomerRepository.java
  3. +2
    -0
      src/main/java/com/ffii/tsms/modules/data/entity/LocationRepository.kt
  4. +1
    -0
      src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java
  5. +1
    -0
      src/main/java/com/ffii/tsms/modules/data/entity/Subsidiary.java
  6. +6
    -0
      src/main/java/com/ffii/tsms/modules/data/entity/SubsidiaryRepository.java
  7. +2
    -0
      src/main/java/com/ffii/tsms/modules/data/entity/TeamRepository.java
  8. +1
    -0
      src/main/java/com/ffii/tsms/modules/data/entity/WorkNatureRepository.kt
  9. +12
    -0
      src/main/java/com/ffii/tsms/modules/data/service/CustomerService.kt
  10. +415
    -90
      src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt
  11. +11
    -0
      src/main/java/com/ffii/tsms/modules/data/service/SubsidiaryService.kt
  12. +24
    -0
      src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt
  13. +1
    -1
      src/main/java/com/ffii/tsms/modules/data/web/SkillController.kt
  14. +2
    -0
      src/main/java/com/ffii/tsms/modules/project/entity/InvoiceRepository.kt
  15. +3
    -0
      src/main/java/com/ffii/tsms/modules/project/entity/Project.kt
  16. +2
    -0
      src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt
  17. +4
    -4
      src/main/java/com/ffii/tsms/modules/project/service/InvoiceService.kt
  18. +344
    -25
      src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt
  19. +24
    -2
      src/main/java/com/ffii/tsms/modules/project/web/ProjectsController.kt
  20. +2
    -1
      src/main/java/com/ffii/tsms/modules/project/web/models/EditProjectDetails.kt
  21. +1
    -0
      src/main/java/com/ffii/tsms/modules/project/web/models/MainProjectDetails.kt
  22. +6
    -3
      src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt
  23. +476
    -158
      src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt
  24. +40
    -2
      src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt
  25. +4
    -0
      src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt
  26. +2
    -0
      src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt
  27. +125
    -24
      src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt
  28. +22
    -0
      src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt
  29. +7
    -7
      src/main/java/com/ffii/tsms/modules/user/service/UserService.java
  30. +9
    -0
      src/main/resources/db/changelog/changes/20240617_01_cyril/01_update_task.sql
  31. +5
    -0
      src/main/resources/db/changelog/changes/20240617_01_cyril/02_update_project.sql
  32. +81
    -0
      src/main/resources/db/changelog/changes/20240702_01_cyril/01_update_authority.sql
  33. +5
    -0
      src/main/resources/db/changelog/changes/20240704_01_cyril/01_update_project.sql
  34. Binär
      src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx
  35. Binär
      src/main/resources/templates/report/AR04_Cost and Expense Report v02.xlsx
  36. Binär
      src/main/resources/templates/report/AR05_Project Completion Report.xlsx
  37. Binär
      src/main/resources/templates/report/AR06_Project Completion Report with Outstanding Accounts Receivable v02.xlsx
  38. Binär
      src/main/resources/templates/report/AR08_Monthly Work Hours Analysis Report.xlsx
  39. Binär
      src/main/resources/templates/report/Cross Team Charge Report.xlsx

+ 1
- 0
src/main/java/com/ffii/tsms/modules/data/entity/BuildingTypeRepository.kt Visa fil

@@ -3,4 +3,5 @@ package com.ffii.tsms.modules.data.entity;
import com.ffii.core.support.AbstractRepository import com.ffii.core.support.AbstractRepository


interface BuildingTypeRepository : AbstractRepository<BuildingType, Long> { interface BuildingTypeRepository : AbstractRepository<BuildingType, Long> {
fun findByName(name: String): BuildingType?
} }

+ 4
- 0
src/main/java/com/ffii/tsms/modules/data/entity/CustomerRepository.java Visa fil

@@ -3,6 +3,7 @@ package com.ffii.tsms.modules.data.entity;
import java.util.Optional; import java.util.Optional;
import java.util.List; import java.util.List;


import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;


import com.ffii.core.support.AbstractRepository; import com.ffii.core.support.AbstractRepository;
@@ -11,4 +12,7 @@ public interface CustomerRepository extends AbstractRepository<Customer, Long> {
List<Customer> findAllByDeletedFalse(); List<Customer> findAllByDeletedFalse();
Optional<Customer> findByCode(@Param("code") String code); Optional<Customer> findByCode(@Param("code") String code);
Optional<Customer> findByName(@Param("name") String name); Optional<Customer> findByName(@Param("name") String name);

@Query("SELECT max(cast(substring_index(c.code, '-', -1) as long)) FROM Customer c")
Long getLatestCodeNumber();
} }

+ 2
- 0
src/main/java/com/ffii/tsms/modules/data/entity/LocationRepository.kt Visa fil

@@ -3,4 +3,6 @@ package com.ffii.tsms.modules.data.entity;
import com.ffii.core.support.AbstractRepository import com.ffii.core.support.AbstractRepository


interface LocationRepository : AbstractRepository<Location, Long> { interface LocationRepository : AbstractRepository<Location, Long> {

fun findByName(name: String): Location
} }

+ 1
- 0
src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java Visa fil

@@ -25,4 +25,5 @@ public interface StaffRepository extends AbstractRepository<Staff, Long> {
Optional<List<Staff>> findAllByDeletedFalse(); Optional<List<Staff>> findAllByDeletedFalse();


Optional<Staff> findIdAndNameByUserIdAndDeletedFalse(Long id); Optional<Staff> findIdAndNameByUserIdAndDeletedFalse(Long id);

} }

+ 1
- 0
src/main/java/com/ffii/tsms/modules/data/entity/Subsidiary.java Visa fil

@@ -10,6 +10,7 @@ import java.util.List;


import static jakarta.persistence.CascadeType.ALL; import static jakarta.persistence.CascadeType.ALL;



@Entity @Entity
@Table(name = "subsidiary") @Table(name = "subsidiary")
public class Subsidiary extends BaseEntity<Long> { public class Subsidiary extends BaseEntity<Long> {


+ 6
- 0
src/main/java/com/ffii/tsms/modules/data/entity/SubsidiaryRepository.java Visa fil

@@ -1,6 +1,7 @@
package com.ffii.tsms.modules.data.entity; package com.ffii.tsms.modules.data.entity;


import com.ffii.core.support.AbstractRepository; import com.ffii.core.support.AbstractRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;


import java.util.List; import java.util.List;
@@ -12,4 +13,9 @@ public interface SubsidiaryRepository extends AbstractRepository<Subsidiary, Lon
List<Subsidiary> findAllByDeletedFalseAndIdIn(List<Long> id); List<Subsidiary> findAllByDeletedFalseAndIdIn(List<Long> id);


Optional<Subsidiary> findByCode(@Param("code") String code); Optional<Subsidiary> findByCode(@Param("code") String code);

Optional<Subsidiary> findByName(@Param("name") String name);

@Query("SELECT max(cast(substring_index(s.code, '-', -1) as long)) FROM Subsidiary s")
Long getLatestCodeNumber();
} }

+ 2
- 0
src/main/java/com/ffii/tsms/modules/data/entity/TeamRepository.java Visa fil

@@ -9,4 +9,6 @@ public interface TeamRepository extends AbstractRepository<Team, Long> {
List<Team> findByDeletedFalse(); List<Team> findByDeletedFalse();


Team findByStaff(Staff staff); Team findByStaff(Staff staff);

Team findByCode(String code);
} }

+ 1
- 0
src/main/java/com/ffii/tsms/modules/data/entity/WorkNatureRepository.kt Visa fil

@@ -3,4 +3,5 @@ package com.ffii.tsms.modules.data.entity;
import com.ffii.core.support.AbstractRepository import com.ffii.core.support.AbstractRepository


interface WorkNatureRepository : AbstractRepository<WorkNature, Long> { interface WorkNatureRepository : AbstractRepository<WorkNature, Long> {
fun findByName(name: String): WorkNature?
} }

+ 12
- 0
src/main/java/com/ffii/tsms/modules/data/service/CustomerService.kt Visa fil

@@ -37,6 +37,18 @@ open class CustomerService(
return customerRepository.findByCode(code); return customerRepository.findByCode(code);
} }


open fun createClientCode(): String {
val prefix = "CT"

val latestClientCode = customerRepository.getLatestCodeNumber()

if (latestClientCode != null) {
return "$prefix-" + String.format("%03d", latestClientCode + 1L)
} else {
return "$prefix-001"
}
}

open fun saveCustomer(saveCustomer: SaveCustomerRequest): SaveCustomerResponse { open fun saveCustomer(saveCustomer: SaveCustomerRequest): SaveCustomerResponse {


val duplicateCustomer = findCustomerByCode(saveCustomer.code) val duplicateCustomer = findCustomerByCode(saveCustomer.code)


+ 415
- 90
src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt Visa fil

@@ -68,19 +68,23 @@ open class DashboardService(


fun searchCustomerSubsidiaryProject(args: Map<String, Any>): List<Map<String, Any>> { fun searchCustomerSubsidiaryProject(args: Map<String, Any>): List<Map<String, Any>> {
val sql = StringBuilder("select" val sql = StringBuilder("select"
+ " ROW_NUMBER() OVER (ORDER BY p.id, p.code, p.name, te.code, s.name, tg.name, p.totalManhour, milestonePayment.comingPaymentMilestone) AS id,"
+ " ROW_NUMBER() OVER (ORDER BY p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone) AS id,"
+ " p.id as id," + " p.id as id,"
+ " p.id as projectId," + " p.id as projectId,"
+ " p.code as projectCode," + " p.code as projectCode,"
+ " p.name as projectName," + " p.name as projectName,"
+ " te.code as team," + " te.code as team,"
+ " s.name as teamLead," + " s.name as teamLead,"
+ " tg.name as expectedStage,"
+ " GROUP_CONCAT(DISTINCT tg.name ORDER BY tg.name) as expectedStage,"
+ " p.totalManhour as budgetedManhour," + " p.totalManhour as budgetedManhour,"
+ " sum(t.normalConsumed) + sum(t.otConsumed) as spentManhour," + " sum(t.normalConsumed) + sum(t.otConsumed) as spentManhour,"
+ " p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed) as remainedManhour," + " p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed) as remainedManhour,"
+ " coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) as manhourConsumptionPercentage," + " coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) as manhourConsumptionPercentage,"
+ " COALESCE (DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d'),'NA') as comingPaymentMilestone"
+ " COALESCE (DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d'),'NA') as comingPaymentMilestone,"
+ " case"
+ " when COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) > 0 then 0"
+ " when COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) <= 0 then 1"
+ " end as alert"
+ " from project p" + " from project p"
+ " left join project_task pt on p.id = pt.project_id" + " left join project_task pt on p.id = pt.project_id"
+ " left join timesheet t on pt.id = t.projectTaskId" + " left join timesheet t on pt.id = t.projectTaskId"
@@ -105,7 +109,7 @@ open class DashboardService(
+ " ) milestonePayment on milestonePayment.pid = p.id" + " ) milestonePayment on milestonePayment.pid = p.id"
+ " where p.customerId = :customerId" + " where p.customerId = :customerId"
+ " and p.customerSubsidiaryId = :subsidiaryId" + " and p.customerSubsidiaryId = :subsidiaryId"
+ " and (tg.name != '5. Miscellaneous' or tg.name is null)"
// + " and (tg.name != '5. Miscellaneous' or tg.name is null)"
+ " and p.status not in (\"Pending to Start\",\"Completed\",\"Deleted\")" + " and p.status not in (\"Pending to Start\",\"Completed\",\"Deleted\")"
) )


@@ -116,25 +120,38 @@ open class DashboardService(
} }
} }


sql.append(" group by p.id, p.code, p.name, te.code, s.name, tg.name, p.totalManhour, milestonePayment.comingPaymentMilestone")
sql.append(" group by p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone")

if (args["tableSorting"] == "ProjectName") {
sql.append(" ORDER BY p.name ASC")
} else if (args["tableSorting"] == "PercentageASC") {
sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) asc")
} else if (args["tableSorting"] == "PercentageDESC") {
sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) desc")
}

return jdbcDao.queryForList(sql.toString(), args) return jdbcDao.queryForList(sql.toString(), args)
} }


fun searchCustomerNonSubsidiaryProject(args: Map<String, Any>): List<Map<String, Any>> { fun searchCustomerNonSubsidiaryProject(args: Map<String, Any>): List<Map<String, Any>> {
val sql = StringBuilder("select" val sql = StringBuilder("select"
+ " ROW_NUMBER() OVER (ORDER BY p.id, p.code, p.name, te.code, s.name, tg.name, p.totalManhour, milestonePayment.comingPaymentMilestone) AS id,"
+ " ROW_NUMBER() OVER (ORDER BY p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone) AS id,"
+ " p.id as id," + " p.id as id,"
+ " p.id as projectId," + " p.id as projectId,"
+ " p.code as projectCode," + " p.code as projectCode,"
+ " p.name as projectName," + " p.name as projectName,"
+ " te.code as team," + " te.code as team,"
+ " s.name as teamLead," + " s.name as teamLead,"
+ " tg.name as expectedStage,"
+ " GROUP_CONCAT(DISTINCT tg.name ORDER BY tg.name) as expectedStage,"
+ " p.totalManhour as budgetedManhour," + " p.totalManhour as budgetedManhour,"
+ " COALESCE (sum(t.normalConsumed) + sum(t.otConsumed),0) as spentManhour," + " COALESCE (sum(t.normalConsumed) + sum(t.otConsumed),0) as spentManhour,"
+ " COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) as remainedManhour," + " COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) as remainedManhour,"
+ " coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) as manhourConsumptionPercentage," + " coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) as manhourConsumptionPercentage,"
+ " COALESCE (DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d'),'NA') as comingPaymentMilestone"
+ " COALESCE (DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d'),'NA') as comingPaymentMilestone,"
+ " case"
+ " when COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) > 0 then 0"
+ " when COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) <= 0 then 1"
+ " end as alert"
+ " from project p" + " from project p"
+ " left join project_task pt on p.id = pt.project_id" + " left join project_task pt on p.id = pt.project_id"
+ " left join timesheet t on pt.id = t.projectTaskId" + " left join timesheet t on pt.id = t.projectTaskId"
@@ -159,10 +176,16 @@ open class DashboardService(
+ " where p.customerId = :customerId" + " where p.customerId = :customerId"
+ " and isNull(p.customerSubsidiaryId)" + " and isNull(p.customerSubsidiaryId)"
+ " and p.status not in (\"Pending to Start\",\"Completed\",\"Deleted\")" + " and p.status not in (\"Pending to Start\",\"Completed\",\"Deleted\")"
+ " and (tg.name != '5. Miscellaneous' or tg.name is null)"
+ " group by p.id, p.code, p.name, te.code, s.name, tg.name, p.totalManhour, milestonePayment.comingPaymentMilestone"
// + " 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"
) )

if (args["tableSorting"] == "ProjectName") {
sql.append(" ORDER BY p.name ASC")
} else if (args["tableSorting"] == "PercentageASC") {
sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) asc")
} else if (args["tableSorting"] == "PercentageDESC") {
sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) desc")
}
return jdbcDao.queryForList(sql.toString(), args) return jdbcDao.queryForList(sql.toString(), args)
} }


@@ -276,19 +299,23 @@ open class DashboardService(
fun searchTeamProject(args: Map<String, Any>): List<Map<String, Any>> { fun searchTeamProject(args: Map<String, Any>): List<Map<String, Any>> {
val sql = StringBuilder( val sql = StringBuilder(
"select" "select"
+ " ROW_NUMBER() OVER (ORDER BY p.id, p.code, p.name, te.code, s.name, tg.name, p.totalManhour, milestonePayment.comingPaymentMilestone) AS id,"
+ " ROW_NUMBER() OVER (ORDER BY p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone) AS id,"
+ " p.id as id," + " p.id as id,"
+ " p.id as projectId," + " p.id as projectId,"
+ " p.code as projectCode," + " p.code as projectCode,"
+ " p.name as projectName," + " p.name as projectName,"
+ " te.code as team," + " te.code as team,"
+ " s.name as teamLead," + " s.name as teamLead,"
+ " tg.name as expectedStage,"
+ " GROUP_CONCAT(DISTINCT tg.name ORDER BY tg.name) as expectedStage,"
+ " p.totalManhour as budgetedManhour," + " p.totalManhour as budgetedManhour,"
+ " COALESCE (sum(t.normalConsumed) + sum(t.otConsumed),0) as spentManhour," + " COALESCE (sum(t.normalConsumed) + sum(t.otConsumed),0) as spentManhour,"
+ " COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) as remainedManhour," + " COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) as remainedManhour,"
+ " coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) as manhourConsumptionPercentage," + " coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) as manhourConsumptionPercentage,"
+ " COALESCE (DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d'),'NA') as comingPaymentMilestone"
+ " COALESCE (DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d'),'NA') as comingPaymentMilestone,"
+ " case"
+ " when COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) > 0 then 0"
+ " when COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) <= 0 then 1"
+ " end as alert"
+ " from project p" + " from project p"
+ " left join project_task pt on p.id = pt.project_id" + " left join project_task pt on p.id = pt.project_id"
+ " left join timesheet t on pt.id = t.projectTaskId" + " left join timesheet t on pt.id = t.projectTaskId"
@@ -312,12 +339,111 @@ open class DashboardService(
+ " ) milestonePayment on milestonePayment.pid = p.id" + " ) milestonePayment on milestonePayment.pid = p.id"
+ " where p.teamLead = :teamLeadId" + " where p.teamLead = :teamLeadId"
+ " and p.status not in (\"Pending to Start\",\"Completed\",\"Deleted\")" + " and p.status not in (\"Pending to Start\",\"Completed\",\"Deleted\")"
+ " and (tg.name != '5. Miscellaneous' or tg.name is null)"
+ " group by p.id, p.code, p.name, te.code, s.name, tg.name, p.totalManhour, milestonePayment.comingPaymentMilestone"
// + " 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"
) )
if (args["tableSorting"] == "ProjectName") {
sql.append(" ORDER BY p.name ASC")
} else if (args["tableSorting"] == "PercentageASC") {
sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) asc")
} else if (args["tableSorting"] == "PercentageDESC") {
sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) desc")
}


return jdbcDao.queryForList(sql.toString(), args) return jdbcDao.queryForList(sql.toString(), args)
} }

fun searchTeamConsumption(args: Map<String, Any>): List<Map<String, Any>>
{
val sql = StringBuilder(
// "select"
// + " ROW_NUMBER() OVER (ORDER BY te.code, s.name, project.budgetedManhour) AS id,"
// + " te.code as team,"
// + " s.name as teamLead,"
// + " project.budgetedManhour as budgetedManhour,"
// + " COALESCE (sum(t.normalConsumed) + sum(t.otConsumed),0) as spentManhour,"
// + " COALESCE (project.budgetedManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) as remainedManhour,"
// + " coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/project.budgetedManhour)*100,2),0) as manhourConsumptionPercentage,"
// + " case"
// + " when COALESCE (project.budgetedManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) > 0 then 0"
// + " when COALESCE (project.budgetedManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) <= 0 then 1"
// + " end as alert"
// + " from team te"
// + " left join project p on p.teamLead = te.teamLead"
// + " left join project_task pt on p.id = pt.project_id"
// + " left join timesheet t on pt.id = t.projectTaskId"
// + " left join staff s on te.teamLead = s.id"
// + " left join ("
// + " select sum(p2.totalManhour) as budgetedManhour"
// + " from team t2"
// + " left join project p2 on p2.teamLead = t2.teamLead"
// + " where p2.teamLead in (:teamIds)"
// + " ) as project on 1 = 1"
// + " where p.teamLead in (:teamIds)"
// + " and p.status not in ('Pending to Start','Completed','Deleted')"
// + " group by te.code, s.name, project.budgetedManhour"
"select"
+ " ROW_NUMBER() OVER (ORDER BY p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone) AS id,"
+ " p.id as id,"
+ " p.id as projectId,"
+ " p.code as projectCode,"
+ " p.name as projectName,"
+ " te.code as team,"
+ " s.name as teamLead,"
+ " GROUP_CONCAT(DISTINCT tg.name ORDER BY tg.name) as expectedStage,"
+ " p.totalManhour as budgetedManhour,"
+ " COALESCE (sum(t.normalConsumed) + sum(t.otConsumed),0) as spentManhour,"
+ " COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) as remainedManhour,"
+ " coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) as manhourConsumptionPercentage,"
+ " COALESCE (DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d'),'NA') as comingPaymentMilestone,"
+ " case"
+ " when COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) > 0 then 0"
+ " when COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) <= 0 then 1"
+ " end as alert"
+ " from project p"
+ " left join project_task pt on p.id = pt.project_id"
+ " left join timesheet t on pt.id = t.projectTaskId"
+ " left join team te on p.teamLead = te.teamLead"
+ " left join staff s on te.teamLead = s.id"
+ " left join milestone m on p.id = m.projectId and curdate() >= m.startDate and curdate() <= m.endDate"
+ " left join task_group tg on m.taskGroupId = tg.id"
+ " left join ("
+ " SELECT pid, MIN(comingPaymentMilestone) AS comingPaymentMilestone"
+ " FROM ("
+ " SELECT p.id AS pid, mp.date AS comingPaymentMilestone"
+ " FROM project p"
+ " LEFT JOIN milestone m ON p.id = m.projectId"
+ " LEFT JOIN milestone_payment mp ON m.id = mp.milestoneId"
+ " WHERE p.teamLead in (:teamIds)"
+ " AND p.status NOT IN ('Pending to Start', 'Completed', 'Deleted')"
+ " AND mp.date >= CURDATE()"
+ " ) AS subquery"
+ " GROUP BY pid"
+ " ORDER BY comingPaymentMilestone ASC"
+ " ) milestonePayment on milestonePayment.pid = p.id"
+ " where p.teamLead in (:teamIds)"
+ " and p.status not in ('Pending to Start','Completed','Deleted')"
+ " group by p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone"
)

// if (args["tableSorting"] == "ProjectName") {
// sql.append(" ORDER BY te.code ASC")
// } else if (args["tableSorting"] == "PercentageASC") {
// sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/project.budgetedManhour)*100,2),0) asc")
// } else if (args["tableSorting"] == "PercentageDESC") {
// sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/project.budgetedManhour)*100,2),0) desc")
// }
if (args["tableSorting"] == "ProjectName") {
sql.append(" ORDER BY p.name ASC")
} else if (args["tableSorting"] == "PercentageASC") {
sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) asc")
} else if (args["tableSorting"] == "PercentageDESC") {
sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) desc")
}

return jdbcDao.queryForList(sql.toString(), args)
}

fun searchFinancialSummaryCard(args: Map<String, Any>): List<Map<String, Any>> { fun searchFinancialSummaryCard(args: Map<String, Any>): List<Map<String, Any>> {
val sql = StringBuilder( val sql = StringBuilder(
"select" "select"
@@ -327,13 +453,19 @@ open class DashboardService(
+ " coalesce(pj.totalFee,0) as totalFee," + " coalesce(pj.totalFee,0) as totalFee,"
+ " coalesce(pj.totalBudget,0) as totalBudget," + " coalesce(pj.totalBudget,0) as totalBudget,"
+ " coalesce(sum(i.issueAmount),0) as totalInvoiced," + " coalesce(sum(i.issueAmount),0) as totalInvoiced,"
+ " coalesce(pj.totalFee,0) - coalesce(sum(i.issueAmount),0) as unInvoiced,"
+ " coalesce(sum(i.paidAmount),0) as totalReceived," + " coalesce(sum(i.paidAmount),0) as totalReceived,"
+ " round(expenditure.cumulativeExpenditure,2) as cumulativeExpenditure," + " round(expenditure.cumulativeExpenditure,2) as cumulativeExpenditure,"
+ " case" + " case"
+ " when coalesce(round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2),0) >= 1 then 'Positive'" + " when coalesce(round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2),0) >= 1 then 'Positive'"
+ " when coalesce(round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2),0) < 1 then 'Negative'" + " when coalesce(round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2),0) < 1 then 'Negative'"
+ " end as cashFlowStatus," + " end as cashFlowStatus,"
+ " coalesce(round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2),0) as cpi"
+ " coalesce(round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2),0) as cpi,"
+ " coalesce(round(coalesce(pj.totalFee,0) / (expenditure.cumulativeExpenditure),2),0) as projectedCpi,"
+ " case"
+ " when coalesce(round(coalesce(pj.totalFee,0) / (expenditure.cumulativeExpenditure),2),0) >= 1 then 'Positive'"
+ " when coalesce(round(coalesce(pj.totalFee,0) / (expenditure.cumulativeExpenditure),2),0) < 1 then 'Negative'"
+ " end as projectedCashFlowStatus"
+ " from team t" + " from team t"
+ " left join (" + " left join ("
+ " select" + " select"
@@ -404,12 +536,18 @@ open class DashboardService(
+ " round(sum(p.expectedTotalFee) * 0.8,2) as totalBudget," + " round(sum(p.expectedTotalFee) * 0.8,2) as totalBudget,"
+ " round(expenditure.cumulativeExpenditure,2) as cumulativeExpenditure," + " round(expenditure.cumulativeExpenditure,2) as cumulativeExpenditure,"
+ " sum(i.issueAmount) as totalInvoiced," + " sum(i.issueAmount) as totalInvoiced,"
+ " sum(p.expectedTotalFee) - sum(i.issueAmount) as unInvoiced,"
+ " sum(i.paidAmount) as totalReceived," + " sum(i.paidAmount) as totalReceived,"
+ " case" + " case"
+ " when round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2) >= 1 then 'Positive'" + " when round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2) >= 1 then 'Positive'"
+ " when round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2) < 1 then 'Negative'" + " when round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2) < 1 then 'Negative'"
+ " end as cashFlowStatus," + " end as cashFlowStatus,"
+ " round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2) as cpi"
+ " round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2) as cpi,"
+ " round(sum(p.expectedTotalFee) / (expenditure.cumulativeExpenditure),2) as projectedCpi,"
+ " case"
+ " when round(sum(p.expectedTotalFee) / (expenditure.cumulativeExpenditure),2) >= 1 then 'Positive'"
+ " when round(sum(p.expectedTotalFee) / (expenditure.cumulativeExpenditure),2) < 1 then 'Negative'"
+ " end as projectedCashFlowStatus"
+ " from project p" + " from project p"
+ " left join (" + " left join ("
+ " select" + " select"
@@ -445,16 +583,103 @@ open class DashboardService(
return jdbcDao.queryForList(sql.toString(), args) return jdbcDao.queryForList(sql.toString(), args)
} }


// " case"
// + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) <= COALESCE(expenditure.cumulativeExpenditure,0) then coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) - coalesce(sum(i.issueAmount),0)"
// + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) < coalesce(sum(i.issueAmount),0) then 0"
// + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) >= coalesce(sum(i.issueAmount),0) then COALESCE(expenditure.cumulativeExpenditure,0) - coalesce(sum(i.issueAmount),0)"
// + " end as totalUninvoiced"

fun searchFinancialSummaryByClient(args: Map<String, Any>): List<Map<String, Any>> { fun searchFinancialSummaryByClient(args: Map<String, Any>): List<Map<String, Any>> {
val sql = StringBuilder( "select"
+ " ROW_NUMBER() OVER (ORDER BY t.id, c.id) AS id,"
// val sql = StringBuilder( "select"
// + " ROW_NUMBER() OVER (ORDER BY t.id, c.id) AS id,"
// + " t.id as teamId,"
// + " c.id as cid,"
// + " c.code as customerCode,"
// + " c.name as customerName,"
// + " count(p.name) as projectNo,"
// + " sum(p.expectedTotalFee) as totalFee,"
// + " round(sum(p.expectedTotalFee) * 0.8,2) as totalBudget,"
// + " COALESCE(round(expenditure.cumulativeExpenditure,2),0) as cumulativeExpenditure,"
// + " coalesce(sum(i.issueAmount),0) as totalInvoiced,"
// + " coalesce(sum(i.paidAmount),0) as totalReceived,"
// + " case"
// + " when coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) >= 1 then 'Positive'"
// + " when coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) < 1 then 'Negative'"
// + " end as cashFlowStatus,"
// + " coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) as cpi,"
// + " case"
// + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) <= COALESCE(expenditure.cumulativeExpenditure,0) then coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) - coalesce(sum(i.issueAmount),0)"
// + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) < coalesce(sum(i.issueAmount),0) then 0"
// + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) >= coalesce(sum(i.issueAmount),0) then COALESCE(expenditure.cumulativeExpenditure,0) - coalesce(sum(i.issueAmount),0)"
// + " end as totalUninvoiced"
// + " from team t"
// + " left join project p on t.teamLead = p.teamLead"
// + " left join customer c on p.customerId = c.id"
// + " left join ("
// + " select"
// + " t3.id as tid,"
// + " c3.id as cid,"
// + " sum(i3.issueAmount) as issueAmount,"
// + " sum(i3.paidAmount) as paidAmount"
// + " from team t3"
// + " left join project p3 on t3.teamLead = p3.teamLead"
// + " left join customer c3 on p3.customerId = c3.id"
// + " left join invoice i3 on p3.code = i3.projectCode"
// + " where t3.deleted = 0"
// + " and p3.status = 'On-going'"
//
// )
//
// if (args != null) {
// if (args.containsKey("teamId"))
// sql.append(" AND t3.id = :teamId");
// }
// sql.append( " group by c3.id, t3.id"
// + " ) as i on i.cid = c.id and i.tid = t.id"
// + " left join ("
// + " select"
// + " r.teamId as teamId,"
// + " r.customerId as customerId,"
// + " sum(r.cumulativeExpenditure) as cumulativeExpenditure"
// + " from ("
// + " select"
// + " t1.id as teamId,"
// + " c2.id as customerId,"
// + " (coalesce(sum(t2.normalConsumed),0) * s2.hourlyRate) + (coalesce(sum(t2.otConsumed),0) * s2.hourlyRate * 1.0) as cumulativeExpenditure,"
// + " s2.hourlyRate as hourlyRate,"
// + " sum(t2.normalConsumed) as normalConsumed,"
// + " sum(t2.otConsumed) as otConsumed"
// + " from team t1"
// + " left join project p2 on t1.teamLead = p2.teamLead"
// + " left join customer c2 on p2.customerId = c2 .id"
// + " left join project_task pt ON p2.id = pt.project_id"
// + " left join timesheet t2 on pt.id = t2.projectTaskId"
// + " left join staff s on t2.staffId = s.id"
// + " left join salary s2 on s.salaryId = s2.salaryPoint"
// + " where p2.status = 'On-going'"
// + " and t2.id is not null"
// + " group by s2.hourlyRate,t1.id,c2.id"
// + " ) as r"
// + " group by r.teamId, r.customerId"
// + " ) as expenditure on expenditure.teamId = t.id and expenditure.customerId = c.id"
// + " where t.deleted = 0"
// + " and p.status = 'On-going'")
//
// if (args != null) {
// if (args.containsKey("teamId"))
// sql.append(" AND t.id = :teamId");
// }
// sql.append(" group by t.id, c.id, c.code, c.name")
val sql = StringBuilder(
"select"
+ " ROW_NUMBER() OVER (ORDER BY t.id, i.cid) AS id,"
+ " t.id as teamId," + " t.id as teamId,"
+ " c.id as cid,"
+ " c.code as customerCode,"
+ " c.name as customerName,"
+ " count(p.name) as projectNo,"
+ " sum(p.expectedTotalFee) as totalFee,"
+ " round(sum(p.expectedTotalFee) * 0.8,2) as totalBudget,"
+ " i.cid as cid,"
+ " i.customerCode as customerCode,"
+ " i.customerName as customerName,"
+ " p.projectNo as projectNo,"
+ " p.totalFee as totalFee,"
+ " p.totalBudget as totalBudget,"
+ " COALESCE(round(expenditure.cumulativeExpenditure,2),0) as cumulativeExpenditure," + " COALESCE(round(expenditure.cumulativeExpenditure,2),0) as cumulativeExpenditure,"
+ " coalesce(sum(i.issueAmount),0) as totalInvoiced," + " coalesce(sum(i.issueAmount),0) as totalInvoiced,"
+ " coalesce(sum(i.paidAmount),0) as totalReceived," + " coalesce(sum(i.paidAmount),0) as totalReceived,"
@@ -463,18 +688,23 @@ open class DashboardService(
+ " when coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) < 1 then 'Negative'" + " when coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) < 1 then 'Negative'"
+ " end as cashFlowStatus," + " end as cashFlowStatus,"
+ " coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) as cpi," + " coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) as cpi,"
+ " coalesce(round(p.totalFee / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) as projectedCpi,"
+ " case"
+ " when coalesce(round(p.totalFee / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) >= 1 then 'Positive'"
+ " when coalesce(round(p.totalFee / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) < 1 then 'Negative'"
+ " end as projectedCashFlowStatus,"
+ " case" + " case"
+ " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) <= COALESCE(expenditure.cumulativeExpenditure,0) then coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) - coalesce(sum(i.issueAmount),0)"
+ " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) < coalesce(sum(i.issueAmount),0) then 0"
+ " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) >= coalesce(sum(i.issueAmount),0) then COALESCE(expenditure.cumulativeExpenditure,0) - coalesce(sum(i.issueAmount),0)"
+ " when p.totalFee - sum(i.issueAmount) >= 0 then coalesce(round(p.totalFee - sum(i.issueAmount),2),0)"
+ " when p.totalFee - sum(i.issueAmount) < 0 then 0"
+ " when p.totalFee - sum(i.issueAmount) is null then 0"
+ " end as totalUninvoiced" + " end as totalUninvoiced"
+ " from team t" + " from team t"
+ " left join project p on t.teamLead = p.teamLead"
+ " left join customer c on p.customerId = c.id"
+ " left join (" + " left join ("
+ " select" + " select"
+ " t3.id as tid," + " t3.id as tid,"
+ " c3.id as cid," + " c3.id as cid,"
+ " c3.code as customerCode,"
+ " c3.name as customerName,"
+ " sum(i3.issueAmount) as issueAmount," + " sum(i3.issueAmount) as issueAmount,"
+ " sum(i3.paidAmount) as paidAmount" + " sum(i3.paidAmount) as paidAmount"
+ " from team t3" + " from team t3"
@@ -483,15 +713,34 @@ open class DashboardService(
+ " left join invoice i3 on p3.code = i3.projectCode" + " left join invoice i3 on p3.code = i3.projectCode"
+ " where t3.deleted = 0" + " where t3.deleted = 0"
+ " and p3.status = 'On-going'" + " and p3.status = 'On-going'"

)

if (args != null) {
if (args.containsKey("teamId"))
)
if (args != null) {
if (args.containsKey("teamId"))
sql.append(" AND t3.id = :teamId"); sql.append(" AND t3.id = :teamId");
}
sql.append( " group by c3.id, t3.id"
+ " ) as i on i.cid = c.id and i.tid = t.id"
}
sql.append(
" group by c3.id, t3.id, customerCode, customerName"
+ " ) as i on i.tid = t.id"
+ " left join ("
+ " select"
+ " t.id as tid,"
+ " c.id as cid,"
+ " count(p.id) as projectNo,"
+ " sum(p.expectedTotalFee) as totalFee,"
+ " round(sum(p.expectedTotalFee) * 0.8,2) as totalBudget"
+ " from team t"
+ " left join project p on t.teamLead = p.teamLead"
+ " left join customer c on p.customerId = c.id"
+ " where t.deleted = 0"
+ " and p.status = 'On-going'"
)
if (args != null) {
if (args.containsKey("teamId"))
sql.append(" AND t.id = :teamId");
}
sql.append(
" group by t.id, c.id"
+ " ) as p on p.tid = t.id and p.cid = i.cid"
+ " left join (" + " left join ("
+ " select" + " select"
+ " r.teamId as teamId," + " r.teamId as teamId,"
@@ -517,15 +766,15 @@ open class DashboardService(
+ " group by s2.hourlyRate,t1.id,c2.id" + " group by s2.hourlyRate,t1.id,c2.id"
+ " ) as r" + " ) as r"
+ " group by r.teamId, r.customerId" + " group by r.teamId, r.customerId"
+ " ) as expenditure on expenditure.teamId = t.id and expenditure.customerId = c.id"
+ " ) as expenditure on expenditure.teamId = t.id and expenditure.customerId = i.cid"
+ " where t.deleted = 0" + " where t.deleted = 0"
+ " and p.status = 'On-going'")
if (args != null) {
if (args.containsKey("teamId"))
+ " and i.cid is not null"
)
if (args != null) {
if (args.containsKey("teamId"))
sql.append(" AND t.id = :teamId"); sql.append(" AND t.id = :teamId");
}
sql.append(" group by t.id, c.id, c.code, c.name")
}
sql.append( " group by t.id, i.cid, i.customerCode, i.customerName, p.projectNo, p.totalFee, p.totalBudget")


return jdbcDao.queryForList(sql.toString(), args) return jdbcDao.queryForList(sql.toString(), args)
} }
@@ -547,10 +796,20 @@ open class DashboardService(
+ " when coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) < 1 then 'Negative'" + " when coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) < 1 then 'Negative'"
+ " end as cashFlowStatus," + " end as cashFlowStatus,"
+ " coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) as cpi," + " coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) as cpi,"
+ " coalesce(round(p.expectedTotalFee / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) as projectedCpi,"
+ " case"
+ " when coalesce(round(p.expectedTotalFee / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) >= 1 then 'Positive'"
+ " when coalesce(round(p.expectedTotalFee / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) < 1 then 'Negative'"
+ " end as projectedCashFlowStatus,"
// + " case"
// + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) <= COALESCE(expenditure.cumulativeExpenditure,0) then coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) - coalesce(sum(i.issueAmount),0)"
// + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) < coalesce(sum(i.issueAmount),0) then 0"
// + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) >= coalesce(sum(i.issueAmount),0) then COALESCE(expenditure.cumulativeExpenditure,0) - coalesce(sum(i.issueAmount),0)"
// + " end as totalUninvoiced"
+ " case" + " case"
+ " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) <= COALESCE(expenditure.cumulativeExpenditure,0) then coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) - coalesce(sum(i.issueAmount),0)"
+ " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) < coalesce(sum(i.issueAmount),0) then 0"
+ " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) >= coalesce(sum(i.issueAmount),0) then COALESCE(expenditure.cumulativeExpenditure,0) - coalesce(sum(i.issueAmount),0)"
+ " when p.expectedTotalFee - sum(i.issueAmount) >= 0 then coalesce(round(p.expectedTotalFee - sum(i.issueAmount),2),0)"
+ " when p.expectedTotalFee - sum(i.issueAmount) < 0 then 0"
+ " when p.expectedTotalFee - sum(i.issueAmount) is null then 0"
+ " end as totalUninvoiced" + " end as totalUninvoiced"
+ " from team t" + " from team t"
+ " left join project p on t.teamLead = p.teamLead" + " left join project p on t.teamLead = p.teamLead"
@@ -739,7 +998,9 @@ open class DashboardService(
+ " coalesce (sum(i.issueAmount) - sum(i.paidAmount),0) as receivable," + " coalesce (sum(i.issueAmount) - sum(i.paidAmount),0) as receivable,"
+ " coalesce (round(sum(p.expectedTotalFee)*0.8,2),0) as totalBudget," + " coalesce (round(sum(p.expectedTotalFee)*0.8,2),0) as totalBudget,"
+ " coalesce (expenditure.expenditure,0) as totalExpenditure," + " coalesce (expenditure.expenditure,0) as totalExpenditure,"
+ " coalesce (sum(p.expectedTotalFee)*0.8 - expenditure.expenditure,0) as expenditureReceivable"
+ " coalesce (sum(p.expectedTotalFee)*0.8 - expenditure.expenditure,0) as expenditureReceivable,"
+ " sum(p.expectedTotalFee) as totalProjectFee,"
+ " coalesce (round(sum(i.issueAmount)/sum(p.expectedTotalFee)*100,0),0) as invoicedPercentage"
+ " from project p" + " from project p"
+ " left join (" + " left join ("
+ " select" + " select"
@@ -1083,7 +1344,9 @@ open class DashboardService(
"select" "select"
+ " concat(p.code,'-',p.name) as projectCodeAndName," + " concat(p.code,'-',p.name) as projectCodeAndName,"
+ " p.expectedTotalFee as totalFee," + " p.expectedTotalFee as totalFee,"
+ " expenditure.expenditure as expenditure,"
+ " p.expectedTotalFee * 0.8 as totalBudget,"
+ " coalesce (expenditure.expenditure,0) as expenditure,"
+ " (p.expectedTotalFee * 0.8) - coalesce (expenditure.expenditure,0) as remainingBudget,"
+ " case" + " case"
+ " when p.expectedTotalFee - expenditure.expenditure >= 0 then 'Within Budget'" + " when p.expectedTotalFee - expenditure.expenditure >= 0 then 'Within Budget'"
+ " when p.expectedTotalFee - expenditure.expenditure < 0 then 'Overconsumption'" + " when p.expectedTotalFee - expenditure.expenditure < 0 then 'Overconsumption'"
@@ -1189,32 +1452,61 @@ open class DashboardService(
+ " left join staff s on s.id = ts.staffId" + " left join staff s on s.id = ts.staffId"
+ " where p.id = :projectId" + " where p.id = :projectId"
+ " group by p.id, t.id, t.name, g.name" + " group by p.id, t.id, t.name, g.name"
+ " order by p.id;"
+ " order by t.id asc, p.id;"
) )


return jdbcDao.queryForList(sql.toString(), args) return jdbcDao.queryForList(sql.toString(), args)
} }
fun monthlyActualTeamTotalManhoursSpent(args: Map<String, Any>): List<Map<String, Any>> { fun monthlyActualTeamTotalManhoursSpent(args: Map<String, Any>): List<Map<String, Any>> {
val sql = StringBuilder( val sql = StringBuilder(
" WITH RECURSIVE date_series AS ("
+ " SELECT DATE_FORMAT(:startdate, '%Y-%m-01') AS month"
+ " UNION ALL"
+ " SELECT DATE_FORMAT(DATE_ADD(month, INTERVAL 1 MONTH), '%Y-%m-01')"
+ " FROM date_series"
+ " WHERE month < DATE_FORMAT(:enddate, '%Y-%m-01')"
+ " )"
+ " SELECT"
+ " ds.month AS yearMonth,"
+ " IFNULL(SUM(IFNULL(ts.normalConsumed, 0) + IFNULL(ts.otConsumed, 0)), 0) AS 'TotalManhourConsumed',"
+ " :teamId AS teamId"
+ " FROM date_series ds"
+ " LEFT JOIN staff st"
+ " on st.teamId = :teamId"
+ " LEFT JOIN timesheet ts"
+ " ON DATE_FORMAT(ts.recordDate, '%Y-%m') = DATE_FORMAT(ds.month, '%Y-%m') and ts.staffID = st.id"
+ " WHERE ds.month BETWEEN DATE_FORMAT(:startdate, '%Y-%m-01') AND DATE_FORMAT(:enddate, '%Y-%m-01')"
+ " GROUP BY ds.month, st.teamId"
+ " ORDER BY ds.month"
// " WITH RECURSIVE date_series AS ("
// + " SELECT DATE_FORMAT(:startdate, '%Y-%m-01') AS month"
// + " UNION ALL"
// + " SELECT DATE_FORMAT(DATE_ADD(month, INTERVAL 1 MONTH), '%Y-%m-01')"
// + " FROM date_series"
// + " WHERE month < DATE_FORMAT(:enddate, '%Y-%m-01')"
// + " )"
// + " SELECT"
// + " ds.month AS yearMonth,"
// + " IFNULL(SUM(IFNULL(ts.normalConsumed, 0) + IFNULL(ts.otConsumed, 0)), 0) AS 'TotalManhourConsumed',"
// + " :teamId AS teamId"
// + " FROM date_series ds"
// + " LEFT JOIN staff st"
// + " on st.teamId = :teamId"
// + " LEFT JOIN timesheet ts"
// + " ON DATE_FORMAT(ts.recordDate, '%Y-%m') = DATE_FORMAT(ds.month, '%Y-%m') and ts.staffID = st.id"
// + " WHERE ds.month BETWEEN DATE_FORMAT(:startdate, '%Y-%m-01') AND DATE_FORMAT(:enddate, '%Y-%m-01')"
// + " and ts.recordDate BETWEEN DATE_FORMAT(:startdate, '%Y-%m-01') AND DATE_FORMAT(:enddate, '%Y-%m-01')"
// + " GROUP BY ds.month, st.teamId"
// + " ORDER BY ds.month"
"WITH RECURSIVE date_series AS ("
+ " SELECT DATE_FORMAT(:startdate, '%Y-%m-01') AS month"
+ " UNION ALL"
+ " SELECT DATE_FORMAT(DATE_ADD(month, INTERVAL 1 MONTH), '%Y-%m-01')"
+ " FROM date_series"
+ " WHERE month < DATE_FORMAT(:enddate, '%Y-%m-01')"
+ " )"
+ " SELECT"
+ " ds.month AS yearMonth,"
+ " COALESCE(SUM(COALESCE(ts.normalConsumed, 0) + COALESCE(ts.otConsumed, 0)), 0) AS 'TotalManhourConsumed',"
+ " :teamId AS teamId"
+ " FROM date_series ds"
+ " LEFT JOIN ("
+ " SELECT"
+ " DATE_FORMAT(ts.recordDate, '%Y-%m-01') AS recordMonth,"
+ " ts.staffID,"
+ " SUM(ts.normalConsumed) AS normalConsumed,"
+ " SUM(ts.otConsumed) AS otConsumed"
+ " FROM staff st"
+ " LEFT JOIN timesheet ts"
+ " on ts.staffID = st.id"
+ " WHERE ts.recordDate BETWEEN DATE_FORMAT(:startdate, '%Y-%m-01') AND LAST_DAY(DATE_FORMAT(:enddate, '%Y-%m-%d'))"
+ " and st.teamId = :teamId"
+ " GROUP BY DATE_FORMAT(ts.recordDate, '%Y-%m-01'), staffID"
+ " ) ts ON ds.month = ts.recordMonth"
+ " GROUP BY ds.month"
+ " ORDER BY ds.month;"

) )


return jdbcDao.queryForList(sql.toString(), args) return jdbcDao.queryForList(sql.toString(), args)
@@ -1253,25 +1545,54 @@ open class DashboardService(
} }
fun weeklyActualTeamTotalManhoursSpent(args: Map<String, Any>): List<Map<String, Any>> { fun weeklyActualTeamTotalManhoursSpent(args: Map<String, Any>): List<Map<String, Any>> {
val sql = StringBuilder( val sql = StringBuilder(
"WITH RECURSIVE date_series AS ("
+ " SELECT DATE_FORMAT(:startdate, '%Y-%m-%d') AS day"
+ " UNION ALL"
+ " SELECT DATE_FORMAT(DATE_ADD(day, INTERVAL 1 DAY), '%Y-%m-%d')"
+ " FROM date_series"
+ " WHERE day < DATE_FORMAT(DATE_ADD(:startdate, INTERVAL 6 DAY), '%Y-%m-%d')"
+ " )"
+ " SELECT"
+ " ds.day AS yearMonth,"
+ " IFNULL(SUM(IFNULL(ts.normalConsumed, 0) + IFNULL(ts.otConsumed, 0)), 0) AS 'TotalManhourConsumed',"
+ " :teamId AS teamId"
+ " FROM date_series ds"
+ " LEFT JOIN staff st"
+ " on st.teamId = :teamId"
+ " LEFT JOIN timesheet ts"
+ " ON DATE_FORMAT(ts.recordDate, '%m-%d') = DATE_FORMAT(ds.day, '%m-%d') and ts.staffID = st.id"
+ " WHERE ds.day BETWEEN DATE_FORMAT(:startdate, '%Y-%m-%d') AND DATE_FORMAT(DATE_ADD(:startdate, INTERVAL 6 DAY), '%Y-%m-%d')"
+ " GROUP BY ds.day, st.teamId"
+ " ORDER BY ds.day"
// "WITH RECURSIVE date_series AS ("
// + " SELECT DATE_FORMAT(:startdate, '%Y-%m-%d') AS day"
// + " UNION ALL"
// + " SELECT DATE_FORMAT(DATE_ADD(day, INTERVAL 1 DAY), '%Y-%m-%d')"
// + " FROM date_series"
// + " WHERE day < DATE_FORMAT(DATE_ADD(:startdate, INTERVAL 6 DAY), '%Y-%m-%d')"
// + " )"
// + " SELECT"
// + " ds.day AS yearMonth,"
// + " IFNULL(SUM(IFNULL(ts.normalConsumed, 0) + IFNULL(ts.otConsumed, 0)), 0) AS 'TotalManhourConsumed',"
// + " :teamId AS teamId"
// + " FROM date_series ds"
// + " LEFT JOIN staff st"
// + " on st.teamId = :teamId"
// + " LEFT JOIN timesheet ts"
// + " ON DATE_FORMAT(ts.recordDate, '%m-%d') = DATE_FORMAT(ds.day, '%m-%d') and ts.staffID = st.id"
// + " WHERE ds.day BETWEEN DATE_FORMAT(:startdate, '%Y-%m-%d') AND DATE_FORMAT(DATE_ADD(:startdate, INTERVAL 6 DAY), '%Y-%m-%d')"
// + " and ts.recordDate BETWEEN DATE_FORMAT(:startdate, '%Y-%m-%d') AND DATE_FORMAT(DATE_ADD(:startdate, INTERVAL 6 DAY), '%Y-%m-%d')"
// + " GROUP BY ds.day, st.teamId"
// + " ORDER BY ds.day"
"WITH RECURSIVE date_series AS ("
+ " SELECT DATE_FORMAT(:startdate, '%Y-%m-%d') AS day"
+ " UNION ALL"
+ " SELECT DATE_FORMAT(DATE_ADD(day, INTERVAL 1 DAY), '%Y-%m-%d')"
+ " FROM date_series"
+ " WHERE day < DATE_FORMAT(DATE_ADD(:startdate, INTERVAL 6 DAY), '%Y-%m-%d')"
+ " )"
+ " SELECT"
+ " ds.day AS yearMonth,"
+ " COALESCE(SUM(COALESCE(ts.normalConsumed, 0) + COALESCE(ts.otConsumed, 0)), 0) AS 'TotalManhourConsumed',"
+ " :teamId AS teamId"
+ " FROM date_series ds"
+ " LEFT JOIN ("
+ " SELECT"
+ " DATE_FORMAT(ts.recordDate, '%Y-%m-%d') AS recordDate,"
+ " ts.staffID,"
+ " SUM(ts.normalConsumed) AS normalConsumed,"
+ " SUM(ts.otConsumed) AS otConsumed"
+ " FROM staff st"
+ " LEFT JOIN timesheet ts"
+ " on ts.staffID = st.id"
+ " WHERE ts.recordDate BETWEEN DATE_FORMAT(:startdate, '%Y-%m-%d') AND DATE_FORMAT(DATE_ADD(:startdate, INTERVAL 6 DAY), '%Y-%m-%d')"
+ " and st.teamId = :teamId"
+ " GROUP BY DATE_FORMAT(ts.recordDate, '%Y-%m-%d'), ts.staffID"
+ " ) ts ON ds.day = ts.recordDate"
+ " GROUP BY ds.day"
+ " ORDER BY ds.day;"

) )


return jdbcDao.queryForList(sql.toString(), args) return jdbcDao.queryForList(sql.toString(), args)
@@ -1346,6 +1667,7 @@ open class DashboardService(
+ " AND dates.missing_date = ts.recordDate" + " AND dates.missing_date = ts.recordDate"
+ " WHERE" + " WHERE"
+ " st.teamId = :teamId" + " st.teamId = :teamId"
+ " and st.deleted = 0"
+ " AND ts.recordDate IS NULL" + " AND ts.recordDate IS NULL"
+ " GROUP BY st.id, st.name" + " GROUP BY st.id, st.name"
+ " ORDER BY" + " ORDER BY"
@@ -1595,6 +1917,7 @@ open class DashboardService(
+ " group by p.id, p.name" + " group by p.id, p.name"
+ " ) as result on result.pid = p2.id" + " ) as result on result.pid = p2.id"
+ " where s2.id = :staffId" + " where s2.id = :staffId"
+ " and result.manhours > 0"
+ " group by p2.id, p2.name, result.manhours" + " group by p2.id, p2.name, result.manhours"
) )


@@ -1626,6 +1949,7 @@ open class DashboardService(
+ " group by p.id, p.name" + " group by p.id, p.name"
+ " ) as result on result.pid = p2.id" + " ) as result on result.pid = p2.id"
+ " where s2.id = :staffId" + " where s2.id = :staffId"
+ " and result.manhours > 0"
+ " group by p2.id, p2.name, result.manhours" + " group by p2.id, p2.name, result.manhours"
) )


@@ -1656,6 +1980,7 @@ open class DashboardService(
+ " group by p.id, p.name" + " group by p.id, p.name"
+ " ) as result on result.pid = p2.id" + " ) as result on result.pid = p2.id"
+ " where s2.id = :staffId" + " where s2.id = :staffId"
+ " and result.manhours > 0"
+ " group by p2.id, p2.name, result.manhours" + " group by p2.id, p2.name, result.manhours"
) )




+ 11
- 0
src/main/java/com/ffii/tsms/modules/data/service/SubsidiaryService.kt Visa fil

@@ -38,6 +38,17 @@ open class SubsidiaryService(
return subsidiaryRepository.findByCode(code); return subsidiaryRepository.findByCode(code);
} }


open fun createSubsidiaryCode(): String {
val prefix = "SY"

val latestSubsidiaryCode = subsidiaryRepository.getLatestCodeNumber()

if (latestSubsidiaryCode != null) {
return "$prefix-" + String.format("%03d", latestSubsidiaryCode + 1L)
} else {
return "$prefix-001"
}
}
open fun saveSubsidiary(saveSubsidiary: SaveSubsidiaryRequest): SaveSubsidiaryResponse { open fun saveSubsidiary(saveSubsidiary: SaveSubsidiaryRequest): SaveSubsidiaryResponse {


val duplicateSubsidiary = findSubsidiaryByCode(saveSubsidiary.code) val duplicateSubsidiary = findSubsidiaryByCode(saveSubsidiary.code)


+ 24
- 0
src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt Visa fil

@@ -51,6 +51,7 @@ class DashboardController(
fun searchCustomerSubsidiaryProject(request: HttpServletRequest?): List<Map<String, Any>> { fun searchCustomerSubsidiaryProject(request: HttpServletRequest?): List<Map<String, Any>> {
val customerId = request?.getParameter("customerId") val customerId = request?.getParameter("customerId")
val subsidiaryId = request?.getParameter("subsidiaryId") val subsidiaryId = request?.getParameter("subsidiaryId")
val tableSorting = request?.getParameter("tableSorting")
val args = mutableMapOf<String, Any>() val args = mutableMapOf<String, Any>()
var result: List<Map<String, Any>> = emptyList() var result: List<Map<String, Any>> = emptyList()
if (customerId != null) { if (customerId != null) {
@@ -59,6 +60,9 @@ class DashboardController(
if (subsidiaryId != null) { if (subsidiaryId != null) {
args["subsidiaryId"] = subsidiaryId args["subsidiaryId"] = subsidiaryId
} }
if (tableSorting != null) {
args["tableSorting"] = tableSorting
}


if (customerId != null && subsidiaryId != null) { if (customerId != null && subsidiaryId != null) {
result = dashboardService.searchCustomerSubsidiaryProject(args) result = dashboardService.searchCustomerSubsidiaryProject(args)
@@ -80,16 +84,36 @@ class DashboardController(
@GetMapping("/searchTeamProject") @GetMapping("/searchTeamProject")
fun searchTeamProject(request: HttpServletRequest?): List<Map<String, Any>> { fun searchTeamProject(request: HttpServletRequest?): List<Map<String, Any>> {
val teamLeadId = request?.getParameter("teamLeadId") val teamLeadId = request?.getParameter("teamLeadId")
val tableSorting = request?.getParameter("tableSorting")
val args = mutableMapOf<String, Any>() val args = mutableMapOf<String, Any>()
var result: List<Map<String, Any>> = emptyList() var result: List<Map<String, Any>> = emptyList()
if (teamLeadId != null) { if (teamLeadId != null) {
args["teamLeadId"] = teamLeadId args["teamLeadId"] = teamLeadId
} }
if (tableSorting != null) {
args["tableSorting"] = tableSorting
}


result = dashboardService.searchTeamProject(args) result = dashboardService.searchTeamProject(args)


return result return result
} }
@GetMapping("/searchTeamConsumption")
fun searchTeamConsumption(request: HttpServletRequest?): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val teamIdList = request?.getParameter("teamIdList")
val tableSorting = request?.getParameter("tableSorting")
val teamIds = teamIdList?.split(",")?.map { it.toInt() }?.toList()
var result: List<Map<String, Any>> = emptyList()
if (teamIds != null) {
args["teamIds"] = teamIds
}
if (tableSorting != null) {
args["tableSorting"] = tableSorting
}
result = dashboardService.searchTeamConsumption(args)
return result
}
@GetMapping("/searchFinancialSummaryCard") @GetMapping("/searchFinancialSummaryCard")
fun searchFinancialSummaryCard(request: HttpServletRequest?): List<Map<String, Any>> { fun searchFinancialSummaryCard(request: HttpServletRequest?): List<Map<String, Any>> {
val authority = dashboardService.viewDashboardAuthority() val authority = dashboardService.viewDashboardAuthority()


+ 1
- 1
src/main/java/com/ffii/tsms/modules/data/web/SkillController.kt Visa fil

@@ -19,7 +19,7 @@ import org.springframework.web.bind.annotation.*
open class SkillController(private val skillService: SkillService) { open class SkillController(private val skillService: SkillService) {


@PostMapping("/save") @PostMapping("/save")
@PreAuthorize("hasAuthority('MAINTAIN_MASTERDATA')")
// @PreAuthorize("hasAuthority('MAINTAIN_MASTERDATA')")
open fun saveSkill(@Valid @RequestBody newSkill: NewSkillRequest): Skill { open fun saveSkill(@Valid @RequestBody newSkill: NewSkillRequest): Skill {
return skillService.saveOrUpdate(newSkill) return skillService.saveOrUpdate(newSkill)
} }


+ 2
- 0
src/main/java/com/ffii/tsms/modules/project/entity/InvoiceRepository.kt Visa fil

@@ -12,4 +12,6 @@ interface InvoiceRepository : AbstractRepository<Invoice, Long> {
fun findInvoiceInfoByPaidAmountIsNotNull(): List<InvoiceInfo> fun findInvoiceInfoByPaidAmountIsNotNull(): List<InvoiceInfo>


fun findByInvoiceNo(invoiceNo: String): Invoice fun findByInvoiceNo(invoiceNo: String): Invoice

fun findAllByProjectCodeAndPaidAmountIsNotNull(projectCode: String): List<Invoice>
} }

+ 3
- 0
src/main/java/com/ffii/tsms/modules/project/entity/Project.kt Visa fil

@@ -77,6 +77,9 @@ open class Project : BaseEntity<Long>() {
@Column(name = "expectedTotalFee") @Column(name = "expectedTotalFee")
open var expectedTotalFee: Double? = null open var expectedTotalFee: Double? = null


@Column(name = "subContractFee")
open var subContractFee: Double? = null

@ManyToOne @ManyToOne
@JoinColumn(name = "serviceTypeId") @JoinColumn(name = "serviceTypeId")
open var serviceType: ServiceType? = null open var serviceType: ServiceType? = null


+ 2
- 0
src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt Visa fil

@@ -34,4 +34,6 @@ interface ProjectRepository : AbstractRepository<Project, Long> {
fun findAllByStatusIsNotAndMainProjectIsNull(status: String): List<Project> fun findAllByStatusIsNotAndMainProjectIsNull(status: String): List<Project>


fun findAllByTeamLeadAndCustomer(teamLead: Staff, customer: Customer): List<Project> fun findAllByTeamLeadAndCustomer(teamLead: Staff, customer: Customer): List<Project>

fun findByCode(code: String): Project?
} }

+ 4
- 4
src/main/java/com/ffii/tsms/modules/project/service/InvoiceService.kt Visa fil

@@ -362,7 +362,7 @@ open class InvoiceService(


// Check the import invoice with the data in DB // Check the import invoice with the data in DB
for (i in 2..sheet.lastRowNum){ for (i in 2..sheet.lastRowNum){
val sheetInvoice = ExcelUtils.getCell(sheet, i, 0).stringCellValue
val sheetInvoice = ExcelUtils.getCell(sheet, i, 0).toString()
val sheetProjectCode = ExcelUtils.getCell(sheet, i, 1).stringCellValue val sheetProjectCode = ExcelUtils.getCell(sheet, i, 1).stringCellValue
checkInvoiceNo(sheetInvoice, invoices, invoicesResult, true) checkInvoiceNo(sheetInvoice, invoices, invoicesResult, true)
checkProjectCode(sheetProjectCode, projects, newProjectCodes) checkProjectCode(sheetProjectCode, projects, newProjectCodes)
@@ -398,7 +398,7 @@ open class InvoiceService(
// val paymentMilestoneId = getMilestonePaymentId(ExcelUtils.getCell(sheet, i, 1).stringCellValue, ExcelUtils.getCell(sheet, i, 5).stringCellValue) // val paymentMilestoneId = getMilestonePaymentId(ExcelUtils.getCell(sheet, i, 1).stringCellValue, ExcelUtils.getCell(sheet, i, 5).stringCellValue)
// val milestonePayment = milestonePaymentRepository.findById(paymentMilestoneId).orElseThrow() // val milestonePayment = milestonePaymentRepository.findById(paymentMilestoneId).orElseThrow()
val invoice = Invoice().apply { val invoice = Invoice().apply {
invoiceNo = ExcelUtils.getCell(sheet, i, 0).stringCellValue
invoiceNo = ExcelUtils.getCell(sheet, i, 0).toString()
projectCode = ExcelUtils.getCell(sheet, i, 1).stringCellValue projectCode = ExcelUtils.getCell(sheet, i, 1).stringCellValue
projectName = ExcelUtils.getCell(sheet, i, 2).stringCellValue projectName = ExcelUtils.getCell(sheet, i, 2).stringCellValue
team = ExcelUtils.getCell(sheet, i, 3).stringCellValue team = ExcelUtils.getCell(sheet, i, 3).stringCellValue
@@ -447,7 +447,7 @@ open class InvoiceService(
val duplicateItemsInInvoice = checkDuplicateItemInImportedInvoice(sheet,2,0) val duplicateItemsInInvoice = checkDuplicateItemInImportedInvoice(sheet,2,0)


for (i in 2..sheet.lastRowNum){ for (i in 2..sheet.lastRowNum){
val sheetInvoice = ExcelUtils.getCell(sheet, i, 0).stringCellValue
val sheetInvoice = ExcelUtils.getCell(sheet, i, 0).toString()
val sheetProjectCode = ExcelUtils.getCell(sheet, i, 1).stringCellValue val sheetProjectCode = ExcelUtils.getCell(sheet, i, 1).stringCellValue
checkInvoiceNo(sheetInvoice, invoices, invoicesResult, false) checkInvoiceNo(sheetInvoice, invoices, invoicesResult, false)
checkProjectCode(sheetProjectCode, projects, newProjectCodes) checkProjectCode(sheetProjectCode, projects, newProjectCodes)
@@ -489,7 +489,7 @@ open class InvoiceService(
} }


for (i in 2..sheet.lastRowNum){ for (i in 2..sheet.lastRowNum){
val invoice = getInvoiceByInvoiceNo(ExcelUtils.getCell(sheet, i, 0).stringCellValue)
val invoice = getInvoiceByInvoiceNo(ExcelUtils.getCell(sheet, i, 0).toString())
invoice.paidAmount = ExcelUtils.getCell(sheet, i, 5).numericCellValue.toBigDecimal() invoice.paidAmount = ExcelUtils.getCell(sheet, i, 5).numericCellValue.toBigDecimal()
invoice.receiptDate = ExcelUtils.getCell(sheet, i, 4).dateCellValue.toInstant().atZone(ZoneId.systemDefault()).toLocalDate() invoice.receiptDate = ExcelUtils.getCell(sheet, i, 4).dateCellValue.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()
saveAndFlush(invoice) saveAndFlush(invoice)


+ 344
- 25
src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt Visa fil

@@ -1,26 +1,26 @@
package com.ffii.tsms.modules.project.service package com.ffii.tsms.modules.project.service


import com.ffii.core.utils.ExcelUtils
import com.ffii.tsms.modules.common.SecurityUtils import com.ffii.tsms.modules.common.SecurityUtils
import com.ffii.tsms.modules.data.entity.* import com.ffii.tsms.modules.data.entity.*
import com.ffii.tsms.modules.data.service.CustomerContactService
import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo
import com.ffii.tsms.modules.data.service.CustomerService
import com.ffii.tsms.modules.data.service.GradeService
import com.ffii.tsms.modules.data.service.SubsidiaryContactService
import com.ffii.tsms.modules.data.service.*
import com.ffii.tsms.modules.project.entity.* import com.ffii.tsms.modules.project.entity.*
import com.ffii.tsms.modules.project.entity.Milestone import com.ffii.tsms.modules.project.entity.Milestone
import com.ffii.tsms.modules.project.entity.projections.InvoiceInfoSearchInfo import com.ffii.tsms.modules.project.entity.projections.InvoiceInfoSearchInfo
import com.ffii.tsms.modules.project.entity.projections.InvoiceSearchInfo import com.ffii.tsms.modules.project.entity.projections.InvoiceSearchInfo
import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo
import com.ffii.tsms.modules.project.web.models.* import com.ffii.tsms.modules.project.web.models.*
import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository
import org.apache.commons.logging.LogFactory
import org.apache.poi.ss.usermodel.Sheet
import org.apache.poi.ss.usermodel.Workbook
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import java.time.LocalDate import java.time.LocalDate
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Optional
import kotlin.jvm.optionals.getOrElse
import kotlin.jvm.optionals.getOrNull import kotlin.jvm.optionals.getOrNull



@Service @Service
open class ProjectsService( open class ProjectsService(
private val projectRepository: ProjectRepository, private val projectRepository: ProjectRepository,
@@ -46,7 +46,11 @@ open class ProjectsService(
private val timesheetRepository: TimesheetRepository, private val timesheetRepository: TimesheetRepository,
private val taskTemplateRepository: TaskTemplateRepository, private val taskTemplateRepository: TaskTemplateRepository,
private val subsidiaryContactService: SubsidiaryContactService, private val subsidiaryContactService: SubsidiaryContactService,
private val subsidiaryContactRepository: SubsidiaryContactRepository
private val subsidiaryContactRepository: SubsidiaryContactRepository,
private val teamRepository: TeamRepository,
private val customerRepository: CustomerRepository,
private val subsidiaryRepository: SubsidiaryRepository,
private val customerSubsidiaryService: CustomerSubsidiaryService,
) { ) {
open fun allProjects(): List<ProjectSearchInfo> { open fun allProjects(): List<ProjectSearchInfo> {
return projectRepository.findProjectSearchInfoByOrderByCreatedDesc() return projectRepository.findProjectSearchInfoByOrderByCreatedDesc()
@@ -107,7 +111,8 @@ open class ProjectsService(
code = project.code!!, code = project.code!!,
name = project.name!!, name = project.name!!,
status = project.status, status = project.status,
tasks = projectTaskRepository.findAllByProject(project).mapNotNull { pt -> pt.task },
tasks = projectTaskRepository.findAllByProject(project).mapNotNull { pt -> pt.task }
.sortedBy { it.taskGroup?.id },
milestones = milestoneRepository.findAllByProject(project) milestones = milestoneRepository.findAllByProject(project)
.filter { milestone -> milestone.taskGroup?.id != null } .filter { milestone -> milestone.taskGroup?.id != null }
.associateBy { milestone -> milestone.taskGroup!!.id!! }.mapValues { (_, milestone) -> .associateBy { milestone -> milestone.taskGroup!!.id!! }.mapValues { (_, milestone) ->
@@ -164,10 +169,15 @@ open class ProjectsService(
.orElseThrow() else null .orElseThrow() else null
val teamLead = staffRepository.findById(request.projectLeadId).orElseThrow() val teamLead = staffRepository.findById(request.projectLeadId).orElseThrow()
val customer = customerService.findCustomer(request.clientId) val customer = customerService.findCustomer(request.clientId)
val subsidiaryContact = subsidiaryContactRepository.findById(request.clientContactId).orElse(null)
val clientContact = customerContactService.findByContactId(request.clientContactId)
val subsidiaryContact =
if (request.clientContactId != null) subsidiaryContactRepository.findById(request.clientContactId)
.orElse(null) else null
val clientContact =
if (request.clientContactId != null) customerContactService.findByContactId(request.clientContactId) else null
val customerSubsidiary = request.clientSubsidiaryId?.let { subsidiaryService.findSubsidiary(it) } val customerSubsidiary = request.clientSubsidiaryId?.let { subsidiaryService.findSubsidiary(it) }
val mainProject = if (request.mainProjectId != null && request.mainProjectId > 0) projectRepository.findById(request.mainProjectId).orElse(null) else null
val mainProject =
if (request.mainProjectId != null && request.mainProjectId > 0) projectRepository.findById(request.mainProjectId)
.orElse(null) else null


val allTasksMap = tasksService.allTasks().associateBy { it.id } val allTasksMap = tasksService.allTasks().associateBy { it.id }
val taskGroupMap = tasksService.allTaskGroups().associateBy { it.id } val taskGroupMap = tasksService.allTaskGroups().associateBy { it.id }
@@ -179,15 +189,23 @@ open class ProjectsService(
project.apply { project.apply {
name = request.projectName name = request.projectName
description = request.projectDescription description = request.projectDescription
code = if (this.code.isNullOrEmpty() && request.mainProjectId == null) createProjectCode(request.isClpProject, project) else if (this.code.isNullOrEmpty() && request.mainProjectId != null && mainProject != null) createSubProjectCode(mainProject, project) else this.code
code = request.projectCode
?: if (this.code.isNullOrEmpty() && request.mainProjectId == null) createProjectCode(
request.isClpProject,
project
) else if (this.code.isNullOrEmpty() && request.mainProjectId != null && mainProject != null) createSubProjectCode(
mainProject,
project
) else this.code
expectedTotalFee = request.expectedProjectFee expectedTotalFee = request.expectedProjectFee
subContractFee = request.subContractFee
totalManhour = request.totalManhour totalManhour = request.totalManhour
actualStart = request.projectActualStart actualStart = request.projectActualStart
actualEnd = request.projectActualEnd actualEnd = request.projectActualEnd
status = if (this.status == "Deleted" || this.deleted == true) "Deleted" status = if (this.status == "Deleted" || this.deleted == true) "Deleted"
else if (this.actualStart != null && this.actualEnd != null) "Completed" else if (this.actualStart != null && this.actualEnd != null) "Completed"
else if (this.actualStart != null) "On-going" else if (this.actualStart != null) "On-going"
else "Pending To Start"
else request.projectStatus ?: "Pending To Start"
isClpProject = request.isClpProject isClpProject = request.isClpProject
this.mainProject = mainProject this.mainProject = mainProject


@@ -203,11 +221,11 @@ open class ProjectsService(
this.teamLead = teamLead this.teamLead = teamLead
this.customer = customer this.customer = customer
custLeadName = custLeadName =
if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.name else subsidiaryContact.name
if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact?.name else subsidiaryContact?.name
custLeadEmail = custLeadEmail =
if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.email else subsidiaryContact.email
if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact?.email else subsidiaryContact?.email
custLeadPhone = custLeadPhone =
if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.phone else subsidiaryContact.phone
if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact?.phone else subsidiaryContact?.phone
this.customerSubsidiary = customerSubsidiary this.customerSubsidiary = customerSubsidiary
this.customerContact = if (customerSubsidiary == null) clientContact else null this.customerContact = if (customerSubsidiary == null) clientContact else null
this.subsidiaryContact = if (customerSubsidiary != null) subsidiaryContact else null this.subsidiaryContact = if (customerSubsidiary != null) subsidiaryContact else null
@@ -279,14 +297,15 @@ open class ProjectsService(
if (milestones.isNotEmpty()) { if (milestones.isNotEmpty()) {
project.apply { project.apply {
planStart = milestones.mapNotNull { it.startDate }.minOrNull() planStart = milestones.mapNotNull { it.startDate }.minOrNull()
planEnd = milestones.mapNotNull { it.endDate }.maxOrNull()
planEnd = request.projectPlanEnd ?: milestones.mapNotNull { it.endDate }.maxOrNull()
} }
} }


val savedProject = projectRepository.save(project)
val savedProject = projectRepository.saveAndFlush(project)


val milestonesToDelete = milestoneRepository.findAllByProject(project).subtract(milestones.toSet()) val milestonesToDelete = milestoneRepository.findAllByProject(project).subtract(milestones.toSet())
val tasksToDelete = projectTaskRepository.findAllByProject(project).subtract(tasksToSave.toSet())
val mapTasksToSave = tasksToSave.map { it.task!!.id }
val tasksToDelete = projectTaskRepository.findAllByProject(project).filter { !mapTasksToSave.contains(it.task!!.id) }
val gradeAllocationsToDelete = val gradeAllocationsToDelete =
gradeAllocationRepository.findByProject(project).subtract(gradeAllocations.toSet()) gradeAllocationRepository.findByProject(project).subtract(gradeAllocations.toSet())
milestoneRepository.deleteAll(milestonesToDelete) milestoneRepository.deleteAll(milestonesToDelete)
@@ -386,7 +405,8 @@ open class ProjectsService(
) )
}) })
}, },
expectedProjectFee = it.expectedTotalFee
expectedProjectFee = it.expectedTotalFee,
subContractFee = it.subContractFee
) )
} }
} }
@@ -422,8 +442,9 @@ open class ProjectsService(
val subsidiaryContact = project.customerSubsidiary?.id?.let { subsidiaryId -> val subsidiaryContact = project.customerSubsidiary?.id?.let { subsidiaryId ->
subsidiaryContactService.findAllBySubsidiaryId(subsidiaryId) subsidiaryContactService.findAllBySubsidiaryId(subsidiaryId)
} ?: emptyList() } ?: emptyList()
val customerContact = project.customer?.id?.let { customerId -> customerContactService.findAllByCustomerId(customerId) }
?: emptyList()
val customerContact =
project.customer?.id?.let { customerId -> customerContactService.findAllByCustomerId(customerId) }
?: emptyList()


MainProjectDetails( MainProjectDetails(
projectId = project.id, projectId = project.id,
@@ -441,10 +462,308 @@ open class ProjectsService(
buildingTypeIds = project.buildingTypes.mapNotNull { buildingType -> buildingType.id }, buildingTypeIds = project.buildingTypes.mapNotNull { buildingType -> buildingType.id },
workNatureIds = project.workNatures.mapNotNull { workNature -> workNature.id }, workNatureIds = project.workNatures.mapNotNull { workNature -> workNature.id },
clientId = project.customer?.id, clientId = project.customer?.id,
clientContactId = subsidiaryContact.find { contact -> contact.name == project.custLeadName }?.id ?: customerContact.find { contact -> contact.name == project.custLeadName }?.id,
clientContactId = subsidiaryContact.find { contact -> contact.name == project.custLeadName }?.id
?: customerContact.find { contact -> contact.name == project.custLeadName }?.id,
clientSubsidiaryId = project.customerSubsidiary?.id, clientSubsidiaryId = project.customerSubsidiary?.id,
expectedProjectFee = project.expectedTotalFee
expectedProjectFee = project.expectedTotalFee,
subContractFee = project.subContractFee,
) )
} }
} }

@Transactional(rollbackFor = [Exception::class])
open fun importFile(workbook: Workbook?): String {
val logger = LogFactory.getLog(javaClass)

if (workbook == null) {
return "No Excel import" // if workbook is null
}

val sheet: Sheet = workbook.getSheetAt(0)

for (i in 2..<sheet.lastRowNum) {
val row = sheet.getRow(i)

if (row?.getCell(0) != null && !row.getCell(0).stringCellValue.isNullOrBlank()) {
logger.info("row :$i | lastCellNum" + row.lastCellNum)

// process client
logger.info("---------client-------")
logger.info(customerRepository.findByName(row.getCell(5).stringCellValue.trim()))
val tempClient = customerRepository.findByName(row.getCell(5).stringCellValue.trim())
val currentClient =
if (tempClient.isPresent) tempClient.orElseThrow() else customerService.saveCustomer(
SaveCustomerRequest(
code = customerService.createClientCode(),
name = ExcelUtils.getStringValue(row.getCell(5)),
typeId = ExcelUtils.getIntValue(row.getCell(6)).toLong(),
deleteSubsidiaryIds = mutableListOf(),
deleteContactIds = mutableListOf(),
addSubsidiaryIds = mutableListOf(),
addContacts = mutableListOf(),
address = null,
brNo = null,
id = null,
district = null,
)
).customer
val clientId = currentClient.id!!

logger.info("customer: $clientId")

// process project
logger.info("---------project-------")
val projectTeamLead = teamRepository.findByCode(ExcelUtils.getStringValue(row.getCell(4))).staff

var mainProjectId: Long? = null
var projectCode = StringBuilder(row.getCell(0).stringCellValue).insert(1, '-').toString()

if (projectCode.contains('(')) {
val splitProjectCode = projectCode.split('(')
val splitMainProjectCode = splitProjectCode[0].split('-')

logger.info("splitProjectCode: " + splitProjectCode[1].split(')')[0])
projectCode =
splitMainProjectCode[0] + '-' + String.format(
"%04d",
splitMainProjectCode[1].toInt()
) + '-' + String.format("%03d", splitProjectCode[1].split(')')[0].toInt())

val mainProject =
projectRepository.findByCode(splitProjectCode[0]) ?: projectRepository.saveAndFlush(
Project().apply {
name = row.getCell(1).stringCellValue
description = row.getCell(1).stringCellValue
code = splitMainProjectCode[0] + '-' + String.format(
"%04d",
splitMainProjectCode[1].toInt()
)
status = "Completed"
projectCategory = projectCategoryRepository.findById(1).orElseThrow()
customer = currentClient
teamLead = projectTeamLead
})

mainProjectId = mainProject.id
} else {
val splitProjectCode = projectCode.split('-')
projectCode = splitProjectCode[0] + '-' + String.format("%04d", splitProjectCode[1].toInt())
}

val project = projectRepository.findByCode(projectCode)
val projectId = project?.id
logger.info("projectCode :$projectCode")

// process subsidiary
logger.info("---------subsidiary-------")
val subsidiary = if (row.getCell(7) != null && !row.getCell(7).stringCellValue.isNullOrBlank()) {
val tempSubsidiary = subsidiaryRepository.findByName(ExcelUtils.getStringValue(row.getCell(7)))

if (tempSubsidiary.isPresent) {
tempSubsidiary.orElseThrow()
} else {
subsidiaryRepository.findByName(ExcelUtils.getStringValue(row.getCell(7))).orElse(
subsidiaryService.saveSubsidiary(
SaveSubsidiaryRequest(
code = subsidiaryService.createSubsidiaryCode(),
name = ExcelUtils.getStringValue(row.getCell(7)),
typeId = ExcelUtils.getIntValue(row.getCell(8)).toLong(),
brNo = null,
address = null,
district = null,
deleteContactIds = mutableListOf(),
addContacts = mutableListOf(),
deleteCustomerIds = mutableListOf(),
addCustomerIds = mutableListOf(clientId),
id = null
)
).subsidiary
)
}
} else null

if (subsidiary != null) {
if (!customerSubsidiaryService.findAllCustomerIdsBySubsidiaryId(subsidiary.id!!)
.contains(clientId)
) {
subsidiaryService.saveSubsidiary(
SaveSubsidiaryRequest(
code = subsidiary.code,
name = subsidiary.name,
typeId = subsidiary.subsidiaryType.id!!,
brNo = subsidiary.brNo,
address = subsidiary.address,
district = subsidiary.district,
deleteContactIds = mutableListOf(),
addContacts = mutableListOf(),
deleteCustomerIds = mutableListOf(),
addCustomerIds = mutableListOf(clientId),
id = subsidiary.id
)
)
}
}

// process building type
logger.info("---------building type-------")
var buildingType = buildingTypeRepository.findByName(row.getCell(13).stringCellValue)
if (buildingType == null) {
buildingType =
buildingTypeRepository.saveAndFlush(BuildingType().apply {
name = row.getCell(13).stringCellValue
})
}

// process work nature
logger.info("---------work nature-------")
var workNature = workNatureRepository.findByName(row.getCell(14).stringCellValue)
if (workNature == null) {
workNature =
workNatureRepository.saveAndFlush(WorkNature().apply { name = row.getCell(14).stringCellValue })
}

// process staff - staff from column R (17)
logger.info("---------staff-------")
val allocatedStaffIds = mutableListOf<Long>()
for (j in 18..<row.lastCellNum step 2) {
logger.info("j: $j | staffId: " + row.getCell(j).stringCellValue)
val tempStaffId = row.getCell(j).stringCellValue

if (!tempStaffId.isNullOrBlank()) {
allocatedStaffIds.add(staffRepository.findByStaffId(tempStaffId).orElseThrow().id!!)
}
}

// get task template - TW-Full QS - stage 5
logger.info("---------task template-------")
val taskTemplate = taskTemplateRepository.findById(3).orElseThrow()
val tasks = taskTemplate.tasks.groupBy { it.taskGroup!!.id }
val groups = taskTemplate.groupAllocations.associateBy(
keySelector = { it.taskGroup!!.id!! },
valueTransform = { it.percentage!! }
)
val projectTasks = if (project != null) projectTaskRepository.findAllByProject(project) else mutableListOf()
var groupedProjectTasks = mapOf<Long, List<Long>>()
if (projectTasks.isNotEmpty()) {
groupedProjectTasks = projectTasks.groupBy {
it.task!!.taskGroup!!.id!!
}.mapValues { (_, projectTask) ->
projectTask.map { it.task!!.id!! }
}
}
val taskGroups = mutableMapOf<Long, TaskGroupAllocation>()
groups.entries.forEach{ (key, value) ->
var taskIds = tasks[key]!!.map { it.id!! }
if (groupedProjectTasks.isNotEmpty() && !groupedProjectTasks[key].isNullOrEmpty()) {
taskIds = (taskIds + groupedProjectTasks[key]!!).distinct()
}
if (key == 5L) {
taskGroups[key] = TaskGroupAllocation(taskIds.plus(41).distinct(), value)
logger.info("taskGroups[${key}]: ${taskGroups[key]}")
} else {
taskGroups[key] = TaskGroupAllocation(taskIds, value)
logger.info("taskGroups[${key}]: ${taskGroups[key]}")
}
}

logger.info("---------Import-------")
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val startDate = ExcelUtils.getDateValue(
row.getCell(2),
DateTimeFormatter.ofPattern("MM/dd/yyyy")
).format(formatter)
val endDate = ExcelUtils.getDateValue(
row.getCell(3),
DateTimeFormatter.ofPattern("MM/dd/yyyy")
).format(formatter)
saveProject(
NewProjectRequest(
projectCode = projectCode,
projectName = row.getCell(1).stringCellValue,
projectDescription = row.getCell(1).stringCellValue,
projectActualStart = ExcelUtils.getDateValue(
row.getCell(2),
DateTimeFormatter.ofPattern("MM/dd/yyyy")
),
projectPlanEnd = ExcelUtils.getDateValue(
row.getCell(3),
DateTimeFormatter.ofPattern("MM/dd/yyyy")
),
projectLeadId = projectTeamLead.id!!,
clientId = clientId,
clientSubsidiaryId = subsidiary?.id,
expectedProjectFee = row.getCell(9).numericCellValue,
subContractFee = null,
totalManhour = row.getCell(11).numericCellValue,
locationId = 1, // HK
buildingTypeIds = mutableListOf(buildingType!!.id!!),
workNatureIds = mutableListOf(workNature!!.id!!),
serviceTypeId = 9,
fundingTypeId = row.getCell(16).numericCellValue.toLong(),
allocatedStaffIds = allocatedStaffIds,
isClpProject = projectCode[0] != 'M',
mainProjectId = mainProjectId,
projectCategoryId = 1,
contractTypeId = if (row.getCell(14).stringCellValue == "Maintenance Term Contract") 5 else 2,
projectId = projectId,
projectStatus = "On-going",
clientContactId = null,
isSubsidiaryContact = subsidiary?.id != null && subsidiary.id!! > 0,
manhourPercentageByGrade = taskTemplate.gradeAllocations.associateBy(
keySelector = { it.grade!!.id!! },
valueTransform = { it.percentage!! }
),
projectActualEnd = null,
taskTemplateId = taskTemplate.id,
taskGroups = taskGroups,
milestones = mapOf(
Pair(
1, com.ffii.tsms.modules.project.web.models.Milestone(
startDate = startDate,
endDate = endDate,
payments = mutableListOf()
)
),
Pair(
2, com.ffii.tsms.modules.project.web.models.Milestone(
startDate = startDate,
endDate = endDate,
payments = mutableListOf()
)
),
Pair(
3, com.ffii.tsms.modules.project.web.models.Milestone(
startDate = startDate,
endDate = endDate,
payments = mutableListOf()
)
),
Pair(
4, com.ffii.tsms.modules.project.web.models.Milestone(
startDate = startDate,
endDate = endDate,
payments = mutableListOf()
)
),
Pair(
5, com.ffii.tsms.modules.project.web.models.Milestone(
startDate = startDate,
endDate = endDate,
payments = mutableListOf(
PaymentInputs(
-1, "Manhour Import", ExcelUtils.getDateValue(
row.getCell(2),
DateTimeFormatter.ofPattern("MM/dd/yyyy")
).toString(), row.getCell(9).numericCellValue
)
)
)
),
))
)
}
}

return if (sheet.lastRowNum > 0) "Import Excel success" else "Import Excel failure"
}
} }

+ 24
- 2
src/main/java/com/ffii/tsms/modules/project/web/ProjectsController.kt Visa fil

@@ -2,15 +2,21 @@ package com.ffii.tsms.modules.project.web


import com.ffii.core.exception.NotFoundException import com.ffii.core.exception.NotFoundException
import com.ffii.tsms.modules.data.entity.* import com.ffii.tsms.modules.data.entity.*
import com.ffii.tsms.modules.project.entity.Project
import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo
import com.ffii.tsms.modules.project.entity.ProjectCategory import com.ffii.tsms.modules.project.entity.ProjectCategory
import com.ffii.tsms.modules.project.entity.ProjectRepository import com.ffii.tsms.modules.project.entity.ProjectRepository
import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo
import com.ffii.tsms.modules.project.service.ProjectsService import com.ffii.tsms.modules.project.service.ProjectsService
import com.ffii.tsms.modules.project.web.models.* import com.ffii.tsms.modules.project.web.models.*
import jakarta.servlet.http.HttpServletRequest
import jakarta.validation.Valid import jakarta.validation.Valid
import org.apache.poi.ss.usermodel.Workbook
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.ServletRequestBindingException
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartHttpServletRequest



@RestController @RestController
@RequestMapping("/projects") @RequestMapping("/projects")
@@ -85,4 +91,20 @@ class ProjectsController(private val projectsService: ProjectsService, private v
fun projectWorkNatures(): List<WorkNature> { fun projectWorkNatures(): List<WorkNature> {
return projectsService.allWorkNatures() return projectsService.allWorkNatures()
} }

@PostMapping("/import")
@Throws(ServletRequestBindingException::class)
fun importFile(request: HttpServletRequest): ResponseEntity<*> {
var workbook: Workbook? = null

try {
val multipartFile = (request as MultipartHttpServletRequest).getFile("multipartFileList")
workbook = XSSFWorkbook(multipartFile?.inputStream)
} catch (e: Exception) {
println("Excel Wrong")
println(e)
}

return ResponseEntity.ok(projectsService.importFile(workbook))
}
} }

+ 2
- 1
src/main/java/com/ffii/tsms/modules/project/web/models/EditProjectDetails.kt Visa fil

@@ -40,5 +40,6 @@ data class EditProjectDetails(
val milestones: Map<Long, Milestone>, val milestones: Map<Long, Milestone>,


// Miscellaneous // Miscellaneous
val expectedProjectFee: Double?
val expectedProjectFee: Double?,
val subContractFee: Double?
) )

+ 1
- 0
src/main/java/com/ffii/tsms/modules/project/web/models/MainProjectDetails.kt Visa fil

@@ -27,4 +27,5 @@ data class MainProjectDetails (
val clientSubsidiaryId: Long?, val clientSubsidiaryId: Long?,


val expectedProjectFee: Double?, val expectedProjectFee: Double?,
val subContractFee: Double?,
) )

+ 6
- 3
src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt Visa fil

@@ -7,7 +7,7 @@ import java.time.LocalDate
data class NewProjectRequest( data class NewProjectRequest(
// Project details // Project details
// @field:NotBlank(message = "project code cannot be empty") // @field:NotBlank(message = "project code cannot be empty")
// val projectCode: String,
val projectCode: String?,
@field:NotBlank(message = "project name cannot be empty") @field:NotBlank(message = "project name cannot be empty")
val projectName: String, val projectName: String,
val projectCategoryId: Long, val projectCategoryId: Long,
@@ -16,8 +16,10 @@ data class NewProjectRequest(
val projectId: Long?, val projectId: Long?,
val projectActualStart: LocalDate?, val projectActualStart: LocalDate?,
val projectActualEnd: LocalDate?, val projectActualEnd: LocalDate?,
val projectPlanEnd: LocalDate?,
val isClpProject: Boolean?, val isClpProject: Boolean?,
val mainProjectId: Long?, val mainProjectId: Long?,
val projectStatus: String?,


val serviceTypeId: Long, val serviceTypeId: Long,
val fundingTypeId: Long, val fundingTypeId: Long,
@@ -30,7 +32,7 @@ data class NewProjectRequest(


// Client details // Client details
val clientId: Long, val clientId: Long,
val clientContactId: Long,
val clientContactId: Long?,
val clientSubsidiaryId: Long?, val clientSubsidiaryId: Long?,


// Allocation // Allocation
@@ -43,7 +45,8 @@ data class NewProjectRequest(
val milestones: Map<Long, Milestone>, val milestones: Map<Long, Milestone>,


// Miscellaneous // Miscellaneous
val expectedProjectFee: Double
val expectedProjectFee: Double,
val subContractFee: Double?
) )


data class TaskGroupAllocation( data class TaskGroupAllocation(


+ 476
- 158
src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt Visa fil

@@ -2,6 +2,7 @@ package com.ffii.tsms.modules.report.service


import com.ffii.core.support.JdbcDao import com.ffii.core.support.JdbcDao
import com.ffii.tsms.modules.data.entity.Customer import com.ffii.tsms.modules.data.entity.Customer
import com.ffii.tsms.modules.data.entity.Grade
import com.ffii.tsms.modules.data.entity.Salary import com.ffii.tsms.modules.data.entity.Salary
import com.ffii.tsms.modules.data.entity.Staff import com.ffii.tsms.modules.data.entity.Staff
import com.ffii.tsms.modules.data.entity.Team import com.ffii.tsms.modules.data.entity.Team
@@ -17,6 +18,8 @@ import org.apache.poi.ss.util.CellRangeAddress
import org.apache.poi.ss.util.CellUtil import org.apache.poi.ss.util.CellUtil
import org.apache.poi.xssf.usermodel.XSSFWorkbook import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.apache.poi.ss.usermodel.FormulaEvaluator import org.apache.poi.ss.usermodel.FormulaEvaluator
import org.apache.poi.ss.util.CellReference
import org.apache.poi.ss.util.RegionUtil
import org.springframework.core.io.ClassPathResource import org.springframework.core.io.ClassPathResource
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
@@ -54,6 +57,7 @@ open class ReportService(
private val COMPLETE_PROJECT_OUTSTANDING_RECEIVABLE = private val COMPLETE_PROJECT_OUTSTANDING_RECEIVABLE =
"templates/report/AR06_Project Completion Report with Outstanding Accounts Receivable v02.xlsx" "templates/report/AR06_Project Completion Report with Outstanding Accounts Receivable v02.xlsx"
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"


// ==============================|| GENERATE REPORT ||============================== // // ==============================|| GENERATE REPORT ||============================== //
fun generalCreateReportIndexed( // just loop through query records one by one, return rowIndex fun generalCreateReportIndexed( // just loop through query records one by one, return rowIndex
@@ -65,7 +69,7 @@ open class ReportService(
var rowIndex = startRow var rowIndex = startRow
var columnIndex = startColumn var columnIndex = startColumn
result.forEachIndexed { index, obj -> result.forEachIndexed { index, obj ->
var tempCell = sheet.getRow(rowIndex).createCell(columnIndex)
var tempCell = (sheet.getRow(rowIndex) ?: sheet.createRow(rowIndex)).createCell(columnIndex)
tempCell.setCellValue((index + 1).toDouble()) tempCell.setCellValue((index + 1).toDouble())
val keys = obj.keys.toList() val keys = obj.keys.toList()
keys.forEachIndexed { keyIndex, key -> keys.forEachIndexed { keyIndex, key ->
@@ -299,6 +303,25 @@ open class ReportService(
return outputStream.toByteArray() return outputStream.toByteArray()
} }


@Throws(IOException::class)
fun generateCrossTeamChargeReport(
month: String,
timesheets: List<Timesheet>,
teams: List<Team>,
grades: List<Grade>,
): ByteArray {
// Generate the Excel report with query results
val workbook: Workbook =
createCrossTeamChargeReport(month, timesheets, teams, grades, CROSS_TEAM_CHARGE_REPORT)

// Write the workbook to a ByteArrayOutputStream
val outputStream: ByteArrayOutputStream = ByteArrayOutputStream()
workbook.write(outputStream)
workbook.close()

return outputStream.toByteArray()
}

// ==============================|| CREATE REPORT ||============================== // // ==============================|| CREATE REPORT ||============================== //


// EX01 Financial Report // EX01 Financial Report
@@ -435,7 +458,8 @@ open class ReportService(
val uninvoiceCell = row.createCell(12) val uninvoiceCell = row.createCell(12)
uninvoiceCell.apply { uninvoiceCell.apply {
cellFormula = cellFormula =
" IF(I${rowNum}<=J${rowNum}, I${rowNum}-L${rowNum}, IF(AND(I${rowNum}>J${rowNum}, J${rowNum}<L${rowNum}), 0, IF(AND(I${rowNum}>J${rowNum}, J${rowNum}>=L${rowNum}), J${rowNum}-L${rowNum}, 0))) "
" IF(H${rowNum}-L${rowNum}<0, 0, H${rowNum}-L${rowNum})"
// " IF(I${rowNum}<=J${rowNum}, I${rowNum}-L${rowNum}, IF(AND(I${rowNum}>J${rowNum}, J${rowNum}<L${rowNum}), 0, IF(AND(I${rowNum}>J${rowNum}, J${rowNum}>=L${rowNum}), J${rowNum}-L${rowNum}, 0))) "
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }


@@ -528,6 +552,11 @@ open class ReportService(
CellUtil.setCellStyleProperty(sumUInvoiceCell, "borderBottom", BorderStyle.DOUBLE) CellUtil.setCellStyleProperty(sumUInvoiceCell, "borderBottom", BorderStyle.DOUBLE)


val lastCpiCell = row.createCell(13) val lastCpiCell = row.createCell(13)
lastCpiCell.apply {
cellFormula = "SUM(N15:N${rowNum})"
cellStyle = boldFontCellStyle
cellStyle.dataFormat = accountingStyle
}
CellUtil.setCellStyleProperty(lastCpiCell, "borderTop", BorderStyle.THIN) CellUtil.setCellStyleProperty(lastCpiCell, "borderTop", BorderStyle.THIN)
CellUtil.setCellStyleProperty(lastCpiCell, "borderBottom", BorderStyle.DOUBLE) CellUtil.setCellStyleProperty(lastCpiCell, "borderBottom", BorderStyle.DOUBLE)


@@ -606,7 +635,7 @@ open class ReportService(
val row9: Row = sheet.getRow(rowNum) val row9: Row = sheet.getRow(rowNum)
val cell5 = row9.createCell(2) val cell5 = row9.createCell(2)
cell5.apply { cell5.apply {
cellFormula = "N${lastRowNum}"
cellFormula = "M${lastRowNum}"
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }


@@ -691,12 +720,12 @@ open class ReportService(
rowIndex = 10 rowIndex = 10
sheet.getRow(rowIndex).apply { sheet.getRow(rowIndex).apply {
createCell(1).apply { createCell(1).apply {
setCellValue(project.expectedTotalFee!! * 0.8)
setCellValue(if (project.expectedTotalFee != null) project.expectedTotalFee!! * 0.8 else 0.0)
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }


createCell(2).apply { createCell(2).apply {
setCellValue(project.expectedTotalFee!!)
setCellValue(project.expectedTotalFee ?: 0.0)
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }
} }
@@ -979,10 +1008,15 @@ open class ReportService(
project.milestones.forEach { milestone: Milestone -> project.milestones.forEach { milestone: Milestone ->


val manHoursSpent = groupedTimesheets[Pair(project.id, milestone.id)]?.sum() ?: 0.0 val manHoursSpent = groupedTimesheets[Pair(project.id, milestone.id)]?.sum() ?: 0.0
val resourceUtilization = manHoursSpent / (milestone.stagePercentAllocation!! / 100 * project.totalManhour!!)
val resourceUtilization =
manHoursSpent / (milestone.stagePercentAllocation!! / 100 * project.totalManhour!!)
// logger.info(project.name + " : " + milestone.taskGroup?.name + " : " + ChronoUnit.DAYS.between(LocalDate.now(), milestone.endDate)) // logger.info(project.name + " : " + milestone.taskGroup?.name + " : " + ChronoUnit.DAYS.between(LocalDate.now(), milestone.endDate))
// logger.info(daysUntilCurrentStageEnd) // logger.info(daysUntilCurrentStageEnd)
if (ChronoUnit.DAYS.between(LocalDate.now(), milestone.endDate) <= daysUntilCurrentStageEnd.toLong() && resourceUtilization <= resourceUtilizationPercentage.toDouble() / 100.0) {
if (ChronoUnit.DAYS.between(
LocalDate.now(),
milestone.endDate
) <= daysUntilCurrentStageEnd.toLong() && resourceUtilization <= resourceUtilizationPercentage.toDouble() / 100.0
) {
milestoneCount++ milestoneCount++
val tempRow = sheet.getRow(rowIndex) ?: sheet.createRow(rowIndex) val tempRow = sheet.getRow(rowIndex) ?: sheet.createRow(rowIndex)
rowIndex++ rowIndex++
@@ -1004,7 +1038,7 @@ open class ReportService(
// //
// val resourceUtilization = // val resourceUtilization =
// manHoursSpent / (milestone.stagePercentAllocation!! / 100 * project.totalManhour!!) // manHoursSpent / (milestone.stagePercentAllocation!! / 100 * project.totalManhour!!)
setCellValue(resourceUtilization)
setCellValue(resourceUtilization)
// } else { // } else {
// setCellValue(0.0) // setCellValue(0.0)
// } // }
@@ -1080,6 +1114,13 @@ open class ReportService(
val boldFont = workbook.createFont() val boldFont = workbook.createFont()
boldFont.bold = true boldFont.bold = true
boldStyle.setFont(boldFont) boldStyle.setFont(boldFont)

val projectsStyle = workbook.createCellStyle()
val projectsFont = workbook.createFont()
projectsFont.bold = true
projectsFont.fontName = "Times New Roman"
projectsStyle.setFont(projectsFont)

val daysOfMonth = (1..month.lengthOfMonth()).map { day -> val daysOfMonth = (1..month.lengthOfMonth()).map { day ->
val date = month.withDayOfMonth(day) val date = month.withDayOfMonth(day)
val formattedDate = date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")) val formattedDate = date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"))
@@ -1228,7 +1269,7 @@ open class ReportService(
projectList.forEachIndexed { index, title -> projectList.forEachIndexed { index, title ->
tempCell = sheet.getRow(7).createCell(columnIndex + index) tempCell = sheet.getRow(7).createCell(columnIndex + index)
tempCell.setCellValue(title) tempCell.setCellValue(title)
tempCell.cellStyle = boldStyle
tempCell.cellStyle = projectsStyle
CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER)
CellUtil.setVerticalAlignment(tempCell, VerticalAlignment.CENTER) CellUtil.setVerticalAlignment(tempCell, VerticalAlignment.CENTER)
CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom) CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom)
@@ -1271,7 +1312,7 @@ open class ReportService(
///////////////////////////////////////////////////////// Leave Hours title //////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////// Leave Hours title ////////////////////////////////////////////////////////////////////
tempCell = sheet.getRow(rowIndex).createCell(columnIndex) tempCell = sheet.getRow(rowIndex).createCell(columnIndex)
tempCell.setCellValue("Leave Hours") tempCell.setCellValue("Leave Hours")
tempCell.cellStyle = boldStyle
tempCell.cellStyle = projectsStyle
CellUtil.setVerticalAlignment(tempCell, VerticalAlignment.CENTER) CellUtil.setVerticalAlignment(tempCell, VerticalAlignment.CENTER)
CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER)
CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom) CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom)
@@ -1279,8 +1320,10 @@ open class ReportService(
columnIndex += 1 columnIndex += 1
tempCell = sheet.getRow(rowIndex).createCell(columnIndex) tempCell = sheet.getRow(rowIndex).createCell(columnIndex)
tempCell.setCellValue("Daily Manhour Spent\n(Excluding Leave Hours)") tempCell.setCellValue("Daily Manhour Spent\n(Excluding Leave Hours)")
tempCell.cellStyle = boldStyle
CellUtil.setAlignment(tempCell, HorizontalAlignment.LEFT)
tempCell.cellStyle = projectsStyle
// CellUtil.setAlignment(tempCell, HorizontalAlignment.LEFT)
CellUtil.setVerticalAlignment(tempCell, VerticalAlignment.CENTER)
CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER)
CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom) CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom)


sheet.addMergedRegion(CellRangeAddress(6, 6, 2, columnIndex)) sheet.addMergedRegion(CellRangeAddress(6, 6, 2, columnIndex))
@@ -1391,7 +1434,7 @@ open class ReportService(
rowIndex = 5 rowIndex = 5
columnIndex = 0 columnIndex = 0


generalCreateReportIndexed(sheet, result, rowIndex, columnIndex)
generalCreateReportIndexed(sheet, result.distinct(), rowIndex, columnIndex)
return workbook return workbook
} }


@@ -1481,7 +1524,7 @@ open class ReportService(
setDataAndConditionalFormatting(workbook, sheet, lateStartData, evaluator) setDataAndConditionalFormatting(workbook, sheet, lateStartData, evaluator)


// Automatically adjust column widths to fit content // Automatically adjust column widths to fit content
autoSizeColumns(sheet)
autoSizeColumns(sheet)


return workbook return workbook
} }
@@ -1627,7 +1670,8 @@ open class ReportService(
// NEW Column F: Subsidiary Name // NEW Column F: Subsidiary Name
val subsidiaryNameCell = row.createCell(5) val subsidiaryNameCell = row.createCell(5)
// subsidiaryNameCell.setCellValue(data["subsidiary_name"] as String) // subsidiaryNameCell.setCellValue(data["subsidiary_name"] as String)
val subsidiaryName = data["subsidiary_name"] as? String ?: "N/A" // Checks if subsidiary_name is null and replaces it with "N/A"
val subsidiaryName =
data["subsidiary_name"] as? String ?: "N/A" // Checks if subsidiary_name is null and replaces it with "N/A"
subsidiaryNameCell.setCellValue(subsidiaryName) subsidiaryNameCell.setCellValue(subsidiaryName)


// Column G: Project Plan Start Date // Column G: Project Plan Start Date
@@ -1748,136 +1792,139 @@ open class ReportService(
) )
return jdbcDao.queryForList(sql.toString(), args) return jdbcDao.queryForList(sql.toString(), args)
} }

open fun getProjectCompletionReport(args: Map<String, Any>): List<Map<String, Any>> { open fun getProjectCompletionReport(args: Map<String, Any>): List<Map<String, Any>> {
val sql = StringBuilder("select"
+ " result.code, "
+ " result.name, "
+ " result.teamCode, "
+ " result.custCode, "
+ " result.subCode, " )
val sql = StringBuilder(
"select"
+ " result.code, "
+ " result.name, "
+ " result.teamCode, "
+ " result.custCode, "
+ " result.subCode, "
)
if (args.get("outstanding") as Boolean) { if (args.get("outstanding") as Boolean) {
sql.append(" result.projectFee - COALESCE(i.paidAmount, 0) as `Receivable Remained`, ") sql.append(" result.projectFee - COALESCE(i.paidAmount, 0) as `Receivable Remained`, ")
// sql.append(" result.projectFee - (result.totalBudget - COALESCE(i.issueAmount , 0) + COALESCE(i.issueAmount, 0) - COALESCE(i.paidAmount, 0)) as `Receivable Remained`, ") // sql.append(" result.projectFee - (result.totalBudget - COALESCE(i.issueAmount , 0) + COALESCE(i.issueAmount, 0) - COALESCE(i.paidAmount, 0)) as `Receivable Remained`, ")
} }
sql.append( sql.append(
" DATE_FORMAT(result.actualEnd, '%d/%m/%Y') as actualEnd " " DATE_FORMAT(result.actualEnd, '%d/%m/%Y') as actualEnd "
+ " from ( "
+ " SELECT "
+ " pt.project_id, "
+ " min(p.code) as code, "
+ " min(p.name) as name, "
+ " min(t.code) as teamCode, "
+ " concat(min(c.code), ' - ', min(c.name)) as custCode, "
+ " COALESCE(concat(min(ss.code),' - ',min(ss.name)), 'N/A') as subCode, "
+ " min(p.actualEnd) as actualEnd, "
+ " min(p.expectedTotalFee) as projectFee, "
+ " sum(COALESCE(tns.totalConsumed*sal.hourlyRate, 0)) as totalBudget "
+ " FROM ( "
+ " SELECT "
+ " t.staffId, "
+ " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, "
+ " t.projectTaskId AS taskId "
+ " FROM timesheet t "
+ " LEFT JOIN staff s ON t.staffId = s.id "
+ " LEFT JOIN team te on s.teamId = te.id "
+ " GROUP BY t.staffId, t.projectTaskId "
+ " order by t.staffId "
+ " ) AS tns "
+ " right join project_task pt ON tns.taskId = pt.id "
+ " left join project p on p.id = pt.project_id "
+ " left JOIN staff s ON p.teamLead = s.id "
+ " left join salary sal on s.salaryId = sal.salaryPoint "
+ " left JOIN team t ON s.teamId = t.id "
+ " left join customer c on c.id = p.customerId "
+ " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id "
+ " where p.deleted = false ")
if (args.containsKey("teamId")) {
sql.append("t.id = :teamId")
}
sql.append(
+ " from ( "
+ " SELECT "
+ " pt.project_id, "
+ " min(p.code) as code, "
+ " min(p.name) as name, "
+ " min(t.code) as teamCode, "
+ " concat(min(c.code), ' - ', min(c.name)) as custCode, "
+ " COALESCE(concat(min(ss.code),' - ',min(ss.name)), 'N/A') as subCode, "
+ " min(p.actualEnd) as actualEnd, "
+ " min(p.expectedTotalFee) as projectFee, "
+ " sum(COALESCE(tns.totalConsumed*sal.hourlyRate, 0)) as totalBudget "
+ " FROM ( "
+ " SELECT "
+ " t.staffId, "
+ " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, "
+ " t.projectTaskId AS taskId "
+ " FROM timesheet t "
+ " LEFT JOIN staff s ON t.staffId = s.id "
+ " LEFT JOIN team te on s.teamId = te.id "
+ " GROUP BY t.staffId, t.projectTaskId "
+ " order by t.staffId "
+ " ) AS tns "
+ " right join project_task pt ON tns.taskId = pt.id "
+ " left join project p on p.id = pt.project_id "
+ " left JOIN staff s ON p.teamLead = s.id "
+ " left join salary sal on s.salaryId = sal.salaryPoint "
+ " left JOIN team t ON s.teamId = t.id "
+ " left join customer c on c.id = p.customerId "
+ " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id "
+ " where p.deleted = false "
)
if (args.containsKey("teamId")) {
sql.append("t.id = :teamId")
}
sql.append(
" and p.status = 'Completed' " " and p.status = 'Completed' "
+ " and p.actualEnd BETWEEN :startDate and :endDate "
+ " group by pt.project_id "
+ " ) as result "
+ " left join invoice i on result.code = i.projectCode "
+ " order by result.actualEnd ")
+ " and p.actualEnd BETWEEN :startDate and :endDate "
+ " group by pt.project_id "
+ " ) as result "
+ " left join invoice i on result.code = i.projectCode "
+ " order by result.actualEnd "
)


return jdbcDao.queryForList(sql.toString(), args) return jdbcDao.queryForList(sql.toString(), args)
} }


open fun getProjectResourceOverconsumptionReport(args: Map<String, Any>): List<Map<String, Any>> { open fun getProjectResourceOverconsumptionReport(args: Map<String, Any>): List<Map<String, Any>> {
val sql = StringBuilder("WITH teamNormalConsumed AS ("
+ " SELECT "
+ " s.teamId, "
+ " pt.project_id, "
+ " SUM(tns.totalConsumed) AS totalConsumed, "
+ " sum(tns.totalBudget) as totalBudget "
+ " FROM ( "
+ " SELECT "
+ " t.staffId, "
+ " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, "
+ " t.projectTaskId AS taskId, "
+ " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) * min(sal.hourlyRate) as totalBudget "
+ " FROM timesheet t "
+ " LEFT JOIN staff s ON t.staffId = s.id "
+ " left join salary sal on sal.salaryPoint = s.salaryId "
+ " GROUP BY t.staffId, t.projectTaskId "
+ " ) AS tns "
+ " INNER JOIN project_task pt ON tns.taskId = pt.id "
+ " JOIN staff s ON tns.staffId = s.id "
+ " JOIN team t ON s.teamId = t.id "
+ " GROUP BY teamId, project_id "
+ " ) "
+ " SELECT "
+ " p.code, "
+ " p.name, "
+ " t.code as team, "
+ " concat(c.code, ' - ', c.name) as client, "
+ " COALESCE(concat(ss.code, ' - ', ss.name), 'N/A') as subsidiary, "
+ " p.expectedTotalFee * 0.8 as plannedBudget, "
+ " COALESCE(tns.totalBudget, 0) as actualConsumedBudget, "
+ " COALESCE(p.totalManhour, 0) as plannedManhour, "
+ " COALESCE(tns.totalConsumed, 0) as actualConsumedManhour, "
+ " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) as budgetConsumptionRate, "
+ " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) as manhourConsumptionRate, "
+ " CASE "
+ " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 "
+ " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 "
+ " then 'Potential Overconsumption' "
+ " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 "
+ " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1 "
+ " then 'Overconsumption' "
+ " else 'Within Budget' "
+ " END as status "
+ " FROM project p "
+ " LEFT JOIN team t ON p.teamLead = t.teamLead "
+ " LEFT JOIN staff s ON p.teamLead = s.id "
+ " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint "
+ " LEFT JOIN customer c ON p.customerId = c.id "
+ " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id "
+ " left join teamNormalConsumed tns on tns.project_id = p.id "
+ " WHERE p.deleted = false "
+ " and p.status = 'On-going' "
val sql = StringBuilder(
"WITH teamNormalConsumed AS ("
+ " SELECT"
+ " tns.project_id,"
+ " SUM(tns.totalConsumed) AS totalConsumed, "
+ " sum(tns.totalBudget) as totalBudget "
+ " FROM ( "
+ " SELECT"
+ " t.staffId,"
+ " t.projectId AS project_id,"
+ " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, "
+ " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) * min(sal.hourlyRate) as totalBudget "
+ " FROM timesheet t"
+ " LEFT JOIN staff s ON t.staffId = s.id "
+ " left join salary sal on sal.salaryPoint = s.salaryId "
+ " GROUP BY t.staffId, t.projectId"
+ " ) AS tns"
+ " GROUP BY project_id"
+ " ) "
+ " SELECT "
+ " p.code, "
+ " p.name, "
+ " t.code as team, "
+ " concat(c.code, ' - ', c.name) as client, "
+ " COALESCE(concat(ss.code, ' - ', ss.name), 'N/A') as subsidiary, "
+ " p.expectedTotalFee * 0.8 as plannedBudget, "
+ " COALESCE(tns.totalBudget, 0) as actualConsumedBudget, "
+ " COALESCE(p.totalManhour, 0) as plannedManhour, "
+ " COALESCE(tns.totalConsumed, 0) as actualConsumedManhour, "
+ " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) as budgetConsumptionRate, "
+ " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) as manhourConsumptionRate, "
+ " CASE "
+ " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 "
+ " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1 "
+ " then 'Overconsumption' "
+ " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 "
+ " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 "
+ " then 'Potential Overconsumption' "
+ " else 'Within Budget' "
+ " END as status "
+ " FROM project p "
+ " LEFT JOIN team t ON p.teamLead = t.teamLead "
+ " LEFT JOIN staff s ON p.teamLead = s.id "
+ " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint "
+ " LEFT JOIN customer c ON p.customerId = c.id "
+ " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id "
+ " left join teamNormalConsumed tns on tns.project_id = p.id "
+ " WHERE p.deleted = false "
+ " and p.status = 'On-going' "
) )
if (args != null) { if (args != null) {
var statusFilter: String = "" var statusFilter: String = ""
if (args.containsKey("teamId")) if (args.containsKey("teamId"))
sql.append("and t.id = :teamId")
sql.append(" and t.id = :teamId")
if (args.containsKey("custId")) if (args.containsKey("custId"))
sql.append("and c.id = :custId")
sql.append(" and c.id = :custId")
if (args.containsKey("subsidiaryId")) if (args.containsKey("subsidiaryId"))
sql.append("and ss.id = :subsidiaryId")
sql.append(" and ss.id = :subsidiaryId")
if (args.containsKey("status")) if (args.containsKey("status"))
statusFilter = when (args.get("status")) { statusFilter = when (args.get("status")) {
"Potential Overconsumption" -> "and " +
"Potential Overconsumption" -> " and " +
" (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit " + " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit " +
" and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " + " and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " +
" or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit " + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit " +
" and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 " " and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 "
"All" -> "and " +

"All" -> " and " +
" (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit " + " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit " +
" or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit " " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit "
// "Overconsumption" -> "and " +
// "Overconsumption" -> " and " +
// " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " + // " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " +
// " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1) " // " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1) "


@@ -1900,6 +1947,15 @@ open class ReportService(
+ " left join salary s2 on s.salaryId = s2.salaryPoint" + " left join salary s2 on s.salaryId = s2.salaryPoint"
+ " left join team t2 on t2.id = s.teamId" + " left join team t2 on t2.id = s.teamId"
+ " )," + " ),"
+ " cte_timesheet_sum as ("
+ " Select p.code, sum((IFNULL(t.normalConsumed, 0) + IFNULL(t.otConsumed , 0)) * s2.hourlyRate) as sumManhourExpenditure"
+ " from timesheet t"
+ " left join project_task pt on pt.id = t.projectTaskId"
+ " left join project p ON p.id = pt.project_id"
+ " left join staff s on s.id = t.staffId"
+ " left join salary s2 on s.salaryId = s2.salaryPoint"
+ " group by p.code"
+ " ),"
+ " cte_invoice as (" + " cte_invoice as ("
+ " select p.code, sum(i.issueAmount) as sumIssuedAmount , sum(i.paidAmount) as sumPaidAmount" + " select p.code, sum(i.issueAmount) as sumIssuedAmount , sum(i.paidAmount) as sumPaidAmount"
+ " from invoice i" + " from invoice i"
@@ -1909,10 +1965,11 @@ open class ReportService(
+ " select p.code, p.description, c.name as client, IFNULL(s2.name, \"N/A\") as subsidiary, concat(t.code, \" - \", t.name) as teamLead," + " select p.code, p.description, c.name as client, IFNULL(s2.name, \"N/A\") as subsidiary, concat(t.code, \" - \", t.name) as teamLead,"
+ " IFNULL(cte_ts.normalConsumed, 0) as normalConsumed, IFNULL(cte_ts.otConsumed, 0) as otConsumed, DATE_FORMAT(cte_ts.recordDate, '%Y-%m') as recordDate, " + " IFNULL(cte_ts.normalConsumed, 0) as normalConsumed, IFNULL(cte_ts.otConsumed, 0) as otConsumed, DATE_FORMAT(cte_ts.recordDate, '%Y-%m') as recordDate, "
+ " IFNULL(cte_ts.salaryPoint, 0) as salaryPoint, " + " IFNULL(cte_ts.salaryPoint, 0) as salaryPoint, "
+ " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate, IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount,"
+ " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate, IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount, IFNULL(cte_tss.sumManhourExpenditure, 0) as sumManhourExpenditure,"
+ " s.name, s.staffId, g.code as gradeCode, g.name as gradeName, t2.code as teamCode, t2.name as teamName" + " s.name, s.staffId, g.code as gradeCode, g.name as gradeName, t2.code as teamCode, t2.name as teamName"
+ " from project p" + " from project p"
+ " left join cte_timesheet cte_ts on p.code = cte_ts.code" + " left join cte_timesheet cte_ts on p.code = cte_ts.code"
+ " left join cte_timesheet_sum cte_tss on p.code = cte_tss.code"
+ " left join customer c on c.id = p.customerId" + " left join customer c on c.id = p.customerId"
+ " left join tsmsdb.team t on t.teamLead = p.teamLead" + " left join tsmsdb.team t on t.teamLead = p.teamLead"
+ " left join cte_invoice cte_i on cte_i.code = p.code" + " left join cte_invoice cte_i on cte_i.code = p.code"
@@ -1975,6 +2032,9 @@ open class ReportService(
if (info["code"] == item["code"] && "subsidiary" !in info) { if (info["code"] == item["code"] && "subsidiary" !in info) {
info["subsidiary"] = item.getValue("subsidiary") info["subsidiary"] = item.getValue("subsidiary")
} }
if (info["manhourExpenditure"] != item.getValue("sumManhourExpenditure")) {
info["manhourExpenditure"] = item.getValue("sumManhourExpenditure")
}
if (info["description"] != item.getValue("description")) { if (info["description"] != item.getValue("description")) {
info["description"] = item.getValue("description") info["description"] = item.getValue("description")
} }
@@ -2162,9 +2222,9 @@ open class ReportService(
rowNum = 6 rowNum = 6
val row6: Row = sheet.getRow(rowNum) val row6: Row = sheet.getRow(rowNum)
val row6Cell = row6.getCell(1) val row6Cell = row6.getCell(1)
val clientSubsidiary = if(info.getValue("subsidiary").toString() != "N/A"){
val clientSubsidiary = if (info.getValue("subsidiary").toString() != "N/A") {
info.getValue("subsidiary").toString() info.getValue("subsidiary").toString()
}else{
} else {
info.getValue("client").toString() info.getValue("client").toString()
} }
row6Cell.setCellValue(clientSubsidiary) row6Cell.setCellValue(clientSubsidiary)
@@ -2335,7 +2395,8 @@ open class ReportService(
} }
val totalManhourECell = totalManhourERow.getCell(1) ?: totalManhourERow.createCell(1) val totalManhourECell = totalManhourERow.getCell(1) ?: totalManhourERow.createCell(1)
totalManhourECell.apply { totalManhourECell.apply {
cellFormula = "SUM(${lastColumnIndex}${startRow}:${lastColumnIndex}${startRow + staffInfoList.size})"
// cellFormula = "SUM(${lastColumnIndex}${startRow}:${lastColumnIndex}${startRow + staffInfoList.size})"
setCellValue(info.getValue("manhourExpenditure") as Double)
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }
CellUtil.setCellStyleProperty(totalManhourETitleCell, "borderBottom", BorderStyle.THIN) CellUtil.setCellStyleProperty(totalManhourETitleCell, "borderBottom", BorderStyle.THIN)
@@ -2358,10 +2419,10 @@ open class ReportService(
return workbook return workbook
} }


fun getCostAndExpense(clientId: Long?, teamId: Long?, type: String): List<Map<String,Any?>>{
fun getCostAndExpense(clientId: Long?, teamId: Long?, type: String): List<Map<String, Any?>> {
val sql = StringBuilder( val sql = StringBuilder(
" with cte_timesheet as ( " " with cte_timesheet as ( "
+ " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.salaryPoint, s2.hourlyRate, t.staffId,"
+ " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.salaryPoint, s2.hourlyRate, t.staffId,"
+ " t.recordDate" + " t.recordDate"
+ " from timesheet t" + " from timesheet t"
+ " left join project_task pt on pt.id = t.projectTaskId" + " left join project_task pt on pt.id = t.projectTaskId"
@@ -2385,20 +2446,20 @@ open class ReportService(
+ " left join team t2 on t2.id = s.teamId" + " left join team t2 on t2.id = s.teamId"
+ " where ISNULL(p.code) = False" + " where ISNULL(p.code) = False"
) )
if(clientId != null){
if(type == "client"){
if (clientId != null) {
if (type == "client") {
sql.append( sql.append(
" and c.id = :clientId " " and c.id = :clientId "
) )
} }
if(type == "subsidiary"){
if (type == "subsidiary") {
sql.append( sql.append(
" and s2.id = :clientId " " and s2.id = :clientId "
) )
} }
} }


if(teamId != null){
if (teamId != null) {
sql.append( sql.append(
" and p.teamLead = :teamId " " and p.teamLead = :teamId "
) )
@@ -2417,9 +2478,9 @@ open class ReportService(
val queryList = jdbcDao.queryForList(sql.toString(), args) val queryList = jdbcDao.queryForList(sql.toString(), args)
val costAndExpenseList = mutableListOf<Map<String, Any?>>() val costAndExpenseList = mutableListOf<Map<String, Any?>>()


for(item in queryList){
for (item in queryList) {
val hourlyRate = (item.getValue("hourlyRate") as BigDecimal).toDouble() val hourlyRate = (item.getValue("hourlyRate") as BigDecimal).toDouble()
if(item["code"] !in costAndExpenseList){
if (item["code"] !in costAndExpenseList.map { it["code"] }) {
costAndExpenseList.add( costAndExpenseList.add(
mapOf( mapOf(
"code" to item["code"], "code" to item["code"],
@@ -2428,22 +2489,27 @@ open class ReportService(
"subsidiary" to item["subsidiary"], "subsidiary" to item["subsidiary"],
"teamLead" to item["teamLead"], "teamLead" to item["teamLead"],
"budget" to item["expectedTotalFee"], "budget" to item["expectedTotalFee"],
"totalManhours" to item["normalConsumed"] as Double + item["otConsumed"] as Double,
"manhourExpenditure" to (hourlyRate * item["normalConsumed"] as Double )
+ (hourlyRate * item["otConsumed"]as Double * otFactor)
"totalManhours" to item["normalConsumed"] as Double + item["otConsumed"] as Double,
"manhourExpenditure" to (hourlyRate * item["normalConsumed"] as Double)
+ (hourlyRate * item["otConsumed"] as Double * otFactor)
) )
) )
}else{
} else {
val existingMap = costAndExpenseList.find { it.containsValue(item["code"]) }!! val existingMap = costAndExpenseList.find { it.containsValue(item["code"]) }!!
costAndExpenseList[costAndExpenseList.indexOf(existingMap)] = existingMap.toMutableMap().apply { costAndExpenseList[costAndExpenseList.indexOf(existingMap)] = existingMap.toMutableMap().apply {
put("totalManhours", get("manhours") as Double + (item["normalConsumed"] as Double + item["otConsumed"] as Double))
put("manhourExpenditure", get("manhourExpenditure") as Double + ((hourlyRate * item["normalConsumed"] as Double )
+ (hourlyRate * item["otConsumed"]as Double * otFactor)))
put(
"totalManhours",
get("totalManhours") as Double + (item["normalConsumed"] as Double + item["otConsumed"] as Double)
)
put(
"manhourExpenditure",
get("manhourExpenditure") as Double + ((hourlyRate * item["normalConsumed"] as Double)
+ (hourlyRate * item["otConsumed"] as Double * otFactor))
)
} }
} }
} }
val result = costAndExpenseList.map {
item ->
val result = costAndExpenseList.map { item ->
val budget = (item["budget"] as? Double)?.times(0.8) ?: 0.0 val budget = (item["budget"] as? Double)?.times(0.8) ?: 0.0
val budgetRemain = budget - (item["manhourExpenditure"] as? Double ?: 0.0) val budgetRemain = budget - (item["manhourExpenditure"] as? Double ?: 0.0)
val remainingPercent = (budgetRemain / budget) val remainingPercent = (budgetRemain / budget)
@@ -2462,7 +2528,7 @@ open class ReportService(
clientId: Long?, clientId: Long?,
budgetPercentage: Double?, budgetPercentage: Double?,
type: String type: String
): Workbook{
): Workbook {
val resource = ClassPathResource(templatePath) val resource = ClassPathResource(templatePath)
val templateInputStream = resource.inputStream val templateInputStream = resource.inputStream
val workbook: Workbook = XSSFWorkbook(templateInputStream) val workbook: Workbook = XSSFWorkbook(templateInputStream)
@@ -2480,9 +2546,9 @@ open class ReportService(
rowNum = 2 rowNum = 2
val row2: Row = sheet.getRow(rowNum) val row2: Row = sheet.getRow(rowNum)
val row2Cell = row2.getCell(2) val row2Cell = row2.getCell(2)
if(teamId == null){
if (teamId == null) {
row2Cell.setCellValue("All") row2Cell.setCellValue("All")
}else{
} else {
val sql = StringBuilder( val sql = StringBuilder(
" select t.id, t.code, t.name, concat(t.code, \" - \" ,t.name) as teamLead from team t where t.id = :teamId " " select t.id, t.code, t.name, concat(t.code, \" - \" ,t.name) as teamLead from team t where t.id = :teamId "
) )
@@ -2493,16 +2559,16 @@ open class ReportService(
rowNum = 3 rowNum = 3
val row3: Row = sheet.getRow(rowNum) val row3: Row = sheet.getRow(rowNum)
val row3Cell = row3.getCell(2) val row3Cell = row3.getCell(2)
if(clientId == null){
if (clientId == null) {
row3Cell.setCellValue("All") row3Cell.setCellValue("All")
}else{
val sql= StringBuilder()
if(type == "client"){
} else {
val sql = StringBuilder()
if (type == "client") {
sql.append( sql.append(
" select c.id, c.name from customer c where c.id = :clientId " " select c.id, c.name from customer c where c.id = :clientId "
) )
} }
if(type == "subsidiary"){
if (type == "subsidiary") {
sql.append( sql.append(
" select s.id, s.name from subsidiary s where s.id = :clientId " " select s.id, s.name from subsidiary s where s.id = :clientId "
) )
@@ -2513,15 +2579,15 @@ open class ReportService(




val filterList: List<Map<String, Any?>> val filterList: List<Map<String, Any?>>
if(budgetPercentage != null){
if (budgetPercentage != null) {
filterList = costAndExpenseList.filter { ((it["budgetPercentage"] as? Double) ?: 0.0) <= budgetPercentage } filterList = costAndExpenseList.filter { ((it["budgetPercentage"] as? Double) ?: 0.0) <= budgetPercentage }
}else{
} else {
filterList = costAndExpenseList filterList = costAndExpenseList
} }




rowNum = 6 rowNum = 6
for(item in filterList){
for (item in filterList) {
val index = filterList.indexOf(item) val index = filterList.indexOf(item)
val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum)
val cell = row.getCell(0) ?: row.createCell(0) val cell = row.getCell(0) ?: row.createCell(0)
@@ -2571,13 +2637,13 @@ open class ReportService(


val cell8 = row.getCell(8) ?: row.createCell(8) val cell8 = row.getCell(8) ?: row.createCell(8)
cell8.apply { cell8.apply {
cellFormula = "G${rowNum+1}-H${rowNum+1}"
cellFormula = "G${rowNum + 1}-H${rowNum + 1}"
} }
CellUtil.setCellStyleProperty(cell8, "dataFormat", accountingStyle) CellUtil.setCellStyleProperty(cell8, "dataFormat", accountingStyle)


val cell9 = row.getCell(9) ?: row.createCell(9) val cell9 = row.getCell(9) ?: row.createCell(9)
cell9.apply { cell9.apply {
cellFormula = "I${rowNum+1}/G${rowNum+1}"
cellFormula = "I${rowNum + 1}/G${rowNum + 1}"
} }
CellUtil.setCellStyleProperty(cell9, "dataFormat", percentStyle) CellUtil.setCellStyleProperty(cell9, "dataFormat", percentStyle)


@@ -2587,11 +2653,18 @@ open class ReportService(
return workbook return workbook
} }


fun genCostAndExpenseReport(request: costAndExpenseRequest): ByteArray{
fun genCostAndExpenseReport(request: costAndExpenseRequest): ByteArray {


val costAndExpenseList = getCostAndExpense(request.clientId, request.teamId, request.type) val costAndExpenseList = getCostAndExpense(request.clientId, request.teamId, request.type)


val workbook: Workbook = createCostAndExpenseWorkbook(COSTANDEXPENSE_REPORT, costAndExpenseList, request.teamId, request.clientId, request.budgetPercentage, request.type)
val workbook: Workbook = createCostAndExpenseWorkbook(
COSTANDEXPENSE_REPORT,
costAndExpenseList,
request.teamId,
request.clientId,
request.budgetPercentage,
request.type
)


val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() val outputStream: ByteArrayOutputStream = ByteArrayOutputStream()
workbook.write(outputStream) workbook.write(outputStream)
@@ -2599,8 +2672,16 @@ open class ReportService(


return outputStream.toByteArray() return outputStream.toByteArray()
} }
open fun getLateStartDetails(teamId: Long?, clientId: Long?, remainedDate: LocalDate, remainedDateTo: LocalDate, type: String?): List<Map<String, Any>> {
val sql = StringBuilder("""

open fun getLateStartDetails(
teamId: Long?,
clientId: Long?,
remainedDate: LocalDate,
remainedDateTo: LocalDate,
type: String?
): List<Map<String, Any>> {
val sql = StringBuilder(
"""
SELECT SELECT
p.code AS project_code, p.code AS project_code,
p.name AS project_name, p.name AS project_name,
@@ -2621,7 +2702,8 @@ open class ReportService(
p.status = 'Pending to Start' p.status = 'Pending to Start'
AND p.planStart < CURRENT_DATE AND p.planStart < CURRENT_DATE
AND m.endDate BETWEEN :remainedDate AND :remainedDateTo AND m.endDate BETWEEN :remainedDate AND :remainedDateTo
""".trimIndent())
""".trimIndent()
)


if (teamId != null && teamId > 0) { if (teamId != null && teamId > 0) {
sql.append(" AND t.id = :teamId") sql.append(" AND t.id = :teamId")
@@ -2645,4 +2727,240 @@ open class ReportService(
) )
return jdbcDao.queryForList(sql.toString(), args) return jdbcDao.queryForList(sql.toString(), args)
} }
}

@Throws(IOException::class)
private fun createCrossTeamChargeReport(
month: String,
timesheets: List<Timesheet>,
teams: List<Team>,
grades: List<Grade>,
templatePath: String,
): Workbook {
// please create a new function for each report template
val resource = ClassPathResource(templatePath)
val templateInputStream = resource.inputStream
val workbook: Workbook = XSSFWorkbook(templateInputStream)

val sheet: Sheet = workbook.getSheetAt(0)

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

// bold font with border style
val boldFont = workbook.createFont().apply {
bold = true
fontName = "Times New Roman"
}

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

// normal font
val normalFont = workbook.createFont().apply {
fontName = "Times New Roman"
}

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

var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field
var 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).createCell(columnIndex).apply {
setCellValue(convertReportMonth)
}

// if (timesheets.isNotEmpty()) {
val combinedTeamCodeColNumber = grades.size
val sortedGrades = grades.sortedBy { it.id }
val sortedTeams = teams.sortedBy { it.id }

val groupedTimesheets = timesheets
.filter { it.project?.teamLead?.team?.id != it.staff?.team?.id }
.groupBy { timesheetEntry ->
Triple(
timesheetEntry.project?.teamLead?.team?.id,
timesheetEntry.staff?.team?.id,
timesheetEntry.staff?.grade?.id
)
}
.mapValues { (_, timesheetEntries) ->
timesheetEntries.map { timesheet ->
if (timesheet.normalConsumed != null) {
mutableMapOf<String, Double>().apply {
this["manHour"] = timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0)
this["salary"] = timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0)
.times(timesheet.staff!!.salary.hourlyRate.toDouble())
}
} else if (timesheet.otConsumed != null) {
mutableMapOf<String, Double>().apply {
this["manHour"] = timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0)
this["salary"] = timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0)
.times(timesheet.staff!!.salary.hourlyRate.toDouble())
}
} else {
mutableMapOf<String, Double>().apply {
this["manHour"] = 0.0
this["salary"] = 0.0
}
}
}
}

if (sortedTeams.isNotEmpty() && sortedTeams.size > 1) {
rowIndex = 3
sortedTeams.forEach { team: Team ->

// Team
sheet.createRow(rowIndex++).apply {
createCell(0).apply {
setCellValue("Team to be charged:")
cellStyle = boldFontWithBorderStyle
CellUtil.setAlignment(this, HorizontalAlignment.LEFT)
}

val rangeAddress = CellRangeAddress(this.rowNum, this.rowNum, 1, 2 + combinedTeamCodeColNumber)
sheet.addMergedRegion(rangeAddress)
RegionUtil.setBorderTop(BorderStyle.THIN, rangeAddress, sheet)
RegionUtil.setBorderLeft(BorderStyle.THIN, rangeAddress, sheet)
RegionUtil.setBorderRight(BorderStyle.THIN, rangeAddress, sheet)
RegionUtil.setBorderBottom(BorderStyle.THIN, rangeAddress, sheet)

createCell(1).apply {
setCellValue(team.code)
cellStyle = normalFontWithBorderStyle
CellUtil.setAlignment(this, HorizontalAlignment.CENTER)
}

}

// Grades
sheet.createRow(rowIndex++).apply {
columnIndex = 0
createCell(columnIndex++).apply {
setCellValue("")
cellStyle = boldFontWithBorderStyle
}


sortedGrades.forEach { grade: Grade ->
createCell(columnIndex++).apply {
setCellValue(grade.name)
cellStyle = boldFontWithBorderStyle
CellUtil.setAlignment(this, HorizontalAlignment.CENTER)
}
}

createCell(columnIndex++).apply {
setCellValue("Total Manhour by Team")
cellStyle = boldFontWithBorderStyle
}

createCell(columnIndex).apply {
setCellValue("Total Cost Adjusted by Salary Point by Team")
cellStyle = boldFontWithBorderStyle
}
}

// Team + Manhour
val startRow = rowIndex
var endRow = rowIndex
sortedTeams.forEach { chargedTeam: Team ->
if (team.id != chargedTeam.id) {
endRow++
sheet.createRow(rowIndex++).apply {
columnIndex = 0
createCell(columnIndex++).apply {
setCellValue(chargedTeam.code)
cellStyle = normalFontWithBorderStyle
CellUtil.setAlignment(this, HorizontalAlignment.CENTER)
}

var totalSalary = 0.0
sortedGrades.forEach { grade: Grade ->
createCell(columnIndex++).apply {
setCellValue(
groupedTimesheets[Triple(
team.id,
chargedTeam.id,
grade.id
)]?.sumOf { it.getValue("manHour") } ?: 0.0)

totalSalary += groupedTimesheets[Triple(
team.id,
chargedTeam.id,
grade.id
)]?.sumOf { it.getValue("salary") } ?: 0.0

cellStyle = normalFontWithBorderStyle.apply {
dataFormat = accountingStyle
}
}
}

createCell(columnIndex++).apply {
val lastCellLetter = CellReference.convertNumToColString(this.columnIndex - 1)
cellFormula = "sum(B${this.rowIndex + 1}:${lastCellLetter}${this.rowIndex + 1})"
cellStyle = boldFontWithBorderStyle.apply {
dataFormat = accountingStyle
}
}

createCell(columnIndex).apply {
setCellValue(totalSalary)
cellStyle = boldFontWithBorderStyle.apply {
dataFormat = accountingStyle
}
}
}
}
}

// Total Manhour by grade
sheet.createRow(rowIndex).apply {
columnIndex = 0
createCell(columnIndex++).apply {
setCellValue("Total Manhour by Grade")
cellStyle = boldFontWithBorderStyle
}

sortedGrades.forEach { grade: Grade ->
createCell(columnIndex++).apply {
val currentCellLetter = CellReference.convertNumToColString(this.columnIndex)
cellFormula = "sum(${currentCellLetter}${startRow}:${currentCellLetter}${endRow})"
cellStyle = normalFontWithBorderStyle.apply {
dataFormat = accountingStyle
}
}
}

createCell(columnIndex++).apply {
setCellValue("")
cellStyle = boldFontWithBorderStyle
}

createCell(columnIndex).apply {
setCellValue("")
cellStyle = boldFontWithBorderStyle
}
}
rowIndex += 2
}
}
// }

return workbook
}
}

+ 40
- 2
src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt Visa fil

@@ -34,8 +34,12 @@ 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 org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory
import org.springframework.data.domain.Example import org.springframework.data.domain.Example
import org.springframework.data.domain.ExampleMatcher import org.springframework.data.domain.ExampleMatcher
import java.time.YearMonth
import java.util.*


@RestController @RestController
@RequestMapping("/reports") @RequestMapping("/reports")
@@ -55,9 +59,11 @@ class ReportController(
private val customerService: CustomerService, private val customerService: CustomerService,
private val subsidiaryService: SubsidiaryService, private val subsidiaryService: SubsidiaryService,
private val invoiceService: InvoiceService, private val gradeAllocationRepository: GradeAllocationRepository, private val invoiceService: InvoiceService, private val gradeAllocationRepository: GradeAllocationRepository,
private val subsidiaryRepository: SubsidiaryRepository
private val subsidiaryRepository: SubsidiaryRepository, private val staffAllocationRepository: StaffAllocationRepository,
private val gradeRepository: GradeRepository
) { ) {


private val logger: Log = LogFactory.getLog(javaClass)
@PostMapping("/fetchProjectsFinancialStatusReport") @PostMapping("/fetchProjectsFinancialStatusReport")
@Throws(ServletRequestBindingException::class, IOException::class) @Throws(ServletRequestBindingException::class, IOException::class)
fun getFinancialStatusReport(@RequestBody @Valid request: FinancialStatusReportRequest): ResponseEntity<Resource> { fun getFinancialStatusReport(@RequestBody @Valid request: FinancialStatusReportRequest): ResponseEntity<Resource> {
@@ -76,7 +82,8 @@ class ReportController(


val project = projectRepository.findById(request.projectId).orElseThrow() val project = projectRepository.findById(request.projectId).orElseThrow()
val projectTasks = projectTaskRepository.findAllByProject(project) val projectTasks = projectTaskRepository.findAllByProject(project)
val invoices = invoiceService.findAllByProjectAndPaidAmountIsNotNull(project)
// val invoices = invoiceService.findAllByProjectAndPaidAmountIsNotNull(project)
val invoices = invoiceRepository.findAllByProjectCodeAndPaidAmountIsNotNull(project.code!!)
val timesheets = timesheetRepository.findAllByProjectTaskIn(projectTasks) val timesheets = timesheetRepository.findAllByProjectTaskIn(projectTasks)


val reportResult: ByteArray = excelReportService.generateProjectCashFlowReport(project, invoices, timesheets, request.dateType) val reportResult: ByteArray = excelReportService.generateProjectCashFlowReport(project, invoices, timesheets, request.dateType)
@@ -300,4 +307,35 @@ class ReportController(
.body(ByteArrayResource(reportResult)) .body(ByteArrayResource(reportResult))
} }


@PostMapping("/CrossTeamChargeReport")
@Throws(ServletRequestBindingException::class, IOException::class)
fun getCrossTeamChargeReport(@RequestBody @Valid request: CrossTeamChargeReportRequest): ResponseEntity<Resource> {

val authorities = staffsService.currentAuthorities() ?: return ResponseEntity.noContent().build()

if (authorities.stream().anyMatch { it.authority.equals("GENERATE_REPORTS") }) {
val crossProjects = staffAllocationRepository.findAll()
.filter { it.project?.teamLead?.team?.id != it.staff?.team?.id }
.map { it.project!! }
.distinct()

val reportMonth = YearMonth.parse(request.month, DateTimeFormatter.ofPattern("yyyy-MM"))
val convertReportMonth = YearMonth.of(reportMonth.year, reportMonth.month)
val startDate = convertReportMonth.atDay(1)
val endDate = convertReportMonth.atEndOfMonth()
val timesheets = timesheetRepository.findAllByProjectIn(crossProjects)
.filter { it.recordDate != null &&
(it.recordDate!!.isEqual(startDate) || it.recordDate!!.isEqual(endDate) || (it.recordDate!!.isAfter(startDate) && it.recordDate!!.isBefore(endDate)))}
val teams = teamRepository.findAll().filter { it.deleted == false }
val grades = gradeRepository.findAll().filter { it.deleted == false }

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

} else {
return ResponseEntity.noContent().build()
}
}
} }

+ 4
- 0
src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt Visa fil

@@ -57,4 +57,8 @@ data class ProjectCompletionReport (
val startDate: LocalDate, val startDate: LocalDate,
val endDate: LocalDate, val endDate: LocalDate,
val outstanding: Boolean val outstanding: Boolean
)

data class CrossTeamChargeReportRequest (
val month: String,
) )

+ 2
- 0
src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt Visa fil

@@ -14,6 +14,8 @@ interface TimesheetRepository : AbstractRepository<Timesheet, Long> {


fun findAllByProjectTaskIn(projectTasks: List<ProjectTask>): List<Timesheet> fun findAllByProjectTaskIn(projectTasks: List<ProjectTask>): List<Timesheet>


fun findAllByProjectIn(project: List<Project>): List<Timesheet>

fun deleteAllByStaffAndRecordDate(staff: Staff, recordDate: LocalDate) fun deleteAllByStaffAndRecordDate(staff: Staff, recordDate: LocalDate)


@Query("SELECT new com.ffii.tsms.modules.timesheet.entity.projections.TimesheetHours(IFNULL(SUM(normalConsumed), 0), IFNULL(SUM(otConsumed), 0)) FROM Timesheet t JOIN ProjectTask pt on t.projectTask = pt WHERE pt.project = ?1") @Query("SELECT new com.ffii.tsms.modules.timesheet.entity.projections.TimesheetHours(IFNULL(SUM(normalConsumed), 0), IFNULL(SUM(otConsumed), 0)) FROM Timesheet t JOIN ProjectTask pt on t.projectTask = pt WHERE pt.project = ?1")


+ 125
- 24
src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt Visa fil

@@ -1,17 +1,22 @@
package com.ffii.tsms.modules.timesheet.service package com.ffii.tsms.modules.timesheet.service


import com.ffii.core.exception.BadRequestException import com.ffii.core.exception.BadRequestException
import com.ffii.core.utils.ExcelUtils
import com.ffii.tsms.modules.data.entity.BuildingType
import com.ffii.tsms.modules.data.entity.Staff import com.ffii.tsms.modules.data.entity.Staff
import com.ffii.tsms.modules.data.entity.StaffRepository import com.ffii.tsms.modules.data.entity.StaffRepository
import com.ffii.tsms.modules.data.entity.WorkNature
import com.ffii.tsms.modules.data.service.StaffsService import com.ffii.tsms.modules.data.service.StaffsService
import com.ffii.tsms.modules.data.service.TeamService import com.ffii.tsms.modules.data.service.TeamService
import com.ffii.tsms.modules.project.entity.ProjectRepository
import com.ffii.tsms.modules.project.entity.ProjectTaskRepository
import com.ffii.tsms.modules.project.entity.TaskRepository
import com.ffii.tsms.modules.project.entity.*
import com.ffii.tsms.modules.project.web.models.*
import com.ffii.tsms.modules.timesheet.entity.Timesheet import com.ffii.tsms.modules.timesheet.entity.Timesheet
import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository
import com.ffii.tsms.modules.timesheet.web.models.TeamMemberTimeEntries import com.ffii.tsms.modules.timesheet.web.models.TeamMemberTimeEntries
import com.ffii.tsms.modules.timesheet.web.models.TimeEntry import com.ffii.tsms.modules.timesheet.web.models.TimeEntry
import org.apache.commons.logging.LogFactory
import org.apache.poi.ss.usermodel.Sheet
import org.apache.poi.ss.usermodel.Workbook
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import java.time.LocalDate import java.time.LocalDate
@@ -40,14 +45,15 @@ open class TimesheetsService(
mergeTimeEntriesByProjectAndTask(timeEntries).map { timeEntry -> mergeTimeEntriesByProjectAndTask(timeEntries).map { timeEntry ->
val task = timeEntry.taskId?.let { taskRepository.findById(it).getOrNull() } val task = timeEntry.taskId?.let { taskRepository.findById(it).getOrNull() }
val project = timeEntry.projectId?.let { projectRepository.findById(it).getOrNull() } val project = timeEntry.projectId?.let { projectRepository.findById(it).getOrNull() }
val projectTask = project?.let { p -> task?.let { t -> projectTaskRepository.findByProjectAndTask(p, t) } }
val projectTask =
project?.let { p -> task?.let { t -> projectTaskRepository.findByProjectAndTask(p, t) } }


Timesheet().apply { Timesheet().apply {
this.staff = currentStaff this.staff = currentStaff
this.recordDate = entryDate this.recordDate = entryDate
this.normalConsumed = timeEntry.inputHours this.normalConsumed = timeEntry.inputHours
this.otConsumed = timeEntry.otHours this.otConsumed = timeEntry.otHours
this.projectTask = projectTask
this.projectTask = projectTask
this.project = project this.project = project
this.remark = timeEntry.remark this.remark = timeEntry.remark
} }
@@ -60,7 +66,11 @@ open class TimesheetsService(
} }


@Transactional @Transactional
open fun saveMemberTimeEntry(staffId: Long, entry: TimeEntry, recordDate: LocalDate?): Map<String, List<TimeEntry>> {
open fun saveMemberTimeEntry(
staffId: Long,
entry: TimeEntry,
recordDate: LocalDate?
): Map<String, List<TimeEntry>> {
val authorities = staffsService.currentAuthorities() ?: throw BadRequestException() val authorities = staffsService.currentAuthorities() ?: throw BadRequestException()


if (!authorities.stream().anyMatch { it.authority.equals("MAINTAIN_TIMESHEET") }) { if (!authorities.stream().anyMatch { it.authority.equals("MAINTAIN_TIMESHEET") }) {
@@ -75,11 +85,11 @@ open class TimesheetsService(
val timesheet = timesheetRepository.findById(entry.id).getOrDefault(Timesheet()).apply { val timesheet = timesheetRepository.findById(entry.id).getOrDefault(Timesheet()).apply {
val task = entry.taskId?.let { taskRepository.findById(it).getOrNull() } val task = entry.taskId?.let { taskRepository.findById(it).getOrNull() }
val project = entry.projectId?.let { projectRepository.findById(it).getOrNull() } val project = entry.projectId?.let { projectRepository.findById(it).getOrNull() }
val projectTask = project?.let { p -> task?.let { t -> projectTaskRepository.findByProjectAndTask(p, t) } }
val projectTask = project?.let { p -> task?.let { t -> projectTaskRepository.findByProjectAndTask(p, t) } }


this.normalConsumed = entry.inputHours this.normalConsumed = entry.inputHours
this.otConsumed = entry.otHours this.otConsumed = entry.otHours
this.projectTask = projectTask
this.projectTask = projectTask
this.project = project this.project = project
this.remark = entry.remark this.remark = entry.remark
this.recordDate = this.recordDate ?: recordDate this.recordDate = this.recordDate ?: recordDate
@@ -109,7 +119,7 @@ open class TimesheetsService(


open fun getTeamMemberTimesheet(): Map<Long, TeamMemberTimeEntries> { open fun getTeamMemberTimesheet(): Map<Long, TeamMemberTimeEntries> {
val authorities = staffsService.currentAuthorities() ?: return emptyMap() val authorities = staffsService.currentAuthorities() ?: return emptyMap()
if (authorities.stream().anyMatch { it.authority.equals("MAINTAIN_TIMESHEET")}) {
if (authorities.stream().anyMatch { it.authority.equals("MAINTAIN_TIMESHEET") }) {
val currentStaff = staffsService.currentStaff() val currentStaff = staffsService.currentStaff()
// Get team where current staff is team lead // Get team where current staff is team lead


@@ -136,27 +146,118 @@ open class TimesheetsService(
private fun transformToTimeEntryMap(timesheets: List<Timesheet>): Map<String, List<TimeEntry>> { private fun transformToTimeEntryMap(timesheets: List<Timesheet>): Map<String, List<TimeEntry>> {
return timesheets return timesheets
.groupBy { timesheet -> timesheet.recordDate!!.format(DateTimeFormatter.ISO_LOCAL_DATE) } .groupBy { timesheet -> timesheet.recordDate!!.format(DateTimeFormatter.ISO_LOCAL_DATE) }
.mapValues { (_, timesheets) -> timesheets.map { timesheet ->
TimeEntry(
id = timesheet.id!!,
projectId = timesheet.projectTask?.project?.id ?: timesheet.project?.id,
taskId = timesheet.projectTask?.task?.id,
taskGroupId = timesheet.projectTask?.task?.taskGroup?.id,
inputHours = timesheet.normalConsumed ?: 0.0,
otHours = timesheet.otConsumed ?: 0.0,
remark = timesheet.remark
)
} }
.mapValues { (_, timesheets) ->
timesheets.map { timesheet ->
TimeEntry(
id = timesheet.id!!,
projectId = timesheet.projectTask?.project?.id ?: timesheet.project?.id,
taskId = timesheet.projectTask?.task?.id,
taskGroupId = timesheet.projectTask?.task?.taskGroup?.id,
inputHours = timesheet.normalConsumed ?: 0.0,
otHours = timesheet.otConsumed ?: 0.0,
remark = timesheet.remark
)
}
}
} }


private fun mergeTimeEntriesByProjectAndTask(entries: List<TimeEntry>): List<TimeEntry> { private fun mergeTimeEntriesByProjectAndTask(entries: List<TimeEntry>): List<TimeEntry> {
return entries return entries
.groupBy { timeEntry -> Pair(timeEntry.projectId, timeEntry.taskId) } .groupBy { timeEntry -> Pair(timeEntry.projectId, timeEntry.taskId) }
.values.map { timeEntries -> .values.map { timeEntries ->
timeEntries.reduce { acc, timeEntry -> acc.copy(
inputHours = (acc.inputHours ?: 0.0) + (timeEntry.inputHours ?: 0.0),
otHours = (acc.otHours ?: 0.0) + (timeEntry.otHours ?: 0.0)
) }
timeEntries.reduce { acc, timeEntry ->
acc.copy(
inputHours = (acc.inputHours ?: 0.0) + (timeEntry.inputHours ?: 0.0),
otHours = (acc.otHours ?: 0.0) + (timeEntry.otHours ?: 0.0)
)
}
}
}

@Transactional(rollbackFor = [Exception::class])
open fun importFile(workbook: Workbook?): String {
val logger = LogFactory.getLog(javaClass)

if (workbook == null) {
return "No Excel import" // if workbook is null
}

val notExistProjectList = mutableListOf<String>()
val sheet: Sheet = workbook.getSheetAt(0)

logger.info("---------Start Import Timesheets-------")
val timesheetList = mutableListOf<Timesheet>().toMutableList();
for (i in 1..<sheet.lastRowNum) {
val row = sheet.getRow(i)

if (row?.getCell(0) != null && !row.getCell(0).stringCellValue.isNullOrBlank() && row.getCell(3) != null && !row.getCell(
3
).stringCellValue.isNullOrBlank()
) {
logger.info("row :$i | lastCellNum" + row.lastCellNum)

// process staff
logger.info("---------staff-------")
var staff = staffRepository.findByStaffId(row.getCell(0).stringCellValue).getOrNull()
if (staff == null) {
staff = staffRepository.findByStaffId("B000").orElseThrow()
}

// process project
logger.info("---------project-------")
var projectCode = StringBuilder(row.getCell(3).stringCellValue).insert(1, '-').toString()

if (row.getCell(4) != null && row.getCell(4).toString().isNotBlank()) {
val subCode = row.getCell(4).numericCellValue
val splitMainProjectCode = projectCode.split('-')
projectCode = splitMainProjectCode[0] + '-' + String.format(
"%04d",
splitMainProjectCode[1].toInt()
) + '-' + String.format("%03d", subCode.toInt())
} else {
val splitProjectCode = projectCode.split('-')
projectCode = splitProjectCode[0] + '-' + String.format("%04d", splitProjectCode[1].toInt())
}
logger.info("Project Code: $projectCode")

val project = projectRepository.findByCode(projectCode)

// process project task
logger.info("---------project task-------")
val task = taskRepository.findById(41).getOrNull()
val projectTask =
project?.let { p -> task?.let { t -> projectTaskRepository.findByProjectAndTask(p, t) } }

// process record date
logger.info("---------record date-------")
val formatter = DateTimeFormatter.ofPattern("dd-MMM-yyyy")
val recordDate = LocalDate.parse(row.getCell(6).toString(), formatter);

// normal hour + ot hour
logger.info("---------normal hour + ot hour-------")
val hours: Double = row.getCell(7).numericCellValue
val normalHours = if (hours > 8.0) 8.0 else hours
val otHours = if (hours > 8.0) hours - 8.0 else 0.0

if (project != null) {
timesheetList += Timesheet().apply {
this.staff = staff
this.recordDate = recordDate
this.normalConsumed = normalHours
this.otConsumed = otHours
this.projectTask = projectTask
this.project = project
}
} else {
notExistProjectList += projectCode
}
} }
}

timesheetRepository.saveAll(timesheetList)
logger.info("---------end-------")
logger.info("Not Exist Project List: "+ notExistProjectList.distinct().joinToString(", "))

return if (sheet.lastRowNum > 0) "Import Excel success btw " + notExistProjectList.joinToString(", ") else "Import Excel failure"
} }
} }

+ 22
- 0
src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt Visa fil

@@ -5,12 +5,18 @@ import com.ffii.tsms.modules.timesheet.entity.LeaveType
import com.ffii.tsms.modules.timesheet.service.LeaveService import com.ffii.tsms.modules.timesheet.service.LeaveService
import com.ffii.tsms.modules.timesheet.service.TimesheetsService import com.ffii.tsms.modules.timesheet.service.TimesheetsService
import com.ffii.tsms.modules.timesheet.web.models.* import com.ffii.tsms.modules.timesheet.web.models.*
import jakarta.servlet.http.HttpServletRequest
import jakarta.validation.Valid import jakarta.validation.Valid
import org.apache.poi.ss.usermodel.Workbook
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.ServletRequestBindingException
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartHttpServletRequest
import java.time.LocalDate import java.time.LocalDate
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter


@@ -85,4 +91,20 @@ class TimesheetsController(private val timesheetsService: TimesheetsService, pri
fun leaveTypes(): List<LeaveType> { fun leaveTypes(): List<LeaveType> {
return leaveService.getLeaveTypes() return leaveService.getLeaveTypes()
} }

@PostMapping("/import")
@Throws(ServletRequestBindingException::class)
fun importFile(request: HttpServletRequest): ResponseEntity<*> {
var workbook: Workbook? = null

try {
val multipartFile = (request as MultipartHttpServletRequest).getFile("multipartFileList")
workbook = XSSFWorkbook(multipartFile?.inputStream)
} catch (e: Exception) {
println("Excel Wrong")
println(e)
}

return ResponseEntity.ok(timesheetsService.importFile(workbook))
}
} }

+ 7
- 7
src/main/java/com/ffii/tsms/modules/user/service/UserService.java Visa fil

@@ -229,6 +229,13 @@ public class UserService extends AbstractBaseEntityService<User, Long, UserRepos
List<Map<String, Integer>> authBatchDeleteValues = req.getRemoveAuthIds().stream() List<Map<String, Integer>> authBatchDeleteValues = req.getRemoveAuthIds().stream()
.map(authId -> Map.of("userId", (int)id, "authId", authId)) .map(authId -> Map.of("userId", (int)id, "authId", authId))
.collect(Collectors.toList()); .collect(Collectors.toList());
if (!authBatchDeleteValues.isEmpty()) {
jdbcDao.batchUpdate(
"DELETE FROM user_authority"
+ " WHERE userId = :userId ",
// + "AND authId = :authId",
authBatchDeleteValues);
}
if (!authBatchInsertValues.isEmpty()) { if (!authBatchInsertValues.isEmpty()) {
jdbcDao.batchUpdate( jdbcDao.batchUpdate(
"INSERT IGNORE INTO user_authority (userId, authId)" "INSERT IGNORE INTO user_authority (userId, authId)"
@@ -236,13 +243,6 @@ public class UserService extends AbstractBaseEntityService<User, Long, UserRepos
authBatchInsertValues); authBatchInsertValues);
} }


if (!authBatchDeleteValues.isEmpty()) {
jdbcDao.batchUpdate(
"DELETE FROM user_authority"
+ " WHERE userId = :userId ",
// + "AND authId = :authId",
authBatchDeleteValues);
}
return instance; return instance;
} }




+ 9
- 0
src/main/resources/db/changelog/changes/20240617_01_cyril/01_update_task.sql Visa fil

@@ -0,0 +1,9 @@
-- liquibase formatted sql
-- changeset cyril:task

INSERT
INTO
task
(name, taskGroupId)
VALUES
('5.8 Manhour Import', 5);

+ 5
- 0
src/main/resources/db/changelog/changes/20240617_01_cyril/02_update_project.sql Visa fil

@@ -0,0 +1,5 @@
-- liquibase formatted sql
-- changeset cyril:task

ALTER TABLE `project`
CHANGE COLUMN `name` `name` VARCHAR(255) CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci' NOT NULL ;

+ 81
- 0
src/main/resources/db/changelog/changes/20240702_01_cyril/01_update_authority.sql Visa fil

@@ -0,0 +1,81 @@
-- liquibase formatted sql
-- changeset cyril:authority, user_authority

UPDATE authority
SET name='Maintain User in Master Data'
WHERE id=1;
UPDATE authority
SET name='Maintain User Group in Master Data'
WHERE id=2;
UPDATE authority
SET name='View User in Master Data'
WHERE id=3;
UPDATE authority
SET name='View User Group in Master Data'
WHERE id=4;
DELETE FROM authority
WHERE id=5;
DELETE FROM authority
WHERE id=6;
INSERT INTO authority (authority,name)
VALUES ('VIEW_CLIENT','View Client in Master Data');
INSERT INTO authority (authority,name)
VALUES ('VIEW_SUBSIDIARY','View Subsidiary in Master Data');
INSERT INTO authority (authority,name)
VALUES ('VIEW_STAFF','View Staff in Master Data');
INSERT INTO authority (authority,name)
VALUES ('VIEW_COMPANY','View Company in Master Data');
INSERT INTO authority (authority,name)
VALUES ('VIEW_SKILL','View Skill in Master Data');
INSERT INTO authority (authority,name)
VALUES ('VIEW_DEPARTMENT','View Department in Master Data');
INSERT INTO authority (authority,name)
VALUES ('VIEW_POSITION','View Position in Master Data');
INSERT INTO authority (authority,name)
VALUES ('VIEW_SALARY','View Salary in Master Data');
INSERT INTO authority (authority,name)
VALUES ('VIEW_TEAM','View Team in Master Data');
INSERT INTO authority (authority,name)
VALUES ('VIEW_HOLIDAY','View Holiday in Master Data');
INSERT INTO authority (authority,name)
VALUES ('MAINTAIN_CLIENT','Maintain Client in Master Data');
INSERT INTO authority (authority,name)
VALUES ('MAINTAIN_SUBSIDIARY','Maintain Subsidiary in Master Data');
INSERT INTO authority (authority,name)
VALUES ('MAINTAIN_STAFF','Maintain Staff in Master Data');
INSERT INTO authority (authority,name)
VALUES ('MAINTAIN_COMPANY','Maintain Company in Master Data');
INSERT INTO authority (authority,name)
VALUES ('MAINTAIN_SKILL','Maintain Skill in Master Data');
INSERT INTO authority (authority,name)
VALUES ('MAINTAIN_DEPARTMENT','Maintain Department in Master Data');
INSERT INTO authority (authority,name)
VALUES ('MAINTAIN_POSITION','Maintain Position in Master Data');
INSERT INTO authority (authority,name)
VALUES ('MAINTAIN_SALARY','Maintain Salary in Master Data');
INSERT INTO authority (authority,name)
VALUES ('MAINTAIN_TEAM','Maintain Team in Master Data');
INSERT INTO authority (authority,name)
VALUES ('MAINTAIN_HOLIDAY','Maintain Holiday in Master Data');

INSERT INTO `user_authority` VALUES
(1,21),
(1,22),
(1,23),
(1,24),
(1,25),
(1,26),
(1,27),
(1,28),
(1,29),
(1,30),
(1,31),
(1,32),
(1,33),
(1,34),
(1,35),
(1,36),
(1,37),
(1,38),
(1,39),
(1,40);

+ 5
- 0
src/main/resources/db/changelog/changes/20240704_01_cyril/01_update_project.sql Visa fil

@@ -0,0 +1,5 @@
-- liquibase formatted sql
-- changeset cyril:project

ALTER TABLE `project`
ADD COLUMN `subContractFee` DOUBLE NULL DEFAULT NULL AFTER `expectedTotalFee`;

Binär
src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx Visa fil


Binär
src/main/resources/templates/report/AR04_Cost and Expense Report v02.xlsx Visa fil


Binär
src/main/resources/templates/report/AR05_Project Completion Report.xlsx Visa fil


Binär
src/main/resources/templates/report/AR06_Project Completion Report with Outstanding Accounts Receivable v02.xlsx Visa fil


Binär
src/main/resources/templates/report/AR08_Monthly Work Hours Analysis Report.xlsx Visa fil


Binär
src/main/resources/templates/report/Cross Team Charge Report.xlsx Visa fil


Laddar…
Avbryt
Spara