diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/BuildingTypeRepository.kt b/src/main/java/com/ffii/tsms/modules/data/entity/BuildingTypeRepository.kt index 1a4443b..336dc1c 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/BuildingTypeRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/data/entity/BuildingTypeRepository.kt @@ -3,4 +3,5 @@ package com.ffii.tsms.modules.data.entity; import com.ffii.core.support.AbstractRepository interface BuildingTypeRepository : AbstractRepository { + fun findByName(name: String): BuildingType? } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/CustomerRepository.java b/src/main/java/com/ffii/tsms/modules/data/entity/CustomerRepository.java index d075a18..58355b6 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/CustomerRepository.java +++ b/src/main/java/com/ffii/tsms/modules/data/entity/CustomerRepository.java @@ -3,6 +3,7 @@ package com.ffii.tsms.modules.data.entity; import java.util.Optional; import java.util.List; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import com.ffii.core.support.AbstractRepository; @@ -11,4 +12,7 @@ public interface CustomerRepository extends AbstractRepository { List findAllByDeletedFalse(); Optional findByCode(@Param("code") String code); Optional findByName(@Param("name") String name); + + @Query("SELECT max(cast(substring_index(c.code, '-', -1) as long)) FROM Customer c") + Long getLatestCodeNumber(); } diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/LocationRepository.kt b/src/main/java/com/ffii/tsms/modules/data/entity/LocationRepository.kt index 278bd35..6ad53c3 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/LocationRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/data/entity/LocationRepository.kt @@ -3,4 +3,6 @@ package com.ffii.tsms.modules.data.entity; import com.ffii.core.support.AbstractRepository interface LocationRepository : AbstractRepository { + + fun findByName(name: String): Location } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java b/src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java index b9fc459..f6e8a46 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java +++ b/src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java @@ -25,4 +25,5 @@ public interface StaffRepository extends AbstractRepository { Optional> findAllByDeletedFalse(); Optional findIdAndNameByUserIdAndDeletedFalse(Long id); + } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/Subsidiary.java b/src/main/java/com/ffii/tsms/modules/data/entity/Subsidiary.java index 980a8ba..725b475 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/Subsidiary.java +++ b/src/main/java/com/ffii/tsms/modules/data/entity/Subsidiary.java @@ -10,6 +10,7 @@ import java.util.List; import static jakarta.persistence.CascadeType.ALL; + @Entity @Table(name = "subsidiary") public class Subsidiary extends BaseEntity { diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/SubsidiaryRepository.java b/src/main/java/com/ffii/tsms/modules/data/entity/SubsidiaryRepository.java index d94b9b9..9edc95c 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/SubsidiaryRepository.java +++ b/src/main/java/com/ffii/tsms/modules/data/entity/SubsidiaryRepository.java @@ -1,6 +1,7 @@ package com.ffii.tsms.modules.data.entity; import com.ffii.core.support.AbstractRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; @@ -12,4 +13,9 @@ public interface SubsidiaryRepository extends AbstractRepository findAllByDeletedFalseAndIdIn(List id); Optional findByCode(@Param("code") String code); + + Optional findByName(@Param("name") String name); + + @Query("SELECT max(cast(substring_index(s.code, '-', -1) as long)) FROM Subsidiary s") + Long getLatestCodeNumber(); } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/TeamRepository.java b/src/main/java/com/ffii/tsms/modules/data/entity/TeamRepository.java index 3a3cf41..a788348 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/TeamRepository.java +++ b/src/main/java/com/ffii/tsms/modules/data/entity/TeamRepository.java @@ -9,4 +9,6 @@ public interface TeamRepository extends AbstractRepository { List findByDeletedFalse(); Team findByStaff(Staff staff); + + Team findByCode(String code); } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/WorkNatureRepository.kt b/src/main/java/com/ffii/tsms/modules/data/entity/WorkNatureRepository.kt index 24f88c7..20144a5 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/WorkNatureRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/data/entity/WorkNatureRepository.kt @@ -3,4 +3,5 @@ package com.ffii.tsms.modules.data.entity; import com.ffii.core.support.AbstractRepository interface WorkNatureRepository : AbstractRepository { + fun findByName(name: String): WorkNature? } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/service/CustomerService.kt b/src/main/java/com/ffii/tsms/modules/data/service/CustomerService.kt index 4abb66e..9202f2d 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/CustomerService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/CustomerService.kt @@ -37,6 +37,18 @@ open class CustomerService( 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 { val duplicateCustomer = findCustomerByCode(saveCustomer.code) diff --git a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt index 69f4567..e9c8b73 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt @@ -68,19 +68,23 @@ open class DashboardService( fun searchCustomerSubsidiaryProject(args: Map): List> { 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 projectId," + " p.code as projectCode," + " p.name as projectName," + " te.code as team," + " s.name as teamLead," - + " tg.name as expectedStage," + + " GROUP_CONCAT(DISTINCT tg.name ORDER BY tg.name) as expectedStage," + " p.totalManhour as budgetedManhour," + " sum(t.normalConsumed) + sum(t.otConsumed) as spentManhour," + " 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 (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" + " left join project_task pt on p.id = pt.project_id" + " left join timesheet t on pt.id = t.projectTaskId" @@ -105,7 +109,7 @@ open class DashboardService( + " ) milestonePayment on milestonePayment.pid = p.id" + " where p.customerId = :customerId" + " 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\")" ) @@ -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) } fun searchCustomerNonSubsidiaryProject(args: Map): List> { 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 projectId," + " p.code as projectCode," + " p.name as projectName," + " te.code as team," + " s.name as teamLead," - + " tg.name as expectedStage," + + " 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" + + " 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" @@ -159,10 +176,16 @@ open class DashboardService( + " where p.customerId = :customerId" + " and isNull(p.customerSubsidiaryId)" + " 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) } @@ -276,19 +299,23 @@ open class DashboardService( fun searchTeamProject(args: Map): List> { 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 projectId," + " p.code as projectCode," + " p.name as projectName," + " te.code as team," + " s.name as teamLead," - + " tg.name as expectedStage," + + " 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" + + " 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" @@ -312,12 +339,111 @@ open class DashboardService( + " ) milestonePayment on milestonePayment.pid = p.id" + " where p.teamLead = :teamLeadId" + " 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) } + + fun searchTeamConsumption(args: Map): List> + { + 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): List> { val sql = StringBuilder( "select" @@ -327,13 +453,19 @@ open class DashboardService( + " coalesce(pj.totalFee,0) as totalFee," + " coalesce(pj.totalBudget,0) as totalBudget," + " 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," + " round(expenditure.cumulativeExpenditure,2) as cumulativeExpenditure," + " 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 'Negative'" + " 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" + " left join (" + " select" @@ -404,12 +536,18 @@ open class DashboardService( + " round(sum(p.expectedTotalFee) * 0.8,2) as totalBudget," + " round(expenditure.cumulativeExpenditure,2) as cumulativeExpenditure," + " sum(i.issueAmount) as totalInvoiced," + + " sum(p.expectedTotalFee) - sum(i.issueAmount) as unInvoiced," + " sum(i.paidAmount) as totalReceived," + " case" + " when round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2) >= 1 then 'Positive'" + " when round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2) < 1 then 'Negative'" + " 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" + " left join (" + " select" @@ -445,16 +583,103 @@ open class DashboardService( 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): List> { - 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," - + " 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(sum(i.issueAmount),0) as totalInvoiced," + " 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'" + " end as cashFlowStatus," + " 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" - + " 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" + " 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," + + " c3.code as customerCode," + + " c3.name as customerName," + " sum(i3.issueAmount) as issueAmount," + " sum(i3.paidAmount) as paidAmount" + " from team t3" @@ -483,15 +713,34 @@ open class DashboardService( + " 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")) + ) + 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" + } + 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 (" + " select" + " r.teamId as teamId," @@ -517,15 +766,15 @@ open class DashboardService( + " 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" + + " ) as expenditure on expenditure.teamId = t.id and expenditure.customerId = i.cid" + " 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(" 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) } @@ -547,10 +796,20 @@ open class DashboardService( + " 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," + + " 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" - + " 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" + " from team t" + " 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 (round(sum(p.expectedTotalFee)*0.8,2),0) as totalBudget," + " 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" + " left join (" + " select" @@ -1083,7 +1344,9 @@ open class DashboardService( "select" + " concat(p.code,'-',p.name) as projectCodeAndName," + " 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" + " when p.expectedTotalFee - expenditure.expenditure >= 0 then 'Within Budget'" + " when p.expectedTotalFee - expenditure.expenditure < 0 then 'Overconsumption'" @@ -1189,32 +1452,61 @@ open class DashboardService( + " left join staff s on s.id = ts.staffId" + " where p.id = :projectId" + " 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) } fun monthlyActualTeamTotalManhoursSpent(args: Map): List> { 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) @@ -1253,25 +1545,54 @@ open class DashboardService( } fun weeklyActualTeamTotalManhoursSpent(args: Map): List> { 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) @@ -1346,6 +1667,7 @@ open class DashboardService( + " AND dates.missing_date = ts.recordDate" + " WHERE" + " st.teamId = :teamId" + + " and st.deleted = 0" + " AND ts.recordDate IS NULL" + " GROUP BY st.id, st.name" + " ORDER BY" @@ -1595,6 +1917,7 @@ open class DashboardService( + " group by p.id, p.name" + " ) as result on result.pid = p2.id" + " where s2.id = :staffId" + + " and result.manhours > 0" + " group by p2.id, p2.name, result.manhours" ) @@ -1626,6 +1949,7 @@ open class DashboardService( + " group by p.id, p.name" + " ) as result on result.pid = p2.id" + " where s2.id = :staffId" + + " and result.manhours > 0" + " group by p2.id, p2.name, result.manhours" ) @@ -1656,6 +1980,7 @@ open class DashboardService( + " group by p.id, p.name" + " ) as result on result.pid = p2.id" + " where s2.id = :staffId" + + " and result.manhours > 0" + " group by p2.id, p2.name, result.manhours" ) diff --git a/src/main/java/com/ffii/tsms/modules/data/service/SubsidiaryService.kt b/src/main/java/com/ffii/tsms/modules/data/service/SubsidiaryService.kt index ea55431..23f53c6 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/SubsidiaryService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/SubsidiaryService.kt @@ -38,6 +38,17 @@ open class SubsidiaryService( 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 { val duplicateSubsidiary = findSubsidiaryByCode(saveSubsidiary.code) diff --git a/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt b/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt index 6cb4910..679a734 100644 --- a/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt +++ b/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt @@ -51,6 +51,7 @@ class DashboardController( fun searchCustomerSubsidiaryProject(request: HttpServletRequest?): List> { val customerId = request?.getParameter("customerId") val subsidiaryId = request?.getParameter("subsidiaryId") + val tableSorting = request?.getParameter("tableSorting") val args = mutableMapOf() var result: List> = emptyList() if (customerId != null) { @@ -59,6 +60,9 @@ class DashboardController( if (subsidiaryId != null) { args["subsidiaryId"] = subsidiaryId } + if (tableSorting != null) { + args["tableSorting"] = tableSorting + } if (customerId != null && subsidiaryId != null) { result = dashboardService.searchCustomerSubsidiaryProject(args) @@ -80,16 +84,36 @@ class DashboardController( @GetMapping("/searchTeamProject") fun searchTeamProject(request: HttpServletRequest?): List> { val teamLeadId = request?.getParameter("teamLeadId") + val tableSorting = request?.getParameter("tableSorting") val args = mutableMapOf() var result: List> = emptyList() if (teamLeadId != null) { args["teamLeadId"] = teamLeadId } + if (tableSorting != null) { + args["tableSorting"] = tableSorting + } result = dashboardService.searchTeamProject(args) return result } + @GetMapping("/searchTeamConsumption") + fun searchTeamConsumption(request: HttpServletRequest?): List> { + val args = mutableMapOf() + val teamIdList = request?.getParameter("teamIdList") + val tableSorting = request?.getParameter("tableSorting") + val teamIds = teamIdList?.split(",")?.map { it.toInt() }?.toList() + var result: List> = emptyList() + if (teamIds != null) { + args["teamIds"] = teamIds + } + if (tableSorting != null) { + args["tableSorting"] = tableSorting + } + result = dashboardService.searchTeamConsumption(args) + return result + } @GetMapping("/searchFinancialSummaryCard") fun searchFinancialSummaryCard(request: HttpServletRequest?): List> { val authority = dashboardService.viewDashboardAuthority() diff --git a/src/main/java/com/ffii/tsms/modules/data/web/SkillController.kt b/src/main/java/com/ffii/tsms/modules/data/web/SkillController.kt index 0b70d12..77a40ca 100644 --- a/src/main/java/com/ffii/tsms/modules/data/web/SkillController.kt +++ b/src/main/java/com/ffii/tsms/modules/data/web/SkillController.kt @@ -19,7 +19,7 @@ import org.springframework.web.bind.annotation.* open class SkillController(private val skillService: SkillService) { @PostMapping("/save") - @PreAuthorize("hasAuthority('MAINTAIN_MASTERDATA')") +// @PreAuthorize("hasAuthority('MAINTAIN_MASTERDATA')") open fun saveSkill(@Valid @RequestBody newSkill: NewSkillRequest): Skill { return skillService.saveOrUpdate(newSkill) } diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/InvoiceRepository.kt b/src/main/java/com/ffii/tsms/modules/project/entity/InvoiceRepository.kt index cdedfc4..940f2d2 100644 --- a/src/main/java/com/ffii/tsms/modules/project/entity/InvoiceRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/project/entity/InvoiceRepository.kt @@ -12,4 +12,6 @@ interface InvoiceRepository : AbstractRepository { fun findInvoiceInfoByPaidAmountIsNotNull(): List fun findByInvoiceNo(invoiceNo: String): Invoice + + fun findAllByProjectCodeAndPaidAmountIsNotNull(projectCode: String): List } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/Project.kt b/src/main/java/com/ffii/tsms/modules/project/entity/Project.kt index 533dcd3..e52f80b 100644 --- a/src/main/java/com/ffii/tsms/modules/project/entity/Project.kt +++ b/src/main/java/com/ffii/tsms/modules/project/entity/Project.kt @@ -77,6 +77,9 @@ open class Project : BaseEntity() { @Column(name = "expectedTotalFee") open var expectedTotalFee: Double? = null + @Column(name = "subContractFee") + open var subContractFee: Double? = null + @ManyToOne @JoinColumn(name = "serviceTypeId") open var serviceType: ServiceType? = null diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt b/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt index 8f367a8..4fdf2dc 100644 --- a/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt @@ -34,4 +34,6 @@ interface ProjectRepository : AbstractRepository { fun findAllByStatusIsNotAndMainProjectIsNull(status: String): List fun findAllByTeamLeadAndCustomer(teamLead: Staff, customer: Customer): List + + fun findByCode(code: String): Project? } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/service/InvoiceService.kt b/src/main/java/com/ffii/tsms/modules/project/service/InvoiceService.kt index bb087eb..950ab7e 100644 --- a/src/main/java/com/ffii/tsms/modules/project/service/InvoiceService.kt +++ b/src/main/java/com/ffii/tsms/modules/project/service/InvoiceService.kt @@ -362,7 +362,7 @@ open class InvoiceService( // Check the import invoice with the data in DB 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 checkInvoiceNo(sheetInvoice, invoices, invoicesResult, true) 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 milestonePayment = milestonePaymentRepository.findById(paymentMilestoneId).orElseThrow() 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 projectName = ExcelUtils.getCell(sheet, i, 2).stringCellValue team = ExcelUtils.getCell(sheet, i, 3).stringCellValue @@ -447,7 +447,7 @@ open class InvoiceService( val duplicateItemsInInvoice = checkDuplicateItemInImportedInvoice(sheet,2,0) 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 checkInvoiceNo(sheetInvoice, invoices, invoicesResult, false) checkProjectCode(sheetProjectCode, projects, newProjectCodes) @@ -489,7 +489,7 @@ open class InvoiceService( } 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.receiptDate = ExcelUtils.getCell(sheet, i, 4).dateCellValue.toInstant().atZone(ZoneId.systemDefault()).toLocalDate() saveAndFlush(invoice) diff --git a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt index dfb9bbe..66f2a71 100644 --- a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt +++ b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt @@ -1,26 +1,26 @@ 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.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.Milestone 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.ProjectSearchInfo import com.ffii.tsms.modules.project.web.models.* 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.transaction.annotation.Transactional import java.time.LocalDate import java.time.format.DateTimeFormatter -import java.util.Optional -import kotlin.jvm.optionals.getOrElse import kotlin.jvm.optionals.getOrNull + @Service open class ProjectsService( private val projectRepository: ProjectRepository, @@ -46,7 +46,11 @@ open class ProjectsService( private val timesheetRepository: TimesheetRepository, private val taskTemplateRepository: TaskTemplateRepository, 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 { return projectRepository.findProjectSearchInfoByOrderByCreatedDesc() @@ -107,7 +111,8 @@ open class ProjectsService( code = project.code!!, name = project.name!!, 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) .filter { milestone -> milestone.taskGroup?.id != null } .associateBy { milestone -> milestone.taskGroup!!.id!! }.mapValues { (_, milestone) -> @@ -164,10 +169,15 @@ open class ProjectsService( .orElseThrow() else null val teamLead = staffRepository.findById(request.projectLeadId).orElseThrow() 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 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 taskGroupMap = tasksService.allTaskGroups().associateBy { it.id } @@ -179,15 +189,23 @@ open class ProjectsService( project.apply { name = request.projectName 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 + subContractFee = request.subContractFee totalManhour = request.totalManhour actualStart = request.projectActualStart actualEnd = request.projectActualEnd status = if (this.status == "Deleted" || this.deleted == true) "Deleted" else if (this.actualStart != null && this.actualEnd != null) "Completed" else if (this.actualStart != null) "On-going" - else "Pending To Start" + else request.projectStatus ?: "Pending To Start" isClpProject = request.isClpProject this.mainProject = mainProject @@ -203,11 +221,11 @@ open class ProjectsService( this.teamLead = teamLead this.customer = customer custLeadName = - if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.name else subsidiaryContact.name + if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact?.name else subsidiaryContact?.name custLeadEmail = - if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.email else subsidiaryContact.email + if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact?.email else subsidiaryContact?.email 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.customerContact = if (customerSubsidiary == null) clientContact else null this.subsidiaryContact = if (customerSubsidiary != null) subsidiaryContact else null @@ -279,14 +297,15 @@ open class ProjectsService( if (milestones.isNotEmpty()) { project.apply { 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 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 = gradeAllocationRepository.findByProject(project).subtract(gradeAllocations.toSet()) 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 -> subsidiaryContactService.findAllBySubsidiaryId(subsidiaryId) } ?: emptyList() - val customerContact = project.customer?.id?.let { customerId -> customerContactService.findAllByCustomerId(customerId) } - ?: emptyList() + val customerContact = + project.customer?.id?.let { customerId -> customerContactService.findAllByCustomerId(customerId) } + ?: emptyList() MainProjectDetails( projectId = project.id, @@ -441,10 +462,308 @@ open class ProjectsService( buildingTypeIds = project.buildingTypes.mapNotNull { buildingType -> buildingType.id }, workNatureIds = project.workNatures.mapNotNull { workNature -> workNature.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, - 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..() + for (j in 18..>() + if (projectTasks.isNotEmpty()) { + groupedProjectTasks = projectTasks.groupBy { + it.task!!.taskGroup!!.id!! + }.mapValues { (_, projectTask) -> + projectTask.map { it.task!!.id!! } + } + } + val taskGroups = mutableMapOf() + 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" + } } diff --git a/src/main/java/com/ffii/tsms/modules/project/web/ProjectsController.kt b/src/main/java/com/ffii/tsms/modules/project/web/ProjectsController.kt index 447a50b..61c070e 100644 --- a/src/main/java/com/ffii/tsms/modules/project/web/ProjectsController.kt +++ b/src/main/java/com/ffii/tsms/modules/project/web/ProjectsController.kt @@ -2,15 +2,21 @@ package com.ffii.tsms.modules.project.web import com.ffii.core.exception.NotFoundException 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.ProjectRepository +import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo import com.ffii.tsms.modules.project.service.ProjectsService import com.ffii.tsms.modules.project.web.models.* +import jakarta.servlet.http.HttpServletRequest 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.ResponseEntity +import org.springframework.web.bind.ServletRequestBindingException import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartHttpServletRequest + @RestController @RequestMapping("/projects") @@ -85,4 +91,20 @@ class ProjectsController(private val projectsService: ProjectsService, private v fun projectWorkNatures(): List { 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)) + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/web/models/EditProjectDetails.kt b/src/main/java/com/ffii/tsms/modules/project/web/models/EditProjectDetails.kt index 704f0fd..0cb0f1e 100644 --- a/src/main/java/com/ffii/tsms/modules/project/web/models/EditProjectDetails.kt +++ b/src/main/java/com/ffii/tsms/modules/project/web/models/EditProjectDetails.kt @@ -40,5 +40,6 @@ data class EditProjectDetails( val milestones: Map, // Miscellaneous - val expectedProjectFee: Double? + val expectedProjectFee: Double?, + val subContractFee: Double? ) diff --git a/src/main/java/com/ffii/tsms/modules/project/web/models/MainProjectDetails.kt b/src/main/java/com/ffii/tsms/modules/project/web/models/MainProjectDetails.kt index 3c2a748..ba2e4bc 100644 --- a/src/main/java/com/ffii/tsms/modules/project/web/models/MainProjectDetails.kt +++ b/src/main/java/com/ffii/tsms/modules/project/web/models/MainProjectDetails.kt @@ -27,4 +27,5 @@ data class MainProjectDetails ( val clientSubsidiaryId: Long?, val expectedProjectFee: Double?, + val subContractFee: Double?, ) \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt b/src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt index 1f59b33..5fdea98 100644 --- a/src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt +++ b/src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt @@ -7,7 +7,7 @@ import java.time.LocalDate data class NewProjectRequest( // Project details // @field:NotBlank(message = "project code cannot be empty") -// val projectCode: String, + val projectCode: String?, @field:NotBlank(message = "project name cannot be empty") val projectName: String, val projectCategoryId: Long, @@ -16,8 +16,10 @@ data class NewProjectRequest( val projectId: Long?, val projectActualStart: LocalDate?, val projectActualEnd: LocalDate?, + val projectPlanEnd: LocalDate?, val isClpProject: Boolean?, val mainProjectId: Long?, + val projectStatus: String?, val serviceTypeId: Long, val fundingTypeId: Long, @@ -30,7 +32,7 @@ data class NewProjectRequest( // Client details val clientId: Long, - val clientContactId: Long, + val clientContactId: Long?, val clientSubsidiaryId: Long?, // Allocation @@ -43,7 +45,8 @@ data class NewProjectRequest( val milestones: Map, // Miscellaneous - val expectedProjectFee: Double + val expectedProjectFee: Double, + val subContractFee: Double? ) data class TaskGroupAllocation( diff --git a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt index ea983f2..7224a7e 100644 --- a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt @@ -2,6 +2,7 @@ package com.ffii.tsms.modules.report.service import com.ffii.core.support.JdbcDao 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.Staff 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.xssf.usermodel.XSSFWorkbook 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.stereotype.Service import java.io.ByteArrayOutputStream @@ -54,6 +57,7 @@ open class ReportService( private val COMPLETE_PROJECT_OUTSTANDING_RECEIVABLE = "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 CROSS_TEAM_CHARGE_REPORT = "templates/report/Cross Team Charge Report.xlsx" // ==============================|| GENERATE REPORT ||============================== // fun generalCreateReportIndexed( // just loop through query records one by one, return rowIndex @@ -65,7 +69,7 @@ open class ReportService( var rowIndex = startRow var columnIndex = startColumn 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()) val keys = obj.keys.toList() keys.forEachIndexed { keyIndex, key -> @@ -299,6 +303,25 @@ open class ReportService( return outputStream.toByteArray() } + @Throws(IOException::class) + fun generateCrossTeamChargeReport( + month: String, + timesheets: List, + teams: List, + grades: List, + ): 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 ||============================== // // EX01 Financial Report @@ -435,7 +458,8 @@ open class ReportService( val uninvoiceCell = row.createCell(12) uninvoiceCell.apply { cellFormula = - " IF(I${rowNum}<=J${rowNum}, I${rowNum}-L${rowNum}, IF(AND(I${rowNum}>J${rowNum}, J${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}J${rowNum}, J${rowNum}>=L${rowNum}), J${rowNum}-L${rowNum}, 0))) " cellStyle.dataFormat = accountingStyle } @@ -528,6 +552,11 @@ open class ReportService( CellUtil.setCellStyleProperty(sumUInvoiceCell, "borderBottom", BorderStyle.DOUBLE) 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, "borderBottom", BorderStyle.DOUBLE) @@ -606,7 +635,7 @@ open class ReportService( val row9: Row = sheet.getRow(rowNum) val cell5 = row9.createCell(2) cell5.apply { - cellFormula = "N${lastRowNum}" + cellFormula = "M${lastRowNum}" cellStyle.dataFormat = accountingStyle } @@ -691,12 +720,12 @@ open class ReportService( rowIndex = 10 sheet.getRow(rowIndex).apply { createCell(1).apply { - setCellValue(project.expectedTotalFee!! * 0.8) + setCellValue(if (project.expectedTotalFee != null) project.expectedTotalFee!! * 0.8 else 0.0) cellStyle.dataFormat = accountingStyle } createCell(2).apply { - setCellValue(project.expectedTotalFee!!) + setCellValue(project.expectedTotalFee ?: 0.0) cellStyle.dataFormat = accountingStyle } } @@ -979,10 +1008,15 @@ open class ReportService( project.milestones.forEach { milestone: Milestone -> 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(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++ val tempRow = sheet.getRow(rowIndex) ?: sheet.createRow(rowIndex) rowIndex++ @@ -1004,7 +1038,7 @@ open class ReportService( // // val resourceUtilization = // manHoursSpent / (milestone.stagePercentAllocation!! / 100 * project.totalManhour!!) - setCellValue(resourceUtilization) + setCellValue(resourceUtilization) // } else { // setCellValue(0.0) // } @@ -1080,6 +1114,13 @@ open class ReportService( val boldFont = workbook.createFont() boldFont.bold = true 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 date = month.withDayOfMonth(day) val formattedDate = date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")) @@ -1228,7 +1269,7 @@ open class ReportService( projectList.forEachIndexed { index, title -> tempCell = sheet.getRow(7).createCell(columnIndex + index) tempCell.setCellValue(title) - tempCell.cellStyle = boldStyle + tempCell.cellStyle = projectsStyle CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) CellUtil.setVerticalAlignment(tempCell, VerticalAlignment.CENTER) CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom) @@ -1271,7 +1312,7 @@ open class ReportService( ///////////////////////////////////////////////////////// Leave Hours title //////////////////////////////////////////////////////////////////// tempCell = sheet.getRow(rowIndex).createCell(columnIndex) tempCell.setCellValue("Leave Hours") - tempCell.cellStyle = boldStyle + tempCell.cellStyle = projectsStyle CellUtil.setVerticalAlignment(tempCell, VerticalAlignment.CENTER) CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom) @@ -1279,8 +1320,10 @@ open class ReportService( columnIndex += 1 tempCell = sheet.getRow(rowIndex).createCell(columnIndex) 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) sheet.addMergedRegion(CellRangeAddress(6, 6, 2, columnIndex)) @@ -1391,7 +1434,7 @@ open class ReportService( rowIndex = 5 columnIndex = 0 - generalCreateReportIndexed(sheet, result, rowIndex, columnIndex) + generalCreateReportIndexed(sheet, result.distinct(), rowIndex, columnIndex) return workbook } @@ -1481,7 +1524,7 @@ open class ReportService( setDataAndConditionalFormatting(workbook, sheet, lateStartData, evaluator) // Automatically adjust column widths to fit content - autoSizeColumns(sheet) + autoSizeColumns(sheet) return workbook } @@ -1627,7 +1670,8 @@ open class ReportService( // NEW Column F: Subsidiary Name val subsidiaryNameCell = row.createCell(5) // 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) // Column G: Project Plan Start Date @@ -1748,136 +1792,139 @@ open class ReportService( ) return jdbcDao.queryForList(sql.toString(), args) } + open fun getProjectCompletionReport(args: Map): List> { - 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) { 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( " 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.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) } open fun getProjectResourceOverconsumptionReport(args: Map): List> { - 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) { var statusFilter: String = "" if (args.containsKey("teamId")) - sql.append("and t.id = :teamId") + sql.append(" and t.id = :teamId") if (args.containsKey("custId")) - sql.append("and c.id = :custId") + sql.append(" and c.id = :custId") if (args.containsKey("subsidiaryId")) - sql.append("and ss.id = :subsidiaryId") + sql.append(" and ss.id = :subsidiaryId") if (args.containsKey("status")) statusFilter = when (args.get("status")) { - "Potential Overconsumption" -> "and " + + "Potential Overconsumption" -> " and " + " (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 " - "All" -> "and " + + + "All" -> " and " + " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit " + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit " -// "Overconsumption" -> "and " + +// "Overconsumption" -> " and " + // " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 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 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 (" + " select p.code, sum(i.issueAmount) as sumIssuedAmount , sum(i.paidAmount) as sumPaidAmount" + " 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," + " 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.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" + " from project p" + " 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 tsmsdb.team t on t.teamLead = p.teamLead" + " 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) { info["subsidiary"] = item.getValue("subsidiary") } + if (info["manhourExpenditure"] != item.getValue("sumManhourExpenditure")) { + info["manhourExpenditure"] = item.getValue("sumManhourExpenditure") + } if (info["description"] != item.getValue("description")) { info["description"] = item.getValue("description") } @@ -2162,9 +2222,9 @@ open class ReportService( rowNum = 6 val row6: Row = sheet.getRow(rowNum) 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() - }else{ + } else { info.getValue("client").toString() } row6Cell.setCellValue(clientSubsidiary) @@ -2335,7 +2395,8 @@ open class ReportService( } val totalManhourECell = totalManhourERow.getCell(1) ?: totalManhourERow.createCell(1) 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 } CellUtil.setCellStyleProperty(totalManhourETitleCell, "borderBottom", BorderStyle.THIN) @@ -2358,10 +2419,10 @@ open class ReportService( return workbook } - fun getCostAndExpense(clientId: Long?, teamId: Long?, type: String): List>{ + fun getCostAndExpense(clientId: Long?, teamId: Long?, type: String): List> { val sql = StringBuilder( " 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" + " from timesheet t" + " 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" + " where ISNULL(p.code) = False" ) - if(clientId != null){ - if(type == "client"){ + if (clientId != null) { + if (type == "client") { sql.append( " and c.id = :clientId " ) } - if(type == "subsidiary"){ + if (type == "subsidiary") { sql.append( " and s2.id = :clientId " ) } } - if(teamId != null){ + if (teamId != null) { sql.append( " and p.teamLead = :teamId " ) @@ -2417,9 +2478,9 @@ open class ReportService( val queryList = jdbcDao.queryForList(sql.toString(), args) val costAndExpenseList = mutableListOf>() - for(item in queryList){ + for (item in queryList) { val hourlyRate = (item.getValue("hourlyRate") as BigDecimal).toDouble() - if(item["code"] !in costAndExpenseList){ + if (item["code"] !in costAndExpenseList.map { it["code"] }) { costAndExpenseList.add( mapOf( "code" to item["code"], @@ -2428,22 +2489,27 @@ open class ReportService( "subsidiary" to item["subsidiary"], "teamLead" to item["teamLead"], "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"]) }!! 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 budgetRemain = budget - (item["manhourExpenditure"] as? Double ?: 0.0) val remainingPercent = (budgetRemain / budget) @@ -2462,7 +2528,7 @@ open class ReportService( clientId: Long?, budgetPercentage: Double?, type: String - ): Workbook{ + ): Workbook { val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) @@ -2480,9 +2546,9 @@ open class ReportService( rowNum = 2 val row2: Row = sheet.getRow(rowNum) val row2Cell = row2.getCell(2) - if(teamId == null){ + if (teamId == null) { row2Cell.setCellValue("All") - }else{ + } else { val sql = StringBuilder( " 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 val row3: Row = sheet.getRow(rowNum) val row3Cell = row3.getCell(2) - if(clientId == null){ + if (clientId == null) { row3Cell.setCellValue("All") - }else{ - val sql= StringBuilder() - if(type == "client"){ + } else { + val sql = StringBuilder() + if (type == "client") { sql.append( " select c.id, c.name from customer c where c.id = :clientId " ) } - if(type == "subsidiary"){ + if (type == "subsidiary") { sql.append( " select s.id, s.name from subsidiary s where s.id = :clientId " ) @@ -2513,15 +2579,15 @@ open class ReportService( val filterList: List> - if(budgetPercentage != null){ + if (budgetPercentage != null) { filterList = costAndExpenseList.filter { ((it["budgetPercentage"] as? Double) ?: 0.0) <= budgetPercentage } - }else{ + } else { filterList = costAndExpenseList } rowNum = 6 - for(item in filterList){ + for (item in filterList) { val index = filterList.indexOf(item) val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) val cell = row.getCell(0) ?: row.createCell(0) @@ -2571,13 +2637,13 @@ open class ReportService( val cell8 = row.getCell(8) ?: row.createCell(8) cell8.apply { - cellFormula = "G${rowNum+1}-H${rowNum+1}" + cellFormula = "G${rowNum + 1}-H${rowNum + 1}" } CellUtil.setCellStyleProperty(cell8, "dataFormat", accountingStyle) val cell9 = row.getCell(9) ?: row.createCell(9) cell9.apply { - cellFormula = "I${rowNum+1}/G${rowNum+1}" + cellFormula = "I${rowNum + 1}/G${rowNum + 1}" } CellUtil.setCellStyleProperty(cell9, "dataFormat", percentStyle) @@ -2587,11 +2653,18 @@ open class ReportService( return workbook } - fun genCostAndExpenseReport(request: costAndExpenseRequest): ByteArray{ + fun genCostAndExpenseReport(request: costAndExpenseRequest): ByteArray { 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() workbook.write(outputStream) @@ -2599,8 +2672,16 @@ open class ReportService( return outputStream.toByteArray() } - open fun getLateStartDetails(teamId: Long?, clientId: Long?, remainedDate: LocalDate, remainedDateTo: LocalDate, type: String?): List> { - val sql = StringBuilder(""" + + open fun getLateStartDetails( + teamId: Long?, + clientId: Long?, + remainedDate: LocalDate, + remainedDateTo: LocalDate, + type: String? + ): List> { + val sql = StringBuilder( + """ SELECT p.code AS project_code, p.name AS project_name, @@ -2621,7 +2702,8 @@ open class ReportService( p.status = 'Pending to Start' AND p.planStart < CURRENT_DATE AND m.endDate BETWEEN :remainedDate AND :remainedDateTo - """.trimIndent()) + """.trimIndent() + ) if (teamId != null && teamId > 0) { sql.append(" AND t.id = :teamId") @@ -2645,4 +2727,240 @@ open class ReportService( ) return jdbcDao.queryForList(sql.toString(), args) } -} + + @Throws(IOException::class) + private fun createCrossTeamChargeReport( + month: String, + timesheets: List, + teams: List, + grades: List, + 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().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().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().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 + } +} \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt b/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt index f84307e..fd6b347 100644 --- a/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt +++ b/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt @@ -34,8 +34,12 @@ import java.time.format.DateTimeFormatter import com.ffii.tsms.modules.project.entity.Project import com.ffii.tsms.modules.project.service.SubsidiaryService 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.ExampleMatcher +import java.time.YearMonth +import java.util.* @RestController @RequestMapping("/reports") @@ -55,9 +59,11 @@ class ReportController( private val customerService: CustomerService, private val subsidiaryService: SubsidiaryService, 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") @Throws(ServletRequestBindingException::class, IOException::class) fun getFinancialStatusReport(@RequestBody @Valid request: FinancialStatusReportRequest): ResponseEntity { @@ -76,7 +82,8 @@ class ReportController( val project = projectRepository.findById(request.projectId).orElseThrow() 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 reportResult: ByteArray = excelReportService.generateProjectCashFlowReport(project, invoices, timesheets, request.dateType) @@ -300,4 +307,35 @@ class ReportController( .body(ByteArrayResource(reportResult)) } + @PostMapping("/CrossTeamChargeReport") + @Throws(ServletRequestBindingException::class, IOException::class) + fun getCrossTeamChargeReport(@RequestBody @Valid request: CrossTeamChargeReportRequest): ResponseEntity { + + 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() + } + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt b/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt index 1233753..1c8b509 100644 --- a/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt +++ b/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt @@ -57,4 +57,8 @@ data class ProjectCompletionReport ( val startDate: LocalDate, val endDate: LocalDate, val outstanding: Boolean +) + +data class CrossTeamChargeReportRequest ( + val month: String, ) \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt b/src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt index 3defcf1..2826b9f 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt @@ -14,6 +14,8 @@ interface TimesheetRepository : AbstractRepository { fun findAllByProjectTaskIn(projectTasks: List): List + fun findAllByProjectIn(project: List): List + 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") diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt b/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt index a387127..1b4e2c2 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt +++ b/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt @@ -1,17 +1,22 @@ package com.ffii.tsms.modules.timesheet.service 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.StaffRepository +import com.ffii.tsms.modules.data.entity.WorkNature import com.ffii.tsms.modules.data.service.StaffsService 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.TimesheetRepository import com.ffii.tsms.modules.timesheet.web.models.TeamMemberTimeEntries 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.transaction.annotation.Transactional import java.time.LocalDate @@ -40,14 +45,15 @@ open class TimesheetsService( mergeTimeEntriesByProjectAndTask(timeEntries).map { timeEntry -> val task = timeEntry.taskId?.let { taskRepository.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 { this.staff = currentStaff this.recordDate = entryDate this.normalConsumed = timeEntry.inputHours this.otConsumed = timeEntry.otHours - this.projectTask = projectTask + this.projectTask = projectTask this.project = project this.remark = timeEntry.remark } @@ -60,7 +66,11 @@ open class TimesheetsService( } @Transactional - open fun saveMemberTimeEntry(staffId: Long, entry: TimeEntry, recordDate: LocalDate?): Map> { + open fun saveMemberTimeEntry( + staffId: Long, + entry: TimeEntry, + recordDate: LocalDate? + ): Map> { val authorities = staffsService.currentAuthorities() ?: throw BadRequestException() 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 task = entry.taskId?.let { taskRepository.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.otConsumed = entry.otHours - this.projectTask = projectTask + this.projectTask = projectTask this.project = project this.remark = entry.remark this.recordDate = this.recordDate ?: recordDate @@ -109,7 +119,7 @@ open class TimesheetsService( open fun getTeamMemberTimesheet(): Map { 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() // Get team where current staff is team lead @@ -136,27 +146,118 @@ open class TimesheetsService( private fun transformToTimeEntryMap(timesheets: List): Map> { return timesheets .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): List { return entries .groupBy { timeEntry -> Pair(timeEntry.projectId, timeEntry.taskId) } .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() + val sheet: Sheet = workbook.getSheetAt(0) + + logger.info("---------Start Import Timesheets-------") + val timesheetList = mutableListOf().toMutableList(); + for (i in 1.. 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" } } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt b/src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt index 6f2d4ce..29d1c28 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt +++ b/src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt @@ -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.TimesheetsService import com.ffii.tsms.modules.timesheet.web.models.* +import jakarta.servlet.http.HttpServletRequest 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.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartHttpServletRequest import java.time.LocalDate import java.time.format.DateTimeFormatter @@ -85,4 +91,20 @@ class TimesheetsController(private val timesheetsService: TimesheetsService, pri fun leaveTypes(): List { 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)) + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/user/service/UserService.java b/src/main/java/com/ffii/tsms/modules/user/service/UserService.java index 089fb00..1e713d2 100644 --- a/src/main/java/com/ffii/tsms/modules/user/service/UserService.java +++ b/src/main/java/com/ffii/tsms/modules/user/service/UserService.java @@ -229,6 +229,13 @@ public class UserService extends AbstractBaseEntityService> authBatchDeleteValues = req.getRemoveAuthIds().stream() .map(authId -> Map.of("userId", (int)id, "authId", authId)) .collect(Collectors.toList()); + if (!authBatchDeleteValues.isEmpty()) { + jdbcDao.batchUpdate( + "DELETE FROM user_authority" + + " WHERE userId = :userId ", +// + "AND authId = :authId", + authBatchDeleteValues); + } if (!authBatchInsertValues.isEmpty()) { jdbcDao.batchUpdate( "INSERT IGNORE INTO user_authority (userId, authId)" @@ -236,13 +243,6 @@ public class UserService extends AbstractBaseEntityService