From 0f42d6ac3d2140421fa85c6080a968a0a793264f Mon Sep 17 00:00:00 2001 From: Yangk Date: Wed, 31 Dec 2025 17:36:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(erp/TimesheetReport):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=B7=A5=E6=97=B6=E4=BA=BA=E5=91=98=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E6=8A=A5=E8=A1=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ErpTimesheetReportController.java | 158 +++++++++++++++++- .../domain/vo/ProjectPersonnelReportVo.java | 60 +++++++ .../erp/mapper/ErpTimesheetReportMapper.java | 40 +++-- .../erp/service/IErpTimesheetInfoService.java | 6 + .../service/IErpTimesheetReportService.java | 24 +-- .../impl/ErpTimesheetInfoServiceImpl.java | 45 ++++- .../impl/ErpTimesheetReportServiceImpl.java | 127 +++++++++++++- .../oa/erp/ErpTimesheetReportMapper.xml | 99 ++++++++++- 8 files changed, 528 insertions(+), 31 deletions(-) create mode 100644 ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/ProjectPersonnelReportVo.java diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/controller/ErpTimesheetReportController.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/controller/ErpTimesheetReportController.java index 989a7847..1418e9a8 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/controller/ErpTimesheetReportController.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/controller/ErpTimesheetReportController.java @@ -3,6 +3,7 @@ package org.dromara.oa.erp.controller; import cn.dev33.satoken.annotation.SaCheckPermission; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.dromara.common.core.domain.R; import org.dromara.common.log.annotation.Log; import org.dromara.common.log.enums.BusinessType; import org.dromara.common.mybatis.core.page.TableDataInfo; @@ -22,6 +23,10 @@ import cn.idev.excel.metadata.data.WriteCellData; import cn.idev.excel.metadata.Head; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.FillPatternType; import java.net.URLEncoder; import java.util.List; import java.util.Map; @@ -31,6 +36,9 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; +import org.dromara.oa.erp.domain.vo.ProjectPersonnelReportVo; +import org.springframework.web.bind.annotation.RequestParam; + /** * 工时报表 * @@ -55,10 +63,23 @@ public class ErpTimesheetReportController { return reportService.queryProjectManHourList(bo, startTime, endTime); } + /** + * 获取项目工时人员统计报表 + */ + @SaCheckPermission("oa:erp:timesheetReport:projectPersonnel:list") + @GetMapping("/projectPersonnelReport") + public R getProjectPersonnelReport(@RequestParam(required = true) Long deptId, + String startTime, String endTime) { + if (deptId == null) { + return R.fail("部门ID不能为空"); + } + return R.ok(reportService.queryProjectPersonnelReport(deptId, startTime, endTime)); + } + /** * 导出项目工时统计列表 */ - @SaCheckPermission("oa:erp:timesheetReport:projectManHour:export") + @SaCheckPermission("oa/erp:timesheetInfo:export") @Log(title = "项目工时统计报表", businessType = BusinessType.EXPORT) @PostMapping("/exportProjectManHour") public void exportProjectManHour(ProjectManHourReportVo bo, String startTime, String endTime, @@ -113,6 +134,7 @@ public class ErpTimesheetReportController { .head(heads) .sheet("项目工时统计报表") .registerWriteHandler(new ProjectCellMergeStrategy(list)) + .registerWriteHandler(new ProjectRowHighlightStrategy(list)) .doWrite(list); } catch (Exception e) { e.printStackTrace(); @@ -120,6 +142,106 @@ public class ErpTimesheetReportController { } } + /** + * 导出项目人员工时统计报表 + */ + @SaCheckPermission("oa/erp:timesheetInfo:export") + @Log(title = "项目人员工时统计报表", businessType = BusinessType.EXPORT) + @PostMapping("/exportProjectPersonnel") + public void exportProjectPersonnel(Long deptId, String startTime, String endTime, HttpServletResponse response) { + try { + ProjectPersonnelReportVo result = reportService.queryProjectPersonnelReport(deptId, startTime, endTime); + List> rows = result.getRows(); + List columns = result.getColumns(); + + // 1. 构建表头 + List> heads = new ArrayList<>(); + heads.add(Collections.singletonList("部门")); + heads.add(Collections.singletonList("项目名称")); + heads.add(Collections.singletonList("项目编号")); + heads.add(Collections.singletonList("汇总")); + + // 动态人员列 + List userKeys = new ArrayList<>(); + if (columns != null) { + for (ProjectPersonnelReportVo.ReportColumn col : columns) { + heads.add(Collections.singletonList(col.getLabel())); + userKeys.add(col.getProp()); + } + } + + heads.add(Collections.singletonList("备注")); + + // 2. 构建数据行 + List> dataList = new ArrayList<>(); + if (rows != null) { + for (Map row : rows) { + List dataRow = new ArrayList<>(); + dataRow.add(row.get("deptName")); + dataRow.add(row.get("projectName")); + dataRow.add(row.get("projectCode")); + dataRow.add(row.get("rowTotal")); // 对应“汇总”列 + + // 人员列数据 + for (String key : userKeys) { + Object hours = row.get(key); + dataRow.add(hours != null ? hours : ""); + } + + dataRow.add(row.get("remark")); + dataList.add(dataRow); + } + } + + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + String fileName = URLEncoder.encode("项目人员工时统计报表", "UTF-8").replaceAll("\\+", "%20"); + response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); + + // 3. 导出 (注册自定义样式策略) + FastExcel.write(response.getOutputStream()) + .head(heads) + .sheet("项目人员工时统计") + .registerWriteHandler(new MapRowHighlightStrategy(rows)) + .doWrite(dataList); + + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("导出失败"); + } + } + + /** + * Map数据的行高亮策略 + */ + public static class MapRowHighlightStrategy implements CellWriteHandler { + private final List> dataList; + + public MapRowHighlightStrategy(List> dataList) { + this.dataList = dataList; + } + + @Override + public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, + List> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { + if (isHead || relativeRowIndex == null) + return; + + if (dataList != null && relativeRowIndex < dataList.size()) { + Map rowData = dataList.get(relativeRowIndex); + Boolean isCrossDept = (Boolean) rowData.get("isCrossDept"); + if (Boolean.TRUE.equals(isCrossDept)) { + Workbook workbook = writeSheetHolder.getSheet().getWorkbook(); + CellStyle cellStyle = workbook.createCellStyle(); + cellStyle.cloneStyleFrom(cell.getCellStyle()); + cellStyle.setFillForegroundColor(IndexedColors.LIGHT_YELLOW.getIndex()); + cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + cell.setCellStyle(cellStyle); + } + } + } + } + /** * 自定义单元格合并策略(按项目ID合并跨部门工时列) */ @@ -176,4 +298,38 @@ public class ErpTimesheetReportController { } } } + + /** + * 自定义行高亮策略 + */ + public static class ProjectRowHighlightStrategy implements CellWriteHandler { + private final List list; + // 黄色背景样式 + private final static short YELLOW_COLOR_INDEX = org.apache.poi.ss.usermodel.IndexedColors.LIGHT_YELLOW + .getIndex(); + + public ProjectRowHighlightStrategy(List list) { + this.list = list; + } + + @Override + public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, + List> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { + if (isHead || relativeRowIndex == null || relativeRowIndex >= list.size()) { + return; + } + + ProjectManHourReportVo vo = list.get(relativeRowIndex); + // 如果 crossDeptHours 不为空(说明有跨部门工时),则整行标黄 + // 注意:前面的逻辑保证了只有不相等时 crossDeptHours 才不为空 + if (vo.getCrossDeptHours() != null) { + org.apache.poi.ss.usermodel.Workbook workbook = writeSheetHolder.getSheet().getWorkbook(); + org.apache.poi.ss.usermodel.CellStyle cellStyle = workbook.createCellStyle(); + cellStyle.cloneStyleFrom(cell.getCellStyle()); + cellStyle.setFillForegroundColor(YELLOW_COLOR_INDEX); + cellStyle.setFillPattern(org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND); + cell.setCellStyle(cellStyle); + } + } + } } diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/ProjectPersonnelReportVo.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/ProjectPersonnelReportVo.java new file mode 100644 index 00000000..438e6e61 --- /dev/null +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/ProjectPersonnelReportVo.java @@ -0,0 +1,60 @@ +package org.dromara.oa.erp.domain.vo; + +import lombok.Data; +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * 项目工时人员统计报表 VO + * + * @author Yangk + * @date 2025-12-30 + */ +@Data +public class ProjectPersonnelReportVo implements Serializable { + + /** + * 动态列头信息 + */ + private List columns; + + /** + * 数据行列表 (Map key对应 column.prop) + */ + private List> rows; + + /** + * 底部统计信息 + */ + private ReportFooter footer; + + @Data + public static class ReportColumn implements Serializable { + private String label; + private String prop; + // userId (可选,用于后续可能的操作) + private Long userId; + + public ReportColumn(String label, String prop) { + this.label = label; + this.prop = prop; + } + + public ReportColumn(String label, String prop, Long userId) { + this.label = label; + this.prop = prop; + this.userId = userId; + } + } + + @Data + public static class ReportFooter implements Serializable { + // 当月天数 + private Integer totalDays; + // 部门人数 + private Integer deptHeadCount; + // 部门总人天 + private Integer totalManDays; + } +} diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/mapper/ErpTimesheetReportMapper.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/mapper/ErpTimesheetReportMapper.java index 4bad76b3..c3dd25aa 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/mapper/ErpTimesheetReportMapper.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/mapper/ErpTimesheetReportMapper.java @@ -5,6 +5,7 @@ import org.apache.ibatis.annotations.Param; import org.dromara.oa.erp.domain.vo.ProjectManHourReportVo; import java.util.List; +import java.util.Map; /** * 项目工时统计报表 Mapper @@ -15,15 +16,32 @@ import java.util.List; @Mapper public interface ErpTimesheetReportMapper { - /** - * 查询项目工时统计列表 - * - * @param bo 查询条件 - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return 列表 - */ - List selectProjectManHourList(@Param("bo") ProjectManHourReportVo bo, - @Param("startTime") String startTime, - @Param("endTime") String endTime); + /** + * 查询项目工时统计列表 + * + * @param bo 查询条件 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 列表 + */ + List selectProjectManHourList(@Param("bo") ProjectManHourReportVo bo, + @Param("startTime") String startTime, + @Param("endTime") String endTime); + + /** + * 查询项目-人员工时分布 + */ + List> selectProjectPersonnelHours(@Param("deptId") Long deptId, + @Param("startTime") String startTime, @Param("endTime") String endTime); + + /** + * 查询项目在全公司的总工时 + */ + List> selectProjectTotalHours(@Param("deptId") Long deptId, + @Param("startTime") String startTime, @Param("endTime") String endTime); + + /** + * 查询部门的所有人员 + */ + List> selectDeptUsers(@Param("deptId") Long deptId); } diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/IErpTimesheetInfoService.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/IErpTimesheetInfoService.java index 1afc580f..4f9983fa 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/IErpTimesheetInfoService.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/IErpTimesheetInfoService.java @@ -72,8 +72,14 @@ public interface IErpTimesheetInfoService { /** * 提交项目工时并启动流程 + * * @param bo * @return */ ErpTimesheetInfoVo submitAndFlowStart(ErpTimesheetInfoBo bo); + + /** + * 实际处理提交流程 + */ + ErpTimesheetInfoVo processSubmitAndFlowStart(ErpTimesheetInfoBo bo); } diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/IErpTimesheetReportService.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/IErpTimesheetReportService.java index 17ed1421..52bd1af6 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/IErpTimesheetReportService.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/IErpTimesheetReportService.java @@ -5,6 +5,8 @@ import org.dromara.oa.erp.domain.vo.ProjectManHourReportVo; import java.util.List; +import org.dromara.oa.erp.domain.vo.ProjectPersonnelReportVo; + /** * 工时报表 Service 接口 * @@ -14,23 +16,23 @@ import java.util.List; public interface IErpTimesheetReportService { /** - * 查询项目工时统计列表 - * - * @param bo 查询条件 - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return 列表 + * 查询项目工时统计列表(分页) */ TableDataInfo queryProjectManHourList(ProjectManHourReportVo bo, String startTime, String endTime); /** - * 查询所有项目工时统计 (导出用) - * - * @param bo 查询条件 - * @param startTime 开始时间 - * @param endTime 结束时间 * @return 列表 */ List queryProjectManHourAll(ProjectManHourReportVo bo, String startTime, String endTime); + + /** + * 查询项目工时人员统计报表 + * + * @param deptId 部门ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 报表数据 + */ + ProjectPersonnelReportVo queryProjectPersonnelReport(Long deptId, String startTime, String endTime); } diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpTimesheetInfoServiceImpl.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpTimesheetInfoServiceImpl.java index 4498d4ed..5b1b3dab 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpTimesheetInfoServiceImpl.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpTimesheetInfoServiceImpl.java @@ -29,6 +29,7 @@ import org.dromara.oa.erp.service.IErpTimesheetProjectService; import org.dromara.system.api.RemoteCodeRuleService; import org.dromara.workflow.api.RemoteWorkflowService; import org.dromara.workflow.api.domain.RemoteStartProcess; +import org.dromara.workflow.api.domain.RemoteStartProcessReturn; import org.dromara.workflow.api.event.ProcessEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; @@ -64,6 +65,10 @@ public class ErpTimesheetInfoServiceImpl implements IErpTimesheetInfoService { private final IErpTimesheetDeptService timesheetDeptService; private final IErpTimesheetProjectService timesheetProjectService; + @jakarta.annotation.Resource + @org.springframework.context.annotation.Lazy + private IErpTimesheetInfoService self; + /** * 查询工时填报 * @@ -254,9 +259,23 @@ public class ErpTimesheetInfoServiceImpl implements IErpTimesheetInfoService { * @param bo * @return */ + /** + * 提交项目售后信息并提交流程 + */ + @Override + public ErpTimesheetInfoVo submitAndFlowStart(ErpTimesheetInfoBo bo) { + // 1. 如果是新增且没有编号,先生成编号 + if (bo.getTimesheetId() == null && StringUtils.isBlank(bo.getTimesheetCode())) { + String timesheetCode = remoteCodeRuleService.selectCodeRuleCode("1015"); + bo.setTimesheetCode(timesheetCode); + } + // 2. 调用实际处理方法 (开启全局事务) + return self.processSubmitAndFlowStart(bo); + } + @Override @GlobalTransactional(rollbackFor = Exception.class) // 开启全局事务 - public ErpTimesheetInfoVo submitAndFlowStart(ErpTimesheetInfoBo bo) { + public ErpTimesheetInfoVo processSubmitAndFlowStart(ErpTimesheetInfoBo bo) { if (bo.getTimesheetId() == null) { this.insertByBo(bo); } else { @@ -309,9 +328,27 @@ public class ErpTimesheetInfoServiceImpl implements IErpTimesheetInfoService { bo.getBizExt().setBusinessId(startProcess.getBusinessId()); } - boolean flag = remoteWorkflowService.startCompleteTask(startProcess); - if (!flag) { - throw new ServiceException("流程发起异常"); + RemoteStartProcessReturn result = remoteWorkflowService.startWorkFlow(startProcess); + if (result == null) { + throw new ServiceException("流程发起失败"); + } + + // 如果返回了 taskId,说明流程停留在第一个节点,需要我们手动完成它 (对应 "startCompleteTask" 的逻辑) + if (result.getTaskId() != null) { + org.dromara.workflow.api.domain.RemoteCompleteTask completeTask = new org.dromara.workflow.api.domain.RemoteCompleteTask(); + completeTask.setTaskId(result.getTaskId()); + // 审批意见,提交时默认为 "提交申请" + completeTask.setMessage("提交申请"); + completeTask.setVariables(variables); + boolean completeFlag = remoteWorkflowService.completeTask(completeTask); + if (!completeFlag) { + // 这里可能需要警告,但既然流程已启动,可能不一定非要抛异常,视业务而定 + log.warn("流程启动成功,但自动完成第一个任务失败: taskId={}", result.getTaskId()); + } + } else { + // taskId 为空,说明流程启动后直接跳过/已结束/进入下一个节点(可能因为自动完成) + // 这种情况下,我们不需要做任何事,直接视为成功 + log.info("流程启动成功,首节点已自动完成或跳过: instanceId={}", result.getProcessInstanceId()); } return MapstructUtils.convert(baseMapper.selectById(bo.getTimesheetId()), ErpTimesheetInfoVo.class); diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpTimesheetReportServiceImpl.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpTimesheetReportServiceImpl.java index a0b372d6..9f75c79e 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpTimesheetReportServiceImpl.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpTimesheetReportServiceImpl.java @@ -5,12 +5,18 @@ import com.github.yulichang.wrapper.MPJLambdaWrapper; import lombok.RequiredArgsConstructor; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.oa.erp.domain.vo.ProjectManHourReportVo; +import org.dromara.oa.erp.domain.vo.ProjectPersonnelReportVo; import org.dromara.oa.erp.mapper.ErpTimesheetReportMapper; import org.dromara.oa.erp.service.IErpTimesheetReportService; import org.springframework.stereotype.Service; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import java.util.List; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.stream.Collectors; /** * 工时报表 Service 实现 @@ -36,4 +42,123 @@ public class ErpTimesheetReportServiceImpl implements IErpTimesheetReportService String endTime) { return reportMapper.selectProjectManHourList(bo, startTime, endTime); } + + @Override + public ProjectPersonnelReportVo queryProjectPersonnelReport(Long deptId, String startTime, String endTime) { + ProjectPersonnelReportVo result = new ProjectPersonnelReportVo(); + + // 1. 查询该部门所有人员 (作为所有的列) + List> deptUsers = reportMapper.selectDeptUsers(deptId); + List columns = new ArrayList<>(); + Set userIds = new HashSet<>(); + + // 记录部门名称 (随便取一个用户的,或者如果没人则为空) + String deptName = ""; + + if (deptUsers != null) { + for (Map u : deptUsers) { + Long uid = (Long) u.get("userId"); + String nickName = (String) u.get("nickName"); + if (deptName.isEmpty() && u.get("deptName") != null) { + deptName = (String) u.get("deptName"); + } + + if (uid != null && !userIds.contains(uid)) { + userIds.add(uid); + columns.add(new ProjectPersonnelReportVo.ReportColumn(nickName, "user_" + uid, uid)); + } + } + } + + // 按名字排序 (或者保持数据库查询顺序) + // columns.sort(Comparator.comparing(ProjectPersonnelReportVo.ReportColumn::getLabel)); + result.setColumns(columns); + + // 2. 查询工时明细数据 (projectId, userId, hours) + List> rawData = reportMapper.selectProjectPersonnelHours(deptId, startTime, endTime); + + // 3. 查询跨部门总工时 (projectId, totalHours) + List> totalHoursData = reportMapper.selectProjectTotalHours(deptId, startTime, endTime); + Map projectTotalHoursMap = new HashMap<>(); + if (totalHoursData != null) { + for (Map map : totalHoursData) { + Long pid = (Long) map.get("projectId"); + BigDecimal total = (BigDecimal) map.get("totalHours"); + projectTotalHoursMap.put(pid, total); + } + } + + // 4. 构建行 (项目) + Map> rowsMap = new LinkedHashMap<>(); + + if (rawData != null) { + for (Map map : rawData) { + Long pid = (Long) map.get("projectId"); + Long uid = (Long) map.get("userId"); + BigDecimal hours = (BigDecimal) map.get("hours"); + + Map row = rowsMap.computeIfAbsent(pid, k -> { + Map r = new HashMap<>(); + r.put("projectId", pid); + r.put("projectDeptId", map.get("projectDeptId")); + r.put("projectName", map.get("projectName")); + r.put("projectCode", map.get("projectCode")); + r.put("rowTotal", BigDecimal.ZERO); + return r; + }); + + // 设置人员工时 + row.put("user_" + uid, hours); + + // 累加行汇总 + BigDecimal currentTotal = (BigDecimal) row.get("rowTotal"); + row.put("rowTotal", currentTotal.add(hours)); + } + } + + List> rows = new ArrayList<>(rowsMap.values()); + + // 5. 跨部门逻辑 & 底部统计 + int days = calculateDays(startTime, endTime); + int headCount = userIds.size(); + + for (Map row : rows) { + // 补充部门名称 + row.put("deptName", deptName); + + Long projectDeptId = (Long) row.get("projectDeptId"); + + // 如果项目所属部门 != 当前报表统计部门,则是跨部门项目 + if (projectDeptId != null && !projectDeptId.equals(deptId)) { + row.put("isCrossDept", true); + row.put("remark", "跨部门"); // 备注列 + } else { + row.put("isCrossDept", false); + row.put("remark", ""); + } + } + + result.setRows(rows); + + // 6. 底部 + ProjectPersonnelReportVo.ReportFooter footer = new ProjectPersonnelReportVo.ReportFooter(); + footer.setTotalDays(days); + footer.setDeptHeadCount(headCount); + footer.setTotalManDays(days * headCount); + result.setFooter(footer); + + return result; + } + + private int calculateDays(String start, String end) { + if (start == null || end == null) + return 0; + try { + LocalDate d1 = LocalDate.parse(start, DateTimeFormatter.ISO_DATE); + LocalDate d2 = LocalDate.parse(end, DateTimeFormatter.ISO_DATE); + return (int) ChronoUnit.DAYS.between(d1, d2) + 1; + } catch (Exception e) { + return 0; + } + } } diff --git a/ruoyi-modules/ruoyi-oa/src/main/resources/mapper/oa/erp/ErpTimesheetReportMapper.xml b/ruoyi-modules/ruoyi-oa/src/main/resources/mapper/oa/erp/ErpTimesheetReportMapper.xml index c8c6579b..6d855779 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/resources/mapper/oa/erp/ErpTimesheetReportMapper.xml +++ b/ruoyi-modules/ruoyi-oa/src/main/resources/mapper/oa/erp/ErpTimesheetReportMapper.xml @@ -13,8 +13,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" p.project_code, p.project_category, COALESCE(SUM(tp.hours), 0) AS total_hours, - - SUM(SUM(tp.hours)) OVER(PARTITION BY p.project_id) AS cross_dept_hours + + COALESCE(global_stats.total_hours, 0) AS cross_dept_hours FROM erp_project_info p LEFT JOIN sys_user u ON p.manager_id = u.user_id @@ -26,6 +26,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" LEFT JOIN sys_dept t_dept ON ti.dept_id = t_dept.dept_id LEFT JOIN sys_dept p_dept ON p.dept_id = p_dept.dept_id + + LEFT JOIN ( + SELECT + tp2.project_id, + SUM(tp2.hours) AS total_hours + FROM erp_timesheet_project tp2 + JOIN erp_timesheet_info ti2 ON tp2.timesheet_id = ti2.timesheet_id AND ti2.del_flag = '0' + WHERE tp2.del_flag = '0' + AND ti2.timesheet_status = '3' + + AND ti2.start_time >= #{startTime} + + + AND ti2.end_time <= #{endTime} + + GROUP BY tp2.project_id + ) global_stats ON p.project_id = global_stats.project_id p.del_flag = '0' @@ -52,9 +69,85 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND ti.timesheet_status = '3' - GROUP BY p.project_id, ti.dept_id + GROUP BY p.project_id, ti.dept_id, global_stats.total_hours HAVING SUM(tp.hours) > 0 ORDER BY p.project_code, t_dept.dept_id + + + + + + + + +