From 6663955e8135d63ae954fe92b9f4488c489fe1dc Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Wed, 29 May 2024 18:21:22 +0800 Subject: [PATCH 1/7] update --- .../com/ffii/tsms/modules/report/service/ReportService.kt | 4 ++-- .../changes/20240529_01_cyril/01_insert_authority.sql | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 src/main/resources/db/changelog/changes/20240529_01_cyril/01_insert_authority.sql diff --git a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt index 4389e35..115ccae 100644 --- a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt @@ -684,12 +684,12 @@ open class ReportService( rowIndex = 9 sheet.getRow(rowIndex).apply { createCell(1).apply { - setCellValue(project.expectedTotalFee!!) + setCellValue(project.expectedTotalFee!! * 0.8) cellStyle.dataFormat = accountingStyle } createCell(2).apply { - setCellValue(project.expectedTotalFee!! / 0.8) + setCellValue(project.expectedTotalFee!!) cellStyle.dataFormat = accountingStyle } } diff --git a/src/main/resources/db/changelog/changes/20240529_01_cyril/01_insert_authority.sql b/src/main/resources/db/changelog/changes/20240529_01_cyril/01_insert_authority.sql new file mode 100644 index 0000000..471ea88 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240529_01_cyril/01_insert_authority.sql @@ -0,0 +1,8 @@ +-- liquibase formatted sql +-- changeset cyril:authority, user_authority + +INSERT INTO authority (authority,name) +VALUES ('DELETE_PROJECT','delete project'); + +INSERT INTO user_authority (userId,authId) +VALUES (1,19); From ecf3b0e4652dff88b3f411516e06824e7b547002 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Wed, 29 May 2024 18:24:38 +0800 Subject: [PATCH 2/7] filter access right --- .../jwt/web/JwtAuthenticationController.java | 16 +++++++++++++++- .../java/com/ffii/tsms/model/JwtResponse.java | 12 ++++++++++-- .../modules/data/entity/StaffRepository.java | 2 ++ .../tsms/modules/data/web/SkillController.kt | 15 +++++++++------ .../tsms/modules/user/entity/UserRepository.java | 5 +++-- .../tsms/modules/user/service/GroupService.java | 12 ++++++++++++ 6 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/ffii/tsms/config/security/jwt/web/JwtAuthenticationController.java b/src/main/java/com/ffii/tsms/config/security/jwt/web/JwtAuthenticationController.java index 93dfe66..ef8b2cb 100644 --- a/src/main/java/com/ffii/tsms/config/security/jwt/web/JwtAuthenticationController.java +++ b/src/main/java/com/ffii/tsms/config/security/jwt/web/JwtAuthenticationController.java @@ -2,8 +2,12 @@ package com.ffii.tsms.config.security.jwt.web; import java.time.Instant; import java.util.HashSet; +import java.util.Map; import java.util.Set; +import com.ffii.tsms.modules.data.entity.Staff; +import com.ffii.tsms.modules.data.entity.StaffRepository; +import com.ffii.tsms.modules.user.service.GroupService; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -51,6 +55,12 @@ public class JwtAuthenticationController { @Autowired private JwtUserDetailsService userDetailsService; + @Autowired + private GroupService groupService; + + @Autowired + private StaffRepository staffRepository; + @Autowired private UserRepository userRepository; @@ -90,10 +100,14 @@ public class JwtAuthenticationController { final String accessToken = jwtTokenUtil.generateToken(user); final String refreshToken = jwtTokenUtil.createRefreshToken(user.getUsername()).getToken(); + final Map args = Map.of("userId", user.getId()); + final String role = groupService.getGroupName(args); + final Staff staff = staffRepository.findIdAndNameByUserIdAndDeletedFalse(user.getId()).orElse(null); + Set abilities = new HashSet<>(); userAuthorityService.getUserAuthority(user).forEach(auth -> abilities.add(new AbilityModel(auth.getAuthority()))); - return ResponseEntity.ok(new JwtResponse(accessToken, refreshToken, null, user, abilities)); + return ResponseEntity.ok(new JwtResponse(accessToken, refreshToken, role, user, abilities, staff)); } @PostMapping("/refresh-token") diff --git a/src/main/java/com/ffii/tsms/model/JwtResponse.java b/src/main/java/com/ffii/tsms/model/JwtResponse.java index 9b0d4e7..beaad81 100644 --- a/src/main/java/com/ffii/tsms/model/JwtResponse.java +++ b/src/main/java/com/ffii/tsms/model/JwtResponse.java @@ -3,6 +3,7 @@ package com.ffii.tsms.model; import java.io.Serializable; import java.util.Set; +import com.ffii.tsms.modules.data.entity.Staff; import com.ffii.tsms.modules.user.entity.User; public class JwtResponse implements Serializable { @@ -15,8 +16,11 @@ public class JwtResponse implements Serializable { private final String refreshToken; private final String role; private final Set abilities; + private final Staff staff; - public JwtResponse(String accessToken, String refreshToken, String role, User user, Set abilities) { + + + public JwtResponse(String accessToken, String refreshToken, String role, User user, Set abilities, Staff staff) { this.accessToken = accessToken; this.refreshToken = refreshToken; this.role = role; @@ -24,7 +28,8 @@ public class JwtResponse implements Serializable { this.name = user.getName(); this.email = user.getEmail(); this.abilities = abilities; - } + this.staff = staff; + } public String getAccessToken() { return this.accessToken; @@ -50,6 +55,9 @@ public class JwtResponse implements Serializable { return email; } + public Staff getStaff() { return staff; } + + public Set getAbilities() { return abilities; } diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java b/src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java index 3b0b4d1..d650327 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java +++ b/src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java @@ -21,4 +21,6 @@ public interface StaffRepository extends AbstractRepository { Optional findByUserId(@Param("userId") Long userId); Optional> findAllByTeamIdAndDeletedFalse(Long id); + + Optional findIdAndNameByUserIdAndDeletedFalse(Long id); } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/web/SkillController.kt b/src/main/java/com/ffii/tsms/modules/data/web/SkillController.kt index a6d9bd0..0b70d12 100644 --- a/src/main/java/com/ffii/tsms/modules/data/web/SkillController.kt +++ b/src/main/java/com/ffii/tsms/modules/data/web/SkillController.kt @@ -2,44 +2,47 @@ package com.ffii.tsms.modules.data.web import com.ffii.core.response.RecordsRes import com.ffii.core.utils.CriteriaArgsBuilder +import com.ffii.tsms.modules.common.SecurityUtils import com.ffii.tsms.modules.data.entity.Skill import com.ffii.tsms.modules.data.service.SkillService import com.ffii.tsms.modules.data.web.models.NewSkillRequest import jakarta.servlet.http.HttpServletRequest import jakarta.validation.Valid import org.springframework.http.HttpStatus +import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.ServletRequestBindingException import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/skill") -class SkillController(private val skillService: SkillService) { +open class SkillController(private val skillService: SkillService) { @PostMapping("/save") - fun saveSkill(@Valid @RequestBody newSkill: NewSkillRequest): Skill { + @PreAuthorize("hasAuthority('MAINTAIN_MASTERDATA')") + open fun saveSkill(@Valid @RequestBody newSkill: NewSkillRequest): Skill { return skillService.saveOrUpdate(newSkill) } @GetMapping("/{id}") - fun list(@Valid @PathVariable id: Long): List> { + open fun list(@Valid @PathVariable id: Long): List> { val args: MutableMap = HashMap() args["id"] = id return skillService.list(args); } @DeleteMapping("/delete/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) - fun delete(@PathVariable id: Long?) { + open fun delete(@PathVariable id: Long?) { skillService.markDelete(id) } @GetMapping - fun list(): List> { + open fun list(): List> { val args: MutableMap = HashMap() return skillService.list(args); } @GetMapping("/combo") @Throws(ServletRequestBindingException::class) - fun combo(request: HttpServletRequest?): RecordsRes> { + open fun combo(request: HttpServletRequest?): RecordsRes> { println(request) return RecordsRes>( skillService.combo( diff --git a/src/main/java/com/ffii/tsms/modules/user/entity/UserRepository.java b/src/main/java/com/ffii/tsms/modules/user/entity/UserRepository.java index 40214d3..7107cb2 100644 --- a/src/main/java/com/ffii/tsms/modules/user/entity/UserRepository.java +++ b/src/main/java/com/ffii/tsms/modules/user/entity/UserRepository.java @@ -4,7 +4,7 @@ import java.util.List; import java.util.Optional; import org.springframework.data.repository.query.Param; - +import org.springframework.data.jpa.repository.Query; import com.ffii.core.support.AbstractRepository; public interface UserRepository extends AbstractRepository { @@ -12,5 +12,6 @@ public interface UserRepository extends AbstractRepository { List findByName(@Param("name") String name); List findAllByAndDeletedFalse(); - Optional findByUsernameAndDeletedFalse(String username); + Optional findByUsernameAndDeletedFalse(String username); + } diff --git a/src/main/java/com/ffii/tsms/modules/user/service/GroupService.java b/src/main/java/com/ffii/tsms/modules/user/service/GroupService.java index 7b128bd..d0fb138 100644 --- a/src/main/java/com/ffii/tsms/modules/user/service/GroupService.java +++ b/src/main/java/com/ffii/tsms/modules/user/service/GroupService.java @@ -172,6 +172,18 @@ public class GroupService extends AbstractBaseEntityService args) { + StringBuilder sql = new StringBuilder("select" + + " g.name " + + " from user u " + + " left join user_group ug on u.id = ug.userId " + + " left join `group`g on ug.groupId = g.id " + + " where g.deleted = false " + + " and u.id = :userId" + ); + return jdbcDao.queryForString(sql.toString(), args); + } @Transactional(rollbackFor = Exception.class) From 02b152d0d248efff1a1b1726f65705591210fde1 Mon Sep 17 00:00:00 2001 From: Wayne Date: Wed, 29 May 2024 23:05:43 +0900 Subject: [PATCH 3/7] Add delete time/leave entry endpoint --- .../tsms/modules/timesheet/service/LeaveService.kt | 11 +++++++++++ .../modules/timesheet/service/TimesheetsService.kt | 11 +++++++++++ .../modules/timesheet/web/TimesheetsController.kt | 10 ++++++++++ .../modules/timesheet/web/models/TeamEntryDelete.kt | 6 ++++++ 4 files changed, 38 insertions(+) create mode 100644 src/main/java/com/ffii/tsms/modules/timesheet/web/models/TeamEntryDelete.kt diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/service/LeaveService.kt b/src/main/java/com/ffii/tsms/modules/timesheet/service/LeaveService.kt index 97658ac..73a42ba 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/service/LeaveService.kt +++ b/src/main/java/com/ffii/tsms/modules/timesheet/service/LeaveService.kt @@ -75,6 +75,17 @@ open class LeaveService( return transformToLeaveEntryMap(leaveRepository.findAllByStaff(memberStaff)) } + open fun deleteMemberLeaveEntry(staffId: Long, entryId: Long): Map> { + val currentStaff = staffsService.currentStaff() ?: throw BadRequestException() + // Make sure current staff is a team lead + teamService.getMyTeamForStaff(currentStaff) ?: throw BadRequestException() + + val memberStaff = staffsService.getStaff(staffId) + + leaveRepository.deleteById(entryId) + return transformToLeaveEntryMap(leaveRepository.findAllByStaff(memberStaff)) + } + open fun getTeamMemberLeave(): Map { val currentStaff = staffsService.currentStaff() ?: return emptyMap() // Get team where current staff is team lead diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt b/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt index 206daf0..96f9c23 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt +++ b/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt @@ -83,6 +83,17 @@ open class TimesheetsService( return transformToTimeEntryMap(timesheetRepository.findAllByStaff(memberStaff)) } + open fun deleteMemberTimeEntry(staffId: Long, entryId: Long): Map> { + val currentStaff = staffsService.currentStaff() ?: throw BadRequestException() + // Make sure current staff is a team lead + teamService.getMyTeamForStaff(currentStaff) ?: throw BadRequestException() + + val memberStaff = staffsService.getStaff(staffId) + + timesheetRepository.deleteById(entryId) + return transformToTimeEntryMap(timesheetRepository.findAllByStaff(memberStaff)) + } + open fun getTimesheet(): Map> { // Need to be associated with a staff val currentStaff = staffsService.currentStaff() ?: return emptyMap() diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt b/src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt index df9bbe3..6f2d4ce 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt +++ b/src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt @@ -66,6 +66,16 @@ class TimesheetsController(private val timesheetsService: TimesheetsService, pri }.getOrNull()) } + @PostMapping("/deleteMemberEntry") + fun deleteMemberEntry(@Valid @RequestBody request: TeamEntryDelete): Map> { + return timesheetsService.deleteMemberTimeEntry(request.staffId, request.entryId) + } + + @PostMapping("/deleteMemberLeave") + fun deleteMemberLeave(@Valid @RequestBody request: TeamEntryDelete): Map> { + return leaveService.deleteMemberLeaveEntry(request.staffId, request.entryId) + } + @GetMapping("/leaves") fun getLeaveEntry(): Map> { return leaveService.getLeaves() diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/web/models/TeamEntryDelete.kt b/src/main/java/com/ffii/tsms/modules/timesheet/web/models/TeamEntryDelete.kt new file mode 100644 index 0000000..a79a278 --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/timesheet/web/models/TeamEntryDelete.kt @@ -0,0 +1,6 @@ +package com.ffii.tsms.modules.timesheet.web.models + +data class TeamEntryDelete( + val staffId: Long, + val entryId: Long, +) From 99337662e34cc12c0cf82c41b5e6de1e3e67496a Mon Sep 17 00:00:00 2001 From: "Mac\\David" Date: Wed, 29 May 2024 23:47:09 +0800 Subject: [PATCH 4/7] bug fix --- .../modules/data/service/DashboardService.kt | 79 ++++++++++++++++--- .../modules/data/web/DashboardController.kt | 20 +++++ 2 files changed, 89 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt index 28cca86..91a963c 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt @@ -110,8 +110,8 @@ open class DashboardService( + " s.name as teamLead," + " 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 (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," + " DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d') as comingPaymentMilestone" + " from project p" @@ -254,8 +254,8 @@ open class DashboardService( + " s.name as teamLead," + " 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 (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," + " DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d') as comingPaymentMilestone" + " from project p" @@ -301,7 +301,18 @@ open class DashboardService( + " coalesce(round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2),0) as cpi" + " from team t" + " left join project p on t.teamLead = p.teamLead" - + " left join invoice i on p.code = i.projectCode" + + " left join (" + + " select" + + " t3.id as tid," + + " sum(i3.issueAmount) as issueAmount," + + " sum(i3.paidAmount) as paidAmount" + + " from team t3" + + " left join project p3 on t3.teamLead = p3.teamLead" + + " left join invoice i3 on p3.code = i3.projectCode" + + " where t3.deleted = 0" + + " and p3.status = 'On-going'" + + " group by t3.id" + + " ) as i on i.tid = t.id" + " left join (" + " select" + " r.teamId as teamId," @@ -347,7 +358,17 @@ open class DashboardService( + " end as cashFlowStatus," + " round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2) as cpi" + " from project p" - + " left join invoice i on p.code = i.projectCode" + + " left join (" + + " select" + + " p3.id as pid," + + " sum(i3.issueAmount) as issueAmount," + + " sum(i3.paidAmount) as paidAmount" + + " from project p3" + + " left join invoice i3 on p3.code = i3.projectCode" + + " where p3.deleted = 0" + + " and p3.status = 'On-going'" + + " group by p3.id" + + " ) as i on i.pid = p.id" + " left join (" + " select" + " sum(r.cumulativeExpenditure) as cumulativeExpenditure" @@ -399,6 +420,7 @@ open class DashboardService( + " 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" @@ -415,8 +437,8 @@ open class DashboardService( if (args.containsKey("teamId")) sql.append(" AND t3.id = :teamId"); } - sql.append( " group by c3.id" - + " ) as i on i.cid = c.id" + 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," @@ -657,7 +679,17 @@ open class DashboardService( + " coalesce (expenditure.expenditure,0) as totalExpenditure," + " coalesce (sum(p.expectedTotalFee)*0.8 - expenditure.expenditure,0) as expenditureReceivable" + " from project p" - + " left join invoice i on p.code = i.projectCode" + + " left join (" + + " select" + + " p3.id as pid," + + " sum(i3.issueAmount) as issueAmount," + + " sum(i3.paidAmount) as paidAmount" + + " from project p3" + + " left join invoice i3 on p3.code = i3.projectCode" + + " where p3.deleted = 0" + + " and p3.status = 'On-going'" + + " group by p3.id" + + " ) as i on i.pid = p.id" + " left join(" + " select" + " sum(r.expenditure) as expenditure" @@ -933,9 +965,12 @@ open class DashboardService( + " p.id as id," + " p.code as projectCode," + " p.name as projectName," - + " concat(c.code,'-',c.name) as customerCodeAndName" + + " concat(c.code,'-',c.name) as customerCodeAndName," + + " concat(c.code,'-',c.name) as customerCodeAndName," + + " concat(s.code,'-',s.name) as subsidiaryCodeAndName" + " from project p" + " left join customer c on p.customerId = c.id" + + " left join subsidiary s on p.customerSubsidiaryId = s.id" + " where p.status = 'On-going'" + " and p.deleted = 0" ) @@ -1033,7 +1068,31 @@ open class DashboardService( return jdbcDao.queryForList(sql.toString(), args) } + fun monthlyActualTeamTotalManhoursSpent(args: Map): List> { + val sql = StringBuilder( + " WITH RECURSIVE date_series AS (" + + " SELECT DATE_FORMAT(:startdate, '%Y-%m-01') AS month" + + " UNION ALL" + + " SELECT DATE_FORMAT(DATE_ADD(month, INTERVAL 1 MONTH), '%Y-%m-01')" + + " FROM date_series" + + " WHERE month < DATE_FORMAT(:enddate, '%Y-%m-01')" + + " )" + + " SELECT" + + " ds.month AS yearMonth," + + " IFNULL(SUM(IFNULL(ts.normalConsumed, 0) + IFNULL(ts.otConsumed, 0)), 0) AS 'TotalManhourConsumed'," + + " :teamId AS teamId" + + " FROM date_series ds" + + " LEFT JOIN staff st" + + " on st.teamId = :teamId" + + " LEFT JOIN timesheet ts" + + " ON DATE_FORMAT(ts.recordDate, '%Y-%m') = DATE_FORMAT(ds.month, '%Y-%m') and ts.staffID = st.id" + + " WHERE ds.month BETWEEN DATE_FORMAT(:startdate, '%Y-%m-01') AND DATE_FORMAT(:enddate, '%Y-%m-01')" + + " GROUP BY ds.month, st.teamId" + + " ORDER BY ds.month" + ) + return jdbcDao.queryForList(sql.toString(), args) + } } diff --git a/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt b/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt index a69305d..70c2162 100644 --- a/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt +++ b/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt @@ -232,4 +232,24 @@ class DashboardController( result["summarySubStage"] = summarySubStage return listOf(result) } + @GetMapping("/searchMonthlyActualTeamTotalManhoursSpent") + fun searchMonthlyActualTeamTotalManhoursSpent(request: HttpServletRequest?): List> { + val args = mutableMapOf() + val teamId = request?.getParameter("teamId") + val startdate = request?.getParameter("startdate") + val enddate = request?.getParameter("enddate") + if (teamId != null) { + args["teamId"] = teamId + } + if (startdate != null) { + args["startdate"] = startdate + } + if (enddate != null) { + args["enddate"] = enddate + } + val result = mutableMapOf() + val monthlyActualTeamTotalManhoursSpent = dashboardService.monthlyActualTeamTotalManhoursSpent(args) + result["monthlyActualTeamTotalManhoursSpent"] = monthlyActualTeamTotalManhoursSpent + return listOf(result) + } } \ No newline at end of file From 6f3de950aebcff2c713e1b6e75232e1e40767c37 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Thu, 30 May 2024 11:40:30 +0800 Subject: [PATCH 5/7] update report --- .../modules/report/service/ReportService.kt | 76 ++++++++++++------ .../modules/report/web/ReportController.kt | 2 +- .../modules/report/web/model/ReportRequest.kt | 4 +- .../report/AR02_Delay Report v02.xlsx | Bin 12388 -> 12440 bytes .../report/EX02_Project Cash Flow Report.xlsx | Bin 13291 -> 13335 bytes 5 files changed, 55 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt index 115ccae..621ae19 100644 --- a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt @@ -178,8 +178,8 @@ open class ReportService( searchedClient: String, projects: List, timesheets: List, - numberOfDays: Int, - projectCompletion: Int, + daysUntilCurrentStageEnd: Int, + resourceUtilizationPercentage: Int, ): ByteArray { // Generate the Excel report with query results val workbook: Workbook = createProjectPotentialDelayReport( @@ -187,8 +187,8 @@ open class ReportService( searchedClient, projects, timesheets, - numberOfDays, - projectCompletion, + daysUntilCurrentStageEnd, + resourceUtilizationPercentage, PROJECT_POTENTIAL_DELAY_REPORT ) @@ -673,15 +673,30 @@ open class ReportService( rowIndex = 4 sheet.getRow(rowIndex).createCell(columnIndex).apply { - setCellValue(if (project.customer?.name == null) "N/A" else project.customer?.name) + setCellValue( + if (project.customer?.code != null && project.customer?.name != null) project.customer!!.code + " - " + project.customer!!.name + else if (project.customer?.code != null) project.customer!!.code + else if (project.customer?.name != null) project.customer!!.name + else "N/A" + ) } rowIndex = 5 + sheet.getRow(rowIndex).createCell(columnIndex).apply { + setCellValue( + if (project.customerSubsidiary?.code != null && project.customerSubsidiary?.name != null) project.customerSubsidiary!!.code + " - " + project.customerSubsidiary!!.name + else if (project.customerSubsidiary?.code != null) project.customerSubsidiary!!.code + else if (project.customerSubsidiary?.name != null) project.customerSubsidiary!!.name + else "N/A" + ) + } + + rowIndex = 6 sheet.getRow(rowIndex).createCell(columnIndex).apply { setCellValue(if (project.teamLead?.team?.name == null) "N/A" else project.teamLead?.team?.name) } - rowIndex = 9 + rowIndex = 10 sheet.getRow(rowIndex).apply { createCell(1).apply { setCellValue(project.expectedTotalFee!! * 0.8) @@ -694,7 +709,7 @@ open class ReportService( } } - rowIndex = 10 + rowIndex = 11 val actualIncome = invoices.sumOf { invoice -> invoice.paidAmount!! } val actualExpenditure = timesheets.sumOf { timesheet -> timesheet.staff!!.salary.hourlyRate.toDouble() * ((timesheet.normalConsumed ?: 0.0) + (timesheet.otConsumed @@ -712,21 +727,20 @@ open class ReportService( } } - rowIndex = 11 + rowIndex = 12 sheet.getRow(rowIndex).apply { createCell(1).apply { - cellFormula = "B10-B11" + cellFormula = "B11-B12" cellStyle.dataFormat = accountingStyle } createCell(2).apply { - cellFormula = "C10-C11" + cellFormula = "C11-C12" cellStyle.dataFormat = accountingStyle } } - rowIndex = 15 - + rowIndex = 16 val dateFormatter = if (dateType == "Date") DateTimeFormatter.ofPattern("yyyy/MM/dd") else DateTimeFormatter.ofPattern("MMM YYYY") @@ -796,7 +810,7 @@ open class ReportService( createCell(3).apply { val lastRow = rowIndex - 1 - if (lastRow == 15) { + if (lastRow == 16) { cellFormula = "C{currentRow}-B{currentRow}".replace("{currentRow}", rowIndex.toString()) } else { @@ -835,7 +849,7 @@ open class ReportService( createCell(3).apply { val lastRow = rowIndex - 1 - if (lastRow == 15) { + if (lastRow == 16) { cellFormula = "C{currentRow}-B{currentRow}".replace("{currentRow}", rowIndex.toString()) } else { cellFormula = @@ -863,8 +877,8 @@ open class ReportService( searchedClient: String, projects: List, timesheets: List, - numberOfDays: Int, - projectCompletion: Int, + daysUntilCurrentStageEnd: Int, + resourceUtilizationPercentage: Int, templatePath: String, ): Workbook { // please create a new function for each report template @@ -939,15 +953,29 @@ open class ReportService( createCell(4).apply { val currentClient = project.customer - val currentSubsidiary = project.customerSubsidiary - setCellValue(if (currentSubsidiary != null) currentSubsidiary.code + " - " + currentSubsidiary.name else currentClient?.code + " - " + currentClient?.name) + setCellValue( + if (currentClient?.code != null && currentClient.name != null) currentClient.code + " - " + currentClient.name + else if (currentClient?.code != null) currentClient.code + else if (currentClient?.name != null) currentClient.name + else "N/A" + ) } createCell(5).apply { - setCellValue(project.actualStart?.format(DATE_FORMATTER)) + val currentSubsidiary = project.customerSubsidiary + setCellValue( + if (currentSubsidiary?.code != null && currentSubsidiary.name != null) currentSubsidiary.code + " - " + currentSubsidiary.name + else if (currentSubsidiary?.code != null) currentSubsidiary.code + else if (currentSubsidiary?.name != null) currentSubsidiary.name + else "N/A" + ) } createCell(6).apply { + setCellValue(project.actualStart?.format(DATE_FORMATTER)) + } + + createCell(7).apply { setCellValue(project.planEnd?.format(DATE_FORMATTER)) } } @@ -958,22 +986,22 @@ open class ReportService( val manHoursSpent = groupedTimesheets[Pair(project.id, milestone.id)]?.sum() ?: 0.0 val resourceUtilization = manHoursSpent / (milestone.stagePercentAllocation!! / 100 * project.totalManhour!!) // logger.info(project.name + " : " + milestone.taskGroup?.name + " : " + ChronoUnit.DAYS.between(LocalDate.now(), milestone.endDate)) -// logger.info(numberOfDays) - if (ChronoUnit.DAYS.between(LocalDate.now(), milestone.endDate) <= numberOfDays.toLong() && resourceUtilization <= projectCompletion.toDouble() / 100.0) { +// logger.info(daysUntilCurrentStageEnd) + 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++ tempRow.apply { - createCell(7).apply { + createCell(8).apply { setCellValue(milestone.taskGroup?.name ?: "N/A") } - createCell(8).apply { + createCell(9).apply { setCellValue(milestone.endDate?.format(DATE_FORMATTER) ?: "N/A") } - createCell(9).apply { + createCell(10).apply { cellStyle.dataFormat = workbook.createDataFormat().getFormat("0.00%") // if (groupedTimesheets.containsKey(Pair(project.id, milestone.id))) { diff --git a/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt b/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt index 44dc0d4..aafd24e 100644 --- a/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt +++ b/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt @@ -119,7 +119,7 @@ class ReportController( val projectTasks = projectTaskRepository.findAllByProjectIn(projects) val timesheets = timesheetRepository.findAllByProjectTaskIn(projectTasks) - val reportResult: ByteArray = excelReportService.generateProjectPotentialDelayReport(searchedTeam, searchedClient, projects, timesheets, request.numberOfDays, request.projectCompletion) + val reportResult: ByteArray = excelReportService.generateProjectPotentialDelayReport(searchedTeam, searchedClient, projects, timesheets, request.daysUntilCurrentStageEnd, request.resourceUtilizationPercentage) return ResponseEntity.ok() .header("filename", "Project Potential Delay Report - " + LocalDate.now() + ".xlsx") .body(ByteArrayResource(reportResult)) diff --git a/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt b/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt index 8d3abb4..e906a38 100644 --- a/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt +++ b/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt @@ -27,8 +27,8 @@ data class ProjectCashFlowReportRequest ( data class ProjectPotentialDelayReportRequest ( val teamId: String, val clientId: String, - val numberOfDays: Int, - val projectCompletion: Int, + val daysUntilCurrentStageEnd: Int, + val resourceUtilizationPercentage: Int, val type: String, ) diff --git a/src/main/resources/templates/report/AR02_Delay Report v02.xlsx b/src/main/resources/templates/report/AR02_Delay Report v02.xlsx index d07a414637b4051f5a1c9282ddbdbf8bf4b085d5..1a4348a228d5f2ab2b12887afc85ae8b525e5292 100644 GIT binary patch delta 2821 zcmZ9Oc|6qX7stOdXe`ml5E|mz#+VtCecuUPvLrK>2~CZ4WJ`mLrMTG{CNxKFJ-4WCMT^ zP5?Lp0KipW#Zdo19}j|@_Iql+sbm4$tjC; zB!uB1h$Y>Y9r0%ErASJzE(Z669Fb9roZVDb9!OHuUoW={mvpf?mm0DHUKYKtOdtO4 zRY`R&F)Q#fn$zl%p;mO`s4_;Zh@Ius*Xa2HpUym0pzI}1vDo*wVHn;JiNfHFy>aib z1a~gu17bp)03$I5iq9%ZP9vSm9t}%{ZU$k;FBvOmR99A+qr;)rmZ<1fgs0S@$4G6+ zgJ*v=`Xwbf@ETNlS$y0acx$2K3&4+MZT1Pof1~93Vje4lc?&YJXJ8*}=B;^jZ zEpS0!-j|#ELQ3eBJ3LoP^)!xjpVXujBI6+E-tS2$tVewm>p*>(^b|6$7`9~Z-;>A< zpSiT+cZ^!l z_-h`P*rieJjuI|dlvL6sp6KpOXF0^{qJ{s!Ij6Nb_i!!nnRE6=3z7HLhNO_Cs{pv62mZiW*r}#*ZFE{ZZ*X~Pi_o)*XBF5o zJSlj=!(X6Dd#?Lq2buY!}T@e5Ld(05Xf&4H7Z@r?k|mOPj5>l zuY0nD%USnCMk5ii4wU9nqC(1+jj`WXgB6BfSJ+H!HX%M;*C^$IU-J`4?rX5`msOy5 zjk?qd{eBjhm9Pm)V^0{vQM=@pzZGu?ms7gBB8gdKO( zP*r)=mRhEQ`2Bfi$Q0KU9IE7Usourj^%(t)(Ur1ZIcFhs&g>_BKt!kS@%H5X!jAK)g-PqFtJE0wPALn)Pam6k?ijnAZ3bO=)>(x^zedzH92 zLXMgGcIt^s;rH`)=qKkvg+n3}qV{KE4TjMICVMrP1V6*hJiTUes)Ll| z>3RF`XJX8bcZ4>4cwFTPTz8qf1eer$edb(rM#i<;eWz!&P}z=Mrc)0oe4ht?xC#~8 z_g(Vs#<4m#8Tk2PtZW;Dgxh`g`K9%OGp~ZWAoNT>ca1`y5nZ1`t`o^+YA{|DNq(wp zeZ5cWRCi{}jcdJNkEa@8H&(dF@j_u)aK;C9sKzZ>cTte(ZO6%08>^;avxL#)(}Xio zuBU*>qZrm#tU1OpiRt;$OqPccX7u;lRKa@PfI1|N4yj8-Hq}%M zFBW(yYD2B4vzu7~v#qDx2aPc(%-V!N=_Yd=-es}Oj9B{yD=SgC@R| zSW%Fj!em*N_w>wcXzu#?4@}CF@KeomyEpA}YA5BjcggMJl2S#TwxaT?-q>AF-a4`4 zUezLZiVLNjO0u|>o03ic6eW3Gf7=k1KA)dh($f`J^8hTsGurn4XA)~OpyNDLoG&LC z2?Bs{FaW^*TLgkU-2*QY?Slio$fTg31t7w1&>DOa8lk>H@CFnRv@FcZTE zgJC5utBCS|fNNPVsfe`xz}^wFVi7N~3SP}5wrQx`;s;24MxA@r1p&^L2FatOo&C$q zGBMF}Hs9zn-}Dfk?;`5X9)L`RtkNxP>m;E(y&PkiUh+!&r-K)QCv9ZTK1VTAAIHd# zVGT9;#hEHHukfZ`RyE7VQJk(Pw0px$LkCO0>~Mt!k(p&jELv^n?Oz=8;6^OtcfAH) z-Ssy%t%hCb3oT{X8ZoEu?y3?hy`~=Az0Vuxs(;S$h^L07wUSsu4#h(Luv2#0OR-=_ zE$g?-+c14v0JmUmwU`z+JEO&2t9DIJ1w{4x054o^@3!V+S4gN&iO@cncsiRQ+3W{AEedn$ZMG~v<5aGPGU*1~HIRmmRxihX?QNz@v#nd@nq=Vqwc zZYi_k!fI0dchX~0UoEhR^r9nAf(yDCwwXHsuRK&Jttn$x)3HFbI$Ab6RrbRl!>F{8CGngm{z3 zhzOqJp6;u|ENflAG<``!c0zpKf}&Qpb5j^kwshM6b3P}KIt9)SKORx^BqJTUgo~(cWFZ3WCLc-MNKm1^wz$WTXij*Z-$US~J=j6iVa9ppXB% zbN$fm-&Rhu!+BXRuS0{`Dj00^VO6b-?nVzgE<8CrxQ5W~Zi=iPq*({B1V delta 2691 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 m4lr%e|26dQR=E5nocw Date: Thu, 30 May 2024 11:42:39 +0800 Subject: [PATCH 6/7] update consumption report --- .../modules/report/service/ReportService.kt | 2 ++ .../report/AR03_Resource Overconsumption.xlsx | Bin 13170 -> 13340 bytes 2 files changed, 2 insertions(+) diff --git a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt index 621ae19..2a54572 100644 --- a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt @@ -1806,6 +1806,7 @@ open class ReportService( + " p.name, " + " t.code as team, " + " c.code as client, " + + " COALESCE(ss.name, 'N/A') as subsidiary, " + " p.expectedTotalFee * 0.8 as plannedBudget, " + " COALESCE(tns.totalBudget, 0) as actualConsumedBudget, " + " COALESCE(p.totalManhour, 0) as plannedManhour, " @@ -1826,6 +1827,7 @@ open class ReportService( + " 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' " diff --git a/src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx b/src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx index 88dd3c84e085a54dff23962317db12778795a695..52e46ba30c385dcd4837082125c3196623179a20 100644 GIT binary patch delta 5456 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 delta 5280 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 Date: Thu, 30 May 2024 13:11:12 +0800 Subject: [PATCH 7/7] update overconsum report --- .../modules/report/service/ReportService.kt | 10 ++++++---- .../modules/report/web/ReportController.kt | 12 +++++++++--- .../modules/report/web/model/ReportRequest.kt | 1 + .../report/AR03_Resource Overconsumption.xlsx | Bin 13340 -> 13354 bytes 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt index 2a54572..664d91b 100644 --- a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt @@ -1424,18 +1424,18 @@ open class ReportService( generalCreateReportIndexed(sheet, result, rowIndex, columnIndex) val sheetCF = sheet.sheetConditionalFormatting - val rule1 = sheetCF.createConditionalFormattingRule("AND(J7 >= $lowerLimit, J7 <= 1)") - val rule2 = sheetCF.createConditionalFormattingRule("J7 > 1") + val rule1 = sheetCF.createConditionalFormattingRule("AND(K7 >= $lowerLimit, K7 <= 1)") + val rule2 = sheetCF.createConditionalFormattingRule("K7 > 1") var fillOrange = rule1.createPatternFormatting() fillOrange.setFillBackgroundColor(IndexedColors.LIGHT_ORANGE.index); fillOrange.setFillPattern(PatternFormatting.SOLID_FOREGROUND) var fillRed = rule2.createPatternFormatting() - fillRed.setFillBackgroundColor(IndexedColors.PINK.index); + fillRed.setFillBackgroundColor(IndexedColors.RED1.index); fillRed.setFillPattern(PatternFormatting.SOLID_FOREGROUND) val cfRules = arrayOf(rule1, rule2) - val regions = arrayOf(CellRangeAddress.valueOf("\$J7:\$K${rowIndex + 1}")) + val regions = arrayOf(CellRangeAddress.valueOf("\$K7:\$L${rowIndex + 1}")) sheetCF.addConditionalFormatting(regions, cfRules); return workbook @@ -1838,6 +1838,8 @@ open class ReportService( sql.append("and t.id = :teamId") if (args.containsKey("custId")) sql.append("and c.id = :custId") + if (args.containsKey("subsidiaryId")) + sql.append("and ss.id = :subsidiaryId") if (args.containsKey("status")) statusFilter = when (args.get("status")) { "Potential Overconsumption" -> "and " + diff --git a/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt b/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt index aafd24e..02ea8fa 100644 --- a/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt +++ b/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt @@ -35,6 +35,7 @@ import java.time.format.DateTimeFormatter import org.springframework.stereotype.Controller import org.springframework.data.jpa.repository.JpaRepository 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 com.ffii.tsms.modules.timesheet.entity.Timesheet import org.springframework.data.domain.Example @@ -56,6 +57,7 @@ class ReportController( private val leaveRepository: LeaveRepository, private val teamService: TeamService, private val customerService: CustomerService, + private val subsidiaryService: SubsidiaryService, private val invoiceService: InvoiceService, private val gradeAllocationRepository: GradeAllocationRepository, private val subsidiaryRepository: SubsidiaryRepository ) { @@ -154,7 +156,7 @@ class ReportController( fun ProjectResourceOverconsumptionReport(@RequestBody @Valid request: ProjectResourceOverconsumptionReport): ResponseEntity { val lowerLimit = request.lowerLimit var team: String = "All" - var customer: String = "All" + var customerOrSubsidiary: String = "All" val args: MutableMap = mutableMapOf( "status" to request.status, "lowerLimit" to lowerLimit @@ -165,10 +167,14 @@ class ReportController( } if (request.custId != null) { args["custId"] = request.custId - customer = customerService.find(request.custId).orElseThrow().name + customerOrSubsidiary = customerService.find(request.custId).orElseThrow().name + } + if (request.subsidiaryId != null) { + args["subsidiaryId"] = request.subsidiaryId + customerOrSubsidiary = subsidiaryService.find(request.subsidiaryId).orElseThrow().name } val result: List> = excelReportService.getProjectResourceOverconsumptionReport(args); - val reportResult: ByteArray = excelReportService.generateProjectResourceOverconsumptionReport(team, customer, result, lowerLimit) + val reportResult: ByteArray = excelReportService.generateProjectResourceOverconsumptionReport(team, customerOrSubsidiary, result, lowerLimit) // val mediaType: MediaType = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") return ResponseEntity.ok() // .contentType(mediaType) diff --git a/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt b/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt index e906a38..f302693 100644 --- a/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt +++ b/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt @@ -46,6 +46,7 @@ data class LateStartReportRequest ( data class ProjectResourceOverconsumptionReport ( val teamId: Long?, val custId: Long?, + val subsidiaryId: Long?, val status: String, val lowerLimit: Double ) diff --git a/src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx b/src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx index 52e46ba30c385dcd4837082125c3196623179a20..fb9418177b8978fff9da6be57a7ac8d1e8afeef5 100644 GIT binary patch delta 3027 zcmV;^3oP`UXsT$i+6DwjD!gZt-Uc0iZEvGE5dMDB{Rb9R+D{ZrfF!8NDhZ&g)a+GB zw!M=gMGiQ{DqzkwO{&%Y_ZvemEvM{KDgBW}X?}eA?xi_CmOztQhF%+R`;r z#4Mr3(?I{Sh>V`D35kmYXRIIt{gnv))A0A-Znlg+uNiyR0H6>9J(aTbO;f~wDao<8 zW+f?LWy5%mB@FqKDN0UoB2q$Ro|(2~d1j7jp*ILVzXXGAHZ&#yi>sUzvH{^F!xHF= zl$N4J%i~Mna?GEr(ui4J0>qkTRKC`rx|YZO?Nh-xUT0wT4!PQn!_R|1vY@fslVIuW zsGP=}3AT~f0Bts;&+JE*iO`9E3wGx|xD3fLIeDQfh6h&MdnYUJfaM**koC)G2&7eO z#RuoT6U{wvv-RQ4hGyiUk*Qstf~aZHwr`E+iqar{h*WGQ&OR2j7dP*(Anh@~WX z3E2T}oRo~q-sO%kH+8GZQir>s7HxEft~{?+sf zIXC8;13_k|WZ;B<_?~O3s}<$^=L!u>iUn=!aac|&Cq@O?{m?Doc1 z2PirbiVO$!En__P9K#Kwj@w1SBy^)AS!Mp_okg{0)?X7>EGsA+su0yUlQ^?Yfld4GJ2v@1MM9_&aDwgg_&xS}GQZhZrSr<9 z;-XFwc75cqnu?s)>l8gdEk{%2C{5~|l%i%SIxvN979T&%cS63X4P)8?LakD?(Yl#= zo}wF85#@@8)i8fk2w4#gf92X!jbtRZNmc0uz8`xP;WauEX7U<_C<;#5ouJ!_)%pmL ztRx!rs|{~dN2};HxQfV^Z8M^xY5=j~CD#WFMNUO$U)QyeWK}}ldmIt!>?M2x_=ep! zcKmFoic=|+D6|V?@2L5_{KQK{oBsNj8)@VI9M11x+eEbd|E zxa4fh^A!D!f^2ekAK+0IhT$l}*<^G(iLz02JH7vuEhCt_|A!WH>yrn$m^Wn29+`e_ z9;8!n{Ujb>0KAI4#oX&xS;?qopa~=A zj}X-(B_)6RZVX$(m=CRBOuiE0#iW0Qg}%{c)67PMYW~VHR>JbnW9XCLc58U-m6^m& z<*A0HZ3`$UX9d}oIuqqDp6gAD64y^A@i-0u*iQZsyDyBdHyTupT^P5fo8^Z)rYLj) z=yF4|Clw_W3BOLoO*eyy$==4@5k7X~Bnj|zV$^@?gE2PTGuStUUr|EdpYT z2(TL@@%03_dP{)V5dwFbOr!8>5mAeP*dnlhhJcDL)B&?r1;h>kCACLzIW=xmA$F

1)nLoeA2k{ih1bF5})+LbxJzB;5eKPmO1UKo@4sdHqCzklid$E1mp2| zJd+s^NC9P&Vh|#KWJBCi5x2>BS@Ayh!nG9PBFQcuq9s92U^OU?5)=|t1h&Hv&49NX zbMRgkOzy^t#coou8PKFMgYVh0UXG5?}|J5?~T)ECEUcGNf7f zn%974=Sy-YF%cY=+WL}XK=R~Eihx9|eN7qAh~%gO5;2PoNJMe00}@dXnt(*yvQubF z{*GEFF;{1Z-FO0C7MPs-atSD{*qr`i$hQiJ22ulQ98tqOCm=166w;G$5h#CizB6*- zuJ$b>(uitAO^=vuM6E*sf_zsOb?m?xr76>r?^bbJ~j1R$q}M zh%}-aQPU%C+)dAo6ABog(?OKB(UiskX;jsSnjUfEZZ2+|P{8<{?x9rljPt^WNF%Bd zH9g|S-Spfzp@4DOIZWp`z5ZVJ$N%2LSBl8k#Ar^ur@{0UbLs%SIeaN1d?`l!rT8;s z>eBRQ2t5sB*HhZg$s`tEn+RW<5x=G9m+wDouZM4-l&-=O-lddZg~!uSVg{z+iTQXM zN)t9sGJfm*1G9D#TMY`1YrDT%0RR9S1C!ey8h@0M%TB{E5JmTu`VW@bz=FE408}NZ zAQcZGplZtpm}J@*#p7U)5&e5i6J7}gvGm;QGozUsjbC)hp20e0Fy;q?A!mS@$rY|* ze!n>1KjF-g#9WpJA?9mve4O-qk#oetUvx1qh^mngE-OGwH!u}oP?pAON#SW%!c`Vz z?tcmZ(xo^Y4v&PE3OUP+M+$w9IrFHVJft6mByuW=D4CNa?;=4-B;G1@^+sZ0yk)6D z=XFJ4d^VE=ZQq&CS3rN-PBZedY;2IT2H&PlONqE$Zd2;!{od~7^<-}w>sM1+DnM#~ zU4YaboHdV-5xbSTvxI4*r6%uaJ3qlk?N#CoDr1RV0D_g9WlzGgJGS>mGjQQJ8L+z$ zt?cWGHk>wn?jM}Wm9*=PQWYU;q_U}HOM`Tk$2{bN)>X(KnsD(ZuUveKM7=8}*e9+Y z@ZUTU!kxU6paT`NQz1GE1^UK>vrV&-D>4Cp(WFgRBuQZ{J*_ZU$Hs1=U)#`VMD5c4 z`oy`EXYo$(&!BJ=TmL*&c90$P7z= za7-}Aa0?9eKtxxh-~&>hOw|Dd$54R;H(hndY{C0* zXrFC46q}}rnn|bO{bT&Td3x!d8kVY^6$eygf>UmQx!M#b3ns&(jdpkyvu@+(yemOG zUWyeMI0nlDQyKo9dRiynBS8dShxYP2x8!ZITy58=NJyN9WEPTi8_#L{Koj!OUjE~J ztJiPHtHQs;Wa@F@glx%_CMiv)BjOEEblyL|MDYcap#v1N4=!5+1V}2pXOnI*fdxz1 zlKD@Q^f6ljiIY$=KmkpYkuo40j%&NWS^)q68v_6U6aWAK0000000000006}wliwc} zlO-|`lk+kf0#PB8VId`xG&3pzQGt^D`p`?l1rV008`Plp_ED delta 3016 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