| @@ -3,4 +3,5 @@ package com.ffii.tsms.modules.data.entity; | |||
| import com.ffii.core.support.AbstractRepository | |||
| interface BuildingTypeRepository : AbstractRepository<BuildingType, Long> { | |||
| fun findByName(name: String): BuildingType? | |||
| } | |||
| @@ -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<Customer, Long> { | |||
| List<Customer> findAllByDeletedFalse(); | |||
| Optional<Customer> findByCode(@Param("code") String code); | |||
| Optional<Customer> findByName(@Param("name") String name); | |||
| @Query("SELECT max(cast(substring_index(c.code, '-', -1) as long)) FROM Customer c") | |||
| Long getLatestCodeNumber(); | |||
| } | |||
| @@ -3,4 +3,6 @@ package com.ffii.tsms.modules.data.entity; | |||
| import com.ffii.core.support.AbstractRepository | |||
| interface LocationRepository : AbstractRepository<Location, Long> { | |||
| fun findByName(name: String): Location | |||
| } | |||
| @@ -25,4 +25,5 @@ public interface StaffRepository extends AbstractRepository<Staff, Long> { | |||
| Optional<List<Staff>> findAllByDeletedFalse(); | |||
| Optional<Staff> findIdAndNameByUserIdAndDeletedFalse(Long id); | |||
| } | |||
| @@ -10,6 +10,7 @@ import java.util.List; | |||
| import static jakarta.persistence.CascadeType.ALL; | |||
| @Entity | |||
| @Table(name = "subsidiary") | |||
| public class Subsidiary extends BaseEntity<Long> { | |||
| @@ -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<Subsidiary, Lon | |||
| List<Subsidiary> findAllByDeletedFalseAndIdIn(List<Long> id); | |||
| Optional<Subsidiary> findByCode(@Param("code") String code); | |||
| Optional<Subsidiary> findByName(@Param("name") String name); | |||
| @Query("SELECT max(cast(substring_index(s.code, '-', -1) as long)) FROM Subsidiary s") | |||
| Long getLatestCodeNumber(); | |||
| } | |||
| @@ -9,4 +9,6 @@ public interface TeamRepository extends AbstractRepository<Team, Long> { | |||
| List<Team> findByDeletedFalse(); | |||
| Team findByStaff(Staff staff); | |||
| Team findByCode(String code); | |||
| } | |||
| @@ -3,4 +3,5 @@ package com.ffii.tsms.modules.data.entity; | |||
| import com.ffii.core.support.AbstractRepository | |||
| interface WorkNatureRepository : AbstractRepository<WorkNature, Long> { | |||
| fun findByName(name: String): WorkNature? | |||
| } | |||
| @@ -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) | |||
| @@ -68,19 +68,23 @@ open class DashboardService( | |||
| fun searchCustomerSubsidiaryProject(args: Map<String, Any>): List<Map<String, Any>> { | |||
| 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<String, Any>): List<Map<String, Any>> { | |||
| 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<String, Any>): List<Map<String, Any>> { | |||
| 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<String, Any>): List<Map<String, Any>> | |||
| { | |||
| val sql = StringBuilder( | |||
| // "select" | |||
| // + " ROW_NUMBER() OVER (ORDER BY te.code, s.name, project.budgetedManhour) AS id," | |||
| // + " te.code as team," | |||
| // + " s.name as teamLead," | |||
| // + " project.budgetedManhour as budgetedManhour," | |||
| // + " COALESCE (sum(t.normalConsumed) + sum(t.otConsumed),0) as spentManhour," | |||
| // + " COALESCE (project.budgetedManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) as remainedManhour," | |||
| // + " coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/project.budgetedManhour)*100,2),0) as manhourConsumptionPercentage," | |||
| // + " case" | |||
| // + " when COALESCE (project.budgetedManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) > 0 then 0" | |||
| // + " when COALESCE (project.budgetedManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) <= 0 then 1" | |||
| // + " end as alert" | |||
| // + " from team te" | |||
| // + " left join project p on p.teamLead = te.teamLead" | |||
| // + " left join project_task pt on p.id = pt.project_id" | |||
| // + " left join timesheet t on pt.id = t.projectTaskId" | |||
| // + " left join staff s on te.teamLead = s.id" | |||
| // + " left join (" | |||
| // + " select sum(p2.totalManhour) as budgetedManhour" | |||
| // + " from team t2" | |||
| // + " left join project p2 on p2.teamLead = t2.teamLead" | |||
| // + " where p2.teamLead in (:teamIds)" | |||
| // + " ) as project on 1 = 1" | |||
| // + " where p.teamLead in (:teamIds)" | |||
| // + " and p.status not in ('Pending to Start','Completed','Deleted')" | |||
| // + " group by te.code, s.name, project.budgetedManhour" | |||
| "select" | |||
| + " ROW_NUMBER() OVER (ORDER BY p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone) AS id," | |||
| + " p.id as id," | |||
| + " p.id as projectId," | |||
| + " p.code as projectCode," | |||
| + " p.name as projectName," | |||
| + " te.code as team," | |||
| + " s.name as teamLead," | |||
| + " GROUP_CONCAT(DISTINCT tg.name ORDER BY tg.name) as expectedStage," | |||
| + " p.totalManhour as budgetedManhour," | |||
| + " COALESCE (sum(t.normalConsumed) + sum(t.otConsumed),0) as spentManhour," | |||
| + " COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) as remainedManhour," | |||
| + " coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) as manhourConsumptionPercentage," | |||
| + " COALESCE (DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d'),'NA') as comingPaymentMilestone," | |||
| + " case" | |||
| + " when COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) > 0 then 0" | |||
| + " when COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) <= 0 then 1" | |||
| + " end as alert" | |||
| + " from project p" | |||
| + " left join project_task pt on p.id = pt.project_id" | |||
| + " left join timesheet t on pt.id = t.projectTaskId" | |||
| + " left join team te on p.teamLead = te.teamLead" | |||
| + " left join staff s on te.teamLead = s.id" | |||
| + " left join milestone m on p.id = m.projectId and curdate() >= m.startDate and curdate() <= m.endDate" | |||
| + " left join task_group tg on m.taskGroupId = tg.id" | |||
| + " left join (" | |||
| + " SELECT pid, MIN(comingPaymentMilestone) AS comingPaymentMilestone" | |||
| + " FROM (" | |||
| + " SELECT p.id AS pid, mp.date AS comingPaymentMilestone" | |||
| + " FROM project p" | |||
| + " LEFT JOIN milestone m ON p.id = m.projectId" | |||
| + " LEFT JOIN milestone_payment mp ON m.id = mp.milestoneId" | |||
| + " WHERE p.teamLead in (:teamIds)" | |||
| + " AND p.status NOT IN ('Pending to Start', 'Completed', 'Deleted')" | |||
| + " AND mp.date >= CURDATE()" | |||
| + " ) AS subquery" | |||
| + " GROUP BY pid" | |||
| + " ORDER BY comingPaymentMilestone ASC" | |||
| + " ) milestonePayment on milestonePayment.pid = p.id" | |||
| + " where p.teamLead in (:teamIds)" | |||
| + " and p.status not in ('Pending to Start','Completed','Deleted')" | |||
| + " group by p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone" | |||
| ) | |||
| // if (args["tableSorting"] == "ProjectName") { | |||
| // sql.append(" ORDER BY te.code ASC") | |||
| // } else if (args["tableSorting"] == "PercentageASC") { | |||
| // sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/project.budgetedManhour)*100,2),0) asc") | |||
| // } else if (args["tableSorting"] == "PercentageDESC") { | |||
| // sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/project.budgetedManhour)*100,2),0) desc") | |||
| // } | |||
| if (args["tableSorting"] == "ProjectName") { | |||
| sql.append(" ORDER BY p.name ASC") | |||
| } else if (args["tableSorting"] == "PercentageASC") { | |||
| sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) asc") | |||
| } else if (args["tableSorting"] == "PercentageDESC") { | |||
| sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) desc") | |||
| } | |||
| return jdbcDao.queryForList(sql.toString(), args) | |||
| } | |||
| fun searchFinancialSummaryCard(args: Map<String, Any>): List<Map<String, Any>> { | |||
| 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<String, Any>): List<Map<String, Any>> { | |||
| val sql = StringBuilder( "select" | |||
| + " ROW_NUMBER() OVER (ORDER BY t.id, c.id) AS id," | |||
| // val sql = StringBuilder( "select" | |||
| // + " ROW_NUMBER() OVER (ORDER BY t.id, c.id) AS id," | |||
| // + " t.id as teamId," | |||
| // + " c.id as cid," | |||
| // + " c.code as customerCode," | |||
| // + " c.name as customerName," | |||
| // + " count(p.name) as projectNo," | |||
| // + " sum(p.expectedTotalFee) as totalFee," | |||
| // + " round(sum(p.expectedTotalFee) * 0.8,2) as totalBudget," | |||
| // + " COALESCE(round(expenditure.cumulativeExpenditure,2),0) as cumulativeExpenditure," | |||
| // + " coalesce(sum(i.issueAmount),0) as totalInvoiced," | |||
| // + " coalesce(sum(i.paidAmount),0) as totalReceived," | |||
| // + " case" | |||
| // + " when coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) >= 1 then 'Positive'" | |||
| // + " when coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) < 1 then 'Negative'" | |||
| // + " end as cashFlowStatus," | |||
| // + " coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) as cpi," | |||
| // + " case" | |||
| // + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) <= COALESCE(expenditure.cumulativeExpenditure,0) then coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) - coalesce(sum(i.issueAmount),0)" | |||
| // + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) < coalesce(sum(i.issueAmount),0) then 0" | |||
| // + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) >= coalesce(sum(i.issueAmount),0) then COALESCE(expenditure.cumulativeExpenditure,0) - coalesce(sum(i.issueAmount),0)" | |||
| // + " end as totalUninvoiced" | |||
| // + " from team t" | |||
| // + " left join project p on t.teamLead = p.teamLead" | |||
| // + " left join customer c on p.customerId = c.id" | |||
| // + " left join (" | |||
| // + " select" | |||
| // + " t3.id as tid," | |||
| // + " c3.id as cid," | |||
| // + " sum(i3.issueAmount) as issueAmount," | |||
| // + " sum(i3.paidAmount) as paidAmount" | |||
| // + " from team t3" | |||
| // + " left join project p3 on t3.teamLead = p3.teamLead" | |||
| // + " left join customer c3 on p3.customerId = c3.id" | |||
| // + " left join invoice i3 on p3.code = i3.projectCode" | |||
| // + " where t3.deleted = 0" | |||
| // + " and p3.status = 'On-going'" | |||
| // | |||
| // ) | |||
| // | |||
| // if (args != null) { | |||
| // if (args.containsKey("teamId")) | |||
| // sql.append(" AND t3.id = :teamId"); | |||
| // } | |||
| // sql.append( " group by c3.id, t3.id" | |||
| // + " ) as i on i.cid = c.id and i.tid = t.id" | |||
| // + " left join (" | |||
| // + " select" | |||
| // + " r.teamId as teamId," | |||
| // + " r.customerId as customerId," | |||
| // + " sum(r.cumulativeExpenditure) as cumulativeExpenditure" | |||
| // + " from (" | |||
| // + " select" | |||
| // + " t1.id as teamId," | |||
| // + " c2.id as customerId," | |||
| // + " (coalesce(sum(t2.normalConsumed),0) * s2.hourlyRate) + (coalesce(sum(t2.otConsumed),0) * s2.hourlyRate * 1.0) as cumulativeExpenditure," | |||
| // + " s2.hourlyRate as hourlyRate," | |||
| // + " sum(t2.normalConsumed) as normalConsumed," | |||
| // + " sum(t2.otConsumed) as otConsumed" | |||
| // + " from team t1" | |||
| // + " left join project p2 on t1.teamLead = p2.teamLead" | |||
| // + " left join customer c2 on p2.customerId = c2 .id" | |||
| // + " left join project_task pt ON p2.id = pt.project_id" | |||
| // + " left join timesheet t2 on pt.id = t2.projectTaskId" | |||
| // + " left join staff s on t2.staffId = s.id" | |||
| // + " left join salary s2 on s.salaryId = s2.salaryPoint" | |||
| // + " where p2.status = 'On-going'" | |||
| // + " and t2.id is not null" | |||
| // + " group by s2.hourlyRate,t1.id,c2.id" | |||
| // + " ) as r" | |||
| // + " group by r.teamId, r.customerId" | |||
| // + " ) as expenditure on expenditure.teamId = t.id and expenditure.customerId = c.id" | |||
| // + " where t.deleted = 0" | |||
| // + " and p.status = 'On-going'") | |||
| // | |||
| // if (args != null) { | |||
| // if (args.containsKey("teamId")) | |||
| // sql.append(" AND t.id = :teamId"); | |||
| // } | |||
| // sql.append(" group by t.id, c.id, c.code, c.name") | |||
| val sql = StringBuilder( | |||
| "select" | |||
| + " ROW_NUMBER() OVER (ORDER BY t.id, i.cid) AS id," | |||
| + " t.id as teamId," | |||
| + " 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<String, Any>): List<Map<String, Any>> { | |||
| 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<String, Any>): List<Map<String, Any>> { | |||
| 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" | |||
| ) | |||
| @@ -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) | |||
| @@ -51,6 +51,7 @@ class DashboardController( | |||
| fun searchCustomerSubsidiaryProject(request: HttpServletRequest?): List<Map<String, Any>> { | |||
| val customerId = request?.getParameter("customerId") | |||
| val subsidiaryId = request?.getParameter("subsidiaryId") | |||
| val tableSorting = request?.getParameter("tableSorting") | |||
| val args = mutableMapOf<String, Any>() | |||
| var result: List<Map<String, Any>> = 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<Map<String, Any>> { | |||
| val teamLeadId = request?.getParameter("teamLeadId") | |||
| val tableSorting = request?.getParameter("tableSorting") | |||
| val args = mutableMapOf<String, Any>() | |||
| var result: List<Map<String, Any>> = 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<Map<String, Any>> { | |||
| val args = mutableMapOf<String, Any>() | |||
| val teamIdList = request?.getParameter("teamIdList") | |||
| val tableSorting = request?.getParameter("tableSorting") | |||
| val teamIds = teamIdList?.split(",")?.map { it.toInt() }?.toList() | |||
| var result: List<Map<String, Any>> = emptyList() | |||
| if (teamIds != null) { | |||
| args["teamIds"] = teamIds | |||
| } | |||
| if (tableSorting != null) { | |||
| args["tableSorting"] = tableSorting | |||
| } | |||
| result = dashboardService.searchTeamConsumption(args) | |||
| return result | |||
| } | |||
| @GetMapping("/searchFinancialSummaryCard") | |||
| fun searchFinancialSummaryCard(request: HttpServletRequest?): List<Map<String, Any>> { | |||
| val authority = dashboardService.viewDashboardAuthority() | |||
| @@ -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) | |||
| } | |||
| @@ -12,4 +12,6 @@ interface InvoiceRepository : AbstractRepository<Invoice, Long> { | |||
| fun findInvoiceInfoByPaidAmountIsNotNull(): List<InvoiceInfo> | |||
| fun findByInvoiceNo(invoiceNo: String): Invoice | |||
| fun findAllByProjectCodeAndPaidAmountIsNotNull(projectCode: String): List<Invoice> | |||
| } | |||
| @@ -77,6 +77,9 @@ open class Project : BaseEntity<Long>() { | |||
| @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 | |||
| @@ -34,4 +34,6 @@ interface ProjectRepository : AbstractRepository<Project, Long> { | |||
| fun findAllByStatusIsNotAndMainProjectIsNull(status: String): List<Project> | |||
| fun findAllByTeamLeadAndCustomer(teamLead: Staff, customer: Customer): List<Project> | |||
| fun findByCode(code: String): Project? | |||
| } | |||
| @@ -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) | |||
| @@ -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<ProjectSearchInfo> { | |||
| 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..<sheet.lastRowNum) { | |||
| val row = sheet.getRow(i) | |||
| if (row?.getCell(0) != null && !row.getCell(0).stringCellValue.isNullOrBlank()) { | |||
| logger.info("row :$i | lastCellNum" + row.lastCellNum) | |||
| // process client | |||
| logger.info("---------client-------") | |||
| logger.info(customerRepository.findByName(row.getCell(5).stringCellValue.trim())) | |||
| val tempClient = customerRepository.findByName(row.getCell(5).stringCellValue.trim()) | |||
| val currentClient = | |||
| if (tempClient.isPresent) tempClient.orElseThrow() else customerService.saveCustomer( | |||
| SaveCustomerRequest( | |||
| code = customerService.createClientCode(), | |||
| name = ExcelUtils.getStringValue(row.getCell(5)), | |||
| typeId = ExcelUtils.getIntValue(row.getCell(6)).toLong(), | |||
| deleteSubsidiaryIds = mutableListOf(), | |||
| deleteContactIds = mutableListOf(), | |||
| addSubsidiaryIds = mutableListOf(), | |||
| addContacts = mutableListOf(), | |||
| address = null, | |||
| brNo = null, | |||
| id = null, | |||
| district = null, | |||
| ) | |||
| ).customer | |||
| val clientId = currentClient.id!! | |||
| logger.info("customer: $clientId") | |||
| // process project | |||
| logger.info("---------project-------") | |||
| val projectTeamLead = teamRepository.findByCode(ExcelUtils.getStringValue(row.getCell(4))).staff | |||
| var mainProjectId: Long? = null | |||
| var projectCode = StringBuilder(row.getCell(0).stringCellValue).insert(1, '-').toString() | |||
| if (projectCode.contains('(')) { | |||
| val splitProjectCode = projectCode.split('(') | |||
| val splitMainProjectCode = splitProjectCode[0].split('-') | |||
| logger.info("splitProjectCode: " + splitProjectCode[1].split(')')[0]) | |||
| projectCode = | |||
| splitMainProjectCode[0] + '-' + String.format( | |||
| "%04d", | |||
| splitMainProjectCode[1].toInt() | |||
| ) + '-' + String.format("%03d", splitProjectCode[1].split(')')[0].toInt()) | |||
| val mainProject = | |||
| projectRepository.findByCode(splitProjectCode[0]) ?: projectRepository.saveAndFlush( | |||
| Project().apply { | |||
| name = row.getCell(1).stringCellValue | |||
| description = row.getCell(1).stringCellValue | |||
| code = splitMainProjectCode[0] + '-' + String.format( | |||
| "%04d", | |||
| splitMainProjectCode[1].toInt() | |||
| ) | |||
| status = "Completed" | |||
| projectCategory = projectCategoryRepository.findById(1).orElseThrow() | |||
| customer = currentClient | |||
| teamLead = projectTeamLead | |||
| }) | |||
| mainProjectId = mainProject.id | |||
| } else { | |||
| val splitProjectCode = projectCode.split('-') | |||
| projectCode = splitProjectCode[0] + '-' + String.format("%04d", splitProjectCode[1].toInt()) | |||
| } | |||
| val project = projectRepository.findByCode(projectCode) | |||
| val projectId = project?.id | |||
| logger.info("projectCode :$projectCode") | |||
| // process subsidiary | |||
| logger.info("---------subsidiary-------") | |||
| val subsidiary = if (row.getCell(7) != null && !row.getCell(7).stringCellValue.isNullOrBlank()) { | |||
| val tempSubsidiary = subsidiaryRepository.findByName(ExcelUtils.getStringValue(row.getCell(7))) | |||
| if (tempSubsidiary.isPresent) { | |||
| tempSubsidiary.orElseThrow() | |||
| } else { | |||
| subsidiaryRepository.findByName(ExcelUtils.getStringValue(row.getCell(7))).orElse( | |||
| subsidiaryService.saveSubsidiary( | |||
| SaveSubsidiaryRequest( | |||
| code = subsidiaryService.createSubsidiaryCode(), | |||
| name = ExcelUtils.getStringValue(row.getCell(7)), | |||
| typeId = ExcelUtils.getIntValue(row.getCell(8)).toLong(), | |||
| brNo = null, | |||
| address = null, | |||
| district = null, | |||
| deleteContactIds = mutableListOf(), | |||
| addContacts = mutableListOf(), | |||
| deleteCustomerIds = mutableListOf(), | |||
| addCustomerIds = mutableListOf(clientId), | |||
| id = null | |||
| ) | |||
| ).subsidiary | |||
| ) | |||
| } | |||
| } else null | |||
| if (subsidiary != null) { | |||
| if (!customerSubsidiaryService.findAllCustomerIdsBySubsidiaryId(subsidiary.id!!) | |||
| .contains(clientId) | |||
| ) { | |||
| subsidiaryService.saveSubsidiary( | |||
| SaveSubsidiaryRequest( | |||
| code = subsidiary.code, | |||
| name = subsidiary.name, | |||
| typeId = subsidiary.subsidiaryType.id!!, | |||
| brNo = subsidiary.brNo, | |||
| address = subsidiary.address, | |||
| district = subsidiary.district, | |||
| deleteContactIds = mutableListOf(), | |||
| addContacts = mutableListOf(), | |||
| deleteCustomerIds = mutableListOf(), | |||
| addCustomerIds = mutableListOf(clientId), | |||
| id = subsidiary.id | |||
| ) | |||
| ) | |||
| } | |||
| } | |||
| // process building type | |||
| logger.info("---------building type-------") | |||
| var buildingType = buildingTypeRepository.findByName(row.getCell(13).stringCellValue) | |||
| if (buildingType == null) { | |||
| buildingType = | |||
| buildingTypeRepository.saveAndFlush(BuildingType().apply { | |||
| name = row.getCell(13).stringCellValue | |||
| }) | |||
| } | |||
| // process work nature | |||
| logger.info("---------work nature-------") | |||
| var workNature = workNatureRepository.findByName(row.getCell(14).stringCellValue) | |||
| if (workNature == null) { | |||
| workNature = | |||
| workNatureRepository.saveAndFlush(WorkNature().apply { name = row.getCell(14).stringCellValue }) | |||
| } | |||
| // process staff - staff from column R (17) | |||
| logger.info("---------staff-------") | |||
| val allocatedStaffIds = mutableListOf<Long>() | |||
| for (j in 18..<row.lastCellNum step 2) { | |||
| logger.info("j: $j | staffId: " + row.getCell(j).stringCellValue) | |||
| val tempStaffId = row.getCell(j).stringCellValue | |||
| if (!tempStaffId.isNullOrBlank()) { | |||
| allocatedStaffIds.add(staffRepository.findByStaffId(tempStaffId).orElseThrow().id!!) | |||
| } | |||
| } | |||
| // get task template - TW-Full QS - stage 5 | |||
| logger.info("---------task template-------") | |||
| val taskTemplate = taskTemplateRepository.findById(3).orElseThrow() | |||
| val tasks = taskTemplate.tasks.groupBy { it.taskGroup!!.id } | |||
| val groups = taskTemplate.groupAllocations.associateBy( | |||
| keySelector = { it.taskGroup!!.id!! }, | |||
| valueTransform = { it.percentage!! } | |||
| ) | |||
| val projectTasks = if (project != null) projectTaskRepository.findAllByProject(project) else mutableListOf() | |||
| var groupedProjectTasks = mapOf<Long, List<Long>>() | |||
| if (projectTasks.isNotEmpty()) { | |||
| groupedProjectTasks = projectTasks.groupBy { | |||
| it.task!!.taskGroup!!.id!! | |||
| }.mapValues { (_, projectTask) -> | |||
| projectTask.map { it.task!!.id!! } | |||
| } | |||
| } | |||
| val taskGroups = mutableMapOf<Long, TaskGroupAllocation>() | |||
| groups.entries.forEach{ (key, value) -> | |||
| var taskIds = tasks[key]!!.map { it.id!! } | |||
| if (groupedProjectTasks.isNotEmpty() && !groupedProjectTasks[key].isNullOrEmpty()) { | |||
| taskIds = (taskIds + groupedProjectTasks[key]!!).distinct() | |||
| } | |||
| if (key == 5L) { | |||
| taskGroups[key] = TaskGroupAllocation(taskIds.plus(41).distinct(), value) | |||
| logger.info("taskGroups[${key}]: ${taskGroups[key]}") | |||
| } else { | |||
| taskGroups[key] = TaskGroupAllocation(taskIds, value) | |||
| logger.info("taskGroups[${key}]: ${taskGroups[key]}") | |||
| } | |||
| } | |||
| logger.info("---------Import-------") | |||
| val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") | |||
| val startDate = ExcelUtils.getDateValue( | |||
| row.getCell(2), | |||
| DateTimeFormatter.ofPattern("MM/dd/yyyy") | |||
| ).format(formatter) | |||
| val endDate = ExcelUtils.getDateValue( | |||
| row.getCell(3), | |||
| DateTimeFormatter.ofPattern("MM/dd/yyyy") | |||
| ).format(formatter) | |||
| saveProject( | |||
| NewProjectRequest( | |||
| projectCode = projectCode, | |||
| projectName = row.getCell(1).stringCellValue, | |||
| projectDescription = row.getCell(1).stringCellValue, | |||
| projectActualStart = ExcelUtils.getDateValue( | |||
| row.getCell(2), | |||
| DateTimeFormatter.ofPattern("MM/dd/yyyy") | |||
| ), | |||
| projectPlanEnd = ExcelUtils.getDateValue( | |||
| row.getCell(3), | |||
| DateTimeFormatter.ofPattern("MM/dd/yyyy") | |||
| ), | |||
| projectLeadId = projectTeamLead.id!!, | |||
| clientId = clientId, | |||
| clientSubsidiaryId = subsidiary?.id, | |||
| expectedProjectFee = row.getCell(9).numericCellValue, | |||
| subContractFee = null, | |||
| totalManhour = row.getCell(11).numericCellValue, | |||
| locationId = 1, // HK | |||
| buildingTypeIds = mutableListOf(buildingType!!.id!!), | |||
| workNatureIds = mutableListOf(workNature!!.id!!), | |||
| serviceTypeId = 9, | |||
| fundingTypeId = row.getCell(16).numericCellValue.toLong(), | |||
| allocatedStaffIds = allocatedStaffIds, | |||
| isClpProject = projectCode[0] != 'M', | |||
| mainProjectId = mainProjectId, | |||
| projectCategoryId = 1, | |||
| contractTypeId = if (row.getCell(14).stringCellValue == "Maintenance Term Contract") 5 else 2, | |||
| projectId = projectId, | |||
| projectStatus = "On-going", | |||
| clientContactId = null, | |||
| isSubsidiaryContact = subsidiary?.id != null && subsidiary.id!! > 0, | |||
| manhourPercentageByGrade = taskTemplate.gradeAllocations.associateBy( | |||
| keySelector = { it.grade!!.id!! }, | |||
| valueTransform = { it.percentage!! } | |||
| ), | |||
| projectActualEnd = null, | |||
| taskTemplateId = taskTemplate.id, | |||
| taskGroups = taskGroups, | |||
| milestones = mapOf( | |||
| Pair( | |||
| 1, com.ffii.tsms.modules.project.web.models.Milestone( | |||
| startDate = startDate, | |||
| endDate = endDate, | |||
| payments = mutableListOf() | |||
| ) | |||
| ), | |||
| Pair( | |||
| 2, com.ffii.tsms.modules.project.web.models.Milestone( | |||
| startDate = startDate, | |||
| endDate = endDate, | |||
| payments = mutableListOf() | |||
| ) | |||
| ), | |||
| Pair( | |||
| 3, com.ffii.tsms.modules.project.web.models.Milestone( | |||
| startDate = startDate, | |||
| endDate = endDate, | |||
| payments = mutableListOf() | |||
| ) | |||
| ), | |||
| Pair( | |||
| 4, com.ffii.tsms.modules.project.web.models.Milestone( | |||
| startDate = startDate, | |||
| endDate = endDate, | |||
| payments = mutableListOf() | |||
| ) | |||
| ), | |||
| Pair( | |||
| 5, com.ffii.tsms.modules.project.web.models.Milestone( | |||
| startDate = startDate, | |||
| endDate = endDate, | |||
| payments = mutableListOf( | |||
| PaymentInputs( | |||
| -1, "Manhour Import", ExcelUtils.getDateValue( | |||
| row.getCell(2), | |||
| DateTimeFormatter.ofPattern("MM/dd/yyyy") | |||
| ).toString(), row.getCell(9).numericCellValue | |||
| ) | |||
| ) | |||
| ) | |||
| ), | |||
| )) | |||
| ) | |||
| } | |||
| } | |||
| return if (sheet.lastRowNum > 0) "Import Excel success" else "Import Excel failure" | |||
| } | |||
| } | |||
| @@ -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<WorkNature> { | |||
| 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)) | |||
| } | |||
| } | |||
| @@ -40,5 +40,6 @@ data class EditProjectDetails( | |||
| val milestones: Map<Long, Milestone>, | |||
| // Miscellaneous | |||
| val expectedProjectFee: Double? | |||
| val expectedProjectFee: Double?, | |||
| val subContractFee: Double? | |||
| ) | |||
| @@ -27,4 +27,5 @@ data class MainProjectDetails ( | |||
| val clientSubsidiaryId: Long?, | |||
| val expectedProjectFee: Double?, | |||
| val subContractFee: Double?, | |||
| ) | |||
| @@ -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<Long, Milestone>, | |||
| // Miscellaneous | |||
| val expectedProjectFee: Double | |||
| val expectedProjectFee: Double, | |||
| val subContractFee: Double? | |||
| ) | |||
| data class TaskGroupAllocation( | |||
| @@ -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<Timesheet>, | |||
| teams: List<Team>, | |||
| grades: List<Grade>, | |||
| ): ByteArray { | |||
| // Generate the Excel report with query results | |||
| val workbook: Workbook = | |||
| createCrossTeamChargeReport(month, timesheets, teams, grades, CROSS_TEAM_CHARGE_REPORT) | |||
| // Write the workbook to a ByteArrayOutputStream | |||
| val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() | |||
| workbook.write(outputStream) | |||
| workbook.close() | |||
| return outputStream.toByteArray() | |||
| } | |||
| // ==============================|| CREATE REPORT ||============================== // | |||
| // 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}<L${rowNum}), 0, IF(AND(I${rowNum}>J${rowNum}, J${rowNum}>=L${rowNum}), J${rowNum}-L${rowNum}, 0))) " | |||
| " IF(H${rowNum}-L${rowNum}<0, 0, H${rowNum}-L${rowNum})" | |||
| // " IF(I${rowNum}<=J${rowNum}, I${rowNum}-L${rowNum}, IF(AND(I${rowNum}>J${rowNum}, J${rowNum}<L${rowNum}), 0, IF(AND(I${rowNum}>J${rowNum}, J${rowNum}>=L${rowNum}), J${rowNum}-L${rowNum}, 0))) " | |||
| cellStyle.dataFormat = accountingStyle | |||
| } | |||
| @@ -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<String, Any>): List<Map<String, Any>> { | |||
| val sql = StringBuilder("select" | |||
| + " result.code, " | |||
| + " result.name, " | |||
| + " result.teamCode, " | |||
| + " result.custCode, " | |||
| + " result.subCode, " ) | |||
| val sql = StringBuilder( | |||
| "select" | |||
| + " result.code, " | |||
| + " result.name, " | |||
| + " result.teamCode, " | |||
| + " result.custCode, " | |||
| + " result.subCode, " | |||
| ) | |||
| if (args.get("outstanding") as Boolean) { | |||
| 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<String, Any>): List<Map<String, Any>> { | |||
| val sql = StringBuilder("WITH teamNormalConsumed AS (" | |||
| + " SELECT " | |||
| + " s.teamId, " | |||
| + " pt.project_id, " | |||
| + " SUM(tns.totalConsumed) AS totalConsumed, " | |||
| + " sum(tns.totalBudget) as totalBudget " | |||
| + " FROM ( " | |||
| + " SELECT " | |||
| + " t.staffId, " | |||
| + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, " | |||
| + " t.projectTaskId AS taskId, " | |||
| + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) * min(sal.hourlyRate) as totalBudget " | |||
| + " FROM timesheet t " | |||
| + " LEFT JOIN staff s ON t.staffId = s.id " | |||
| + " left join salary sal on sal.salaryPoint = s.salaryId " | |||
| + " GROUP BY t.staffId, t.projectTaskId " | |||
| + " ) AS tns " | |||
| + " INNER JOIN project_task pt ON tns.taskId = pt.id " | |||
| + " JOIN staff s ON tns.staffId = s.id " | |||
| + " JOIN team t ON s.teamId = t.id " | |||
| + " GROUP BY teamId, project_id " | |||
| + " ) " | |||
| + " SELECT " | |||
| + " p.code, " | |||
| + " p.name, " | |||
| + " t.code as team, " | |||
| + " concat(c.code, ' - ', c.name) as client, " | |||
| + " COALESCE(concat(ss.code, ' - ', ss.name), 'N/A') as subsidiary, " | |||
| + " p.expectedTotalFee * 0.8 as plannedBudget, " | |||
| + " COALESCE(tns.totalBudget, 0) as actualConsumedBudget, " | |||
| + " COALESCE(p.totalManhour, 0) as plannedManhour, " | |||
| + " COALESCE(tns.totalConsumed, 0) as actualConsumedManhour, " | |||
| + " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) as budgetConsumptionRate, " | |||
| + " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) as manhourConsumptionRate, " | |||
| + " CASE " | |||
| + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " | |||
| + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 " | |||
| + " then 'Potential Overconsumption' " | |||
| + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " | |||
| + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1 " | |||
| + " then 'Overconsumption' " | |||
| + " else 'Within Budget' " | |||
| + " END as status " | |||
| + " FROM project p " | |||
| + " LEFT JOIN team t ON p.teamLead = t.teamLead " | |||
| + " LEFT JOIN staff s ON p.teamLead = s.id " | |||
| + " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint " | |||
| + " LEFT JOIN customer c ON p.customerId = c.id " | |||
| + " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id " | |||
| + " left join teamNormalConsumed tns on tns.project_id = p.id " | |||
| + " WHERE p.deleted = false " | |||
| + " and p.status = 'On-going' " | |||
| val sql = StringBuilder( | |||
| "WITH teamNormalConsumed AS (" | |||
| + " SELECT" | |||
| + " tns.project_id," | |||
| + " SUM(tns.totalConsumed) AS totalConsumed, " | |||
| + " sum(tns.totalBudget) as totalBudget " | |||
| + " FROM ( " | |||
| + " SELECT" | |||
| + " t.staffId," | |||
| + " t.projectId AS project_id," | |||
| + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, " | |||
| + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) * min(sal.hourlyRate) as totalBudget " | |||
| + " FROM timesheet t" | |||
| + " LEFT JOIN staff s ON t.staffId = s.id " | |||
| + " left join salary sal on sal.salaryPoint = s.salaryId " | |||
| + " GROUP BY t.staffId, t.projectId" | |||
| + " ) AS tns" | |||
| + " GROUP BY project_id" | |||
| + " ) " | |||
| + " SELECT " | |||
| + " p.code, " | |||
| + " p.name, " | |||
| + " t.code as team, " | |||
| + " concat(c.code, ' - ', c.name) as client, " | |||
| + " COALESCE(concat(ss.code, ' - ', ss.name), 'N/A') as subsidiary, " | |||
| + " p.expectedTotalFee * 0.8 as plannedBudget, " | |||
| + " COALESCE(tns.totalBudget, 0) as actualConsumedBudget, " | |||
| + " COALESCE(p.totalManhour, 0) as plannedManhour, " | |||
| + " COALESCE(tns.totalConsumed, 0) as actualConsumedManhour, " | |||
| + " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) as budgetConsumptionRate, " | |||
| + " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) as manhourConsumptionRate, " | |||
| + " CASE " | |||
| + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " | |||
| + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1 " | |||
| + " then 'Overconsumption' " | |||
| + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " | |||
| + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 " | |||
| + " then 'Potential Overconsumption' " | |||
| + " else 'Within Budget' " | |||
| + " END as status " | |||
| + " FROM project p " | |||
| + " LEFT JOIN team t ON p.teamLead = t.teamLead " | |||
| + " LEFT JOIN staff s ON p.teamLead = s.id " | |||
| + " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint " | |||
| + " LEFT JOIN customer c ON p.customerId = c.id " | |||
| + " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id " | |||
| + " left join teamNormalConsumed tns on tns.project_id = p.id " | |||
| + " WHERE p.deleted = false " | |||
| + " and p.status = 'On-going' " | |||
| ) | |||
| if (args != null) { | |||
| 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<Map<String,Any?>>{ | |||
| fun getCostAndExpense(clientId: Long?, teamId: Long?, type: String): List<Map<String, Any?>> { | |||
| 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<Map<String, Any?>>() | |||
| 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<Map<String, Any?>> | |||
| 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<Map<String, Any>> { | |||
| val sql = StringBuilder(""" | |||
| open fun getLateStartDetails( | |||
| teamId: Long?, | |||
| clientId: Long?, | |||
| remainedDate: LocalDate, | |||
| remainedDateTo: LocalDate, | |||
| type: String? | |||
| ): List<Map<String, Any>> { | |||
| val sql = StringBuilder( | |||
| """ | |||
| SELECT | |||
| 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<Timesheet>, | |||
| teams: List<Team>, | |||
| grades: List<Grade>, | |||
| templatePath: String, | |||
| ): Workbook { | |||
| // please create a new function for each report template | |||
| val resource = ClassPathResource(templatePath) | |||
| val templateInputStream = resource.inputStream | |||
| val workbook: Workbook = XSSFWorkbook(templateInputStream) | |||
| val sheet: Sheet = workbook.getSheetAt(0) | |||
| // accounting style + comma style | |||
| val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") | |||
| // bold font with border style | |||
| val boldFont = workbook.createFont().apply { | |||
| bold = true | |||
| fontName = "Times New Roman" | |||
| } | |||
| val boldFontWithBorderStyle = workbook.createCellStyle().apply { | |||
| setFont(boldFont) | |||
| borderTop = BorderStyle.THIN | |||
| borderBottom = BorderStyle.THIN | |||
| borderLeft = BorderStyle.THIN | |||
| borderRight = BorderStyle.THIN | |||
| } | |||
| // normal font | |||
| val normalFont = workbook.createFont().apply { | |||
| fontName = "Times New Roman" | |||
| } | |||
| val normalFontWithBorderStyle = workbook.createCellStyle().apply { | |||
| setFont(normalFont) | |||
| borderTop = BorderStyle.THIN | |||
| borderBottom = BorderStyle.THIN | |||
| borderLeft = BorderStyle.THIN | |||
| borderRight = BorderStyle.THIN | |||
| } | |||
| var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field | |||
| var columnIndex = 2 | |||
| val monthFormat = DateTimeFormatter.ofPattern("MMMM yyyy", Locale.ENGLISH) | |||
| val reportMonth = YearMonth.parse(month, DateTimeFormatter.ofPattern("yyyy-MM")) | |||
| val convertReportMonth = YearMonth.of(reportMonth.year, reportMonth.month).format(monthFormat) | |||
| sheet.getRow(rowIndex).createCell(columnIndex).apply { | |||
| setCellValue(convertReportMonth) | |||
| } | |||
| // if (timesheets.isNotEmpty()) { | |||
| val combinedTeamCodeColNumber = grades.size | |||
| val sortedGrades = grades.sortedBy { it.id } | |||
| val sortedTeams = teams.sortedBy { it.id } | |||
| val groupedTimesheets = timesheets | |||
| .filter { it.project?.teamLead?.team?.id != it.staff?.team?.id } | |||
| .groupBy { timesheetEntry -> | |||
| Triple( | |||
| timesheetEntry.project?.teamLead?.team?.id, | |||
| timesheetEntry.staff?.team?.id, | |||
| timesheetEntry.staff?.grade?.id | |||
| ) | |||
| } | |||
| .mapValues { (_, timesheetEntries) -> | |||
| timesheetEntries.map { timesheet -> | |||
| if (timesheet.normalConsumed != null) { | |||
| mutableMapOf<String, Double>().apply { | |||
| this["manHour"] = timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0) | |||
| this["salary"] = timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0) | |||
| .times(timesheet.staff!!.salary.hourlyRate.toDouble()) | |||
| } | |||
| } else if (timesheet.otConsumed != null) { | |||
| mutableMapOf<String, Double>().apply { | |||
| this["manHour"] = timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0) | |||
| this["salary"] = timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0) | |||
| .times(timesheet.staff!!.salary.hourlyRate.toDouble()) | |||
| } | |||
| } else { | |||
| mutableMapOf<String, Double>().apply { | |||
| this["manHour"] = 0.0 | |||
| this["salary"] = 0.0 | |||
| } | |||
| } | |||
| } | |||
| } | |||
| if (sortedTeams.isNotEmpty() && sortedTeams.size > 1) { | |||
| rowIndex = 3 | |||
| sortedTeams.forEach { team: Team -> | |||
| // Team | |||
| sheet.createRow(rowIndex++).apply { | |||
| createCell(0).apply { | |||
| setCellValue("Team to be charged:") | |||
| cellStyle = boldFontWithBorderStyle | |||
| CellUtil.setAlignment(this, HorizontalAlignment.LEFT) | |||
| } | |||
| val rangeAddress = CellRangeAddress(this.rowNum, this.rowNum, 1, 2 + combinedTeamCodeColNumber) | |||
| sheet.addMergedRegion(rangeAddress) | |||
| RegionUtil.setBorderTop(BorderStyle.THIN, rangeAddress, sheet) | |||
| RegionUtil.setBorderLeft(BorderStyle.THIN, rangeAddress, sheet) | |||
| RegionUtil.setBorderRight(BorderStyle.THIN, rangeAddress, sheet) | |||
| RegionUtil.setBorderBottom(BorderStyle.THIN, rangeAddress, sheet) | |||
| createCell(1).apply { | |||
| setCellValue(team.code) | |||
| cellStyle = normalFontWithBorderStyle | |||
| CellUtil.setAlignment(this, HorizontalAlignment.CENTER) | |||
| } | |||
| } | |||
| // Grades | |||
| sheet.createRow(rowIndex++).apply { | |||
| columnIndex = 0 | |||
| createCell(columnIndex++).apply { | |||
| setCellValue("") | |||
| cellStyle = boldFontWithBorderStyle | |||
| } | |||
| sortedGrades.forEach { grade: Grade -> | |||
| createCell(columnIndex++).apply { | |||
| setCellValue(grade.name) | |||
| cellStyle = boldFontWithBorderStyle | |||
| CellUtil.setAlignment(this, HorizontalAlignment.CENTER) | |||
| } | |||
| } | |||
| createCell(columnIndex++).apply { | |||
| setCellValue("Total Manhour by Team") | |||
| cellStyle = boldFontWithBorderStyle | |||
| } | |||
| createCell(columnIndex).apply { | |||
| setCellValue("Total Cost Adjusted by Salary Point by Team") | |||
| cellStyle = boldFontWithBorderStyle | |||
| } | |||
| } | |||
| // Team + Manhour | |||
| val startRow = rowIndex | |||
| var endRow = rowIndex | |||
| sortedTeams.forEach { chargedTeam: Team -> | |||
| if (team.id != chargedTeam.id) { | |||
| endRow++ | |||
| sheet.createRow(rowIndex++).apply { | |||
| columnIndex = 0 | |||
| createCell(columnIndex++).apply { | |||
| setCellValue(chargedTeam.code) | |||
| cellStyle = normalFontWithBorderStyle | |||
| CellUtil.setAlignment(this, HorizontalAlignment.CENTER) | |||
| } | |||
| var totalSalary = 0.0 | |||
| sortedGrades.forEach { grade: Grade -> | |||
| createCell(columnIndex++).apply { | |||
| setCellValue( | |||
| groupedTimesheets[Triple( | |||
| team.id, | |||
| chargedTeam.id, | |||
| grade.id | |||
| )]?.sumOf { it.getValue("manHour") } ?: 0.0) | |||
| totalSalary += groupedTimesheets[Triple( | |||
| team.id, | |||
| chargedTeam.id, | |||
| grade.id | |||
| )]?.sumOf { it.getValue("salary") } ?: 0.0 | |||
| cellStyle = normalFontWithBorderStyle.apply { | |||
| dataFormat = accountingStyle | |||
| } | |||
| } | |||
| } | |||
| createCell(columnIndex++).apply { | |||
| val lastCellLetter = CellReference.convertNumToColString(this.columnIndex - 1) | |||
| cellFormula = "sum(B${this.rowIndex + 1}:${lastCellLetter}${this.rowIndex + 1})" | |||
| cellStyle = boldFontWithBorderStyle.apply { | |||
| dataFormat = accountingStyle | |||
| } | |||
| } | |||
| createCell(columnIndex).apply { | |||
| setCellValue(totalSalary) | |||
| cellStyle = boldFontWithBorderStyle.apply { | |||
| dataFormat = accountingStyle | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // Total Manhour by grade | |||
| sheet.createRow(rowIndex).apply { | |||
| columnIndex = 0 | |||
| createCell(columnIndex++).apply { | |||
| setCellValue("Total Manhour by Grade") | |||
| cellStyle = boldFontWithBorderStyle | |||
| } | |||
| sortedGrades.forEach { grade: Grade -> | |||
| createCell(columnIndex++).apply { | |||
| val currentCellLetter = CellReference.convertNumToColString(this.columnIndex) | |||
| cellFormula = "sum(${currentCellLetter}${startRow}:${currentCellLetter}${endRow})" | |||
| cellStyle = normalFontWithBorderStyle.apply { | |||
| dataFormat = accountingStyle | |||
| } | |||
| } | |||
| } | |||
| createCell(columnIndex++).apply { | |||
| setCellValue("") | |||
| cellStyle = boldFontWithBorderStyle | |||
| } | |||
| createCell(columnIndex).apply { | |||
| setCellValue("") | |||
| cellStyle = boldFontWithBorderStyle | |||
| } | |||
| } | |||
| rowIndex += 2 | |||
| } | |||
| } | |||
| // } | |||
| return workbook | |||
| } | |||
| } | |||
| @@ -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<Resource> { | |||
| @@ -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<Resource> { | |||
| val authorities = staffsService.currentAuthorities() ?: return ResponseEntity.noContent().build() | |||
| if (authorities.stream().anyMatch { it.authority.equals("GENERATE_REPORTS") }) { | |||
| val crossProjects = staffAllocationRepository.findAll() | |||
| .filter { it.project?.teamLead?.team?.id != it.staff?.team?.id } | |||
| .map { it.project!! } | |||
| .distinct() | |||
| val reportMonth = YearMonth.parse(request.month, DateTimeFormatter.ofPattern("yyyy-MM")) | |||
| val convertReportMonth = YearMonth.of(reportMonth.year, reportMonth.month) | |||
| val startDate = convertReportMonth.atDay(1) | |||
| val endDate = convertReportMonth.atEndOfMonth() | |||
| val timesheets = timesheetRepository.findAllByProjectIn(crossProjects) | |||
| .filter { it.recordDate != null && | |||
| (it.recordDate!!.isEqual(startDate) || it.recordDate!!.isEqual(endDate) || (it.recordDate!!.isAfter(startDate) && it.recordDate!!.isBefore(endDate)))} | |||
| val teams = teamRepository.findAll().filter { it.deleted == false } | |||
| val grades = gradeRepository.findAll().filter { it.deleted == false } | |||
| val reportResult: ByteArray = excelReportService.generateCrossTeamChargeReport(request.month, timesheets, teams, grades) | |||
| return ResponseEntity.ok() | |||
| .header("filename", "Cross Team Charge Report - " + LocalDate.now() + ".xlsx") | |||
| .body(ByteArrayResource(reportResult)) | |||
| } else { | |||
| return ResponseEntity.noContent().build() | |||
| } | |||
| } | |||
| } | |||
| @@ -57,4 +57,8 @@ data class ProjectCompletionReport ( | |||
| val startDate: LocalDate, | |||
| val endDate: LocalDate, | |||
| val outstanding: Boolean | |||
| ) | |||
| data class CrossTeamChargeReportRequest ( | |||
| val month: String, | |||
| ) | |||
| @@ -14,6 +14,8 @@ interface TimesheetRepository : AbstractRepository<Timesheet, Long> { | |||
| fun findAllByProjectTaskIn(projectTasks: List<ProjectTask>): List<Timesheet> | |||
| fun findAllByProjectIn(project: List<Project>): List<Timesheet> | |||
| 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") | |||
| @@ -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<String, List<TimeEntry>> { | |||
| open fun saveMemberTimeEntry( | |||
| staffId: Long, | |||
| entry: TimeEntry, | |||
| recordDate: LocalDate? | |||
| ): Map<String, List<TimeEntry>> { | |||
| 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<Long, TeamMemberTimeEntries> { | |||
| 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<Timesheet>): Map<String, List<TimeEntry>> { | |||
| 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<TimeEntry>): List<TimeEntry> { | |||
| 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<String>() | |||
| val sheet: Sheet = workbook.getSheetAt(0) | |||
| logger.info("---------Start Import Timesheets-------") | |||
| val timesheetList = mutableListOf<Timesheet>().toMutableList(); | |||
| for (i in 1..<sheet.lastRowNum) { | |||
| val row = sheet.getRow(i) | |||
| if (row?.getCell(0) != null && !row.getCell(0).stringCellValue.isNullOrBlank() && row.getCell(3) != null && !row.getCell( | |||
| 3 | |||
| ).stringCellValue.isNullOrBlank() | |||
| ) { | |||
| logger.info("row :$i | lastCellNum" + row.lastCellNum) | |||
| // process staff | |||
| logger.info("---------staff-------") | |||
| var staff = staffRepository.findByStaffId(row.getCell(0).stringCellValue).getOrNull() | |||
| if (staff == null) { | |||
| staff = staffRepository.findByStaffId("B000").orElseThrow() | |||
| } | |||
| // process project | |||
| logger.info("---------project-------") | |||
| var projectCode = StringBuilder(row.getCell(3).stringCellValue).insert(1, '-').toString() | |||
| if (row.getCell(4) != null && row.getCell(4).toString().isNotBlank()) { | |||
| val subCode = row.getCell(4).numericCellValue | |||
| val splitMainProjectCode = projectCode.split('-') | |||
| projectCode = splitMainProjectCode[0] + '-' + String.format( | |||
| "%04d", | |||
| splitMainProjectCode[1].toInt() | |||
| ) + '-' + String.format("%03d", subCode.toInt()) | |||
| } else { | |||
| val splitProjectCode = projectCode.split('-') | |||
| projectCode = splitProjectCode[0] + '-' + String.format("%04d", splitProjectCode[1].toInt()) | |||
| } | |||
| logger.info("Project Code: $projectCode") | |||
| val project = projectRepository.findByCode(projectCode) | |||
| // process project task | |||
| logger.info("---------project task-------") | |||
| val task = taskRepository.findById(41).getOrNull() | |||
| val projectTask = | |||
| project?.let { p -> task?.let { t -> projectTaskRepository.findByProjectAndTask(p, t) } } | |||
| // process record date | |||
| logger.info("---------record date-------") | |||
| val formatter = DateTimeFormatter.ofPattern("dd-MMM-yyyy") | |||
| val recordDate = LocalDate.parse(row.getCell(6).toString(), formatter); | |||
| // normal hour + ot hour | |||
| logger.info("---------normal hour + ot hour-------") | |||
| val hours: Double = row.getCell(7).numericCellValue | |||
| val normalHours = if (hours > 8.0) 8.0 else hours | |||
| val otHours = if (hours > 8.0) hours - 8.0 else 0.0 | |||
| if (project != null) { | |||
| timesheetList += Timesheet().apply { | |||
| this.staff = staff | |||
| this.recordDate = recordDate | |||
| this.normalConsumed = normalHours | |||
| this.otConsumed = otHours | |||
| this.projectTask = projectTask | |||
| this.project = project | |||
| } | |||
| } else { | |||
| notExistProjectList += projectCode | |||
| } | |||
| } | |||
| } | |||
| timesheetRepository.saveAll(timesheetList) | |||
| logger.info("---------end-------") | |||
| logger.info("Not Exist Project List: "+ notExistProjectList.distinct().joinToString(", ")) | |||
| return if (sheet.lastRowNum > 0) "Import Excel success btw " + notExistProjectList.joinToString(", ") else "Import Excel failure" | |||
| } | |||
| } | |||
| @@ -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<LeaveType> { | |||
| 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)) | |||
| } | |||
| } | |||
| @@ -229,6 +229,13 @@ public class UserService extends AbstractBaseEntityService<User, Long, UserRepos | |||
| List<Map<String, Integer>> 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<User, Long, UserRepos | |||
| authBatchInsertValues); | |||
| } | |||
| if (!authBatchDeleteValues.isEmpty()) { | |||
| jdbcDao.batchUpdate( | |||
| "DELETE FROM user_authority" | |||
| + " WHERE userId = :userId ", | |||
| // + "AND authId = :authId", | |||
| authBatchDeleteValues); | |||
| } | |||
| return instance; | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| -- liquibase formatted sql | |||
| -- changeset cyril:task | |||
| INSERT | |||
| INTO | |||
| task | |||
| (name, taskGroupId) | |||
| VALUES | |||
| ('5.8 Manhour Import', 5); | |||
| @@ -0,0 +1,5 @@ | |||
| -- liquibase formatted sql | |||
| -- changeset cyril:task | |||
| ALTER TABLE `project` | |||
| CHANGE COLUMN `name` `name` VARCHAR(255) CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci' NOT NULL ; | |||
| @@ -0,0 +1,81 @@ | |||
| -- liquibase formatted sql | |||
| -- changeset cyril:authority, user_authority | |||
| UPDATE authority | |||
| SET name='Maintain User in Master Data' | |||
| WHERE id=1; | |||
| UPDATE authority | |||
| SET name='Maintain User Group in Master Data' | |||
| WHERE id=2; | |||
| UPDATE authority | |||
| SET name='View User in Master Data' | |||
| WHERE id=3; | |||
| UPDATE authority | |||
| SET name='View User Group in Master Data' | |||
| WHERE id=4; | |||
| DELETE FROM authority | |||
| WHERE id=5; | |||
| DELETE FROM authority | |||
| WHERE id=6; | |||
| INSERT INTO authority (authority,name) | |||
| VALUES ('VIEW_CLIENT','View Client in Master Data'); | |||
| INSERT INTO authority (authority,name) | |||
| VALUES ('VIEW_SUBSIDIARY','View Subsidiary in Master Data'); | |||
| INSERT INTO authority (authority,name) | |||
| VALUES ('VIEW_STAFF','View Staff in Master Data'); | |||
| INSERT INTO authority (authority,name) | |||
| VALUES ('VIEW_COMPANY','View Company in Master Data'); | |||
| INSERT INTO authority (authority,name) | |||
| VALUES ('VIEW_SKILL','View Skill in Master Data'); | |||
| INSERT INTO authority (authority,name) | |||
| VALUES ('VIEW_DEPARTMENT','View Department in Master Data'); | |||
| INSERT INTO authority (authority,name) | |||
| VALUES ('VIEW_POSITION','View Position in Master Data'); | |||
| INSERT INTO authority (authority,name) | |||
| VALUES ('VIEW_SALARY','View Salary in Master Data'); | |||
| INSERT INTO authority (authority,name) | |||
| VALUES ('VIEW_TEAM','View Team in Master Data'); | |||
| INSERT INTO authority (authority,name) | |||
| VALUES ('VIEW_HOLIDAY','View Holiday in Master Data'); | |||
| INSERT INTO authority (authority,name) | |||
| VALUES ('MAINTAIN_CLIENT','Maintain Client in Master Data'); | |||
| INSERT INTO authority (authority,name) | |||
| VALUES ('MAINTAIN_SUBSIDIARY','Maintain Subsidiary in Master Data'); | |||
| INSERT INTO authority (authority,name) | |||
| VALUES ('MAINTAIN_STAFF','Maintain Staff in Master Data'); | |||
| INSERT INTO authority (authority,name) | |||
| VALUES ('MAINTAIN_COMPANY','Maintain Company in Master Data'); | |||
| INSERT INTO authority (authority,name) | |||
| VALUES ('MAINTAIN_SKILL','Maintain Skill in Master Data'); | |||
| INSERT INTO authority (authority,name) | |||
| VALUES ('MAINTAIN_DEPARTMENT','Maintain Department in Master Data'); | |||
| INSERT INTO authority (authority,name) | |||
| VALUES ('MAINTAIN_POSITION','Maintain Position in Master Data'); | |||
| INSERT INTO authority (authority,name) | |||
| VALUES ('MAINTAIN_SALARY','Maintain Salary in Master Data'); | |||
| INSERT INTO authority (authority,name) | |||
| VALUES ('MAINTAIN_TEAM','Maintain Team in Master Data'); | |||
| INSERT INTO authority (authority,name) | |||
| VALUES ('MAINTAIN_HOLIDAY','Maintain Holiday in Master Data'); | |||
| INSERT INTO `user_authority` VALUES | |||
| (1,21), | |||
| (1,22), | |||
| (1,23), | |||
| (1,24), | |||
| (1,25), | |||
| (1,26), | |||
| (1,27), | |||
| (1,28), | |||
| (1,29), | |||
| (1,30), | |||
| (1,31), | |||
| (1,32), | |||
| (1,33), | |||
| (1,34), | |||
| (1,35), | |||
| (1,36), | |||
| (1,37), | |||
| (1,38), | |||
| (1,39), | |||
| (1,40); | |||
| @@ -0,0 +1,5 @@ | |||
| -- liquibase formatted sql | |||
| -- changeset cyril:project | |||
| ALTER TABLE `project` | |||
| ADD COLUMN `subContractFee` DOUBLE NULL DEFAULT NULL AFTER `expectedTotalFee`; | |||